| // Copyright 2021 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "content/browser/interest_group/auction_runner.h" |
| |
| #include <stdint.h> |
| |
| #include <limits> |
| #include <map> |
| #include <memory> |
| #include <string> |
| #include <vector> |
| |
| #include "base/check.h" |
| #include "base/containers/contains.h" |
| #include "base/feature_list.h" |
| #include "base/functional/callback_helpers.h" |
| #include "base/json/json_writer.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/memory/scoped_refptr.h" |
| #include "base/notreached.h" |
| #include "base/run_loop.h" |
| #include "base/strings/strcat.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/test/bind.h" |
| #include "base/test/metrics/histogram_tester.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/test/task_environment.h" |
| #include "base/time/time.h" |
| #include "build/build_config.h" |
| #include "content/browser/fenced_frame/fenced_frame_reporter.h" |
| #include "content/browser/interest_group/auction_process_manager.h" |
| #include "content/browser/interest_group/auction_worklet_manager.h" |
| #include "content/browser/interest_group/debuggable_auction_worklet.h" |
| #include "content/browser/interest_group/debuggable_auction_worklet_tracker.h" |
| #include "content/browser/interest_group/interest_group_auction.h" |
| #include "content/browser/interest_group/interest_group_k_anonymity_manager.h" |
| #include "content/browser/interest_group/interest_group_manager_impl.h" |
| #include "content/browser/interest_group/interest_group_storage.h" |
| #include "content/browser/interest_group/mock_auction_process_manager.h" |
| #include "content/browser/interest_group/test_interest_group_manager_impl.h" |
| #include "content/browser/interest_group/test_interest_group_private_aggregation_manager.h" |
| #include "content/browser/renderer_host/render_frame_host_impl.h" |
| #include "content/public/browser/site_instance.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/test/test_renderer_host.h" |
| #include "content/services/auction_worklet/auction_v8_helper.h" |
| #include "content/services/auction_worklet/auction_worklet_service_impl.h" |
| #include "content/services/auction_worklet/public/mojom/auction_shared_storage_host.mojom.h" |
| #include "content/services/auction_worklet/public/mojom/auction_worklet_service.mojom.h" |
| #include "content/services/auction_worklet/public/mojom/bidder_worklet.mojom.h" |
| #include "content/services/auction_worklet/public/mojom/private_aggregation_request.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 "mojo/public/cpp/bindings/associated_remote.h" |
| #include "mojo/public/cpp/bindings/pending_associated_remote.h" |
| #include "mojo/public/cpp/system/functions.h" |
| #include "net/http/http_status_code.h" |
| #include "net/traffic_annotation/network_traffic_annotation_test_helper.h" |
| #include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h" |
| #include "services/network/public/mojom/client_security_state.mojom.h" |
| #include "services/network/test/test_url_loader_factory.h" |
| #include "testing/gmock/include/gmock/gmock-matchers.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/abseil-cpp/absl/types/optional.h" |
| #include "third_party/blink/public/common/features.h" |
| #include "third_party/blink/public/common/interest_group/ad_auction_constants.h" |
| #include "third_party/blink/public/common/interest_group/interest_group.h" |
| #include "third_party/blink/public/common/interest_group/test_interest_group_builder.h" |
| #include "third_party/blink/public/mojom/fenced_frame/fenced_frame.mojom-shared.h" |
| #include "third_party/blink/public/mojom/private_aggregation/aggregatable_report.mojom-shared.h" |
| #include "third_party/blink/public/mojom/private_aggregation/private_aggregation_host.mojom.h" |
| |
| using auction_worklet::TestDevToolsAgentClient; |
| using testing::HasSubstr; |
| |
| namespace content { |
| namespace { |
| |
| using InterestGroupKey = blink::InterestGroupKey; |
| using PostAuctionSignals = InterestGroupAuction::PostAuctionSignals; |
| using blink::FencedFrame::ReportingDestination; |
| using PrivateAggregationRequests = AuctionRunner::PrivateAggregationRequests; |
| |
| const std::string kBidder1Name{"Ad Platform"}; |
| const std::string kBidder1NameAlt{"Ad Platform Alt"}; |
| const char kBidder1DebugLossReportUrl[] = |
| "https://bidder1-debug-loss-reporting.com/"; |
| const char kBidder1DebugWinReportUrl[] = |
| "https://bidder1-debug-win-reporting.com/"; |
| const char kBidder2DebugLossReportUrl[] = |
| "https://bidder2-debug-loss-reporting.com/"; |
| const char kBidder2DebugWinReportUrl[] = |
| "https://bidder2-debug-win-reporting.com/"; |
| |
| const char kBidderDebugLossReportBaseUrl[] = |
| "https://bidder-debug-loss-reporting.com/"; |
| const char kBidderDebugWinReportBaseUrl[] = |
| "https://bidder-debug-win-reporting.com/"; |
| const char kSellerDebugLossReportBaseUrl[] = |
| "https://seller-debug-loss-reporting.com/"; |
| const char kSellerDebugWinReportBaseUrl[] = |
| "https://seller-debug-win-reporting.com/"; |
| |
| // Trusted bidding signals typically used for bidder1 and bidder2. |
| const char kBidder1SignalsJson[] = |
| R"({"keys": {"k1":"a", "k2": "b", "extra": "c"}})"; |
| const char kBidder2SignalsJson[] = |
| R"({"keys": {"l1":"a", "l2": "b", "extra": "c"}})"; |
| |
| const char kPostAuctionSignalsPlaceholder[] = |
| "?winningBid=${winningBid}&madeWinningBid=${madeWinningBid}&" |
| "highestScoringOtherBid=${highestScoringOtherBid}&" |
| "madeHighestScoringOtherBid=${madeHighestScoringOtherBid}"; |
| |
| const char kTopLevelPostAuctionSignalsPlaceholder[] = |
| "topLevelWinningBid=${topLevelWinningBid}&" |
| "topLevelMadeWinningBid=${topLevelMadeWinningBid}"; |
| |
| const auction_worklet::mojom::PrivateAggregationRequestPtr |
| kExpectedGenerateBidPrivateAggregationRequest = |
| auction_worklet::mojom::PrivateAggregationRequest::New( |
| auction_worklet::mojom::AggregatableReportContribution:: |
| NewHistogramContribution( |
| blink::mojom::AggregatableReportHistogramContribution::New( |
| /*bucket=*/1, |
| /*value=*/2)), |
| blink::mojom::AggregationServiceMode::kDefault, |
| blink::mojom::DebugModeDetails::New()); |
| |
| const auction_worklet::mojom::PrivateAggregationRequestPtr |
| kExpectedReportWinPrivateAggregationRequest = |
| auction_worklet::mojom::PrivateAggregationRequest::New( |
| auction_worklet::mojom::AggregatableReportContribution:: |
| NewHistogramContribution( |
| blink::mojom::AggregatableReportHistogramContribution::New( |
| /*bucket=*/3, |
| /*value=*/4)), |
| blink::mojom::AggregationServiceMode::kDefault, |
| blink::mojom::DebugModeDetails::New()); |
| |
| const auction_worklet::mojom::PrivateAggregationRequestPtr |
| kExpectedScoreAdPrivateAggregationRequest = |
| auction_worklet::mojom::PrivateAggregationRequest::New( |
| auction_worklet::mojom::AggregatableReportContribution:: |
| NewHistogramContribution( |
| blink::mojom::AggregatableReportHistogramContribution::New( |
| /*bucket=*/5, |
| /*value=*/6)), |
| blink::mojom::AggregationServiceMode::kDefault, |
| blink::mojom::DebugModeDetails::New()); |
| |
| const auction_worklet::mojom::PrivateAggregationRequestPtr |
| kExpectedReportResultPrivateAggregationRequest = |
| auction_worklet::mojom::PrivateAggregationRequest::New( |
| auction_worklet::mojom::AggregatableReportContribution:: |
| NewHistogramContribution( |
| blink::mojom::AggregatableReportHistogramContribution::New( |
| /*bucket=*/7, |
| /*value=*/8)), |
| blink::mojom::AggregationServiceMode::kDefault, |
| blink::mojom::DebugModeDetails::New()); |
| |
| // Helper to avoid excess boilerplate. |
| template <typename... Ts> |
| auto ElementsAreRequests(Ts&... requests) { |
| static_assert( |
| std::conjunction<std::is_same< |
| std::remove_const_t<Ts>, |
| auction_worklet::mojom::PrivateAggregationRequestPtr>...>::value); |
| // Need to use `std::ref` as `mojo::StructPtr`s are move-only. |
| return testing::UnorderedElementsAre(testing::Eq(std::ref(requests))...); |
| } |
| |
| // 0 `num_component_urls` means no component URLs, as opposed to an empty list |
| // (which isn't tested at this layer). |
| std::string MakeBidScript(const url::Origin& seller, |
| const std::string& bid, |
| const std::string& render_url, |
| int num_ad_components, |
| const url::Origin& interest_group_owner, |
| const std::string& interest_group_name, |
| bool has_signals = false, |
| const std::string& signal_key = "", |
| const std::string& signal_val = "", |
| bool report_post_auction_signals = false, |
| const std::string& debug_loss_report_url = "", |
| const std::string& debug_win_report_url = "", |
| bool report_reject_reason = false) { |
| // TODO(morlovich): Use JsReplace. |
| constexpr char kBidScript[] = R"( |
| const seller = "%s"; |
| const bid = %s; |
| const renderUrl = "%s"; |
| const numAdComponents = %i; |
| const interestGroupOwner = "%s"; |
| const interestGroupName = "%s"; |
| const hasSignals = %s; |
| const reportPostAuctionSignals = %s; |
| const reportRejectReason = %s; |
| const postAuctionSignalsPlaceholder = "%s"; |
| let debugLossReportUrl = "%s"; |
| let debugWinReportUrl = "%s"; |
| const signalsKey = "%s"; |
| const signalsValue = "%s"; |
| const topLevelSeller = "https://adstuff.publisher1.com"; |
| |
| function generateBid(interestGroup, auctionSignals, perBuyerSignals, |
| trustedBiddingSignals, browserSignals) { |
| let result = {ad: {"bidKey": "data for " + bid, |
| "groupName": interestGroupName, |
| "renderUrl": "data for " + renderUrl, |
| "seller": seller}, |
| bid: bid, |
| render: renderUrl, |
| // Only need to allow component auction participation when |
| // `topLevelSeller` is populated. |
| allowComponentAuction: "topLevelSeller" in browserSignals}; |
| if (interestGroup.adComponents) { |
| result.adComponents = [interestGroup.adComponents[0].renderUrl]; |
| result.ad.adComponentsUrl = interestGroup.adComponents[0].renderUrl; |
| } |
| |
| if (interestGroup.name !== interestGroupName) |
| throw new Error("wrong interestGroupName"); |
| if (interestGroup.owner !== interestGroupOwner) |
| throw new Error("wrong interestGroupOwner"); |
| // The actual priority should be hidden from the worklet. |
| if (interestGroup.priority !== undefined) |
| throw new Error("wrong priority: " + interestGroup.priority); |
| // None of these tests set a updateUrl. Non-empty values are tested by |
| // browser tests. |
| if ("updateUrl" in interestGroup) |
| throw new Error("Unexpected updateUrl"); |
| if (interestGroup.ads.length != 1) |
| throw new Error("wrong interestGroup.ads length"); |
| if (interestGroup.ads[0].renderUrl != renderUrl) |
| throw new Error("wrong interestGroup.ads URL"); |
| if (numAdComponents == 0) { |
| if (interestGroup.adComponents !== undefined) |
| throw new Error("Non-empty adComponents"); |
| } else { |
| if (interestGroup.adComponents.length !== numAdComponents) |
| throw new Error("Wrong adComponents length"); |
| for (let i = 0; i < numAdComponents; ++i) { |
| if (interestGroup.adComponents[i].renderUrl != |
| renderUrl.slice(0, -1) + "-component" + (i+1) + ".com/") { |
| throw new Error("Wrong adComponents renderUrl"); |
| } |
| } |
| } |
| // Skip the `perBuyerSignals` check if the interest group name matches |
| // the bid. This is for auctions that use more than the two standard |
| // bidders, since there's currently no way to inject new perBuyerSignals |
| // into the top-level auction. |
| // TODO(mmenke): Worth fixing that? |
| if (interestGroupName !== bid + '') { |
| if (perBuyerSignals === null) { |
| throw new Error("unexpectedly perBuyerSignals is null"); |
| } |
| if (perBuyerSignals[seller + 'Signals'] !== |
| interestGroupName + 'Signals') { |
| throw new Error("wrong perBuyerSignals"); |
| } |
| } |
| if (auctionSignals !== "auctionSignalsFor " + seller) |
| throw new Error("wrong auctionSignals"); |
| if (hasSignals) { |
| if ('extra' in trustedBiddingSignals) |
| throw new Error("why extra?"); |
| if (!interestGroup.trustedBiddingSignalsKeys.includes(signalsKey)) |
| throw new Error("Wrong interestGroup.trustedBiddingSignalsKeys"); |
| if (trustedBiddingSignals[signalsKey] !== signalsValue) |
| throw new Error("wrong signals"); |
| } else { |
| if (trustedBiddingSignals !== null) { |
| throw new Error("Expected null trustedBiddingSignals"); |
| } |
| } |
| if (browserSignals.topWindowHostname !== 'publisher1.com') |
| throw new Error("wrong topWindowHostname"); |
| if (browserSignals.seller !== seller) |
| throw new Error("wrong seller"); |
| if (browserSignals.seller === topLevelSeller) { |
| if ("topLevelSeller" in browserSignals) |
| throw new Error("expected no browserSignals.topLevelSeller"); |
| } else { |
| if (browserSignals.topLevelSeller !== topLevelSeller) |
| throw new Error("wrong browserSignals.topLevelSeller"); |
| } |
| if (browserSignals.joinCount !== 3) |
| throw new Error("joinCount") |
| if (browserSignals.bidCount !== 5) |
| throw new Error("bidCount"); |
| if (browserSignals.prevWins.length !== 3) |
| throw new Error("prevWins"); |
| for (let i = 0; i < browserSignals.prevWins.length; ++i) { |
| if (!(browserSignals.prevWins[i] instanceof Array)) |
| throw new Error("prevWins entry not an array"); |
| if (typeof browserSignals.prevWins[i][0] != "number") |
| throw new Error("Not a Number in prevWin?"); |
| if (browserSignals.prevWins[i][1].winner !== -i) |
| throw new Error("prevWin MD not what passed in"); |
| } |
| if (debugLossReportUrl) { |
| if (reportPostAuctionSignals) |
| debugLossReportUrl += postAuctionSignalsPlaceholder; |
| if (reportRejectReason) { |
| debugLossReportUrl += reportPostAuctionSignals ? '&' : '?'; |
| debugLossReportUrl += 'rejectReason=${rejectReason}'; |
| } |
| forDebuggingOnly.reportAdAuctionLoss(debugLossReportUrl); |
| } |
| if (debugWinReportUrl) { |
| if (reportPostAuctionSignals) |
| debugWinReportUrl += postAuctionSignalsPlaceholder; |
| forDebuggingOnly.reportAdAuctionWin(debugWinReportUrl); |
| } |
| if (browserSignals.dataVersion !== undefined) |
| throw new Error(`wrong dataVersion (${browserSignals.dataVersion})`); |
| privateAggregation.sendHistogramReport({bucket: 1n, value: 2}); |
| // Private aggregation requests with non-reserved event types will only be |
| // reported for a winning bidder. |
| privateAggregation.reportContributionForEvent( |
| 'click', {bucket: 10n, value: 20 + bid}); |
| return result; |
| } |
| |
| function reportWin(auctionSignals, perBuyerSignals, sellerSignals, |
| browserSignals) { |
| if (auctionSignals !== "auctionSignalsFor " + seller) |
| throw new Error("wrong auctionSignals"); |
| // Skip the `perBuyerSignals` check if the interest group name matches |
| // the bid. This is for auctions that use more than the two standard |
| // bidders, since there's currently no way to inject new perBuyerSignals |
| // into the top-level auction. |
| // TODO(mmenke): Worth fixing that? |
| if (interestGroupName !== bid + '') { |
| if (perBuyerSignals[seller + 'Signals'] !== |
| interestGroupName + 'Signals') { |
| throw new Error("wrong perBuyerSignals"); |
| } |
| } |
| |
| // sellerSignals in these tests is just sellers' browserSignals, since |
| // that's what reportResult passes through. |
| if (sellerSignals.topWindowHostname !== 'publisher1.com') |
| throw new Error("wrong topWindowHostname"); |
| if (sellerSignals.interestGroupOwner !== interestGroupOwner) |
| throw new Error("wrong interestGroupOwner"); |
| if (sellerSignals.renderUrl !== renderUrl) |
| throw new Error("wrong renderUrl"); |
| if (sellerSignals.bid !== bid) |
| throw new Error("wrong bid"); |
| // `sellerSignals` is the `browserSignals` for the seller that's |
| // associated with the bid. If it's the top-level seller, the seller's |
| // `browserSignals` should have no `componentSeller`, since the bid |
| // was made directly to the top-level seller. If it's the component |
| // seller, the seller's `browserSignals` should have a `topLevelSeller` |
| // instead of a `componentSeller`, so `componentSeller` should never |
| // be present in `sellerSignals` here. |
| if ("componentSeller" in sellerSignals) |
| throw new Error("wrong componentSeller in sellerSignals"); |
| if (browserSignals.seller === topLevelSeller) { |
| if ("topLevelSeller" in sellerSignals) |
| throw new Error("wrong topLevelSeller in sellerSignals"); |
| } else { |
| // If the seller is a component seller, then then the seller's |
| // `browserSignals` should have the top-level seller. |
| if (sellerSignals.topLevelSeller !== topLevelSeller) |
| throw new Error("wrong topLevelSeller in browserSignals"); |
| } |
| |
| if (browserSignals.topWindowHostname !== 'publisher1.com') |
| throw new Error("wrong browserSignals.topWindowHostname"); |
| if (browserSignals.seller !== seller) |
| throw new Error("wrong seller"); |
| if (browserSignals.seller === topLevelSeller) { |
| if ("topLevelSeller" in browserSignals) |
| throw new Error("expected no browserSignals.topLevelSeller"); |
| } else { |
| if (browserSignals.topLevelSeller !== topLevelSeller) |
| throw new Error("wrong browserSignals.topLevelSeller"); |
| } |
| if ("desirability" in browserSignals) |
| throw new Error("why is desirability here?"); |
| if (browserSignals.interestGroupName !== interestGroupName) |
| throw new Error("wrong browserSignals.interestGroupName"); |
| if (browserSignals.interestGroupOwner !== interestGroupOwner) |
| throw new Error("wrong browserSignals.interestGroupOwner"); |
| |
| if (browserSignals.renderUrl !== renderUrl) |
| throw new Error("wrong browserSignals.renderUrl"); |
| if (browserSignals.bid !== bid) |
| throw new Error("wrong browserSignals.bid"); |
| if (browserSignals.seller != seller) |
| throw new Error("wrong seller"); |
| if (browserSignals.dataVersion !== undefined) |
| throw new Error(`wrong dataVersion (${browserSignals.dataVersion})`); |
| let sendReportUrl = "https://buyer-reporting.example.com/"; |
| if (reportPostAuctionSignals) { |
| sendReportUrl += |
| '?highestScoringOtherBid=' + browserSignals.highestScoringOtherBid + |
| '&madeHighestScoringOtherBid=' + |
| browserSignals.madeHighestScoringOtherBid + '&bid='; |
| } |
| sendReportTo(sendReportUrl + bid); |
| registerAdBeacon({ |
| "click": "https://buyer-reporting.example.com/" + 2*bid, |
| }); |
| privateAggregation.sendHistogramReport({bucket: 3n, value: 4}); |
| privateAggregation.reportContributionForEvent( |
| 'click', {bucket: 30n, value: 40 + bid}); |
| } |
| )"; |
| return base::StringPrintf( |
| kBidScript, seller.Serialize().c_str(), bid.c_str(), render_url.c_str(), |
| num_ad_components, interest_group_owner.Serialize().c_str(), |
| interest_group_name.c_str(), has_signals ? "true" : "false", |
| report_post_auction_signals ? "true" : "false", |
| report_reject_reason ? "true" : "false", kPostAuctionSignalsPlaceholder, |
| debug_loss_report_url.c_str(), debug_win_report_url.c_str(), |
| signal_key.c_str(), signal_val.c_str()); |
| } |
| |
| // This can be appended to the standard script to override the function. |
| constexpr char kReportWinNoUrl[] = R"( |
| function reportWin(auctionSignals, perBuyerSignals, sellerSignals, |
| browserSignals) { |
| } |
| )"; |
| |
| constexpr char kSimpleReportWin[] = R"( |
| function reportWin(auctionSignals, perBuyerSignals, sellerSignals, |
| browserSignals) { |
| sendReportTo( |
| "https://buyer-reporting.example.com/" + |
| '?highestScoringOtherBid=' + browserSignals.highestScoringOtherBid + |
| '&madeHighestScoringOtherBid=' + |
| browserSignals.madeHighestScoringOtherBid + |
| '&bid=' + browserSignals.bid); |
| } |
| )"; |
| |
| // A simple bid script that returns either `bid` or nothing depending on whether |
| // all incoming ads got filtered. If the interestGroup has components, the |
| // ad URL with /1 and /2 generated will be returned as components in the bid. |
| std::string MakeFilteringBidScript(int bid) { |
| return base::StringPrintf(R"( |
| function generateBid(interestGroup, auctionSignals, perBuyerSignals, |
| trustedBiddingSignals, browserSignals) { |
| if (interestGroup.ads.length === 0) |
| return; |
| |
| let result = { |
| ad: {}, |
| bid: %d, |
| render: interestGroup.ads[0].renderUrl, |
| allowComponentAuction: true |
| }; |
| |
| if (interestGroup.adComponents) { |
| result.adComponents = [ |
| interestGroup.ads[0].renderUrl + "1", |
| interestGroup.ads[0].renderUrl + "2", |
| ]; |
| } |
| |
| return result; |
| })", |
| bid); |
| } |
| |
| // A bid script that always bids the same value + URL. |
| std::string MakeConstBidScript(int bid, const std::string& url) { |
| return base::StringPrintf(R"( |
| function generateBid(interestGroup, auctionSignals, perBuyerSignals, |
| trustedBiddingSignals, browserSignals) { |
| return {ad: {}, |
| bid: %d, |
| render: "%s", |
| allowComponentAuction: true}; |
| })", |
| bid, url.c_str()); |
| } |
| |
| // This can be appended to the standard script to override the function. |
| constexpr char kReportWinExpectNullAuctionSignals[] = R"( |
| function reportWin(auctionSignals, perBuyerSignals, sellerSignals, |
| browserSignals) { |
| if (sellerSignals === null) |
| sendReportTo("https://seller.signals.were.null.test"); |
| } |
| )"; |
| |
| constexpr char kMinimumDecisionScript[] = R"( |
| function scoreAd(adMetadata, bid, auctionConfig, trustedScoringSignals, |
| browserSignals) { |
| return {desirability: bid, |
| allowComponentAuction: true, |
| ad: adMetadata}; |
| } |
| )"; |
| |
| std::string MakeDecisionScript( |
| const GURL& decision_logic_url, |
| absl::optional<GURL> send_report_url = absl::nullopt, |
| bool bid_from_component_auction_wins = false, |
| bool report_post_auction_signals = false, |
| const std::string& debug_loss_report_url = "", |
| const std::string& debug_win_report_url = "", |
| bool report_top_level_post_auction_signals = false) { |
| constexpr char kCheckingAuctionScript[] = R"( |
| const decisionLogicUrl = "%s"; |
| let sendReportUrl = "%s"; |
| const reportPostAuctionSignals = %s; |
| const postAuctionSignalsPlaceholder = "%s"; |
| let debugLossReportUrl = "%s"; |
| let debugWinReportUrl = "%s"; |
| const topLevelSeller = "https://adstuff.publisher1.com"; |
| const bidFromComponentAuctionWins = %s; |
| const reportTopLevelPostAuctionSignals = %s; |
| const topLevelPostAuctionSignalsPlaceholder = "%s"; |
| function scoreAd(adMetadata, bid, auctionConfig, trustedScoringSignals, |
| browserSignals) { |
| if (adMetadata.bidKey !== ("data for " + bid)) { |
| throw new Error("wrong data for bid:" + |
| JSON.stringify(adMetadata) + "/" + bid); |
| } |
| if (adMetadata.renderUrl !== ("data for " + browserSignals.renderUrl)) { |
| throw new Error("wrong data for renderUrl:" + |
| JSON.stringify(adMetadata) + "/" + |
| browserSignals.renderUrl); |
| } |
| let components = browserSignals.adComponents; |
| if (adMetadata.adComponentsUrl) { |
| if (components.length !== 1 || |
| components[0] !== adMetadata.adComponentsUrl) { |
| throw new Error("wrong data for adComponents:" + |
| JSON.stringify(adMetadata) + "/" + |
| browserSignals.adComponents); |
| } |
| } else if (components !== undefined) { |
| throw new Error("wrong data for adComponents:" + |
| JSON.stringify(adMetadata) + "/" + |
| browserSignals.adComponents); |
| } |
| // If this is the top-level auction scoring a bid from a component |
| // auction, the component auction should have added a |
| // "fromComponentAuction" field to `adMetadata`. |
| if ("fromComponentAuction" in adMetadata != |
| "componentSeller" in browserSignals) { |
| throw new Error("wrong adMetadata.fromComponentAuction"); |
| } |
| if (auctionConfig.decisionLogicUrl !== decisionLogicUrl) |
| throw new Error("wrong decisionLogicUrl in auctionConfig"); |
| // Check `perBuyerSignals` for the first bidder. |
| let signals1 = auctionConfig.perBuyerSignals['https://adplatform.com']; |
| if (signals1[auctionConfig.seller + 'Signals'] !== 'Ad PlatformSignals') |
| throw new Error("Wrong perBuyerSignals in auctionConfig"); |
| if (typeof auctionConfig.perBuyerTimeouts['https://adplatform.com'] !== |
| "number" || |
| typeof auctionConfig.perBuyerTimeouts['*'] !== "number") { |
| throw new Error("timeout in auctionConfig.perBuyerTimeouts is not a " + |
| "number. huh"); |
| } |
| if (auctionConfig.perBuyerCumulativeTimeouts['https://adplatform.com'] !== |
| 12345 || |
| auctionConfig.perBuyerCumulativeTimeouts['*'] !== 23456) { |
| throw new Error("timeout in auctionConfig.perBuyerCumulativeTimeouts " + |
| "is the wrong value. huh"); |
| } |
| if (auctionConfig.sellerSignals["url"] != decisionLogicUrl) |
| throw new Error("Wrong sellerSignals"); |
| if (typeof auctionConfig.sellerTimeout !== "number") |
| throw new Error("auctionConfig.sellerTimeout is not a number. huh"); |
| if (browserSignals.topWindowHostname !== 'publisher1.com') |
| throw new Error("wrong topWindowHostname"); |
| |
| if (decisionLogicUrl.startsWith(topLevelSeller)) { |
| // Top-level sellers should receive component sellers, but only for |
| // bids received from component auctions. |
| if ("topLevelSeller" in browserSignals) |
| throw new Error("Expected no topLevelSeller in browserSignals."); |
| if (adMetadata.seller == topLevelSeller) { |
| // If the bidder sent its bid directly to this top-level seller, |
| // there should be no `componentSeller`. |
| if ("componentSeller" in browserSignals) |
| throw new Error("Expected no componentSeller in browserSignals."); |
| } else { |
| // If the bidder sent its bid to a some other seller seller, that |
| // was the component seller, so `componentSeller` should be populated. |
| if (!browserSignals.componentSeller.includes("component")) |
| throw new Error("Incorrect componentSeller in browserSignals."); |
| } |
| } else { |
| // Component sellers should receive only the top-level seller. |
| if (browserSignals.topLevelSeller !== topLevelSeller) |
| throw new Error("Incorrect topLevelSeller in browserSignals."); |
| if ("componentSeller" in browserSignals) |
| throw new Error("Expected no componentSeller in browserSignals."); |
| } |
| |
| if ("joinCount" in browserSignals) |
| throw new Error("wrong kind of browser signals"); |
| if (typeof browserSignals.biddingDurationMsec !== "number") |
| throw new Error("biddingDurationMsec is not a number. huh"); |
| if (browserSignals.biddingDurationMsec < 0) |
| throw new Error("biddingDurationMsec should be non-negative."); |
| if (browserSignals.dataVersion !== undefined) |
| throw new Error(`wrong dataVersion (${browserSignals.dataVersion})`); |
| if (debugLossReportUrl) { |
| forDebuggingOnly.reportAdAuctionLoss( |
| buildDebugReportUrl(debugLossReportUrl) + bid); |
| } |
| if (debugWinReportUrl) { |
| forDebuggingOnly.reportAdAuctionWin( |
| buildDebugReportUrl(debugWinReportUrl) + bid); |
| } |
| privateAggregation.sendHistogramReport({bucket: 5n, value: 6}); |
| // Private aggregation requests with non-reserved event types in a seller |
| // script will not be reported. |
| privateAggregation.reportContributionForEvent( |
| 'click', {bucket: 50n, value: 60}); |
| |
| adMetadata.fromComponentAuction = true; |
| |
| return {desirability: computeScore(bid), |
| // Only allow a component auction when the passed in ad is from |
| // one. |
| allowComponentAuction: |
| browserSignals.topLevelSeller !== undefined || |
| browserSignals.componentSeller !== undefined, |
| ad: adMetadata} |
| } |
| |
| // A helper function to build a debug report URL. |
| function buildDebugReportUrl(debugReportUrl) { |
| if (reportPostAuctionSignals) |
| debugReportUrl += postAuctionSignalsPlaceholder; |
| if (reportTopLevelPostAuctionSignals) { |
| debugReportUrl += reportPostAuctionSignals ? '&' : '?'; |
| debugReportUrl += topLevelPostAuctionSignalsPlaceholder; |
| } |
| // Only add key "bid=" to the report URL when report post auction signals |
| // where the URL has many keys. Otherwise it's the only key so only have |
| // the value in the URL is fine. |
| if (reportPostAuctionSignals || reportTopLevelPostAuctionSignals) |
| debugReportUrl += "&bid="; |
| return debugReportUrl; |
| } |
| |
| function reportResult(auctionConfig, browserSignals) { |
| // Check `perBuyerSignals` for the first bidder. |
| let signals1 = auctionConfig.perBuyerSignals['https://adplatform.com']; |
| if (signals1[auctionConfig.seller + 'Signals'] !== 'Ad PlatformSignals') |
| throw new Error("Wrong perBuyerSignals in auctionConfig"); |
| if (auctionConfig.decisionLogicUrl !== decisionLogicUrl) |
| throw new Error("wrong decisionLogicUrl in auctionConfig"); |
| if (browserSignals.topWindowHostname !== 'publisher1.com') |
| throw new Error("wrong topWindowHostname in browserSignals"); |
| |
| if (decisionLogicUrl.startsWith(topLevelSeller)) { |
| // Top-level sellers should receive component sellers, but only for |
| // bids received from component auctions. |
| if ("topLevelSeller" in browserSignals) |
| throw new Error("Expected no topLevelSeller in browserSignals."); |
| if (bidFromComponentAuctionWins) { |
| if (!browserSignals.componentSeller.includes("component")) |
| throw new Error("Incorrect componentSeller in browserSignals."); |
| } else { |
| if ("componentSeller" in browserSignals) |
| throw new Error("Expected no componentSeller in browserSignals."); |
| } |
| |
| if ("topLevelSellerSignals" in browserSignals) |
| throw new Error("Unexpected browserSignals.topLevelSellerSignals"); |
| } else { |
| // Component sellers should receive only the top-level seller. |
| if (browserSignals.topLevelSeller !== topLevelSeller) |
| throw new Error("Incorrect topLevelSeller in browserSignals."); |
| if ("componentSeller" in browserSignals) |
| throw new Error("Expected no componentSeller in browserSignals."); |
| |
| // Component sellers should get the return value of the top-level |
| // seller's `reportResult()` call, which is, in this case, the |
| // `browserSignals` of the top-level seller. |
| if (browserSignals.topLevelSellerSignals.componentSeller != |
| auctionConfig.seller) { |
| throw new Error("Unexpected browserSignals.topLevelSellerSignals"); |
| } |
| } |
| |
| if (browserSignals.desirability != computeScore(browserSignals.bid)) |
| throw new Error("wrong bid or desirability in browserSignals"); |
| // The default scoreAd() script does not modify bids. |
| if ("modifiedBid" in browserSignals) |
| throw new Error("modifiedBid unexpectedly in browserSignals"); |
| if (browserSignals.dataVersion !== undefined) |
| throw new Error(`wrong dataVersion (${browserSignals.dataVersion})`); |
| if (sendReportUrl) { |
| registerAdBeacon({ |
| "click": sendReportUrl + 2*browserSignals.bid, |
| }); |
| if (reportPostAuctionSignals) { |
| sendReportUrl += "?highestScoringOtherBid=" + |
| browserSignals.highestScoringOtherBid + "&bid="; |
| } |
| sendReportTo(sendReportUrl + browserSignals.bid); |
| } |
| privateAggregation.sendHistogramReport({bucket: 7n, value: 8}); |
| // Private aggregation requests with non-reserved event types in seller |
| // script will not be reported. |
| privateAggregation.reportContributionForEvent( |
| 'click', {bucket: 70n, value: 80}); |
| |
| return browserSignals; |
| } |
| |
| // Use different scoring functions for the top-level seller and component |
| // sellers, so can verify that each ReportResult() method gets the score |
| // from the correct seller, and so that the the wrong bidder will win |
| // in some tests if either component auction scores are used for the |
| // top-level auction, or if all bidders from component auctions are passed |
| // to the top-level auction. |
| function computeScore(bid) { |
| if (decisionLogicUrl == "https://adstuff.publisher1.com/auction.js") |
| return 2 * bid; |
| return 100 - bid; |
| } |
| )"; |
| |
| return base::StringPrintf( |
| kCheckingAuctionScript, decision_logic_url.spec().c_str(), |
| send_report_url ? send_report_url->spec().c_str() : "", |
| report_post_auction_signals ? "true" : "false", |
| kPostAuctionSignalsPlaceholder, debug_loss_report_url.c_str(), |
| debug_win_report_url.c_str(), |
| bid_from_component_auction_wins ? "true" : "false", |
| report_top_level_post_auction_signals ? "true" : "false", |
| kTopLevelPostAuctionSignalsPlaceholder); |
| } |
| |
| std::string MakeAuctionScript(bool report_post_auction_signals = false, |
| const GURL& decision_logic_url = GURL( |
| "https://adstuff.publisher1.com/auction.js"), |
| const std::string& debug_loss_report_url = "", |
| const std::string& debug_win_report_url = "") { |
| return MakeDecisionScript( |
| decision_logic_url, |
| /*send_report_url=*/GURL("https://reporting.example.com"), |
| /*bid_from_component_auction_wins=*/false, |
| /*report_post_auction_signals=*/report_post_auction_signals, |
| debug_loss_report_url, debug_win_report_url); |
| } |
| |
| std::string MakeAuctionScriptNoReportUrl( |
| const GURL& decision_logic_url = |
| GURL("https://adstuff.publisher1.com/auction.js"), |
| bool report_post_auction_signals = false, |
| const std::string& debug_loss_report_url = "", |
| const std::string& debug_win_report_url = "") { |
| return MakeDecisionScript(decision_logic_url, |
| /*send_report_url=*/absl::nullopt, |
| /*bid_from_component_auction_wins=*/false, |
| report_post_auction_signals, debug_loss_report_url, |
| debug_win_report_url); |
| } |
| |
| const char kBasicReportResult[] = R"( |
| function reportResult(auctionConfig, browserSignals) { |
| privateAggregation.sendHistogramReport({bucket: 7n, value: 8}); |
| sendReportTo("https://reporting.example.com/" + browserSignals.bid); |
| registerAdBeacon({ |
| "click": "https://reporting.example.com/" + 2*browserSignals.bid, |
| }); |
| return browserSignals; |
| } |
| )"; |
| |
| std::string MakeAuctionScriptReject2( |
| const std::string& reject_reason = "not-available") { |
| constexpr char kAuctionScriptRejects2[] = R"( |
| function scoreAd(adMetadata, bid, auctionConfig, browserSignals) { |
| privateAggregation.sendHistogramReport({bucket: 5n, value: 6}); |
| if (bid === 2) |
| return {desirability: -1, rejectReason: '%s'}; |
| return bid + 1; |
| } |
| )"; |
| return base::StringPrintf(kAuctionScriptRejects2, reject_reason.c_str()) + |
| kBasicReportResult; |
| } |
| |
| std::string MakeAuctionScriptReject1And2WithDebugReporting( |
| const std::string& debug_loss_report_url = "", |
| const std::string& debug_win_report_url = "") { |
| constexpr char kReject1And2WithDebugReporting[] = R"( |
| const debugLossReportUrl = "%s"; |
| const debugWinReportUrl = "%s"; |
| function scoreAd(adMetadata, bid, auctionConfig, browserSignals) { |
| let result = bid + 1; |
| let rejectReason = "not-available"; |
| if (bid === 1) { |
| result = -1; |
| rejectReason = 'invalid-bid'; |
| } else if (bid === 2) { |
| result = -1; |
| rejectReason = 'bid-below-auction-floor'; |
| } |
| |
| if (debugLossReportUrl) { |
| forDebuggingOnly.reportAdAuctionLoss( |
| debugLossReportUrl + '&bid=' + bid); |
| } |
| if (debugWinReportUrl) |
| forDebuggingOnly.reportAdAuctionWin(debugWinReportUrl + "&bid=" + bid); |
| return { |
| desirability: result, |
| allowComponentAuction: true, |
| rejectReason: rejectReason |
| }; |
| } |
| )"; |
| return base::StringPrintf(kReject1And2WithDebugReporting, |
| debug_loss_report_url.c_str(), |
| debug_win_report_url.c_str()) + |
| kBasicReportResult; |
| } |
| |
| // Treats interest group name as bid. Interest group name needs to be |
| // convertible to a valid number in order to use this script. |
| std::string MakeBidScriptSupportsTie() { |
| constexpr char kBidScriptSupportsTie[] = R"( |
| const debugLossReportUrl = '%s'; |
| const debugWinReportUrl = '%s'; |
| |
| const postAuctionSignalsPlaceholder = '%s'; |
| function generateBid( |
| interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals, |
| browserSignals) { |
| const bid = parseInt(interestGroup.name); |
| forDebuggingOnly.reportAdAuctionLoss( |
| debugLossReportUrl + postAuctionSignalsPlaceholder + '&bid=' + bid); |
| forDebuggingOnly.reportAdAuctionWin( |
| debugWinReportUrl + postAuctionSignalsPlaceholder + '&bid=' + bid); |
| return {ad: [], bid: bid, render: interestGroup.ads[0].renderUrl}; |
| } |
| function reportWin( |
| auctionSignals, perBuyerSignals, sellerSignals, browserSignals) { |
| sendReportTo( |
| 'https://buyer-reporting.example.com/?highestScoringOtherBid=' + |
| browserSignals.highestScoringOtherBid + |
| '&madeHighestScoringOtherBid=' + |
| browserSignals.madeHighestScoringOtherBid + |
| '&bid=' + browserSignals.bid); |
| } |
| )"; |
| return base::StringPrintf( |
| kBidScriptSupportsTie, kBidderDebugLossReportBaseUrl, |
| kBidderDebugWinReportBaseUrl, kPostAuctionSignalsPlaceholder); |
| } |
| |
| // Score is 3 if bid is 3 or 4, otherwise score is 1. |
| std::string MakeAuctionScriptSupportsTie() { |
| constexpr char kAuctionScriptSupportsTie[] = R"( |
| const debugLossReportUrl = "%s"; |
| const debugWinReportUrl = "%s"; |
| const postAuctionSignalsPlaceholder = "%s"; |
| function scoreAd(adMetadata, bid, auctionConfig, browserSignals) { |
| forDebuggingOnly.reportAdAuctionLoss( |
| debugLossReportUrl + postAuctionSignalsPlaceholder + "&bid=" + bid); |
| forDebuggingOnly.reportAdAuctionWin( |
| debugWinReportUrl + postAuctionSignalsPlaceholder + "&bid=" + bid); |
| return bid = (bid == 3 || bid == 4) ? 3 : 1; |
| } |
| function reportResult(auctionConfig, browserSignals) { |
| sendReportTo( |
| "https://reporting.example.com/?highestScoringOtherBid=" + |
| browserSignals.highestScoringOtherBid + "&bid=" + |
| browserSignals.bid); |
| } |
| )"; |
| return base::StringPrintf( |
| kAuctionScriptSupportsTie, kSellerDebugLossReportBaseUrl, |
| kSellerDebugWinReportBaseUrl, kPostAuctionSignalsPlaceholder); |
| } |
| |
| // Represents an entry in trusted bidding signal's `perInterestGroupData` field. |
| struct BiddingSignalsPerInterestGroupData { |
| std::string interest_group_name; |
| absl::optional<base::flat_map<std::string, double>> priority_vector; |
| }; |
| |
| // Creates a trusted bidding signals response body with the provided data. |
| std::string MakeBiddingSignalsWithPerInterestGroupData( |
| std::vector<BiddingSignalsPerInterestGroupData> per_interest_group_data) { |
| base::Value::Dict per_interest_group_dict; |
| for (const auto& data : per_interest_group_data) { |
| base::Value::Dict interest_group_dict; |
| if (data.priority_vector) { |
| base::Value::Dict priority_vector; |
| for (const auto& pair : *data.priority_vector) { |
| priority_vector.Set(pair.first, pair.second); |
| } |
| interest_group_dict.Set("priorityVector", std::move(priority_vector)); |
| } |
| per_interest_group_dict.Set(data.interest_group_name, |
| std::move(interest_group_dict)); |
| } |
| |
| base::Value::Dict bidding_signals_dict; |
| bidding_signals_dict.Set("perInterestGroupData", |
| std::move(per_interest_group_dict)); |
| |
| std::string bidding_signals_string; |
| CHECK(base::JSONWriter::Write(bidding_signals_dict, &bidding_signals_string)); |
| return bidding_signals_string; |
| } |
| |
| // Returns a report URL with given parameters for reportWin(), with post auction |
| // signals included in the URL |
| const GURL ReportWinUrl( |
| double bid, |
| double highest_scoring_other_bid, |
| bool made_highest_scoring_other_bid, |
| const std::string& url = "https://buyer-reporting.example.com/") { |
| // Only keeps integer part of bid values for simplicity for now. |
| return GURL(base::StringPrintf( |
| "%s" |
| "?highestScoringOtherBid=%.0f&madeHighestScoringOtherBid=%s&bid=%.0f", |
| url.c_str(), highest_scoring_other_bid, |
| made_highest_scoring_other_bid ? "true" : "false", bid)); |
| } |
| |
| // Returns a report URL with given parameters for forDebuggingOnly win/loss |
| // report APIs, with post auction signals included in the URL. |
| const GURL DebugReportUrl( |
| const std::string& url, |
| const PostAuctionSignals& signals, |
| absl::optional<double> bid = absl::nullopt, |
| absl::optional<std::string> reject_reason = absl::nullopt) { |
| // Post auction signals needs to be consistent with |
| // `kPostAuctionSignalsPlaceholder`. Only keeps integer part of bid values for |
| // simplicity for now. |
| std::string report_url_string = base::StringPrintf( |
| "%s" |
| // Post auction signals |
| "?winningBid=%.0f&madeWinningBid=%s&highestScoringOtherBid=%.0f&" |
| "madeHighestScoringOtherBid=%s", |
| url.c_str(), signals.winning_bid, |
| signals.made_winning_bid ? "true" : "false", |
| signals.highest_scoring_other_bid, |
| signals.made_highest_scoring_other_bid ? "true" : "false"); |
| if (reject_reason.has_value()) { |
| report_url_string.append( |
| base::StringPrintf("&rejectReason=%s", reject_reason.value().c_str())); |
| } |
| |
| if (bid.has_value()) { |
| return GURL(base::StringPrintf("%s&bid=%.0f", report_url_string.c_str(), |
| bid.value())); |
| } |
| return GURL(report_url_string); |
| } |
| |
| // Returns a report URL for component auction seller with given parameters for |
| // forDebuggingOnly win/loss report APIs, with post auction signals from both |
| // component auction and top level auction included in the URL. When no |
| // `top_level_signals` is needed, just use function DebugReportUrl(). |
| const GURL ComponentSellerDebugReportUrl( |
| const std::string& url, |
| const PostAuctionSignals& signals, |
| const PostAuctionSignals& top_level_signals, |
| double bid) { |
| // Post auction signals needs to be consistent with |
| // `kPostAuctionSignalsPlaceholder`, and top level post auction signals needs |
| // to be consistent with `kTopLevelPostAuctionSignalsPlaceholder`. Only keeps |
| // integer part of bid values for simplicity for now. |
| return GURL(base::StringPrintf( |
| "%s" |
| // Post auction signals. |
| "?winningBid=%.0f&madeWinningBid=%s&highestScoringOtherBid=%.0f&" |
| "madeHighestScoringOtherBid=%s" |
| // Top level post auction signals. |
| "&topLevelWinningBid=%.0f&topLevelMadeWinningBid=%s" |
| // Bid value. |
| "&bid=%.0f", |
| url.c_str(), signals.winning_bid, |
| signals.made_winning_bid ? "true" : "false", |
| signals.highest_scoring_other_bid, |
| signals.made_highest_scoring_other_bid ? "true" : "false", |
| top_level_signals.winning_bid, |
| top_level_signals.made_winning_bid ? "true" : "false", bid)); |
| } |
| |
| // Builds a PrivateAggregationRequest with histogram contribution using given |
| // `bucket` and `value`. |
| const auction_worklet::mojom::PrivateAggregationRequestPtr |
| BuildPrivateAggregationRequest(absl::uint128 bucket, int value) { |
| return auction_worklet::mojom::PrivateAggregationRequest::New( |
| auction_worklet::mojom::AggregatableReportContribution:: |
| NewHistogramContribution( |
| blink::mojom::AggregatableReportHistogramContribution::New( |
| bucket, value)), |
| blink::mojom::AggregationServiceMode::kDefault, |
| blink::mojom::DebugModeDetails::New()); |
| } |
| |
| const auction_worklet::mojom::PrivateAggregationRequestPtr |
| BuildPrivateAggregationForEventRequest(absl::uint128 bucket, |
| int value, |
| std::string event_type) { |
| auction_worklet::mojom::AggregatableReportForEventContribution contribution( |
| auction_worklet::mojom::ForEventSignalBucket::NewIdBucket(bucket), |
| auction_worklet::mojom::ForEventSignalValue::NewIntValue(value), |
| event_type); |
| |
| return auction_worklet::mojom::PrivateAggregationRequest::New( |
| auction_worklet::mojom::AggregatableReportContribution:: |
| NewForEventContribution(contribution.Clone()), |
| blink::mojom::AggregationServiceMode::kDefault, |
| blink::mojom::DebugModeDetails::New()); |
| } |
| |
| // Marks `ad` in `group` k-anonymous, double-checking that its url is `url`. |
| void AuthorizeKAnonAd(const blink::InterestGroup::Ad& ad, |
| const char* url, |
| StorageInterestGroup& group) { |
| DCHECK_EQ(url, ad.render_url.spec()); |
| group.bidding_ads_kanon.emplace_back(); |
| group.bidding_ads_kanon.back().key = |
| blink::KAnonKeyForAdBid(group.interest_group, ad.render_url); |
| group.bidding_ads_kanon.back().is_k_anonymous = true; |
| group.bidding_ads_kanon.back().last_updated = base::Time::Now(); |
| } |
| |
| void AuthorizeKAnonAdComponent(const blink::InterestGroup::Ad& ad, |
| const char* url, |
| StorageInterestGroup& group) { |
| DCHECK_EQ(url, ad.render_url.spec()); |
| group.component_ads_kanon.emplace_back(); |
| group.component_ads_kanon.back().key = |
| blink::KAnonKeyForAdComponentBid(ad.render_url); |
| group.component_ads_kanon.back().is_k_anonymous = true; |
| group.component_ads_kanon.back().last_updated = base::Time::Now(); |
| } |
| |
| class SameProcessAuctionProcessManager : public AuctionProcessManager { |
| public: |
| SameProcessAuctionProcessManager() = default; |
| SameProcessAuctionProcessManager(const SameProcessAuctionProcessManager&) = |
| delete; |
| SameProcessAuctionProcessManager& operator=( |
| const SameProcessAuctionProcessManager&) = delete; |
| ~SameProcessAuctionProcessManager() override = default; |
| |
| // Resume all worklets paused waiting for debugger on startup. |
| void ResumeAllPaused() { |
| for (const auto& svc : auction_worklet_services_) { |
| for (const auto& v8_helper : svc->AuctionV8HelpersForTesting()) { |
| v8_helper->v8_runner()->PostTask( |
| FROM_HERE, |
| base::BindOnce( |
| [](scoped_refptr<auction_worklet::AuctionV8Helper> v8_helper) { |
| v8_helper->ResumeAllForTesting(); |
| }, |
| v8_helper)); |
| } |
| } |
| } |
| |
| int NumBidderWorklets() const { |
| int total = 0; |
| for (const auto& svc : auction_worklet_services_) { |
| total += svc->NumBidderWorkletsForTesting(); |
| } |
| return total; |
| } |
| |
| int NumSellerWorklets() const { |
| int total = 0; |
| for (const auto& svc : auction_worklet_services_) { |
| total += svc->NumSellerWorkletsForTesting(); |
| } |
| return total; |
| } |
| |
| private: |
| RenderProcessHost* LaunchProcess( |
| mojo::PendingReceiver<auction_worklet::mojom::AuctionWorkletService> |
| auction_worklet_service_receiver, |
| const ProcessHandle* handle, |
| const std::string& display_name) override { |
| // Create one AuctionWorkletServiceImpl per Mojo pipe, just like in |
| // production code. Don't bother to delete the service on pipe close, |
| // though; just keep it in a vector instead. |
| auction_worklet_services_.push_back( |
| auction_worklet::AuctionWorkletServiceImpl::CreateForService( |
| std::move(auction_worklet_service_receiver))); |
| return nullptr; |
| } |
| |
| scoped_refptr<SiteInstance> MaybeComputeSiteInstance( |
| SiteInstance* frame_site_instance, |
| const url::Origin& worklet_origin) override { |
| return nullptr; |
| } |
| |
| bool TryUseSharedProcess(ProcessHandle* process_handle) override { |
| return false; |
| } |
| |
| std::vector<std::unique_ptr<auction_worklet::AuctionWorkletServiceImpl>> |
| auction_worklet_services_; |
| }; |
| |
| class AuctionRunnerTest : public RenderViewHostTestHarness, |
| public AuctionWorkletManager::Delegate, |
| public DebuggableAuctionWorkletTracker::Observer { |
| protected: |
| // Output of the RunAuctionCallback passed to AuctionRunner::CreateAndStart(). |
| struct Result { |
| Result() = default; |
| // Can't use default copy logic, since it contains Mojo types. |
| Result(const Result&) = delete; |
| Result& operator=(const Result&) = delete; |
| |
| bool manually_aborted = false; |
| absl::optional<InterestGroupKey> winning_group_id; |
| absl::optional<blink::AdDescriptor> ad_descriptor; |
| std::vector<blink::AdDescriptor> ad_component_descriptors; |
| std::string winning_group_ad_metadata; |
| std::vector<GURL> report_urls; |
| std::vector<GURL> debug_loss_report_urls; |
| std::vector<GURL> debug_win_report_urls; |
| base::flat_map<blink::FencedFrame::ReportingDestination, |
| FencedFrameReporter::ReportingUrlMap> |
| ad_beacon_map; |
| std::map<std::string, PrivateAggregationRequests> |
| private_aggregation_event_map; |
| std::vector<blink::InterestGroupKey> interest_groups_that_bid; |
| |
| std::vector<std::string> errors; |
| }; |
| |
| explicit AuctionRunnerTest( |
| bool should_enable_private_aggregation = true, |
| bool should_enable_private_aggregation_fledge_extension = true, |
| auction_worklet::mojom::KAnonymityBidMode kanon_mode = |
| auction_worklet::mojom::KAnonymityBidMode::kNone) |
| : RenderViewHostTestHarness( |
| base::test::TaskEnvironment::TimeSource::MOCK_TIME) { |
| mojo::SetDefaultProcessErrorHandler(base::BindRepeating( |
| &AuctionRunnerTest::OnBadMessage, base::Unretained(this))); |
| DebuggableAuctionWorkletTracker::GetInstance()->AddObserver(this); |
| |
| std::vector<base::test::FeatureRefAndParams> enabled_features; |
| std::vector<base::test::FeatureRef> disabled_features; |
| |
| if (should_enable_private_aggregation) { |
| enabled_features.push_back( |
| {blink::features::kPrivateAggregationApi, |
| {{"fledge_extensions_enabled", |
| should_enable_private_aggregation_fledge_extension ? "true" |
| : "false"}}}); |
| } else { |
| disabled_features.push_back(blink::features::kPrivateAggregationApi); |
| } |
| |
| switch (kanon_mode) { |
| case auction_worklet::mojom::KAnonymityBidMode::kEnforce: |
| enabled_features.push_back( |
| {blink::features::kFledgeConsiderKAnonymity, {}}); |
| enabled_features.push_back( |
| {blink::features::kFledgeEnforceKAnonymity, {}}); |
| break; |
| case auction_worklet::mojom::KAnonymityBidMode::kSimulate: |
| enabled_features.push_back( |
| {blink::features::kFledgeConsiderKAnonymity, {}}); |
| disabled_features.push_back(blink::features::kFledgeEnforceKAnonymity); |
| break; |
| case auction_worklet::mojom::KAnonymityBidMode::kNone: |
| disabled_features.push_back(blink::features::kFledgeConsiderKAnonymity); |
| disabled_features.push_back(blink::features::kFledgeEnforceKAnonymity); |
| break; |
| } |
| |
| scoped_feature_list_.InitWithFeaturesAndParameters(enabled_features, |
| disabled_features); |
| } |
| |
| void TearDown() override { |
| DebuggableAuctionWorkletTracker::GetInstance()->RemoveObserver(this); |
| |
| // Any bad message should have been inspected and cleared before the end of |
| // the test. |
| EXPECT_EQ(std::string(), bad_message_); |
| mojo::SetDefaultProcessErrorHandler(base::NullCallback()); |
| |
| // Give off-thread things a chance to delete. |
| task_environment()->RunUntilIdle(); |
| |
| // `interest_group_manager_` needs to be reset before the task environment |
| // is destroyed (in `RenderViewHostTestHarness::TearDown()`). |
| auction_runner_.reset(); |
| interest_group_manager_.reset(); |
| |
| RenderViewHostTestHarness::TearDown(); |
| } |
| |
| ~AuctionRunnerTest() override = default; |
| |
| void OnBadMessage(const std::string& reason) { |
| // No test expects multiple bad messages at a time |
| EXPECT_EQ(std::string(), bad_message_); |
| // Empty bad messages aren't expected. This check allows an empty |
| // `bad_message_` field to mean no bad message, avoiding using an optional, |
| // which has less helpful output on EXPECT failures. |
| EXPECT_FALSE(reason.empty()); |
| |
| bad_message_ = reason; |
| } |
| |
| // Gets and clear most recent bad Mojo message. |
| std::string TakeBadMessage() { return std::move(bad_message_); } |
| |
| blink::AuctionConfig::MaybePromiseJson MakeSellerSignals( |
| bool use_promise, |
| const GURL& seller_decision_logic_url) { |
| if (use_promise) { |
| return blink::AuctionConfig::MaybePromiseJson::FromPromise(); |
| } else { |
| return blink::AuctionConfig::MaybePromiseJson::FromValue( |
| base::StringPrintf(R"({"url": "%s"})", |
| seller_decision_logic_url.spec().c_str())); |
| } |
| } |
| |
| blink::AuctionConfig::MaybePromiseJson MakeAuctionSignals( |
| bool use_promise, |
| const url::Origin& seller) { |
| if (use_promise) { |
| return blink::AuctionConfig::MaybePromiseJson::FromPromise(); |
| } else { |
| return blink::AuctionConfig::MaybePromiseJson::FromValue( |
| base::StringPrintf(R"("auctionSignalsFor %s")", |
| seller.Serialize().c_str())); |
| } |
| } |
| |
| blink::AuctionConfig::MaybePromisePerBuyerSignals MakePerBuyerSignals( |
| bool use_promise, |
| const url::Origin& seller) { |
| if (use_promise) { |
| return blink::AuctionConfig::MaybePromisePerBuyerSignals::FromPromise(); |
| } else { |
| base::flat_map<url::Origin, std::string> per_buyer_signals; |
| // Use a combination of bidder and seller values, so can make sure bidders |
| // get the value from the correct seller script. Also append a fixed |
| // string, as a defense against pulling the right values from the wrong |
| // places. |
| per_buyer_signals[kBidder1] = |
| base::StringPrintf(R"({"%sSignals": "%sSignals"})", |
| seller.Serialize().c_str(), kBidder1Name.c_str()); |
| per_buyer_signals[kBidder2] = |
| base::StringPrintf(R"({"%sSignals": "%sSignals"})", |
| seller.Serialize().c_str(), kBidder2Name.c_str()); |
| return blink::AuctionConfig::MaybePromisePerBuyerSignals::FromValue( |
| std::move(per_buyer_signals)); |
| } |
| } |
| |
| blink::AuctionConfig::MaybePromiseBuyerTimeouts MakeBuyerTimeouts( |
| bool use_promise) { |
| if (use_promise) { |
| return blink::AuctionConfig::MaybePromiseBuyerTimeouts::FromPromise(); |
| } else { |
| blink::AuctionConfig::BuyerTimeouts buyer_timeouts; |
| // Any per buyer timeout higher than 500 ms will be clamped to 500 ms by |
| // the AuctionRunner. |
| buyer_timeouts.per_buyer_timeouts.emplace(); |
| buyer_timeouts.per_buyer_timeouts.value()[kBidder1] = |
| base::Milliseconds(1000); |
| buyer_timeouts.all_buyers_timeout = base::Milliseconds(150); |
| return blink::AuctionConfig::MaybePromiseBuyerTimeouts::FromValue( |
| std::move(buyer_timeouts)); |
| } |
| } |
| |
| blink::AuctionConfig::MaybePromiseBuyerTimeouts MakeBuyerCumulativeTimeouts( |
| bool use_promise) { |
| if (use_promise) { |
| return blink::AuctionConfig::MaybePromiseBuyerTimeouts::FromPromise(); |
| } else { |
| blink::AuctionConfig::BuyerTimeouts buyer_cumulative_timeouts; |
| buyer_cumulative_timeouts.per_buyer_timeouts.emplace(); |
| buyer_cumulative_timeouts.per_buyer_timeouts.value()[kBidder1] = |
| kBidder1CumulativeTimeout; |
| buyer_cumulative_timeouts.all_buyers_timeout = |
| kAllBuyersCumulativeTimeout; |
| return blink::AuctionConfig::MaybePromiseBuyerTimeouts::FromValue( |
| std::move(buyer_cumulative_timeouts)); |
| } |
| } |
| |
| // Helper to create an auction config with the specified values. |
| blink::AuctionConfig CreateAuctionConfig( |
| const GURL& seller_decision_logic_url, |
| absl::optional<std::vector<url::Origin>> buyers) { |
| blink::AuctionConfig auction_config; |
| auction_config.seller = url::Origin::Create(seller_decision_logic_url); |
| auction_config.decision_logic_url = seller_decision_logic_url; |
| if (pass_promise_for_direct_from_seller_signals_) { |
| auction_config.direct_from_seller_signals = blink::AuctionConfig:: |
| MaybePromiseDirectFromSellerSignals::FromPromise(); |
| } |
| |
| auction_config.non_shared_params.interest_group_buyers = std::move(buyers); |
| |
| auction_config.non_shared_params.seller_signals = MakeSellerSignals( |
| use_promise_for_seller_signals_, seller_decision_logic_url); |
| auction_config.non_shared_params.seller_timeout = base::Milliseconds(1000); |
| auction_config.non_shared_params.per_buyer_signals = MakePerBuyerSignals( |
| use_promise_for_per_buyer_signals_, auction_config.seller); |
| auction_config.non_shared_params.buyer_timeouts = |
| MakeBuyerTimeouts(use_promise_for_buyer_timeouts_); |
| auction_config.non_shared_params.buyer_cumulative_timeouts = |
| MakeBuyerCumulativeTimeouts(use_promise_for_buyer_cumulative_timeouts_); |
| auction_config.non_shared_params.auction_signals = MakeAuctionSignals( |
| use_promise_for_auction_signals_, auction_config.seller); |
| |
| auction_config.seller_experiment_group_id = seller_experiment_group_id_; |
| auction_config.all_buyer_experiment_group_id = |
| all_buyer_experiment_group_id_; |
| |
| for (const auto& kv : per_buyer_experiment_group_id_) { |
| auction_config.per_buyer_experiment_group_ids[kv.first] = kv.second; |
| } |
| |
| auction_config.non_shared_params.all_buyers_group_limit = |
| all_buyers_group_limit_; |
| auction_config.non_shared_params.all_buyers_priority_signals = |
| all_buyers_priority_signals_; |
| |
| auction_config.non_shared_params.auction_report_buyer_keys = |
| auction_report_buyer_keys_; |
| auction_config.non_shared_params.auction_report_buyers = |
| auction_report_buyers_; |
| |
| return auction_config; |
| } |
| |
| // Starts an auction without waiting for it to complete. Useful when using |
| // MockAuctionProcessManager. |
| // |
| // `bidders` are added to a new InterestGroupManager before running the |
| // auction. The times of their previous wins are ignored, as the |
| // InterestGroupManager automatically attaches the current time, though their |
| // wins will be added in order, with chronologically increasing times within |
| // each InterestGroup. |
| void StartAuction(const GURL& seller_decision_logic_url, |
| const std::vector<StorageInterestGroup>& bidders) { |
| auction_complete_ = false; |
| |
| auto auction_config = |
| CreateAuctionConfig(seller_decision_logic_url, interest_group_buyers_); |
| |
| auction_config.trusted_scoring_signals_url = trusted_scoring_signals_url_; |
| |
| for (const auto& component_auction : component_auctions_) { |
| auction_config.non_shared_params.component_auctions.push_back( |
| component_auction); |
| } |
| |
| // Need to clear `same_process_auction_process_manager_` since underlying |
| // object may be owned by `interest_group_manager_`. |
| same_process_auction_process_manager_ = nullptr; |
| interest_group_manager_ = std::make_unique<TestInterestGroupManagerImpl>( |
| frame_origin_, GetClientSecurityState(), |
| dummy_report_shared_url_loader_factory_); |
| if (!auction_process_manager_) { |
| auto same_process_auction_process_manager = |
| std::make_unique<SameProcessAuctionProcessManager>(); |
| same_process_auction_process_manager_ = |
| same_process_auction_process_manager.get(); |
| auction_process_manager_ = |
| std::move(same_process_auction_process_manager); |
| } |
| auction_worklet_manager_ = std::make_unique<AuctionWorkletManager>( |
| auction_process_manager_.get(), top_frame_origin_, frame_origin_, this); |
| interest_group_manager_->set_auction_process_manager_for_testing( |
| std::move(auction_process_manager_)); |
| |
| histogram_tester_ = std::make_unique<base::HistogramTester>(); |
| |
| // Add previous wins and bids to the interest group manager. |
| for (auto& bidder : bidders) { |
| for (int i = 0; i < bidder.bidding_browser_signals->join_count; i++) { |
| interest_group_manager_->JoinInterestGroup( |
| bidder.interest_group, bidder.joining_origin.GetURL()); |
| } |
| for (int i = 0; i < bidder.bidding_browser_signals->bid_count; i++) { |
| interest_group_manager_->RecordInterestGroupBids( |
| {blink::InterestGroupKey(bidder.interest_group.owner, |
| bidder.interest_group.name)}); |
| } |
| for (const auto& prev_win : bidder.bidding_browser_signals->prev_wins) { |
| interest_group_manager_->RecordInterestGroupWin( |
| InterestGroupKey(bidder.interest_group.owner, |
| bidder.interest_group.name), |
| prev_win->ad_json); |
| // Add some time between interest group wins, so that they'll be added |
| // to the database in the order they appear. Their times will *not* |
| // match those in `prev_wins`. |
| task_environment()->FastForwardBy(base::Seconds(1)); |
| } |
| |
| for (const auto& kanon_data : bidder.bidding_ads_kanon) { |
| interest_group_manager_->UpdateKAnonymity(kanon_data); |
| } |
| for (const auto& kanon_data : bidder.component_ads_kanon) { |
| interest_group_manager_->UpdateKAnonymity(kanon_data); |
| } |
| for (const auto& kanon_data : bidder.reporting_ads_kanon) { |
| interest_group_manager_->UpdateKAnonymity(kanon_data); |
| } |
| } |
| |
| interest_group_manager_->ClearLoggedData(); |
| |
| auction_run_loop_ = std::make_unique<base::RunLoop>(); |
| abortable_ad_auction_.reset(); |
| auction_runner_ = AuctionRunner::CreateAndStart( |
| auction_worklet_manager_.get(), interest_group_manager_.get(), |
| /*attribution_manager=*/nullptr, &private_aggregation_manager_, |
| private_aggregation_manager_.GetLogPrivateAggregationRequestsCallback(), |
| std::move(auction_config), top_frame_origin_, frame_origin_, |
| GetClientSecurityState(), dummy_report_shared_url_loader_factory_, |
| IsInterestGroupApiAllowedCallback(), |
| abortable_ad_auction_.BindNewPipeAndPassReceiver(), |
| base::BindOnce(&AuctionRunnerTest::OnAuctionComplete, |
| base::Unretained(this))); |
| } |
| |
| void RunAuctionAndWait(const GURL& seller_decision_logic_url, |
| std::vector<StorageInterestGroup> bidders) { |
| StartAuction(seller_decision_logic_url, std::move(bidders)); |
| auction_run_loop_->Run(); |
| } |
| |
| void OnAuctionComplete( |
| AuctionRunner* auction_runner, |
| bool manually_aborted, |
| absl::optional<InterestGroupKey> winning_group_key, |
| absl::optional<blink::AdDescriptor> ad_descriptor, |
| std::vector<blink::AdDescriptor> ad_component_descriptors, |
| std::vector<std::string> errors, |
| std::unique_ptr<InterestGroupAuctionReporter> reporter) { |
| DCHECK(auction_run_loop_); |
| DCHECK(!auction_complete_); |
| DCHECK_EQ(auction_runner, auction_runner_.get()); |
| |
| // Delete the auction runner, which is needed to update histograms. Don't do |
| // it immediately, so the Reporter is started before its destruction, |
| // allowing reuse of the seller worklet, just as happens in production. |
| std::unique_ptr<AuctionRunner> owned_auction_runner; |
| if (!dont_reset_auction_runner_) |
| owned_auction_runner = std::move(auction_runner_); |
| |
| auction_complete_ = true; |
| result_.manually_aborted = manually_aborted; |
| result_.winning_group_id = std::move(winning_group_key); |
| result_.ad_descriptor = std::move(ad_descriptor); |
| result_.ad_component_descriptors = std::move(ad_component_descriptors); |
| result_.winning_group_ad_metadata.clear(); |
| result_.report_urls.clear(); |
| result_.errors = std::move(errors); |
| result_.debug_loss_report_urls.clear(); |
| result_.debug_win_report_urls.clear(); |
| result_.ad_beacon_map.clear(); |
| result_.interest_groups_that_bid.clear(); |
| |
| if (!reporter) { |
| result_.debug_loss_report_urls = |
| interest_group_manager_->TakeReportUrlsOfType( |
| InterestGroupManagerImpl::ReportType::kDebugLoss); |
| result_.interest_groups_that_bid = |
| interest_group_manager_->TakeInterestGroupsThatBid(); |
| |
| // There should be no reports of any other type queued. |
| interest_group_manager_->ExpectReports({}); |
| |
| EXPECT_FALSE(result_.winning_group_id); |
| EXPECT_FALSE(result_.ad_descriptor); |
| EXPECT_TRUE(result_.ad_component_descriptors.empty()); |
| auction_run_loop_->Quit(); |
| return; |
| } |
| |
| // No reports should have been queued yet, on success. |
| interest_group_manager_->ExpectReports({}); |
| |
| EXPECT_TRUE(result_.winning_group_id); |
| EXPECT_TRUE(result_.ad_descriptor); |
| |
| // These are handled by the reporter, in the case an auction has a winner, |
| // so they're only requested if the winning ad is used. |
| interest_group_manager_->ExpectReports({}); |
| EXPECT_TRUE(result_.debug_loss_report_urls.empty()); |
| |
| reporter_ = std::move(reporter); |
| |
| reporter_->Start(base::BindOnce(&AuctionRunnerTest::OnReportingComplete, |
| base::Unretained(this))); |
| // Invoke callback immediately, so as not to block reporter completion. |
| reporter_->OnNavigateToWinningAdCallback().Run(); |
| } |
| |
| void OnReportingComplete() { |
| DCHECK(reporter_); |
| result_.report_urls = interest_group_manager_->TakeReportUrlsOfType( |
| InterestGroupManagerImpl::ReportType::kSendReportTo); |
| result_.ad_beacon_map = |
| reporter_->fenced_frame_reporter()->GetAdBeaconMapForTesting(); |
| result_.debug_loss_report_urls = |
| interest_group_manager_->TakeReportUrlsOfType( |
| InterestGroupManagerImpl::ReportType::kDebugLoss); |
| result_.debug_win_report_urls = |
| interest_group_manager_->TakeReportUrlsOfType( |
| InterestGroupManagerImpl::ReportType::kDebugWin); |
| result_.private_aggregation_event_map = |
| reporter_->fenced_frame_reporter() |
| ->GetPrivateAggregationEventMapForTesting(); |
| result_.interest_groups_that_bid = |
| interest_group_manager_->TakeInterestGroupsThatBid(); |
| const auto& report_errors = reporter_->errors(); |
| result_.errors.insert(result_.errors.end(), report_errors.begin(), |
| report_errors.end()); |
| |
| reporter_.reset(); |
| |
| // Retrieve the winning interest group to extract the most recently added ad |
| // metadata. |
| interest_group_manager_->GetInterestGroup( |
| InterestGroupKey(result_.winning_group_id->owner, |
| result_.winning_group_id->name), |
| base::BindOnce( |
| &AuctionRunnerTest::OnInterestGroupRetrievedAfterReporterDone, |
| base::Unretained(this))); |
| } |
| |
| void OnInterestGroupRetrievedAfterReporterDone( |
| absl::optional<StorageInterestGroup> interest_group) { |
| EXPECT_TRUE(interest_group); |
| EXPECT_FALSE(interest_group->bidding_browser_signals->prev_wins.empty()); |
| base::Time most_recent_win_time; |
| // Find the most recent win and write its metadata to |
| // `winning_group_ad_metadata`. |
| for (const auto& prev_win : |
| interest_group->bidding_browser_signals->prev_wins) { |
| if (prev_win->time > most_recent_win_time) { |
| most_recent_win_time = prev_win->time; |
| result_.winning_group_ad_metadata = prev_win->ad_json; |
| } |
| } |
| auction_run_loop_->Quit(); |
| } |
| |
| // Returns the specified interest group. |
| absl::optional<StorageInterestGroup> GetInterestGroup( |
| const url::Origin& owner, |
| const std::string& name) { |
| return interest_group_manager_->BlockingGetInterestGroup(owner, name); |
| } |
| |
| StorageInterestGroup MakeInterestGroup( |
| url::Origin owner, |
| std::string name, |
| absl::optional<GURL> bidding_url, |
| absl::optional<GURL> trusted_bidding_signals_url, |
| std::vector<std::string> trusted_bidding_signals_keys, |
| absl::optional<GURL> ad_url, |
| absl::optional<std::vector<GURL>> ad_component_urls = absl::nullopt) { |
| absl::optional<std::vector<blink::InterestGroup::Ad>> ads; |
| // Give only kBidder1 an InterestGroupAd ad with non-empty metadata, to |
| // better test the `ad_metadata` output. |
| if (ad_url) { |
| ads.emplace(); |
| if (owner == kBidder1) { |
| ads->emplace_back(*ad_url, R"({"ads": true})"); |
| } else { |
| ads->emplace_back(*ad_url, absl::nullopt); |
| } |
| } |
| |
| absl::optional<std::vector<blink::InterestGroup::Ad>> ad_components; |
| if (ad_component_urls) { |
| ad_components.emplace(); |
| for (const GURL& ad_component_url : *ad_component_urls) |
| ad_components->emplace_back(ad_component_url, absl::nullopt); |
| } |
| |
| return MakeInterestGroup( |
| blink::TestInterestGroupBuilder(owner, name) |
| .SetExpiry(base::Time::Max()) |
| .SetPriority(1.0) |
| .SetBiddingUrl(bidding_url) |
| .SetTrustedBiddingSignalsUrl(trusted_bidding_signals_url) |
| .SetTrustedBiddingSignalsKeys({trusted_bidding_signals_keys}) |
| .SetAds(ads) |
| .SetAdComponents(ad_components) |
| .Build()); |
| } |
| |
| StorageInterestGroup MakeInterestGroup(blink::InterestGroup interest_group) { |
| // Create fake previous wins. The time of these wins is ignored, since the |
| // InterestGroupManager attaches the current time when logging a win. |
| std::vector<auction_worklet::mojom::PreviousWinPtr> previous_wins; |
| // Log a time that's before now, so that any new entry will have the largest |
| // time. |
| base::Time the_past = base::Time::Now() - base::Milliseconds(1); |
| previous_wins.push_back( |
| auction_worklet::mojom::PreviousWin::New(the_past, R"({"winner": 0})")); |
| previous_wins.push_back(auction_worklet::mojom::PreviousWin::New( |
| the_past, R"({"winner": -1})")); |
| previous_wins.push_back(auction_worklet::mojom::PreviousWin::New( |
| the_past, R"({"winner": -2})")); |
| |
| StorageInterestGroup storage_group; |
| storage_group.interest_group = std::move(interest_group); |
| storage_group.bidding_browser_signals = |
| auction_worklet::mojom::BiddingBrowserSignals::New( |
| 3, 5, std::move(previous_wins)); |
| storage_group.joining_origin = storage_group.interest_group.owner; |
| return storage_group; |
| } |
| |
| void StartStandardAuction(bool request_trusted_bidding_signals = true) { |
| std::vector<StorageInterestGroup> bidders; |
| absl::optional<GURL> bidder1_signals_url; |
| absl::optional<GURL> bidder2_signals_url; |
| if (request_trusted_bidding_signals) { |
| bidder1_signals_url = kBidder1TrustedSignalsUrl; |
| bidder2_signals_url = kBidder2TrustedSignalsUrl; |
| } |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, kBidder1Name, kBidder1Url, std::move(bidder1_signals_url), |
| {"k1", "k2"}, GURL("https://ad1.com"), |
| std::vector<GURL>{GURL("https://ad1.com-component1.com"), |
| GURL("https://ad1.com-component2.com")})); |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder2, kBidder2Name, kBidder2Url, std::move(bidder2_signals_url), |
| {"l1", "l2"}, GURL("https://ad2.com"), |
| std::vector<GURL>{GURL("https://ad2.com-component1.com"), |
| GURL("https://ad2.com-component2.com")})); |
| |
| StartAuction(kSellerUrl, std::move(bidders)); |
| } |
| |
| void RunStandardAuction(bool request_trusted_bidding_signals = true) { |
| StartStandardAuction(request_trusted_bidding_signals); |
| auction_run_loop_->Run(); |
| } |
| |
| // Starts the standard auction with the mock worklet service, and waits for |
| // the service to receive the worklet construction calls. |
| // |
| // `num_expected_bidder_worklets` is the number of bidder worklets that are |
| // expected to be created. |
| void StartStandardAuctionWithMockService( |
| int num_expected_bidder_worklets = 2) { |
| UseMockWorkletService(); |
| StartStandardAuction(); |
| mock_auction_process_manager_->WaitForWorklets( |
| /*num_bidders=*/num_expected_bidder_worklets, |
| /*num_sellers=*/1 + component_auctions_.size()); |
| } |
| |
| // Runs an auction that exercises the extended private aggregation buyers |
| // metrics. Uses a mock service to control the metrics returned from bidder |
| // worklets. |
| // |
| // `bidders` All interest groups that will bid in the auction. |
| // |
| // `trusted_fetch_latency` For each interest group, the trusted fetch |
| // duration that should be returned from the mock worklet. |
| // |
| // `bidding_latency` For each interest group, the bidding duration that |
| // should be returned from the mock worklet. |
| // |
| // `should_bid` For each worklet, true indicates that a bid should be |
| // generated, and false indicates that a null bid should be returned. |
| // |
| // NOTE: This method isn't compatible with auction limits like |
| // `all_buyers_group_limit_`, since interest group load order isn't |
| // deterministic -- instead, use a non-mock based runner like |
| // RunAuctionAndWait(). |
| void RunExtendedPABuyersAuction( |
| const std::vector<StorageInterestGroup>& bidders, |
| const std::vector<base::TimeDelta> trusted_fetch_latency, |
| const std::vector<base::TimeDelta> bidding_latency = {base::TimeDelta()}, |
| const std::vector<bool> should_bid = {true}) { |
| ASSERT_EQ(bidders.size(), trusted_fetch_latency.size()); |
| ASSERT_EQ(bidders.size(), bidding_latency.size()); |
| ASSERT_EQ(bidders.size(), should_bid.size()); |
| UseMockWorkletService(); |
| StartAuction(kSellerUrl, bidders); |
| mock_auction_process_manager_->WaitForWorklets( |
| /*num_bidders=*/bidders.size(), /*num_sellers=*/1); |
| |
| auto seller_worklet = mock_auction_process_manager_->TakeSellerWorklet(); |
| ASSERT_TRUE(seller_worklet); |
| for (size_t i = 0; i < bidders.size(); i++) { |
| auto bidder_worklet = mock_auction_process_manager_->TakeBidderWorklet( |
| *bidders[i].interest_group.bidding_url); |
| ASSERT_TRUE(bidder_worklet); |
| |
| if (!trusted_fetch_latency[i].is_zero()) { |
| bidder_worklet->SetBidderTrustedSignalsFetchLatency( |
| trusted_fetch_latency[i]); |
| } |
| if (!bidding_latency[i].is_zero()) { |
| bidder_worklet->SetBiddingLatency(bidding_latency[i]); |
| } |
| bidder_worklet->InvokeGenerateBidCallback( |
| /*bid=*/should_bid[i] ? absl::make_optional(1) : absl::nullopt, |
| blink::AdDescriptor(GURL("https://ad1.com/"))); |
| if (should_bid[i]) { |
| auto score_ad_params = seller_worklet->WaitForScoreAd(); |
| mojo::Remote<auction_worklet::mojom::ScoreAdClient>( |
| std::move(score_ad_params.score_ad_client)) |
| ->OnScoreAdComplete( |
| /*score=*/1, |
| /*reject_reason=*/ |
| auction_worklet::mojom::RejectReason::kNotAvailable, |
| auction_worklet::mojom::ComponentAuctionModifiedBidParamsPtr(), |
| /*scoring_signals_data_version=*/0, |
| /*has_scoring_signals_data_version=*/false, |
| /*debug_loss_report_url=*/absl::nullopt, |
| /*debug_win_report_url=*/absl::nullopt, /*pa_requests=*/{}, |
| /*errors=*/{}); |
| } |
| } |
| |
| // Need to flush the service pipe to make sure the AuctionRunner has |
| // received the score. |
| seller_worklet->Flush(); |
| seller_worklet->WaitForReportResult(); |
| seller_worklet->InvokeReportResultCallback(); |
| mock_auction_process_manager_->WaitForWinningBidderReload(); |
| for (const StorageInterestGroup& bidder : bidders) { |
| auto bidder_worklet = mock_auction_process_manager_->TakeBidderWorklet( |
| *bidder.interest_group.bidding_url); |
| if (bidder_worklet) { |
| bidder_worklet->WaitForReportWin(); |
| bidder_worklet->InvokeReportWinCallback(); |
| } |
| } |
| |
| auction_run_loop_->Run(); |
| } |
| |
| // AuctionWorkletManager::Delegate implementation: |
| network::mojom::URLLoaderFactory* GetFrameURLLoaderFactory() override { |
| return &url_loader_factory_; |
| } |
| network::mojom::URLLoaderFactory* GetTrustedURLLoaderFactory() override { |
| return &url_loader_factory_; |
| } |
| void PreconnectSocket( |
| const GURL& url, |
| const net::NetworkAnonymizationKey& network_anonymization_key) override {} |
| RenderFrameHostImpl* GetFrame() override { |
| return static_cast<RenderFrameHostImpl*>( |
| web_contents()->GetPrimaryMainFrame()); |
| } |
| scoped_refptr<SiteInstance> GetFrameSiteInstance() override { |
| return scoped_refptr<SiteInstance>(); |
| } |
| network::mojom::ClientSecurityStatePtr GetClientSecurityState() override { |
| return network::mojom::ClientSecurityState::New(); |
| } |
| |
| // DebuggableAuctionWorkletTracker::Observer implementation |
| void AuctionWorkletCreated(DebuggableAuctionWorklet* worklet, |
| bool& should_pause_on_start) override { |
| should_pause_on_start = (worklet->url() == pause_worklet_url_); |
| observer_log_.push_back(base::StrCat({"Create ", worklet->url().spec()})); |
| title_log_.push_back(worklet->Title()); |
| } |
| |
| void AuctionWorkletDestroyed(DebuggableAuctionWorklet* worklet) override { |
| observer_log_.push_back(base::StrCat({"Destroy ", worklet->url().spec()})); |
| } |
| |
| // Gets script URLs of currently live DebuggableAuctionWorklet. |
| std::vector<std::string> LiveDebuggables() { |
| std::vector<std::string> result; |
| for (DebuggableAuctionWorklet* debuggable : |
| DebuggableAuctionWorkletTracker::GetInstance()->GetAll()) { |
| result.push_back(debuggable->url().spec()); |
| } |
| return result; |
| } |
| |
| // Enables use of a mock AuctionProcessManager when the next auction is run. |
| void UseMockWorkletService() { |
| auto mock_auction_process_manager = |
| std::make_unique<MockAuctionProcessManager>(); |
| // Any per buyer timeout in auction_config higher than 500 ms should be |
| // clamped to 500 ms by the AuctionRunner before being passed to |
| // GenerateBid(), and kBidder1's per buyer timeout is 1000 ms in |
| // auction_config so it should be 500 ms here. |
| mock_auction_process_manager->SetExpectedBuyerBidTimeout( |
| kBidder1Name, base::Milliseconds(500)); |
| mock_auction_process_manager->SetExpectedBuyerBidTimeout( |
| kBidder1NameAlt, base::Milliseconds(500)); |
| // Bidder 2's per buyer timeout should be 150 ms, since `auction_config's` |
| // `all_buyers_timeout` is set to 150 ms in all tests. |
| mock_auction_process_manager->SetExpectedBuyerBidTimeout( |
| kBidder2Name, base::Milliseconds(150)); |
| same_process_auction_process_manager_ = nullptr; |
| mock_auction_process_manager_ = mock_auction_process_manager.get(); |
| auction_process_manager_ = std::move(mock_auction_process_manager); |
| } |
| |
| // Check histogram values. If `expected_interest_groups` or `expected_owners` |
| // is null, expect the auction to be aborted before the corresponding |
| // histograms are recorded. |
| void CheckHistograms(InterestGroupAuction::AuctionResult expected_result, |
| absl::optional<int> expected_interest_groups, |
| absl::optional<int> expected_owners, |
| absl::optional<int> expected_sellers) { |
| histogram_tester_->ExpectUniqueSample("Ads.InterestGroup.Auction.Result", |
| expected_result, 1); |
| |
| if (expected_interest_groups.has_value()) { |
| histogram_tester_->ExpectUniqueSample( |
| "Ads.InterestGroup.Auction.NumInterestGroups", |
| *expected_interest_groups, 1); |
| } else { |
| histogram_tester_->ExpectTotalCount( |
| "Ads.InterestGroup.Auction.NumInterestGroups", 0); |
| } |
| |
| if (expected_owners.has_value()) { |
| histogram_tester_->ExpectUniqueSample( |
| "Ads.InterestGroup.Auction.NumOwnersWithInterestGroups", |
| *expected_owners, 1); |
| } else { |
| histogram_tester_->ExpectTotalCount( |
| "Ads.InterestGroup.Auction.NumOwnersWithInterestGroups", 0); |
| } |
| |
| if (expected_sellers.has_value()) { |
| histogram_tester_->ExpectUniqueSample( |
| "Ads.InterestGroup.Auction.NumSellersWithBidders", *expected_sellers, |
| 1); |
| } else { |
| histogram_tester_->ExpectTotalCount( |
| "Ads.InterestGroup.Auction.NumSellersWithBidders", 0); |
| } |
| |
| histogram_tester_->ExpectTotalCount( |
| "Ads.InterestGroup.Auction.AbortTime", |
| expected_result == InterestGroupAuction::AuctionResult::kAborted); |
| histogram_tester_->ExpectTotalCount( |
| "Ads.InterestGroup.Auction.CompletedWithoutWinnerTime", |
| expected_result == InterestGroupAuction::AuctionResult::kNoBids || |
| expected_result == |
| InterestGroupAuction::AuctionResult::kAllBidsRejected); |
| histogram_tester_->ExpectTotalCount( |
| "Ads.InterestGroup.Auction.AuctionWithWinnerTime", |
| expected_result == InterestGroupAuction::AuctionResult::kSuccess); |
| } |
| |
| AuctionRunner::IsInterestGroupApiAllowedCallback |
| IsInterestGroupApiAllowedCallback() { |
| return base::BindRepeating(&AuctionRunnerTest::IsInterestGroupAPIAllowed, |
| base::Unretained(this)); |
| } |
| |
| bool IsInterestGroupAPIAllowed(ContentBrowserClient::InterestGroupApiOperation |
| interest_group_api_operation, |
| const url::Origin& origin) { |
| if (interest_group_api_operation == |
| ContentBrowserClient::InterestGroupApiOperation::kSell) { |
| return disallowed_sellers_.find(origin) == disallowed_sellers_.end(); |
| } |
| if (interest_group_api_operation == |
| ContentBrowserClient::InterestGroupApiOperation::kUpdate) { |
| // Force the auction runner to not issue post-auction interest group |
| // updates in this test environment; these are tested in other test |
| // environments. |
| return false; |
| } |
| DCHECK_EQ(ContentBrowserClient::InterestGroupApiOperation::kBuy, |
| interest_group_api_operation); |
| return disallowed_buyers_.find(origin) == disallowed_buyers_.end(); |
| } |
| |
| // Creates an auction with 1-2 component sellers and 2 bidders, and sets up |
| // `url_loader_factory_` to provide the standard responses needed to run the |
| // auction. `bidder1_seller` and `bidder2_seller` identify the seller whose |
| // auction each bidder is in, and must be either kComponentSeller1 or |
| // kComponentSeller2. kComponentSeller1 is always added to the auction, |
| // kComponentSeller2 is only added to the auction if one of the bidders uses |
| // it as a seller. |
| void SetUpComponentAuctionAndResponses( |
| const url::Origin& bidder1_seller, |
| const url::Origin& bidder2_seller, |
| bool bid_from_component_auction_wins, |
| bool report_post_auction_signals = false) { |
| interest_group_buyers_.emplace(); |
| std::vector<url::Origin> component1_buyers; |
| std::vector<url::Origin> component2_buyers; |
| |
| if (bidder1_seller == kComponentSeller1) { |
| component1_buyers.push_back(kBidder1); |
| } else if (bidder1_seller == kComponentSeller2) { |
| component2_buyers.push_back(kBidder1); |
| } else { |
| NOTREACHED(); |
| } |
| |
| if (bidder2_seller == kComponentSeller1) { |
| component1_buyers.push_back(kBidder2); |
| } else if (bidder2_seller == kComponentSeller2) { |
| component2_buyers.push_back(kBidder2); |
| } else { |
| NOTREACHED(); |
| } |
| |
| component_auctions_.emplace_back(CreateAuctionConfig( |
| kComponentSeller1Url, std::move(component1_buyers))); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kComponentSeller1Url, |
| MakeDecisionScript( |
| kComponentSeller1Url, |
| /*send_report_url=*/GURL("https://component1-report.test/"), |
| /*bid_from_component_auction_wins=*/false, |
| /*report_post_auction_signals=*/report_post_auction_signals)); |
| |
| if (!component2_buyers.empty()) { |
| component_auctions_.emplace_back(CreateAuctionConfig( |
| kComponentSeller2Url, std::move(component2_buyers))); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kComponentSeller2Url, |
| MakeDecisionScript( |
| kComponentSeller2Url, |
| /*send_report_url=*/GURL("https://component2-report.test/"), |
| /*bid_from_component_auction_wins=*/false, |
| /*report_post_auction_signals=*/report_post_auction_signals)); |
| } |
| |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder1Url, |
| MakeBidScript(bidder1_seller, "1", "https://ad1.com/", |
| /*num_ad_components=*/2, kBidder1, kBidder1Name, |
| /*has_signals=*/true, "k1", "a", |
| report_post_auction_signals)); |
| auction_worklet::AddBidderJsonResponse( |
| &url_loader_factory_, |
| GURL(kBidder1TrustedSignalsUrl.spec() + |
| "?hostname=publisher1.com&keys=k1,k2" |
| "&interestGroupNames=Ad+Platform"), |
| kBidder1SignalsJson); |
| |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder2Url, |
| MakeBidScript(bidder2_seller, "2", "https://ad2.com/", |
| /*num_ad_components=*/2, kBidder2, kBidder2Name, |
| /*has_signals=*/true, "l2", "b", |
| report_post_auction_signals)); |
| auction_worklet::AddBidderJsonResponse( |
| &url_loader_factory_, |
| GURL(kBidder2TrustedSignalsUrl.spec() + |
| "?hostname=publisher1.com&keys=l1,l2" |
| "&interestGroupNames=Another+Ad+Thing"), |
| kBidder2SignalsJson); |
| |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kSellerUrl, |
| MakeDecisionScript( |
| kSellerUrl, |
| /*send_report_url=*/GURL("https://reporting.example.com"), |
| bid_from_component_auction_wins, report_post_auction_signals)); |
| } |
| |
| // This is what an "empty" FLEDGE ad beacon map looks like. |
| const base::flat_map<blink::FencedFrame::ReportingDestination, |
| FencedFrameReporter::ReportingUrlMap> |
| kEmptyAdBeaconMap = { |
| {blink::FencedFrame::ReportingDestination::kSeller, {}}, |
| {blink::FencedFrame::ReportingDestination::kComponentSeller, {}}, |
| {blink::FencedFrame::ReportingDestination::kBuyer, {}}, |
| }; |
| |
| bool use_promise_for_seller_signals_ = false; |
| bool use_promise_for_auction_signals_ = false; |
| bool use_promise_for_per_buyer_signals_ = false; |
| bool use_promise_for_buyer_timeouts_ = false; |
| bool use_promise_for_buyer_cumulative_timeouts_ = false; |
| |
| // Unlike others, this is only test with promises at this level. |
| bool pass_promise_for_direct_from_seller_signals_ = false; |
| absl::optional<uint16_t> seller_experiment_group_id_; |
| absl::optional<uint16_t> all_buyer_experiment_group_id_; |
| std::map<url::Origin, uint16_t> per_buyer_experiment_group_id_; |
| uint16_t all_buyers_group_limit_ = std::numeric_limits<std::uint16_t>::max(); |
| absl::optional<base::flat_map<std::string, double>> |
| all_buyers_priority_signals_; |
| |
| absl::optional<std::vector<absl::uint128>> auction_report_buyer_keys_; |
| absl::optional<base::flat_map< |
| blink::AuctionConfig::NonSharedParams::BuyerReportType, |
| blink::AuctionConfig::NonSharedParams::AuctionReportBuyersConfig>> |
| auction_report_buyers_; |
| |
| const url::Origin top_frame_origin_ = |
| url::Origin::Create(GURL("https://publisher1.com")); |
| const url::Origin frame_origin_ = |
| url::Origin::Create(GURL("https://frame.origin.test")); |
| const GURL kSellerUrl{"https://adstuff.publisher1.com/auction.js"}; |
| const url::Origin kSeller = url::Origin::Create(kSellerUrl); |
| absl::optional<GURL> trusted_scoring_signals_url_; |
| |
| const GURL kComponentSeller1Url{"https://component.seller1.test/foo.js"}; |
| const url::Origin kComponentSeller1 = |
| url::Origin::Create(kComponentSeller1Url); |
| const GURL kComponentSeller2Url{"https://component.seller2.test/bar.js"}; |
| const url::Origin kComponentSeller2 = |
| url::Origin::Create(kComponentSeller2Url); |
| |
| const GURL kBidder1Url{"https://adplatform.com/offers.js"}; |
| const GURL kBidder1UrlAlt{"https://adplatform.com/offers_alt.js"}; |
| const url::Origin kBidder1 = url::Origin::Create(kBidder1Url); |
| const InterestGroupKey kBidder1Key{kBidder1, kBidder1Name}; |
| const GURL kBidder1TrustedSignalsUrl{"https://adplatform.com/signals1"}; |
| const base::TimeDelta kBidder1CumulativeTimeout = base::Milliseconds(12345); |
| |
| const GURL kBidder2Url{"https://anotheradthing.com/bids.js"}; |
| const url::Origin kBidder2 = url::Origin::Create(kBidder2Url); |
| const std::string kBidder2Name{"Another Ad Thing"}; |
| const InterestGroupKey kBidder2Key{kBidder2, kBidder2Name}; |
| const GURL kBidder2TrustedSignalsUrl{"https://anotheradthing.com/signals2"}; |
| |
| const base::TimeDelta kAllBuyersCumulativeTimeout = base::Milliseconds(23456); |
| |
| // Timeout tests can wait until this amount before a timeout, make sure |
| // nothing has happened, and then wait this amount, and check the timeout |
| // happened. |
| const base::TimeDelta kTinyTime = base::Milliseconds(1); |
| |
| absl::optional<std::vector<url::Origin>> interest_group_buyers_ = { |
| {kBidder1, kBidder2}}; |
| |
| std::vector<blink::AuctionConfig> component_auctions_; |
| |
| // Origins which are not allowed to take part in auctions, as the |
| // corresponding participant types. |
| std::set<url::Origin> disallowed_sellers_; |
| std::set<url::Origin> disallowed_buyers_; |
| |
| base::test::ScopedFeatureList scoped_feature_list_; |
| |
| // RunLoop that's quit on auction completion. |
| std::unique_ptr<base::RunLoop> auction_run_loop_; |
| // True if the most recently started auction has completed. |
| bool auction_complete_ = false; |
| // Result of the most recent auction. |
| Result result_; |
| |
| network::TestURLLoaderFactory url_loader_factory_; |
| |
| // ScopedURLLoaderFactory used for reports. The FencedFrameReporter is never |
| // told to send any reports in these tests, and reports sent directly through |
| // the InterestGroupManager are short-circuited by the |
| // TestInterestGroupManagerImpl before they make it over the network, so this |
| // is only used for equality checks around making sure the right factory is |
| // passed to it. |
| scoped_refptr<network::SharedURLLoaderFactory> |
| dummy_report_shared_url_loader_factory_ = |
| base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>( |
| nullptr); |
| |
| std::unique_ptr<AuctionWorkletManager> auction_worklet_manager_; |
| TestInterestGroupPrivateAggregationManager private_aggregation_manager_{ |
| top_frame_origin_}; |
| |
| // This is used (and consumed) when starting an auction, if non-null. Allows |
| // either using a MockAuctionProcessManager instead of a |
| // SameProcessAuctionProcessManager, or using a SameProcessAuctionProcessManager |
| // that has already vended processes. If nullptr, a new |
| // SameProcessAuctionProcessManager() is created when an auction is started. |
| std::unique_ptr<AuctionProcessManager> auction_process_manager_; |
| |
| // Set by UseMockWorkletService(). Non-owning reference to the |
| // AuctionProcessManager that will be / has been passed to the |
| // InterestGroupManager. |
| raw_ptr<MockAuctionProcessManager> mock_auction_process_manager_ = nullptr; |
| |
| // If StartAuction() created a SameProcessAuctionProcessManager for |
| // `auction_process_manager_`, this alises it. |
| // Reset by other things that set `auction_process_manager_`. |
| raw_ptr<SameProcessAuctionProcessManager> |
| same_process_auction_process_manager_ = nullptr; |
| |
| // The TestInterestGroupManager is recreated and repopulated for each auction. |
| std::unique_ptr<TestInterestGroupManagerImpl> interest_group_manager_; |
| |
| std::unique_ptr<AuctionRunner> auction_runner_; |
| std::unique_ptr<InterestGroupAuctionReporter> reporter_; |
| bool dont_reset_auction_runner_ = false; |
| // This should be inspected using TakeBadMessage(), which also clears it. |
| std::string bad_message_; |
| |
| std::unique_ptr<base::HistogramTester> histogram_tester_; |
| |
| std::vector<std::string> observer_log_; |
| std::vector<std::string> title_log_; |
| |
| // Can be used to interrupt currently running auction. |
| mojo::Remote<blink::mojom::AbortableAdAuction> abortable_ad_auction_; |
| |
| // Which worklet to pause, if any. |
| GURL pause_worklet_url_; |
| }; |
| |
| // Runs an auction with an empty buyers field. |
| TEST_F(AuctionRunnerTest, NullBuyers) { |
| interest_group_buyers_->clear(); |
| RunAuctionAndWait(kSellerUrl, std::vector<StorageInterestGroup>()); |
| |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| EXPECT_FALSE(result_.winning_group_id); |
| EXPECT_FALSE(result_.ad_descriptor); |
| EXPECT_TRUE(result_.ad_component_descriptors.empty()); |
| EXPECT_THAT(result_.report_urls, testing::UnorderedElementsAre()); |
| EXPECT_THAT(result_.interest_groups_that_bid, |
| testing::UnorderedElementsAre()); |
| CheckHistograms(InterestGroupAuction::AuctionResult::kNoInterestGroups, |
| /*expected_interest_groups=*/absl::nullopt, |
| /*expected_owners=*/absl::nullopt, |
| /*expected_sellers=*/absl::nullopt); |
| } |
| |
| // Runs a component auction with all buyers fields null. |
| TEST_F(AuctionRunnerTest, ComponentAuctionNullBuyers) { |
| interest_group_buyers_.reset(); |
| component_auctions_.emplace_back( |
| CreateAuctionConfig(kComponentSeller1Url, /*buyers=*/absl::nullopt)); |
| RunAuctionAndWait(kSellerUrl, std::vector<StorageInterestGroup>()); |
| |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| EXPECT_FALSE(result_.winning_group_id); |
| EXPECT_FALSE(result_.ad_descriptor); |
| EXPECT_TRUE(result_.ad_component_descriptors.empty()); |
| EXPECT_THAT(result_.report_urls, testing::UnorderedElementsAre()); |
| EXPECT_THAT(result_.interest_groups_that_bid, |
| testing::UnorderedElementsAre()); |
| CheckHistograms(InterestGroupAuction::AuctionResult::kNoInterestGroups, |
| /*expected_interest_groups=*/absl::nullopt, |
| /*expected_owners=*/absl::nullopt, |
| /*expected_sellers=*/absl::nullopt); |
| } |
| |
| // Runs an auction with an empty buyers field. |
| TEST_F(AuctionRunnerTest, EmptyBuyers) { |
| interest_group_buyers_->clear(); |
| RunAuctionAndWait(kSellerUrl, std::vector<StorageInterestGroup>()); |
| |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| EXPECT_FALSE(result_.winning_group_id); |
| EXPECT_FALSE(result_.ad_descriptor); |
| EXPECT_TRUE(result_.ad_component_descriptors.empty()); |
| EXPECT_THAT(result_.report_urls, testing::UnorderedElementsAre()); |
| EXPECT_THAT(result_.interest_groups_that_bid, |
| testing::UnorderedElementsAre()); |
| CheckHistograms(InterestGroupAuction::AuctionResult::kNoInterestGroups, |
| /*expected_interest_groups=*/absl::nullopt, |
| /*expected_owners=*/absl::nullopt, |
| /*expected_sellers=*/absl::nullopt); |
| } |
| |
| // Runs a component auction with all buyers fields empty. |
| TEST_F(AuctionRunnerTest, ComponentAuctionEmptyBuyers) { |
| interest_group_buyers_->clear(); |
| component_auctions_.emplace_back( |
| CreateAuctionConfig(kComponentSeller1Url, /*buyers=*/{})); |
| RunAuctionAndWait(kSellerUrl, std::vector<StorageInterestGroup>()); |
| |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| EXPECT_FALSE(result_.winning_group_id); |
| EXPECT_FALSE(result_.ad_descriptor); |
| EXPECT_TRUE(result_.ad_component_descriptors.empty()); |
| EXPECT_THAT(result_.report_urls, testing::UnorderedElementsAre()); |
| EXPECT_THAT(result_.interest_groups_that_bid, |
| testing::UnorderedElementsAre()); |
| CheckHistograms(InterestGroupAuction::AuctionResult::kNoInterestGroups, |
| /*expected_interest_groups=*/absl::nullopt, |
| /*expected_owners=*/absl::nullopt, |
| /*expected_sellers=*/absl::nullopt); |
| } |
| |
| // Runs the standard auction, but without adding any interest groups to the |
| // manager. |
| TEST_F(AuctionRunnerTest, NoInterestGroups) { |
| RunAuctionAndWait(kSellerUrl, std::vector<StorageInterestGroup>()); |
| |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| EXPECT_FALSE(result_.winning_group_id); |
| EXPECT_FALSE(result_.ad_descriptor); |
| EXPECT_TRUE(result_.ad_component_descriptors.empty()); |
| EXPECT_THAT(result_.report_urls, testing::UnorderedElementsAre()); |
| EXPECT_THAT(result_.interest_groups_that_bid, |
| testing::UnorderedElementsAre()); |
| CheckHistograms(InterestGroupAuction::AuctionResult::kNoInterestGroups, |
| /*expected_interest_groups=*/0, /*expected_owners=*/0, |
| /*expected_sellers=*/0); |
| } |
| |
| // Runs a component auction, but without adding any interest groups to the |
| // manager. |
| TEST_F(AuctionRunnerTest, ComponentAuctionNoInterestGroups) { |
| interest_group_buyers_->clear(); |
| component_auctions_.emplace_back( |
| CreateAuctionConfig(kComponentSeller1Url, {{kBidder1}})); |
| component_auctions_.emplace_back( |
| CreateAuctionConfig(kComponentSeller2Url, {{kBidder2}})); |
| RunAuctionAndWait(kSellerUrl, std::vector<StorageInterestGroup>()); |
| |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| EXPECT_FALSE(result_.winning_group_id); |
| EXPECT_FALSE(result_.ad_descriptor); |
| EXPECT_TRUE(result_.ad_component_descriptors.empty()); |
| EXPECT_THAT(result_.report_urls, testing::UnorderedElementsAre()); |
| EXPECT_THAT(result_.interest_groups_that_bid, |
| testing::UnorderedElementsAre()); |
| CheckHistograms(InterestGroupAuction::AuctionResult::kNoInterestGroups, |
| /*expected_interest_groups=*/0, /*expected_owners=*/0, |
| /*expected_sellers=*/0); |
| } |
| |
| // Runs an standard auction, but with an interest group that does not list any |
| // ads. |
| TEST_F(AuctionRunnerTest, OneInterestGroupNoAds) { |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, kBidder1Name, kBidder1Url, kBidder1TrustedSignalsUrl, |
| {"k1", "k2"}, /*ad_url=*/absl::nullopt)); |
| |
| RunAuctionAndWait(kSellerUrl, std::move(bidders)); |
| |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| EXPECT_FALSE(result_.winning_group_id); |
| EXPECT_FALSE(result_.ad_descriptor); |
| EXPECT_TRUE(result_.ad_component_descriptors.empty()); |
| EXPECT_THAT(result_.report_urls, testing::UnorderedElementsAre()); |
| EXPECT_THAT(result_.interest_groups_that_bid, |
| testing::UnorderedElementsAre()); |
| CheckHistograms(InterestGroupAuction::AuctionResult::kNoInterestGroups, |
| /*expected_interest_groups=*/0, /*expected_owners=*/0, |
| /*expected_sellers=*/0); |
| } |
| |
| // Runs an auction with one component that has a buyer with an interest group, |
| // but that group has no ads. |
| TEST_F(AuctionRunnerTest, ComponentAuctionOneInterestGroupNoAds) { |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, kBidder1Name, kBidder1Url, kBidder1TrustedSignalsUrl, |
| {"k1", "k2"}, /*ad_url=*/absl::nullopt)); |
| |
| RunAuctionAndWait(kSellerUrl, std::move(bidders)); |
| |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| EXPECT_FALSE(result_.winning_group_id); |
| EXPECT_FALSE(result_.ad_descriptor); |
| EXPECT_TRUE(result_.ad_component_descriptors.empty()); |
| EXPECT_THAT(result_.report_urls, testing::UnorderedElementsAre()); |
| EXPECT_THAT(result_.interest_groups_that_bid, |
| testing::UnorderedElementsAre()); |
| CheckHistograms(InterestGroupAuction::AuctionResult::kNoInterestGroups, |
| /*expected_interest_groups=*/0, /*expected_owners=*/0, |
| /*expected_sellers=*/0); |
| } |
| |
| // Runs an standard auction, but with an interest group that does not list a |
| // bidding script. |
| TEST_F(AuctionRunnerTest, OneInterestGroupNoBidScript) { |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, kBidder1Name, /*bidding_url=*/absl::nullopt, |
| kBidder1TrustedSignalsUrl, {"k1", "k2"}, GURL("https://ad1.com"))); |
| |
| RunAuctionAndWait(kSellerUrl, std::move(bidders)); |
| |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| EXPECT_FALSE(result_.winning_group_id); |
| EXPECT_FALSE(result_.ad_descriptor); |
| EXPECT_TRUE(result_.ad_component_descriptors.empty()); |
| EXPECT_THAT(result_.report_urls, testing::UnorderedElementsAre()); |
| EXPECT_THAT(result_.interest_groups_that_bid, |
| testing::UnorderedElementsAre()); |
| CheckHistograms(InterestGroupAuction::AuctionResult::kNoInterestGroups, |
| /*expected_interest_groups=*/0, /*expected_owners=*/0, |
| /*expected_sellers=*/0); |
| } |
| |
| // Runs the standard auction, but with only adding one of the two standard |
| // interest groups to the manager. |
| TEST_F(AuctionRunnerTest, OneInterestGroup) { |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder1Url, |
| MakeBidScript(kSeller, "1", "https://ad1.com/", /*num_ad_components=*/0, |
| kBidder1, kBidder1Name, |
| /*has_signals=*/true, "k1", "a", |
| /*report_post_auction_signals=*/true)); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kSellerUrl, |
| MakeAuctionScript(/*report_post_auction_signals=*/true)); |
| auction_worklet::AddBidderJsonResponse( |
| &url_loader_factory_, |
| GURL(kBidder1TrustedSignalsUrl.spec() + |
| "?hostname=publisher1.com&keys=k1,k2" |
| "&interestGroupNames=Ad+Platform"), |
| kBidder1SignalsJson); |
| |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, kBidder1Name, kBidder1Url, kBidder1TrustedSignalsUrl, |
| {"k1", "k2"}, GURL("https://ad1.com"))); |
| |
| RunAuctionAndWait(kSellerUrl, std::move(bidders)); |
| |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| EXPECT_EQ(kBidder1Key, result_.winning_group_id); |
| EXPECT_EQ(GURL("https://ad1.com/"), result_.ad_descriptor->url); |
| EXPECT_TRUE(result_.ad_component_descriptors.empty()); |
| EXPECT_THAT( |
| result_.report_urls, |
| testing::UnorderedElementsAre( |
| GURL("https://reporting.example.com/?highestScoringOtherBid=0&bid=1"), |
| ReportWinUrl(/*bid=*/1, /*highest_scoring_other_bid=*/0, |
| /*made_highest_scoring_other_bid=*/false))); |
| EXPECT_THAT(result_.interest_groups_that_bid, |
| testing::UnorderedElementsAre(kBidder1Key)); |
| EXPECT_EQ(R"({"render_url":"https://ad1.com/","metadata":{"ads": true}})", |
| result_.winning_group_ad_metadata); |
| CheckHistograms(InterestGroupAuction::AuctionResult::kSuccess, |
| /*expected_interest_groups=*/1, /*expected_owners=*/1, |
| /*expected_sellers=*/1); |
| EXPECT_THAT(observer_log_, |
| testing::UnorderedElementsAre( |
| "Create https://adstuff.publisher1.com/auction.js", |
| "Create https://adplatform.com/offers.js", |
| "Destroy https://adplatform.com/offers.js", |
| "Destroy https://adstuff.publisher1.com/auction.js", |
| "Create https://adplatform.com/offers.js", |
| "Destroy https://adplatform.com/offers.js")); |
| } |
| |
| // An auction specifying buyer and seller experiment IDs. |
| TEST_F(AuctionRunnerTest, ExperimentId) { |
| trusted_scoring_signals_url_ = |
| GURL("https://adstuff.publisher1.com/seller_signals"); |
| seller_experiment_group_id_ = 498u; |
| all_buyer_experiment_group_id_ = 940u; |
| |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder1Url, |
| MakeBidScript(kSeller, "1", "https://ad1.com/", /*num_ad_components=*/0, |
| kBidder1, kBidder1Name, |
| /*has_signals=*/true, "k1", "a")); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| MakeAuctionScript()); |
| auction_worklet::AddBidderJsonResponse( |
| &url_loader_factory_, |
| GURL(kBidder1TrustedSignalsUrl.spec() + |
| "?hostname=publisher1.com&keys=k1,k2" |
| "&interestGroupNames=Ad+Platform" |
| "&experimentGroupId=940"), |
| kBidder1SignalsJson); |
| auction_worklet::AddJsonResponse( |
| &url_loader_factory_, |
| GURL(trusted_scoring_signals_url_->spec() + |
| "?hostname=publisher1.com&renderUrls=https%3A%2F%2Fad1.com%2F" + |
| "&experimentGroupId=498"), |
| R"({"renderUrls":{"https://ad1.com/":"accept", |
| "https://ad2.com/":"reject"}} |
| )"); |
| |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, kBidder1Name, kBidder1Url, kBidder1TrustedSignalsUrl, |
| {"k1", "k2"}, GURL("https://ad1.com"))); |
| |
| RunAuctionAndWait(kSellerUrl, std::move(bidders)); |
| |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| EXPECT_EQ(GURL("https://ad1.com/"), result_.ad_descriptor->url); |
| } |
| |
| // An auction specifying a per-buyer experiment ID as well as fallback all-buyer |
| // experiment id. |
| TEST_F(AuctionRunnerTest, ExperimentIdPerBuyer) { |
| all_buyer_experiment_group_id_ = 940u; |
| per_buyer_experiment_group_id_[kBidder2] = 93u; |
| |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder1Url, |
| MakeBidScript(kSeller, "1", "https://ad1.com/", /*num_ad_components=*/0, |
| kBidder1, kBidder1Name, |
| /*has_signals=*/true, "k1", "a")); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder2Url, |
| MakeBidScript(kSeller, "2", "https://ad2.com/", /*num_ad_components=*/0, |
| kBidder2, kBidder2Name, |
| /*has_signals=*/true, "l2", "b")); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| MakeAuctionScript()); |
| auction_worklet::AddBidderJsonResponse( |
| &url_loader_factory_, |
| GURL(kBidder1TrustedSignalsUrl.spec() + |
| "?hostname=publisher1.com&keys=k1,k2" |
| "&interestGroupNames=Ad+Platform" |
| "&experimentGroupId=940"), |
| kBidder1SignalsJson); |
| auction_worklet::AddBidderJsonResponse( |
| &url_loader_factory_, |
| GURL(kBidder2TrustedSignalsUrl.spec() + |
| "?hostname=publisher1.com&keys=l1,l2" |
| "&interestGroupNames=Another+Ad+Thing" |
| "&experimentGroupId=93"), |
| kBidder2SignalsJson); |
| |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, kBidder1Name, kBidder1Url, kBidder1TrustedSignalsUrl, |
| {"k1", "k2"}, GURL("https://ad1.com"))); |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder2, kBidder2Name, kBidder2Url, kBidder2TrustedSignalsUrl, |
| {"l1", "l2"}, GURL("https://ad2.com"))); |
| |
| RunAuctionAndWait(kSellerUrl, std::move(bidders)); |
| |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| EXPECT_EQ(GURL("https://ad2.com/"), result_.ad_descriptor->url); |
| } |
| |
| // An auction with two successful bids. |
| TEST_F(AuctionRunnerTest, Basic) { |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder1Url, |
| MakeBidScript(kSeller, "1", "https://ad1.com/", /*num_ad_components=*/2, |
| kBidder1, kBidder1Name, |
| /*has_signals=*/true, "k1", "a", |
| /*report_post_auction_signals=*/true)); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder2Url, |
| MakeBidScript(kSeller, "2", "https://ad2.com/", /*num_ad_components=*/2, |
| kBidder2, kBidder2Name, |
| /*has_signals=*/true, "l2", "b", |
| /*report_post_auction_signals=*/true)); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kSellerUrl, |
| MakeAuctionScript(/*report_post_auction_signals=*/true)); |
| auction_worklet::AddBidderJsonResponse( |
| &url_loader_factory_, |
| GURL(kBidder1TrustedSignalsUrl.spec() + |
| "?hostname=publisher1.com&keys=k1,k2" |
| "&interestGroupNames=Ad+Platform"), |
| kBidder1SignalsJson); |
| auction_worklet::AddBidderJsonResponse( |
| &url_loader_factory_, |
| GURL(kBidder2TrustedSignalsUrl.spec() + |
| "?hostname=publisher1.com&keys=l1,l2" |
| "&interestGroupNames=Another+Ad+Thing"), |
| kBidder2SignalsJson); |
| |
| RunStandardAuction(); |
| EXPECT_FALSE(result_.manually_aborted); |
| EXPECT_EQ(kBidder2Key, result_.winning_group_id); |
| EXPECT_EQ(GURL("https://ad2.com/"), result_.ad_descriptor->url); |
| EXPECT_EQ(std::vector<blink::AdDescriptor>{blink::AdDescriptor( |
| GURL("https://ad2.com-component1.com"))}, |
| result_.ad_component_descriptors); |
| EXPECT_THAT( |
| result_.report_urls, |
| testing::UnorderedElementsAre( |
| GURL("https://reporting.example.com/?highestScoringOtherBid=1&bid=2"), |
| ReportWinUrl(/*bid=*/2, /*highest_scoring_other_bid=*/1, |
| /*made_highest_scoring_other_bid=*/false))); |
| EXPECT_THAT( |
| result_.ad_beacon_map, |
| testing::UnorderedElementsAre( |
| testing::Pair(ReportingDestination::kSeller, |
| testing::ElementsAre(testing::Pair( |
| "click", GURL("https://reporting.example.com/4")))), |
| testing::Pair(ReportingDestination::kComponentSeller, |
| testing::ElementsAre()), |
| testing::Pair( |
| ReportingDestination::kBuyer, |
| testing::ElementsAre(testing::Pair( |
| "click", GURL("https://buyer-reporting.example.com/4")))))); |
| |
| EXPECT_THAT( |
| private_aggregation_manager_.TakePrivateAggregationRequests(), |
| testing::UnorderedElementsAre( |
| testing::Pair(kBidder1, |
| ElementsAreRequests( |
| kExpectedGenerateBidPrivateAggregationRequest)), |
| testing::Pair( |
| kBidder2, |
| ElementsAreRequests(kExpectedGenerateBidPrivateAggregationRequest, |
| kExpectedReportWinPrivateAggregationRequest)), |
| testing::Pair(kSeller, |
| ElementsAreRequests( |
| kExpectedScoreAdPrivateAggregationRequest, |
| kExpectedScoreAdPrivateAggregationRequest, |
| kExpectedReportResultPrivateAggregationRequest)))); |
| |
| EXPECT_THAT( |
| private_aggregation_manager_.TakeLoggedPrivateAggregationRequests(), |
| ElementsAreRequests( |
| kExpectedGenerateBidPrivateAggregationRequest, |
| kExpectedGenerateBidPrivateAggregationRequest, |
| kExpectedReportWinPrivateAggregationRequest, |
| kExpectedScoreAdPrivateAggregationRequest, |
| kExpectedScoreAdPrivateAggregationRequest, |
| kExpectedReportResultPrivateAggregationRequest, |
| BuildPrivateAggregationForEventRequest(/*bucket=*/10, /*value=*/21, |
| /*event_type=*/"click"), |
| BuildPrivateAggregationForEventRequest(/*bucket=*/10, /*value=*/22, |
| /*event_type=*/"click"), |
| BuildPrivateAggregationForEventRequest(/*bucket=*/30, /*value=*/42, |
| /*event_type=*/"click"), |
| BuildPrivateAggregationForEventRequest(/*bucket=*/50, /*value=*/60, |
| /*event_type=*/"click"), |
| BuildPrivateAggregationForEventRequest(/*bucket=*/50, /*value=*/60, |
| /*event_type=*/"click"), |
| BuildPrivateAggregationForEventRequest(/*bucket=*/70, /*value=*/80, |
| /*event_type=*/"click"))); |
| |
| EXPECT_THAT(result_.private_aggregation_event_map, |
| testing::UnorderedElementsAre(testing::Pair( |
| "click", ElementsAreRequests( |
| BuildPrivateAggregationRequest(/*bucket=*/10, |
| /*value=*/22), |
| BuildPrivateAggregationRequest(/*bucket=*/30, |
| /*value=*/42))))); |
| |
| EXPECT_THAT(result_.interest_groups_that_bid, |
| testing::UnorderedElementsAre(kBidder1Key, kBidder2Key)); |
| EXPECT_EQ(R"({"render_url":"https://ad2.com/"})", |
| result_.winning_group_ad_metadata); |
| EXPECT_TRUE(result_.errors.empty()); |
| CheckHistograms(InterestGroupAuction::AuctionResult::kSuccess, |
| /*expected_interest_groups=*/2, |
| /*expected_owners=*/2, |
| /*expected_sellers=*/1); |
| EXPECT_THAT(observer_log_, |
| testing::UnorderedElementsAre( |
| "Create https://adstuff.publisher1.com/auction.js", |
| "Create https://adplatform.com/offers.js", |
| "Create https://anotheradthing.com/bids.js", |
| "Destroy https://adplatform.com/offers.js", |
| "Destroy https://anotheradthing.com/bids.js", |
| "Destroy https://adstuff.publisher1.com/auction.js", |
| "Create https://anotheradthing.com/bids.js", |
| "Destroy https://anotheradthing.com/bids.js")); |
| EXPECT_THAT( |
| title_log_, |
| testing::UnorderedElementsAre( |
| "FLEDGE seller worklet for https://adstuff.publisher1.com/auction.js", |
| "FLEDGE bidder worklet for https://adplatform.com/offers.js", |
| "FLEDGE bidder worklet for https://anotheradthing.com/bids.js", |
| "FLEDGE bidder worklet for https://anotheradthing.com/bids.js")); |
| } |
| |
| TEST_F(AuctionRunnerTest, BasicDebug) { |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder1Url, |
| MakeBidScript(kSeller, "1", "https://ad1.com/", /*num_ad_components=*/2, |
| kBidder1, kBidder1Name, /*has_signals=*/true, "k1", "a")); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder2Url, |
| MakeBidScript(kSeller, "2", "https://ad2.com/", /*num_ad_components=*/2, |
| kBidder2, kBidder2Name, /*has_signals=*/true, "l2", "b")); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| MakeAuctionScript()); |
| auction_worklet::AddBidderJsonResponse( |
| &url_loader_factory_, |
| GURL(kBidder1TrustedSignalsUrl.spec() + |
| "?hostname=publisher1.com&keys=k1,k2" |
| "&interestGroupNames=Ad+Platform"), |
| kBidder1SignalsJson); |
| auction_worklet::AddBidderJsonResponse( |
| &url_loader_factory_, |
| GURL(kBidder2TrustedSignalsUrl.spec() + |
| "?hostname=publisher1.com&keys=l1,l2" |
| "&interestGroupNames=Another+Ad+Thing"), |
| kBidder2SignalsJson); |
| |
| for (const GURL& debug_url : {kBidder1Url, kBidder2Url, kSellerUrl}) { |
| SCOPED_TRACE(debug_url); |
| pause_worklet_url_ = debug_url; |
| |
| // Seller breakpoint is expected to hit twice. |
| int expected_hits = (debug_url == kSellerUrl ? 2 : 1); |
| |
| StartStandardAuction(); |
| task_environment()->RunUntilIdle(); |
| |
| bool found = false; |
| mojo::AssociatedRemote<blink::mojom::DevToolsAgent> agent; |
| |
| for (DebuggableAuctionWorklet* debuggable : |
| DebuggableAuctionWorkletTracker::GetInstance()->GetAll()) { |
| if (debuggable->url() == debug_url) { |
| found = true; |
| debuggable->ConnectDevToolsAgent( |
| agent.BindNewEndpointAndPassReceiver()); |
| } |
| } |
| ASSERT_TRUE(found); |
| |
| TestDevToolsAgentClient debug(std::move(agent), "S1", |
| /*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 a breakpoint, and let the worklet run. |
| const char kBreakpointTemplate[] = R"({ |
| "id":3, |
| "method":"Debugger.setBreakpointByUrl", |
| "params": { |
| "lineNumber": 11, |
| "url": "%s", |
| "columnNumber": 0, |
| "condition": "" |
| }})"; |
| |
| debug.RunCommandAndWaitForResult( |
| TestDevToolsAgentClient::Channel::kMain, 3, |
| "Debugger.setBreakpointByUrl", |
| base::StringPrintf(kBreakpointTemplate, debug_url.spec().c_str())); |
| debug.RunCommandAndWaitForResult( |
| TestDevToolsAgentClient::Channel::kMain, 4, |
| "Runtime.runIfWaitingForDebugger", |
| R"({"id":4,"method":"Runtime.runIfWaitingForDebugger","params":{}})"); |
| |
| // Should get breakpoint hit eventually. |
| for (int hit = 0; hit < expected_hits; ++hit) { |
| TestDevToolsAgentClient::Event breakpoint_hit = |
| debug.WaitForMethodNotification("Debugger.paused"); |
| |
| ASSERT_TRUE(breakpoint_hit.value.is_dict()); |
| base::Value::List* hit_breakpoints = |
| breakpoint_hit.value.GetDict().FindListByDottedPath( |
| "params.hitBreakpoints"); |
| ASSERT_TRUE(hit_breakpoints); |
| // This is LE and not EQ to work around |
| // https://bugs.chromium.org/p/v8/issues/detail?id=12586 |
| ASSERT_LE(1u, hit_breakpoints->size()); |
| ASSERT_TRUE((*hit_breakpoints)[0].is_string()); |
| EXPECT_EQ(base::StringPrintf("1:11:0:%s", debug_url.spec().c_str()), |
| (*hit_breakpoints)[0].GetString()); |
| |
| // Just resume execution. |
| debug.RunCommandAndWaitForResult( |
| TestDevToolsAgentClient::Channel::kIO, 6, "Debugger.resume", |
| R"({"id":6,"method":"Debugger.resume","params":{}})"); |
| } |
| |
| // In the case bidder 2 wins the auction, the script will be reloaded, and |
| // the second time it's loaded the worklet will also start in the paused |
| // state. Resume it, so the test doesn't hang. |
| if (debug_url == kBidder2Url) { |
| task_environment()->RunUntilIdle(); |
| found = false; |
| mojo::AssociatedRemote<blink::mojom::DevToolsAgent> agent2; |
| for (DebuggableAuctionWorklet* debuggable : |
| DebuggableAuctionWorkletTracker::GetInstance()->GetAll()) { |
| if (debuggable->url() == debug_url) { |
| found = true; |
| debuggable->ConnectDevToolsAgent( |
| agent2.BindNewEndpointAndPassReceiver()); |
| } |
| } |
| ASSERT_TRUE(found); |
| |
| TestDevToolsAgentClient debug2(std::move(agent2), "S2", |
| /*use_binary_protocol=*/true); |
| |
| debug2.RunCommandAndWaitForResult( |
| TestDevToolsAgentClient::Channel::kMain, 1, |
| "Runtime.runIfWaitingForDebugger", |
| R"({"id":1,"method":"Runtime.runIfWaitingForDebugger","params":{}})"); |
| } |
| |
| // Let it finish --- result should as in Basic test since this didn't |
| // actually change anything. |
| auction_run_loop_->Run(); |
| EXPECT_EQ(kBidder2Key, result_.winning_group_id); |
| EXPECT_EQ(GURL("https://ad2.com/"), result_.ad_descriptor->url); |
| EXPECT_THAT(result_.report_urls, |
| testing::UnorderedElementsAre( |
| GURL("https://reporting.example.com/2"), |
| GURL("https://buyer-reporting.example.com/2"))); |
| EXPECT_THAT( |
| result_.ad_beacon_map, |
| testing::UnorderedElementsAre( |
| testing::Pair( |
| ReportingDestination::kSeller, |
| testing::ElementsAre(testing::Pair( |
| "click", GURL("https://reporting.example.com/4")))), |
| testing::Pair(ReportingDestination::kComponentSeller, |
| testing::ElementsAre()), |
| testing::Pair( |
| ReportingDestination::kBuyer, |
| testing::ElementsAre(testing::Pair( |
| "click", GURL("https://buyer-reporting.example.com/4")))))); |
| EXPECT_THAT( |
| private_aggregation_manager_.TakePrivateAggregationRequests(), |
| testing::UnorderedElementsAre( |
| testing::Pair(kBidder1, |
| ElementsAreRequests( |
| kExpectedGenerateBidPrivateAggregationRequest)), |
| testing::Pair(kBidder2, |
| ElementsAreRequests( |
| kExpectedGenerateBidPrivateAggregationRequest, |
| kExpectedReportWinPrivateAggregationRequest)), |
| testing::Pair( |
| kSeller, ElementsAreRequests( |
| kExpectedScoreAdPrivateAggregationRequest, |
| kExpectedScoreAdPrivateAggregationRequest, |
| kExpectedReportResultPrivateAggregationRequest)))); |
| EXPECT_THAT( |
| result_.private_aggregation_event_map, |
| testing::UnorderedElementsAre(testing::Pair( |
| "click", |
| ElementsAreRequests( |
| BuildPrivateAggregationRequest(/*bucket=*/10, /*value=*/22), |
| BuildPrivateAggregationRequest(/*bucket=*/30, /*value=*/42))))); |
| } |
| } |
| |
| TEST_F(AuctionRunnerTest, PauseBidder) { |
| pause_worklet_url_ = kBidder2Url; |
| |
| // Have a 404 for script 2 until ready to resume. |
| url_loader_factory_.AddResponse(kBidder2Url.spec(), "", net::HTTP_NOT_FOUND); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder1Url, |
| MakeBidScript(kSeller, "1", "https://ad1.com/", /*num_ad_components=*/2, |
| kBidder1, kBidder1Name, |
| /*has_signals=*/true, "k1", "a")); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| MakeAuctionScript()); |
| auction_worklet::AddBidderJsonResponse( |
| &url_loader_factory_, |
| GURL(kBidder1TrustedSignalsUrl.spec() + |
| "?hostname=publisher1.com&keys=k1,k2" |
| "&interestGroupNames=Ad+Platform"), |
| kBidder1SignalsJson); |
| auction_worklet::AddBidderJsonResponse( |
| &url_loader_factory_, |
| GURL(kBidder2TrustedSignalsUrl.spec() + |
| "?hostname=publisher1.com&keys=l1,l2" |
| "&interestGroupNames=Another+Ad+Thing"), |
| kBidder2SignalsJson); |
| |
| StartStandardAuction(); |
| // Run all threads as far as they can get. |
| task_environment()->RunUntilIdle(); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder2Url, |
| MakeBidScript(kSeller, "2", "https://ad2.com/", /*num_ad_components=*/2, |
| kBidder2, kBidder2Name, |
| /*has_signals=*/true, "l2", "b")); |
| |
| same_process_auction_process_manager_->ResumeAllPaused(); |
| |
| // Need to resume a second time, when the script is re-loaded to run |
| // ReportWin(). |
| task_environment()->RunUntilIdle(); |
| same_process_auction_process_manager_->ResumeAllPaused(); |
| |
| auction_run_loop_->Run(); |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| EXPECT_EQ(kBidder2Key, result_.winning_group_id); |
| EXPECT_EQ(GURL("https://ad2.com/"), result_.ad_descriptor->url); |
| EXPECT_EQ(std::vector<blink::AdDescriptor>{blink::AdDescriptor( |
| GURL("https://ad2.com-component1.com"))}, |
| result_.ad_component_descriptors); |
| EXPECT_THAT(result_.report_urls, |
| testing::UnorderedElementsAre( |
| GURL("https://reporting.example.com/2"), |
| GURL("https://buyer-reporting.example.com/2"))); |
| EXPECT_THAT( |
| result_.ad_beacon_map, |
| testing::UnorderedElementsAre( |
| testing::Pair(ReportingDestination::kSeller, |
| testing::ElementsAre(testing::Pair( |
| "click", GURL("https://reporting.example.com/4")))), |
| testing::Pair(ReportingDestination::kComponentSeller, |
| testing::ElementsAre()), |
| testing::Pair( |
| ReportingDestination::kBuyer, |
| testing::ElementsAre(testing::Pair( |
| "click", GURL("https://buyer-reporting.example.com/4")))))); |
| EXPECT_THAT( |
| private_aggregation_manager_.TakePrivateAggregationRequests(), |
| testing::UnorderedElementsAre( |
| testing::Pair(kBidder1, |
| ElementsAreRequests( |
| kExpectedGenerateBidPrivateAggregationRequest)), |
| testing::Pair( |
| kBidder2, |
| ElementsAreRequests(kExpectedGenerateBidPrivateAggregationRequest, |
| kExpectedReportWinPrivateAggregationRequest)), |
| testing::Pair(kSeller, |
| ElementsAreRequests( |
| kExpectedScoreAdPrivateAggregationRequest, |
| kExpectedScoreAdPrivateAggregationRequest, |
| kExpectedReportResultPrivateAggregationRequest)))); |
| EXPECT_THAT( |
| result_.private_aggregation_event_map, |
| testing::UnorderedElementsAre(testing::Pair( |
| "click", |
| ElementsAreRequests( |
| BuildPrivateAggregationRequest(/*bucket=*/10, /*value=*/22), |
| BuildPrivateAggregationRequest(/*bucket=*/30, /*value=*/42))))); |
| } |
| |
| TEST_F(AuctionRunnerTest, PauseSeller) { |
| pause_worklet_url_ = kSellerUrl; |
| |
| // Have a 404 for seller until ready to resume. |
| url_loader_factory_.AddResponse(kSellerUrl.spec(), "", net::HTTP_NOT_FOUND); |
| |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder1Url, |
| MakeBidScript(kSeller, "1", "https://ad1.com/", /*num_ad_components=*/2, |
| kBidder1, kBidder1Name, |
| /*has_signals=*/true, "k1", "a")); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder2Url, |
| MakeBidScript(kSeller, "2", "https://ad2.com/", /*num_ad_components=*/2, |
| kBidder2, kBidder2Name, |
| /*has_signals=*/true, "l2", "b")); |
| auction_worklet::AddBidderJsonResponse( |
| &url_loader_factory_, |
| GURL(kBidder1TrustedSignalsUrl.spec() + |
| "?hostname=publisher1.com&keys=k1,k2" |
| "&interestGroupNames=Ad+Platform"), |
| kBidder1SignalsJson); |
| auction_worklet::AddBidderJsonResponse( |
| &url_loader_factory_, |
| GURL(kBidder2TrustedSignalsUrl.spec() + |
| "?hostname=publisher1.com&keys=l1,l2" |
| "&interestGroupNames=Another+Ad+Thing"), |
| kBidder2SignalsJson); |
| |
| StartStandardAuction(); |
| // Run all threads as far as they can get. |
| task_environment()->RunUntilIdle(); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| MakeAuctionScript()); |
| |
| same_process_auction_process_manager_->ResumeAllPaused(); |
| |
| auction_run_loop_->Run(); |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| EXPECT_EQ(kBidder2Key, result_.winning_group_id); |
| EXPECT_EQ(GURL("https://ad2.com/"), result_.ad_descriptor->url); |
| EXPECT_EQ(std::vector<blink::AdDescriptor>{blink::AdDescriptor( |
| GURL("https://ad2.com-component1.com"))}, |
| result_.ad_component_descriptors); |
| EXPECT_THAT(result_.report_urls, |
| testing::UnorderedElementsAre( |
| GURL("https://reporting.example.com/2"), |
| GURL("https://buyer-reporting.example.com/2"))); |
| EXPECT_THAT( |
| result_.ad_beacon_map, |
| testing::UnorderedElementsAre( |
| testing::Pair(ReportingDestination::kSeller, |
| testing::ElementsAre(testing::Pair( |
| "click", GURL("https://reporting.example.com/4")))), |
| testing::Pair(ReportingDestination::kComponentSeller, |
| testing::ElementsAre()), |
| testing::Pair( |
| ReportingDestination::kBuyer, |
| testing::ElementsAre(testing::Pair( |
| "click", GURL("https://buyer-reporting.example.com/4")))))); |
| EXPECT_THAT( |
| private_aggregation_manager_.TakePrivateAggregationRequests(), |
| testing::UnorderedElementsAre( |
| testing::Pair(kBidder1, |
| ElementsAreRequests( |
| kExpectedGenerateBidPrivateAggregationRequest)), |
| testing::Pair( |
| kBidder2, |
| ElementsAreRequests(kExpectedGenerateBidPrivateAggregationRequest, |
| kExpectedReportWinPrivateAggregationRequest)), |
| testing::Pair(kSeller, |
| ElementsAreRequests( |
| kExpectedScoreAdPrivateAggregationRequest, |
| kExpectedScoreAdPrivateAggregationRequest, |
| kExpectedReportResultPrivateAggregationRequest)))); |
| EXPECT_THAT( |
| result_.private_aggregation_event_map, |
| testing::UnorderedElementsAre(testing::Pair( |
| "click", |
| ElementsAreRequests( |
| BuildPrivateAggregationRequest(/*bucket=*/10, /*value=*/22), |
| BuildPrivateAggregationRequest(/*bucket=*/30, /*value=*/42))))); |
| } |
| |
| // A component auction with two successful bids from different components. |
| TEST_F(AuctionRunnerTest, ComponentAuction) { |
| SetUpComponentAuctionAndResponses(/*bidder1_seller=*/kComponentSeller1, |
| /*bidder2_seller=*/kComponentSeller2, |
| /*bid_from_component_auction_wins=*/true, |
| /*report_post_auction_signals=*/true); |
| |
| RunStandardAuction(); |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| EXPECT_EQ(GURL("https://ad2.com/"), result_.ad_descriptor->url); |
| EXPECT_EQ(std::vector<blink::AdDescriptor>{blink::AdDescriptor( |
| GURL("https://ad2.com-component1.com"))}, |
| result_.ad_component_descriptors); |
| EXPECT_THAT( |
| result_.report_urls, |
| testing::UnorderedElementsAre( |
| GURL("https://reporting.example.com/?highestScoringOtherBid=1&bid=2"), |
| GURL( |
| "https://component2-report.test/?highestScoringOtherBid=0&bid=2"), |
| ReportWinUrl(/*bid=*/2, /*highest_scoring_other_bid=*/0, |
| /*made_highest_scoring_other_bid=*/false))); |
| EXPECT_THAT( |
| result_.ad_beacon_map, |
| testing::UnorderedElementsAre( |
| testing::Pair(ReportingDestination::kSeller, |
| testing::ElementsAre(testing::Pair( |
| "click", GURL("https://reporting.example.com/4")))), |
| testing::Pair( |
| ReportingDestination::kComponentSeller, |
| testing::ElementsAre(testing::Pair( |
| "click", GURL("https://component2-report.test/4")))), |
| testing::Pair( |
| ReportingDestination::kBuyer, |
| testing::ElementsAre(testing::Pair( |
| "click", GURL("https://buyer-reporting.example.com/4")))))); |
| EXPECT_THAT( |
| private_aggregation_manager_.TakePrivateAggregationRequests(), |
| testing::UnorderedElementsAre( |
| testing::Pair(kBidder1, |
| ElementsAreRequests( |
| kExpectedGenerateBidPrivateAggregationRequest)), |
| testing::Pair( |
| kBidder2, |
| ElementsAreRequests(kExpectedGenerateBidPrivateAggregationRequest, |
| kExpectedReportWinPrivateAggregationRequest)), |
| testing::Pair(kSeller, |
| ElementsAreRequests( |
| kExpectedScoreAdPrivateAggregationRequest, |
| kExpectedScoreAdPrivateAggregationRequest, |
| kExpectedReportResultPrivateAggregationRequest)), |
| testing::Pair( |
| kComponentSeller1, |
| ElementsAreRequests(kExpectedScoreAdPrivateAggregationRequest)), |
| testing::Pair(kComponentSeller2, |
| ElementsAreRequests( |
| kExpectedScoreAdPrivateAggregationRequest, |
| kExpectedReportResultPrivateAggregationRequest)))); |
| EXPECT_THAT( |
| result_.private_aggregation_event_map, |
| testing::UnorderedElementsAre(testing::Pair( |
| "click", |
| ElementsAreRequests( |
| BuildPrivateAggregationRequest(/*bucket=*/10, /*value=*/22), |
| BuildPrivateAggregationRequest(/*bucket=*/30, /*value=*/42))))); |
| |
| EXPECT_THAT(result_.interest_groups_that_bid, |
| testing::UnorderedElementsAre(kBidder1Key, kBidder2Key)); |
| EXPECT_EQ(R"({"render_url":"https://ad2.com/"})", |
| result_.winning_group_ad_metadata); |
| CheckHistograms(InterestGroupAuction::AuctionResult::kSuccess, |
| /*expected_interest_groups=*/2, /*expected_owners=*/2, |
| /*expected_sellers=*/3); |
| } |
| |
| // Test a component auction where the top level seller rejects all bids. This |
| // should fail with kAllBidsRejected instead of kNoBids. |
| TEST_F(AuctionRunnerTest, ComponentAuctionTopSellerRejectsBids) { |
| // Run a standard component auction, but replace the default seller script |
| // with one that rejects bids. |
| SetUpComponentAuctionAndResponses(/*bidder1_seller=*/kComponentSeller1, |
| /*bidder2_seller=*/kComponentSeller1, |
| /*bid_from_component_auction_wins=*/true, |
| /*report_post_auction_signals=*/false); |
| |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| R"( |
| function scoreAd() { |
| return {desirability: 0, |
| allowComponentAuction: true}; |
| } |
| )"); |
| |
| RunStandardAuction(); |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| EXPECT_EQ(absl::nullopt, result_.ad_descriptor); |
| EXPECT_THAT(result_.report_urls, testing::UnorderedElementsAre()); |
| EXPECT_THAT(result_.interest_groups_that_bid, |
| testing::UnorderedElementsAre(kBidder1Key, kBidder2Key)); |
| CheckHistograms(InterestGroupAuction::AuctionResult::kAllBidsRejected, |
| /*expected_interest_groups=*/2, /*expected_owners=*/2, |
| /*expected_sellers=*/2); |
| } |
| |
| // Test case where the two components have the same buyer, which makes different |
| // bids for both auctions. |
| // |
| // This tests that parameters are separated, that bid counts are updated |
| // correctly, and how histograms are updated in these cases. |
| TEST_F(AuctionRunnerTest, ComponentAuctionSharedBuyer) { |
| const GURL kComponent1BidUrl = GURL("https://component1-bid.test/"); |
| const GURL kComponent2BidUrl = GURL("https://component2-bid.test/"); |
| |
| // Bid script used in both auctions. The bid amount is based on the seller: |
| // It bids the most in auctions run by kComponentSeller2Url, and the least in |
| // auctions run by kComponentSeller1Url. |
| const char kBidScript[] = R"( |
| function generateBid(interestGroup, auctionSignals, perBuyerSignals, |
| trustedBiddingSignals, browserSignals) { |
| privateAggregation.sendHistogramReport({bucket: 1n, value: 2}); |
| if (browserSignals.seller == "https://component.seller1.test") { |
| privateAggregation.reportContributionForEvent( |
| 'click', {bucket: 10n, value: 21}); |
| return {ad: [], bid: 1, render: "https://component1-bid.test/", |
| allowComponentAuction: true}; |
| } |
| if (browserSignals.seller == "https://component.seller2.test") { |
| privateAggregation.reportContributionForEvent( |
| 'click', {bucket: 10n, value: 23}); |
| return {ad: [], bid: 3, render: "https://component2-bid.test/", |
| allowComponentAuction: true}; |
| } |
| return 0; |
| } |
| |
| function reportWin(auctionSignals, perBuyerSignals, sellerSignals, |
| browserSignals) { |
| sendReportTo("https://buyer-reporting.example.com/" + browserSignals.bid); |
| registerAdBeacon({ |
| "click": "https://buyer-reporting.example.com/" + 2*browserSignals.bid, |
| }); |
| privateAggregation.sendHistogramReport({bucket: 3n, value: 4}); |
| privateAggregation.reportContributionForEvent( |
| 'click', {bucket: 30n, value: 40 + browserSignals.bid}); |
| } |
| )"; |
| |
| // Script used for both sellers. Return different desireability scores based |
| // on bid and seller, to make sure correct values are plumbed through. |
| const std::string kSellerScript = R"( |
| function scoreAd(adMetadata, bid, auctionConfig, browserSignals) { |
| privateAggregation.sendHistogramReport({bucket: 5n, value: 6}); |
| if (auctionConfig.seller == "https://adstuff.publisher1.com") |
| return {desirability: 20 + bid, allowComponentAuction: true}; |
| if (auctionConfig.seller == "https://component.seller2.test") |
| return {desirability: 30 + bid, allowComponentAuction: true}; |
| return {desirability: 10 + bid, allowComponentAuction: true}; |
| } |
| |
| function reportResult(auctionConfig, browserSignals) { |
| sendReportTo(auctionConfig.seller + "/" + |
| browserSignals.desirability); |
| registerAdBeacon({ |
| "click": auctionConfig.seller + "/" + 2*browserSignals.desirability, |
| }); |
| privateAggregation.sendHistogramReport({bucket: 7n, value: 8}); |
| } |
| )"; |
| |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kBidder1Url, |
| kBidScript); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| kSellerScript); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, |
| kComponentSeller1Url, kSellerScript); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, |
| kComponentSeller2Url, kSellerScript); |
| |
| interest_group_buyers_.reset(); |
| component_auctions_.emplace_back( |
| CreateAuctionConfig(kComponentSeller1Url, {{kBidder1}})); |
| component_auctions_.emplace_back( |
| CreateAuctionConfig(kComponentSeller2Url, {{kBidder1}})); |
| |
| // Custom interest group with two ads, so both bid URLs are valid. |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, kBidder1Name, kBidder1Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, kComponent1BidUrl)); |
| bidders[0].interest_group.ads->emplace_back(kComponent2BidUrl, absl::nullopt); |
| |
| StartAuction(kSellerUrl, std::move(bidders)); |
| auction_run_loop_->Run(); |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| |
| EXPECT_EQ(kComponent2BidUrl, result_.ad_descriptor->url); |
| EXPECT_THAT(result_.report_urls, |
| testing::UnorderedElementsAre( |
| GURL("https://adstuff.publisher1.com/23"), |
| GURL("https://component.seller2.test/33"), |
| GURL("https://buyer-reporting.example.com/3"))); |
| EXPECT_THAT( |
| result_.ad_beacon_map, |
| testing::UnorderedElementsAre( |
| testing::Pair( |
| ReportingDestination::kSeller, |
| testing::ElementsAre(testing::Pair( |
| "click", GURL("https://adstuff.publisher1.com/46")))), |
| testing::Pair( |
| ReportingDestination::kComponentSeller, |
| testing::ElementsAre(testing::Pair( |
| "click", GURL("https://component.seller2.test/66")))), |
| testing::Pair( |
| ReportingDestination::kBuyer, |
| testing::ElementsAre(testing::Pair( |
| "click", GURL("https://buyer-reporting.example.com/6")))))); |
| EXPECT_THAT( |
| private_aggregation_manager_.TakePrivateAggregationRequests(), |
| testing::UnorderedElementsAre( |
| testing::Pair( |
| kBidder1, |
| ElementsAreRequests(kExpectedGenerateBidPrivateAggregationRequest, |
| kExpectedGenerateBidPrivateAggregationRequest, |
| kExpectedReportWinPrivateAggregationRequest)), |
| testing::Pair(kSeller, |
| ElementsAreRequests( |
| kExpectedScoreAdPrivateAggregationRequest, |
| kExpectedScoreAdPrivateAggregationRequest, |
| kExpectedReportResultPrivateAggregationRequest)), |
| testing::Pair( |
| kComponentSeller1, |
| ElementsAreRequests(kExpectedScoreAdPrivateAggregationRequest)), |
| testing::Pair(kComponentSeller2, |
| ElementsAreRequests( |
| kExpectedScoreAdPrivateAggregationRequest, |
| kExpectedReportResultPrivateAggregationRequest)))); |
| EXPECT_THAT( |
| result_.private_aggregation_event_map, |
| testing::UnorderedElementsAre(testing::Pair( |
| "click", |
| ElementsAreRequests( |
| BuildPrivateAggregationRequest(/*bucket=*/10, /*value=*/23), |
| BuildPrivateAggregationRequest(/*bucket=*/30, /*value=*/43))))); |
| // Bid count should only be incremented by 1. |
| EXPECT_THAT(result_.interest_groups_that_bid, |
| testing::UnorderedElementsAre(kBidder1Key)); |
| EXPECT_EQ(R"({"render_url":"https://component2-bid.test/"})", |
| result_.winning_group_ad_metadata); |
| // Currently an interest group participating twice in an auction is counted |
| // twice. |
| CheckHistograms(InterestGroupAuction::AuctionResult::kSuccess, |
| /*expected_interest_groups=*/2, /*expected_owners=*/2, |
| /*expected_sellers=*/3); |
| } |
| |
| // Test case where a single component auction accepts one bid and rejects |
| // another. This is a regression test for https://crbug.com/1321941, where a |
| // rejected bid from a component auction would be treated as a security error, |
| // and result in bidding in the component auction being aborted, and all |
| // previous bids being thrown out. |
| TEST_F(AuctionRunnerTest, ComponentAuctionAcceptsBidRejectsBid) { |
| // Script used by the winning bidder. It makes the lower bid. |
| const char kBidder1Script[] = R"( |
| function generateBid(interestGroup, auctionSignals, perBuyerSignals, |
| trustedBiddingSignals, browserSignals) { |
| return {bid: 1, render: interestGroup.ads[0].renderUrl, |
| allowComponentAuction: true}; |
| } |
| |
| function reportWin() {} |
| )"; |
| |
| // Script used by the losing bidder. It makes the higher bid. |
| const char kBidder2Script[] = R"( |
| function generateBid(interestGroup, auctionSignals, perBuyerSignals, |
| trustedBiddingSignals, browserSignals) { |
| return {bid: 2, render: interestGroup.ads[0].renderUrl, |
| allowComponentAuction: true}; |
| } |
| )"; |
| |
| // Script used for both sellers. It rejects bids over 1. |
| const std::string kSellerScript = R"( |
| function scoreAd(adMetadata, bid, auctionConfig, browserSignals) { |
| if (bid > 1) |
| return {desirability: 0, allowComponentAuction: true}; |
| return {desirability: bid, allowComponentAuction: true}; |
| } |
| |
| function reportResult() {} |
| )"; |
| |
| // Set up a component auction using the normal helper function, but then |
| // overwrite the scripts. |
| SetUpComponentAuctionAndResponses(/*bidder1_seller=*/kComponentSeller1, |
| /*bidder2_seller=*/kComponentSeller1, |
| /*bid_from_component_auction_wins=*/false); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kBidder1Url, |
| kBidder1Script); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kBidder2Url, |
| kBidder2Script); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| kSellerScript); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, |
| kComponentSeller1Url, kSellerScript); |
| |
| RunStandardAuction(); |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| |
| EXPECT_EQ("https://ad1.com/", result_.ad_descriptor->url); |
| CheckHistograms(InterestGroupAuction::AuctionResult::kSuccess, |
| /*expected_interest_groups=*/2, /*expected_owners=*/2, |
| /*expected_sellers=*/2); |
| } |
| |
| // A component auction with one component that has two buyers. In this auction, |
| // the top-level auction would score kBidder2 higher (since it bids more), but |
| // kBidder1 wins this auction, because the component auctions use a different |
| // scoring function, which favors kBidder1's lower bid. |
| TEST_F(AuctionRunnerTest, ComponentAuctionOneComponentTwoBidders) { |
| SetUpComponentAuctionAndResponses(/*bidder1_seller=*/kComponentSeller1, |
| /*bidder2_seller=*/kComponentSeller1, |
| /*bid_from_component_auction_wins=*/true, |
| /*report_post_auction_signals=*/true); |
| |
| RunStandardAuction(); |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| EXPECT_EQ(GURL("https://ad1.com/"), result_.ad_descriptor->url); |
| EXPECT_EQ(std::vector<blink::AdDescriptor>{blink::AdDescriptor( |
| GURL("https://ad1.com-component1.com"))}, |
| result_.ad_component_descriptors); |
| EXPECT_THAT( |
| result_.report_urls, |
| testing::UnorderedElementsAre( |
| GURL("https://reporting.example.com/?highestScoringOtherBid=0&bid=1"), |
| GURL( |
| "https://component1-report.test/?highestScoringOtherBid=2&bid=1"), |
| ReportWinUrl(/*bid=*/1, /*highest_scoring_other_bid=*/2, |
| /*made_highest_scoring_other_bid=*/false))); |
| EXPECT_THAT( |
| result_.ad_beacon_map, |
| testing::UnorderedElementsAre( |
| testing::Pair(ReportingDestination::kSeller, |
| testing::ElementsAre(testing::Pair( |
| "click", GURL("https://reporting.example.com/2")))), |
| testing::Pair( |
| ReportingDestination::kComponentSeller, |
| testing::ElementsAre(testing::Pair( |
| "click", GURL("https://component1-report.test/2")))), |
| testing::Pair( |
| ReportingDestination::kBuyer, |
| testing::ElementsAre(testing::Pair( |
| "click", GURL("https://buyer-reporting.example.com/2")))))); |
| EXPECT_THAT( |
| private_aggregation_manager_.TakePrivateAggregationRequests(), |
| testing::UnorderedElementsAre( |
| testing::Pair( |
| kBidder1, |
| ElementsAreRequests(kExpectedGenerateBidPrivateAggregationRequest, |
| kExpectedReportWinPrivateAggregationRequest)), |
| testing::Pair(kBidder2, |
| ElementsAreRequests( |
| kExpectedGenerateBidPrivateAggregationRequest)), |
| testing::Pair(kSeller, |
| ElementsAreRequests( |
| kExpectedScoreAdPrivateAggregationRequest, |
| kExpectedReportResultPrivateAggregationRequest)), |
| testing::Pair(kComponentSeller1, |
| ElementsAreRequests( |
| kExpectedScoreAdPrivateAggregationRequest, |
| kExpectedScoreAdPrivateAggregationRequest, |
| kExpectedReportResultPrivateAggregationRequest)))); |
| EXPECT_THAT( |
| result_.private_aggregation_event_map, |
| testing::UnorderedElementsAre(testing::Pair( |
| "click", |
| ElementsAreRequests( |
| BuildPrivateAggregationRequest(/*bucket=*/10, /*value=*/21), |
| BuildPrivateAggregationRequest(/*bucket=*/30, /*value=*/41))))); |
| |
| EXPECT_THAT(result_.interest_groups_that_bid, |
| testing::UnorderedElementsAre(kBidder1Key, kBidder2Key)); |
| EXPECT_EQ(R"({"render_url":"https://ad1.com/","metadata":{"ads": true}})", |
| result_.winning_group_ad_metadata); |
| CheckHistograms(InterestGroupAuction::AuctionResult::kSuccess, |
| /*expected_interest_groups=*/2, /*expected_owners=*/2, |
| /*expected_sellers=*/2); |
| } |
| |
| // Test the case a top-level seller returns no signals in its reportResult |
| // method. The default scripts return signals, so only need to individually test |
| // the no-value case. |
| TEST_F(AuctionRunnerTest, ComponentAuctionNoTopLevelReportResultSignals) { |
| // Basic bid script. |
| const char kBidScript[] = R"( |
| function generateBid(interestGroup, auctionSignals, perBuyerSignals, |
| trustedBiddingSignals, browserSignals) { |
| privateAggregation.sendHistogramReport({bucket: 1n, value: 2}); |
| return {ad: [], bid: 2, render: interestGroup.ads[0].renderUrl, |
| allowComponentAuction: true}; |
| } |
| |
| function reportWin(auctionSignals, perBuyerSignals, sellerSignals, |
| browserSignals) { |
| sendReportTo("https://buyer-reporting.example.com/" + browserSignals.bid); |
| registerAdBeacon({ |
| "click": "https://buyer-reporting.example.com/" + 2*browserSignals.bid, |
| }); |
| privateAggregation.sendHistogramReport({bucket: 3n, value: 4}); |
| } |
| )"; |
| |
| // Component seller script that makes a report to a URL based on whether the |
| // top-level seller signals are null. |
| const std::string kComponentSellerScript = R"( |
| function scoreAd(adMetadata, bid, auctionConfig, browserSignals) { |
| privateAggregation.sendHistogramReport({bucket: 5n, value: 6}); |
| return {desirability: 10, allowComponentAuction: true}; |
| } |
| |
| function reportResult(auctionConfig, browserSignals) { |
| sendReportTo(auctionConfig.seller + "/" + |
| (browserSignals.topLevelSellerSignals === null)); |
| registerAdBeacon({ |
| "click": auctionConfig.seller + "/" + |
| (browserSignals.topLevelSellerSignals === null), |
| }); |
| privateAggregation.sendHistogramReport({bucket: 7n, value: 8}); |
| } |
| )"; |
| |
| // Top-level seller script with a reportResult method that has no return |
| // value. |
| const std::string kTopLevelSellerScript = R"( |
| function scoreAd(adMetadata, bid, auctionConfig, browserSignals) { |
| privateAggregation.sendHistogramReport({bucket: 5n, value: 6}); |
| return {desirability: 10, allowComponentAuction: true}; |
| } |
| |
| function reportResult(auctionConfig, browserSignals) { |
| sendReportTo(auctionConfig.seller + "/" + browserSignals.bid); |
| registerAdBeacon({ |
| "click": auctionConfig.seller + "/" + 2 * browserSignals.bid, |
| }); |
| privateAggregation.sendHistogramReport({bucket: 7n, value: 8}); |
| // Note that there's no return value here. |
| } |
| )"; |
| |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kBidder1Url, |
| kBidScript); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| kTopLevelSellerScript); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kComponentSeller1Url, kComponentSellerScript); |
| |
| interest_group_buyers_.reset(); |
| component_auctions_.emplace_back( |
| CreateAuctionConfig(kComponentSeller1Url, {{kBidder1}})); |
| |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, kBidder1Name, kBidder1Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad1.com"))); |
| |
| StartAuction(kSellerUrl, std::move(bidders)); |
| auction_run_loop_->Run(); |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| EXPECT_EQ(GURL("https://ad1.com"), result_.ad_descriptor->url); |
| EXPECT_THAT(result_.report_urls, |
| testing::UnorderedElementsAre( |
| GURL("https://buyer-reporting.example.com/2"), |
| GURL("https://component.seller1.test/true"), |
| GURL("https://adstuff.publisher1.com/2"))); |
| EXPECT_THAT( |
| result_.ad_beacon_map, |
| testing::UnorderedElementsAre( |
| testing::Pair( |
| ReportingDestination::kSeller, |
| testing::ElementsAre(testing::Pair( |
| "click", GURL("https://adstuff.publisher1.com/4")))), |
| testing::Pair( |
| ReportingDestination::kComponentSeller, |
| testing::ElementsAre(testing::Pair( |
| "click", GURL("https://component.seller1.test/true")))), |
| testing::Pair( |
| ReportingDestination::kBuyer, |
| testing::ElementsAre(testing::Pair( |
| "click", GURL("https://buyer-reporting.example.com/4")))))); |
| EXPECT_THAT( |
| private_aggregation_manager_.TakePrivateAggregationRequests(), |
| testing::UnorderedElementsAre( |
| testing::Pair( |
| kBidder1, |
| ElementsAreRequests(kExpectedGenerateBidPrivateAggregationRequest, |
| kExpectedReportWinPrivateAggregationRequest)), |
| testing::Pair(kSeller, |
| ElementsAreRequests( |
| kExpectedScoreAdPrivateAggregationRequest, |
| kExpectedReportResultPrivateAggregationRequest)), |
| testing::Pair(kComponentSeller1, |
| ElementsAreRequests( |
| kExpectedScoreAdPrivateAggregationRequest, |
| kExpectedReportResultPrivateAggregationRequest)))); |
| CheckHistograms(InterestGroupAuction::AuctionResult::kSuccess, |
| /*expected_interest_groups=*/1, /*expected_owners=*/1, |
| /*expected_sellers=*/2); |
| } |
| |
| TEST_F(AuctionRunnerTest, ComponentAuctionModifiesBid) { |
| // Basic bid script. |
| const char kBidScript[] = R"( |
| function generateBid(interestGroup, auctionSignals, perBuyerSignals, |
| trustedBiddingSignals, browserSignals) { |
| return {ad: [], bid: 2, render: interestGroup.ads[0].renderUrl, |
| allowComponentAuction: true}; |
| } |
| |
| function reportWin(auctionSignals, perBuyerSignals, sellerSignals, |
| browserSignals) { |
| sendReportTo("https://buyer-reporting.example.com/" + browserSignals.bid); |
| registerAdBeacon({ |
| "click": "https://buyer-reporting.example.com/" + 2 * browserSignals.bid, |
| }); |
| } |
| )"; |
| |
| // Component seller script that modifies the bid to 3. |
| const std::string kComponentSellerScript = R"( |
| function scoreAd(adMetadata, bid, auctionConfig, browserSignals) { |
| return {desirability: 10, allowComponentAuction: true, bid: 3}; |
| } |
| |
| function reportResult(auctionConfig, browserSignals) { |
| sendReportTo(auctionConfig.seller + "/" + browserSignals.bid + |
| "_" + browserSignals.modifiedBid); |
| registerAdBeacon({ |
| "click": auctionConfig.seller + "/" + 2 * browserSignals.bid + |
| "_" + browserSignals.modifiedBid, |
| }); |
| } |
| )"; |
| |
| // Top-level seller script that rejects bids that aren't 3.. |
| const std::string kTopLevelSellerScript = R"( |
| function scoreAd(adMetadata, bid, auctionConfig, browserSignals) { |
| if (bid != 3) |
| return 0; |
| return {desirability: 10, allowComponentAuction: true}; |
| } |
| |
| function reportResult(auctionConfig, browserSignals) { |
| sendReportTo(auctionConfig.seller + "/" + browserSignals.bid); |
| registerAdBeacon({ |
| "click": auctionConfig.seller + "/" + 2 * browserSignals.bid, |
| }); |
| } |
| )"; |
| |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kBidder1Url, |
| kBidScript); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| kTopLevelSellerScript); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kComponentSeller1Url, kComponentSellerScript); |
| |
| interest_group_buyers_.reset(); |
| component_auctions_.emplace_back( |
| CreateAuctionConfig(kComponentSeller1Url, {{kBidder1}})); |
| |
| // Basic interest group. |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, kBidder1Name, kBidder1Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad1.com"))); |
| |
| StartAuction(kSellerUrl, std::move(bidders)); |
| auction_run_loop_->Run(); |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| EXPECT_EQ(GURL("https://ad1.com"), result_.ad_descriptor->url); |
| // The reporting URLs contain the bids - the top-level seller report should |
| // see the modified bid, the other worklets see the original bid. |
| EXPECT_THAT(result_.report_urls, |
| testing::UnorderedElementsAre( |
| GURL("https://buyer-reporting.example.com/2"), |
| GURL("https://component.seller1.test/2_3"), |
| GURL("https://adstuff.publisher1.com/3"))); |
| EXPECT_THAT( |
| result_.ad_beacon_map, |
| testing::UnorderedElementsAre( |
| testing::Pair( |
| ReportingDestination::kSeller, |
| testing::ElementsAre(testing::Pair( |
| "click", GURL("https://adstuff.publisher1.com/6")))), |
| testing::Pair( |
| ReportingDestination::kComponentSeller, |
| testing::ElementsAre(testing::Pair( |
| "click", GURL("https://component.seller1.test/4_3")))), |
| testing::Pair( |
| ReportingDestination::kBuyer, |
| testing::ElementsAre(testing::Pair( |
| "click", GURL("https://buyer-reporting.example.com/4")))))); |
| EXPECT_TRUE( |
| private_aggregation_manager_.TakePrivateAggregationRequests().empty()); |
| CheckHistograms(InterestGroupAuction::AuctionResult::kSuccess, |
| /*expected_interest_groups=*/1, /*expected_owners=*/1, |
| /*expected_sellers=*/2); |
| } |
| |
| // An auction in which the seller origin is not allowed to use the interest |
| // group API. |
| TEST_F(AuctionRunnerTest, DisallowedSeller) { |
| disallowed_sellers_.insert(url::Origin::Create(kSellerUrl)); |
| |
| // The lack of Javascript responses means the auction should hang if any |
| // script URLs are incorrectly requested. |
| RunStandardAuction(); |
| |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| EXPECT_FALSE(result_.winning_group_id); |
| EXPECT_FALSE(result_.ad_descriptor); |
| EXPECT_TRUE(result_.ad_component_descriptors.empty()); |
| EXPECT_TRUE( |
| private_aggregation_manager_.TakePrivateAggregationRequests().empty()); |
| EXPECT_THAT(result_.interest_groups_that_bid, |
| testing::UnorderedElementsAre()); |
| CheckHistograms(InterestGroupAuction::AuctionResult::kSellerRejected, |
| /*expected_interest_groups=*/absl::nullopt, |
| /*expected_owners=*/absl::nullopt, |
| /*expected_sellers=*/absl::nullopt); |
| |
| // No requests for the bidder worklet URLs should be made. |
| task_environment()->RunUntilIdle(); |
| EXPECT_EQ(0, url_loader_factory_.NumPending()); |
| } |
| |
| // A component auction in which the component seller is disallowed, and the |
| // top-level seller has no buyers. |
| TEST_F(AuctionRunnerTest, DisallowedComponentAuctionSeller) { |
| interest_group_buyers_.reset(); |
| component_auctions_.emplace_back( |
| CreateAuctionConfig(kComponentSeller1Url, {{kBidder1}})); |
| |
| disallowed_sellers_.insert(kComponentSeller1); |
| |
| // The lack of Javascript responses means the auction should hang if any |
| // script URLs are incorrectly requested. |
| RunStandardAuction(); |
| |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| EXPECT_FALSE(result_.winning_group_id); |
| EXPECT_FALSE(result_.ad_descriptor); |
| EXPECT_TRUE(result_.ad_component_descriptors.empty()); |
| EXPECT_TRUE( |
| private_aggregation_manager_.TakePrivateAggregationRequests().empty()); |
| EXPECT_THAT(result_.interest_groups_that_bid, |
| testing::UnorderedElementsAre()); |
| CheckHistograms(InterestGroupAuction::AuctionResult::kNoInterestGroups, |
| /*expected_interest_groups=*/absl::nullopt, |
| /*expected_owners=*/absl::nullopt, |
| /*expected_sellers=*/absl::nullopt); |
| |
| // No requests for the bidder worklet URLs should be made. |
| task_environment()->RunUntilIdle(); |
| EXPECT_EQ(0, url_loader_factory_.NumPending()); |
| } |
| |
| // A component auction in which the one component seller is disallowed, but the |
| // other is not. |
| TEST_F(AuctionRunnerTest, DisallowedComponentAuctionOneSeller) { |
| SetUpComponentAuctionAndResponses(/*bidder1_seller=*/kComponentSeller1, |
| /*bidder2_seller=*/kComponentSeller2, |
| /*bid_from_component_auction_wins=*/true); |
| |
| // Bidder 2 bids more, so would win the auction if component seller 2 were |
| // allowed to participate. |
| disallowed_sellers_.insert(kComponentSeller2); |
| |
| RunAuctionAndWait(kSellerUrl, std::vector<StorageInterestGroup>()); |
| |
| // The lack of Javascript responses means the auction should hang if any |
| // script URLs are incorrectly requested. |
| RunStandardAuction(); |
| |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| EXPECT_EQ(GURL("https://ad1.com/"), result_.ad_descriptor->url); |
| EXPECT_EQ(std::vector<blink::AdDescriptor>{blink::AdDescriptor( |
| GURL("https://ad1.com-component1.com"))}, |
| result_.ad_component_descriptors); |
| EXPECT_THAT(result_.report_urls, |
| testing::UnorderedElementsAre( |
| GURL("https://reporting.example.com/1"), |
| GURL("https://component1-report.test/1"), |
| GURL("https://buyer-reporting.example.com/1"))); |
| EXPECT_THAT( |
| result_.ad_beacon_map, |
| testing::UnorderedElementsAre( |
| testing::Pair(ReportingDestination::kSeller, |
| testing::ElementsAre(testing::Pair( |
| "click", GURL("https://reporting.example.com/2")))), |
| testing::Pair( |
| ReportingDestination::kComponentSeller, |
| testing::ElementsAre(testing::Pair( |
| "click", GURL("https://component1-report.test/2")))), |
| testing::Pair( |
| ReportingDestination::kBuyer, |
| testing::ElementsAre(testing::Pair( |
| "click", GURL("https://buyer-reporting.example.com/2")))))); |
| EXPECT_THAT( |
| private_aggregation_manager_.TakePrivateAggregationRequests(), |
| testing::UnorderedElementsAre( |
| testing::Pair( |
| kBidder1, |
| ElementsAreRequests(kExpectedGenerateBidPrivateAggregationRequest, |
| kExpectedReportWinPrivateAggregationRequest)), |
| testing::Pair(kSeller, |
| ElementsAreRequests( |
| kExpectedScoreAdPrivateAggregationRequest, |
| kExpectedReportResultPrivateAggregationRequest)), |
| testing::Pair(kComponentSeller1, |
| ElementsAreRequests( |
| kExpectedScoreAdPrivateAggregationRequest, |
| kExpectedReportResultPrivateAggregationRequest)))); |
| EXPECT_THAT( |
| result_.private_aggregation_event_map, |
| testing::UnorderedElementsAre(testing::Pair( |
| "click", |
| ElementsAreRequests( |
| BuildPrivateAggregationRequest(/*bucket=*/10, /*value=*/21), |
| BuildPrivateAggregationRequest(/*bucket=*/30, /*value=*/41))))); |
| EXPECT_THAT(result_.interest_groups_that_bid, |
| testing::UnorderedElementsAre(kBidder1Key)); |
| EXPECT_EQ(R"({"render_url":"https://ad1.com/","metadata":{"ads": true}})", |
| result_.winning_group_ad_metadata); |
| CheckHistograms(InterestGroupAuction::AuctionResult::kSuccess, |
| /*expected_interest_groups=*/1, /*expected_owners=*/1, |
| /*expected_sellers=*/2); |
| } |
| |
| // An auction in which the buyer origins are not allowed to use the interest |
| // group API. |
| TEST_F(AuctionRunnerTest, DisallowedBuyers) { |
| disallowed_buyers_.insert(kBidder1); |
| disallowed_buyers_.insert(kBidder2); |
| |
| // The lack of Javascript responses means the auction should hang if any |
| // script URLs are incorrectly requested. |
| RunStandardAuction(); |
| |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| EXPECT_FALSE(result_.winning_group_id); |
| EXPECT_FALSE(result_.ad_descriptor); |
| EXPECT_TRUE(result_.ad_component_descriptors.empty()); |
| EXPECT_TRUE( |
| private_aggregation_manager_.TakePrivateAggregationRequests().empty()); |
| EXPECT_TRUE(result_.private_aggregation_event_map.empty()); |
| EXPECT_THAT(result_.interest_groups_that_bid, |
| testing::UnorderedElementsAre()); |
| CheckHistograms(InterestGroupAuction::AuctionResult::kNoInterestGroups, |
| /*expected_interest_groups=*/absl::nullopt, |
| /*expected_owners=*/absl::nullopt, |
| /*expected_sellers=*/absl::nullopt); |
| |
| // No requests for the seller worklet URL should be made. |
| task_environment()->RunUntilIdle(); |
| EXPECT_EQ(0, url_loader_factory_.NumPending()); |
| } |
| |
| // Run the standard auction, but disallow one bidder from participating. |
| TEST_F(AuctionRunnerTest, DisallowedSingleBuyer) { |
| // The lack of a bidder script 2 means that this test should hang if bidder |
| // 2's script is requested. |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder1Url, |
| MakeBidScript(kSeller, "1", "https://ad1.com/", /*num_ad_components=*/2, |
| kBidder1, kBidder1Name, |
| /*has_signals=*/true, "k1", "a")); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| MakeAuctionScript()); |
| auction_worklet::AddBidderJsonResponse( |
| &url_loader_factory_, |
| GURL(kBidder1TrustedSignalsUrl.spec() + |
| "?hostname=publisher1.com&keys=k1,k2" |
| "&interestGroupNames=Ad+Platform"), |
| kBidder1SignalsJson); |
| |
| disallowed_buyers_.insert(kBidder2); |
| RunStandardAuction(); |
| |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| EXPECT_EQ(kBidder1Key, result_.winning_group_id); |
| EXPECT_EQ(GURL("https://ad1.com/"), result_.ad_descriptor->url); |
| EXPECT_EQ(std::vector<blink::AdDescriptor>{blink::AdDescriptor( |
| GURL("https://ad1.com-component1.com"))}, |
| result_.ad_component_descriptors); |
| EXPECT_THAT(result_.report_urls, |
| testing::UnorderedElementsAre( |
| GURL("https://reporting.example.com/1"), |
| GURL("https://buyer-reporting.example.com/1"))); |
| EXPECT_THAT( |
| result_.ad_beacon_map, |
| testing::UnorderedElementsAre( |
| testing::Pair(ReportingDestination::kSeller, |
| testing::ElementsAre(testing::Pair( |
| "click", GURL("https://reporting.example.com/2")))), |
| testing::Pair(ReportingDestination::kComponentSeller, |
| testing::ElementsAre()), |
| testing::Pair( |
| ReportingDestination::kBuyer, |
| testing::ElementsAre(testing::Pair( |
| "click", GURL("https://buyer-reporting.example.com/2")))))); |
| EXPECT_THAT( |
| private_aggregation_manager_.TakePrivateAggregationRequests(), |
| testing::UnorderedElementsAre( |
| testing::Pair( |
| kBidder1, |
| ElementsAreRequests(kExpectedGenerateBidPrivateAggregationRequest, |
| kExpectedReportWinPrivateAggregationRequest)), |
| testing::Pair(kSeller, |
| ElementsAreRequests( |
| kExpectedScoreAdPrivateAggregationRequest, |
| kExpectedReportResultPrivateAggregationRequest)))); |
| EXPECT_THAT( |
| result_.private_aggregation_event_map, |
| testing::UnorderedElementsAre(testing::Pair( |
| "click", |
| ElementsAreRequests( |
| BuildPrivateAggregationRequest(/*bucket=*/10, /*value=*/21), |
| BuildPrivateAggregationRequest(/*bucket=*/30, /*value=*/41))))); |
| |
| EXPECT_THAT(result_.interest_groups_that_bid, |
| testing::UnorderedElementsAre(kBidder1Key)); |
| EXPECT_EQ(R"({"render_url":"https://ad1.com/","metadata":{"ads": true}})", |
| result_.winning_group_ad_metadata); |
| CheckHistograms(InterestGroupAuction::AuctionResult::kSuccess, |
| /*expected_interest_groups=*/1, /*expected_owners=*/1, |
| /*expected_sellers=*/1); |
| |
| // No requests for bidder2's worklet URL should be made. |
| task_environment()->RunUntilIdle(); |
| EXPECT_EQ(0, url_loader_factory_.NumPending()); |
| } |
| |
| // A component auction in which all buyers are disallowed. |
| TEST_F(AuctionRunnerTest, DisallowedComponentAuctionBuyers) { |
| interest_group_buyers_->clear(); |
| component_auctions_.emplace_back( |
| CreateAuctionConfig(kComponentSeller1Url, {{kBidder1}})); |
| component_auctions_.emplace_back( |
| CreateAuctionConfig(kComponentSeller2Url, {{kBidder2}})); |
| |
| disallowed_buyers_.insert(kBidder1); |
| disallowed_buyers_.insert(kBidder2); |
| |
| // The lack of Javascript responses means the auction should hang if any |
| // script URLs are incorrectly requested. |
| RunStandardAuction(); |
| |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| EXPECT_FALSE(result_.winning_group_id); |
| EXPECT_FALSE(result_.ad_descriptor); |
| EXPECT_TRUE(result_.ad_component_descriptors.empty()); |
| EXPECT_TRUE( |
| private_aggregation_manager_.TakePrivateAggregationRequests().empty()); |
| EXPECT_TRUE(result_.private_aggregation_event_map.empty()); |
| EXPECT_THAT(result_.interest_groups_that_bid, |
| testing::UnorderedElementsAre()); |
| CheckHistograms(InterestGroupAuction::AuctionResult::kNoInterestGroups, |
| /*expected_interest_groups=*/absl::nullopt, |
| /*expected_owners=*/absl::nullopt, |
| /*expected_sellers=*/absl::nullopt); |
| |
| // No requests for the bidder worklet URLs should be made. |
| task_environment()->RunUntilIdle(); |
| EXPECT_EQ(0, url_loader_factory_.NumPending()); |
| } |
| |
| // A component auction in which a single buyer is disallowed. |
| TEST_F(AuctionRunnerTest, DisallowedComponentAuctionSingleBuyer) { |
| SetUpComponentAuctionAndResponses(/*bidder1_seller=*/kComponentSeller1, |
| /*bidder2_seller=*/kComponentSeller2, |
| /*bid_from_component_auction_wins=*/true); |
| |
| disallowed_buyers_.insert(kBidder2); |
| |
| // The lack of Javascript responses means the auction should hang if any |
| // script URLs are incorrectly requested. |
| RunStandardAuction(); |
| |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| EXPECT_EQ(GURL("https://ad1.com/"), result_.ad_descriptor->url); |
| EXPECT_EQ(std::vector<blink::AdDescriptor>{blink::AdDescriptor( |
| GURL("https://ad1.com-component1.com"))}, |
| result_.ad_component_descriptors); |
| EXPECT_THAT(result_.report_urls, |
| testing::UnorderedElementsAre( |
| GURL("https://reporting.example.com/1"), |
| GURL("https://component1-report.test/1"), |
| GURL("https://buyer-reporting.example.com/1"))); |
| EXPECT_THAT( |
| result_.ad_beacon_map, |
| testing::UnorderedElementsAre( |
| testing::Pair(ReportingDestination::kSeller, |
| testing::ElementsAre(testing::Pair( |
| "click", GURL("https://reporting.example.com/2")))), |
| testing::Pair( |
| ReportingDestination::kComponentSeller, |
| testing::ElementsAre(testing::Pair( |
| "click", GURL("https://component1-report.test/2")))), |
| testing::Pair( |
| ReportingDestination::kBuyer, |
| testing::ElementsAre(testing::Pair( |
| "click", GURL("https://buyer-reporting.example.com/2")))))); |
| EXPECT_THAT( |
| private_aggregation_manager_.TakePrivateAggregationRequests(), |
| testing::UnorderedElementsAre( |
| testing::Pair( |
| kBidder1, |
| ElementsAreRequests(kExpectedGenerateBidPrivateAggregationRequest, |
| kExpectedReportWinPrivateAggregationRequest)), |
| testing::Pair(kSeller, |
| ElementsAreRequests( |
| kExpectedScoreAdPrivateAggregationRequest, |
| kExpectedReportResultPrivateAggregationRequest)), |
| testing::Pair(kComponentSeller1, |
| ElementsAreRequests( |
| kExpectedScoreAdPrivateAggregationRequest, |
| kExpectedReportResultPrivateAggregationRequest)))); |
| EXPECT_THAT( |
| result_.private_aggregation_event_map, |
| testing::UnorderedElementsAre(testing::Pair( |
| "click", |
| ElementsAreRequests( |
| BuildPrivateAggregationRequest(/*bucket=*/10, /*value=*/21), |
| BuildPrivateAggregationRequest(/*bucket=*/30, /*value=*/41))))); |
| EXPECT_THAT(result_.interest_groups_that_bid, |
| testing::UnorderedElementsAre(kBidder1Key)); |
| EXPECT_EQ(R"({"render_url":"https://ad1.com/","metadata":{"ads": true}})", |
| result_.winning_group_ad_metadata); |
| CheckHistograms(InterestGroupAuction::AuctionResult::kSuccess, |
| /*expected_interest_groups=*/1, /*expected_owners=*/1, |
| /*expected_sellers=*/2); |
| } |
| |
| // Disallow bidders as sellers and disallow seller as bidder. Auction should |
| // still succeed. |
| TEST_F(AuctionRunnerTest, DisallowedAsOtherParticipant) { |
| disallowed_sellers_.insert(kBidder1); |
| disallowed_sellers_.insert(kBidder2); |
| disallowed_buyers_.insert(url::Origin::Create(kSellerUrl)); |
| |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder1Url, |
| MakeBidScript(kSeller, "1", "https://ad1.com/", /*num_ad_components=*/2, |
| kBidder1, kBidder1Name, |
| /*has_signals=*/true, "k1", "a")); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder2Url, |
| MakeBidScript(kSeller, "2", "https://ad2.com/", /*num_ad_components=*/2, |
| kBidder2, kBidder2Name, |
| /*has_signals=*/true, "l2", "b")); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| MakeAuctionScript()); |
| auction_worklet::AddBidderJsonResponse( |
| &url_loader_factory_, |
| GURL(kBidder1TrustedSignalsUrl.spec() + |
| "?hostname=publisher1.com&keys=k1,k2" |
| "&interestGroupNames=Ad+Platform"), |
| kBidder1SignalsJson); |
| auction_worklet::AddBidderJsonResponse( |
| &url_loader_factory_, |
| GURL(kBidder2TrustedSignalsUrl.spec() + |
| "?hostname=publisher1.com&keys=l1,l2" |
| "&interestGroupNames=Another+Ad+Thing"), |
| kBidder2SignalsJson); |
| |
| RunStandardAuction(); |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| EXPECT_EQ(kBidder2Key, result_.winning_group_id); |
| EXPECT_EQ(GURL("https://ad2.com/"), result_.ad_descriptor->url); |
| CheckHistograms(InterestGroupAuction::AuctionResult::kSuccess, |
| /*expected_interest_groups=*/2, /*expected_owners=*/2, |
| /*expected_sellers=*/1); |
| } |
| |
| // An auction where one bid is successful, another's script 404s. |
| TEST_F(AuctionRunnerTest, OneBidOne404) { |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder1Url, |
| MakeBidScript(kSeller, "1", "https://ad1.com/", /*num_ad_components=*/2, |
| kBidder1, kBidder1Name, |
| /*has_signals=*/true, "k1", "a")); |
| url_loader_factory_.AddResponse(kBidder2Url.spec(), "", net::HTTP_NOT_FOUND); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| MakeAuctionScript()); |
| auction_worklet::AddBidderJsonResponse( |
| &url_loader_factory_, |
| GURL(kBidder1TrustedSignalsUrl.spec() + |
| "?hostname=publisher1.com&keys=k1,k2" |
| "&interestGroupNames=Ad+Platform"), |
| kBidder1SignalsJson); |
| auction_worklet::AddBidderJsonResponse( |
| &url_loader_factory_, |
| GURL(kBidder2TrustedSignalsUrl.spec() + |
| "?hostname=publisher1.com&keys=l1,l2" |
| "&interestGroupNames=Another+Ad+Thing"), |
| kBidder2SignalsJson); |
| |
| RunStandardAuction(); |
| EXPECT_EQ(kBidder1Key, result_.winning_group_id); |
| EXPECT_EQ(GURL("https://ad1.com/"), result_.ad_descriptor->url); |
| EXPECT_EQ(std::vector<blink::AdDescriptor>{blink::AdDescriptor( |
| GURL("https://ad1.com-component1.com"))}, |
| result_.ad_component_descriptors); |
| EXPECT_THAT(result_.report_urls, |
| testing::UnorderedElementsAre( |
| GURL("https://reporting.example.com/1"), |
| GURL("https://buyer-reporting.example.com/1"))); |
| EXPECT_THAT( |
| result_.ad_beacon_map, |
| testing::UnorderedElementsAre( |
| testing::Pair(ReportingDestination::kSeller, |
| testing::ElementsAre(testing::Pair( |
| "click", GURL("https://reporting.example.com/2")))), |
| testing::Pair(ReportingDestination::kComponentSeller, |
| testing::ElementsAre()), |
| testing::Pair( |
| ReportingDestination::kBuyer, |
| testing::ElementsAre(testing::Pair( |
| "click", GURL("https://buyer-reporting.example.com/2")))))); |
| EXPECT_THAT( |
| private_aggregation_manager_.TakePrivateAggregationRequests(), |
| testing::UnorderedElementsAre( |
| testing::Pair( |
| kBidder1, |
| ElementsAreRequests(kExpectedGenerateBidPrivateAggregationRequest, |
| kExpectedReportWinPrivateAggregationRequest)), |
| testing::Pair(kSeller, |
| ElementsAreRequests( |
| kExpectedScoreAdPrivateAggregationRequest, |
| kExpectedReportResultPrivateAggregationRequest)))); |
| EXPECT_THAT( |
| result_.private_aggregation_event_map, |
| testing::UnorderedElementsAre(testing::Pair( |
| "click", |
| ElementsAreRequests( |
| BuildPrivateAggregationRequest(/*bucket=*/10, /*value=*/21), |
| BuildPrivateAggregationRequest(/*bucket=*/30, /*value=*/41))))); |
| |
| EXPECT_THAT(result_.interest_groups_that_bid, |
| testing::UnorderedElementsAre(kBidder1Key)); |
| EXPECT_EQ(R"({"render_url":"https://ad1.com/","metadata":{"ads": true}})", |
| result_.winning_group_ad_metadata); |
| EXPECT_THAT( |
| result_.errors, |
| testing::ElementsAre("Failed to load https://anotheradthing.com/bids.js " |
| "HTTP status = 404 Not Found.")); |
| CheckHistograms(InterestGroupAuction::AuctionResult::kSuccess, |
| /*expected_interest_groups=*/2, /*expected_owners=*/2, |
| /*expected_sellers=*/1); |
| |
| // 404 is detected after the worklet is created, so there are still events |
| // for it. |
| EXPECT_THAT(observer_log_, |
| testing::UnorderedElementsAre( |
| "Create https://adstuff.publisher1.com/auction.js", |
| "Create https://adplatform.com/offers.js", |
| "Create https://anotheradthing.com/bids.js", |
| "Destroy https://adplatform.com/offers.js", |
| "Destroy https://anotheradthing.com/bids.js", |
| "Destroy https://adstuff.publisher1.com/auction.js", |
| "Create https://adplatform.com/offers.js", |
| "Destroy https://adplatform.com/offers.js")); |
| } |
| |
| // An auction where one component seller fails to load, but the other loads, so |
| // the auction succeeds. |
| TEST_F(AuctionRunnerTest, ComponentAuctionOneSeller404) { |
| SetUpComponentAuctionAndResponses(/*bidder1_seller=*/kComponentSeller1, |
| /*bidder2_seller=*/kComponentSeller2, |
| /*bid_from_component_auction_wins=*/true); |
| url_loader_factory_.AddResponse(kComponentSeller2Url.spec(), "", |
| net::HTTP_NOT_FOUND); |
| |
| RunStandardAuction(); |
| EXPECT_THAT(result_.errors, |
| testing::ElementsAre( |
| "Failed to load https://component.seller2.test/bar.js " |
| "HTTP status = 404 Not Found.")); |
| EXPECT_EQ(GURL("https://ad1.com/"), result_.ad_descriptor->url); |
| EXPECT_THAT(result_.report_urls, |
| testing::UnorderedElementsAre( |
| GURL("https://reporting.example.com/1"), |
| GURL("https://component1-report.test/1"), |
| GURL("https://buyer-reporting.example.com/1"))); |
| EXPECT_THAT( |
| result_.ad_beacon_map, |
| testing::UnorderedElementsAre( |
| testing::Pair(ReportingDestination::kSeller, |
| testing::ElementsAre(testing::Pair( |
| "click", GURL("https://reporting.example.com/2")))), |
| testing::Pair( |
| ReportingDestination::kComponentSeller, |
| testing::ElementsAre(testing::Pair( |
| "click", GURL("https://component1-report.test/2")))), |
| testing::Pair( |
| ReportingDestination::kBuyer, |
| testing::ElementsAre(testing::Pair( |
| "click", GURL("https://buyer-reporting.example.com/2")))))); |
| EXPECT_THAT( |
| private_aggregation_manager_.TakePrivateAggregationRequests(), |
| testing::UnorderedElementsAre( |
| testing::Pair( |
| kBidder1, |
| ElementsAreRequests(kExpectedGenerateBidPrivateAggregationRequest, |
| kExpectedReportWinPrivateAggregationRequest)), |
| testing::Pair(kSeller, |
| ElementsAreRequests( |
| kExpectedScoreAdPrivateAggregationRequest, |
| kExpectedReportResultPrivateAggregationRequest)), |
| testing::Pair(kComponentSeller1, |
| ElementsAreRequests( |
| kExpectedScoreAdPrivateAggregationRequest, |
| kExpectedReportResultPrivateAggregationRequest)))); |
| EXPECT_THAT( |
| result_.private_aggregation_event_map, |
| testing::UnorderedElementsAre(testing::Pair( |
| "click", |
| ElementsAreRequests( |
| BuildPrivateAggregationRequest(/*bucket=*/10, /*value=*/21), |
| BuildPrivateAggregationRequest(/*bucket=*/30, /*value=*/41))))); |
| |
| // The bid send to the failing component seller worklet isn't counted, |
| // regardless of whether the bid completed before the worklet failed to load. |
| EXPECT_THAT(result_.interest_groups_that_bid, |
| testing::UnorderedElementsAre(kBidder1Key)); |
| CheckHistograms(InterestGroupAuction::AuctionResult::kSuccess, |
| /*expected_interest_groups=*/2, /*expected_owners=*/2, |
| /*expected_sellers=*/3); |
| } |
| |
| // An auction where one bid is successful, another's script does not provide a |
| // bidding function. |
| TEST_F(AuctionRunnerTest, OneBidOneNotMade) { |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder1Url, |
| MakeBidScript(kSeller, "1", "https://ad1.com/", /*num_ad_components=*/2, |
| kBidder1, kBidder1Name, |
| /*has_signals=*/true, "k1", "a")); |
| |
| // The auction script doesn't make any bids. |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kBidder2Url, |
| MakeAuctionScript()); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| MakeAuctionScript()); |
| auction_worklet::AddBidderJsonResponse( |
| &url_loader_factory_, |
| GURL(kBidder1TrustedSignalsUrl.spec() + |
| "?hostname=publisher1.com&keys=k1,k2" |
| "&interestGroupNames=Ad+Platform"), |
| kBidder1SignalsJson); |
| auction_worklet::AddBidderJsonResponse( |
| &url_loader_factory_, |
| GURL(kBidder2TrustedSignalsUrl.spec() + |
| "?hostname=publisher1.com&keys=l1,l2" |
| "&interestGroupNames=Another+Ad+Thing"), |
| kBidder2SignalsJson); |
| |
| RunStandardAuction(); |
| EXPECT_EQ(kBidder1Key, result_.winning_group_id); |
| EXPECT_EQ(GURL("https://ad1.com/"), result_.ad_descriptor->url); |
| EXPECT_EQ(std::vector<blink::AdDescriptor>{blink::AdDescriptor( |
| GURL("https://ad1.com-component1.com"))}, |
| result_.ad_component_descriptors); |
| EXPECT_THAT(result_.report_urls, |
| testing::UnorderedElementsAre( |
| GURL("https://reporting.example.com/1"), |
| GURL("https://buyer-reporting.example.com/1"))); |
| EXPECT_THAT( |
| result_.ad_beacon_map, |
| testing::UnorderedElementsAre( |
| testing::Pair(ReportingDestination::kSeller, |
| testing::ElementsAre(testing::Pair( |
| "click", GURL("https://reporting.example.com/2")))), |
| testing::Pair(ReportingDestination::kComponentSeller, |
| testing::ElementsAre()), |
| testing::Pair( |
| ReportingDestination::kBuyer, |
| testing::ElementsAre(testing::Pair( |
| "click", GURL("https://buyer-reporting.example.com/2")))))); |
| EXPECT_THAT( |
| private_aggregation_manager_.TakePrivateAggregationRequests(), |
| testing::UnorderedElementsAre( |
| testing::Pair( |
| kBidder1, |
| ElementsAreRequests(kExpectedGenerateBidPrivateAggregationRequest, |
| kExpectedReportWinPrivateAggregationRequest)), |
| testing::Pair(kSeller, |
| ElementsAreRequests( |
| kExpectedScoreAdPrivateAggregationRequest, |
| kExpectedReportResultPrivateAggregationRequest)))); |
| EXPECT_THAT( |
| result_.private_aggregation_event_map, |
| testing::UnorderedElementsAre(testing::Pair( |
| "click", |
| ElementsAreRequests( |
| BuildPrivateAggregationRequest(/*bucket=*/10, /*value=*/21), |
| BuildPrivateAggregationRequest(/*bucket=*/30, /*value=*/41))))); |
| |
| EXPECT_THAT(result_.interest_groups_that_bid, |
| testing::UnorderedElementsAre(kBidder1Key)); |
| EXPECT_EQ(R"({"render_url":"https://ad1.com/","metadata":{"ads": true}})", |
| result_.winning_group_ad_metadata); |
| EXPECT_THAT(result_.errors, |
| testing::ElementsAre("https://anotheradthing.com/bids.js " |
| "`generateBid` is not a function.")); |
| CheckHistograms(InterestGroupAuction::AuctionResult::kSuccess, |
| /*expected_interest_groups=*/2, /*expected_owners=*/2, |
| /*expected_sellers=*/1); |
| } |
| |
| // An auction where no bidding scripts load successfully. |
| TEST_F(AuctionRunnerTest, NoBids) { |
| url_loader_factory_.AddResponse(kBidder1Url.spec(), "", net::HTTP_NOT_FOUND); |
| url_loader_factory_.AddResponse(kBidder2Url.spec(), "", net::HTTP_NOT_FOUND); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| MakeAuctionScript()); |
| auction_worklet::AddBidderJsonResponse( |
| &url_loader_factory_, |
| GURL(kBidder1TrustedSignalsUrl.spec() + |
| "?hostname=publisher1.com&keys=k1,k2" |
| "&interestGroupNames=Ad+Platform"), |
| kBidder1SignalsJson); |
| auction_worklet::AddBidderJsonResponse( |
| &url_loader_factory_, |
| GURL(kBidder2TrustedSignalsUrl.spec() + |
| "?hostname=publisher1.com&keys=l1,l2" |
| "&interestGroupNames=Another+Ad+Thing"), |
| kBidder2SignalsJson); |
| |
| RunStandardAuction(); |
| EXPECT_FALSE(result_.winning_group_id); |
| EXPECT_FALSE(result_.ad_descriptor); |
| EXPECT_TRUE(result_.ad_component_descriptors.empty()); |
| EXPECT_TRUE( |
| private_aggregation_manager_.TakePrivateAggregationRequests().empty()); |
| EXPECT_TRUE(result_.private_aggregation_event_map.empty()); |
| EXPECT_THAT(result_.interest_groups_that_bid, |
| testing::UnorderedElementsAre()); |
| EXPECT_THAT(result_.errors, |
| testing::UnorderedElementsAre( |
| "Failed to load https://adplatform.com/offers.js " |
| "HTTP status = 404 Not Found.", |
| "Failed to load https://anotheradthing.com/bids.js " |
| "HTTP status = 404 Not Found.")); |
| CheckHistograms(InterestGroupAuction::AuctionResult::kNoBids, |
| /*expected_interest_groups=*/2, /*expected_owners=*/2, |
| /*expected_sellers=*/1); |
| } |
| |
| // An auction where none of the bidding scripts has a valid bidding function. |
| TEST_F(AuctionRunnerTest, NoBidMadeByScript) { |
| // MakeAuctionScript() is a valid script that doesn't have a bidding function. |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kBidder1Url, |
| MakeAuctionScript()); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kBidder2Url, |
| MakeAuctionScript()); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| MakeAuctionScript()); |
| auction_worklet::AddBidderJsonResponse( |
| &url_loader_factory_, |
| GURL(kBidder1TrustedSignalsUrl.spec() + |
| "?hostname=publisher1.com&keys=k1,k2" |
| "&interestGroupNames=Ad+Platform"), |
| kBidder1SignalsJson); |
| auction_worklet::AddBidderJsonResponse( |
| &url_loader_factory_, |
| GURL(kBidder2TrustedSignalsUrl.spec() + |
| "?hostname=publisher1.com&keys=l1,l2" |
| "&interestGroupNames=Another+Ad+Thing"), |
| kBidder2SignalsJson); |
| |
| RunStandardAuction(); |
| EXPECT_FALSE(result_.winning_group_id); |
| EXPECT_FALSE(result_.ad_descriptor); |
| EXPECT_TRUE(result_.ad_component_descriptors.empty()); |
| EXPECT_TRUE( |
| private_aggregation_manager_.TakePrivateAggregationRequests().empty()); |
| EXPECT_TRUE(result_.private_aggregation_event_map.empty()); |
| EXPECT_THAT(result_.interest_groups_that_bid, |
| testing::UnorderedElementsAre()); |
| EXPECT_THAT( |
| result_.errors, |
| testing::UnorderedElementsAre( |
| "https://adplatform.com/offers.js `generateBid` is not a function.", |
| "https://anotheradthing.com/bids.js `generateBid` is not a " |
| "function.")); |
| CheckHistograms(InterestGroupAuction::AuctionResult::kNoBids, |
| /*expected_interest_groups=*/2, /*expected_owners=*/2, |
| /*expected_sellers=*/1); |
| } |
| |
| // An auction where the seller script doesn't have a scoring function. |
| TEST_F(AuctionRunnerTest, SellerRejectsAll) { |
| std::string bid_script1 = |
| MakeBidScript(kSeller, "1", "https://ad1.com/", /*num_ad_components=*/2, |
| kBidder1, kBidder1Name, |
| /*has_signals=*/true, "k1", "a"); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kBidder1Url, |
| bid_script1); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder2Url, |
| MakeBidScript(kSeller, "2", "https://ad2.com/", /*num_ad_components=*/2, |
| kBidder2, kBidder2Name, |
| /*has_signals=*/true, "l2", "b")); |
| |
| // No seller scoring function in a bid script. |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| bid_script1); |
| auction_worklet::AddBidderJsonResponse( |
| &url_loader_factory_, |
| GURL(kBidder1TrustedSignalsUrl.spec() + |
| "?hostname=publisher1.com&keys=k1,k2" |
| "&interestGroupNames=Ad+Platform"), |
| kBidder1SignalsJson); |
| auction_worklet::AddBidderJsonResponse( |
| &url_loader_factory_, |
| GURL(kBidder2TrustedSignalsUrl.spec() + |
| "?hostname=publisher1.com&keys=l1,l2" |
| "&interestGroupNames=Another+Ad+Thing"), |
| kBidder2SignalsJson); |
| |
| RunStandardAuction(); |
| EXPECT_FALSE(result_.winning_group_id); |
| EXPECT_FALSE(result_.ad_descriptor); |
| EXPECT_TRUE(result_.ad_component_descriptors.empty()); |
| EXPECT_THAT( |
| private_aggregation_manager_.TakePrivateAggregationRequests(), |
| testing::UnorderedElementsAre( |
| testing::Pair(kBidder1, |
| ElementsAreRequests( |
| kExpectedGenerateBidPrivateAggregationRequest)), |
| testing::Pair(kBidder2, |
| ElementsAreRequests( |
| kExpectedGenerateBidPrivateAggregationRequest)))); |
| EXPECT_TRUE(result_.private_aggregation_event_map.empty()); |
| EXPECT_THAT(result_.interest_groups_that_bid, |
| testing::UnorderedElementsAre(kBidder1Key, kBidder2Key)); |
| EXPECT_THAT(result_.errors, testing::UnorderedElementsAre( |
| "https://adstuff.publisher1.com/auction.js " |
| "`scoreAd` is not a function.", |
| "https://adstuff.publisher1.com/auction.js " |
| "`scoreAd` is not a function.")); |
| CheckHistograms(InterestGroupAuction::AuctionResult::kAllBidsRejected, |
| /*expected_interest_groups=*/2, /*expected_owners=*/2, |
| /*expected_sellers=*/1); |
| } |
| |
| // An auction where seller rejects one bid when scoring. |
| TEST_F(AuctionRunnerTest, SellerRejectsOne) { |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder1Url, |
| MakeBidScript(kSeller, "1", "https://ad1.com/", /*num_ad_components=*/2, |
| kBidder1, kBidder1Name, |
| /*has_signals=*/true, "k1", "a")); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder2Url, |
| MakeBidScript(kSeller, "2", "https://ad2.com/", /*num_ad_components=*/2, |
| kBidder2, kBidder2Name, |
| /*has_signals=*/true, "l2", "b")); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| MakeAuctionScriptReject2()); |
| auction_worklet::AddBidderJsonResponse( |
| &url_loader_factory_, |
| GURL(kBidder1TrustedSignalsUrl.spec() + |
| "?hostname=publisher1.com&keys=k1,k2" |
| "&interestGroupNames=Ad+Platform"), |
| kBidder1SignalsJson); |
| auction_worklet::AddBidderJsonResponse( |
| &url_loader_factory_, |
| GURL(kBidder2TrustedSignalsUrl.spec() + |
| "?hostname=publisher1.com&keys=l1,l2" |
| "&interestGroupNames=Another+Ad+Thing"), |
| kBidder2SignalsJson); |
| |
| RunStandardAuction(); |
| EXPECT_EQ(kBidder1Key, result_.winning_group_id); |
| EXPECT_EQ(GURL("https://ad1.com/"), result_.ad_descriptor->url); |
| EXPECT_EQ(std::vector<blink::AdDescriptor>{blink::AdDescriptor( |
| GURL("https://ad1.com-component1.com"))}, |
| result_.ad_component_descriptors); |
| EXPECT_THAT(result_.report_urls, |
| testing::UnorderedElementsAre( |
| GURL("https://reporting.example.com/1"), |
| GURL("https://buyer-reporting.example.com/1"))); |
| EXPECT_THAT( |
| result_.ad_beacon_map, |
| testing::UnorderedElementsAre( |
| testing::Pair(ReportingDestination::kSeller, |
| testing::ElementsAre(testing::Pair( |
| "click", GURL("https://reporting.example.com/2")))), |
| testing::Pair(ReportingDestination::kComponentSeller, |
| testing::ElementsAre()), |
| testing::Pair( |
| ReportingDestination::kBuyer, |
| testing::ElementsAre(testing::Pair( |
| "click", GURL("https://buyer-reporting.example.com/2")))))); |
| EXPECT_THAT( |
| private_aggregation_manager_.TakePrivateAggregationRequests(), |
| testing::UnorderedElementsAre( |
| testing::Pair( |
| kBidder1, |
| ElementsAreRequests(kExpectedGenerateBidPrivateAggregationRequest, |
| kExpectedReportWinPrivateAggregationRequest)), |
| testing::Pair(kBidder2, |
| ElementsAreRequests( |
| kExpectedGenerateBidPrivateAggregationRequest)), |
| testing::Pair(kSeller, |
| ElementsAreRequests( |
| kExpectedScoreAdPrivateAggregationRequest, |
| kExpectedScoreAdPrivateAggregationRequest, |
| kExpectedReportResultPrivateAggregationRequest)))); |
| EXPECT_THAT( |
| result_.private_aggregation_event_map, |
| testing::UnorderedElementsAre(testing::Pair( |
| "click", |
| ElementsAreRequests( |
| BuildPrivateAggregationRequest(/*bucket=*/10, /*value=*/21), |
| BuildPrivateAggregationRequest(/*bucket=*/30, /*value=*/41))))); |
| |
| EXPECT_THAT(result_.interest_groups_that_bid, |
| testing::UnorderedElementsAre(kBidder1Key, kBidder2Key)); |
| EXPECT_EQ(R"({"render_url":"https://ad1.com/","metadata":{"ads": true}})", |
| result_.winning_group_ad_metadata); |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| CheckHistograms(InterestGroupAuction::AuctionResult::kSuccess, |
| /*expected_interest_groups=*/2, /*expected_owners=*/2, |
| /*expected_sellers=*/1); |
| } |
| |
| // An auction where the seller script fails to load. |
| TEST_F(AuctionRunnerTest, NoSellerScript) { |
| // Tests to make sure that if seller script fails the other fetches are |
| // cancelled, too. |
| url_loader_factory_.AddResponse(kSellerUrl.spec(), "", net::HTTP_NOT_FOUND); |
| RunStandardAuction(); |
| EXPECT_FALSE(result_.winning_group_id); |
| EXPECT_FALSE(result_.ad_descriptor); |
| EXPECT_TRUE(result_.ad_component_descriptors.empty()); |
| EXPECT_TRUE( |
| private_aggregation_manager_.TakePrivateAggregationRequests().empty()); |
| EXPECT_TRUE(result_.private_aggregation_event_map.empty()); |
| |
| EXPECT_EQ(0, url_loader_factory_.NumPending()); |
| EXPECT_THAT(result_.interest_groups_that_bid, |
| testing::UnorderedElementsAre()); |
| EXPECT_THAT(result_.errors, |
| testing::ElementsAre( |
| "Failed to load https://adstuff.publisher1.com/auction.js " |
| "HTTP status = 404 Not Found.")); |
| CheckHistograms(InterestGroupAuction::AuctionResult::kSellerWorkletLoadFailed, |
| /*expected_interest_groups=*/2, /*expected_owners=*/2, |
| /*expected_sellers=*/1); |
| } |
| |
| // An auction where bidders don't request trusted bidding signals. |
| TEST_F(AuctionRunnerTest, NoTrustedBiddingSignals) { |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder1Url, |
| MakeBidScript(kSeller, "1", "https://ad1.com/", /*num_ad_components=*/0, |
| kBidder1, kBidder1Name, |
| /*has_signals=*/false, "k1", "a")); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder2Url, |
| MakeBidScript(kSeller, "2", "https://ad2.com/", /*num_ad_components=*/0, |
| kBidder2, kBidder2Name, |
| /*has_signals=*/false, "l2", "b")); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| MakeAuctionScript()); |
| |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup(kBidder1, kBidder1Name, kBidder1Url, |
| absl::nullopt, {"k1", "k2"}, |
| GURL("https://ad1.com"))); |
| bidders.emplace_back(MakeInterestGroup(kBidder2, kBidder2Name, kBidder2Url, |
| absl::nullopt, {"l1", "l2"}, |
| GURL("https://ad2.com"))); |
| |
| RunAuctionAndWait(kSellerUrl, std::move(bidders)); |
| |
| EXPECT_EQ(kBidder2Key, result_.winning_group_id); |
| EXPECT_EQ(GURL("https://ad2.com/"), result_.ad_descriptor->url); |
| EXPECT_TRUE(result_.ad_component_descriptors.empty()); |
| EXPECT_THAT(result_.report_urls, |
| testing::UnorderedElementsAre( |
| GURL("https://reporting.example.com/2"), |
| GURL("https://buyer-reporting.example.com/2"))); |
| EXPECT_THAT( |
| result_.ad_beacon_map, |
| testing::UnorderedElementsAre( |
| testing::Pair(ReportingDestination::kSeller, |
| testing::ElementsAre(testing::Pair( |
| "click", GURL("https://reporting.example.com/4")))), |
| testing::Pair(ReportingDestination::kComponentSeller, |
| testing::ElementsAre()), |
| testing::Pair( |
| ReportingDestination::kBuyer, |
| testing::ElementsAre(testing::Pair( |
| "click", GURL("https://buyer-reporting.example.com/4")))))); |
| EXPECT_THAT( |
| private_aggregation_manager_.TakePrivateAggregationRequests(), |
| testing::UnorderedElementsAre( |
| testing::Pair(kBidder1, |
| ElementsAreRequests( |
| kExpectedGenerateBidPrivateAggregationRequest)), |
| testing::Pair( |
| kBidder2, |
| ElementsAreRequests(kExpectedGenerateBidPrivateAggregationRequest, |
| kExpectedReportWinPrivateAggregationRequest)), |
| testing::Pair(kSeller, |
| ElementsAreRequests( |
| kExpectedScoreAdPrivateAggregationRequest, |
| kExpectedScoreAdPrivateAggregationRequest, |
| kExpectedReportResultPrivateAggregationRequest)))); |
| EXPECT_THAT( |
| result_.private_aggregation_event_map, |
| testing::UnorderedElementsAre(testing::Pair( |
| "click", |
| ElementsAreRequests( |
| BuildPrivateAggregationRequest(/*bucket=*/10, /*value=*/22), |
| BuildPrivateAggregationRequest(/*bucket=*/30, /*value=*/42))))); |
| |
| EXPECT_THAT(result_.interest_groups_that_bid, |
| testing::UnorderedElementsAre(kBidder1Key, kBidder2Key)); |
| EXPECT_EQ(R"({"render_url":"https://ad2.com/"})", |
| result_.winning_group_ad_metadata); |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| CheckHistograms(InterestGroupAuction::AuctionResult::kSuccess, |
| /*expected_interest_groups=*/2, /*expected_owners=*/2, |
| /*expected_sellers=*/1); |
| } |
| |
| // An auction where trusted bidding signals are requested, but the fetch 404s. |
| TEST_F(AuctionRunnerTest, TrustedBiddingSignals404) { |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder1Url, |
| MakeBidScript(kSeller, "1", "https://ad1.com/", /*num_ad_components=*/2, |
| kBidder1, kBidder1Name, |
| /*has_signals=*/false, "k1", "a")); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder2Url, |
| MakeBidScript(kSeller, "2", "https://ad2.com/", /*num_ad_components=*/2, |
| kBidder2, kBidder2Name, |
| /*has_signals=*/false, "l2", "b")); |
| url_loader_factory_.AddResponse(kBidder1TrustedSignalsUrl.spec() + |
| "?hostname=publisher1.com&keys=k1,k2" |
| "&interestGroupNames=Ad+Platform", |
| "", net::HTTP_NOT_FOUND); |
| url_loader_factory_.AddResponse(kBidder2TrustedSignalsUrl.spec() + |
| "?hostname=publisher1.com&keys=l1,l2" |
| "&interestGroupNames=Another+Ad+Thing", |
| "", net::HTTP_NOT_FOUND); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| MakeAuctionScript()); |
| |
| RunStandardAuction(); |
| EXPECT_EQ(kBidder2Key, result_.winning_group_id); |
| EXPECT_EQ(GURL("https://ad2.com/"), result_.ad_descriptor->url); |
| EXPECT_EQ(std::vector<blink::AdDescriptor>{blink::AdDescriptor( |
| GURL("https://ad2.com-component1.com"))}, |
| result_.ad_component_descriptors); |
| EXPECT_THAT(result_.report_urls, |
| testing::UnorderedElementsAre( |
| GURL("https://reporting.example.com/2"), |
| GURL("https://buyer-reporting.example.com/2"))); |
| EXPECT_THAT( |
| result_.ad_beacon_map, |
| testing::UnorderedElementsAre( |
| testing::Pair(ReportingDestination::kSeller, |
| testing::ElementsAre(testing::Pair( |
| "click", GURL("https://reporting.example.com/4")))), |
| testing::Pair(ReportingDestination::kComponentSeller, |
| testing::ElementsAre()), |
| testing::Pair( |
| ReportingDestination::kBuyer, |
| testing::ElementsAre(testing::Pair( |
| "click", GURL("https://buyer-reporting.example.com/4")))))); |
| EXPECT_THAT( |
| private_aggregation_manager_.TakePrivateAggregationRequests(), |
| testing::UnorderedElementsAre( |
| testing::Pair(kBidder1, |
| ElementsAreRequests( |
| kExpectedGenerateBidPrivateAggregationRequest)), |
| testing::Pair( |
| kBidder2, |
| ElementsAreRequests(kExpectedGenerateBidPrivateAggregationRequest, |
| kExpectedReportWinPrivateAggregationRequest)), |
| testing::Pair(kSeller, |
| ElementsAreRequests( |
| kExpectedScoreAdPrivateAggregationRequest, |
| kExpectedScoreAdPrivateAggregationRequest, |
| kExpectedReportResultPrivateAggregationRequest)))); |
| EXPECT_THAT( |
| result_.private_aggregation_event_map, |
| testing::UnorderedElementsAre(testing::Pair( |
| "click", |
| ElementsAreRequests( |
| BuildPrivateAggregationRequest(/*bucket=*/10, /*value=*/22), |
| BuildPrivateAggregationRequest(/*bucket=*/30, /*value=*/42))))); |
| |
| EXPECT_THAT(result_.interest_groups_that_bid, |
| testing::UnorderedElementsAre(kBidder1Key, kBidder2Key)); |
| EXPECT_EQ(R"({"render_url":"https://ad2.com/"})", |
| result_.winning_group_ad_metadata); |
| EXPECT_THAT(result_.errors, testing::UnorderedElementsAre( |
| "Failed to load " |
| "https://adplatform.com/" |
| "signals1?hostname=publisher1.com&keys=k1,k2&" |
| "interestGroupNames=Ad+Platform " |
| "HTTP status = 404 Not Found.", |
| "Failed to load " |
| "https://anotheradthing.com/" |
| "signals2?hostname=publisher1.com&keys=l1,l2" |
| "&interestGroupNames=Another+Ad+Thing " |
| "HTTP status = 404 Not Found.")); |
| CheckHistograms(InterestGroupAuction::AuctionResult::kSuccess, |
| /*expected_interest_groups=*/2, /*expected_owners=*/2, |
| /*expected_sellers=*/1); |
| } |
| |
| // A successful auction where seller reporting worklet doesn't set a URL. |
| TEST_F(AuctionRunnerTest, NoReportResultUrl) { |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder1Url, |
| MakeBidScript(kSeller, "1", "https://ad1.com/", /*num_ad_components=*/2, |
| kBidder1, kBidder1Name, |
| /*has_signals=*/true, "k1", "a")); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder2Url, |
| MakeBidScript(kSeller, "2", "https://ad2.com/", /*num_ad_components=*/2, |
| kBidder2, kBidder2Name, |
| /*has_signals=*/true, "l2", "b")); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| MakeAuctionScriptNoReportUrl()); |
| auction_worklet::AddBidderJsonResponse( |
| &url_loader_factory_, |
| GURL(kBidder1TrustedSignalsUrl.spec() + |
| "?hostname=publisher1.com&keys=k1,k2" |
| "&interestGroupNames=Ad+Platform"), |
| kBidder1SignalsJson); |
| auction_worklet::AddBidderJsonResponse( |
| &url_loader_factory_, |
| GURL(kBidder2TrustedSignalsUrl.spec() + |
| "?hostname=publisher1.com&keys=l1,l2" |
| "&interestGroupNames=Another+Ad+Thing"), |
| kBidder2SignalsJson); |
| |
| RunStandardAuction(); |
| EXPECT_EQ(kBidder2Key, result_.winning_group_id); |
| EXPECT_EQ(GURL("https://ad2.com/"), result_.ad_descriptor->url); |
| EXPECT_EQ(std::vector<blink::AdDescriptor>{blink::AdDescriptor( |
| GURL("https://ad2.com-component1.com"))}, |
| result_.ad_component_descriptors); |
| EXPECT_THAT(result_.report_urls, |
| testing::UnorderedElementsAre( |
| GURL("https://buyer-reporting.example.com/2"))); |
| EXPECT_THAT( |
| result_.ad_beacon_map, |
| testing::UnorderedElementsAre( |
| testing::Pair(ReportingDestination::kSeller, testing::ElementsAre()), |
| testing::Pair(ReportingDestination::kComponentSeller, |
| testing::ElementsAre()), |
| testing::Pair( |
| ReportingDestination::kBuyer, |
| testing::ElementsAre(testing::Pair( |
| "click", GURL("https://buyer-reporting.example.com/4")))))); |
| EXPECT_THAT( |
| private_aggregation_manager_.TakePrivateAggregationRequests(), |
| testing::UnorderedElementsAre( |
| testing::Pair(kBidder1, |
| ElementsAreRequests( |
| kExpectedGenerateBidPrivateAggregationRequest)), |
| testing::Pair( |
| kBidder2, |
| ElementsAreRequests(kExpectedGenerateBidPrivateAggregationRequest, |
| kExpectedReportWinPrivateAggregationRequest)), |
| testing::Pair(kSeller, |
| ElementsAreRequests( |
| kExpectedScoreAdPrivateAggregationRequest, |
| kExpectedScoreAdPrivateAggregationRequest, |
| kExpectedReportResultPrivateAggregationRequest)))); |
| EXPECT_THAT( |
| result_.private_aggregation_event_map, |
| testing::UnorderedElementsAre(testing::Pair( |
| "click", |
| ElementsAreRequests( |
| BuildPrivateAggregationRequest(/*bucket=*/10, /*value=*/22), |
| BuildPrivateAggregationRequest(/*bucket=*/30, /*value=*/42))))); |
| |
| EXPECT_THAT(result_.interest_groups_that_bid, |
| testing::UnorderedElementsAre(kBidder1Key, kBidder2Key)); |
| EXPECT_EQ(R"({"render_url":"https://ad2.com/"})", |
| result_.winning_group_ad_metadata); |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| CheckHistograms(InterestGroupAuction::AuctionResult::kSuccess, |
| /*expected_interest_groups=*/2, /*expected_owners=*/2, |
| /*expected_sellers=*/1); |
| } |
| |
| // A successful auction where bidder reporting worklet doesn't set a URL. |
| TEST_F(AuctionRunnerTest, NoReportWinUrl) { |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder1Url, |
| MakeBidScript(kSeller, "1", "https://ad1.com/", /*num_ad_components=*/2, |
| kBidder1, kBidder1Name, |
| /*has_signals=*/true, "k1", "a") + |
| kReportWinNoUrl); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder2Url, |
| MakeBidScript(kSeller, "2", "https://ad2.com/", /*num_ad_components=*/2, |
| kBidder2, kBidder2Name, |
| /*has_signals=*/true, "l2", "b") + |
| kReportWinNoUrl); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| MakeAuctionScript()); |
| auction_worklet::AddBidderJsonResponse( |
| &url_loader_factory_, |
| GURL(kBidder1TrustedSignalsUrl.spec() + |
| "?hostname=publisher1.com&keys=k1,k2" |
| "&interestGroupNames=Ad+Platform"), |
| kBidder1SignalsJson); |
| auction_worklet::AddBidderJsonResponse( |
| &url_loader_factory_, |
| GURL(kBidder2TrustedSignalsUrl.spec() + |
| "?hostname=publisher1.com&keys=l1,l2" |
| "&interestGroupNames=Another+Ad+Thing"), |
| kBidder2SignalsJson); |
| |
| RunStandardAuction(); |
| EXPECT_EQ(kBidder2Key, result_.winning_group_id); |
| EXPECT_EQ(GURL("https://ad2.com/"), result_.ad_descriptor->url); |
| EXPECT_EQ(std::vector<blink::AdDescriptor>{blink::AdDescriptor( |
| GURL("https://ad2.com-component1.com"))}, |
| result_.ad_component_descriptors); |
| EXPECT_THAT( |
| result_.report_urls, |
| testing::UnorderedElementsAre(GURL("https://reporting.example.com/2"))); |
| EXPECT_THAT( |
| result_.ad_beacon_map, |
| testing::UnorderedElementsAre( |
| testing::Pair(ReportingDestination::kSeller, |
| testing::ElementsAre(testing::Pair( |
| "click", GURL("https://reporting.example.com/4")))), |
| testing::Pair(ReportingDestination::kComponentSeller, |
| testing::ElementsAre()), |
| testing::Pair(ReportingDestination::kBuyer, testing::ElementsAre()))); |
| EXPECT_THAT( |
| private_aggregation_manager_.TakePrivateAggregationRequests(), |
| testing::UnorderedElementsAre( |
| testing::Pair(kBidder1, |
| ElementsAreRequests( |
| kExpectedGenerateBidPrivateAggregationRequest)), |
| testing::Pair(kBidder2, |
| ElementsAreRequests( |
| // ReportWin script override doesn't send a report |
| kExpectedGenerateBidPrivateAggregationRequest)), |
| testing::Pair(kSeller, |
| ElementsAreRequests( |
| kExpectedScoreAdPrivateAggregationRequest, |
| kExpectedScoreAdPrivateAggregationRequest, |
| kExpectedReportResultPrivateAggregationRequest)))); |
| EXPECT_THAT(result_.private_aggregation_event_map, |
| testing::UnorderedElementsAre(testing::Pair( |
| "click", ElementsAreRequests(BuildPrivateAggregationRequest( |
| /*bucket=*/10, /*value=*/22))))); |
| |
| EXPECT_THAT(result_.interest_groups_that_bid, |
| testing::UnorderedElementsAre(kBidder1Key, kBidder2Key)); |
| EXPECT_EQ(R"({"render_url":"https://ad2.com/"})", |
| result_.winning_group_ad_metadata); |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| CheckHistograms(InterestGroupAuction::AuctionResult::kSuccess, |
| /*expected_interest_groups=*/2, /*expected_owners=*/2, |
| /*expected_sellers=*/1); |
| } |
| |
| // A successful auction where neither reporting worklets sets a URL. |
| TEST_F(AuctionRunnerTest, NeitherReportUrl) { |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder1Url, |
| MakeBidScript(kSeller, "1", "https://ad1.com/", /*num_ad_components=*/2, |
| kBidder1, kBidder1Name, |
| /*has_signals=*/true, "k1", "a") + |
| kReportWinNoUrl); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder2Url, |
| MakeBidScript(kSeller, "2", "https://ad2.com/", /*num_ad_components=*/2, |
| kBidder2, kBidder2Name, |
| /*has_signals=*/true, "l2", "b") + |
| kReportWinNoUrl); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| MakeAuctionScriptNoReportUrl()); |
| auction_worklet::AddBidderJsonResponse( |
| &url_loader_factory_, |
| GURL(kBidder1TrustedSignalsUrl.spec() + |
| "?hostname=publisher1.com&keys=k1,k2" |
| "&interestGroupNames=Ad+Platform"), |
| kBidder1SignalsJson); |
| auction_worklet::AddBidderJsonResponse( |
| &url_loader_factory_, |
| GURL(kBidder2TrustedSignalsUrl.spec() + |
| "?hostname=publisher1.com&keys=l1,l2" |
| "&interestGroupNames=Another+Ad+Thing"), |
| kBidder2SignalsJson); |
| |
| RunStandardAuction(); |
| EXPECT_EQ(kBidder2Key, result_.winning_group_id); |
| EXPECT_EQ(GURL("https://ad2.com/"), result_.ad_descriptor->url); |
| EXPECT_EQ(std::vector<blink::AdDescriptor>{blink::AdDescriptor( |
| GURL("https://ad2.com-component1.com"))}, |
| result_.ad_component_descriptors); |
| EXPECT_THAT(result_.report_urls, testing::UnorderedElementsAre()); |
| EXPECT_THAT(result_.ad_beacon_map, |
| testing::UnorderedElementsAreArray(kEmptyAdBeaconMap)); |
| EXPECT_THAT( |
| private_aggregation_manager_.TakePrivateAggregationRequests(), |
| testing::UnorderedElementsAre( |
| testing::Pair(kBidder1, |
| ElementsAreRequests( |
| kExpectedGenerateBidPrivateAggregationRequest)), |
| testing::Pair(kBidder2, |
| ElementsAreRequests( |
| // ReportWin script override doesn't send a report |
| kExpectedGenerateBidPrivateAggregationRequest)), |
| testing::Pair(kSeller, |
| ElementsAreRequests( |
| kExpectedScoreAdPrivateAggregationRequest, |
| kExpectedScoreAdPrivateAggregationRequest, |
| kExpectedReportResultPrivateAggregationRequest)))); |
| EXPECT_THAT(result_.private_aggregation_event_map, |
| testing::UnorderedElementsAre(testing::Pair( |
| "click", ElementsAreRequests(BuildPrivateAggregationRequest( |
| /*bucket=*/10, /*value=*/22))))); |
| |
| EXPECT_THAT(result_.interest_groups_that_bid, |
| testing::UnorderedElementsAre(kBidder1Key, kBidder2Key)); |
| EXPECT_EQ(R"({"render_url":"https://ad2.com/"})", |
| result_.winning_group_ad_metadata); |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| CheckHistograms(InterestGroupAuction::AuctionResult::kSuccess, |
| /*expected_interest_groups=*/2, /*expected_owners=*/2, |
| /*expected_sellers=*/1); |
| } |
| |
| // Test the case where the seller worklet provides no signals for the winner, |
| // since it has no reportResult() method. The winning bidder's reportWin() |
| // function should be passed null as `sellerSignals`, and should still be able |
| // to send a report. |
| TEST_F(AuctionRunnerTest, NoReportResult) { |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder1Url, |
| MakeBidScript(kSeller, "1", "https://ad1.com/", /*num_ad_components=*/2, |
| kBidder1, kBidder1Name, |
| /*has_signals=*/true, "k1", "a")); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder2Url, |
| MakeBidScript(kSeller, "2", "https://ad2.com/", /*num_ad_components=*/2, |
| kBidder2, kBidder2Name, |
| /*has_signals=*/true, "l2", "b") + |
| kReportWinExpectNullAuctionSignals); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| R"( |
| function scoreAd(adMetadata, bid, auctionConfig, trustedScoringSignals, |
| browserSignals) { |
| return bid * 2; |
| } |
| )"); |
| auction_worklet::AddBidderJsonResponse( |
| &url_loader_factory_, |
| GURL(kBidder1TrustedSignalsUrl.spec() + |
| "?hostname=publisher1.com&keys=k1,k2" |
| "&interestGroupNames=Ad+Platform"), |
| kBidder1SignalsJson); |
| auction_worklet::AddBidderJsonResponse( |
| &url_loader_factory_, |
| GURL(kBidder2TrustedSignalsUrl.spec() + |
| "?hostname=publisher1.com&keys=l1,l2" |
| "&interestGroupNames=Another+Ad+Thing"), |
| kBidder2SignalsJson); |
| |
| RunStandardAuction(); |
| EXPECT_EQ(kBidder2Key, result_.winning_group_id); |
| EXPECT_EQ(GURL("https://ad2.com/"), result_.ad_descriptor->url); |
| EXPECT_EQ(std::vector<blink::AdDescriptor>{blink::AdDescriptor( |
| GURL("https://ad2.com-component1.com"))}, |
| result_.ad_component_descriptors); |
| EXPECT_THAT(result_.report_urls, |
| testing::UnorderedElementsAre( |
| GURL("https://seller.signals.were.null.test/"))); |
| EXPECT_THAT(result_.ad_beacon_map, |
| testing::UnorderedElementsAreArray(kEmptyAdBeaconMap)); |
| EXPECT_THAT( |
| private_aggregation_manager_.TakePrivateAggregationRequests(), |
| testing::UnorderedElementsAre( |
| testing::Pair(kBidder1, |
| ElementsAreRequests( |
| kExpectedGenerateBidPrivateAggregationRequest)), |
| testing::Pair(kBidder2, |
| ElementsAreRequests( |
| // ReportWin script override doesn't send a report. |
| kExpectedGenerateBidPrivateAggregationRequest)))); |
| EXPECT_THAT( |
| result_.private_aggregation_event_map, |
| testing::UnorderedElementsAre(testing::Pair( |
| "click", |
| ElementsAreRequests( |
| // ReportWin script override doesn't send a report. |
| BuildPrivateAggregationRequest(/*bucket=*/10, /*value=*/22))))); |
| |
| EXPECT_THAT(result_.interest_groups_that_bid, |
| testing::UnorderedElementsAre(kBidder1Key, kBidder2Key)); |
| EXPECT_EQ(R"({"render_url":"https://ad2.com/"})", |
| result_.winning_group_ad_metadata); |
| EXPECT_THAT(result_.errors, testing::ElementsAre(base::StringPrintf( |
| "%s `reportResult` is not a function.", |
| kSellerUrl.spec().c_str()))); |
| CheckHistograms(InterestGroupAuction::AuctionResult::kSuccess, |
| /*expected_interest_groups=*/2, /*expected_owners=*/2, |
| /*expected_sellers=*/1); |
| } |
| |
| TEST_F(AuctionRunnerTest, TrustedScoringSignals) { |
| trusted_scoring_signals_url_ = |
| GURL("https://adstuff.publisher1.com/seller_signals"); |
| |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder1Url, |
| MakeBidScript(kSeller, "1", "https://ad1.com/", /*num_ad_components=*/2, |
| kBidder1, kBidder1Name, |
| /*has_signals=*/true, "k1", "a")); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder2Url, |
| MakeBidScript(kSeller, "2", "https://ad2.com/", /*num_ad_components=*/2, |
| kBidder2, kBidder2Name, |
| /*has_signals=*/true, "l2", "b") + |
| kReportWinExpectNullAuctionSignals); |
| auction_worklet::AddBidderJsonResponse( |
| &url_loader_factory_, |
| GURL(kBidder1TrustedSignalsUrl.spec() + |
| "?hostname=publisher1.com&keys=k1,k2" |
| "&interestGroupNames=Ad+Platform"), |
| kBidder1SignalsJson); |
| auction_worklet::AddBidderJsonResponse( |
| &url_loader_factory_, |
| GURL(kBidder2TrustedSignalsUrl.spec() + |
| "?hostname=publisher1.com&keys=l1,l2" |
| "&interestGroupNames=Another+Ad+Thing"), |
| kBidder2SignalsJson); |
| |
| // scoreAd() that only accepts bids where the scoring signals of the |
| // `renderUrl` is "accept". |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| std::string(R"( |
| function scoreAd(adMetadata, bid, auctionConfig, trustedScoringSignals, |
| browserSignals) { |
| let signal = trustedScoringSignals.renderUrl[browserSignals.renderUrl]; |
| if (browserSignals.dataVersion !== 2) { |
| throw new Error(`wrong dataVersion (${browserSignals.dataVersion})`); |
| } |
| // 2 * bid is expected by the BidderWorklet ReportWin() script. |
| if (signal == "accept") |
| return 2 * bid; |
| if (signal == "reject") |
| return 0; |
| throw "incorrect trustedScoringSignals"; |
| } |
| |
| function reportResult(auctionConfig, browserSignals) { |
| sendReportTo("https://reporting.example.com/" + browserSignals.bid); |
| registerAdBeacon({ |
| "click": "https://reporting.example.com/" + 2 * browserSignals.bid, |
| }); |
| if (browserSignals.dataVersion !== 2) { |
| throw new Error(`wrong dataVersion (${browserSignals.dataVersion})`); |
| } |
| return browserSignals; |
| } |
| )")); |
| |
| // Response body that only accept first bidder's bid. |
| const char kTrustedScoringSignalsBody[] = |
| R"({"renderUrls":{"https://ad1.com/":"accept", "https://ad2.com/":"reject"}})"; |
| |
| // There may be one merged trusted scoring signals request, or two separate |
| // requests. |
| |
| // Response in the case of a single merged trusted scoring signals request. |
| auction_worklet::AddVersionedJsonResponse( |
| &url_loader_factory_, |
| GURL(trusted_scoring_signals_url_->spec() + |
| "?hostname=publisher1.com" |
| "&renderUrls=https%3A%2F%2Fad1.com%2F,https%3A%2F%2Fad2.com%2F" |
| "&adComponentRenderUrls=https%3A%2F%2Fad1.com-component1.com%2F," |
| "https%3A%2F%2Fad2.com-component1.com%2F"), |
| kTrustedScoringSignalsBody, |
| /*data_version=*/2); |
| |
| // Responses in the case of two separate trusted scoring signals requests. |
| // Extra entries in the response dictionary will be ignored, so can use the |
| // same body as in the merged request case. |
| auction_worklet::AddVersionedJsonResponse( |
| &url_loader_factory_, |
| GURL(trusted_scoring_signals_url_->spec() + |
| "?hostname=publisher1.com" |
| "&renderUrls=https%3A%2F%2Fad1.com%2F" |
| "&adComponentRenderUrls=https%3A%2F%2Fad1.com-component1.com%2F"), |
| kTrustedScoringSignalsBody, |
| /*data_version=*/2); |
| auction_worklet::AddVersionedJsonResponse( |
| &url_loader_factory_, |
| GURL(trusted_scoring_signals_url_->spec() + |
| "?hostname=publisher1.com" |
| "&renderUrls=https%3A%2F%2Fad2.com%2F" |
| "&adComponentRenderUrls=https%3A%2F%2Fad2.com-component1.com%2F"), |
| kTrustedScoringSignalsBody, |
| /*data_version=*/2); |
| |
| RunStandardAuction(); |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| EXPECT_EQ(kBidder1Key, result_.winning_group_id); |
| EXPECT_EQ(GURL("https://ad1.com/"), result_.ad_descriptor->url); |
| EXPECT_EQ(std::vector<blink::AdDescriptor>{blink::AdDescriptor( |
| GURL("https://ad1.com-component1.com"))}, |
| result_.ad_component_descriptors); |
| EXPECT_THAT(result_.report_urls, |
| testing::UnorderedElementsAre( |
| GURL("https://reporting.example.com/1"), |
| GURL("https://buyer-reporting.example.com/1"))); |
| EXPECT_THAT( |
| result_.ad_beacon_map, |
| testing::UnorderedElementsAre( |
| testing::Pair(ReportingDestination::kSeller, |
| testing::ElementsAre(testing::Pair( |
| "click", GURL("https://reporting.example.com/2")))), |
| testing::Pair(ReportingDestination::kComponentSeller, |
| testing::ElementsAre()), |
| testing::Pair( |
| ReportingDestination::kBuyer, |
| testing::ElementsAre(testing::Pair( |
| "click", GURL("https://buyer-reporting.example.com/2")))))); |
| EXPECT_THAT( |
| private_aggregation_manager_.TakePrivateAggregationRequests(), |
| // Overridden script functions don't send reports |
| testing::UnorderedElementsAre( |
| testing::Pair( |
| kBidder1, |
| ElementsAreRequests(kExpectedGenerateBidPrivateAggregationRequest, |
| kExpectedReportWinPrivateAggregationRequest)), |
| testing::Pair(kBidder2, |
| ElementsAreRequests( |
| kExpectedGenerateBidPrivateAggregationRequest)))); |
| EXPECT_THAT( |
| result_.private_aggregation_event_map, |
| testing::UnorderedElementsAre(testing::Pair( |
| "click", |
| ElementsAreRequests( |
| BuildPrivateAggregationRequest(/*bucket=*/10, /*value=*/21), |
| BuildPrivateAggregationRequest(/*bucket=*/30, /*value=*/41))))); |
| |
| EXPECT_THAT(result_.interest_groups_that_bid, |
| testing::UnorderedElementsAre(kBidder1Key, kBidder2Key)); |
| EXPECT_EQ(R"({"render_url":"https://ad1.com/","metadata":{"ads": true}})", |
| result_.winning_group_ad_metadata); |
| CheckHistograms(InterestGroupAuction::AuctionResult::kSuccess, |
| /*expected_interest_groups=*/2, /*expected_owners=*/2, |
| /*expected_sellers=*/1); |
| } |
| |
| // An auction that passes auctionSignals via promises. This makes sure to |
| // order worklet process creation before promise delivery (compare to |
| // PromiseAuctionSignalsDeliveredBeforeWorklet). |
| TEST_F(AuctionRunnerTest, PromiseAuctionSignals) { |
| use_promise_for_auction_signals_ = true; |
| |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder1Url, |
| MakeBidScript(kSeller, "1", "https://ad1.com/", /*num_ad_components=*/0, |
| kBidder1, kBidder1Name)); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder2Url, |
| MakeBidScript(kSeller, "2", "https://ad2.com/", /*num_ad_components=*/0, |
| kBidder2, kBidder2Name)); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| MakeAuctionScript()); |
| |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, kBidder1Name, kBidder1Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad1.com"), |
| /*ad_component_urls=*/absl::nullopt)); |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder2, kBidder2Name, kBidder2Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad2.com"), |
| /*ad_component_urls=*/absl::nullopt)); |
| StartAuction(kSellerUrl, std::move(bidders)); |
| |
| // Make sure the worklet processes are created. Since promises haven't |
| // resolved yet, the auction should not complete. |
| task_environment()->RunUntilIdle(); |
| EXPECT_FALSE(auction_run_loop_->AnyQuitCalled()); |
| EXPECT_EQ(same_process_auction_process_manager_->NumBidderWorklets(), 2); |
| |
| // Feed in auctionSignals. |
| abortable_ad_auction_->ResolvedPromiseParam( |
| blink::mojom::AuctionAdConfigAuctionId::NewMainAuction(0), |
| blink::mojom::AuctionAdConfigField::kAuctionSignals, |
| MakeAuctionSignals(/*use_promise=*/false, url::Origin::Create(kSellerUrl)) |
| .value()); |
| |
| auction_run_loop_->Run(); |
| |
| EXPECT_EQ(InterestGroupKey(kBidder2, kBidder2Name), result_.winning_group_id); |
| EXPECT_EQ(GURL("https://ad2.com/"), result_.ad_descriptor->url); |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| } |
| |
| // Checks case where auction signals promises resolves before the bidder worklet |
| // process is ready. |
| TEST_F(AuctionRunnerTest, PromiseAuctionSignalsDeliveredBeforeWorklet) { |
| use_promise_for_auction_signals_ = true; |
| |
| // Create AuctionProcessManager in advance of starting the auction so can |
| // create worklets before the auction starts. |
| auction_process_manager_ = |
| std::make_unique<SameProcessAuctionProcessManager>(); |
| |
| std::vector<std::unique_ptr<AuctionProcessManager::ProcessHandle>> |
| busy_processes; |
| for (size_t i = 0; i < AuctionProcessManager::kMaxBidderProcesses; ++i) { |
| busy_processes.push_back( |
| std::make_unique<AuctionProcessManager::ProcessHandle>()); |
| url::Origin origin = url::Origin::Create( |
| GURL(base::StringPrintf("https://blocking.bidder.%zu.test", i))); |
| EXPECT_TRUE(auction_process_manager_->RequestWorkletService( |
| AuctionProcessManager::WorkletType::kBidder, origin, |
| scoped_refptr<SiteInstance>(), &*busy_processes.back(), |
| base::BindOnce( |
| []() { ADD_FAILURE() << "This should not be called"; }))); |
| } |
| task_environment()->RunUntilIdle(); |
| |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder1Url, |
| MakeBidScript(kSeller, "1", "https://ad1.com/", /*num_ad_components=*/0, |
| kBidder1, kBidder1Name)); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder2Url, |
| MakeBidScript(kSeller, "2", "https://ad2.com/", /*num_ad_components=*/0, |
| kBidder2, kBidder2Name)); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| MakeAuctionScript()); |
| |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, kBidder1Name, kBidder1Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad1.com"), |
| /*ad_component_urls=*/absl::nullopt)); |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder2, kBidder2Name, kBidder2Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad2.com"), |
| /*ad_component_urls=*/absl::nullopt)); |
| StartAuction(kSellerUrl, std::move(bidders)); |
| |
| // Can't complete yet. |
| task_environment()->RunUntilIdle(); |
| EXPECT_FALSE(auction_run_loop_->AnyQuitCalled()); |
| |
| // Feed in auctionSignals. |
| abortable_ad_auction_->ResolvedPromiseParam( |
| blink::mojom::AuctionAdConfigAuctionId::NewMainAuction(0), |
| blink::mojom::AuctionAdConfigField::kAuctionSignals, |
| MakeAuctionSignals(/*use_promise=*/false, url::Origin::Create(kSellerUrl)) |
| .value()); |
| |
| // Can't complete yet since there is no process slot. |
| task_environment()->RunUntilIdle(); |
| EXPECT_FALSE(auction_run_loop_->AnyQuitCalled()); |
| |
| // Free up process space to permit this to proceed. |
| busy_processes.clear(); |
| |
| auction_run_loop_->Run(); |
| |
| EXPECT_EQ(InterestGroupKey(kBidder2, kBidder2Name), result_.winning_group_id); |
| EXPECT_EQ(GURL("https://ad2.com/"), result_.ad_descriptor->url); |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| } |
| |
| // An auction that passes sellerSignals and auctionSignals via promises. |
| TEST_F(AuctionRunnerTest, PromiseSignals) { |
| use_promise_for_seller_signals_ = true; |
| use_promise_for_auction_signals_ = true; |
| |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder1Url, |
| MakeBidScript(kSeller, "1", "https://ad1.com/", /*num_ad_components=*/0, |
| kBidder1, kBidder1Name)); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder2Url, |
| MakeBidScript(kSeller, "2", "https://ad2.com/", /*num_ad_components=*/0, |
| kBidder2, kBidder2Name)); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| MakeAuctionScript()); |
| |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, kBidder1Name, kBidder1Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad1.com"), |
| /*ad_component_urls=*/absl::nullopt)); |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder2, kBidder2Name, kBidder2Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad2.com"), |
| /*ad_component_urls=*/absl::nullopt)); |
| StartAuction(kSellerUrl, std::move(bidders)); |
| |
| // Can't complete yet. |
| task_environment()->RunUntilIdle(); |
| EXPECT_FALSE(auction_run_loop_->AnyQuitCalled()); |
| |
| // Feed in sellerSignals. |
| abortable_ad_auction_->ResolvedPromiseParam( |
| blink::mojom::AuctionAdConfigAuctionId::NewMainAuction(0), |
| blink::mojom::AuctionAdConfigField::kSellerSignals, |
| MakeSellerSignals(/*use_promise=*/false, kSellerUrl).value()); |
| task_environment()->RunUntilIdle(); |
| EXPECT_FALSE(auction_run_loop_->AnyQuitCalled()); |
| |
| // Feed in auctionSignals. |
| abortable_ad_auction_->ResolvedPromiseParam( |
| blink::mojom::AuctionAdConfigAuctionId::NewMainAuction(0), |
| blink::mojom::AuctionAdConfigField::kAuctionSignals, |
| MakeAuctionSignals(/*use_promise=*/false, url::Origin::Create(kSellerUrl)) |
| .value()); |
| |
| auction_run_loop_->Run(); |
| |
| EXPECT_EQ(InterestGroupKey(kBidder2, kBidder2Name), result_.winning_group_id); |
| EXPECT_EQ(GURL("https://ad2.com/"), result_.ad_descriptor->url); |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| } |
| |
| // An auction that passes sellerSignals and auctionSignals via promises. |
| // Empty values are provided, which causes the validation scripts to complain. |
| TEST_F(AuctionRunnerTest, PromiseSignals2) { |
| use_promise_for_seller_signals_ = true; |
| use_promise_for_auction_signals_ = true; |
| |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder1Url, |
| MakeBidScript(kSeller, "1", "https://ad1.com/", /*num_ad_components=*/0, |
| kBidder1, kBidder1Name)); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder2Url, |
| MakeBidScript(kSeller, "2", "https://ad2.com/", /*num_ad_components=*/0, |
| kBidder2, kBidder2Name)); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| MakeAuctionScript()); |
| |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, kBidder1Name, kBidder1Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad1.com"), |
| /*ad_component_urls=*/absl::nullopt)); |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder2, kBidder2Name, kBidder2Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad2.com"), |
| /*ad_component_urls=*/absl::nullopt)); |
| StartAuction(kSellerUrl, std::move(bidders)); |
| |
| // Can't complete yet. |
| task_environment()->RunUntilIdle(); |
| EXPECT_FALSE(auction_run_loop_->AnyQuitCalled()); |
| |
| // Feed in sellerSignals. |
| abortable_ad_auction_->ResolvedPromiseParam( |
| blink::mojom::AuctionAdConfigAuctionId::NewMainAuction(0), |
| blink::mojom::AuctionAdConfigField::kSellerSignals, absl::nullopt); |
| task_environment()->RunUntilIdle(); |
| EXPECT_FALSE(auction_run_loop_->AnyQuitCalled()); |
| |
| // Feed in auctionSignals. |
| abortable_ad_auction_->ResolvedPromiseParam( |
| blink::mojom::AuctionAdConfigAuctionId::NewMainAuction(0), |
| blink::mojom::AuctionAdConfigField::kAuctionSignals, absl::nullopt); |
| |
| auction_run_loop_->Run(); |
| |
| EXPECT_FALSE(result_.winning_group_id.has_value()); |
| EXPECT_FALSE(result_.ad_descriptor.has_value()); |
| EXPECT_THAT( |
| result_.errors, |
| testing::UnorderedElementsAre( |
| testing::AllOf(HasSubstr("https://adplatform.com/offers.js"), |
| HasSubstr("Uncaught Error: wrong auctionSignals.")), |
| testing::AllOf(HasSubstr("https://anotheradthing.com/bids.js"), |
| HasSubstr("Uncaught Error: wrong auctionSignals.")))); |
| } |
| |
| // An auction that passes perBuyerSignals, perBuyerTimeouts, and |
| // perBuyerCumulativeTimeouts via promises. |
| TEST_F(AuctionRunnerTest, PromiseSignals3) { |
| use_promise_for_per_buyer_signals_ = true; |
| use_promise_for_buyer_timeouts_ = true; |
| use_promise_for_buyer_cumulative_timeouts_ = true; |
| |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder1Url, |
| MakeBidScript(kSeller, "1", "https://ad1.com/", /*num_ad_components=*/0, |
| kBidder1, kBidder1Name)); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder2Url, |
| MakeBidScript(kSeller, "2", "https://ad2.com/", /*num_ad_components=*/0, |
| kBidder2, kBidder2Name)); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| MakeAuctionScript()); |
| |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, kBidder1Name, kBidder1Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad1.com"), |
| /*ad_component_urls=*/absl::nullopt)); |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder2, kBidder2Name, kBidder2Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad2.com"), |
| /*ad_component_urls=*/absl::nullopt)); |
| StartAuction(kSellerUrl, std::move(bidders)); |
| |
| // Can't complete yet. |
| task_environment()->RunUntilIdle(); |
| EXPECT_FALSE(auction_run_loop_->AnyQuitCalled()); |
| |
| // Feed in perBuyerSignals. |
| abortable_ad_auction_->ResolvedPerBuyerSignalsPromise( |
| blink::mojom::AuctionAdConfigAuctionId::NewMainAuction(0), |
| MakePerBuyerSignals(/*use_promise=*/false, |
| url::Origin::Create(kSellerUrl)) |
| .value()); |
| task_environment()->RunUntilIdle(); |
| EXPECT_FALSE(auction_run_loop_->AnyQuitCalled()); |
| |
| // Feed in perBuyerTimeouts. |
| abortable_ad_auction_->ResolvedBuyerTimeoutsPromise( |
| blink::mojom::AuctionAdConfigAuctionId::NewMainAuction(0), |
| blink::mojom::AuctionAdConfigBuyerTimeoutField::kPerBuyerTimeouts, |
| MakeBuyerTimeouts(/*use_promise=*/false).value()); |
| task_environment()->RunUntilIdle(); |
| EXPECT_FALSE(auction_run_loop_->AnyQuitCalled()); |
| |
| // Feed in perBuyerCumulativeTimeouts. |
| abortable_ad_auction_->ResolvedBuyerTimeoutsPromise( |
| blink::mojom::AuctionAdConfigAuctionId::NewMainAuction(0), |
| blink::mojom::AuctionAdConfigBuyerTimeoutField:: |
| kPerBuyerCumulativeTimeouts, |
| MakeBuyerCumulativeTimeouts(/*use_promise=*/false).value()); |
| auction_run_loop_->Run(); |
| |
| EXPECT_EQ(InterestGroupKey(kBidder2, kBidder2Name), result_.winning_group_id); |
| EXPECT_EQ(GURL("https://ad2.com/"), result_.ad_descriptor->url); |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| } |
| |
| // An auction that passes perBuyerSignals and perBuyerTimeouts via promises. |
| // Empty values are provided, which causes the validation scripts to complain. |
| TEST_F(AuctionRunnerTest, PromiseSignals4) { |
| use_promise_for_per_buyer_signals_ = true; |
| use_promise_for_buyer_timeouts_ = true; |
| |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder1Url, |
| MakeBidScript(kSeller, "1", "https://ad1.com/", /*num_ad_components=*/0, |
| kBidder1, kBidder1Name)); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder2Url, |
| MakeBidScript(kSeller, "2", "https://ad2.com/", /*num_ad_components=*/0, |
| kBidder2, kBidder2Name)); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| MakeAuctionScript()); |
| |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, kBidder1Name, kBidder1Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad1.com"), |
| /*ad_component_urls=*/absl::nullopt)); |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder2, kBidder2Name, kBidder2Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad2.com"), |
| /*ad_component_urls=*/absl::nullopt)); |
| StartAuction(kSellerUrl, std::move(bidders)); |
| |
| // Can't complete yet. |
| task_environment()->RunUntilIdle(); |
| EXPECT_FALSE(auction_run_loop_->AnyQuitCalled()); |
| |
| // Feed in perBuyerSignals. |
| abortable_ad_auction_->ResolvedPerBuyerSignalsPromise( |
| blink::mojom::AuctionAdConfigAuctionId::NewMainAuction(0), absl::nullopt); |
| task_environment()->RunUntilIdle(); |
| EXPECT_FALSE(auction_run_loop_->AnyQuitCalled()); |
| |
| // Feed in perBuyerTimeouts. |
| abortable_ad_auction_->ResolvedBuyerTimeoutsPromise( |
| blink::mojom::AuctionAdConfigAuctionId::NewMainAuction(0), |
| blink::mojom::AuctionAdConfigBuyerTimeoutField::kPerBuyerTimeouts, |
| blink::AuctionConfig::BuyerTimeouts()); |
| |
| auction_run_loop_->Run(); |
| |
| EXPECT_FALSE(result_.winning_group_id.has_value()); |
| EXPECT_FALSE(result_.ad_descriptor.has_value()); |
| EXPECT_THAT( |
| result_.errors, |
| testing::UnorderedElementsAre( |
| testing::AllOf( |
| HasSubstr("https://adplatform.com/offers.js"), |
| HasSubstr( |
| "Uncaught Error: unexpectedly perBuyerSignals is null.")), |
| testing::AllOf( |
| HasSubstr("https://anotheradthing.com/bids.js"), |
| HasSubstr( |
| "Uncaught Error: unexpectedly perBuyerSignals is null.")))); |
| } |
| |
| // Runs an auction that passes auctionSignals via a promise, and makes sure that |
| // URL fetches begin, and worklet processes are launched, before the promise is |
| // resolved. |
| TEST_F(AuctionRunnerTest, PromiseSignalsParallelism) { |
| use_promise_for_seller_signals_ = true; |
| |
| StartStandardAuction(); |
| |
| // Various scripts and trusted signals should be pending, and worklets should |
| // be created. |
| task_environment()->RunUntilIdle(); |
| |
| EXPECT_EQ(1, same_process_auction_process_manager_->NumSellerWorklets()); |
| EXPECT_EQ(2, same_process_auction_process_manager_->NumBidderWorklets()); |
| GURL trusted_bidding_signals_url1 = GURL(kBidder1TrustedSignalsUrl.spec() + |
| "?hostname=publisher1.com&keys=k1,k2" |
| "&interestGroupNames=Ad+Platform"); |
| GURL trusted_bidding_signals_url2 = |
| GURL(kBidder2TrustedSignalsUrl.spec() + |
| "?hostname=publisher1.com&keys=l1,l2" |
| "&interestGroupNames=Another+Ad+Thing"); |
| EXPECT_TRUE(url_loader_factory_.IsPending(kBidder1Url.spec())); |
| EXPECT_TRUE(url_loader_factory_.IsPending(kBidder2Url.spec())); |
| EXPECT_TRUE(url_loader_factory_.IsPending(kSellerUrl.spec())); |
| EXPECT_TRUE( |
| url_loader_factory_.IsPending(trusted_bidding_signals_url1.spec())); |
| EXPECT_TRUE( |
| url_loader_factory_.IsPending(trusted_bidding_signals_url2.spec())); |
| |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder1Url, |
| MakeBidScript(kSeller, "1", "https://ad1.com/", /*num_ad_components=*/2, |
| kBidder1, kBidder1Name, |
| /*has_signals=*/true, "k1", "a", |
| /*report_post_auction_signals=*/true)); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder2Url, |
| MakeBidScript(kSeller, "2", "https://ad2.com/", /*num_ad_components=*/2, |
| kBidder2, kBidder2Name, |
| /*has_signals=*/true, "l2", "b", |
| /*report_post_auction_signals=*/true)); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kSellerUrl, |
| MakeAuctionScript(/*report_post_auction_signals=*/true)); |
| auction_worklet::AddBidderJsonResponse( |
| &url_loader_factory_, trusted_bidding_signals_url1, kBidder1SignalsJson); |
| auction_worklet::AddBidderJsonResponse( |
| &url_loader_factory_, trusted_bidding_signals_url2, kBidder2SignalsJson); |
| // Feed in the sources. |
| task_environment()->RunUntilIdle(); |
| |
| // Feed in sellerSignals. |
| abortable_ad_auction_->ResolvedPromiseParam( |
| blink::mojom::AuctionAdConfigAuctionId::NewMainAuction(0), |
| blink::mojom::AuctionAdConfigField::kSellerSignals, |
| MakeSellerSignals(/*use_promise=*/false, kSellerUrl).value()); |
| auction_run_loop_->Run(); |
| |
| EXPECT_EQ(InterestGroupKey(kBidder2, kBidder2Name), result_.winning_group_id); |
| EXPECT_EQ(GURL("https://ad2.com/"), result_.ad_descriptor->url); |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| } |
| |
| TEST_F(AuctionRunnerTest, PromiseSignalsResolveAfterAbort) { |
| use_promise_for_seller_signals_ = true; |
| dont_reset_auction_runner_ = true; |
| |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder1Url, |
| MakeBidScript(kSeller, "1", "https://ad1.com/", /*num_ad_components=*/0, |
| kBidder1, kBidder1Name)); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder2Url, |
| MakeBidScript(kSeller, "2", "https://ad2.com/", /*num_ad_components=*/0, |
| kBidder2, kBidder2Name)); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| MakeAuctionScript()); |
| |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, kBidder1Name, kBidder1Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad1.com"), |
| /*ad_component_urls=*/absl::nullopt)); |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder2, kBidder2Name, kBidder2Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad2.com"), |
| /*ad_component_urls=*/absl::nullopt)); |
| StartAuction(kSellerUrl, std::move(bidders)); |
| |
| // Can't complete yet. |
| task_environment()->RunUntilIdle(); |
| ; |
| EXPECT_FALSE(auction_run_loop_->AnyQuitCalled()); |
| |
| abortable_ad_auction_->Abort(); |
| auction_run_loop_->Run(); |
| EXPECT_TRUE(result_.manually_aborted); |
| |
| // Feed in sellerSignals. Nothing weird should happen. |
| auction_run_loop_ = std::make_unique<base::RunLoop>(); |
| abortable_ad_auction_->ResolvedPromiseParam( |
| blink::mojom::AuctionAdConfigAuctionId::NewMainAuction(0), |
| blink::mojom::AuctionAdConfigField::kSellerSignals, |
| MakeSellerSignals(/*use_promise=*/false, kSellerUrl).value()); |
| task_environment()->RunUntilIdle(); |
| EXPECT_FALSE(auction_run_loop_->AnyQuitCalled()); |
| EXPECT_TRUE(result_.manually_aborted); |
| } |
| |
| TEST_F(AuctionRunnerTest, PromiseSignalsComponentAuction) { |
| use_promise_for_seller_signals_ = true; |
| use_promise_for_auction_signals_ = true; |
| |
| SetUpComponentAuctionAndResponses(/*bidder1_seller=*/kComponentSeller1, |
| /*bidder2_seller=*/kComponentSeller2, |
| /*bid_from_component_auction_wins=*/true, |
| /*report_post_auction_signals=*/false); |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, kBidder1Name, kBidder1Url, kBidder1TrustedSignalsUrl, |
| {"k1", "k2"}, GURL("https://ad1.com"), |
| std::vector<GURL>{GURL("https://ad1.com-component1.com"), |
| GURL("https://ad1.com-component2.com")})); |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder2, kBidder2Name, kBidder2Url, kBidder2TrustedSignalsUrl, |
| {"l1", "l2"}, GURL("https://ad2.com"), |
| std::vector<GURL>{GURL("https://ad2.com-component1.com"), |
| GURL("https://ad2.com-component2.com")})); |
| StartAuction(kSellerUrl, std::move(bidders)); |
| |
| // Can't complete yet. |
| task_environment()->RunUntilIdle(); |
| EXPECT_FALSE(auction_run_loop_->AnyQuitCalled()); |
| |
| // Feed in the signals. Updating main before component auctions is significant |
| // because it makes sure main auction is notified of config readiness |
| // triggered by component config updates, too. |
| abortable_ad_auction_->ResolvedPromiseParam( |
| blink::mojom::AuctionAdConfigAuctionId::NewMainAuction(0), |
| blink::mojom::AuctionAdConfigField::kSellerSignals, |
| MakeSellerSignals(/*use_promise=*/false, kSellerUrl).value()); |
| task_environment()->RunUntilIdle(); |
| EXPECT_FALSE(auction_run_loop_->AnyQuitCalled()); |
| |
| abortable_ad_auction_->ResolvedPromiseParam( |
| blink::mojom::AuctionAdConfigAuctionId::NewMainAuction(0), |
| blink::mojom::AuctionAdConfigField::kAuctionSignals, |
| MakeAuctionSignals(/*use_promise=*/false, url::Origin::Create(kSellerUrl)) |
| .value()); |
| task_environment()->RunUntilIdle(); |
| EXPECT_FALSE(auction_run_loop_->AnyQuitCalled()); |
| |
| for (int component = 0; component < 2; ++component) { |
| const GURL& url = |
| (component == 0) ? kComponentSeller1Url : kComponentSeller2Url; |
| abortable_ad_auction_->ResolvedPromiseParam( |
| blink::mojom::AuctionAdConfigAuctionId::NewComponentAuction(component), |
| blink::mojom::AuctionAdConfigField::kSellerSignals, |
| MakeSellerSignals(/*use_promise=*/false, url).value()); |
| task_environment()->RunUntilIdle(); |
| EXPECT_FALSE(auction_run_loop_->AnyQuitCalled()); |
| abortable_ad_auction_->ResolvedPromiseParam( |
| blink::mojom::AuctionAdConfigAuctionId::NewComponentAuction(component), |
| blink::mojom::AuctionAdConfigField::kAuctionSignals, |
| MakeAuctionSignals(/*use_promise=*/false, url::Origin::Create(url)) |
| .value()); |
| if (component != 1) { |
| task_environment()->RunUntilIdle(); |
| EXPECT_FALSE(auction_run_loop_->AnyQuitCalled()); |
| } |
| } |
| |
| auction_run_loop_->Run(); |
| |
| EXPECT_EQ(GURL("https://ad2.com/"), result_.ad_descriptor->url); |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| } |
| |
| // Coverage of what happens when promises come in for a component auction that |
| // got dropped at the database load stage, due to not having anything to bid, |
| // including that it still gets error-checked. |
| TEST_F(AuctionRunnerTest, PromiseSignalsComponentAuctionRejected) { |
| use_promise_for_auction_signals_ = true; |
| |
| SetUpComponentAuctionAndResponses(/*bidder1_seller=*/kComponentSeller1, |
| /*bidder2_seller=*/kComponentSeller2, |
| /*bid_from_component_auction_wins=*/true, |
| /*report_post_auction_signals=*/false); |
| |
| for (bool inject_incorrect_call : {false, true}) { |
| SCOPED_TRACE(inject_incorrect_call); |
| std::vector<StorageInterestGroup> bidders; |
| // Only component 2's bidder added. |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder2, kBidder2Name, kBidder2Url, kBidder2TrustedSignalsUrl, |
| {"l1", "l2"}, GURL("https://ad2.com"), |
| std::vector<GURL>{GURL("https://ad2.com-component1.com"), |
| GURL("https://ad2.com-component2.com")})); |
| StartAuction(kSellerUrl, std::move(bidders)); |
| |
| // Can't complete yet. |
| task_environment()->RunUntilIdle(); |
| EXPECT_FALSE(auction_run_loop_->AnyQuitCalled()); |
| |
| // Feed in the signals. |
| abortable_ad_auction_->ResolvedPromiseParam( |
| blink::mojom::AuctionAdConfigAuctionId::NewMainAuction(0), |
| blink::mojom::AuctionAdConfigField::kAuctionSignals, |
| MakeAuctionSignals(/*use_promise=*/false, |
| url::Origin::Create(kSellerUrl)) |
| .value()); |
| task_environment()->RunUntilIdle(); |
| EXPECT_FALSE(auction_run_loop_->AnyQuitCalled()); |
| |
| for (int component = 0; component < 2; ++component) { |
| const GURL& url = |
| (component == 0) ? kComponentSeller1Url : kComponentSeller2Url; |
| abortable_ad_auction_->ResolvedPromiseParam( |
| blink::mojom::AuctionAdConfigAuctionId::NewComponentAuction( |
| component), |
| blink::mojom::AuctionAdConfigField::kAuctionSignals, |
| MakeAuctionSignals(/*use_promise=*/false, url::Origin::Create(url)) |
| .value()); |
| |
| if (component == 0) { |
| if (inject_incorrect_call) { |
| abortable_ad_auction_->ResolvedPromiseParam( |
| blink::mojom::AuctionAdConfigAuctionId::NewComponentAuction( |
| component), |
| blink::mojom::AuctionAdConfigField::kAuctionSignals, |
| MakeAuctionSignals(/*use_promise=*/false, |
| url::Origin::Create(url)) |
| .value()); |
| } |
| task_environment()->RunUntilIdle(); |
| EXPECT_FALSE(auction_run_loop_->AnyQuitCalled()); |
| } |
| } |
| |
| if (inject_incorrect_call) { |
| EXPECT_EQ("ResolvedPromiseParam updating non-promise", TakeBadMessage()); |
| } |
| |
| // TODO(morlovich): This should eventually abort rather than succeed in the |
| // `inject_incorrect_call` case. |
| auction_run_loop_->Run(); |
| |
| EXPECT_EQ(GURL("https://ad2.com/"), result_.ad_descriptor->url); |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| } |
| } |
| |
| // Make sure the scoring portion of the auction waits to have promises resolved. |
| // Checking at bidding time is not enough since a top-level auction can receive |
| // bids to score from component auctions, and those complete their configuration |
| // independently. |
| TEST_F(AuctionRunnerTest, PromiseSignalsSellerDependency) { |
| use_promise_for_seller_signals_ = true; |
| |
| SetUpComponentAuctionAndResponses(/*bidder1_seller=*/kComponentSeller1, |
| /*bidder2_seller=*/kComponentSeller2, |
| /*bid_from_component_auction_wins=*/true, |
| /*report_post_auction_signals=*/false); |
| // Make sure there is nothing at top-level. |
| interest_group_buyers_->clear(); |
| |
| StartStandardAuction(); |
| |
| // Can't complete yet. |
| task_environment()->RunUntilIdle(); |
| EXPECT_FALSE(auction_run_loop_->AnyQuitCalled()); |
| |
| // Feed in the signals for component auctions (but not top-level auction) |
| for (int component = 0; component < 2; ++component) { |
| const GURL& url = |
| (component == 0) ? kComponentSeller1Url : kComponentSeller2Url; |
| abortable_ad_auction_->ResolvedPromiseParam( |
| blink::mojom::AuctionAdConfigAuctionId::NewComponentAuction(component), |
| blink::mojom::AuctionAdConfigField::kSellerSignals, |
| MakeSellerSignals(/*use_promise=*/false, url).value()); |
| |
| task_environment()->RunUntilIdle(); |
| EXPECT_FALSE(auction_run_loop_->AnyQuitCalled()); |
| } |
| |
| // Now finally pass in the main auction param, unblocking the seller worklet. |
| abortable_ad_auction_->ResolvedPromiseParam( |
| blink::mojom::AuctionAdConfigAuctionId::NewMainAuction(0), |
| blink::mojom::AuctionAdConfigField::kSellerSignals, |
| MakeSellerSignals(/*use_promise=*/false, kSellerUrl).value()); |
| |
| auction_run_loop_->Run(); |
| EXPECT_EQ(InterestGroupKey(kBidder2, kBidder2Name), result_.winning_group_id); |
| EXPECT_EQ(GURL("https://ad2.com/"), result_.ad_descriptor->url); |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| } |
| |
| TEST_F(AuctionRunnerTest, PromiseSignalsBadAuctionId) { |
| use_promise_for_seller_signals_ = true; |
| use_promise_for_auction_signals_ = true; |
| |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder1Url, |
| MakeBidScript(kSeller, "1", "https://ad1.com/", /*num_ad_components=*/0, |
| kBidder1, kBidder1Name)); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder2Url, |
| MakeBidScript(kSeller, "2", "https://ad2.com/", /*num_ad_components=*/0, |
| kBidder2, kBidder2Name)); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| MakeAuctionScript()); |
| |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, kBidder1Name, kBidder1Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad1.com"), |
| /*ad_component_urls=*/absl::nullopt)); |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder2, kBidder2Name, kBidder2Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad2.com"), |
| /*ad_component_urls=*/absl::nullopt)); |
| StartAuction(kSellerUrl, std::move(bidders)); |
| |
| // Can't complete yet. |
| task_environment()->RunUntilIdle(); |
| EXPECT_FALSE(auction_run_loop_->AnyQuitCalled()); |
| |
| // Feed in sellerSignals with wrong component ID. |
| abortable_ad_auction_->ResolvedPromiseParam( |
| blink::mojom::AuctionAdConfigAuctionId::NewComponentAuction(0), |
| blink::mojom::AuctionAdConfigField::kSellerSignals, absl::nullopt); |
| auction_run_loop_->RunUntilIdle(); |
| EXPECT_EQ("Invalid auction ID in ResolvedPromiseParam", TakeBadMessage()); |
| } |
| |
| TEST_F(AuctionRunnerTest, PromiseSignalsBadAuctionId2) { |
| use_promise_for_per_buyer_signals_ = true; |
| |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder1Url, |
| MakeBidScript(kSeller, "1", "https://ad1.com/", /*num_ad_components=*/0, |
| kBidder1, kBidder1Name)); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder2Url, |
| MakeBidScript(kSeller, "2", "https://ad2.com/", /*num_ad_components=*/0, |
| kBidder2, kBidder2Name)); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| MakeAuctionScript()); |
| |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, kBidder1Name, kBidder1Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad1.com"), |
| /*ad_component_urls=*/absl::nullopt)); |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder2, kBidder2Name, kBidder2Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad2.com"), |
| /*ad_component_urls=*/absl::nullopt)); |
| StartAuction(kSellerUrl, std::move(bidders)); |
| |
| // Can't complete yet. |
| task_environment()->RunUntilIdle(); |
| EXPECT_FALSE(auction_run_loop_->AnyQuitCalled()); |
| |
| // Feed in sellerSignals with wrong component ID. |
| abortable_ad_auction_->ResolvedPerBuyerSignalsPromise( |
| blink::mojom::AuctionAdConfigAuctionId::NewComponentAuction(0), |
| absl::nullopt); |
| auction_run_loop_->RunUntilIdle(); |
| EXPECT_EQ("Invalid auction ID in ResolvedPerBuyerSignalsPromise", |
| TakeBadMessage()); |
| } |
| |
| TEST_F(AuctionRunnerTest, PromiseSignalsBadAuctionId3) { |
| use_promise_for_buyer_timeouts_ = true; |
| |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder1Url, |
| MakeBidScript(kSeller, "1", "https://ad1.com/", /*num_ad_components=*/0, |
| kBidder1, kBidder1Name)); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder2Url, |
| MakeBidScript(kSeller, "2", "https://ad2.com/", /*num_ad_components=*/0, |
| kBidder2, kBidder2Name)); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| MakeAuctionScript()); |
| |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, kBidder1Name, kBidder1Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad1.com"), |
| /*ad_component_urls=*/absl::nullopt)); |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder2, kBidder2Name, kBidder2Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad2.com"), |
| /*ad_component_urls=*/absl::nullopt)); |
| StartAuction(kSellerUrl, std::move(bidders)); |
| |
| // Can't complete yet. |
| task_environment()->RunUntilIdle(); |
| EXPECT_FALSE(auction_run_loop_->AnyQuitCalled()); |
| |
| // Feed in perBuyerSignals with wrong component ID. |
| blink::AuctionConfig::BuyerTimeouts buyer_timeouts; |
| abortable_ad_auction_->ResolvedBuyerTimeoutsPromise( |
| blink::mojom::AuctionAdConfigAuctionId::NewComponentAuction(0), |
| blink::mojom::AuctionAdConfigBuyerTimeoutField::kPerBuyerTimeouts, |
| buyer_timeouts); |
| auction_run_loop_->RunUntilIdle(); |
| EXPECT_EQ("Invalid auction ID in ResolvedBuyerTimeoutsPromise", |
| TakeBadMessage()); |
| } |
| |
| TEST_F(AuctionRunnerTest, PromiseSignalsBadAuctionId4) { |
| use_promise_for_buyer_cumulative_timeouts_ = true; |
| |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder1Url, |
| MakeBidScript(kSeller, "1", "https://ad1.com/", /*num_ad_components=*/0, |
| kBidder1, kBidder1Name)); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder2Url, |
| MakeBidScript(kSeller, "2", "https://ad2.com/", /*num_ad_components=*/0, |
| kBidder2, kBidder2Name)); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| MakeAuctionScript()); |
| |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, kBidder1Name, kBidder1Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad1.com"), |
| /*ad_component_urls=*/absl::nullopt)); |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder2, kBidder2Name, kBidder2Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad2.com"), |
| /*ad_component_urls=*/absl::nullopt)); |
| StartAuction(kSellerUrl, std::move(bidders)); |
| |
| // Can't complete yet. |
| task_environment()->RunUntilIdle(); |
| EXPECT_FALSE(auction_run_loop_->AnyQuitCalled()); |
| |
| // Feed in perBuyerSignals with wrong component ID. |
| blink::AuctionConfig::BuyerTimeouts buyer_cumulative_timeouts; |
| abortable_ad_auction_->ResolvedBuyerTimeoutsPromise( |
| blink::mojom::AuctionAdConfigAuctionId::NewComponentAuction(0), |
| blink::mojom::AuctionAdConfigBuyerTimeoutField:: |
| kPerBuyerCumulativeTimeouts, |
| buyer_cumulative_timeouts); |
| auction_run_loop_->RunUntilIdle(); |
| EXPECT_EQ("Invalid auction ID in ResolvedBuyerTimeoutsPromise", |
| TakeBadMessage()); |
| } |
| |
| TEST_F(AuctionRunnerTest, PromiseSignalsBadAuctionId5) { |
| pass_promise_for_direct_from_seller_signals_ = true; |
| |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder1Url, |
| MakeBidScript(kSeller, "1", "https://ad1.com/", /*num_ad_components=*/0, |
| kBidder1, kBidder1Name)); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder2Url, |
| MakeBidScript(kSeller, "2", "https://ad2.com/", /*num_ad_components=*/0, |
| kBidder2, kBidder2Name)); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| MakeAuctionScript()); |
| |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, kBidder1Name, kBidder1Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad1.com"), |
| /*ad_component_urls=*/absl::nullopt)); |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder2, kBidder2Name, kBidder2Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad2.com"), |
| /*ad_component_urls=*/absl::nullopt)); |
| StartAuction(kSellerUrl, std::move(bidders)); |
| |
| // Can't complete yet. |
| task_environment()->RunUntilIdle(); |
| EXPECT_FALSE(auction_run_loop_->AnyQuitCalled()); |
| |
| // Feed in directFromSellerSignals with wrong component ID. |
| abortable_ad_auction_->ResolvedDirectFromSellerSignalsPromise( |
| blink::mojom::AuctionAdConfigAuctionId::NewComponentAuction(0), |
| absl::nullopt); |
| auction_run_loop_->RunUntilIdle(); |
| EXPECT_EQ("Invalid auction ID in ResolvedDirectFromSellerSignalsPromise", |
| TakeBadMessage()); |
| } |
| |
| TEST_F(AuctionRunnerTest, PromiseInvalidDirectFromSellerSignals) { |
| pass_promise_for_direct_from_seller_signals_ = true; |
| |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder1Url, |
| MakeBidScript(kSeller, "1", "https://ad1.com/", /*num_ad_components=*/0, |
| kBidder1, kBidder1Name)); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder2Url, |
| MakeBidScript(kSeller, "2", "https://ad2.com/", /*num_ad_components=*/0, |
| kBidder2, kBidder2Name)); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| MakeAuctionScript()); |
| |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, kBidder1Name, kBidder1Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad1.com"), |
| /*ad_component_urls=*/absl::nullopt)); |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder2, kBidder2Name, kBidder2Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad2.com"), |
| /*ad_component_urls=*/absl::nullopt)); |
| StartAuction(kSellerUrl, std::move(bidders)); |
| |
| // Can't complete yet. |
| task_environment()->RunUntilIdle(); |
| EXPECT_FALSE(auction_run_loop_->AnyQuitCalled()); |
| |
| blink::DirectFromSellerSignals direct_from_seller_signals; |
| direct_from_seller_signals.prefix = |
| GURL("https://seller.test/?query_invalid"); |
| |
| // Feed in directFromSellerSignals with wrong component ID. |
| abortable_ad_auction_->ResolvedDirectFromSellerSignalsPromise( |
| blink::mojom::AuctionAdConfigAuctionId::NewMainAuction(0), |
| std::move(direct_from_seller_signals)); |
| auction_run_loop_->RunUntilIdle(); |
| EXPECT_EQ("ResolvedDirectFromSellerSignalsPromise with invalid signals", |
| TakeBadMessage()); |
| } |
| |
| // Trying to update auctionSignals which wasn't originally passed in as a |
| // promise. |
| TEST_F(AuctionRunnerTest, PromiseSignalsUpdateNonPromise) { |
| use_promise_for_seller_signals_ = true; |
| use_promise_for_auction_signals_ = false; |
| |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder1Url, |
| MakeBidScript(kSeller, "1", "https://ad1.com/", /*num_ad_components=*/0, |
| kBidder1, kBidder1Name)); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder2Url, |
| MakeBidScript(kSeller, "2", "https://ad2.com/", /*num_ad_components=*/0, |
| kBidder2, kBidder2Name)); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| MakeAuctionScript()); |
| |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, kBidder1Name, kBidder1Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad1.com"), |
| /*ad_component_urls=*/absl::nullopt)); |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder2, kBidder2Name, kBidder2Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad2.com"), |
| /*ad_component_urls=*/absl::nullopt)); |
| StartAuction(kSellerUrl, std::move(bidders)); |
| |
| // Can't complete yet. |
| task_environment()->RunUntilIdle(); |
| EXPECT_FALSE(auction_run_loop_->AnyQuitCalled()); |
| |
| // Feed in auctionSignals, which isn't a promise in the first place. |
| abortable_ad_auction_->ResolvedPromiseParam( |
| blink::mojom::AuctionAdConfigAuctionId::NewMainAuction(0), |
| blink::mojom::AuctionAdConfigField::kAuctionSignals, absl::nullopt); |
| auction_run_loop_->RunUntilIdle(); |
| EXPECT_EQ("ResolvedPromiseParam updating non-promise", TakeBadMessage()); |
| } |
| |
| // Trying to update auctionSignals twice. |
| TEST_F(AuctionRunnerTest, PromiseSignalsUpdateNonPromise2) { |
| use_promise_for_seller_signals_ = true; |
| use_promise_for_auction_signals_ = true; |
| |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder1Url, |
| MakeBidScript(kSeller, "1", "https://ad1.com/", /*num_ad_components=*/0, |
| kBidder1, kBidder1Name)); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder2Url, |
| MakeBidScript(kSeller, "2", "https://ad2.com/", /*num_ad_components=*/0, |
| kBidder2, kBidder2Name)); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| MakeAuctionScript()); |
| |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, kBidder1Name, kBidder1Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad1.com"), |
| /*ad_component_urls=*/absl::nullopt)); |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder2, kBidder2Name, kBidder2Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad2.com"), |
| /*ad_component_urls=*/absl::nullopt)); |
| StartAuction(kSellerUrl, std::move(bidders)); |
| |
| // Can't complete yet. |
| task_environment()->RunUntilIdle(); |
| EXPECT_FALSE(auction_run_loop_->AnyQuitCalled()); |
| |
| // Feed in auctionSignals twice. |
| abortable_ad_auction_->ResolvedPromiseParam( |
| blink::mojom::AuctionAdConfigAuctionId::NewMainAuction(0), |
| blink::mojom::AuctionAdConfigField::kAuctionSignals, absl::nullopt); |
| abortable_ad_auction_->ResolvedPromiseParam( |
| blink::mojom::AuctionAdConfigAuctionId::NewMainAuction(0), |
| blink::mojom::AuctionAdConfigField::kAuctionSignals, absl::nullopt); |
| task_environment()->RunUntilIdle(); |
| EXPECT_EQ("ResolvedPromiseParam updating non-promise", TakeBadMessage()); |
| } |
| |
| // Trying to update sellerSignals which wasn't originally passed in as a |
| // promise. |
| TEST_F(AuctionRunnerTest, PromiseSignalsUpdateNonPromise3) { |
| use_promise_for_seller_signals_ = false; |
| use_promise_for_auction_signals_ = true; |
| |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder1Url, |
| MakeBidScript(kSeller, "1", "https://ad1.com/", /*num_ad_components=*/0, |
| kBidder1, kBidder1Name)); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder2Url, |
| MakeBidScript(kSeller, "2", "https://ad2.com/", /*num_ad_components=*/0, |
| kBidder2, kBidder2Name)); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| MakeAuctionScript()); |
| |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, kBidder1Name, kBidder1Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad1.com"), |
| /*ad_component_urls=*/absl::nullopt)); |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder2, kBidder2Name, kBidder2Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad2.com"), |
| /*ad_component_urls=*/absl::nullopt)); |
| StartAuction(kSellerUrl, std::move(bidders)); |
| |
| // Can't complete yet. |
| task_environment()->RunUntilIdle(); |
| EXPECT_FALSE(auction_run_loop_->AnyQuitCalled()); |
| |
| abortable_ad_auction_->ResolvedPromiseParam( |
| blink::mojom::AuctionAdConfigAuctionId::NewMainAuction(0), |
| blink::mojom::AuctionAdConfigField::kSellerSignals, absl::nullopt); |
| task_environment()->RunUntilIdle(); |
| EXPECT_EQ("ResolvedPromiseParam updating non-promise", TakeBadMessage()); |
| } |
| |
| // Trying to update sellerSignals twice. |
| TEST_F(AuctionRunnerTest, PromiseSignalsUpdateNonPromise4) { |
| use_promise_for_seller_signals_ = true; |
| use_promise_for_auction_signals_ = true; |
| |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder1Url, |
| MakeBidScript(kSeller, "1", "https://ad1.com/", /*num_ad_components=*/0, |
| kBidder1, kBidder1Name)); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder2Url, |
| MakeBidScript(kSeller, "2", "https://ad2.com/", /*num_ad_components=*/0, |
| kBidder2, kBidder2Name)); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| MakeAuctionScript()); |
| |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, kBidder1Name, kBidder1Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad1.com"), |
| /*ad_component_urls=*/absl::nullopt)); |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder2, kBidder2Name, kBidder2Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad2.com"), |
| /*ad_component_urls=*/absl::nullopt)); |
| StartAuction(kSellerUrl, std::move(bidders)); |
| |
| // Can't complete yet. |
| task_environment()->RunUntilIdle(); |
| EXPECT_FALSE(auction_run_loop_->AnyQuitCalled()); |
| |
| // Feed in sellerSignals twice. |
| abortable_ad_auction_->ResolvedPromiseParam( |
| blink::mojom::AuctionAdConfigAuctionId::NewMainAuction(0), |
| blink::mojom::AuctionAdConfigField::kSellerSignals, absl::nullopt); |
| abortable_ad_auction_->ResolvedPromiseParam( |
| blink::mojom::AuctionAdConfigAuctionId::NewMainAuction(0), |
| blink::mojom::AuctionAdConfigField::kSellerSignals, absl::nullopt); |
| task_environment()->RunUntilIdle(); |
| EXPECT_EQ("ResolvedPromiseParam updating non-promise", TakeBadMessage()); |
| } |
| |
| // Trying to update perBuyerSignals twice. |
| TEST_F(AuctionRunnerTest, PromiseSignalsUpdateNonPromise5) { |
| // Have two kind of promises so we don't just finish after first |
| // perBuyerSignals update |
| use_promise_for_per_buyer_signals_ = true; |
| use_promise_for_buyer_timeouts_ = true; |
| |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder1Url, |
| MakeBidScript(kSeller, "1", "https://ad1.com/", /*num_ad_components=*/0, |
| kBidder1, kBidder1Name)); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder2Url, |
| MakeBidScript(kSeller, "2", "https://ad2.com/", /*num_ad_components=*/0, |
| kBidder2, kBidder2Name)); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| MakeAuctionScript()); |
| |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, kBidder1Name, kBidder1Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad1.com"), |
| /*ad_component_urls=*/absl::nullopt)); |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder2, kBidder2Name, kBidder2Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad2.com"), |
| /*ad_component_urls=*/absl::nullopt)); |
| StartAuction(kSellerUrl, std::move(bidders)); |
| |
| // Can't complete yet. |
| task_environment()->RunUntilIdle(); |
| EXPECT_FALSE(auction_run_loop_->AnyQuitCalled()); |
| |
| // Feed in perBuyerSignals twice. |
| abortable_ad_auction_->ResolvedPerBuyerSignalsPromise( |
| blink::mojom::AuctionAdConfigAuctionId::NewMainAuction(0), absl::nullopt); |
| abortable_ad_auction_->ResolvedPerBuyerSignalsPromise( |
| blink::mojom::AuctionAdConfigAuctionId::NewMainAuction(0), absl::nullopt); |
| task_environment()->RunUntilIdle(); |
| EXPECT_EQ("ResolvedPerBuyerSignalsPromise updating non-promise", |
| TakeBadMessage()); |
| } |
| |
| // Trying to update buyer timeouts twice. |
| TEST_F(AuctionRunnerTest, PromiseSignalsUpdateNonPromise6) { |
| // Have two kind of promises so we don't just finish after first update. |
| use_promise_for_per_buyer_signals_ = true; |
| use_promise_for_buyer_timeouts_ = true; |
| |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder1Url, |
| MakeBidScript(kSeller, "1", "https://ad1.com/", /*num_ad_components=*/0, |
| kBidder1, kBidder1Name)); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder2Url, |
| MakeBidScript(kSeller, "2", "https://ad2.com/", /*num_ad_components=*/0, |
| kBidder2, kBidder2Name)); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| MakeAuctionScript()); |
| |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, kBidder1Name, kBidder1Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad1.com"), |
| /*ad_component_urls=*/absl::nullopt)); |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder2, kBidder2Name, kBidder2Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad2.com"), |
| /*ad_component_urls=*/absl::nullopt)); |
| StartAuction(kSellerUrl, std::move(bidders)); |
| |
| // Can't complete yet. |
| task_environment()->RunUntilIdle(); |
| EXPECT_FALSE(auction_run_loop_->AnyQuitCalled()); |
| |
| // Feed in buyer timeouts twice. |
| blink::AuctionConfig::BuyerTimeouts timeouts; |
| abortable_ad_auction_->ResolvedBuyerTimeoutsPromise( |
| blink::mojom::AuctionAdConfigAuctionId::NewMainAuction(0), |
| blink::mojom::AuctionAdConfigBuyerTimeoutField::kPerBuyerTimeouts, |
| timeouts); |
| abortable_ad_auction_->ResolvedBuyerTimeoutsPromise( |
| blink::mojom::AuctionAdConfigAuctionId::NewMainAuction(0), |
| blink::mojom::AuctionAdConfigBuyerTimeoutField::kPerBuyerTimeouts, |
| timeouts); |
| task_environment()->RunUntilIdle(); |
| EXPECT_EQ("ResolvedBuyerTimeoutsPromise updating non-promise", |
| TakeBadMessage()); |
| } |
| |
| // Trying to update buyer cumulative timeouts twice. |
| TEST_F(AuctionRunnerTest, PromiseSignalsUpdateNonPromise7) { |
| // Have two kind of promises so we don't just finish after first update. |
| use_promise_for_per_buyer_signals_ = true; |
| use_promise_for_buyer_cumulative_timeouts_ = true; |
| |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder1Url, |
| MakeBidScript(kSeller, "1", "https://ad1.com/", /*num_ad_components=*/0, |
| kBidder1, kBidder1Name)); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder2Url, |
| MakeBidScript(kSeller, "2", "https://ad2.com/", /*num_ad_components=*/0, |
| kBidder2, kBidder2Name)); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| MakeAuctionScript()); |
| |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, kBidder1Name, kBidder1Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad1.com"), |
| /*ad_component_urls=*/absl::nullopt)); |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder2, kBidder2Name, kBidder2Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad2.com"), |
| /*ad_component_urls=*/absl::nullopt)); |
| StartAuction(kSellerUrl, std::move(bidders)); |
| |
| // Can't complete yet. |
| task_environment()->RunUntilIdle(); |
| EXPECT_FALSE(auction_run_loop_->AnyQuitCalled()); |
| |
| // Feed in buyer timeouts twice. |
| blink::AuctionConfig::BuyerTimeouts timeouts; |
| abortable_ad_auction_->ResolvedBuyerTimeoutsPromise( |
| blink::mojom::AuctionAdConfigAuctionId::NewMainAuction(0), |
| blink::mojom::AuctionAdConfigBuyerTimeoutField::kPerBuyerTimeouts, |
| timeouts); |
| abortable_ad_auction_->ResolvedBuyerTimeoutsPromise( |
| blink::mojom::AuctionAdConfigAuctionId::NewMainAuction(0), |
| blink::mojom::AuctionAdConfigBuyerTimeoutField:: |
| kPerBuyerCumulativeTimeouts, |
| timeouts); |
| task_environment()->RunUntilIdle(); |
| EXPECT_EQ("ResolvedBuyerTimeoutsPromise updating non-promise", |
| TakeBadMessage()); |
| } |
| |
| // Trying to update direct from seller signals twice. |
| TEST_F(AuctionRunnerTest, PromiseSignalsUpdateNonPromise8) { |
| // Have two kind of promises so we don't just finish after first update. |
| use_promise_for_per_buyer_signals_ = true; |
| pass_promise_for_direct_from_seller_signals_ = true; |
| |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder1Url, |
| MakeBidScript(kSeller, "1", "https://ad1.com/", /*num_ad_components=*/0, |
| kBidder1, kBidder1Name)); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder2Url, |
| MakeBidScript(kSeller, "2", "https://ad2.com/", /*num_ad_components=*/0, |
| kBidder2, kBidder2Name)); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| MakeAuctionScript()); |
| |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, kBidder1Name, kBidder1Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad1.com"), |
| /*ad_component_urls=*/absl::nullopt)); |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder2, kBidder2Name, kBidder2Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad2.com"), |
| /*ad_component_urls=*/absl::nullopt)); |
| StartAuction(kSellerUrl, std::move(bidders)); |
| |
| // Can't complete yet. |
| task_environment()->RunUntilIdle(); |
| EXPECT_FALSE(auction_run_loop_->AnyQuitCalled()); |
| |
| // Feed in direct from seller signals twice. |
| abortable_ad_auction_->ResolvedDirectFromSellerSignalsPromise( |
| blink::mojom::AuctionAdConfigAuctionId::NewMainAuction(0), absl::nullopt); |
| abortable_ad_auction_->ResolvedDirectFromSellerSignalsPromise( |
| blink::mojom::AuctionAdConfigAuctionId::NewMainAuction(0), absl::nullopt); |
| task_environment()->RunUntilIdle(); |
| EXPECT_EQ("ResolvedDirectFromSellerSignalsPromise updating non-promise", |
| TakeBadMessage()); |
| } |
| |
| // Test the case where the ProcessManager initially prevents creating worklets, |
| // due to being at its process limit. |
| TEST_F(AuctionRunnerTest, ProcessManagerBlocksWorkletCreation) { |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder1Url, |
| MakeBidScript(kSeller, "1", "https://ad1.com/", /*num_ad_components=*/2, |
| kBidder1, kBidder1Name, |
| /*has_signals=*/true, "k1", "a")); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder2Url, |
| MakeBidScript(kSeller, "2", "https://ad2.com/", /*num_ad_components=*/2, |
| kBidder2, kBidder2Name, |
| /*has_signals=*/true, "l2", "b")); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| MakeAuctionScript()); |
| auction_worklet::AddBidderJsonResponse( |
| &url_loader_factory_, |
| GURL(kBidder1TrustedSignalsUrl.spec() + |
| "?hostname=publisher1.com&keys=k1,k2" |
| "&interestGroupNames=Ad+Platform"), |
| kBidder1SignalsJson); |
| auction_worklet::AddBidderJsonResponse( |
| &url_loader_factory_, |
| GURL(kBidder2TrustedSignalsUrl.spec() + |
| "?hostname=publisher1.com&keys=l1,l2" |
| "&interestGroupNames=Another+Ad+Thing"), |
| kBidder2SignalsJson); |
| |
| // For the seller worklet, it only matters if the worklet process limit has |
| // been hit or not. |
| for (bool seller_worklet_creation_delayed : {false, true}) { |
| SCOPED_TRACE(seller_worklet_creation_delayed); |
| |
| // For bidder worklets, in addition to testing the cases with no processes |
| // and at the process limit, also test the case where we're one below the |
| // limit, which should serialize bidder worklet creation and execution. |
| for (size_t num_used_bidder_worklet_processes : |
| std::vector<size_t>{static_cast<size_t>(0), |
| AuctionProcessManager::kMaxBidderProcesses - 1, |
| AuctionProcessManager::kMaxBidderProcesses}) { |
| SCOPED_TRACE(num_used_bidder_worklet_processes); |
| |
| bool bidder_worklet_creation_delayed = |
| num_used_bidder_worklet_processes == |
| AuctionProcessManager::kMaxBidderProcesses; |
| |
| // Create AuctionProcessManager in advance of starting the auction so can |
| // create worklets before the auction starts. |
| auction_process_manager_ = |
| std::make_unique<SameProcessAuctionProcessManager>(); |
| |
| AuctionProcessManager* auction_process_manager = |
| auction_process_manager_.get(); |
| |
| std::list<std::unique_ptr<AuctionProcessManager::ProcessHandle>> sellers; |
| if (seller_worklet_creation_delayed) { |
| // Make kMaxSellerProcesses seller worklet requests for other origins so |
| // seller worklet creation will be blocked by the process limit. |
| for (size_t i = 0; i < AuctionProcessManager::kMaxSellerProcesses; |
| ++i) { |
| sellers.push_back( |
| std::make_unique<AuctionProcessManager::ProcessHandle>()); |
| url::Origin origin = url::Origin::Create( |
| GURL(base::StringPrintf("https://%zu.test", i))); |
| EXPECT_TRUE(auction_process_manager->RequestWorkletService( |
| AuctionProcessManager::WorkletType::kSeller, origin, |
| scoped_refptr<SiteInstance>(), &*sellers.back(), |
| base::BindOnce( |
| []() { ADD_FAILURE() << "This should not be called"; }))); |
| } |
| } |
| |
| // Make `num_used_bidder_worklet_processes` bidder worklet requests for |
| // different origins. |
| std::list<std::unique_ptr<AuctionProcessManager::ProcessHandle>> bidders; |
| for (size_t i = 0; i < num_used_bidder_worklet_processes; ++i) { |
| bidders.push_back( |
| std::make_unique<AuctionProcessManager::ProcessHandle>()); |
| url::Origin origin = url::Origin::Create( |
| GURL(base::StringPrintf("https://blocking.bidder.%zu.test", i))); |
| EXPECT_TRUE(auction_process_manager->RequestWorkletService( |
| AuctionProcessManager::WorkletType::kBidder, origin, |
| scoped_refptr<SiteInstance>(), &*bidders.back(), |
| base::BindOnce( |
| []() { ADD_FAILURE() << "This should not be called"; }))); |
| } |
| |
| // If neither sellers nor bidders are at their limit, the auction should |
| // complete. |
| if (!seller_worklet_creation_delayed && |
| !bidder_worklet_creation_delayed) { |
| RunStandardAuction(); |
| } else { |
| // Otherwise, the auction should be blocked. |
| StartStandardAuction(); |
| task_environment()->RunUntilIdle(); |
| |
| EXPECT_EQ( |
| seller_worklet_creation_delayed ? 1u : 0u, |
| auction_process_manager->GetPendingSellerRequestsForTesting()); |
| EXPECT_EQ( |
| bidder_worklet_creation_delayed ? 2u : 0u, |
| auction_process_manager->GetPendingBidderRequestsForTesting()); |
| EXPECT_FALSE(auction_complete_); |
| |
| // Free up a seller slot, if needed. |
| if (seller_worklet_creation_delayed) { |
| sellers.pop_front(); |
| task_environment()->RunUntilIdle(); |
| EXPECT_EQ( |
| 0u, |
| auction_process_manager->GetPendingSellerRequestsForTesting()); |
| EXPECT_EQ( |
| bidder_worklet_creation_delayed ? 2u : 0, |
| auction_process_manager->GetPendingBidderRequestsForTesting()); |
| } |
| |
| // Free up a single bidder slot, if needed. |
| if (bidder_worklet_creation_delayed) { |
| EXPECT_FALSE(auction_complete_); |
| bidders.pop_front(); |
| } |
| |
| // The auction should now be able to run to completion. |
| auction_run_loop_->Run(); |
| } |
| EXPECT_TRUE(auction_complete_); |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| EXPECT_EQ(kBidder2Key, result_.winning_group_id); |
| EXPECT_EQ(GURL("https://ad2.com/"), result_.ad_descriptor->url); |
| EXPECT_EQ(std::vector<blink::AdDescriptor>{blink::AdDescriptor( |
| GURL("https://ad2.com-component1.com"))}, |
| result_.ad_component_descriptors); |
| EXPECT_THAT(result_.report_urls, |
| testing::UnorderedElementsAre( |
| GURL("https://reporting.example.com/2"), |
| GURL("https://buyer-reporting.example.com/2"))); |
| EXPECT_THAT( |
| result_.ad_beacon_map, |
| testing::UnorderedElementsAre( |
| testing::Pair( |
| ReportingDestination::kSeller, |
| testing::ElementsAre(testing::Pair( |
| "click", GURL("https://reporting.example.com/4")))), |
| testing::Pair(ReportingDestination::kComponentSeller, |
| testing::ElementsAre()), |
| testing::Pair( |
| ReportingDestination::kBuyer, |
| testing::ElementsAre(testing::Pair( |
| "click", |
| GURL("https://buyer-reporting.example.com/4")))))); |
| EXPECT_THAT( |
| private_aggregation_manager_.TakePrivateAggregationRequests(), |
| testing::UnorderedElementsAre( |
| testing::Pair(kBidder1, |
| ElementsAreRequests( |
| kExpectedGenerateBidPrivateAggregationRequest)), |
| testing::Pair(kBidder2, |
| ElementsAreRequests( |
| kExpectedGenerateBidPrivateAggregationRequest, |
| kExpectedReportWinPrivateAggregationRequest)), |
| testing::Pair( |
| kSeller, |
| ElementsAreRequests( |
| kExpectedScoreAdPrivateAggregationRequest, |
| kExpectedScoreAdPrivateAggregationRequest, |
| kExpectedReportResultPrivateAggregationRequest)))); |
| EXPECT_THAT( |
| result_.private_aggregation_event_map, |
| testing::UnorderedElementsAre(testing::Pair( |
| "click", ElementsAreRequests(BuildPrivateAggregationRequest( |
| /*bucket=*/10, /*value=*/22), |
| BuildPrivateAggregationRequest( |
| /*bucket=*/30, /*value=*/42))))); |
| |
| EXPECT_THAT(result_.interest_groups_that_bid, |
| testing::UnorderedElementsAre(kBidder1Key, kBidder2Key)); |
| EXPECT_EQ(R"({"render_url":"https://ad2.com/"})", |
| result_.winning_group_ad_metadata); |
| CheckHistograms(InterestGroupAuction::AuctionResult::kSuccess, |
| /*expected_interest_groups=*/2, |
| /*expected_owners=*/2, /*expected_sellers=*/1); |
| } |
| } |
| } |
| |
| // Tests ComponentAuctions and their interactions with the ProcessManager |
| // delaying worklet creation. |
| TEST_F(AuctionRunnerTest, ComponentAuctionProcessManagerBlocksWorkletCreation) { |
| SetUpComponentAuctionAndResponses(/*bidder1_seller=*/kComponentSeller1, |
| /*bidder2_seller=*/kComponentSeller2, |
| /*bid_from_component_auction_wins=*/true); |
| |
| // For both worklet types, in addition to testing the cases with no processes |
| // and at the process limit, also test the case where we're one below the |
| // limit, which should serialize worklet creation and execution. |
| for (size_t num_used_seller_worklet_processes : |
| {static_cast<size_t>(0), AuctionProcessManager::kMaxSellerProcesses - 1, |
| AuctionProcessManager::kMaxSellerProcesses}) { |
| SCOPED_TRACE(num_used_seller_worklet_processes); |
| |
| bool seller_worklet_creation_delayed = |
| num_used_seller_worklet_processes == |
| AuctionProcessManager::kMaxSellerProcesses; |
| |
| for (size_t num_used_bidder_worklet_processes : |
| std::vector<size_t>{static_cast<size_t>(0), |
| AuctionProcessManager::kMaxBidderProcesses - 1, |
| AuctionProcessManager::kMaxBidderProcesses}) { |
| SCOPED_TRACE(num_used_bidder_worklet_processes); |
| |
| bool bidder_worklet_creation_delayed = |
| num_used_bidder_worklet_processes == |
| AuctionProcessManager::kMaxBidderProcesses; |
| |
| // Create AuctionProcessManager in advance of starting the auction so can |
| // create worklets before the auction starts. |
| auction_process_manager_ = |
| std::make_unique<SameProcessAuctionProcessManager>(); |
| |
| AuctionProcessManager* auction_process_manager = |
| auction_process_manager_.get(); |
| |
| // Make `num_used_seller_worklet_processes` bidder worklet requests for |
| // different origins. |
| std::list<std::unique_ptr<AuctionProcessManager::ProcessHandle>> sellers; |
| for (size_t i = 0; i < num_used_seller_worklet_processes; ++i) { |
| sellers.push_back( |
| std::make_unique<AuctionProcessManager::ProcessHandle>()); |
| url::Origin origin = url::Origin::Create( |
| GURL(base::StringPrintf("https://%zu.test", i))); |
| EXPECT_TRUE(auction_process_manager->RequestWorkletService( |
| AuctionProcessManager::WorkletType::kSeller, origin, |
| scoped_refptr<SiteInstance>(), &*sellers.back(), |
| base::BindOnce( |
| []() { ADD_FAILURE() << "This should not be called"; }))); |
| } |
| |
| // Make `num_used_bidder_worklet_processes` bidder worklet requests for |
| // different origins. |
| std::list<std::unique_ptr<AuctionProcessManager::ProcessHandle>> bidders; |
| for (size_t i = 0; i < num_used_bidder_worklet_processes; ++i) { |
| bidders.push_back( |
| std::make_unique<AuctionProcessManager::ProcessHandle>()); |
| url::Origin origin = url::Origin::Create( |
| GURL(base::StringPrintf("https://blocking.bidder.%zu.test", i))); |
| EXPECT_TRUE(auction_process_manager->RequestWorkletService( |
| AuctionProcessManager::WorkletType::kBidder, origin, |
| scoped_refptr<SiteInstance>(), &*bidders.back(), |
| base::BindOnce( |
| []() { ADD_FAILURE() << "This should not be called"; }))); |
| } |
| |
| // If neither sellers nor bidders are at their limit, the auction should |
| // complete. |
| if (!seller_worklet_creation_delayed && |
| !bidder_worklet_creation_delayed) { |
| RunStandardAuction(); |
| } else { |
| // Otherwise, the auction should be blocked. |
| StartStandardAuction(); |
| task_environment()->RunUntilIdle(); |
| |
| if (seller_worklet_creation_delayed) { |
| // In the case of `seller_worklet_creation_delayed`, only the two |
| // component worklet's loads should have been queued. |
| EXPECT_EQ( |
| 2u, |
| auction_process_manager->GetPendingSellerRequestsForTesting()); |
| } else if (num_used_seller_worklet_processes == |
| AuctionProcessManager::kMaxSellerProcesses - 1 && |
| bidder_worklet_creation_delayed) { |
| // IF there's only one available seller worklet process, and |
| // `bidder_worklet_creation_delayed` is true, one component seller |
| // should have been created, the component seller should be queued, |
| // waiting on a process slot, and the top-level seller should not have |
| // been requested yet, waiting on the component sellers to both be |
| // loaded. |
| EXPECT_EQ( |
| 1u, |
| auction_process_manager->GetPendingSellerRequestsForTesting()); |
| } else { |
| // Otherwise, no seller worklet requests should be pending.. |
| EXPECT_EQ( |
| 0u, |
| auction_process_manager->GetPendingSellerRequestsForTesting()); |
| } |
| |
| EXPECT_EQ( |
| bidder_worklet_creation_delayed ? 2u : 0u, |
| auction_process_manager->GetPendingBidderRequestsForTesting()); |
| |
| // Free up a seller slot, if needed. |
| if (seller_worklet_creation_delayed) { |
| sellers.pop_front(); |
| task_environment()->RunUntilIdle(); |
| if (bidder_worklet_creation_delayed) { |
| // If bidder creation was also delayed, one component seller should |
| // have been made, but is waiting on a bid. Creating the other |
| // component seller should be queued. The main seller should be |
| // blocked on loading that component seller. |
| EXPECT_EQ( |
| 1u, |
| auction_process_manager->GetPendingSellerRequestsForTesting()); |
| EXPECT_EQ( |
| 2u, |
| auction_process_manager->GetPendingBidderRequestsForTesting()); |
| } else { |
| // Otherwise, the auction should have completed. |
| EXPECT_TRUE(auction_complete_); |
| } |
| } |
| |
| // Free up a single bidder slot, if needed. |
| if (bidder_worklet_creation_delayed) { |
| EXPECT_FALSE(auction_complete_); |
| bidders.pop_front(); |
| } |
| |
| // The auction should now be able to run to completion. |
| auction_run_loop_->Run(); |
| } |
| EXPECT_TRUE(auction_complete_); |
| |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| EXPECT_EQ(GURL("https://ad2.com/"), result_.ad_descriptor->url); |
| EXPECT_EQ(std::vector<blink::AdDescriptor>{blink::AdDescriptor( |
| GURL("https://ad2.com-component1.com"))}, |
| result_.ad_component_descriptors); |
| EXPECT_THAT(result_.report_urls, |
| testing::UnorderedElementsAre( |
| GURL("https://reporting.example.com/2"), |
| GURL("https://component2-report.test/2"), |
| GURL("https://buyer-reporting.example.com/2"))); |
| EXPECT_THAT( |
| result_.ad_beacon_map, |
| testing::UnorderedElementsAre( |
| testing::Pair( |
| ReportingDestination::kSeller, |
| testing::ElementsAre(testing::Pair( |
| "click", GURL("https://reporting.example.com/4")))), |
| testing::Pair( |
| ReportingDestination::kComponentSeller, |
| testing::ElementsAre(testing::Pair( |
| "click", GURL("https://component2-report.test/4")))), |
| testing::Pair( |
| ReportingDestination::kBuyer, |
| testing::ElementsAre(testing::Pair( |
| "click", |
| GURL("https://buyer-reporting.example.com/4")))))); |
| EXPECT_THAT( |
| private_aggregation_manager_.TakePrivateAggregationRequests(), |
| testing::UnorderedElementsAre( |
| testing::Pair(kBidder1, |
| ElementsAreRequests( |
| kExpectedGenerateBidPrivateAggregationRequest)), |
| testing::Pair(kBidder2, |
| ElementsAreRequests( |
| kExpectedGenerateBidPrivateAggregationRequest, |
| kExpectedReportWinPrivateAggregationRequest)), |
| testing::Pair( |
| kSeller, ElementsAreRequests( |
| kExpectedScoreAdPrivateAggregationRequest, |
| kExpectedScoreAdPrivateAggregationRequest, |
| kExpectedReportResultPrivateAggregationRequest)), |
| testing::Pair(kComponentSeller1, |
| ElementsAreRequests( |
| kExpectedScoreAdPrivateAggregationRequest)), |
| testing::Pair( |
| kComponentSeller2, |
| ElementsAreRequests( |
| kExpectedScoreAdPrivateAggregationRequest, |
| kExpectedReportResultPrivateAggregationRequest)))); |
| EXPECT_THAT( |
| result_.private_aggregation_event_map, |
| testing::UnorderedElementsAre(testing::Pair( |
| "click", ElementsAreRequests(BuildPrivateAggregationRequest( |
| /*bucket=*/10, /*value=*/22), |
| BuildPrivateAggregationRequest( |
| /*bucket=*/30, /*value=*/42))))); |
| CheckHistograms(InterestGroupAuction::AuctionResult::kSuccess, |
| /*expected_interest_groups=*/2, /*expected_owners=*/2, |
| /*expected_sellers=*/3); |
| } |
| } |
| } |
| |
| // Test a seller worklet load failure while waiting on bidder worklet processes |
| // to be allocated. Most of the tests for global seller worklet failures at a |
| // particular phase use seller crashes instead of load errors (see SellerCrash |
| // test), but this case is simplest to test with a seller load error. |
| TEST_F(AuctionRunnerTest, SellerLoadErrorWhileWaitingForBidders) { |
| // Create AuctionProcessManager in advance of starting the auction so can |
| // create worklets before the auction starts. |
| auction_process_manager_ = |
| std::make_unique<SameProcessAuctionProcessManager>(); |
| |
| // Make kMaxBidderProcesses bidder worklet requests for different origins. |
| std::list<std::unique_ptr<AuctionProcessManager::ProcessHandle>> |
| other_bidders; |
| for (size_t i = 0; i < AuctionProcessManager::kMaxBidderProcesses; ++i) { |
| other_bidders.push_back( |
| std::make_unique<AuctionProcessManager::ProcessHandle>()); |
| url::Origin origin = url::Origin::Create( |
| GURL(base::StringPrintf("https://blocking.bidder.%zu.test", i))); |
| EXPECT_TRUE(auction_process_manager_->RequestWorkletService( |
| AuctionProcessManager::WorkletType::kBidder, origin, |
| scoped_refptr<SiteInstance>(), &*other_bidders.back(), |
| base::BindOnce( |
| []() { ADD_FAILURE() << "This should not be called"; }))); |
| } |
| |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| MakeAuctionScript()); |
| |
| url_loader_factory_.AddResponse(kSellerUrl.spec(), "", net::HTTP_NOT_FOUND); |
| |
| RunStandardAuction(); |
| |
| EXPECT_THAT(result_.errors, |
| testing::ElementsAre( |
| "Failed to load https://adstuff.publisher1.com/auction.js " |
| "HTTP status = 404 Not Found.")); |
| EXPECT_FALSE(result_.winning_group_id); |
| EXPECT_FALSE(result_.ad_descriptor); |
| EXPECT_THAT(result_.interest_groups_that_bid, |
| testing::UnorderedElementsAre()); |
| CheckHistograms(InterestGroupAuction::AuctionResult::kSellerWorkletLoadFailed, |
| /*expected_interest_groups=*/2, /*expected_owners=*/2, |
| /*expected_sellers=*/1); |
| } |
| |
| // Tests ComponentAuction where a component seller worklet has a load error with |
| // a hanging bidder worklet request. The auction runs when the process manager |
| // only has 1 bidder and 1 seller slot, so this test makes sure that in this |
| // case the bidder and seller processes are freed up, so they don't potentially |
| // cause deadlock preventing the auction from completing. |
| TEST_F(AuctionRunnerTest, |
| ComponentAuctionSellerWorkletLoadErrorWithPendingBidderLoad) { |
| interest_group_buyers_.emplace(); |
| |
| // First component seller worklet request fails. No response is returned for |
| // the bidder worklet, so it hangs. |
| component_auctions_.emplace_back( |
| CreateAuctionConfig(kComponentSeller1Url, {{kBidder1}})); |
| url_loader_factory_.AddResponse(kComponentSeller1Url.spec(), "", |
| net::HTTP_NOT_FOUND); |
| |
| // Second component worklet loads as normal, as does its bidder. |
| component_auctions_.emplace_back( |
| CreateAuctionConfig(kComponentSeller2Url, {{kBidder2}})); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kComponentSeller2Url, |
| MakeDecisionScript(kComponentSeller2Url, /*send_report_url=*/GURL( |
| "https://component2-report.test/"))); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder2Url, |
| MakeBidScript(kComponentSeller2, "2", "https://ad2.com/", |
| /*num_ad_components=*/2, kBidder2, kBidder2Name, |
| /*has_signals=*/true, "l2", "b")); |
| auction_worklet::AddBidderJsonResponse( |
| &url_loader_factory_, |
| GURL(kBidder2TrustedSignalsUrl.spec() + |
| "?hostname=publisher1.com&keys=l1,l2" |
| "&interestGroupNames=Another+Ad+Thing"), |
| kBidder2SignalsJson); |
| |
| // Top-level seller uses the default script. |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kSellerUrl, |
| MakeDecisionScript( |
| kSellerUrl, |
| /*send_report_url=*/GURL("https://reporting.example.com"), |
| /*bid_from_component_auction_wins=*/true)); |
| |
| auction_process_manager_ = |
| std::make_unique<SameProcessAuctionProcessManager>(); |
| |
| // Take up all but 1 of the seller worklet process slots. |
| std::list<std::unique_ptr<AuctionProcessManager::ProcessHandle>> sellers; |
| for (size_t i = 0; i < AuctionProcessManager::kMaxSellerProcesses - 1; ++i) { |
| sellers.push_back(std::make_unique<AuctionProcessManager::ProcessHandle>()); |
| url::Origin origin = |
| url::Origin::Create(GURL(base::StringPrintf("https://%zu.test", i))); |
| EXPECT_TRUE(auction_process_manager_->RequestWorkletService( |
| AuctionProcessManager::WorkletType::kSeller, origin, |
| scoped_refptr<SiteInstance>(), &*sellers.back(), base::BindOnce([]() { |
| ADD_FAILURE() << "This should not be called"; |
| }))); |
| } |
| |
| // Take up but 1 of the bidder worklet process slots. |
| std::list<std::unique_ptr<AuctionProcessManager::ProcessHandle>> bidders; |
| for (size_t i = 0; i < AuctionProcessManager::kMaxBidderProcesses - 1; ++i) { |
| bidders.push_back(std::make_unique<AuctionProcessManager::ProcessHandle>()); |
| url::Origin origin = url::Origin::Create( |
| GURL(base::StringPrintf("https://blocking.bidder.%zu.test", i))); |
| EXPECT_TRUE(auction_process_manager_->RequestWorkletService( |
| AuctionProcessManager::WorkletType::kBidder, origin, |
| scoped_refptr<SiteInstance>(), &*bidders.back(), base::BindOnce([]() { |
| ADD_FAILURE() << "This should not be called"; |
| }))); |
| } |
| |
| RunStandardAuction(); |
| |
| EXPECT_THAT(result_.errors, |
| testing::UnorderedElementsAre( |
| "Failed to load https://component.seller1.test/foo.js HTTP " |
| "status = 404 Not Found.")); |
| EXPECT_EQ(GURL("https://ad2.com/"), result_.ad_descriptor->url); |
| EXPECT_EQ(std::vector<blink::AdDescriptor>{blink::AdDescriptor( |
| GURL("https://ad2.com-component1.com"))}, |
| result_.ad_component_descriptors); |
| EXPECT_THAT(result_.report_urls, |
| testing::UnorderedElementsAre( |
| GURL("https://reporting.example.com/2"), |
| GURL("https://component2-report.test/2"), |
| GURL("https://buyer-reporting.example.com/2"))); |
| EXPECT_THAT( |
| result_.ad_beacon_map, |
| testing::UnorderedElementsAre( |
| testing::Pair(ReportingDestination::kSeller, |
| testing::ElementsAre(testing::Pair( |
| "click", GURL("https://reporting.example.com/4")))), |
| testing::Pair( |
| ReportingDestination::kComponentSeller, |
| testing::ElementsAre(testing::Pair( |
| "click", GURL("https://component2-report.test/4")))), |
| testing::Pair( |
| ReportingDestination::kBuyer, |
| testing::ElementsAre(testing::Pair( |
| "click", GURL("https://buyer-reporting.example.com/4")))))); |
| EXPECT_THAT( |
| private_aggregation_manager_.TakePrivateAggregationRequests(), |
| testing::UnorderedElementsAre( |
| testing::Pair( |
| kBidder2, |
| ElementsAreRequests(kExpectedGenerateBidPrivateAggregationRequest, |
| kExpectedReportWinPrivateAggregationRequest)), |
| testing::Pair(kSeller, |
| ElementsAreRequests( |
| kExpectedScoreAdPrivateAggregationRequest, |
| kExpectedReportResultPrivateAggregationRequest)), |
| testing::Pair(kComponentSeller2, |
| ElementsAreRequests( |
| kExpectedScoreAdPrivateAggregationRequest, |
| kExpectedReportResultPrivateAggregationRequest)))); |
| EXPECT_THAT( |
| result_.private_aggregation_event_map, |
| testing::UnorderedElementsAre(testing::Pair( |
| "click", |
| ElementsAreRequests( |
| BuildPrivateAggregationRequest(/*bucket=*/10, /*value=*/22), |
| BuildPrivateAggregationRequest(/*bucket=*/30, /*value=*/42))))); |
| CheckHistograms(InterestGroupAuction::AuctionResult::kSuccess, |
| /*expected_interest_groups=*/2, /*expected_owners=*/2, |
| /*expected_sellers=*/3); |
| } |
| |
| // Test to make sure SendPendingSignalsRequests is called on a seller worklet |
| // if the worklet becomes available only after everything is queued. |
| TEST_F(AuctionRunnerTest, LateSellerWorkletSendPendingSignalsRequestsCalled) { |
| UseMockWorkletService(); |
| |
| std::list<std::unique_ptr<AuctionProcessManager::ProcessHandle>> sellers; |
| // Make kMaxSellerProcesses seller worklet requests for other origins so |
| // seller worklet creation will be blocked by the process limit. |
| for (size_t i = 0; i < AuctionProcessManager::kMaxSellerProcesses; ++i) { |
| sellers.push_back(std::make_unique<AuctionProcessManager::ProcessHandle>()); |
| url::Origin origin = |
| url::Origin::Create(GURL(base::StringPrintf("https://%zu.test", i))); |
| EXPECT_TRUE(mock_auction_process_manager_->RequestWorkletService( |
| AuctionProcessManager::WorkletType::kSeller, origin, |
| scoped_refptr<SiteInstance>(), &*sellers.back(), base::BindOnce([]() { |
| ADD_FAILURE() << "This should not be called"; |
| }))); |
| } |
| |
| StartStandardAuction(); |
| mock_auction_process_manager_->WaitForWorklets(/*num_bidders=*/2, |
| /*num_sellers=*/0); |
| |
| // Let bidder worklets finish all the work while seller worklet is not |
| // available yet. |
| auto bidder1_worklet = |
| mock_auction_process_manager_->TakeBidderWorklet(kBidder1Url); |
| ASSERT_TRUE(bidder1_worklet); |
| auto bidder2_worklet = |
| mock_auction_process_manager_->TakeBidderWorklet(kBidder2Url); |
| ASSERT_TRUE(bidder2_worklet); |
| |
| bidder1_worklet->InvokeGenerateBidCallback( |
| /*bid=*/6, blink::AdDescriptor(GURL("https://ad1.com/"))); |
| bidder1_worklet.reset(); |
| bidder2_worklet->InvokeGenerateBidCallback( |
| /*bid=*/7, blink::AdDescriptor(GURL("https://ad2.com/"))); |
| bidder2_worklet.reset(); |
| |
| mock_auction_process_manager_->Flush(); |
| |
| // Make seller worklet available and finish the auction. |
| sellers.clear(); |
| mock_auction_process_manager_->WaitForWorklets(/*num_bidders=*/0, |
| /*num_sellers=*/1); |
| |
| auto seller_worklet = mock_auction_process_manager_->TakeSellerWorklet(); |
| ASSERT_TRUE(seller_worklet); |
| |
| auto score_ad_params = seller_worklet->WaitForScoreAd(); |
| EXPECT_EQ(kBidder1, score_ad_params.interest_group_owner); |
| EXPECT_EQ(6, score_ad_params.bid); |
| mojo::Remote<auction_worklet::mojom::ScoreAdClient>( |
| std::move(score_ad_params.score_ad_client)) |
| ->OnScoreAdComplete( |
| /*score=*/10, |
| /*reject_reason=*/ |
| auction_worklet::mojom::RejectReason::kNotAvailable, |
| auction_worklet::mojom::ComponentAuctionModifiedBidParamsPtr(), |
| /*scoring_signals_data_version=*/0, |
| /*has_scoring_signals_data_version=*/false, |
| /*debug_loss_report_url=*/absl::nullopt, |
| /*debug_win_report_url=*/absl::nullopt, /*pa_requests=*/{}, |
| /*errors=*/{}); |
| |
| score_ad_params = seller_worklet->WaitForScoreAd(); |
| EXPECT_EQ(kBidder2, score_ad_params.interest_group_owner); |
| EXPECT_EQ(7, score_ad_params.bid); |
| mojo::Remote<auction_worklet::mojom::ScoreAdClient>( |
| std::move(score_ad_params.score_ad_client)) |
| ->OnScoreAdComplete( |
| /*score=*/11, |
| /*reject_reason=*/ |
| auction_worklet::mojom::RejectReason::kNotAvailable, |
| auction_worklet::mojom::ComponentAuctionModifiedBidParamsPtr(), |
| /*scoring_signals_data_version=*/0, |
| /*has_scoring_signals_data_version=*/false, |
| /*debug_loss_report_url=*/absl::nullopt, |
| /*debug_win_report_url=*/absl::nullopt, /*pa_requests=*/{}, |
| /*errors=*/{}); |
| |
| // Finish the auction. |
| seller_worklet->WaitForReportResult(); |
| seller_worklet->InvokeReportResultCallback(); |
| |
| // Worklet 2 should be reloaded and ReportWin() invoked. |
| mock_auction_process_manager_->WaitForWinningBidderReload(); |
| bidder2_worklet = |
| mock_auction_process_manager_->TakeBidderWorklet(kBidder2Url); |
| bidder2_worklet->WaitForReportWin(); |
| bidder2_worklet->InvokeReportWinCallback(); |
| |
| // Bidder2 won. |
| auction_run_loop_->Run(); |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| EXPECT_EQ(kBidder2Key, result_.winning_group_id); |
| EXPECT_EQ(GURL("https://ad2.com/"), result_.ad_descriptor->url); |
| |
| // ~MockSellerWorklet verifies whether SendPendingSignalsRequests() was |
| // called. |
| seller_worklet->set_expect_send_pending_signals_requests_called(true); |
| seller_worklet.reset(); |
| } |
| |
| // Test the case where two interest groups use the same BidderWorklet, with a |
| // trusted bidding signals URL. The requests should be batched. This test |
| // basically makes sure that SendPendingSignalsRequests() is only invoked on the |
| // BidderWorklet after both GenerateBid() calls have been invoked. |
| TEST_F(AuctionRunnerTest, ReusedBidderWorkletBatchesSignalsRequests) { |
| // Bidding script used by both interest groups. Since the default bid script |
| // checks the interest group name, and this test uses two interest groups with |
| // the same bidder script, have to use a different script for this test. |
| // |
| // This script uses trusted bidding signals and the interest group name to |
| // select a winner, to make sure the trusted bidding signals makes it to the |
| // bidder. |
| const char kBidderScript[] = R"( |
| function generateBid(interestGroup, auctionSignals, perBuyerSignals, |
| trustedBiddingSignals, browserSignals) { |
| if (browserSignals.dataVersion !== 4) { |
| throw new Error(`wrong dataVersion (${browserSignals.dataVersion})`); |
| } |
| return { |
| ad: 0, |
| bid: trustedBiddingSignals['key' + interestGroup.name], |
| render: interestGroup.ads[0].renderUrl |
| }; |
| } |
| |
| // Prevent an error about this method not existing. |
| function reportWin(auctionSignals, perBuyerSignals, sellerSignals, |
| browserSignals) { |
| if (browserSignals.dataVersion !== 4) { |
| throw new Error(`wrong dataVersion (${browserSignals.dataVersion})`); |
| } |
| } |
| )"; |
| |
| // Need to use a different seller script as well, due to the validation logic |
| // in the default one being dependent on the details of the default bidder |
| // script. |
| const char kSellerScript[] = R"( |
| function scoreAd(adMetadata, bid, auctionConfig, trustedScoringSignals, |
| browserSignals) { |
| return 2 * bid; |
| } |
| |
| // Prevent an error about this method not existing. |
| function reportResult() {} |
| )"; |
| |
| // Two interest groups with all of the same URLs. They vary only in name, |
| // render URL, and bidding signals key. |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, /*name=*/"0", kBidder1Url, kBidder1TrustedSignalsUrl, |
| /*trusted_bidding_signals_keys=*/{"key0"}, GURL("https://ad1.com"))); |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, /*name=*/"1", kBidder1Url, kBidder1TrustedSignalsUrl, |
| /*trusted_bidding_signals_keys=*/{"key1"}, GURL("https://ad2.com"))); |
| |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kBidder1Url, |
| kBidderScript); |
| |
| // Trusted signals response for the single expected request. Interest group |
| // "0" bids 2, interest group "1" bids 1. |
| auction_worklet::AddVersionedJsonResponse( |
| &url_loader_factory_, |
| GURL(kBidder1TrustedSignalsUrl.spec() + |
| "?hostname=publisher1.com&keys=key0,key1&interestGroupNames=0,1"), |
| R"({"key0":2, "key1": 1})", 4); |
| |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| kSellerScript); |
| |
| StartAuction(kSellerUrl, std::move(bidders)); |
| auction_run_loop_->Run(); |
| EXPECT_TRUE(auction_complete_); |
| |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| EXPECT_EQ(InterestGroupKey(kBidder1, "0"), result_.winning_group_id); |
| EXPECT_EQ(GURL("https://ad1.com/"), result_.ad_descriptor->url); |
| EXPECT_TRUE(result_.ad_component_descriptors.empty()); |
| EXPECT_THAT(result_.report_urls, testing::UnorderedElementsAre()); |
| EXPECT_THAT(result_.ad_beacon_map, |
| testing::UnorderedElementsAreArray(kEmptyAdBeaconMap)); |
| EXPECT_TRUE( |
| private_aggregation_manager_.TakePrivateAggregationRequests().empty()); |
| EXPECT_TRUE(result_.private_aggregation_event_map.empty()); |
| } |
| |
| TEST_F(AuctionRunnerTest, AllBiddersCrashBeforeBidding) { |
| StartStandardAuctionWithMockService(); |
| auto seller_worklet = mock_auction_process_manager_->TakeSellerWorklet(); |
| ASSERT_TRUE(seller_worklet); |
| auto bidder1_worklet = |
| mock_auction_process_manager_->TakeBidderWorklet(kBidder1Url); |
| ASSERT_TRUE(bidder1_worklet); |
| auto bidder2_worklet = |
| mock_auction_process_manager_->TakeBidderWorklet(kBidder2Url); |
| ASSERT_TRUE(bidder2_worklet); |
| |
| EXPECT_FALSE(auction_complete_); |
| |
| EXPECT_THAT(observer_log_, |
| testing::UnorderedElementsAre( |
| "Create https://adstuff.publisher1.com/auction.js", |
| "Create https://adplatform.com/offers.js", |
| "Create https://anotheradthing.com/bids.js")); |
| |
| EXPECT_THAT(LiveDebuggables(), |
| testing::UnorderedElementsAre( |
| "https://adplatform.com/offers.js", |
| "https://anotheradthing.com/bids.js", |
| "https://adstuff.publisher1.com/auction.js")); |
| |
| bidder1_worklet.reset(); |
| bidder2_worklet.reset(); |
| |
| task_environment()->RunUntilIdle(); |
| |
| EXPECT_THAT(observer_log_, |
| testing::UnorderedElementsAre( |
| "Create https://adstuff.publisher1.com/auction.js", |
| "Create https://adplatform.com/offers.js", |
| "Create https://anotheradthing.com/bids.js", |
| "Destroy https://adplatform.com/offers.js", |
| "Destroy https://anotheradthing.com/bids.js", |
| "Destroy https://adstuff.publisher1.com/auction.js")); |
| |
| EXPECT_THAT(LiveDebuggables(), testing::ElementsAre()); |
| |
| auction_run_loop_->Run(); |
| |
| EXPECT_THAT( |
| result_.errors, |
| testing::UnorderedElementsAre( |
| base::StringPrintf("%s crashed while trying to run generateBid().", |
| kBidder1Url.spec().c_str()), |
| base::StringPrintf("%s crashed while trying to run generateBid().", |
| kBidder2Url.spec().c_str()))); |
| EXPECT_FALSE(result_.winning_group_id); |
| EXPECT_FALSE(result_.ad_descriptor); |
| EXPECT_TRUE(result_.ad_component_descriptors.empty()); |
| EXPECT_TRUE( |
| private_aggregation_manager_.TakePrivateAggregationRequests().empty()); |
| EXPECT_TRUE(result_.private_aggregation_event_map.empty()); |
| EXPECT_THAT(result_.interest_groups_that_bid, |
| testing::UnorderedElementsAre()); |
| |
| CheckHistograms(InterestGroupAuction::AuctionResult::kNoBids, |
| /*expected_interest_groups=*/2, /*expected_owners=*/2, |
| /*expected_sellers=*/1); |
| } |
| |
| // Test the case a single bidder worklet crashes before bidding. The auction |
| // should continue, without that bidder's bid. |
| TEST_F(AuctionRunnerTest, BidderCrashBeforeBidding) { |
| for (bool other_bidder_finishes_first : {false, true}) { |
| SCOPED_TRACE(other_bidder_finishes_first); |
| |
| observer_log_.clear(); |
| StartStandardAuctionWithMockService(); |
| auto seller_worklet = mock_auction_process_manager_->TakeSellerWorklet(); |
| ASSERT_TRUE(seller_worklet); |
| auto bidder1_worklet = |
| mock_auction_process_manager_->TakeBidderWorklet(kBidder1Url); |
| ASSERT_TRUE(bidder1_worklet); |
| auto bidder2_worklet = |
| mock_auction_process_manager_->TakeBidderWorklet(kBidder2Url); |
| ASSERT_TRUE(bidder2_worklet); |
| |
| ASSERT_FALSE(auction_complete_); |
| if (other_bidder_finishes_first) { |
| bidder2_worklet->InvokeGenerateBidCallback( |
| /*bid=*/7, blink::AdDescriptor(GURL("https://ad2.com/"))); |
| // The bidder pipe should be closed after it bids. |
| EXPECT_TRUE(bidder2_worklet->PipeIsClosed()); |
| bidder2_worklet.reset(); |
| } |
| mock_auction_process_manager_->Flush(); |
| |
| ASSERT_FALSE(auction_complete_); |
| |
| // Close Bidder1's pipe. |
| bidder1_worklet.reset(); |
| // Can't flush the closed pipe without reaching into AuctionRunner, so use |
| // RunUntilIdle() instead. |
| task_environment()->RunUntilIdle(); |
| |
| if (!other_bidder_finishes_first) { |
| bidder2_worklet->InvokeGenerateBidCallback( |
| /*bid=*/7, blink::AdDescriptor(GURL("https://ad2.com/"))); |
| // The bidder pipe should be closed after it bids. |
| EXPECT_TRUE(bidder2_worklet->PipeIsClosed()); |
| bidder2_worklet.reset(); |
| } |
| mock_auction_process_manager_->Flush(); |
| |
| EXPECT_THAT(observer_log_, |
| testing::UnorderedElementsAre( |
| "Create https://adstuff.publisher1.com/auction.js", |
| "Create https://adplatform.com/offers.js", |
| "Create https://anotheradthing.com/bids.js", |
| "Destroy https://adplatform.com/offers.js", |
| "Destroy https://anotheradthing.com/bids.js")); |
| |
| EXPECT_THAT( |
| LiveDebuggables(), |
| testing::ElementsAre("https://adstuff.publisher1.com/auction.js")); |
| |
| // The auction should be scored without waiting on the crashed kBidder1. |
| auto score_ad_params = seller_worklet->WaitForScoreAd(); |
| EXPECT_EQ(kBidder2, score_ad_params.interest_group_owner); |
| EXPECT_EQ(7, score_ad_params.bid); |
| mojo::Remote<auction_worklet::mojom::ScoreAdClient>( |
| std::move(score_ad_params.score_ad_client)) |
| ->OnScoreAdComplete( |
| /*score=*/11, |
| /*reject_reason=*/ |
| auction_worklet::mojom::RejectReason::kNotAvailable, |
| auction_worklet::mojom::ComponentAuctionModifiedBidParamsPtr(), |
| /*scoring_signals_data_version=*/0, |
| /*has_scoring_signals_data_version=*/false, |
| /*debug_loss_report_url=*/absl::nullopt, |
| /*debug_win_report_url=*/absl::nullopt, /*pa_requests=*/{}, |
| /*errors=*/{}); |
| |
| // Finish the auction. |
| seller_worklet->WaitForReportResult(); |
| seller_worklet->InvokeReportResultCallback(); |
| |
| // Worklet 2 should be reloaded and ReportWin() invoked. |
| mock_auction_process_manager_->WaitForWinningBidderReload(); |
| bidder2_worklet = |
| mock_auction_process_manager_->TakeBidderWorklet(kBidder2Url); |
| bidder2_worklet->WaitForReportWin(); |
| bidder2_worklet->InvokeReportWinCallback(); |
| |
| // Bidder2 won, Bidder1 crashed. |
| auction_run_loop_->Run(); |
| EXPECT_THAT(result_.errors, |
| testing::ElementsAre(base::StringPrintf( |
| "%s crashed while trying to run generateBid().", |
| kBidder1Url.spec().c_str()))); |
| EXPECT_EQ(kBidder2Key, result_.winning_group_id); |
| EXPECT_EQ(GURL("https://ad2.com/"), result_.ad_descriptor->url); |
| EXPECT_TRUE(result_.ad_component_descriptors.empty()); |
| EXPECT_THAT(result_.report_urls, testing::UnorderedElementsAre()); |
| EXPECT_THAT(result_.ad_beacon_map, |
| testing::UnorderedElementsAreArray(kEmptyAdBeaconMap)); |
| EXPECT_TRUE( |
| private_aggregation_manager_.TakePrivateAggregationRequests().empty()); |
| EXPECT_TRUE(result_.private_aggregation_event_map.empty()); |
| EXPECT_THAT(result_.interest_groups_that_bid, |
| testing::UnorderedElementsAre(kBidder2Key)); |
| EXPECT_EQ(R"({"render_url":"https://ad2.com/"})", |
| result_.winning_group_ad_metadata); |
| CheckHistograms(InterestGroupAuction::AuctionResult::kSuccess, |
| /*expected_interest_groups=*/2, /*expected_owners=*/2, |
| /*expected_sellers=*/1); |
| } |
| } |
| |
| // Should not have any debugging win/loss report URLs after auction when feature |
| // kBiddingAndScoringDebugReportingAPI is not enabled. |
| TEST_F(AuctionRunnerTest, ForDebuggingOnlyReporting) { |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder1Url, |
| MakeBidScript(kSeller, "1", "https://ad1.com/", /*num_ad_components=*/2, |
| kBidder1, kBidder1Name, |
| /*has_signals=*/false, "k1", "a", |
| /*report_post_auction_signals=*/false, |
| kBidder1DebugLossReportUrl, kBidder1DebugWinReportUrl)); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder2Url, |
| MakeBidScript(kSeller, "2", "https://ad2.com/", /*num_ad_components=*/2, |
| kBidder2, kBidder2Name, |
| /*has_signals=*/false, "l2", "b", |
| /*report_post_auction_signals=*/false, |
| kBidder2DebugLossReportUrl, kBidder2DebugWinReportUrl)); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kSellerUrl, |
| MakeAuctionScript(/*report_post_auction_signals=*/false, kSellerUrl, |
| kSellerDebugLossReportBaseUrl, |
| kSellerDebugWinReportBaseUrl)); |
| |
| RunStandardAuction(/*request_trusted_bidding_signals=*/false); |
| EXPECT_THAT(result_.errors, testing::UnorderedElementsAre()); |
| |
| // Bidder 2 won the auction. |
| EXPECT_EQ(kBidder2Key, result_.winning_group_id); |
| EXPECT_EQ(GURL("https://ad2.com/"), result_.ad_descriptor->url); |
| |
| EXPECT_EQ(0u, result_.debug_loss_report_urls.size()); |
| EXPECT_EQ(0u, result_.debug_win_report_urls.size()); |
| } |
| |
| // If the seller crashes before all bids are scored, the auction fails. Seller |
| // load failures look the same to auctions, so this test also covers load |
| // failures in the same places. Note that a seller worklet load error while |
| // waiting for bidder worklet processes is covered in another test, and looks |
| // exactly like a crash at the same point to the AuctionRunner. |
| TEST_F(AuctionRunnerTest, SellerCrash) { |
| enum class CrashPhase { |
| kLoad, |
| kScoreBid, |
| }; |
| for (CrashPhase crash_phase : {CrashPhase::kLoad, CrashPhase::kScoreBid}) { |
| SCOPED_TRACE(static_cast<int>(crash_phase)); |
| |
| StartStandardAuctionWithMockService(); |
| |
| auto seller_worklet = mock_auction_process_manager_->TakeSellerWorklet(); |
| ASSERT_TRUE(seller_worklet); |
| auto bidder1_worklet = |
| mock_auction_process_manager_->TakeBidderWorklet(kBidder1Url); |
| ASSERT_TRUE(bidder1_worklet); |
| auto bidder2_worklet = |
| mock_auction_process_manager_->TakeBidderWorklet(kBidder2Url); |
| ASSERT_TRUE(bidder2_worklet); |
| |
| if (crash_phase == CrashPhase::kLoad) { |
| seller_worklet->set_expect_send_pending_signals_requests_called(false); |
| } else { |
| // Generate both bids, wait for seller to receive them.. |
| bidder1_worklet->InvokeGenerateBidCallback( |
| /*bid=*/5, blink::AdDescriptor(GURL("https://ad1.com/"))); |
| bidder2_worklet->InvokeGenerateBidCallback( |
| /*bid=*/7, blink::AdDescriptor(GURL("https://ad2.com/"))); |
| auto score_ad_params = seller_worklet->WaitForScoreAd(); |
| auto score_ad_params2 = seller_worklet->WaitForScoreAd(); |
| |
| // Wait for SendPendingSignalsRequests() invocation. |
| task_environment()->RunUntilIdle(); |
| } |
| |
| // Simulate seller crash. |
| seller_worklet.reset(); |
| |
| // Wait for auction to complete. |
| auction_run_loop_->Run(); |
| |
| EXPECT_THAT(result_.errors, testing::ElementsAre(base::StringPrintf( |
| "%s crashed.", kSellerUrl.spec().c_str()))); |
| // No bidder won, seller crashed. |
| EXPECT_FALSE(result_.winning_group_id); |
| EXPECT_FALSE(result_.ad_descriptor); |
| EXPECT_TRUE(result_.ad_component_descriptors.empty()); |
| EXPECT_TRUE( |
| private_aggregation_manager_.TakePrivateAggregationRequests().empty()); |
| EXPECT_TRUE(result_.private_aggregation_event_map.empty()); |
| EXPECT_THAT(result_.interest_groups_that_bid, |
| testing::UnorderedElementsAre()); |
| CheckHistograms(InterestGroupAuction::AuctionResult::kSellerWorkletCrashed, |
| /*expected_interest_groups=*/2, /*expected_owners=*/2, |
| /*expected_sellers=*/1); |
| } |
| } |
| |
| TEST_F(AuctionRunnerTest, ComponentAuctionAllBiddersCrashBeforeBidding) { |
| SetUpComponentAuctionAndResponses(/*bidder1_seller=*/kComponentSeller1, |
| /*bidder2_seller=*/kComponentSeller2, |
| /*bid_from_component_auction_wins=*/false); |
| StartStandardAuctionWithMockService(); |
| |
| EXPECT_FALSE(auction_complete_); |
| |
| auto bidder1_worklet = |
| mock_auction_process_manager_->TakeBidderWorklet(kBidder1Url); |
| ASSERT_TRUE(bidder1_worklet); |
| bidder1_worklet.reset(); |
| |
| auto bidder2_worklet = |
| mock_auction_process_manager_->TakeBidderWorklet(kBidder2Url); |
| ASSERT_TRUE(bidder2_worklet); |
| bidder2_worklet.reset(); |
| |
| auction_run_loop_->Run(); |
| |
| EXPECT_THAT( |
| result_.errors, |
| testing::UnorderedElementsAre( |
| base::StringPrintf("%s crashed while trying to run generateBid().", |
| kBidder1Url.spec().c_str()), |
| base::StringPrintf("%s crashed while trying to run generateBid().", |
| kBidder2Url.spec().c_str()))); |
| EXPECT_FALSE(result_.ad_descriptor); |
| |
| CheckHistograms(InterestGroupAuction::AuctionResult::kNoBids, |
| /*expected_interest_groups=*/2, /*expected_owners=*/2, |
| /*expected_sellers=*/3); |
| } |
| |
| // Test the case that one component has both bidders, one of which crashes, to |
| // make sure a single bidder crash doesn't result in the component auction |
| // failing. |
| TEST_F(AuctionRunnerTest, ComponentAuctionOneBidderCrashesBeforeBidding) { |
| SetUpComponentAuctionAndResponses(/*bidder1_seller=*/kComponentSeller1, |
| /*bidder2_seller=*/kComponentSeller1, |
| /*bid_from_component_auction_wins=*/true); |
| StartStandardAuctionWithMockService(); |
| |
| EXPECT_FALSE(auction_complete_); |
| |
| // Close the first bidder worklet's pipe, simulating a crash. |
| auto bidder1_worklet = |
| mock_auction_process_manager_->TakeBidderWorklet(kBidder1Url); |
| ASSERT_TRUE(bidder1_worklet); |
| bidder1_worklet.reset(); |
| // Wait for the AuctionRunner to observe the crash. |
| task_environment()->RunUntilIdle(); |
| |
| // The second bidder worklet bids. |
| auto bidder2_worklet = |
| mock_auction_process_manager_->TakeBidderWorklet(kBidder2Url); |
| ASSERT_TRUE(bidder2_worklet); |
| bidder2_worklet->InvokeGenerateBidCallback( |
| 2, blink::AdDescriptor(GURL("https://ad2.com/"))); |
| |
| // Component worklet scores the bid. |
| auto component_seller_worklet = |
| mock_auction_process_manager_->TakeSellerWorklet(kComponentSeller1Url); |
| ASSERT_TRUE(component_seller_worklet); |
| auto score_ad_params = component_seller_worklet->WaitForScoreAd(); |
| EXPECT_EQ(kBidder2, score_ad_params.interest_group_owner); |
| EXPECT_EQ(2, score_ad_params.bid); |
| mojo::Remote<auction_worklet::mojom::ScoreAdClient>( |
| std::move(score_ad_params.score_ad_client)) |
| ->OnScoreAdComplete( |
| /*score=*/3, |
| /*reject_reason=*/ |
| auction_worklet::mojom::RejectReason::kNotAvailable, |
| auction_worklet::mojom::ComponentAuctionModifiedBidParams::New( |
| /*ad=*/"null", |
| /*bid=*/0, |
| /*has_bid=*/false), |
| /*scoring_signals_data_version=*/0, |
| /*has_scoring_signals_data_version=*/false, |
| /*debug_loss_report_url=*/absl::nullopt, |
| /*debug_win_report_url=*/absl::nullopt, /*pa_requests=*/{}, |
| /*errors=*/{}); |
| |
| // Top-level seller worklet scores the bid. |
| auto top_level_seller_worklet = |
| mock_auction_process_manager_->TakeSellerWorklet(kSellerUrl); |
| ASSERT_TRUE(top_level_seller_worklet); |
| score_ad_params = top_level_seller_worklet->WaitForScoreAd(); |
| EXPECT_EQ(kBidder2, score_ad_params.interest_group_owner); |
| EXPECT_EQ(2, score_ad_params.bid); |
| mojo::Remote<auction_worklet::mojom::ScoreAdClient>( |
| std::move(score_ad_params.score_ad_client)) |
| ->OnScoreAdComplete( |
| /*score=*/4, |
| /*reject_reason=*/ |
| auction_worklet::mojom::RejectReason::kNotAvailable, |
| auction_worklet::mojom::ComponentAuctionModifiedBidParamsPtr(), |
| /*scoring_signals_data_version=*/0, |
| /*has_scoring_signals_data_version=*/false, |
| /*debug_loss_report_url=*/absl::nullopt, |
| /*debug_win_report_url=*/absl::nullopt, /*pa_requests=*/{}, |
| /*errors=*/{}); |
| |
| // Top-level seller worklet returns a report url. |
| top_level_seller_worklet->WaitForReportResult(); |
| top_level_seller_worklet->InvokeReportResultCallback( |
| GURL("https://report1.test/")); |
| |
| // The component seller worklet should be reloaded and ReportResult() invoked. |
| mock_auction_process_manager_->WaitForWinningSellerReload(); |
| component_seller_worklet = |
| mock_auction_process_manager_->TakeSellerWorklet(kComponentSeller1Url); |
| component_seller_worklet->set_expect_send_pending_signals_requests_called( |
| false); |
| ASSERT_TRUE(component_seller_worklet); |
| component_seller_worklet->WaitForReportResult(); |
| component_seller_worklet->InvokeReportResultCallback( |
| GURL("https://report2.test/")); |
| |
| // Bidder worklet 2 should be reloaded and ReportWin() invoked. |
| mock_auction_process_manager_->WaitForWinningBidderReload(); |
| bidder2_worklet = |
| mock_auction_process_manager_->TakeBidderWorklet(kBidder2Url); |
| bidder2_worklet->WaitForReportWin(); |
| bidder2_worklet->InvokeReportWinCallback(GURL("https://report3.test/")); |
| |
| // Bidder2 won, Bidder1 crashed. |
| auction_run_loop_->Run(); |
| EXPECT_THAT(result_.errors, |
| testing::UnorderedElementsAre(base::StringPrintf( |
| "%s crashed while trying to run generateBid().", |
| kBidder1Url.spec().c_str()))); |
| EXPECT_EQ(GURL("https://ad2.com/"), result_.ad_descriptor->url); |
| EXPECT_THAT(result_.report_urls, |
| testing::UnorderedElementsAre(GURL("https://report1.test/"), |
| GURL("https://report2.test/"), |
| GURL("https://report3.test/"))); |
| EXPECT_THAT(result_.interest_groups_that_bid, |
| testing::UnorderedElementsAre(kBidder2Key)); |
| CheckHistograms(InterestGroupAuction::AuctionResult::kSuccess, |
| /*expected_interest_groups=*/2, /*expected_owners=*/2, |
| /*expected_sellers=*/2); |
| } |
| |
| // Test the case that all component sellers crash. |
| TEST_F(AuctionRunnerTest, ComponentAuctionComponentSellersAllCrash) { |
| SetUpComponentAuctionAndResponses(/*bidder1_seller=*/kComponentSeller1, |
| /*bidder2_seller=*/kComponentSeller2, |
| /*bid_from_component_auction_wins=*/false); |
| StartStandardAuctionWithMockService(); |
| |
| EXPECT_FALSE(auction_complete_); |
| |
| // First component seller worklet crashes. Auction should not complete. |
| auto component_seller_worklet1 = |
| mock_auction_process_manager_->TakeSellerWorklet(kComponentSeller1Url); |
| component_seller_worklet1->set_expect_send_pending_signals_requests_called( |
| false); |
| component_seller_worklet1.reset(); |
| task_environment()->RunUntilIdle(); |
| EXPECT_FALSE(auction_complete_); |
| |
| // Second component seller worklet crashes. Auction should complete. |
| auto component_seller_worklet2 = |
| mock_auction_process_manager_->TakeSellerWorklet(kComponentSeller2Url); |
| component_seller_worklet2->set_expect_send_pending_signals_requests_called( |
| false); |
| component_seller_worklet2.reset(); |
| auction_run_loop_->Run(); |
| |
| EXPECT_THAT(result_.errors, |
| testing::UnorderedElementsAre( |
| base::StringPrintf("%s crashed.", |
| kComponentSeller1Url.spec().c_str()), |
| base::StringPrintf("%s crashed.", |
| kComponentSeller2Url.spec().c_str()))); |
| EXPECT_FALSE(result_.ad_descriptor); |
| EXPECT_THAT(result_.interest_groups_that_bid, |
| testing::UnorderedElementsAre()); |
| CheckHistograms(InterestGroupAuction::AuctionResult::kNoBids, |
| /*expected_interest_groups=*/2, /*expected_owners=*/2, |
| /*expected_sellers=*/3); |
| } |
| |
| // Test cases where a component seller returns an invalid |
| // ComponentAuctionModifiedBidParams. |
| TEST_F(AuctionRunnerTest, ComponentAuctionComponentSellerBadBidParams) { |
| const struct { |
| auction_worklet::mojom::ComponentAuctionModifiedBidParamsPtr params; |
| const char* expected_error; |
| } kTestCases[] = { |
| // Empty parameters are invalid. |
| {auction_worklet::mojom::ComponentAuctionModifiedBidParamsPtr(), |
| "Invalid component_auction_modified_bid_params"}, |
| |
| // Bad bids. |
| {auction_worklet::mojom::ComponentAuctionModifiedBidParams::New( |
| /*ad=*/"null", |
| /*bid=*/0, |
| /*has_bid=*/true), |
| "Invalid component_auction_modified_bid_params bid"}, |
| {auction_worklet::mojom::ComponentAuctionModifiedBidParams::New( |
| /*ad=*/"null", |
| /*bid=*/-1, |
| /*has_bid=*/true), |
| "Invalid component_auction_modified_bid_params bid"}, |
| {auction_worklet::mojom::ComponentAuctionModifiedBidParams::New( |
| /*ad=*/"null", |
| /*bid=*/std::numeric_limits<double>::infinity(), |
| /*has_bid=*/true), |
| "Invalid component_auction_modified_bid_params bid"}, |
| {auction_worklet::mojom::ComponentAuctionModifiedBidParams::New( |
| /*ad=*/"null", |
| /*bid=*/-std::numeric_limits<double>::infinity(), |
| /*has_bid=*/true), |
| "Invalid component_auction_modified_bid_params bid"}, |
| {auction_worklet::mojom::ComponentAuctionModifiedBidParams::New( |
| /*ad=*/"null", |
| /*bid=*/-std::numeric_limits<double>::quiet_NaN(), |
| /*has_bid=*/true), |
| "Invalid component_auction_modified_bid_params bid"}, |
| }; |
| |
| SetUpComponentAuctionAndResponses(/*bidder1_seller=*/kComponentSeller1, |
| /*bidder2_seller=*/kComponentSeller1, |
| /*bid_from_component_auction_wins=*/true); |
| |
| for (const auto& test_case : kTestCases) { |
| StartStandardAuctionWithMockService(); |
| |
| // First bidder doesn't finish scoring the bid. This should not stall the |
| // auction, since these errors represent security errors from the component |
| // auction's seller worklet. |
| auto bidder1_worklet = |
| mock_auction_process_manager_->TakeBidderWorklet(kBidder1Url); |
| ASSERT_TRUE(bidder1_worklet); |
| |
| // The second bidder worklet bids. |
| auto bidder2_worklet = |
| mock_auction_process_manager_->TakeBidderWorklet(kBidder2Url); |
| ASSERT_TRUE(bidder2_worklet); |
| bidder2_worklet->InvokeGenerateBidCallback( |
| 2, blink::AdDescriptor(GURL("https://ad2.com/"))); |
| |
| // Component seller scores the bid, but returns a bad |
| // ComponentAuctionModifiedBidParams. |
| auto component_seller_worklet = |
| mock_auction_process_manager_->TakeSellerWorklet(kComponentSeller1Url); |
| ASSERT_TRUE(component_seller_worklet); |
| component_seller_worklet->set_expect_send_pending_signals_requests_called( |
| false); |
| auto score_ad_params = component_seller_worklet->WaitForScoreAd(); |
| EXPECT_EQ(kBidder2, score_ad_params.interest_group_owner); |
| EXPECT_EQ(2, score_ad_params.bid); |
| mojo::Remote<auction_worklet::mojom::ScoreAdClient>( |
| std::move(score_ad_params.score_ad_client)) |
| ->OnScoreAdComplete(/*score=*/3, |
| /*reject_reason=*/ |
| auction_worklet::mojom::RejectReason::kNotAvailable, |
| test_case.params.Clone(), |
| /*scoring_signals_data_version=*/0, |
| /*has_scoring_signals_data_version=*/false, |
| /*debug_loss_report_url=*/absl::nullopt, |
| /*debug_win_report_url=*/absl::nullopt, |
| /*pa_requests=*/{}, |
| /*errors=*/{}); |
| |
| // The auction fails, because of the bad ComponentAuctionModifiedBidParams. |
| auction_run_loop_->Run(); |
| EXPECT_THAT(result_.errors, testing::UnorderedElementsAre()); |
| EXPECT_FALSE(result_.ad_descriptor); |
| EXPECT_THAT(result_.interest_groups_that_bid, |
| testing::UnorderedElementsAre()); |
| |
| // Since these are security errors rather than script errors, they're |
| // reported as bad Mojo messages, instead of in the return error list. |
| EXPECT_EQ(test_case.expected_error, TakeBadMessage()); |
| |
| // The component auction failed with a Mojo error, but the top-level auction |
| // sees that as no bids. |
| CheckHistograms(InterestGroupAuction::AuctionResult::kNoBids, |
| /*expected_interest_groups=*/2, /*expected_owners=*/2, |
| /*expected_sellers=*/2); |
| } |
| } |
| |
| // Test cases where a top-level seller returns an |
| // ComponentAuctionModifiedBidParams, which should result in failing the |
| // auction. |
| TEST_F(AuctionRunnerTest, TopLevelSellerBadBidParams) { |
| // Run a standard auction, with only a top-level seller. |
| StartStandardAuctionWithMockService(); |
| |
| // First bidder doesn't finish scoring the bid. This should not stall the |
| // auction, since these errors represent security errors from the component |
| // auction's seller worklet. |
| auto bidder1_worklet = |
| mock_auction_process_manager_->TakeBidderWorklet(kBidder1Url); |
| ASSERT_TRUE(bidder1_worklet); |
| |
| // The second bidder worklet bids. |
| auto bidder2_worklet = |
| mock_auction_process_manager_->TakeBidderWorklet(kBidder2Url); |
| ASSERT_TRUE(bidder2_worklet); |
| bidder2_worklet->InvokeGenerateBidCallback( |
| 2, blink::AdDescriptor(GURL("https://ad2.com/"))); |
| |
| // Seller scores the bid, but returns a ComponentAuctionModifiedBidParams. |
| auto seller_worklet = |
| mock_auction_process_manager_->TakeSellerWorklet(kSellerUrl); |
| ASSERT_TRUE(seller_worklet); |
| seller_worklet->set_expect_send_pending_signals_requests_called(false); |
| auto score_ad_params = seller_worklet->WaitForScoreAd(); |
| EXPECT_EQ(kBidder2, score_ad_params.interest_group_owner); |
| EXPECT_EQ(2, score_ad_params.bid); |
| mojo::Remote<auction_worklet::mojom::ScoreAdClient>( |
| std::move(score_ad_params.score_ad_client)) |
| ->OnScoreAdComplete( |
| /*score=*/3, |
| /*reject_reason=*/ |
| auction_worklet::mojom::RejectReason::kNotAvailable, |
| auction_worklet::mojom::ComponentAuctionModifiedBidParams::New( |
| /*ad=*/"null", |
| /*bid=*/0, |
| /*has_bid=*/false), |
| /*scoring_signals_data_version=*/0, |
| /*has_scoring_signals_data_version=*/false, |
| /*debug_loss_report_url=*/absl::nullopt, |
| /*debug_win_report_url=*/absl::nullopt, /*pa_requests=*/{}, |
| /*errors=*/{}); |
| |
| auction_run_loop_->Run(); |
| |
| // The auction fails, because of the unexpected |
| // ComponentAuctionModifiedBidParams. |
| // |
| // Since these are security errors rather than script errors, they're |
| // reported as bad Mojo messages, instead of in the return error list. |
| EXPECT_THAT(result_.errors, testing::UnorderedElementsAre()); |
| EXPECT_EQ("Invalid component_auction_modified_bid_params", TakeBadMessage()); |
| EXPECT_FALSE(result_.ad_descriptor); |
| EXPECT_THAT(result_.interest_groups_that_bid, |
| testing::UnorderedElementsAre()); |
| |
| CheckHistograms(InterestGroupAuction::AuctionResult::kBadMojoMessage, |
| /*expected_interest_groups=*/2, /*expected_owners=*/2, |
| /*expected_sellers=*/1); |
| } |
| |
| TEST_F(AuctionRunnerTest, NullAdComponents) { |
| const GURL kRenderUrl = GURL("https://ad1.com"); |
| const struct { |
| absl::optional<std::vector<blink::AdDescriptor>> bid_ad_component_urls; |
| bool expect_successful_bid; |
| } kTestCases[] = { |
| {absl::nullopt, true}, |
| {std::vector<blink::AdDescriptor>{}, false}, |
| {std::vector<blink::AdDescriptor>{ |
| blink::AdDescriptor(GURL("https://ad1.com-component1.com"))}, |
| false}, |
| }; |
| |
| for (const auto& test_case : kTestCases) { |
| UseMockWorkletService(); |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back( |
| MakeInterestGroup(kBidder1, kBidder1Name, kBidder1Url, |
| kBidder1TrustedSignalsUrl, {"k1", "k2"}, kRenderUrl, |
| /*ad_component_urls=*/absl::nullopt)); |
| |
| StartAuction(kSellerUrl, std::move(bidders)); |
| |
| mock_auction_process_manager_->WaitForWorklets(/*num_bidders=*/1); |
| |
| auto seller_worklet = mock_auction_process_manager_->TakeSellerWorklet(); |
| ASSERT_TRUE(seller_worklet); |
| auto bidder_worklet = |
| mock_auction_process_manager_->TakeBidderWorklet(kBidder1Url); |
| ASSERT_TRUE(bidder_worklet); |
| |
| bidder_worklet->InvokeGenerateBidCallback( |
| /*bid=*/1, blink::AdDescriptor(kRenderUrl), /*mojo_kanon_bid=*/nullptr, |
| test_case.bid_ad_component_urls, base::TimeDelta()); |
| |
| if (test_case.expect_successful_bid) { |
| // Since the bid was valid, it should be scored. |
| auto score_ad_params = seller_worklet->WaitForScoreAd(); |
| EXPECT_EQ(kBidder1, score_ad_params.interest_group_owner); |
| EXPECT_EQ(1, score_ad_params.bid); |
| mojo::Remote<auction_worklet::mojom::ScoreAdClient>( |
| std::move(score_ad_params.score_ad_client)) |
| ->OnScoreAdComplete( |
| /*score=*/11, |
| /*reject_reason=*/ |
| auction_worklet::mojom::RejectReason::kNotAvailable, |
| auction_worklet::mojom::ComponentAuctionModifiedBidParamsPtr(), |
| /*scoring_signals_data_version=*/0, |
| /*has_scoring_signals_data_version=*/false, |
| /*debug_loss_report_url=*/absl::nullopt, |
| /*debug_win_report_url=*/absl::nullopt, /*pa_requests=*/{}, |
| /*errors=*/{}); |
| |
| // Finish the auction. |
| seller_worklet->WaitForReportResult(); |
| seller_worklet->InvokeReportResultCallback(); |
| mock_auction_process_manager_->WaitForWinningBidderReload(); |
| bidder_worklet = |
| mock_auction_process_manager_->TakeBidderWorklet(kBidder1Url); |
| bidder_worklet->WaitForReportWin(); |
| bidder_worklet->InvokeReportWinCallback(); |
| auction_run_loop_->Run(); |
| |
| // The bidder should win the auction. |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| EXPECT_EQ(kBidder1Key, result_.winning_group_id); |
| EXPECT_EQ(GURL("https://ad1.com/"), result_.ad_descriptor->url); |
| EXPECT_TRUE(result_.ad_component_descriptors.empty()); |
| EXPECT_THAT(result_.report_urls, testing::UnorderedElementsAre()); |
| EXPECT_THAT(result_.ad_beacon_map, |
| testing::UnorderedElementsAreArray(kEmptyAdBeaconMap)); |
| EXPECT_TRUE(private_aggregation_manager_.TakePrivateAggregationRequests() |
| .empty()); |
| EXPECT_TRUE(result_.private_aggregation_event_map.empty()); |
| EXPECT_THAT(result_.interest_groups_that_bid, |
| testing::UnorderedElementsAre(kBidder1Key)); |
| EXPECT_EQ(R"({"render_url":"https://ad1.com/","metadata":{"ads": true}})", |
| result_.winning_group_ad_metadata); |
| CheckHistograms(InterestGroupAuction::AuctionResult::kSuccess, |
| /*expected_interest_groups=*/1, |
| /*expected_owners=*/1, /*expected_sellers=*/1); |
| } else { |
| // Since there's no acceptable bid, the seller worklet is never asked to |
| // score a bid. |
| auction_run_loop_->Run(); |
| |
| EXPECT_EQ("Unexpected non-null ad component list", TakeBadMessage()); |
| |
| // No bidder won. |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| EXPECT_FALSE(result_.winning_group_id); |
| EXPECT_FALSE(result_.ad_descriptor); |
| EXPECT_TRUE(result_.ad_component_descriptors.empty()); |
| EXPECT_TRUE(private_aggregation_manager_.TakePrivateAggregationRequests() |
| .empty()); |
| EXPECT_TRUE(result_.private_aggregation_event_map.empty()); |
| EXPECT_THAT(result_.interest_groups_that_bid, |
| testing::UnorderedElementsAre()); |
| CheckHistograms(InterestGroupAuction::AuctionResult::kNoBids, |
| /*expected_interest_groups=*/1, /*expected_owners=*/1, |
| /*expected_sellers=*/1); |
| } |
| } |
| } |
| |
| // Test that the limit of kMaxAdComponents ad components per bid is enforced. |
| TEST_F(AuctionRunnerTest, AdComponentsLimit) { |
| const GURL kRenderUrl = GURL("https://ad1.com"); |
| |
| for (size_t num_components = 1; |
| num_components < blink::kMaxAdAuctionAdComponents + 2; |
| num_components++) { |
| std::vector<GURL> ad_component_urls; |
| std::vector<blink::AdDescriptor> ad_component_descriptors; |
| for (size_t i = 0; i < num_components; ++i) { |
| ad_component_urls.emplace_back(base::StringPrintf("https://%zu.com", i)); |
| ad_component_descriptors.emplace_back( |
| GURL(base::StringPrintf("https://%zu.com", i))); |
| } |
| UseMockWorkletService(); |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, kBidder1Name, kBidder1Url, kBidder1TrustedSignalsUrl, |
| {"k1", "k2"}, kRenderUrl, ad_component_urls)); |
| |
| StartAuction(kSellerUrl, std::move(bidders)); |
| |
| mock_auction_process_manager_->WaitForWorklets(/*num_bidders=*/1); |
| |
| auto seller_worklet = mock_auction_process_manager_->TakeSellerWorklet(); |
| ASSERT_TRUE(seller_worklet); |
| auto bidder_worklet = |
| mock_auction_process_manager_->TakeBidderWorklet(kBidder1Url); |
| ASSERT_TRUE(bidder_worklet); |
| |
| bidder_worklet->InvokeGenerateBidCallback( |
| /*bid=*/1, blink::AdDescriptor(kRenderUrl), /*mojo_kanon_bid=*/nullptr, |
| ad_component_descriptors, base::TimeDelta()); |
| |
| if (num_components <= blink::kMaxAdAuctionAdComponents) { |
| // Since the bid was valid, it should be scored. |
| auto score_ad_params = seller_worklet->WaitForScoreAd(); |
| EXPECT_EQ(kBidder1, score_ad_params.interest_group_owner); |
| EXPECT_EQ(1, score_ad_params.bid); |
| mojo::Remote<auction_worklet::mojom::ScoreAdClient>( |
| std::move(score_ad_params.score_ad_client)) |
| ->OnScoreAdComplete( |
| /*score=*/11, |
| /*reject_reason=*/ |
| auction_worklet::mojom::RejectReason::kNotAvailable, |
| auction_worklet::mojom::ComponentAuctionModifiedBidParamsPtr(), |
| /*scoring_signals_data_version=*/0, |
| /*has_scoring_signals_data_version=*/false, |
| /*debug_loss_report_url=*/absl::nullopt, |
| /*debug_win_report_url=*/absl::nullopt, /*pa_requests=*/{}, |
| /*errors=*/{}); |
| |
| // Finish the auction. |
| seller_worklet->WaitForReportResult(); |
| seller_worklet->InvokeReportResultCallback(); |
| mock_auction_process_manager_->WaitForWinningBidderReload(); |
| bidder_worklet = |
| mock_auction_process_manager_->TakeBidderWorklet(kBidder1Url); |
| bidder_worklet->WaitForReportWin(); |
| bidder_worklet->InvokeReportWinCallback(); |
| auction_run_loop_->Run(); |
| |
| // The bidder should win the auction. |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| EXPECT_EQ(kBidder1Key, result_.winning_group_id); |
| EXPECT_EQ(GURL("https://ad1.com/"), result_.ad_descriptor->url); |
| EXPECT_EQ(ad_component_descriptors, result_.ad_component_descriptors); |
| EXPECT_THAT(result_.report_urls, testing::UnorderedElementsAre()); |
| EXPECT_THAT(result_.ad_beacon_map, |
| testing::UnorderedElementsAreArray(kEmptyAdBeaconMap)); |
| EXPECT_TRUE(private_aggregation_manager_.TakePrivateAggregationRequests() |
| .empty()); |
| EXPECT_TRUE(result_.private_aggregation_event_map.empty()); |
| EXPECT_THAT(result_.interest_groups_that_bid, |
| testing::UnorderedElementsAre(kBidder1Key)); |
| EXPECT_EQ(R"({"render_url":"https://ad1.com/","metadata":{"ads": true}})", |
| result_.winning_group_ad_metadata); |
| CheckHistograms(InterestGroupAuction::AuctionResult::kSuccess, |
| /*expected_interest_groups=*/1, |
| /*expected_owners=*/1, /*expected_sellers=*/1); |
| } else { |
| // Since there's no acceptable bid, the seller worklet is never asked to |
| // score a bid. |
| auction_run_loop_->Run(); |
| |
| EXPECT_EQ("Too many ad component URLs", TakeBadMessage()); |
| |
| // No bidder won. |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| EXPECT_FALSE(result_.winning_group_id); |
| EXPECT_FALSE(result_.ad_descriptor); |
| EXPECT_TRUE(result_.ad_component_descriptors.empty()); |
| EXPECT_TRUE(private_aggregation_manager_.TakePrivateAggregationRequests() |
| .empty()); |
| EXPECT_TRUE(result_.private_aggregation_event_map.empty()); |
| EXPECT_THAT(result_.interest_groups_that_bid, |
| testing::UnorderedElementsAre()); |
| CheckHistograms(InterestGroupAuction::AuctionResult::kNoBids, |
| /*expected_interest_groups=*/1, /*expected_owners=*/1, |
| /*expected_sellers=*/1); |
| } |
| } |
| } |
| |
| // Test cases where a bad bid is received over Mojo. Bad bids should be rejected |
| // in the Mojo process, so these are treated as security errors. |
| TEST_F(AuctionRunnerTest, BadBid) { |
| const struct TestCase { |
| const char* expected_error_message; |
| double bid; |
| blink::AdDescriptor ad_descriptor; |
| absl::optional<std::vector<blink::AdDescriptor>> ad_component_descriptors; |
| base::TimeDelta duration; |
| } kTestCases[] = { |
| // Bids that aren't positive integers. |
| { |
| "Invalid bid value", |
| -10, |
| blink::AdDescriptor(GURL("https://ad1.com")), |
| absl::nullopt, |
| base::TimeDelta(), |
| }, |
| { |
| "Invalid bid value", |
| 0, |
| blink::AdDescriptor(GURL("https://ad1.com")), |
| absl::nullopt, |
| base::TimeDelta(), |
| }, |
| { |
| "Invalid bid value", |
| std::numeric_limits<double>::infinity(), |
| blink::AdDescriptor(GURL("https://ad1.com")), |
| absl::nullopt, |
| base::TimeDelta(), |
| }, |
| { |
| "Invalid bid value", |
| std::numeric_limits<double>::quiet_NaN(), |
| blink::AdDescriptor(GURL("https://ad1.com")), |
| absl::nullopt, |
| base::TimeDelta(), |
| }, |
| |
| // Invalid render URL. |
| { |
| "Bid render ad must have a valid URL and size (if specified)", |
| 1, |
| blink::AdDescriptor(GURL(":")), |
| absl::nullopt, |
| base::TimeDelta(), |
| }, |
| |
| // Non-HTTPS render URLs. |
| { |
| "Bid render ad must have a valid URL and size (if specified)", |
| 1, |
| blink::AdDescriptor(GURL("data:,foo")), |
| absl::nullopt, |
| base::TimeDelta(), |
| }, |
| { |
| "Bid render ad must have a valid URL and size (if specified)", |
| 1, |
| blink::AdDescriptor(GURL("http://ad1.com")), |
| absl::nullopt, |
| base::TimeDelta(), |
| }, |
| |
| // HTTPS render URL that's not in the list of allowed renderUrls. |
| { |
| "Bid render ad must have a valid URL and size (if specified)", |
| 1, |
| blink::AdDescriptor(GURL("https://ad2.com")), |
| absl::nullopt, |
| base::TimeDelta(), |
| }, |
| |
| // HTTPS render URL with an invalid size value. |
| { |
| "Bid render ad must have a valid URL and size (if specified)", |
| 1, |
| blink::AdDescriptor( |
| GURL("https://ad1.com"), |
| blink::AdSize(0, blink::AdSize::LengthUnit::kPixels, 100, |
| blink::AdSize::LengthUnit::kPixels)), |
| absl::nullopt, |
| base::TimeDelta(), |
| }, |
| |
| // HTTPS render URL with an invalid size unit. |
| { |
| "Bid render ad must have a valid URL and size (if specified)", |
| 1, |
| blink::AdDescriptor( |
| GURL("https://ad1.com"), |
| blink::AdSize(100, blink::AdSize::LengthUnit::kInvalid, 100, |
| blink::AdSize::LengthUnit::kPixels)), |
| absl::nullopt, |
| base::TimeDelta(), |
| }, |
| |
| // HTTPS render URL with a size specification that does not match any |
| // allowed ad descriptors. |
| { |
| "Bid render ad must have a valid URL and size (if specified)", |
| 1, |
| blink::AdDescriptor( |
| GURL("https://ad1.com"), |
| blink::AdSize(100, blink::AdSize::LengthUnit::kPixels, 100, |
| blink::AdSize::LengthUnit::kPixels)), |
| absl::nullopt, |
| base::TimeDelta(), |
| }, |
| |
| // Invalid component URL. |
| { |
| "Bid ad component must have a valid URL and size (if specified)", |
| 1, |
| blink::AdDescriptor(GURL("https://ad1.com")), |
| std::vector<blink::AdDescriptor>{blink::AdDescriptor(GURL(":"))}, |
| base::TimeDelta(), |
| }, |
| |
| // HTTPS component URL that's not in the list of allowed ad component |
| // URLs. |
| { |
| "Bid ad component must have a valid URL and size (if specified)", |
| 1, |
| blink::AdDescriptor(GURL("https://ad1.com")), |
| std::vector<blink::AdDescriptor>{ |
| blink::AdDescriptor(GURL("https://ad2.com-component1.com"))}, |
| base::TimeDelta(), |
| }, |
| { |
| "Bid ad component must have a valid URL and size (if specified)", |
| 1, |
| blink::AdDescriptor(GURL("https://ad1.com")), |
| std::vector<blink::AdDescriptor>{ |
| blink::AdDescriptor(GURL("https://ad1.com-component1.com")), |
| blink::AdDescriptor(GURL("https://ad2.com-component1.com"))}, |
| base::TimeDelta(), |
| }, |
| |
| // HTTPS component URL with an invalid size value. |
| { |
| "Bid ad component must have a valid URL and size (if specified)", |
| 1, |
| blink::AdDescriptor(GURL("https://ad1.com")), |
| std::vector<blink::AdDescriptor>{blink::AdDescriptor( |
| GURL("https://ad1.com-component1.com"), |
| blink::AdSize(0, blink::AdSize::LengthUnit::kPixels, 100, |
| blink::AdSize::LengthUnit::kPixels))}, |
| base::TimeDelta(), |
| }, |
| // HTTPS component URL with an invalid size unit. |
| { |
| "Bid ad component must have a valid URL and size (if specified)", |
| 1, |
| blink::AdDescriptor(GURL("https://ad1.com")), |
| std::vector<blink::AdDescriptor>{blink::AdDescriptor( |
| GURL("https://ad1.com-component1.com"), |
| blink::AdSize(100, blink::AdSize::LengthUnit::kInvalid, 100, |
| blink::AdSize::LengthUnit::kPixels))}, |
| base::TimeDelta(), |
| }, |
| // HTTPS component URL with a size specification that does not match any |
| // allowed ad descriptors. |
| { |
| "Bid ad component must have a valid URL and size (if specified)", |
| 1, |
| blink::AdDescriptor(GURL("https://ad1.com")), |
| std::vector<blink::AdDescriptor>{blink::AdDescriptor( |
| GURL("https://ad1.com-component1.com"), |
| blink::AdSize(100, blink::AdSize::LengthUnit::kPixels, 100, |
| blink::AdSize::LengthUnit::kPixels))}, |
| base::TimeDelta(), |
| }, |
| |
| // Negative time. |
| { |
| "Invalid bid duration", |
| 1, |
| blink::AdDescriptor(GURL("https://ad2.com")), |
| absl::nullopt, |
| base::Milliseconds(-1), |
| }, |
| }; |
| |
| for (const auto& test_case : kTestCases) { |
| StartStandardAuctionWithMockService(); |
| |
| auto seller_worklet = mock_auction_process_manager_->TakeSellerWorklet(); |
| ASSERT_TRUE(seller_worklet); |
| auto bidder1_worklet = |
| mock_auction_process_manager_->TakeBidderWorklet(kBidder1Url); |
| ASSERT_TRUE(bidder1_worklet); |
| auto bidder2_worklet = |
| mock_auction_process_manager_->TakeBidderWorklet(kBidder2Url); |
| ASSERT_TRUE(bidder2_worklet); |
| |
| bidder1_worklet->InvokeGenerateBidCallback( |
| test_case.bid, test_case.ad_descriptor, |
| /*mojo_kanon_bid=*/nullptr, test_case.ad_component_descriptors, |
| test_case.duration); |
| // Bidder 2 doesn't bid. |
| bidder2_worklet->InvokeGenerateBidCallback(/*bid=*/absl::nullopt); |
| |
| // Since there's no acceptable bid, the seller worklet is never asked to |
| // score a bid. |
| auction_run_loop_->Run(); |
| |
| EXPECT_EQ(test_case.expected_error_message, TakeBadMessage()); |
| |
| // No bidder won. |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| EXPECT_FALSE(result_.winning_group_id); |
| EXPECT_FALSE(result_.ad_descriptor); |
| EXPECT_TRUE(result_.ad_component_descriptors.empty()); |
| EXPECT_TRUE( |
| private_aggregation_manager_.TakePrivateAggregationRequests().empty()); |
| EXPECT_TRUE(result_.private_aggregation_event_map.empty()); |
| EXPECT_THAT(result_.interest_groups_that_bid, |
| testing::UnorderedElementsAre()); |
| CheckHistograms(InterestGroupAuction::AuctionResult::kNoBids, |
| /*expected_interest_groups=*/2, /*expected_owners=*/2, |
| /*expected_sellers=*/1); |
| } |
| } |
| |
| // Check that BidderWorklets that don't make a bid are destroyed immediately. |
| TEST_F(AuctionRunnerTest, DestroyBidderWorkletWithoutBid) { |
| StartStandardAuctionWithMockService(); |
| |
| auto seller_worklet = mock_auction_process_manager_->TakeSellerWorklet(); |
| ASSERT_TRUE(seller_worklet); |
| auto bidder1_worklet = |
| mock_auction_process_manager_->TakeBidderWorklet(kBidder1Url); |
| ASSERT_TRUE(bidder1_worklet); |
| auto bidder2_worklet = |
| mock_auction_process_manager_->TakeBidderWorklet(kBidder2Url); |
| ASSERT_TRUE(bidder2_worklet); |
| |
| bidder1_worklet->InvokeGenerateBidCallback(/*bid=*/absl::nullopt); |
| // Need to flush the service pipe to make sure the AuctionRunner has received |
| // the bid. |
| mock_auction_process_manager_->Flush(); |
| // The AuctionRunner should have closed the pipe. |
| EXPECT_TRUE(bidder1_worklet->PipeIsClosed()); |
| |
| // Bidder2 returns a bid, which is then scored. |
| bidder2_worklet->InvokeGenerateBidCallback( |
| /*bid=*/7, blink::AdDescriptor(GURL("https://ad2.com/"))); |
| auto score_ad_params = seller_worklet->WaitForScoreAd(); |
| EXPECT_EQ(kBidder2, score_ad_params.interest_group_owner); |
| EXPECT_EQ(7, score_ad_params.bid); |
| mojo::Remote<auction_worklet::mojom::ScoreAdClient>( |
| std::move(score_ad_params.score_ad_client)) |
| ->OnScoreAdComplete( |
| /*score=*/11, |
| /*reject_reason=*/ |
| auction_worklet::mojom::RejectReason::kNotAvailable, |
| auction_worklet::mojom::ComponentAuctionModifiedBidParamsPtr(), |
| /*scoring_signals_data_version=*/0, |
| /*has_scoring_signals_data_version=*/false, |
| /*debug_loss_report_url=*/absl::nullopt, |
| /*debug_win_report_url=*/absl::nullopt, /*pa_requests=*/{}, |
| /*errors=*/{}); |
| |
| // Finish the auction. |
| seller_worklet->WaitForReportResult(); |
| seller_worklet->InvokeReportResultCallback(); |
| mock_auction_process_manager_->WaitForWinningBidderReload(); |
| bidder2_worklet = |
| mock_auction_process_manager_->TakeBidderWorklet(kBidder2Url); |
| bidder2_worklet->WaitForReportWin(); |
| bidder2_worklet->InvokeReportWinCallback(); |
| auction_run_loop_->Run(); |
| |
| // Bidder2 won. |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| EXPECT_EQ(kBidder2Key, result_.winning_group_id); |
| EXPECT_EQ(GURL("https://ad2.com/"), result_.ad_descriptor->url); |
| EXPECT_TRUE(result_.ad_component_descriptors.empty()); |
| EXPECT_THAT(result_.report_urls, testing::UnorderedElementsAre()); |
| EXPECT_THAT(result_.ad_beacon_map, |
| testing::UnorderedElementsAreArray(kEmptyAdBeaconMap)); |
| EXPECT_TRUE( |
| private_aggregation_manager_.TakePrivateAggregationRequests().empty()); |
| EXPECT_TRUE(result_.private_aggregation_event_map.empty()); |
| EXPECT_THAT(result_.interest_groups_that_bid, |
| testing::UnorderedElementsAre(kBidder2Key)); |
| EXPECT_EQ(R"({"render_url":"https://ad2.com/"})", |
| result_.winning_group_ad_metadata); |
| CheckHistograms(InterestGroupAuction::AuctionResult::kSuccess, |
| /*expected_interest_groups=*/2, /*expected_owners=*/2, |
| /*expected_sellers=*/1); |
| } |
| |
| // Check that the winner of ties is randomized. Mock out bidders so can make |
| // sure that which bidder wins isn't changed just due to script execution order |
| // changing. |
| TEST_F(AuctionRunnerTest, Tie) { |
| bool seen_bidder1_win = false; |
| bool seen_bidder2_win = false; |
| |
| while (!seen_bidder1_win || !seen_bidder2_win) { |
| StartStandardAuctionWithMockService(); |
| |
| auto seller_worklet = mock_auction_process_manager_->TakeSellerWorklet(); |
| ASSERT_TRUE(seller_worklet); |
| auto bidder1_worklet = |
| mock_auction_process_manager_->TakeBidderWorklet(kBidder1Url); |
| ASSERT_TRUE(bidder1_worklet); |
| auto bidder2_worklet = |
| mock_auction_process_manager_->TakeBidderWorklet(kBidder2Url); |
| ASSERT_TRUE(bidder2_worklet); |
| |
| // Bidder1 returns a bid, which is then scored. |
| bidder1_worklet->InvokeGenerateBidCallback( |
| /*bid=*/5, blink::AdDescriptor(GURL("https://ad1.com/"))); |
| auto score_ad_params = seller_worklet->WaitForScoreAd(); |
| EXPECT_EQ(kBidder1, score_ad_params.interest_group_owner); |
| EXPECT_EQ(5, score_ad_params.bid); |
| mojo::Remote<auction_worklet::mojom::ScoreAdClient>( |
| std::move(score_ad_params.score_ad_client)) |
| ->OnScoreAdComplete( |
| /*score=*/10, |
| /*reject_reason=*/ |
| auction_worklet::mojom::RejectReason::kNotAvailable, |
| auction_worklet::mojom::ComponentAuctionModifiedBidParamsPtr(), |
| /*scoring_signals_data_version=*/0, |
| /*has_scoring_signals_data_version=*/false, |
| /*debug_loss_report_url=*/absl::nullopt, |
| /*debug_win_report_url=*/absl::nullopt, /*pa_requests=*/{}, |
| /*errors=*/{}); |
| |
| // Bidder2 returns a bid, which is then scored. |
| bidder2_worklet->InvokeGenerateBidCallback( |
| /*bid=*/5, blink::AdDescriptor(GURL("https://ad2.com/"))); |
| score_ad_params = seller_worklet->WaitForScoreAd(); |
| EXPECT_EQ(kBidder2, score_ad_params.interest_group_owner); |
| EXPECT_EQ(5, score_ad_params.bid); |
| mojo::Remote<auction_worklet::mojom::ScoreAdClient>( |
| std::move(score_ad_params.score_ad_client)) |
| ->OnScoreAdComplete( |
| /*score=*/10, |
| /*reject_reason=*/ |
| auction_worklet::mojom::RejectReason::kNotAvailable, |
| auction_worklet::mojom::ComponentAuctionModifiedBidParamsPtr(), |
| /*scoring_signals_data_version=*/0, |
| /*has_scoring_signals_data_version=*/false, |
| /*debug_loss_report_url=*/absl::nullopt, |
| /*debug_win_report_url=*/absl::nullopt, /*pa_requests=*/{}, |
| /*errors=*/{}); |
| // Need to flush the service pipe to make sure the AuctionRunner has |
| // received the score. |
| seller_worklet->Flush(); |
| |
| seller_worklet->WaitForReportResult(); |
| seller_worklet->InvokeReportResultCallback(); |
| |
| // Wait for a worklet to be reloaded, and try to get worklets for both |
| // InterestGroups - only the InterestGroup that was picked as the winner |
| // will be non-null. |
| mock_auction_process_manager_->WaitForWinningBidderReload(); |
| bidder1_worklet = |
| mock_auction_process_manager_->TakeBidderWorklet(kBidder1Url); |
| bidder2_worklet = |
| mock_auction_process_manager_->TakeBidderWorklet(kBidder2Url); |
| |
| if (bidder1_worklet) { |
| seen_bidder1_win = true; |
| bidder1_worklet->WaitForReportWin(); |
| bidder1_worklet->InvokeReportWinCallback(); |
| auction_run_loop_->Run(); |
| |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| EXPECT_EQ(kBidder1Key, result_.winning_group_id); |
| EXPECT_EQ(GURL("https://ad1.com/"), result_.ad_descriptor->url); |
| EXPECT_TRUE(result_.ad_component_descriptors.empty()); |
| EXPECT_EQ(R"({"render_url":"https://ad1.com/","metadata":{"ads": true}})", |
| result_.winning_group_ad_metadata); |
| } else { |
| seen_bidder2_win = true; |
| |
| ASSERT_TRUE(bidder2_worklet); |
| bidder2_worklet->WaitForReportWin(); |
| bidder2_worklet->InvokeReportWinCallback(); |
| auction_run_loop_->Run(); |
| |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| EXPECT_EQ(kBidder2Key, result_.winning_group_id); |
| EXPECT_EQ(GURL("https://ad2.com/"), result_.ad_descriptor->url); |
| EXPECT_TRUE(result_.ad_component_descriptors.empty()); |
| EXPECT_EQ(R"({"render_url":"https://ad2.com/"})", |
| result_.winning_group_ad_metadata); |
| } |
| |
| EXPECT_THAT(result_.report_urls, testing::UnorderedElementsAre()); |
| EXPECT_THAT(result_.ad_beacon_map, |
| testing::UnorderedElementsAreArray(kEmptyAdBeaconMap)); |
| EXPECT_TRUE( |
| private_aggregation_manager_.TakePrivateAggregationRequests().empty()); |
| EXPECT_TRUE(result_.private_aggregation_event_map.empty()); |
| EXPECT_THAT(result_.interest_groups_that_bid, |
| testing::UnorderedElementsAre(kBidder1Key, kBidder2Key)); |
| CheckHistograms(InterestGroupAuction::AuctionResult::kSuccess, |
| /*expected_interest_groups=*/2, /*expected_owners=*/2, |
| /*expected_sellers=*/1); |
| } |
| } |
| |
| // Test worklets completing in an order different from the one in which they're |
| // invoked. |
| TEST_F(AuctionRunnerTest, WorkletOrder) { |
| // Events that can ordered differently for each loop iteration. All events |
| // must happen, and a bid must be generated before it is scored. |
| enum class Event { |
| kBid1Generated, |
| kBid2Generated, |
| kBid1Scored, |
| kBid2Scored, |
| }; |
| |
| // All possible orderings. This test assumes the order bidders are loaded in |
| // is deterministic, which currently is the case (though that may change down |
| // the line). |
| const Event kTestCases[][4] = { |
| {Event::kBid1Generated, Event::kBid1Scored, Event::kBid2Generated, |
| Event::kBid2Scored}, |
| {Event::kBid1Generated, Event::kBid2Generated, Event::kBid1Scored, |
| Event::kBid2Scored}, |
| {Event::kBid1Generated, Event::kBid2Generated, Event::kBid2Scored, |
| Event::kBid1Scored}, |
| {Event::kBid2Generated, Event::kBid2Scored, Event::kBid1Generated, |
| Event::kBid1Scored}, |
| {Event::kBid2Generated, Event::kBid1Generated, Event::kBid2Scored, |
| Event::kBid1Scored}, |
| {Event::kBid2Generated, Event::kBid1Generated, Event::kBid1Scored, |
| Event::kBid2Scored}, |
| }; |
| |
| for (const auto& test_case : kTestCases) { |
| for (bool bidder1_wins : {false, true}) { |
| StartStandardAuctionWithMockService(); |
| |
| auto seller_worklet = mock_auction_process_manager_->TakeSellerWorklet(); |
| ASSERT_TRUE(seller_worklet); |
| auto bidder1_worklet = |
| mock_auction_process_manager_->TakeBidderWorklet(kBidder1Url); |
| ASSERT_TRUE(bidder1_worklet); |
| auto bidder2_worklet = |
| mock_auction_process_manager_->TakeBidderWorklet(kBidder2Url); |
| ASSERT_TRUE(bidder2_worklet); |
| |
| MockSellerWorklet::ScoreAdParams score_ad_params1; |
| MockSellerWorklet::ScoreAdParams score_ad_params2; |
| |
| for (Event event : test_case) { |
| switch (event) { |
| case Event::kBid1Generated: |
| bidder1_worklet->InvokeGenerateBidCallback( |
| /*bid=*/9, blink::AdDescriptor(GURL("https://ad1.com/"))); |
| score_ad_params1 = seller_worklet->WaitForScoreAd(); |
| EXPECT_EQ(kBidder1, score_ad_params1.interest_group_owner); |
| EXPECT_EQ(9, score_ad_params1.bid); |
| break; |
| case Event::kBid2Generated: |
| bidder2_worklet->InvokeGenerateBidCallback( |
| /*bid=*/10, blink::AdDescriptor(GURL("https://ad2.com/"))); |
| score_ad_params2 = seller_worklet->WaitForScoreAd(); |
| EXPECT_EQ(kBidder2, score_ad_params2.interest_group_owner); |
| EXPECT_EQ(10, score_ad_params2.bid); |
| break; |
| case Event::kBid1Scored: |
| mojo::Remote<auction_worklet::mojom::ScoreAdClient>( |
| std::move(score_ad_params1.score_ad_client)) |
| ->OnScoreAdComplete( |
| /*score=*/bidder1_wins ? 11 : 9, |
| /*reject_reason=*/ |
| auction_worklet::mojom::RejectReason::kNotAvailable, |
| auction_worklet::mojom:: |
| ComponentAuctionModifiedBidParamsPtr(), |
| /*scoring_signals_data_version=*/0, |
| /*has_scoring_signals_data_version=*/false, |
| /*debug_loss_report_url=*/absl::nullopt, |
| /*debug_win_report_url=*/absl::nullopt, /*pa_requests=*/{}, |
| /*errors=*/{}); |
| // Wait for the AuctionRunner to receive the score. |
| task_environment()->RunUntilIdle(); |
| break; |
| case Event::kBid2Scored: |
| mojo::Remote<auction_worklet::mojom::ScoreAdClient>( |
| std::move(score_ad_params2.score_ad_client)) |
| ->OnScoreAdComplete( |
| /*score=*/10, |
| /*reject_reason=*/ |
| auction_worklet::mojom::RejectReason::kNotAvailable, |
| auction_worklet::mojom:: |
| ComponentAuctionModifiedBidParamsPtr(), |
| /*scoring_signals_data_version=*/0, |
| /*has_scoring_signals_data_version=*/false, |
| /*debug_loss_report_url=*/absl::nullopt, |
| /*debug_win_report_url=*/absl::nullopt, |
| /*pa_requests=*/{}, |
| /*errors=*/{}); |
| // Wait for the AuctionRunner to receive the score. |
| task_environment()->RunUntilIdle(); |
| break; |
| } |
| } |
| |
| // Finish the auction. |
| seller_worklet->WaitForReportResult(); |
| seller_worklet->InvokeReportResultCallback(); |
| |
| mock_auction_process_manager_->WaitForWinningBidderReload(); |
| auto winning_worklet = mock_auction_process_manager_->TakeBidderWorklet( |
| bidder1_wins ? kBidder1Url : kBidder2Url); |
| winning_worklet->WaitForReportWin(); |
| winning_worklet->InvokeReportWinCallback(); |
| auction_run_loop_->Run(); |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| |
| if (bidder1_wins) { |
| EXPECT_EQ(kBidder1Key, result_.winning_group_id); |
| EXPECT_EQ(GURL("https://ad1.com/"), result_.ad_descriptor->url); |
| EXPECT_EQ( |
| R"({"render_url":"https://ad1.com/","metadata":{"ads": true}})", |
| result_.winning_group_ad_metadata); |
| } else { |
| EXPECT_EQ(kBidder2Key, result_.winning_group_id); |
| EXPECT_EQ(GURL("https://ad2.com/"), result_.ad_descriptor->url); |
| EXPECT_EQ(R"({"render_url":"https://ad2.com/"})", |
| result_.winning_group_ad_metadata); |
| } |
| } |
| } |
| } |
| |
| // Check that the top bid and `highestScoringOtherBid` are randomized in a 3-way |
| // tie for the highest bid. |
| TEST_F(AuctionRunnerTest, ThreeWayTie) { |
| bool seen_result[3][3] = {{false}}; |
| int total_seen_results = 0; |
| |
| const GURL kBidder3Url{"https://bidder3.test/bids.js"}; |
| const url::Origin kBidder3 = url::Origin::Create(kBidder3Url); |
| interest_group_buyers_ = {{kBidder1, kBidder2, kBidder3}}; |
| |
| while (total_seen_results < 6) { |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kBidder1Url, |
| MakeBidScriptSupportsTie()); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kBidder2Url, |
| MakeBidScriptSupportsTie()); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kBidder3Url, |
| MakeBidScriptSupportsTie()); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| MakeAuctionScriptSupportsTie()); |
| |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, /*name=*/"1", kBidder1Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad1.com"))); |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder2, /*name=*/"2", kBidder2Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad2.com"))); |
| // Use name "5" so that the IG bids "5", which is given the same score as |
| // bids of "1" and "2" (A bid of "3" is given a different score). |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder3, /*name=*/"5", kBidder3Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad3.com"))); |
| |
| RunAuctionAndWait(kSellerUrl, std::move(bidders)); |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| ASSERT_TRUE(result_.ad_descriptor); |
| |
| int winner; |
| if (result_.ad_descriptor->url.spec() == "https://ad1.com/") { |
| winner = 0; |
| } else if (result_.ad_descriptor->url.spec() == "https://ad2.com/") { |
| winner = 1; |
| } else { |
| ASSERT_EQ(result_.ad_descriptor->url.spec(), "https://ad3.com/"); |
| winner = 2; |
| } |
| |
| int highest_other_bidder; |
| ASSERT_EQ(2u, result_.report_urls.size()); |
| if (result_.report_urls[0].spec().find("highestScoringOtherBid=1") != |
| std::string::npos) { |
| highest_other_bidder = 0; |
| } else if (result_.report_urls[0].spec().find("highestScoringOtherBid=2") != |
| std::string::npos) { |
| highest_other_bidder = 1; |
| } else { |
| ASSERT_NE(std::string::npos, |
| result_.report_urls[0].spec().find("highestScoringOtherBid=5")); |
| highest_other_bidder = 2; |
| } |
| |
| ASSERT_NE(winner, highest_other_bidder); |
| if (!seen_result[winner][highest_other_bidder]) { |
| seen_result[winner][highest_other_bidder] = true; |
| ++total_seen_results; |
| } |
| } |
| } |
| |
| // Test the case where there's one IG with two groups, a size limit of 1, and |
| // the highest priority group has no bid script. The lower priority group should |
| // get a chance to bid, rather than being filtered out. |
| TEST_F(AuctionRunnerTest, SizeLimitHighestPriorityGroupHasNoBidScript) { |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder1Url, |
| MakeBidScript(kSeller, "1", "https://ad1.com/", /*num_ad_components=*/0, |
| kBidder1, kBidder1Name)); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| MakeAuctionScript()); |
| |
| std::vector<StorageInterestGroup> bidders; |
| // Low priority group with a bidding URL. |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, kBidder1Name, kBidder1Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad1.com"))); |
| bidders.back().interest_group.priority = 0; |
| |
| // High priority group without a bidding URL. |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, "other-interest-group-name", /*bidding_url=*/absl::nullopt, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad2.com"))); |
| bidders.back().interest_group.priority = 10; |
| |
| RunAuctionAndWait(kSellerUrl, std::move(bidders)); |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| EXPECT_EQ(kBidder1Key, result_.winning_group_id); |
| EXPECT_EQ(GURL("https://ad1.com/"), result_.ad_descriptor->url); |
| } |
| |
| TEST_F(AuctionRunnerTest, ExecutionModeGroupByOrigin) { |
| // Test of group-by-origin execution mode at AuctionRunner level; |
| // this primarily shows that the sorting actually groups things, and that |
| // distinct groups are kept separate. |
| const char kScript[] = R"( |
| if (!('count' in globalThis)) |
| globalThis.count = 0; |
| function generateBid() { |
| ++count; |
| return {ad: ["ad"], bid:count, render:"https://response.test/"}; |
| } |
| function reportWin(auctionSignals, perBuyerSignals, sellerSignals, |
| browserSignals) { |
| sendReportTo("https://adplatform.com/metrics/" + browserSignals.bid); |
| } |
| )"; |
| |
| const char kSellerScript[] = R"( |
| function scoreAd(adMetadata, bid, auctionConfig, trustedScoringSignals, |
| browserSignals) { |
| return {desirability: bid, |
| ad: adMetadata}; |
| } |
| function reportResult() {} |
| )"; |
| |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kBidder1Url, |
| kScript); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| kSellerScript); |
| |
| std::vector<StorageInterestGroup> bidders; |
| // Add 5 group-by-origin, 2 regular execution mode IGs. |
| for (int i = 0; i < 7; ++i) { |
| StorageInterestGroup ig = MakeInterestGroup( |
| kBidder1, kBidder1Name + base::NumberToString(i), kBidder1Url, |
| /* trusted_bidding_signals_url=*/absl::nullopt, |
| /* trusted_bidding_signals_keys=*/{}, GURL("https://response.test/")); |
| ig.joining_origin = url::Origin::Create(GURL("https://sports.example.org")); |
| ig.interest_group.execution_mode = |
| i < 5 ? blink::InterestGroup::ExecutionMode::kGroupedByOriginMode |
| : blink::InterestGroup::ExecutionMode::kCompatibilityMode; |
| bidders.push_back(std::move(ig)); |
| } |
| |
| // Add one with different join origin. |
| StorageInterestGroup ig = MakeInterestGroup( |
| kBidder1, kBidder1Name + std::string("8"), kBidder1Url, |
| /* trusted_bidding_signals_url=*/absl::nullopt, |
| /* trusted_bidding_signals_keys=*/{}, GURL("https://response.test/")); |
| ig.joining_origin = url::Origin::Create(GURL("https://shopping.example.us")); |
| ig.interest_group.execution_mode = |
| blink::InterestGroup::ExecutionMode::kGroupedByOriginMode; |
| bidders.push_back(std::move(ig)); |
| |
| StartAuction(kSellerUrl, std::move(bidders)); |
| auction_run_loop_->Run(); |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| ASSERT_TRUE(result_.winning_group_id); |
| EXPECT_THAT(result_.report_urls, |
| testing::ElementsAre(GURL("https://adplatform.com/metrics/5"))); |
| } |
| |
| // Test the case where the only bidder times out due to the |
| // perBuyerCumulativeTimeouts. |
| TEST_F(AuctionRunnerTest, PerBuyerCumulativeTimeouts) { |
| interest_group_buyers_ = {{kBidder1}}; |
| StartStandardAuctionWithMockService(/*num_expected_bidder_worklets=*/1); |
| |
| auto seller_worklet = mock_auction_process_manager_->TakeSellerWorklet(); |
| ASSERT_TRUE(seller_worklet); |
| auto bidder1_worklet = |
| mock_auction_process_manager_->TakeBidderWorklet(kBidder1Url); |
| ASSERT_TRUE(bidder1_worklet); |
| |
| task_environment()->FastForwardBy(kBidder1CumulativeTimeout - kTinyTime); |
| EXPECT_FALSE(auction_complete_); |
| task_environment()->FastForwardBy(kTinyTime); |
| EXPECT_TRUE(auction_complete_); |
| auction_run_loop_->Run(); |
| EXPECT_THAT(result_.errors, |
| testing::UnorderedElementsAre( |
| "https://adplatform.com/offers.js perBuyerCumulativeTimeout " |
| "exceeded during bid generation.")); |
| EXPECT_EQ(absl::nullopt, result_.winning_group_id); |
| } |
| |
| // Test the case where the perBuyerCumulativeTimeout expires during the |
| // scoreAd() call. The bid should not be timed out. |
| TEST_F(AuctionRunnerTest, |
| PerBuyerCumulativeTimeoutsTimeoutPassesDuringScoreAd) { |
| interest_group_buyers_ = {{kBidder1}}; |
| StartStandardAuctionWithMockService(/*num_expected_bidder_worklets=*/1); |
| |
| auto seller_worklet = mock_auction_process_manager_->TakeSellerWorklet(); |
| ASSERT_TRUE(seller_worklet); |
| auto bidder1_worklet = |
| mock_auction_process_manager_->TakeBidderWorklet(kBidder1Url); |
| ASSERT_TRUE(bidder1_worklet); |
| |
| // The timeout isn't quite hit. |
| task_environment()->FastForwardBy(kBidder1CumulativeTimeout - kTinyTime); |
| EXPECT_FALSE(auction_complete_); |
| |
| // Bid generation completes. |
| bidder1_worklet->InvokeGenerateBidCallback( |
| /*bid=*/2, blink::AdDescriptor(GURL("https://ad1.com/"))); |
| |
| // More than the timeout time passes, but since the bid is being blocked on |
| // the seller, there should be no timeout. |
| task_environment()->FastForwardBy(2 * kBidder1CumulativeTimeout); |
| EXPECT_FALSE(auction_complete_); |
| |
| // Score the ad. |
| auto score_ad_params = seller_worklet->WaitForScoreAd(); |
| EXPECT_EQ(kBidder1, score_ad_params.interest_group_owner); |
| EXPECT_EQ(2, score_ad_params.bid); |
| mojo::Remote<auction_worklet::mojom::ScoreAdClient>( |
| std::move(score_ad_params.score_ad_client)) |
| ->OnScoreAdComplete( |
| /*score=*/10, |
| /*reject_reason=*/ |
| auction_worklet::mojom::RejectReason::kNotAvailable, |
| auction_worklet::mojom::ComponentAuctionModifiedBidParamsPtr(), |
| /*scoring_signals_data_version=*/0, |
| /*has_scoring_signals_data_version=*/false, |
| /*debug_loss_report_url=*/absl::nullopt, |
| /*debug_win_report_url=*/absl::nullopt, /*pa_requests=*/{}, |
| /*errors=*/{}); |
| |
| // Finish the auction. |
| seller_worklet->WaitForReportResult(); |
| seller_worklet->InvokeReportResultCallback(); |
| mock_auction_process_manager_->WaitForWinningBidderReload(); |
| bidder1_worklet = |
| mock_auction_process_manager_->TakeBidderWorklet(kBidder1Url); |
| bidder1_worklet->WaitForReportWin(); |
| bidder1_worklet->InvokeReportWinCallback(); |
| auction_run_loop_->Run(); |
| |
| EXPECT_THAT(result_.errors, testing::UnorderedElementsAre()); |
| EXPECT_EQ(kBidder1Key, result_.winning_group_id); |
| EXPECT_EQ(GURL("https://ad1.com/"), result_.ad_descriptor->url); |
| } |
| |
| // Test the case where a pending promise delays the start of the |
| // perBuyerCumulativeTimeout, but generating a bid still times out since |
| // perBuyerCumulativeTimeout passes after promise resolution. |
| TEST_F(AuctionRunnerTest, |
| PerBuyerCumulativeTimeoutsPromiseDelaysTimeoutButStillTimesOut) { |
| use_promise_for_buyer_cumulative_timeouts_ = true; |
| interest_group_buyers_ = {{kBidder1}}; |
| StartStandardAuctionWithMockService(/*num_expected_bidder_worklets=*/1); |
| |
| auto seller_worklet = mock_auction_process_manager_->TakeSellerWorklet(); |
| ASSERT_TRUE(seller_worklet); |
| auto bidder1_worklet = |
| mock_auction_process_manager_->TakeBidderWorklet(kBidder1Url); |
| ASSERT_TRUE(bidder1_worklet); |
| |
| // The timeout duration passes, but since the seller is being waited on, too, |
| // this doesn't count towards the timeout. |
| task_environment()->FastForwardBy(2 * kBidder1CumulativeTimeout); |
| EXPECT_FALSE(auction_complete_); |
| |
| // Feed in perBuyerCumulativeTimeouts. |
| abortable_ad_auction_->ResolvedBuyerTimeoutsPromise( |
| blink::mojom::AuctionAdConfigAuctionId::NewMainAuction(0), |
| blink::mojom::AuctionAdConfigBuyerTimeoutField:: |
| kPerBuyerCumulativeTimeouts, |
| MakeBuyerCumulativeTimeouts(/*use_promise=*/false).value()); |
| |
| // The timeout passes again, but this time, it counts towards the cumulative |
| // timeout. |
| task_environment()->FastForwardBy(kBidder1CumulativeTimeout - kTinyTime); |
| EXPECT_FALSE(auction_complete_); |
| task_environment()->FastForwardBy(kTinyTime); |
| EXPECT_TRUE(auction_complete_); |
| |
| auction_run_loop_->Run(); |
| EXPECT_THAT(result_.errors, |
| testing::UnorderedElementsAre( |
| "https://adplatform.com/offers.js perBuyerCumulativeTimeout " |
| "exceeded during bid generation.")); |
| EXPECT_EQ(absl::nullopt, result_.winning_group_id); |
| } |
| |
| // Test the case where a pending promise delays the start of the |
| // perBuyerCumulativeTimeout, and a bid is ultimately generated successfully |
| // because of the delayed promise resolution. |
| TEST_F(AuctionRunnerTest, |
| PerBuyerCumulativeTimeoutsPromiseDelaysTimeoutAndNoTimeout) { |
| use_promise_for_buyer_cumulative_timeouts_ = true; |
| interest_group_buyers_ = {{kBidder1}}; |
| StartStandardAuctionWithMockService(/*num_expected_bidder_worklets=*/1); |
| |
| auto seller_worklet = mock_auction_process_manager_->TakeSellerWorklet(); |
| ASSERT_TRUE(seller_worklet); |
| auto bidder1_worklet = |
| mock_auction_process_manager_->TakeBidderWorklet(kBidder1Url); |
| ASSERT_TRUE(bidder1_worklet); |
| |
| // The timeout duration passes, but since the seller is being waited on, too, |
| // this doesn't count towards the timeout. |
| task_environment()->FastForwardBy(2 * kBidder1CumulativeTimeout); |
| EXPECT_FALSE(auction_complete_); |
| |
| // Feed in perBuyerCumulativeTimeouts. |
| abortable_ad_auction_->ResolvedBuyerTimeoutsPromise( |
| blink::mojom::AuctionAdConfigAuctionId::NewMainAuction(0), |
| blink::mojom::AuctionAdConfigBuyerTimeoutField:: |
| kPerBuyerCumulativeTimeouts, |
| MakeBuyerCumulativeTimeouts(/*use_promise=*/false).value()); |
| |
| // The timeout doesn't quite pass after the promise is resolved. |
| task_environment()->FastForwardBy(kBidder1CumulativeTimeout - kTinyTime); |
| EXPECT_FALSE(auction_complete_); |
| |
| // Bid generation completes. |
| bidder1_worklet->InvokeGenerateBidCallback( |
| /*bid=*/2, blink::AdDescriptor(GURL("https://ad1.com/"))); |
| |
| // Score the ad. |
| auto score_ad_params = seller_worklet->WaitForScoreAd(); |
| EXPECT_EQ(kBidder1, score_ad_params.interest_group_owner); |
| EXPECT_EQ(2, score_ad_params.bid); |
| mojo::Remote<auction_worklet::mojom::ScoreAdClient>( |
| std::move(score_ad_params.score_ad_client)) |
| ->OnScoreAdComplete( |
| /*score=*/10, |
| /*reject_reason=*/ |
| auction_worklet::mojom::RejectReason::kNotAvailable, |
| auction_worklet::mojom::ComponentAuctionModifiedBidParamsPtr(), |
| /*scoring_signals_data_version=*/0, |
| /*has_scoring_signals_data_version=*/false, |
| /*debug_loss_report_url=*/absl::nullopt, |
| /*debug_win_report_url=*/absl::nullopt, /*pa_requests=*/{}, |
| /*errors=*/{}); |
| |
| // Finish the auction. |
| seller_worklet->WaitForReportResult(); |
| seller_worklet->InvokeReportResultCallback(); |
| mock_auction_process_manager_->WaitForWinningBidderReload(); |
| bidder1_worklet = |
| mock_auction_process_manager_->TakeBidderWorklet(kBidder1Url); |
| bidder1_worklet->WaitForReportWin(); |
| bidder1_worklet->InvokeReportWinCallback(); |
| auction_run_loop_->Run(); |
| |
| EXPECT_THAT(result_.errors, testing::UnorderedElementsAre()); |
| EXPECT_EQ(kBidder1Key, result_.winning_group_id); |
| EXPECT_EQ(GURL("https://ad1.com/"), result_.ad_descriptor->url); |
| } |
| |
| // Test that the cumulative timeout only starts once a process is assigned. |
| TEST_F(AuctionRunnerTest, PerBuyerCumulativeTimeoutsWaitForProcess) { |
| // Create AuctionProcessManager in advance of starting the auction so can |
| // create worklets before the auction starts. |
| UseMockWorkletService(); |
| |
| // Fill up all bidding process slots. |
| std::vector<std::unique_ptr<AuctionProcessManager::ProcessHandle>> |
| busy_processes; |
| for (size_t i = 0; i < AuctionProcessManager::kMaxBidderProcesses; ++i) { |
| busy_processes.push_back( |
| std::make_unique<AuctionProcessManager::ProcessHandle>()); |
| url::Origin origin = url::Origin::Create( |
| GURL(base::StringPrintf("https://blocking.bidder.%zu.test", i))); |
| EXPECT_TRUE(auction_process_manager_->RequestWorkletService( |
| AuctionProcessManager::WorkletType::kBidder, origin, |
| scoped_refptr<SiteInstance>(), &*busy_processes.back(), |
| base::BindOnce( |
| []() { ADD_FAILURE() << "This should not be called"; }))); |
| } |
| task_environment()->RunUntilIdle(); |
| |
| // Start a 1-bidder auction. |
| interest_group_buyers_ = {{kBidder1}}; |
| StartStandardAuction(); |
| |
| // The timeout should not have started, since the bidder is still waiting on a |
| // process slot. |
| task_environment()->FastForwardBy(2 * kBidder1CumulativeTimeout); |
| EXPECT_FALSE(auction_complete_); |
| |
| // Free up a process slot. |
| busy_processes.erase(busy_processes.begin()); |
| |
| // Wait for all worklet requests. |
| mock_auction_process_manager_->WaitForWorklets(/*num_bidders=*/1, |
| /*num_sellers=*/1); |
| |
| auto seller_worklet = mock_auction_process_manager_->TakeSellerWorklet(); |
| ASSERT_TRUE(seller_worklet); |
| auto bidder1_worklet = |
| mock_auction_process_manager_->TakeBidderWorklet(kBidder1Url); |
| ASSERT_TRUE(bidder1_worklet); |
| |
| // Wait for the timeout to pass again. This time, it should result in the |
| // bidder timing out. |
| task_environment()->FastForwardBy(kBidder1CumulativeTimeout - kTinyTime); |
| EXPECT_FALSE(auction_complete_); |
| task_environment()->FastForwardBy(kTinyTime); |
| EXPECT_TRUE(auction_complete_); |
| |
| auction_run_loop_->Run(); |
| EXPECT_THAT(result_.errors, |
| testing::UnorderedElementsAre( |
| "https://adplatform.com/offers.js perBuyerCumulativeTimeout " |
| "exceeded during bid generation.")); |
| EXPECT_EQ(absl::nullopt, result_.winning_group_id); |
| } |
| |
| // Test the case where the only bidder times out due to the |
| // perBuyerCumulativeTimeout's "*" field. |
| TEST_F(AuctionRunnerTest, PerBuyerCumulativeTimeoutsAllBuyersTimeout) { |
| interest_group_buyers_ = {{kBidder2}}; |
| StartStandardAuctionWithMockService(/*num_expected_bidder_worklets=*/1); |
| |
| auto seller_worklet = mock_auction_process_manager_->TakeSellerWorklet(); |
| ASSERT_TRUE(seller_worklet); |
| auto bidder2_worklet = |
| mock_auction_process_manager_->TakeBidderWorklet(kBidder2Url); |
| ASSERT_TRUE(bidder2_worklet); |
| |
| task_environment()->FastForwardBy(kAllBuyersCumulativeTimeout - kTinyTime); |
| EXPECT_FALSE(auction_complete_); |
| task_environment()->FastForwardBy(kTinyTime); |
| EXPECT_TRUE(auction_complete_); |
| auction_run_loop_->Run(); |
| EXPECT_THAT(result_.errors, |
| testing::UnorderedElementsAre( |
| "https://anotheradthing.com/bids.js " |
| "perBuyerCumulativeTimeout exceeded during bid generation.")); |
| EXPECT_EQ(absl::nullopt, result_.winning_group_id); |
| } |
| |
| // Auction with only one interest group participating. The priority calculated |
| // using its priority vector is negative, so it should be filtered out, and |
| // there should be no winner. |
| TEST_F(AuctionRunnerTest, PriorityVectorFiltersOnlyGroup) { |
| // Only include bidder 1. Having a second bidder results in following a |
| // slightly different path. With two bidders, the first bidder loads an |
| // interest group, which is filtered, and then the bidder is deleted. Then the |
| // second bidder loads no interest groups, and the auction is deleted. With a |
| // single bidder, the auction is deleted immediately after filtering out the |
| // bidders, which potentially affects the dangling pointer detection code, |
| // since the discarded BuyerHelper must be deleted before the |
| // InterestGroupAuction it has a pointer to. |
| interest_group_buyers_ = {{kBidder1}}; |
| |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kBidder1Url, |
| MakeBidScriptSupportsTie()); |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, kBidder1Name, kBidder1Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad1.com"))); |
| // Priority should be 1 * -1 = -1. |
| bidders.back().interest_group.priority_vector = { |
| {{"browserSignals.one", -1}}}; |
| |
| RunAuctionAndWait(kSellerUrl, std::move(bidders)); |
| EXPECT_THAT(result_.errors, testing::UnorderedElementsAre()); |
| EXPECT_EQ(result_.winning_group_id, absl::nullopt); |
| EXPECT_EQ(result_.ad_descriptor, absl::nullopt); |
| |
| // No interest groups participated in the auction. |
| CheckHistograms(InterestGroupAuction::AuctionResult::kNoInterestGroups, |
| /*expected_interest_groups=*/0, |
| /*expected_owners=*/1, |
| /*expected_sellers=*/0); |
| } |
| |
| // Check that when the priority vector calculation results in a zero priority, |
| // the interest group is not filtered. |
| TEST_F(AuctionRunnerTest, PriorityVectorZeroPriorityNotFiltered) { |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kBidder1Url, |
| MakeBidScriptSupportsTie()); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder1Url, |
| MakeBidScript(kSeller, /*bid=*/"1", "https://ad1.com/", |
| /*num_ad_components=*/0, kBidder1, kBidder1Name)); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| MakeAuctionScript()); |
| |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, kBidder1Name, kBidder1Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad1.com"))); |
| // Priority should be 0. |
| bidders.back().interest_group.priority_vector = {{{"browserSignals.one", 0}}}; |
| |
| RunAuctionAndWait(kSellerUrl, std::move(bidders)); |
| EXPECT_THAT(result_.errors, testing::UnorderedElementsAre()); |
| EXPECT_EQ(kBidder1Key, result_.winning_group_id); |
| EXPECT_EQ(GURL("https://ad1.com/"), result_.ad_descriptor->url); |
| |
| // No interest groups participated in the auction. |
| CheckHistograms(InterestGroupAuction::AuctionResult::kSuccess, |
| /*expected_interest_groups=*/1, |
| /*expected_owners=*/1, |
| /*expected_sellers=*/1); |
| } |
| |
| // Check that both empty and null priority signals vectors are ignored. |
| TEST_F(AuctionRunnerTest, EmptyPriorityVector) { |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kBidder1Url, |
| MakeBidScriptSupportsTie()); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder1Url, |
| MakeBidScript(kSeller, /*bid=*/"1", "https://ad1.com/", |
| /*num_ad_components=*/0, kBidder1, kBidder1Name)); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| MakeAuctionScript()); |
| |
| for (bool use_empty_priority_signals : {false, true}) { |
| std::vector<StorageInterestGroup> bidders; |
| // A higher priority interest group that has a null / empty priority vector. |
| // The priority vector should be ignored, resulting in only this bidder |
| // participating in the auction. |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, kBidder1Name, kBidder1Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad1.com"))); |
| bidders.back().interest_group.priority = 10; |
| if (use_empty_priority_signals) |
| bidders.back().interest_group.priority_vector = {}; |
| |
| // A lower priority interest group with a priority greater than 0 (which |
| // is what multiplying an empty priority vector would result in). |
| const GURL kBidder1OtherUrl = GURL("https://adplatform.com/other_ad.js"); |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, "other-bidder-1-group", kBidder1OtherUrl, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad2.com"))); |
| bidders.back().interest_group.priority = 1; |
| |
| all_buyers_group_limit_ = 1; |
| |
| RunAuctionAndWait(kSellerUrl, std::move(bidders)); |
| EXPECT_THAT(result_.errors, testing::UnorderedElementsAre()); |
| EXPECT_EQ(kBidder1Key, result_.winning_group_id); |
| EXPECT_EQ(GURL("https://ad1.com/"), result_.ad_descriptor->url); |
| // No request should have been made for the other URL. |
| EXPECT_FALSE(url_loader_factory_.IsPending(kBidder1OtherUrl.spec())); |
| |
| // The second interest group is not counted as having participated in the |
| // auction. |
| CheckHistograms(InterestGroupAuction::AuctionResult::kSuccess, |
| /*expected_interest_groups=*/1, /*expected_owners=*/1, |
| /*expected_sellers=*/1); |
| } |
| } |
| |
| // Run an auction where there are two interest groups with the same owner, and a |
| // limit of one interest group per buyer. One group has a higher base priority, |
| // but the other group has a higher priority after the priority vector is taken |
| // into account, so should be the only bidder to participate in the auction. |
| TEST_F(AuctionRunnerTest, PriorityVector) { |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kBidder1Url, |
| MakeBidScriptSupportsTie()); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder1Url, |
| MakeBidScript(kSeller, /*bid=*/"1", "https://ad1.com/", |
| /*num_ad_components=*/0, kBidder1, kBidder1Name)); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| MakeAuctionScript()); |
| std::vector<StorageInterestGroup> bidders; |
| |
| // A low priority interest group with a priority vector that results in a high |
| // priority after multiplication. |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, kBidder1Name, kBidder1Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad1.com"))); |
| // Priority should be -1 * -10 = 10. |
| bidders.back().interest_group.priority = -1; |
| bidders.back().interest_group.priority_vector = { |
| {{"browserSignals.basePriority", -10}}}; |
| |
| // A higher priority interest group that should end up being filtered out due |
| // to having a lower (but non-negative) priority after the vector |
| // multiplication. |
| const GURL kBidder1OtherUrl = GURL("https://adplatform.com/other_ad.js"); |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, "other-bidder-1-group", kBidder1OtherUrl, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad2.com"))); |
| // Priority should be 1 * 1 = 1. |
| bidders.back().interest_group.priority = 1; |
| bidders.back().interest_group.priority_vector = { |
| {{"browserSignals.basePriority", 1}}}; |
| |
| all_buyers_group_limit_ = 1; |
| |
| RunAuctionAndWait(kSellerUrl, std::move(bidders)); |
| EXPECT_THAT(result_.errors, testing::UnorderedElementsAre()); |
| EXPECT_EQ(kBidder1Key, result_.winning_group_id); |
| EXPECT_EQ(GURL("https://ad1.com/"), result_.ad_descriptor->url); |
| // No request should have been made for the other URL. |
| EXPECT_FALSE(url_loader_factory_.IsPending(kBidder1OtherUrl.spec())); |
| |
| // The second interest group is not counted as having participated in the |
| // auction. |
| CheckHistograms(InterestGroupAuction::AuctionResult::kSuccess, |
| /*expected_interest_groups=*/1, /*expected_owners=*/1, |
| /*expected_sellers=*/1); |
| } |
| |
| // Auction with only one interest group participating. The priority calculated |
| // using the priority vector fetch in bidding signals is negative, so it should |
| // be filtered out after the bidding signals fetch, and there should be no |
| // winner. |
| TEST_F(AuctionRunnerTest, |
| TrustedBiddingSignalsPriorityVectorOnlyGroupFiltered) { |
| const GURL kFullTrustedSignalsUrl = |
| GURL(kBidder1TrustedSignalsUrl.spec() + |
| "?hostname=publisher1.com&interestGroupNames=1"); |
| |
| url_loader_factory_.ClearResponses(); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kBidder1Url, |
| MakeBidScriptSupportsTie()); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| MakeAuctionScriptSupportsTie()); |
| |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, /*name=*/"1", kBidder1Url, kBidder1TrustedSignalsUrl, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad1.com"))); |
| |
| StartAuction(kSellerUrl, std::move(bidders)); |
| task_environment()->RunUntilIdle(); |
| EXPECT_FALSE(auction_complete_); |
| ASSERT_EQ(1, url_loader_factory_.NumPending()); |
| EXPECT_EQ(kFullTrustedSignalsUrl, |
| url_loader_factory_.GetPendingRequest(0)->request.url); |
| |
| auction_worklet::AddBidderJsonResponse( |
| &url_loader_factory_, kFullTrustedSignalsUrl, |
| MakeBiddingSignalsWithPerInterestGroupData( |
| {{"1", {{{"browserSignals.one", -1}}}}})); |
| auction_run_loop_->Run(); |
| |
| EXPECT_THAT(result_.errors, testing::UnorderedElementsAre()); |
| EXPECT_FALSE(result_.winning_group_id); |
| EXPECT_FALSE(result_.ad_descriptor); |
| |
| // The interest group is considered to have participated in the auction. |
| CheckHistograms(InterestGroupAuction::AuctionResult::kNoBids, |
| /*expected_interest_groups=*/1, |
| /*expected_owners=*/1, |
| /*expected_sellers=*/1); |
| } |
| |
| // Auction with only one interest group participating. The priority calculated |
| // using the priority vector fetch in bidding signals is zero, so it should |
| // not be filtered out. |
| TEST_F(AuctionRunnerTest, |
| TrustedBiddingSignalsPriorityVectorOnlyGroupNotFiltered) { |
| const GURL kFullTrustedSignalsUrl = |
| GURL(kBidder1TrustedSignalsUrl.spec() + |
| "?hostname=publisher1.com&interestGroupNames=1"); |
| |
| url_loader_factory_.ClearResponses(); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kBidder1Url, |
| MakeBidScriptSupportsTie()); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| MakeAuctionScriptSupportsTie()); |
| |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, /*name=*/"1", kBidder1Url, kBidder1TrustedSignalsUrl, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad1.com"))); |
| |
| StartAuction(kSellerUrl, std::move(bidders)); |
| task_environment()->RunUntilIdle(); |
| EXPECT_FALSE(auction_complete_); |
| ASSERT_EQ(1, url_loader_factory_.NumPending()); |
| EXPECT_EQ(kFullTrustedSignalsUrl, |
| url_loader_factory_.GetPendingRequest(0)->request.url); |
| |
| auction_worklet::AddBidderJsonResponse( |
| &url_loader_factory_, kFullTrustedSignalsUrl, |
| MakeBiddingSignalsWithPerInterestGroupData( |
| {{"1", {{{"browserSignals.one", 0}}}}})); |
| auction_run_loop_->Run(); |
| |
| EXPECT_THAT(result_.errors, testing::UnorderedElementsAre()); |
| EXPECT_EQ(InterestGroupKey(kBidder1, "1"), result_.winning_group_id); |
| EXPECT_EQ(GURL("https://ad1.com"), result_.ad_descriptor->url); |
| |
| CheckHistograms(InterestGroupAuction::AuctionResult::kSuccess, |
| /*expected_interest_groups=*/1, |
| /*expected_owners=*/1, |
| /*expected_sellers=*/1); |
| } |
| |
| // Auction with two interest groups participating, both with the same owner. The |
| // priority calculated using the priority vector fetch in bidding signals is |
| // negative for both groups. The group limit is 1 and |
| // `enable_bidding_signals_prioritization` is set to true for one of the groups, |
| // so the auction should be set up to filter only after all priority vectors |
| // have been received, but then they eliminates both interest groups. |
| TEST_F(AuctionRunnerTest, |
| TrustedBiddingSignalsPriorityVectorBothGroupsFiltered) { |
| const GURL kFullTrustedSignalsUrl = |
| GURL(kBidder1TrustedSignalsUrl.spec() + |
| "?hostname=publisher1.com&interestGroupNames=1,2"); |
| |
| url_loader_factory_.ClearResponses(); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kBidder1Url, |
| MakeBidScriptSupportsTie()); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| MakeAuctionScriptSupportsTie()); |
| |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, /*name=*/"1", kBidder1Url, kBidder1TrustedSignalsUrl, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad1.com"))); |
| bidders[0].interest_group.enable_bidding_signals_prioritization = true; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, /*name=*/"2", kBidder1Url, kBidder1TrustedSignalsUrl, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad2.com"))); |
| |
| all_buyers_group_limit_ = 1; |
| StartAuction(kSellerUrl, std::move(bidders)); |
| task_environment()->RunUntilIdle(); |
| EXPECT_FALSE(auction_complete_); |
| ASSERT_EQ(1, url_loader_factory_.NumPending()); |
| EXPECT_EQ(kFullTrustedSignalsUrl, |
| url_loader_factory_.GetPendingRequest(0)->request.url); |
| |
| auction_worklet::AddBidderJsonResponse( |
| &url_loader_factory_, kFullTrustedSignalsUrl, |
| MakeBiddingSignalsWithPerInterestGroupData( |
| {{"1", {{{"browserSignals.one", -1}}}}, |
| {"2", {{{"browserSignals.one", -2}}}}})); |
| auction_run_loop_->Run(); |
| |
| EXPECT_THAT(result_.errors, testing::UnorderedElementsAre()); |
| EXPECT_FALSE(result_.winning_group_id); |
| EXPECT_FALSE(result_.ad_descriptor); |
| |
| CheckHistograms(InterestGroupAuction::AuctionResult::kNoBids, |
| /*expected_interest_groups=*/2, |
| /*expected_owners=*/1, |
| /*expected_sellers=*/1); |
| } |
| |
| // Auction with two interest groups participating, both with the same owner. |
| // The priority calculated using the priority vector fetch in bidding signals is |
| // negative for the first group to receive trusted signals (which is group 2). |
| // The group limit is 1 and `enable_bidding_signals_prioritization` is set to |
| // true for one of the groups, so the auction should be set up to filter only |
| // after all priority vectors have been received. |
| // |
| // The two interest groups use different trusted signals URLs, so the order the |
| // responses are received in can be controlled. |
| TEST_F(AuctionRunnerTest, |
| TrustedBiddingSignalsPriorityVectorFirstGroupFiltered) { |
| const GURL kFullTrustedSignalsUrl1 = |
| GURL(kBidder1TrustedSignalsUrl.spec() + |
| "?hostname=publisher1.com&interestGroupNames=1"); |
| const GURL kBidder1TrustedSignalsUrl2 = |
| GURL(kBidder1TrustedSignalsUrl.spec() + "2"); |
| const GURL kFullTrustedSignalsUrl2 = |
| GURL(kBidder1TrustedSignalsUrl2.spec() + |
| "?hostname=publisher1.com&interestGroupNames=2"); |
| |
| url_loader_factory_.ClearResponses(); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kBidder1Url, |
| MakeBidScriptSupportsTie()); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| MakeAuctionScriptSupportsTie()); |
| |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, /*name=*/"1", kBidder1Url, kBidder1TrustedSignalsUrl, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad1.com"))); |
| bidders[0].interest_group.enable_bidding_signals_prioritization = true; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, /*name=*/"2", kBidder1Url, kBidder1TrustedSignalsUrl2, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad2.com"))); |
| |
| all_buyers_group_limit_ = 1; |
| StartAuction(kSellerUrl, std::move(bidders)); |
| task_environment()->RunUntilIdle(); |
| EXPECT_FALSE(auction_complete_); |
| ASSERT_EQ(2, url_loader_factory_.NumPending()); |
| |
| // Group 2 has a negative priority. |
| auction_worklet::AddBidderJsonResponse( |
| &url_loader_factory_, kFullTrustedSignalsUrl2, |
| MakeBiddingSignalsWithPerInterestGroupData( |
| {{"2", {{{"browserSignals.one", -2}}}}})); |
| task_environment()->RunUntilIdle(); |
| EXPECT_FALSE(auction_complete_); |
| |
| auction_worklet::AddBidderJsonResponse( |
| &url_loader_factory_, kFullTrustedSignalsUrl1, |
| MakeBiddingSignalsWithPerInterestGroupData( |
| {{"1", {{{"browserSignals.one", 1}}}}})); |
| auction_run_loop_->Run(); |
| |
| EXPECT_THAT(result_.errors, testing::UnorderedElementsAre()); |
| EXPECT_EQ(InterestGroupKey(kBidder1, "1"), result_.winning_group_id); |
| EXPECT_EQ(GURL("https://ad1.com"), result_.ad_descriptor->url); |
| |
| CheckHistograms(InterestGroupAuction::AuctionResult::kSuccess, |
| /*expected_interest_groups=*/2, |
| /*expected_owners=*/1, |
| /*expected_sellers=*/1); |
| } |
| |
| // Auction with two interest groups participating, both with the same owner. |
| // The priority calculated using the priority vector fetch in bidding signals is |
| // negative for the second group to receive trusted signals (which is group 2). |
| // The group limit is 1 and `enable_bidding_signals_prioritization` is set to |
| // true for one of the groups, so the auction should be set up to filter only |
| // after all priority vectors have been received. |
| // |
| // The two interest groups use different trusted signals URLs, so the order the |
| // responses are received in can be controlled. |
| TEST_F(AuctionRunnerTest, |
| TrustedBiddingSignalsPriorityVectorSecondGroupFiltered) { |
| const GURL kFullTrustedSignalsUrl1 = |
| GURL(kBidder1TrustedSignalsUrl.spec() + |
| "?hostname=publisher1.com&interestGroupNames=1"); |
| const GURL kBidder1TrustedSignalsUrl2 = |
| GURL(kBidder1TrustedSignalsUrl.spec() + "2"); |
| const GURL kFullTrustedSignalsUrl2 = |
| GURL(kBidder1TrustedSignalsUrl2.spec() + |
| "?hostname=publisher1.com&interestGroupNames=2"); |
| |
| url_loader_factory_.ClearResponses(); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kBidder1Url, |
| MakeBidScriptSupportsTie()); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| MakeAuctionScriptSupportsTie()); |
| |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, /*name=*/"1", kBidder1Url, kBidder1TrustedSignalsUrl, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad1.com"))); |
| bidders[0].interest_group.enable_bidding_signals_prioritization = true; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, /*name=*/"2", kBidder1Url, kBidder1TrustedSignalsUrl2, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad2.com"))); |
| |
| all_buyers_group_limit_ = 1; |
| StartAuction(kSellerUrl, std::move(bidders)); |
| task_environment()->RunUntilIdle(); |
| EXPECT_FALSE(auction_complete_); |
| ASSERT_EQ(2, url_loader_factory_.NumPending()); |
| |
| auction_worklet::AddBidderJsonResponse( |
| &url_loader_factory_, kFullTrustedSignalsUrl1, |
| MakeBiddingSignalsWithPerInterestGroupData( |
| {{"1", {{{"browserSignals.one", 1}}}}})); |
| task_environment()->RunUntilIdle(); |
| EXPECT_FALSE(auction_complete_); |
| |
| // Group 2 has a negative priority. |
| auction_worklet::AddBidderJsonResponse( |
| &url_loader_factory_, kFullTrustedSignalsUrl2, |
| MakeBiddingSignalsWithPerInterestGroupData( |
| {{"2", {{{"browserSignals.one", -2}}}}})); |
| auction_run_loop_->Run(); |
| |
| EXPECT_THAT(result_.errors, testing::UnorderedElementsAre()); |
| EXPECT_EQ(InterestGroupKey(kBidder1, "1"), result_.winning_group_id); |
| EXPECT_EQ(GURL("https://ad1.com"), result_.ad_descriptor->url); |
| |
| CheckHistograms(InterestGroupAuction::AuctionResult::kSuccess, |
| /*expected_interest_groups=*/2, |
| /*expected_owners=*/1, |
| /*expected_sellers=*/1); |
| } |
| |
| // Auction with two interest groups participating, both with the same owner. |
| // The priority calculated using the priority vector fetch in bidding signals is |
| // negative for both groups. The group limit is 1 and |
| // `enable_bidding_signals_prioritization` is set to true for one of the groups, |
| // so the auction should be set up to filter only after all priority vectors |
| // have been received. |
| // |
| // In this test, the group with the lower priority is removed when enforcing the |
| // per-bidder size limit. The other interest group goes on to win the auction. |
| TEST_F(AuctionRunnerTest, |
| TrustedBiddingSignalsPriorityVectorSizeLimitFiltersOneGroup) { |
| const GURL kFullTrustedSignalsUrl = |
| GURL(kBidder1TrustedSignalsUrl.spec() + |
| "?hostname=publisher1.com&interestGroupNames=1,2"); |
| |
| url_loader_factory_.ClearResponses(); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kBidder1Url, |
| MakeBidScriptSupportsTie()); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| MakeAuctionScriptSupportsTie()); |
| |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, /*name=*/"1", kBidder1Url, kBidder1TrustedSignalsUrl, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad1.com"))); |
| bidders[0].interest_group.enable_bidding_signals_prioritization = true; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, /*name=*/"2", kBidder1Url, kBidder1TrustedSignalsUrl, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad2.com"))); |
| |
| all_buyers_group_limit_ = 1; |
| StartAuction(kSellerUrl, std::move(bidders)); |
| task_environment()->RunUntilIdle(); |
| EXPECT_FALSE(auction_complete_); |
| ASSERT_EQ(1, url_loader_factory_.NumPending()); |
| EXPECT_EQ(kFullTrustedSignalsUrl, |
| url_loader_factory_.GetPendingRequest(0)->request.url); |
| |
| // Group 2 has a lower, but non-negative, priority. |
| auction_worklet::AddBidderJsonResponse( |
| &url_loader_factory_, kFullTrustedSignalsUrl, |
| MakeBiddingSignalsWithPerInterestGroupData( |
| {{"1", {{{"browserSignals.one", 1}}}}, |
| {"2", {{{"browserSignals.one", 0.5}}}}})); |
| auction_run_loop_->Run(); |
| |
| EXPECT_THAT(result_.errors, testing::UnorderedElementsAre()); |
| EXPECT_EQ(InterestGroupKey(kBidder1, "1"), result_.winning_group_id); |
| EXPECT_EQ(GURL("https://ad1.com"), result_.ad_descriptor->url); |
| |
| CheckHistograms(InterestGroupAuction::AuctionResult::kSuccess, |
| /*expected_interest_groups=*/2, |
| /*expected_owners=*/1, |
| /*expected_sellers=*/1); |
| } |
| |
| // Auction with two interest groups participating, both with the same owner. |
| // The priority calculated using the priority vector fetch in bidding signals is |
| // negative for both groups. The group limit is 1 and |
| // `enable_bidding_signals_prioritization` is set to true for one of the groups, |
| // so the auction should be set up to filter only after all priority vectors |
| // have been received. |
| // |
| // In this test, neither group is filtered due to having a negative priority, |
| // however, the group that would otherwise bid higher is filtered out due to the |
| // per buyer interest group limit. |
| TEST_F(AuctionRunnerTest, TrustedBiddingSignalsPriorityVectorNoGroupFiltered) { |
| const GURL kFullTrustedSignalsUrl = |
| GURL(kBidder1TrustedSignalsUrl.spec() + |
| "?hostname=publisher1.com&interestGroupNames=1,2"); |
| |
| url_loader_factory_.ClearResponses(); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kBidder1Url, |
| MakeBidScriptSupportsTie()); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| MakeAuctionScriptSupportsTie()); |
| |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, /*name=*/"1", kBidder1Url, kBidder1TrustedSignalsUrl, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad1.com"))); |
| bidders[0].interest_group.enable_bidding_signals_prioritization = true; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, /*name=*/"2", kBidder1Url, kBidder1TrustedSignalsUrl, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad2.com"))); |
| |
| all_buyers_group_limit_ = 1; |
| StartAuction(kSellerUrl, std::move(bidders)); |
| task_environment()->RunUntilIdle(); |
| EXPECT_FALSE(auction_complete_); |
| ASSERT_EQ(1, url_loader_factory_.NumPending()); |
| EXPECT_EQ(kFullTrustedSignalsUrl, |
| url_loader_factory_.GetPendingRequest(0)->request.url); |
| |
| auction_worklet::AddBidderJsonResponse( |
| &url_loader_factory_, kFullTrustedSignalsUrl, |
| MakeBiddingSignalsWithPerInterestGroupData( |
| {{"1", {{{"browserSignals.one", 2}}}}, |
| {"2", {{{"browserSignals.one", 1}}}}})); |
| auction_run_loop_->Run(); |
| |
| EXPECT_THAT(result_.errors, testing::UnorderedElementsAre()); |
| EXPECT_EQ(InterestGroupKey(kBidder1, "1"), result_.winning_group_id); |
| EXPECT_EQ(GURL("https://ad1.com"), result_.ad_descriptor->url); |
| |
| CheckHistograms(InterestGroupAuction::AuctionResult::kSuccess, |
| /*expected_interest_groups=*/2, |
| /*expected_owners=*/1, |
| /*expected_sellers=*/1); |
| } |
| |
| // Test that `basePriority` works as expected. Interest groups have one priority |
| // order with base priorities, another with the priority vectors that are part |
| // of the interest groups, and then the priority vectors downloaded as signals |
| // echo the base priority values, which should be the order that takes effect, |
| // when one group has `enable_bidding_signals_prioritization` set to true. |
| TEST_F(AuctionRunnerTest, TrustedBiddingSignalsPriorityVectorBasePriority) { |
| const GURL kFullTrustedSignalsUrl = |
| GURL(kBidder1TrustedSignalsUrl.spec() + |
| "?hostname=publisher1.com&interestGroupNames=1,2"); |
| |
| url_loader_factory_.ClearResponses(); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kBidder1Url, |
| MakeBidScriptSupportsTie()); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| MakeAuctionScriptSupportsTie()); |
| |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, /*name=*/"1", kBidder1Url, kBidder1TrustedSignalsUrl, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad1.com"))); |
| bidders[0].interest_group.priority = 2; |
| bidders[0].interest_group.priority_vector = {{"browserSignals.one", 1}}; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, /*name=*/"2", kBidder1Url, kBidder1TrustedSignalsUrl, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad2.com"))); |
| bidders[1].interest_group.priority = 1; |
| bidders[1].interest_group.priority_vector = {{"browserSignals.one", 2}}; |
| bidders[1].interest_group.enable_bidding_signals_prioritization = true; |
| |
| auction_worklet::AddBidderJsonResponse( |
| &url_loader_factory_, kFullTrustedSignalsUrl, |
| MakeBiddingSignalsWithPerInterestGroupData( |
| {{"1", {{{"browserSignals.basePriority", 1}}}}, |
| {"2", {{{"browserSignals.basePriority", 1}}}}})); |
| |
| all_buyers_group_limit_ = 1; |
| RunAuctionAndWait(kSellerUrl, std::move(bidders)); |
| |
| EXPECT_THAT(result_.errors, testing::UnorderedElementsAre()); |
| EXPECT_EQ(InterestGroupKey(kBidder1, "1"), result_.winning_group_id); |
| EXPECT_EQ(GURL("https://ad1.com"), result_.ad_descriptor->url); |
| |
| CheckHistograms(InterestGroupAuction::AuctionResult::kSuccess, |
| /*expected_interest_groups=*/2, |
| /*expected_owners=*/1, |
| /*expected_sellers=*/1); |
| } |
| |
| // Test that `firstDotProductPriority` works as expected. Interest groups have |
| // one priority order with base priorities, another with the priority vectors |
| // that are part of the interest groups, and then the priority vectors |
| // downloaded as signals echo the values of the previous priority vector dot |
| // product, which should be the order that takes effect, when one group has |
| // `enable_bidding_signals_prioritization` set to true. |
| TEST_F(AuctionRunnerTest, |
| TrustedBiddingSignalsPriorityVectorFirstDotProductPriority) { |
| const GURL kFullTrustedSignalsUrl = |
| GURL(kBidder1TrustedSignalsUrl.spec() + |
| "?hostname=publisher1.com&interestGroupNames=1,2"); |
| |
| url_loader_factory_.ClearResponses(); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kBidder1Url, |
| MakeBidScriptSupportsTie()); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| MakeAuctionScriptSupportsTie()); |
| |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, /*name=*/"1", kBidder1Url, kBidder1TrustedSignalsUrl, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad1.com"))); |
| bidders[0].interest_group.priority = 1; |
| bidders[0].interest_group.priority_vector = {{"browserSignals.one", 2}}; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, /*name=*/"2", kBidder1Url, kBidder1TrustedSignalsUrl, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad2.com"))); |
| bidders[1].interest_group.priority = 2; |
| bidders[1].interest_group.priority_vector = {{"browserSignals.one", 1}}; |
| bidders[1].interest_group.enable_bidding_signals_prioritization = true; |
| |
| auction_worklet::AddBidderJsonResponse( |
| &url_loader_factory_, kFullTrustedSignalsUrl, |
| MakeBiddingSignalsWithPerInterestGroupData( |
| {{"1", {{{"browserSignals.firstDotProductPriority", 1}}}}, |
| {"2", {{{"browserSignals.firstDotProductPriority", 1}}}}})); |
| |
| all_buyers_group_limit_ = 1; |
| RunAuctionAndWait(kSellerUrl, std::move(bidders)); |
| |
| EXPECT_THAT(result_.errors, testing::UnorderedElementsAre()); |
| EXPECT_EQ(InterestGroupKey(kBidder1, "1"), result_.winning_group_id); |
| EXPECT_EQ(GURL("https://ad1.com"), result_.ad_descriptor->url); |
| |
| CheckHistograms(InterestGroupAuction::AuctionResult::kSuccess, |
| /*expected_interest_groups=*/2, |
| /*expected_owners=*/1, |
| /*expected_sellers=*/1); |
| } |
| |
| // Test that when no priority vector is received, the result of the first |
| // priority calculation using the interest group's priority vector is used, if |
| // available, and if not, the base priority is used. |
| TEST_F(AuctionRunnerTest, |
| TrustedBiddingSignalsPriorityVectorNotreceivedMixPrioritySources) { |
| const GURL kFullTrustedSignalsUrl = |
| GURL(kBidder1TrustedSignalsUrl.spec() + |
| "?hostname=publisher1.com&interestGroupNames=1,2"); |
| |
| url_loader_factory_.ClearResponses(); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kBidder1Url, |
| MakeBidScriptSupportsTie()); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| MakeAuctionScriptSupportsTie()); |
| |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, /*name=*/"1", kBidder1Url, kBidder1TrustedSignalsUrl, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad1.com"))); |
| bidders[0].interest_group.priority = 0; |
| bidders[0].interest_group.priority_vector = {{"browserSignals.one", 2}}; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, /*name=*/"2", kBidder1Url, kBidder1TrustedSignalsUrl, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad2.com"))); |
| bidders[1].interest_group.priority = 1; |
| bidders[1].interest_group.enable_bidding_signals_prioritization = true; |
| |
| // Empty priority vector. |
| auction_worklet::AddBidderJsonResponse(&url_loader_factory_, |
| kFullTrustedSignalsUrl, |
| /*content=*/"{}"); |
| |
| all_buyers_group_limit_ = 1; |
| RunAuctionAndWait(kSellerUrl, std::move(bidders)); |
| |
| EXPECT_THAT(result_.errors, testing::UnorderedElementsAre()); |
| EXPECT_EQ(InterestGroupKey(kBidder1, "1"), result_.winning_group_id); |
| EXPECT_EQ(GURL("https://ad1.com"), result_.ad_descriptor->url); |
| |
| CheckHistograms(InterestGroupAuction::AuctionResult::kSuccess, |
| /*expected_interest_groups=*/2, |
| /*expected_owners=*/1, |
| /*expected_sellers=*/1); |
| } |
| |
| // Auction with two interest groups participating, both with the same owner. |
| // `enable_bidding_signals_prioritization` is set to true and the size limit is |
| // one, so the worklets wait until all other worklets have received signals |
| // before proceeding. However, the worklets' Javascript fails to load before any |
| // signals are received, which should safely fail the auction. This follows the |
| // same path as if the worklet crashed, so no need to test crashing combined |
| // with `enable_bidding_signals_prioritization`. |
| TEST_F(AuctionRunnerTest, |
| TrustedBiddingSignalsPriorityVectorSharedScriptLoadErrorAfterSignals) { |
| const GURL kFullTrustedSignalsUrl = |
| GURL(kBidder1TrustedSignalsUrl.spec() + |
| "?hostname=publisher1.com&interestGroupNames=1,2"); |
| url_loader_factory_.ClearResponses(); |
| |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, /*name=*/"1", kBidder1Url, kBidder1TrustedSignalsUrl, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad1.com"))); |
| bidders[0].interest_group.enable_bidding_signals_prioritization = true; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, /*name=*/"2", kBidder1Url, kBidder1TrustedSignalsUrl, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad2.com"))); |
| |
| all_buyers_group_limit_ = 1; |
| StartAuction(kSellerUrl, std::move(bidders)); |
| task_environment()->RunUntilIdle(); |
| EXPECT_FALSE(auction_complete_); |
| // Seller script, bidder script, signals URL should all be pending. |
| EXPECT_EQ(3, url_loader_factory_.NumPending()); |
| |
| // Bidding signals received. Auction should still be pending. |
| auction_worklet::AddBidderJsonResponse( |
| &url_loader_factory_, kFullTrustedSignalsUrl, |
| MakeBiddingSignalsWithPerInterestGroupData( |
| {{"1", {{{"browserSignals.one", 1}}}}, |
| {"2", {{{"browserSignals.one", 2}}}}})); |
| task_environment()->RunUntilIdle(); |
| EXPECT_FALSE(auction_complete_); |
| // Seller script, bidder script should still be pending. |
| EXPECT_EQ(2, url_loader_factory_.NumPending()); |
| |
| // Script loads fail. The auction should safely fail. |
| url_loader_factory_.AddResponse(kBidder1Url.spec(), "", net::HTTP_NOT_FOUND); |
| auction_run_loop_->Run(); |
| |
| // Only get an error for one interest group - the other was filtered out due |
| // to having a lower priority. |
| EXPECT_THAT( |
| result_.errors, |
| testing::UnorderedElementsAre( |
| "Failed to load https://adplatform.com/offers.js HTTP status =" |
| " 404 Not Found.")); |
| EXPECT_EQ(absl::nullopt, result_.winning_group_id); |
| EXPECT_EQ(absl::nullopt, result_.ad_descriptor); |
| |
| CheckHistograms(InterestGroupAuction::AuctionResult::kNoBids, |
| /*expected_interest_groups=*/2, |
| /*expected_owners=*/1, |
| /*expected_sellers=*/1); |
| } |
| |
| // Auction with two interest groups participating, both with the same owner. |
| // `enable_bidding_signals_prioritization` is set to true and the size limit is |
| // one, so the worklets wait until all other worklets have received signals |
| // before proceeding. However, the worklet's Javascript fails to load after |
| // signals are received, which should safely fail the auction. This follows the |
| // same path as if the worklet crashed, so no need to test crashing combined |
| // with `enable_bidding_signals_prioritization`. |
| TEST_F(AuctionRunnerTest, |
| TrustedBiddingSignalsPriorityVectorSharedScriptLoadErrorBeforeSignals) { |
| url_loader_factory_.ClearResponses(); |
| |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, /*name=*/"1", kBidder1Url, kBidder1TrustedSignalsUrl, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad1.com"))); |
| bidders[0].interest_group.enable_bidding_signals_prioritization = true; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, /*name=*/"2", kBidder1Url, kBidder1TrustedSignalsUrl, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad2.com"))); |
| |
| all_buyers_group_limit_ = 1; |
| StartAuction(kSellerUrl, std::move(bidders)); |
| task_environment()->RunUntilIdle(); |
| EXPECT_FALSE(auction_complete_); |
| // Seller script, bidder script, signals URL should all be pending. |
| EXPECT_EQ(3, url_loader_factory_.NumPending()); |
| |
| // Script loads fail. The auction should safely fail. |
| url_loader_factory_.AddResponse(kBidder1Url.spec(), "", net::HTTP_NOT_FOUND); |
| auction_run_loop_->Run(); |
| |
| EXPECT_THAT( |
| result_.errors, |
| testing::UnorderedElementsAre( |
| "Failed to load https://adplatform.com/offers.js HTTP status =" |
| " 404 Not Found.", |
| "Failed to load https://adplatform.com/offers.js HTTP status =" |
| " 404 Not Found.")); |
| EXPECT_EQ(absl::nullopt, result_.winning_group_id); |
| EXPECT_EQ(absl::nullopt, result_.ad_descriptor); |
| |
| CheckHistograms(InterestGroupAuction::AuctionResult::kNoBids, |
| /*expected_interest_groups=*/2, |
| /*expected_owners=*/1, |
| /*expected_sellers=*/1); |
| } |
| |
| TEST_F(AuctionRunnerTest, SetPrioritySignalsOverride) { |
| const char kBidderScript[] = R"( |
| function generateBid() { |
| setPrioritySignalsOverride("key", 3); |
| return {bid:1, render:"https://ad1.com/"}; |
| } |
| function reportWin() {} |
| )"; |
| |
| const char kSellerScript[] = R"( |
| function scoreAd() { |
| return {desirability: 1}; |
| } |
| function reportResult() {} |
| )"; |
| |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kBidder1Url, |
| kBidderScript); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| kSellerScript); |
| |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, kBidder1Name, kBidder1Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad1.com"))); |
| RunAuctionAndWait(kSellerUrl, std::move(bidders)); |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| ASSERT_TRUE(result_.winning_group_id); |
| EXPECT_EQ(kBidder1Key, result_.winning_group_id); |
| EXPECT_EQ(GURL("https://ad1.com/"), result_.ad_descriptor->url); |
| |
| auto storage_interest_group = GetInterestGroup(kBidder1, kBidder1Name); |
| ASSERT_TRUE(storage_interest_group); |
| EXPECT_EQ((base::flat_map<std::string, double>{{"key", 3}}), |
| storage_interest_group->interest_group.priority_signals_overrides); |
| } |
| |
| // If there's no valid bid, setPrioritySignalsOverride() should still be |
| // respected. |
| TEST_F(AuctionRunnerTest, SetPrioritySignalsOverrideNoBid) { |
| const char kBidderScript[] = R"( |
| function generateBid() { |
| setPrioritySignalsOverride("key", 3); |
| return {bid:0, render:"https://ad1.com/"}; |
| } |
| function reportWin() {} |
| )"; |
| |
| const char kSellerScript[] = R"( |
| function scoreAd() { |
| return {desirability: 1}; |
| } |
| function reportResult() {} |
| )"; |
| |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kBidder1Url, |
| kBidderScript); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| kSellerScript); |
| |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, kBidder1Name, kBidder1Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad1.com"))); |
| RunAuctionAndWait(kSellerUrl, std::move(bidders)); |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| EXPECT_FALSE(result_.winning_group_id); |
| EXPECT_FALSE(result_.ad_descriptor); |
| |
| auto storage_interest_group = GetInterestGroup(kBidder1, kBidder1Name); |
| ASSERT_TRUE(storage_interest_group); |
| EXPECT_EQ((base::flat_map<std::string, double>{{"key", 3}}), |
| storage_interest_group->interest_group.priority_signals_overrides); |
| } |
| |
| TEST_F(AuctionRunnerTest, Abort) { |
| // Not adding kBidder1Url to block things in predictable spot. |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kSellerUrl, |
| MakeAuctionScript(/*report_post_auction_signals=*/true)); |
| |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, kBidder1Name, kBidder1Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad1.com"), |
| /*ad_component_urls=*/absl::nullopt)); |
| |
| StartAuction(kSellerUrl, std::move(bidders)); |
| abortable_ad_auction_->Abort(); |
| auction_run_loop_->Run(); |
| EXPECT_TRUE(result_.manually_aborted); |
| EXPECT_FALSE(result_.winning_group_id.has_value()); |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| EXPECT_THAT(result_.interest_groups_that_bid, |
| testing::UnorderedElementsAre()); |
| } |
| |
| // Testing what happens when Abort() is called after auction is done. |
| TEST_F(AuctionRunnerTest, AbortLate) { |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder1Url, |
| MakeBidScript(kSeller, "1", "https://ad1.com/", /*num_ad_components=*/0, |
| kBidder1, kBidder1Name)); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kSellerUrl, |
| MakeAuctionScript(/*report_post_auction_signals=*/true)); |
| |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, kBidder1Name, kBidder1Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad1.com"), |
| /*ad_component_urls=*/absl::nullopt)); |
| |
| // Want AuctionRunner still around to make sure that it handles Abort() OK |
| // in that timing. |
| dont_reset_auction_runner_ = true; |
| RunAuctionAndWait(kSellerUrl, std::move(bidders)); |
| EXPECT_EQ(kBidder1Name, result_.winning_group_id->name); |
| EXPECT_FALSE(result_.manually_aborted); |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| abortable_ad_auction_->Abort(); |
| task_environment()->RunUntilIdle(); |
| auction_runner_.reset(); |
| } |
| |
| // An auction with two successful bids. sendHistogramReport() and |
| // reportContributionForEvent() are both called in all of generateBid(), |
| // scoreAd(), reportWin() and reportResult(). |
| TEST_F(AuctionRunnerTest, PrivateAggregationRequestForEventContributionEvents) { |
| const char kBidScript[] = R"( |
| const bid = %d; |
| function generateBid( |
| interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals, |
| browserSignals) { |
| privateAggregation.sendHistogramReport({bucket: 1n, value: 2}); |
| privateAggregation.reportContributionForEvent( |
| 'reserved.always', {bucket: 10n, value: 20}); |
| privateAggregation.reportContributionForEvent( |
| 'reserved.win', {bucket: 11n, value: 21}); |
| privateAggregation.reportContributionForEvent( |
| 'reserved.loss', {bucket: 12n, value: 22}); |
| privateAggregation.reportContributionForEvent( |
| 'arbitrary', {bucket: 100n, value: 200}); |
| privateAggregation.reportContributionForEvent( |
| 'click', {bucket: 101n, value: 201}); |
| return {bid: bid, render: interestGroup.ads[0].renderUrl}; |
| } |
| |
| function reportWin( |
| auctionSignals, perBuyerSignals, sellerSignals, browserSignals) { |
| privateAggregation.sendHistogramReport({bucket: 3n, value: 4}); |
| privateAggregation.reportContributionForEvent( |
| 'reserved.always', {bucket: 30n, value: 40}); |
| privateAggregation.reportContributionForEvent( |
| 'reserved.win', {bucket: 31n, value: 41}); |
| privateAggregation.reportContributionForEvent( |
| 'reserved.loss', {bucket: 32n, value: 42}); |
| privateAggregation.reportContributionForEvent( |
| 'arbitrary', {bucket: 300n, value: 400}); |
| privateAggregation.reportContributionForEvent( |
| 'click', {bucket: 301n, value: 401}); |
| } |
| )"; |
| |
| const std::string kSellerScript = R"( |
| function scoreAd(adMetadata, bid, auctionConfig, browserSignals) { |
| privateAggregation.sendHistogramReport({bucket: 5n, value: 6}); |
| privateAggregation.reportContributionForEvent( |
| 'reserved.always', {bucket: 50n, value: 60}); |
| privateAggregation.reportContributionForEvent( |
| 'reserved.win', {bucket: 51n, value: 61}); |
| privateAggregation.reportContributionForEvent( |
| 'reserved.loss', {bucket: 52n, value: 62}); |
| privateAggregation.reportContributionForEvent( |
| 'arbitrary', {bucket: 500n, value: 600}); |
| privateAggregation.reportContributionForEvent( |
| 'click', {bucket: 501n, value: 601}); |
| return {desirability: 2 * bid, allowComponentAuction: true}; |
| } |
| |
| function reportResult(auctionConfig, browserSignals) { |
| privateAggregation.sendHistogramReport({bucket: 7n, value: 8}); |
| privateAggregation.reportContributionForEvent( |
| 'reserved.always', {bucket: 70n, value: 80}); |
| privateAggregation.reportContributionForEvent( |
| 'reserved.win', {bucket: 71n, value: 81}); |
| privateAggregation.reportContributionForEvent( |
| 'reserved.loss', {bucket: 72n, value: 82}); |
| privateAggregation.reportContributionForEvent( |
| 'arbitrary', {bucket: 700n, value: 800}); |
| privateAggregation.reportContributionForEvent( |
| 'click', {bucket: 701n, value: 801}); |
| } |
| )"; |
| |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kBidder1Url, |
| base::StringPrintf(kBidScript, 1)); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kBidder2Url, |
| base::StringPrintf(kBidScript, 2)); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| kSellerScript); |
| |
| // Bidder 2 won the auction. |
| RunStandardAuction(/*request_trusted_bidding_signals=*/false); |
| EXPECT_THAT(result_.errors, testing::UnorderedElementsAre()); |
| EXPECT_FALSE(result_.manually_aborted); |
| EXPECT_EQ(kBidder2Key, result_.winning_group_id); |
| EXPECT_EQ(GURL("https://ad2.com/"), result_.ad_descriptor->url); |
| |
| EXPECT_THAT( |
| private_aggregation_manager_.TakePrivateAggregationRequests(), |
| testing::UnorderedElementsAre( |
| testing::Pair( |
| kBidder1, |
| ElementsAreRequests( |
| kExpectedGenerateBidPrivateAggregationRequest, |
| // generateBid() "reserved.always". |
| BuildPrivateAggregationRequest(/*bucket=*/10, /*value=*/20), |
| // generateBid() "reserved.loss". |
| BuildPrivateAggregationRequest(/*bucket=*/12, /*value=*/22))), |
| testing::Pair( |
| kBidder2, |
| ElementsAreRequests( |
| kExpectedGenerateBidPrivateAggregationRequest, |
| BuildPrivateAggregationRequest(/*bucket=*/10, /*value=*/20), |
| // generateBid() "reserved.win". |
| BuildPrivateAggregationRequest(/*bucket=*/11, /*value=*/21), |
| kExpectedReportWinPrivateAggregationRequest, |
| // reportWin() "reserved.always". |
| BuildPrivateAggregationRequest(/*bucket=*/30, /*value=*/40), |
| // reportWin() "reserved.win". |
| BuildPrivateAggregationRequest(/*bucket=*/31, /*value=*/41))), |
| testing::Pair( |
| kSeller, |
| ElementsAreRequests( |
| kExpectedScoreAdPrivateAggregationRequest, |
| // scoreAd() "reserved.always". |
| BuildPrivateAggregationRequest(/*bucket=*/50, /*value=*/60), |
| // scoreAd() "reserved.loss". |
| BuildPrivateAggregationRequest(/*bucket=*/52, /*value=*/62), |
| kExpectedScoreAdPrivateAggregationRequest, |
| BuildPrivateAggregationRequest(/*bucket=*/50, /*value=*/60), |
| // scoreAd() "reserved.win". |
| BuildPrivateAggregationRequest(/*bucket=*/51, /*value=*/61), |
| kExpectedReportResultPrivateAggregationRequest, |
| // reportResult() "reserved.always". |
| BuildPrivateAggregationRequest(/*bucket=*/70, /*value=*/80), |
| // reportResult() "reserved.win". |
| BuildPrivateAggregationRequest(/*bucket=*/71, |
| /*value=*/81))))); |
| EXPECT_THAT( |
| result_.private_aggregation_event_map, |
| testing::UnorderedElementsAre( |
| testing::Pair("arbitrary", ElementsAreRequests( |
| BuildPrivateAggregationRequest( |
| /*bucket=*/100, /*value=*/200), |
| BuildPrivateAggregationRequest( |
| /*bucket=*/300, /*value=*/400))), |
| testing::Pair("click", ElementsAreRequests( |
| BuildPrivateAggregationRequest( |
| /*bucket=*/101, /*value=*/201), |
| BuildPrivateAggregationRequest( |
| /*bucket=*/301, /*value=*/401))))); |
| } |
| |
| // Base values in contribution's bucket. |
| TEST_F(AuctionRunnerTest, |
| PrivateAggregationRequestForEventContributionBucketBaseValue) { |
| const char kBidScript[] = R"( |
| const bid = %d; |
| function generateBid( |
| interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals, |
| browserSignals) { |
| privateAggregation.reportContributionForEvent('reserved.always', { |
| bucket: {baseValue: 'winning-bid', scale: 1.0, offset: 0n}, |
| value: 1 + 100 * bid, |
| }); |
| privateAggregation.reportContributionForEvent('reserved.always', { |
| bucket: {baseValue: 'highest-scoring-other-bid', scale: 1.0}, |
| value: 2 + 100 * bid, |
| }); |
| privateAggregation.reportContributionForEvent('reserved.always', { |
| bucket: {baseValue: 'bid-reject-reason', offset: 0n}, |
| value: 3 + 100 * bid, |
| }); |
| return {bid: bid, render: interestGroup.ads[0].renderUrl}; |
| } |
| |
| function reportWin( |
| auctionSignals, perBuyerSignals, sellerSignals, browserSignals) { |
| privateAggregation.reportContributionForEvent('reserved.win', { |
| bucket: {baseValue: 'winning-bid'}, |
| value: 11 + 100 * browserSignals.bid, |
| }); |
| privateAggregation.reportContributionForEvent('reserved.win', { |
| bucket: {baseValue: 'highest-scoring-other-bid', scale: 1.0, |
| offset: 0n}, |
| value: 12 + 100 * browserSignals.bid, |
| }); |
| privateAggregation.reportContributionForEvent('reserved.always', { |
| bucket: {baseValue: 'bid-reject-reason'}, |
| value: 13 + 100 * browserSignals.bid, |
| }); |
| } |
| )"; |
| |
| const std::string kSellerScript = R"( |
| function scoreAd(adMetadata, bid, auctionConfig, browserSignals) { |
| privateAggregation.reportContributionForEvent('reserved.always', { |
| bucket: {baseValue: 'winning-bid'}, |
| value: 21 + 100 * bid, |
| }); |
| privateAggregation.reportContributionForEvent('reserved.always', { |
| bucket: {baseValue: 'highest-scoring-other-bid'}, |
| value: 22 + 100 * bid, |
| }); |
| privateAggregation.reportContributionForEvent('reserved.always', { |
| bucket: {baseValue: 'bid-reject-reason'}, |
| value: 23 + 100 * bid, |
| }); |
| if (bid === 2) return {desirability: -1, rejectReason: 'invalid-bid'}; |
| return bid; |
| } |
| |
| function reportResult(auctionConfig, browserSignals) { |
| privateAggregation.reportContributionForEvent('reserved.win', { |
| bucket: {baseValue: 'winning-bid'}, |
| value: 31 + 100 * browserSignals.bid, |
| }); |
| privateAggregation.reportContributionForEvent('reserved.win', { |
| bucket: {baseValue: 'highest-scoring-other-bid'}, |
| value: 32 + 100 * browserSignals.bid, |
| }); |
| privateAggregation.reportContributionForEvent('reserved.always', { |
| bucket: {baseValue: 'bid-reject-reason'}, |
| value: 33 + 100 * browserSignals.bid, |
| }); |
| } |
| )"; |
| |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kBidder1Url, |
| base::StringPrintf(kBidScript, 1)); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kBidder2Url, |
| base::StringPrintf(kBidScript, 2)); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| kSellerScript); |
| |
| // kBidder2 was rejected by seller, so kBidder1 won the auction. |
| RunStandardAuction(/*request_trusted_bidding_signals=*/false); |
| EXPECT_THAT(result_.errors, testing::UnorderedElementsAre()); |
| EXPECT_FALSE(result_.manually_aborted); |
| EXPECT_EQ(kBidder1Key, result_.winning_group_id); |
| EXPECT_EQ(GURL("https://ad1.com/"), result_.ad_descriptor->url); |
| |
| // Post auction signals of this auction: |
| // winning-bid is 1, highest-scoring-other-bid is 0, bid-reject-reason for |
| // kBidder1 is kNotAvailable (0), and bid-reject-reason for kBidder2 is |
| // kInvalidBid (1). |
| EXPECT_THAT( |
| private_aggregation_manager_.TakePrivateAggregationRequests(), |
| testing::UnorderedElementsAre( |
| testing::Pair( |
| kBidder1, |
| ElementsAreRequests( |
| // generateBid(). |
| BuildPrivateAggregationRequest(/*bucket=*/1, /*value=*/101), |
| BuildPrivateAggregationRequest(/*bucket=*/0, /*value=*/102), |
| BuildPrivateAggregationRequest(/*bucket=*/0, /*value=*/103), |
| // reportWin(). No request for 'bid-reject-reason' whose value |
| // is 113, because it's not a supported base value in |
| // reportWin(). |
| BuildPrivateAggregationRequest(/*bucket=*/1, /*value=*/111), |
| BuildPrivateAggregationRequest(/*bucket=*/0, /*value=*/112))), |
| testing::Pair( |
| kBidder2, |
| ElementsAreRequests( |
| // generateBid(). |
| BuildPrivateAggregationRequest(/*bucket=*/1, /*value=*/201), |
| BuildPrivateAggregationRequest(/*bucket=*/0, /*value=*/202), |
| BuildPrivateAggregationRequest(/*bucket=*/1, /*value=*/203))), |
| testing::Pair( |
| kSeller, |
| ElementsAreRequests( |
| // scoreAd() for kBidder1. |
| BuildPrivateAggregationRequest(/*bucket=*/1, /*value=*/121), |
| BuildPrivateAggregationRequest(/*bucket=*/0, /*value=*/122), |
| BuildPrivateAggregationRequest(/*bucket=*/0, /*value=*/123), |
| // scoreAd() for kBidder2. |
| BuildPrivateAggregationRequest(/*bucket=*/1, /*value=*/221), |
| BuildPrivateAggregationRequest(/*bucket=*/0, /*value=*/222), |
| BuildPrivateAggregationRequest(/*bucket=*/1, /*value=*/223), |
| // reportResult() for kBidder1. No request for |
| // 'bid-reject-reason' whose value is 133, because it's not a |
| // supported base value in reportResult(). |
| BuildPrivateAggregationRequest(/*bucket=*/1, /*value=*/131), |
| BuildPrivateAggregationRequest(/*bucket=*/0, |
| /*value=*/132))))); |
| EXPECT_TRUE(result_.private_aggregation_event_map.empty()); |
| } |
| |
| // Similar to `PrivateAggregationRequestForEventContributionBucketBaseValue()` |
| // above, but no bid is rejected. |
| TEST_F(AuctionRunnerTest, |
| PrivateAggregationRequestForEventContributionTwoBidsNotRejected) { |
| const char kBidScript[] = R"( |
| const bid = %d; |
| function generateBid( |
| interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals, |
| browserSignals) { |
| privateAggregation.reportContributionForEvent('reserved.always', { |
| bucket: {baseValue: 'winning-bid', scale: 1.0, offset: 0n}, |
| value: 1 + 100 * bid, |
| }); |
| privateAggregation.reportContributionForEvent('reserved.always', { |
| bucket: {baseValue: 'highest-scoring-other-bid', scale: 1.0}, |
| value: 2 + 100 * bid, |
| }); |
| privateAggregation.reportContributionForEvent('reserved.always', { |
| bucket: {baseValue: 'bid-reject-reason', offset: 0n}, |
| value: 3 + 100 * bid, |
| }); |
| return {bid: bid, render: interestGroup.ads[0].renderUrl}; |
| } |
| |
| function reportWin( |
| auctionSignals, perBuyerSignals, sellerSignals, browserSignals) { |
| privateAggregation.reportContributionForEvent('reserved.win', { |
| bucket: {baseValue: 'winning-bid'}, |
| value: 11 + 100 * browserSignals.bid, |
| }); |
| privateAggregation.reportContributionForEvent('reserved.win', { |
| bucket: {baseValue: 'highest-scoring-other-bid', scale: 1.0, |
| offset: 0n}, |
| value: 12 + 100 * browserSignals.bid, |
| }); |
| privateAggregation.reportContributionForEvent('reserved.always', { |
| bucket: {baseValue: 'bid-reject-reason'}, |
| value: 13 + 100 * browserSignals.bid, |
| }); |
| } |
| )"; |
| |
| const std::string kSellerScript = R"( |
| function scoreAd(adMetadata, bid, auctionConfig, browserSignals) { |
| privateAggregation.reportContributionForEvent('reserved.always', { |
| bucket: {baseValue: 'winning-bid'}, |
| value: 21 + 100 * bid, |
| }); |
| privateAggregation.reportContributionForEvent('reserved.always', { |
| bucket: {baseValue: 'highest-scoring-other-bid'}, |
| value: 22 + 100 * bid, |
| }); |
| privateAggregation.reportContributionForEvent('reserved.always', { |
| bucket: {baseValue: 'bid-reject-reason'}, |
| value: 23 + 100 * bid, |
| }); |
| return bid; |
| } |
| |
| function reportResult(auctionConfig, browserSignals) { |
| privateAggregation.reportContributionForEvent('reserved.win', { |
| bucket: {baseValue: 'winning-bid'}, |
| value: 31 + 100 * browserSignals.bid, |
| }); |
| privateAggregation.reportContributionForEvent('reserved.win', { |
| bucket: {baseValue: 'highest-scoring-other-bid'}, |
| value: 32 + 100 * browserSignals.bid, |
| }); |
| privateAggregation.reportContributionForEvent('reserved.always', { |
| bucket: {baseValue: 'bid-reject-reason'}, |
| value: 33 + 100 * browserSignals.bid, |
| }); |
| } |
| )"; |
| |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kBidder1Url, |
| base::StringPrintf(kBidScript, 1)); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kBidder2Url, |
| base::StringPrintf(kBidScript, 2)); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| kSellerScript); |
| |
| // kBidder2 won the auction. |
| RunStandardAuction(/*request_trusted_bidding_signals=*/false); |
| EXPECT_THAT(result_.errors, testing::UnorderedElementsAre()); |
| EXPECT_FALSE(result_.manually_aborted); |
| EXPECT_EQ(kBidder2Key, result_.winning_group_id); |
| EXPECT_EQ(GURL("https://ad2.com/"), result_.ad_descriptor->url); |
| |
| // Post auction signals of this auction: |
| // winning-bid is 2, highest-scoring-other-bid is 1, bid-reject-reason for |
| // both bidders are kNotAvailable (0). |
| EXPECT_THAT( |
| private_aggregation_manager_.TakePrivateAggregationRequests(), |
| testing::UnorderedElementsAre( |
| testing::Pair( |
| kBidder1, |
| ElementsAreRequests( |
| // generateBid(). |
| BuildPrivateAggregationRequest(/*bucket=*/2, /*value=*/101), |
| BuildPrivateAggregationRequest(/*bucket=*/1, /*value=*/102), |
| BuildPrivateAggregationRequest(/*bucket=*/0, /*value=*/103))), |
| testing::Pair( |
| kBidder2, |
| ElementsAreRequests( |
| // generateBid(). |
| BuildPrivateAggregationRequest( |
| /*bucket=*/2, /*value=*/201), |
| BuildPrivateAggregationRequest( |
| /*bucket=*/1, /*value=*/202), |
| BuildPrivateAggregationRequest( |
| /*bucket=*/0, /*value=*/203), |
| // reportWin(). No request for 'bid-reject-reason' whose value |
| // is 213, because it's not a supported base value in |
| // reportWin(). |
| BuildPrivateAggregationRequest(/*bucket=*/2, /*value=*/211), |
| BuildPrivateAggregationRequest(/*bucket=*/1, /*value=*/212))), |
| testing::Pair( |
| kSeller, |
| ElementsAreRequests( |
| // scoreAd() for kBidder1. |
| BuildPrivateAggregationRequest(/*bucket=*/2, /*value=*/121), |
| BuildPrivateAggregationRequest(/*bucket=*/1, /*value=*/122), |
| BuildPrivateAggregationRequest(/*bucket=*/0, /*value=*/123), |
| // scoreAd() for kBidder2. |
| BuildPrivateAggregationRequest(/*bucket=*/2, /*value=*/221), |
| BuildPrivateAggregationRequest(/*bucket=*/1, /*value=*/222), |
| BuildPrivateAggregationRequest(/*bucket=*/0, /*value=*/223), |
| // reportResult() for kBidder2. No request for |
| // 'bid-reject-reason' whose value is 233, because it's not a |
| // supported base value in reportResult(). |
| BuildPrivateAggregationRequest(/*bucket=*/2, /*value=*/231), |
| BuildPrivateAggregationRequest(/*bucket=*/1, |
| /*value=*/232))))); |
| } |
| |
| // Similar to PrivateAggregationRequestForEventContributionBucketBaseValue, |
| // but with contribution's value field as object. |
| TEST_F(AuctionRunnerTest, |
| PrivateAggregationRequestForEventContributionValueBaseValue) { |
| const char kBidScript[] = R"( |
| const bid = %d; |
| function generateBid( |
| interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals, |
| browserSignals) { |
| privateAggregation.reportContributionForEvent('reserved.always', { |
| bucket: BigInt(1 + 100 * bid), |
| value: {baseValue: 'winning-bid', scale: 1.0, offset: 0}, |
| }); |
| privateAggregation.reportContributionForEvent('reserved.always', { |
| bucket: BigInt(2 + 100 * bid), |
| value: {baseValue: 'highest-scoring-other-bid', scale: 1.0}, |
| }); |
| privateAggregation.reportContributionForEvent('reserved.always', { |
| bucket: BigInt(3 + 100 * bid), |
| value: {baseValue: 'bid-reject-reason', offset: 0}, |
| }); |
| return {bid: bid, render: interestGroup.ads[0].renderUrl}; |
| } |
| |
| function reportWin( |
| auctionSignals, perBuyerSignals, sellerSignals, browserSignals) { |
| privateAggregation.reportContributionForEvent('reserved.win', { |
| bucket: BigInt(11 + 100 * browserSignals.bid), |
| value: {baseValue: 'winning-bid'}, |
| }); |
| privateAggregation.reportContributionForEvent('reserved.win', { |
| bucket: BigInt(12 + 100 * browserSignals.bid), |
| value: {baseValue: 'highest-scoring-other-bid'}, |
| }); |
| privateAggregation.reportContributionForEvent('reserved.always', { |
| bucket: BigInt(13 + 100 * browserSignals.bid), |
| value: {baseValue: 'bid-reject-reason'}, |
| }); |
| } |
| )"; |
| |
| const std::string kSellerScript = R"( |
| function scoreAd(adMetadata, bid, auctionConfig, browserSignals) { |
| privateAggregation.reportContributionForEvent('reserved.always', { |
| bucket: BigInt(21 + 100 * bid), |
| value: {baseValue: 'winning-bid'}, |
| }); |
| privateAggregation.reportContributionForEvent('reserved.always', { |
| bucket: BigInt(22 + 100 * bid), |
| value: {baseValue: 'highest-scoring-other-bid'}, |
| }); |
| privateAggregation.reportContributionForEvent('reserved.always', { |
| bucket: BigInt(23 + 100 * bid), |
| value: {baseValue: 'bid-reject-reason'}, |
| }); |
| if (bid === 2) return {desirability: -1, rejectReason: 'invalid-bid'}; |
| return bid; |
| } |
| |
| function reportResult(auctionConfig, browserSignals) { |
| privateAggregation.reportContributionForEvent('reserved.win', { |
| bucket: BigInt(31 + 100 * browserSignals.bid), |
| value: {baseValue: 'winning-bid'}, |
| }); |
| privateAggregation.reportContributionForEvent('reserved.win', { |
| bucket: BigInt(32 + 100 * browserSignals.bid), |
| value: {baseValue: 'highest-scoring-other-bid'}, |
| }); |
| privateAggregation.reportContributionForEvent('reserved.always', { |
| bucket: BigInt(33 + 100 * browserSignals.bid), |
| value: {baseValue: 'bid-reject-reason'}, |
| }); |
| } |
| )"; |
| |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kBidder1Url, |
| base::StringPrintf(kBidScript, 1)); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kBidder2Url, |
| base::StringPrintf(kBidScript, 2)); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| kSellerScript); |
| |
| // kBidder2 was rejected by seller, so kBidder1 won the auction. |
| RunStandardAuction(/*request_trusted_bidding_signals=*/false); |
| EXPECT_THAT(result_.errors, testing::UnorderedElementsAre()); |
| EXPECT_FALSE(result_.manually_aborted); |
| EXPECT_EQ(kBidder1Key, result_.winning_group_id); |
| EXPECT_EQ(GURL("https://ad1.com/"), result_.ad_descriptor->url); |
| |
| // Post auction signals of this auction: |
| // winning-bid is 1, highest-scoring-other-bid is 0, bid-reject-reason for |
| // kBidder1 is kNotAvailable (0), and bid-reject-reason for kBidder2 is |
| // kInvalidBid (1). |
| EXPECT_THAT( |
| private_aggregation_manager_.TakePrivateAggregationRequests(), |
| testing::UnorderedElementsAre( |
| testing::Pair( |
| kBidder1, |
| ElementsAreRequests( |
| // generateBid(). |
| BuildPrivateAggregationRequest(/*bucket=*/101, /*value=*/1), |
| BuildPrivateAggregationRequest(/*bucket=*/102, /*value=*/0), |
| BuildPrivateAggregationRequest(/*bucket=*/103, /*value=*/0), |
| // reportWin(). |
| BuildPrivateAggregationRequest(/*bucket=*/111, /*value=*/1), |
| BuildPrivateAggregationRequest(/*bucket=*/112, /*value=*/0))), |
| testing::Pair( |
| kBidder2, |
| ElementsAreRequests( |
| // generateBid(). |
| BuildPrivateAggregationRequest(/*bucket=*/201, /*value=*/1), |
| BuildPrivateAggregationRequest(/*bucket=*/202, /*value=*/0), |
| BuildPrivateAggregationRequest(/*bucket=*/203, /*value=*/1))), |
| testing::Pair( |
| kSeller, |
| ElementsAreRequests( |
| // scoreAd() for kBidder1. |
| BuildPrivateAggregationRequest(/*bucket=*/121, /*value=*/1), |
| BuildPrivateAggregationRequest(/*bucket=*/122, /*value=*/0), |
| BuildPrivateAggregationRequest(/*bucket=*/123, /*value=*/0), |
| // scoreAd() for kBidder2. |
| BuildPrivateAggregationRequest(/*bucket=*/221, /*value=*/1), |
| BuildPrivateAggregationRequest(/*bucket=*/222, /*value=*/0), |
| BuildPrivateAggregationRequest(/*bucket=*/223, /*value=*/1), |
| // reportResult() for kBidder1. |
| BuildPrivateAggregationRequest(/*bucket=*/131, /*value=*/1), |
| BuildPrivateAggregationRequest(/*bucket=*/132, |
| /*value=*/0))))); |
| } |
| |
| TEST_F(AuctionRunnerTest, |
| PrivateAggregationRequestForEventContributionScaleAndOffset) { |
| // Only one bidder participating the auction, to keep things simple. |
| interest_group_buyers_ = {{kBidder1}}; |
| |
| const char kBidScript[] = R"( |
| const bid = %d; |
| function reportContributionForEvent() { |
| privateAggregation.reportContributionForEvent('reserved.always', { |
| bucket: {baseValue: 'winning-bid', scale: 2.1, offset: 10n}, |
| value: {baseValue: 'winning-bid', scale: 2.1, offset: 20}, |
| }); |
| privateAggregation.reportContributionForEvent('reserved.always', { |
| bucket: {baseValue: 'winning-bid', scale: 0, offset: 10n}, |
| value: {baseValue: 'winning-bid', scale: 0, offset: 20}, |
| }); |
| privateAggregation.reportContributionForEvent('reserved.always', { |
| bucket: {baseValue: 'winning-bid', scale: -1, offset: 10n}, |
| value: {baseValue: 'winning-bid', scale: -1, offset: 20}, |
| }); |
| // Bucket overflows due to being negative, so will be clamped to 0. |
| privateAggregation.reportContributionForEvent('reserved.always', { |
| bucket: {baseValue: 'winning-bid', scale: -200, offset: 10n}, |
| value: 1, |
| }); |
| } |
| |
| function generateBid( |
| interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals, |
| browserSignals) { |
| reportContributionForEvent(); |
| return {bid: bid, render: interestGroup.ads[0].renderUrl}; |
| } |
| |
| function reportWin( |
| auctionSignals, perBuyerSignals, sellerSignals, browserSignals) { |
| reportContributionForEvent(); |
| } |
| )"; |
| |
| const std::string kSellerScript = R"( |
| function reportContributionForEvent() { |
| privateAggregation.reportContributionForEvent('reserved.always', { |
| bucket: {baseValue: 'winning-bid', scale: 2.1, offset: 10n}, |
| value: {baseValue: 'winning-bid', scale: 2.1, offset: 20}, |
| }); |
| privateAggregation.reportContributionForEvent('reserved.always', { |
| bucket: {baseValue: 'winning-bid', scale: 0, offset: 10n}, |
| value: {baseValue: 'winning-bid', scale: 0, offset: 20}, |
| }); |
| privateAggregation.reportContributionForEvent('reserved.always', { |
| bucket: {baseValue: 'winning-bid', scale: -1, offset: 10n}, |
| value: {baseValue: 'winning-bid', scale: -1, offset: 20}, |
| }); |
| // Bucket overflows due to being negative, so will be clamped to 0. |
| privateAggregation.reportContributionForEvent('reserved.always', { |
| bucket: {baseValue: 'winning-bid', scale: -200, offset: 10n}, |
| value: 1, |
| }); |
| } |
| |
| function scoreAd(adMetadata, bid, auctionConfig, browserSignals) { |
| reportContributionForEvent(); |
| return bid; |
| } |
| |
| function reportResult(auctionConfig, browserSignals) { |
| reportContributionForEvent(); |
| } |
| )"; |
| |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kBidder1Url, |
| base::StringPrintf(kBidScript, 1)); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| kSellerScript); |
| |
| RunStandardAuction(/*request_trusted_bidding_signals=*/false); |
| EXPECT_THAT(result_.errors, testing::UnorderedElementsAre()); |
| EXPECT_FALSE(result_.manually_aborted); |
| EXPECT_EQ(kBidder1Key, result_.winning_group_id); |
| EXPECT_EQ(GURL("https://ad1.com/"), result_.ad_descriptor->url); |
| |
| // winning-bid is 1. |
| EXPECT_THAT( |
| private_aggregation_manager_.TakePrivateAggregationRequests(), |
| testing::UnorderedElementsAre( |
| testing::Pair( |
| kBidder1, |
| ElementsAreRequests( |
| // generateBid(). |
| BuildPrivateAggregationRequest(/*bucket=*/12, /*value=*/22), |
| BuildPrivateAggregationRequest(/*bucket=*/10, /*value=*/20), |
| BuildPrivateAggregationRequest(/*bucket=*/9, /*value=*/19), |
| BuildPrivateAggregationRequest(/*bucket=*/0, /*value=*/1), |
| // reportWin(). |
| BuildPrivateAggregationRequest(/*bucket=*/12, /*value=*/22), |
| BuildPrivateAggregationRequest(/*bucket=*/10, /*value=*/20), |
| BuildPrivateAggregationRequest(/*bucket=*/9, /*value=*/19), |
| BuildPrivateAggregationRequest(/*bucket=*/0, /*value=*/1))), |
| |
| testing::Pair( |
| kSeller, |
| ElementsAreRequests( |
| // scoreAd(). |
| BuildPrivateAggregationRequest(/*bucket=*/12, /*value=*/22), |
| BuildPrivateAggregationRequest(/*bucket=*/10, /*value=*/20), |
| BuildPrivateAggregationRequest(/*bucket=*/9, /*value=*/19), |
| BuildPrivateAggregationRequest(/*bucket=*/0, /*value=*/1), |
| // reportResult(). |
| BuildPrivateAggregationRequest(/*bucket=*/12, /*value=*/22), |
| BuildPrivateAggregationRequest(/*bucket=*/10, /*value=*/20), |
| BuildPrivateAggregationRequest(/*bucket=*/9, /*value=*/19), |
| BuildPrivateAggregationRequest(/*bucket=*/0, /*value=*/1))))); |
| } |
| |
| TEST_F(AuctionRunnerTest, |
| PrivateAggregationReportGenerateBidInvalidReservedEventType) { |
| StartStandardAuctionWithMockService(); |
| |
| auto seller_worklet = mock_auction_process_manager_->TakeSellerWorklet(); |
| ASSERT_TRUE(seller_worklet); |
| auto bidder1_worklet = |
| mock_auction_process_manager_->TakeBidderWorklet(kBidder1Url); |
| ASSERT_TRUE(bidder1_worklet); |
| auto bidder2_worklet = |
| mock_auction_process_manager_->TakeBidderWorklet(kBidder2Url); |
| ASSERT_TRUE(bidder2_worklet); |
| |
| // Only Bidder1 bids, to keep things simple. |
| PrivateAggregationRequests bidder_1_pa_requests; |
| bidder_1_pa_requests.push_back( |
| BuildPrivateAggregationForEventRequest( |
| /*bucket=*/10, /*value=*/20, /*event_type=*/"reserved.always") |
| .Clone()); |
| bidder_1_pa_requests.push_back( |
| BuildPrivateAggregationForEventRequest( |
| /*bucket=*/11, /*value=*/21, /*event_type=*/"reserved.not-supported") |
| .Clone()); |
| |
| // Bidder1 returns a bid with a private aggregation request whose reserved |
| // event type is not a supported one. This could only happen when the bidder |
| // worklet is compromised. |
| bidder1_worklet->InvokeGenerateBidCallback( |
| /*bid=*/5, blink::AdDescriptor(GURL("https://ad1.com")), |
| /*mojo_kanon_bid=*/nullptr, |
| /*ad_component_descriptors=*/absl::nullopt, |
| /*duration=*/base::TimeDelta(), |
| /*bidding_signals_data_version=*/absl::nullopt, |
| /*debug_loss_report_url=*/absl::nullopt, |
| /*debug_win_report_url=*/absl::nullopt, |
| /*pa_requests=*/ |
| std::move(bidder_1_pa_requests)); |
| bidder2_worklet->InvokeGenerateBidCallback(/*bid=*/absl::nullopt); |
| |
| auto score_ad_params = seller_worklet->WaitForScoreAd(); |
| EXPECT_EQ(kBidder1, score_ad_params.interest_group_owner); |
| EXPECT_EQ(5, score_ad_params.bid); |
| PrivateAggregationRequests score_ad_1_pa_requests; |
| score_ad_1_pa_requests.push_back( |
| kExpectedScoreAdPrivateAggregationRequest.Clone()); |
| mojo::Remote<auction_worklet::mojom::ScoreAdClient>( |
| std::move(score_ad_params.score_ad_client)) |
| ->OnScoreAdComplete( |
| /*score=*/10, |
| /*reject_reason=*/ |
| auction_worklet::mojom::RejectReason::kNotAvailable, |
| auction_worklet::mojom::ComponentAuctionModifiedBidParamsPtr(), |
| /*scoring_signals_data_version=*/0, |
| /*has_scoring_signals_data_version=*/false, |
| /*debug_loss_report_url=*/absl::nullopt, |
| /*debug_win_report_url=*/absl::nullopt, |
| std::move(score_ad_1_pa_requests), |
| /*errors=*/{}); |
| |
| PrivateAggregationRequests report_win_pa_requests; |
| report_win_pa_requests.push_back( |
| kExpectedReportWinPrivateAggregationRequest.Clone()); |
| PrivateAggregationRequests report_result_pa_requests; |
| report_result_pa_requests.push_back( |
| kExpectedReportResultPrivateAggregationRequest.Clone()); |
| |
| seller_worklet->WaitForReportResult(); |
| seller_worklet->InvokeReportResultCallback( |
| /*report_url=*/absl::nullopt, |
| /*ad_beacon_map=*/{}, std::move(report_result_pa_requests)); |
| mock_auction_process_manager_->WaitForWinningBidderReload(); |
| bidder1_worklet = |
| mock_auction_process_manager_->TakeBidderWorklet(kBidder1Url); |
| bidder1_worklet->WaitForReportWin(); |
| bidder1_worklet->InvokeReportWinCallback( |
| /*report_url=*/absl::nullopt, |
| /*ad_beacon_map=*/{}, /*pa_requests=*/std::move(report_win_pa_requests)); |
| auction_run_loop_->Run(); |
| |
| EXPECT_EQ(kBidder1Key, result_.winning_group_id); |
| EXPECT_EQ(GURL("https://ad1.com/"), result_.ad_descriptor->url); |
| EXPECT_THAT(result_.interest_groups_that_bid, |
| testing::UnorderedElementsAre(kBidder1Key)); |
| EXPECT_EQ(R"({"render_url":"https://ad1.com/","metadata":{"ads": true}})", |
| result_.winning_group_ad_metadata); |
| |
| EXPECT_THAT( |
| private_aggregation_manager_.TakePrivateAggregationRequests(), |
| testing::UnorderedElementsAre( |
| testing::Pair( |
| kBidder1, |
| ElementsAreRequests( |
| BuildPrivateAggregationRequest(/*bucket=*/10, /*value=*/20), |
| kExpectedReportWinPrivateAggregationRequest)), |
| testing::Pair(kSeller, |
| ElementsAreRequests( |
| kExpectedScoreAdPrivateAggregationRequest, |
| kExpectedReportResultPrivateAggregationRequest)))); |
| } |
| |
| TEST_F(AuctionRunnerTest, |
| PrivateAggregationBuyersReportTrustedSignalsFetchLatency) { |
| constexpr base::TimeDelta kTrustedSignalsFetchLatency = base::Milliseconds(2); |
| constexpr absl::uint128 kBaseBucket = 100l; |
| constexpr absl::uint128 kOffset = 0l; |
| constexpr double kScale = 1.0; |
| |
| auction_report_buyer_keys_ = {{kBaseBucket}}; |
| auction_report_buyers_ = {{{blink::AuctionConfig::NonSharedParams:: |
| BuyerReportType::kTotalSignalsFetchLatency, |
| {/*bucket=*/kOffset, |
| /*scale=*/kScale}}}}; |
| |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| blink::TestInterestGroupBuilder(kBidder1, kBidder1Name) |
| .SetBiddingUrl(kBidder1Url) |
| .SetTrustedBiddingSignalsUrl(kBidder1TrustedSignalsUrl) |
| .SetTrustedBiddingSignalsKeys({{"k1", "k2"}}) |
| .SetAds({{blink::InterestGroup::Ad(GURL("https://ad1.com"), |
| absl::nullopt)}}) |
| .SetSellerCapabilities({{{url::Origin::Create(kSellerUrl), |
| blink::SellerCapabilities::kLatencyStats}}}) |
| .Build())); |
| |
| RunExtendedPABuyersAuction(bidders, {kTrustedSignalsFetchLatency}); |
| |
| EXPECT_THAT(private_aggregation_manager_.TakePrivateAggregationRequests(), |
| testing::UnorderedElementsAre(testing::Pair( |
| url::Origin::Create(kSellerUrl), |
| ElementsAreRequests(BuildPrivateAggregationRequest( |
| /*bucket=*/kBaseBucket + kOffset, |
| /*value=*/kTrustedSignalsFetchLatency.InMilliseconds() * |
| kScale))))); |
| } |
| |
| TEST_F(AuctionRunnerTest, PrivateAggregationBuyersReportBiddingDuration) { |
| constexpr base::TimeDelta kBiddingDuration = base::Milliseconds(2); |
| constexpr absl::uint128 kBaseBucket = 100l; |
| constexpr absl::uint128 kOffset = 0l; |
| constexpr double kScale = 1.0; |
| |
| auction_report_buyer_keys_ = {{kBaseBucket}}; |
| auction_report_buyers_ = {{{blink::AuctionConfig::NonSharedParams:: |
| BuyerReportType::kTotalGenerateBidLatency, |
| {/*bucket=*/kOffset, |
| /*scale=*/kScale}}}}; |
| |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| blink::TestInterestGroupBuilder(kBidder1, kBidder1Name) |
| .SetBiddingUrl(kBidder1Url) |
| .SetTrustedBiddingSignalsUrl(kBidder1TrustedSignalsUrl) |
| .SetTrustedBiddingSignalsKeys({{"k1", "k2"}}) |
| .SetAds({{blink::InterestGroup::Ad(GURL("https://ad1.com"), |
| absl::nullopt)}}) |
| .SetSellerCapabilities({{{url::Origin::Create(kSellerUrl), |
| blink::SellerCapabilities::kLatencyStats}}}) |
| .Build())); |
| |
| RunExtendedPABuyersAuction(bidders, |
| /*trusted_fetch_latency=*/{base::TimeDelta()}, |
| /*bidding_latency=*/{kBiddingDuration}); |
| |
| EXPECT_THAT(private_aggregation_manager_.TakePrivateAggregationRequests(), |
| testing::UnorderedElementsAre(testing::Pair( |
| url::Origin::Create(kSellerUrl), |
| ElementsAreRequests(BuildPrivateAggregationRequest( |
| /*bucket=*/kBaseBucket + kOffset, |
| /*value=*/kBiddingDuration.InMilliseconds() * kScale))))); |
| } |
| |
| TEST_F(AuctionRunnerTest, |
| PrivateAggregationBuyersReportAllSellersCapabilities) { |
| constexpr base::TimeDelta kTrustedSignalsFetchLatency = base::Milliseconds(2); |
| constexpr absl::uint128 kBaseBucket = 100l; |
| constexpr absl::uint128 kOffset = 0l; |
| constexpr double kScale = 1.0; |
| |
| auction_report_buyer_keys_ = {{kBaseBucket}}; |
| auction_report_buyers_ = {{{blink::AuctionConfig::NonSharedParams:: |
| BuyerReportType::kTotalSignalsFetchLatency, |
| {/*bucket=*/kOffset, |
| /*scale=*/kScale}}}}; |
| |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| blink::TestInterestGroupBuilder(kBidder1, kBidder1Name) |
| .SetBiddingUrl(kBidder1Url) |
| .SetTrustedBiddingSignalsUrl(kBidder1TrustedSignalsUrl) |
| .SetTrustedBiddingSignalsKeys({{"k1", "k2"}}) |
| .SetAds({{blink::InterestGroup::Ad(GURL("https://ad1.com"), |
| absl::nullopt)}}) |
| .SetAllSellerCapabilities(blink::SellerCapabilities::kLatencyStats) |
| .Build())); |
| |
| RunExtendedPABuyersAuction(bidders, {kTrustedSignalsFetchLatency}); |
| |
| EXPECT_THAT(private_aggregation_manager_.TakePrivateAggregationRequests(), |
| testing::UnorderedElementsAre(testing::Pair( |
| url::Origin::Create(kSellerUrl), |
| ElementsAreRequests(BuildPrivateAggregationRequest( |
| /*bucket=*/kBaseBucket + kOffset, |
| /*value=*/kTrustedSignalsFetchLatency.InMilliseconds() * |
| kScale))))); |
| } |
| |
| TEST_F(AuctionRunnerTest, |
| PrivateAggregationBuyersReportDifferentDurationScaleAndOffset) { |
| // Use a non-whole number of milliseconds to check truncation. |
| constexpr base::TimeDelta kTrustedSignalsFetchLatency = |
| base::Microseconds(2500); |
| constexpr absl::uint128 kBaseBucket = 100l; |
| constexpr absl::uint128 kOffset = 2l; |
| constexpr double kScale = 0.5; |
| |
| auction_report_buyer_keys_ = {{kBaseBucket}}; |
| auction_report_buyers_ = {{{blink::AuctionConfig::NonSharedParams:: |
| BuyerReportType::kTotalSignalsFetchLatency, |
| {/*bucket=*/kOffset, |
| /*scale=*/kScale}}}}; |
| |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| blink::TestInterestGroupBuilder(kBidder1, kBidder1Name) |
| .SetBiddingUrl(kBidder1Url) |
| .SetTrustedBiddingSignalsUrl(kBidder1TrustedSignalsUrl) |
| .SetTrustedBiddingSignalsKeys({{"k1", "k2"}}) |
| .SetAds({{blink::InterestGroup::Ad(GURL("https://ad1.com"), |
| absl::nullopt)}}) |
| .SetSellerCapabilities({{{url::Origin::Create(kSellerUrl), |
| blink::SellerCapabilities::kLatencyStats}}}) |
| .Build())); |
| |
| RunExtendedPABuyersAuction(bidders, {kTrustedSignalsFetchLatency}); |
| |
| EXPECT_THAT(private_aggregation_manager_.TakePrivateAggregationRequests(), |
| testing::UnorderedElementsAre(testing::Pair( |
| url::Origin::Create(kSellerUrl), |
| ElementsAreRequests(BuildPrivateAggregationRequest( |
| /*bucket=*/kBaseBucket + kOffset, |
| // 2500 microseconds = 2.5 milliseconds, |
| // 2.5 * 0.5 = 1.25 |
| // 1.25 gets truncated to 1, since an integer is required |
| /*value=*/1))))); |
| } |
| |
| TEST_F(AuctionRunnerTest, PrivateAggregationBuyersReportInfiniteScale) { |
| constexpr base::TimeDelta kTrustedSignalsFetchLatency = base::Milliseconds(2); |
| constexpr absl::uint128 kBaseBucket = 100l; |
| constexpr absl::uint128 kOffset = 2l; |
| constexpr double kScale = std::numeric_limits<double>::infinity(); |
| |
| auction_report_buyer_keys_ = {{kBaseBucket}}; |
| auction_report_buyers_ = {{{blink::AuctionConfig::NonSharedParams:: |
| BuyerReportType::kTotalSignalsFetchLatency, |
| {/*bucket=*/kOffset, |
| /*scale=*/kScale}}}}; |
| |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| blink::TestInterestGroupBuilder(kBidder1, kBidder1Name) |
| .SetBiddingUrl(kBidder1Url) |
| .SetTrustedBiddingSignalsUrl(kBidder1TrustedSignalsUrl) |
| .SetTrustedBiddingSignalsKeys({{"k1", "k2"}}) |
| .SetAds({{blink::InterestGroup::Ad(GURL("https://ad1.com"), |
| absl::nullopt)}}) |
| .SetSellerCapabilities({{{url::Origin::Create(kSellerUrl), |
| blink::SellerCapabilities::kLatencyStats}}}) |
| .Build())); |
| |
| RunExtendedPABuyersAuction(bidders, {kTrustedSignalsFetchLatency}); |
| |
| EXPECT_THAT(private_aggregation_manager_.TakePrivateAggregationRequests(), |
| testing::UnorderedElementsAre(testing::Pair( |
| url::Origin::Create(kSellerUrl), |
| ElementsAreRequests(BuildPrivateAggregationRequest( |
| /*bucket=*/kBaseBucket + kOffset, |
| /*value=*/std::numeric_limits<int32_t>::max()))))); |
| } |
| |
| TEST_F(AuctionRunnerTest, PrivateAggregationBuyersReportNaNScale) { |
| constexpr base::TimeDelta kTrustedSignalsFetchLatency = base::Milliseconds(2); |
| constexpr absl::uint128 kBaseBucket = 100l; |
| constexpr absl::uint128 kOffset = 2l; |
| constexpr double kScale = std::numeric_limits<double>::quiet_NaN(); |
| |
| auction_report_buyer_keys_ = {{kBaseBucket}}; |
| auction_report_buyers_ = {{{blink::AuctionConfig::NonSharedParams:: |
| BuyerReportType::kTotalSignalsFetchLatency, |
| {/*bucket=*/kOffset, |
| /*scale=*/kScale}}}}; |
| |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| blink::TestInterestGroupBuilder(kBidder1, kBidder1Name) |
| .SetBiddingUrl(kBidder1Url) |
| .SetTrustedBiddingSignalsUrl(kBidder1TrustedSignalsUrl) |
| .SetTrustedBiddingSignalsKeys({{"k1", "k2"}}) |
| .SetAds({{blink::InterestGroup::Ad(GURL("https://ad1.com"), |
| absl::nullopt)}}) |
| .SetSellerCapabilities({{{url::Origin::Create(kSellerUrl), |
| blink::SellerCapabilities::kLatencyStats}}}) |
| .Build())); |
| |
| RunExtendedPABuyersAuction(bidders, {kTrustedSignalsFetchLatency}); |
| |
| EXPECT_THAT(private_aggregation_manager_.TakePrivateAggregationRequests(), |
| testing::UnorderedElementsAre(testing::Pair( |
| url::Origin::Create(kSellerUrl), |
| ElementsAreRequests(BuildPrivateAggregationRequest( |
| /*bucket=*/kBaseBucket + kOffset, |
| /*value=*/0))))); |
| } |
| |
| TEST_F(AuctionRunnerTest, PrivateAggregationBuyersReportNegativeScale) { |
| constexpr base::TimeDelta kTrustedSignalsFetchLatency = base::Milliseconds(2); |
| constexpr absl::uint128 kBaseBucket = 100l; |
| constexpr absl::uint128 kOffset = 2l; |
| constexpr double kScale = -1.0; |
| |
| auction_report_buyer_keys_ = {{kBaseBucket}}; |
| auction_report_buyers_ = {{{blink::AuctionConfig::NonSharedParams:: |
| BuyerReportType::kTotalSignalsFetchLatency, |
| {/*bucket=*/kOffset, |
| /*scale=*/kScale}}}}; |
| |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| blink::TestInterestGroupBuilder(kBidder1, kBidder1Name) |
| .SetBiddingUrl(kBidder1Url) |
| .SetTrustedBiddingSignalsUrl(kBidder1TrustedSignalsUrl) |
| .SetTrustedBiddingSignalsKeys({{"k1", "k2"}}) |
| .SetAds({{blink::InterestGroup::Ad(GURL("https://ad1.com"), |
| absl::nullopt)}}) |
| .SetSellerCapabilities({{{url::Origin::Create(kSellerUrl), |
| blink::SellerCapabilities::kLatencyStats}}}) |
| .Build())); |
| |
| RunExtendedPABuyersAuction(bidders, {kTrustedSignalsFetchLatency}); |
| |
| EXPECT_THAT(private_aggregation_manager_.TakePrivateAggregationRequests(), |
| testing::UnorderedElementsAre(testing::Pair( |
| url::Origin::Create(kSellerUrl), |
| ElementsAreRequests(BuildPrivateAggregationRequest( |
| /*bucket=*/kBaseBucket + kOffset, |
| /*value=*/0))))); |
| } |
| |
| TEST_F(AuctionRunnerTest, |
| PrivateAggregationBuyersReportBucketOverflowDoesntCrash) { |
| constexpr base::TimeDelta kTrustedSignalsFetchLatency = base::Milliseconds(2); |
| constexpr absl::uint128 kBaseBucket = absl::Uint128Max(); |
| constexpr absl::uint128 kOffset = 1l; |
| constexpr double kScale = 1.0; |
| |
| auction_report_buyer_keys_ = {{kBaseBucket}}; |
| auction_report_buyers_ = {{{blink::AuctionConfig::NonSharedParams:: |
| BuyerReportType::kTotalSignalsFetchLatency, |
| {/*bucket=*/kOffset, |
| /*scale=*/kScale}}}}; |
| |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| blink::TestInterestGroupBuilder(kBidder1, kBidder1Name) |
| .SetBiddingUrl(kBidder1Url) |
| .SetTrustedBiddingSignalsUrl(kBidder1TrustedSignalsUrl) |
| .SetTrustedBiddingSignalsKeys({{"k1", "k2"}}) |
| .SetAds({{blink::InterestGroup::Ad(GURL("https://ad1.com"), |
| absl::nullopt)}}) |
| .SetSellerCapabilities({{{url::Origin::Create(kSellerUrl), |
| blink::SellerCapabilities::kLatencyStats}}}) |
| .Build())); |
| |
| RunExtendedPABuyersAuction(bidders, {kTrustedSignalsFetchLatency}); |
| |
| // The actual report doesn't matter for this test -- we just want to ensure |
| // that no crash occurs. |
| } |
| |
| TEST_F(AuctionRunnerTest, PrivateAggregationBuyersNotAuthorized) { |
| constexpr base::TimeDelta kTrustedSignalsFetchLatency = base::Milliseconds(2); |
| constexpr absl::uint128 kBaseBucket = 100l; |
| constexpr absl::uint128 kOffset = 0l; |
| constexpr double kScale = 1.0; |
| |
| auction_report_buyer_keys_ = {{kBaseBucket}}; |
| auction_report_buyers_ = {{{blink::AuctionConfig::NonSharedParams:: |
| BuyerReportType::kTotalSignalsFetchLatency, |
| {/*bucket=*/kOffset, |
| /*scale=*/kScale}}}}; |
| |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| blink::TestInterestGroupBuilder(kBidder1, kBidder1Name) |
| .SetBiddingUrl(kBidder1Url) |
| .SetTrustedBiddingSignalsUrl(kBidder1TrustedSignalsUrl) |
| .SetTrustedBiddingSignalsKeys({{"k1", "k2"}}) |
| .SetAds({{blink::InterestGroup::Ad(GURL("https://ad1.com"), |
| absl::nullopt)}}) |
| .Build())); |
| |
| RunExtendedPABuyersAuction(bidders, {kTrustedSignalsFetchLatency}); |
| |
| EXPECT_THAT(private_aggregation_manager_.TakePrivateAggregationRequests(), |
| testing::UnorderedElementsAre()); |
| } |
| |
| TEST_F(AuctionRunnerTest, PrivateAggregationBuyersNoReportBuyerKeys) { |
| constexpr base::TimeDelta kTrustedSignalsFetchLatency = base::Milliseconds(2); |
| constexpr absl::uint128 kOffset = 0l; |
| constexpr double kScale = 1.0; |
| |
| auction_report_buyer_keys_ = absl::nullopt; |
| auction_report_buyers_ = {{{blink::AuctionConfig::NonSharedParams:: |
| BuyerReportType::kTotalSignalsFetchLatency, |
| {/*bucket=*/kOffset, |
| /*scale=*/kScale}}}}; |
| |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| blink::TestInterestGroupBuilder(kBidder1, kBidder1Name) |
| .SetBiddingUrl(kBidder1Url) |
| .SetTrustedBiddingSignalsUrl(kBidder1TrustedSignalsUrl) |
| .SetTrustedBiddingSignalsKeys({{"k1", "k2"}}) |
| .SetAds({{blink::InterestGroup::Ad(GURL("https://ad1.com"), |
| absl::nullopt)}}) |
| .SetSellerCapabilities({{{url::Origin::Create(kSellerUrl), |
| blink::SellerCapabilities::kLatencyStats}}}) |
| .Build())); |
| |
| RunExtendedPABuyersAuction(bidders, {kTrustedSignalsFetchLatency}); |
| |
| EXPECT_THAT(private_aggregation_manager_.TakePrivateAggregationRequests(), |
| testing::UnorderedElementsAre()); |
| } |
| |
| TEST_F(AuctionRunnerTest, PrivateAggregationBuyersNoReportBuyers) { |
| constexpr base::TimeDelta kTrustedSignalsFetchLatency = base::Milliseconds(2); |
| constexpr absl::uint128 kBaseBucket = 100l; |
| |
| auction_report_buyer_keys_ = {{kBaseBucket}}; |
| auction_report_buyers_ = absl::nullopt; |
| |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| blink::TestInterestGroupBuilder(kBidder1, kBidder1Name) |
| .SetBiddingUrl(kBidder1Url) |
| .SetTrustedBiddingSignalsUrl(kBidder1TrustedSignalsUrl) |
| .SetTrustedBiddingSignalsKeys({{"k1", "k2"}}) |
| .SetAds({{blink::InterestGroup::Ad(GURL("https://ad1.com"), |
| absl::nullopt)}}) |
| .SetSellerCapabilities({{{url::Origin::Create(kSellerUrl), |
| blink::SellerCapabilities::kLatencyStats}}}) |
| .Build())); |
| |
| RunExtendedPABuyersAuction(bidders, {kTrustedSignalsFetchLatency}); |
| |
| EXPECT_THAT(private_aggregation_manager_.TakePrivateAggregationRequests(), |
| testing::UnorderedElementsAre()); |
| } |
| |
| TEST_F(AuctionRunnerTest, |
| PrivateAggregationBuyersReportBuyersDoesntMatchCapabilities) { |
| constexpr base::TimeDelta kTrustedSignalsFetchLatency = base::Milliseconds(2); |
| constexpr absl::uint128 kBaseBucket = 100l; |
| constexpr absl::uint128 kOffset = 0l; |
| constexpr double kScale = 1.0; |
| |
| auction_report_buyer_keys_ = {{kBaseBucket}}; |
| auction_report_buyers_ = {{{blink::AuctionConfig::NonSharedParams:: |
| BuyerReportType::kInterestGroupCount, |
| {/*bucket=*/kOffset, |
| /*scale=*/kScale}}}}; |
| |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| blink::TestInterestGroupBuilder(kBidder1, kBidder1Name) |
| .SetBiddingUrl(kBidder1Url) |
| .SetTrustedBiddingSignalsUrl(kBidder1TrustedSignalsUrl) |
| .SetTrustedBiddingSignalsKeys({{"k1", "k2"}}) |
| .SetAds({{blink::InterestGroup::Ad(GURL("https://ad1.com"), |
| absl::nullopt)}}) |
| .SetSellerCapabilities({{{url::Origin::Create(kSellerUrl), |
| blink::SellerCapabilities::kLatencyStats}}}) |
| .Build())); |
| |
| RunExtendedPABuyersAuction(bidders, {kTrustedSignalsFetchLatency}); |
| |
| EXPECT_THAT(private_aggregation_manager_.TakePrivateAggregationRequests(), |
| testing::UnorderedElementsAre()); |
| } |
| |
| TEST_F(AuctionRunnerTest, PrivateAggregationBuyersReportMultipleBidders) { |
| constexpr base::TimeDelta kTrustedSignalsFetchLatency1 = |
| base::Milliseconds(2); |
| constexpr base::TimeDelta kTrustedSignalsFetchLatency2 = |
| base::Milliseconds(4); |
| constexpr absl::uint128 kBaseBucket1 = 100l; |
| constexpr absl::uint128 kBaseBucket2 = 105l; |
| constexpr absl::uint128 kOffset = 0l; |
| constexpr double kScale = 1.0; |
| |
| auction_report_buyer_keys_ = {{kBaseBucket1, kBaseBucket2}}; |
| auction_report_buyers_ = {{{blink::AuctionConfig::NonSharedParams:: |
| BuyerReportType::kTotalSignalsFetchLatency, |
| {/*bucket=*/kOffset, |
| /*scale=*/kScale}}}}; |
| |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| blink::TestInterestGroupBuilder(kBidder1, kBidder1Name) |
| .SetBiddingUrl(kBidder1Url) |
| .SetTrustedBiddingSignalsUrl(kBidder1TrustedSignalsUrl) |
| .SetTrustedBiddingSignalsKeys({{"k1", "k2"}}) |
| .SetAds({{blink::InterestGroup::Ad(GURL("https://ad1.com"), |
| absl::nullopt)}}) |
| .SetSellerCapabilities({{{url::Origin::Create(kSellerUrl), |
| blink::SellerCapabilities::kLatencyStats}}}) |
| .Build())); |
| bidders.emplace_back(MakeInterestGroup( |
| blink::TestInterestGroupBuilder(kBidder2, kBidder2Name) |
| .SetBiddingUrl(kBidder2Url) |
| .SetTrustedBiddingSignalsUrl(kBidder2TrustedSignalsUrl) |
| .SetTrustedBiddingSignalsKeys({{"k1", "k2"}}) |
| .SetAds({{blink::InterestGroup::Ad(GURL("https://ad1.com"), |
| absl::nullopt)}}) |
| .SetSellerCapabilities({{{url::Origin::Create(kSellerUrl), |
| blink::SellerCapabilities::kLatencyStats}}}) |
| .Build())); |
| |
| RunExtendedPABuyersAuction( |
| bidders, /*trusted_fetch_latency=*/ |
| {kTrustedSignalsFetchLatency1, kTrustedSignalsFetchLatency2}, |
| /*bidding_latency=*/{base::TimeDelta(), base::TimeDelta()}, |
| /*should_bid=*/{true, true}); |
| |
| EXPECT_THAT( |
| private_aggregation_manager_.TakePrivateAggregationRequests(), |
| testing::UnorderedElementsAre(testing::Pair( |
| url::Origin::Create(kSellerUrl), |
| ElementsAreRequests( |
| BuildPrivateAggregationRequest( |
| /*bucket=*/kBaseBucket1 + kOffset, |
| /*value=*/kTrustedSignalsFetchLatency1.InMilliseconds() * |
| kScale), |
| BuildPrivateAggregationRequest( |
| /*bucket=*/kBaseBucket2 + kOffset, |
| /*value=*/kTrustedSignalsFetchLatency2.InMilliseconds() * |
| kScale))))); |
| } |
| |
| TEST_F(AuctionRunnerTest, |
| PrivateAggregationBuyersReportMultipleBiddersIncompleteBuyerKeys) { |
| constexpr base::TimeDelta kTrustedSignalsFetchLatency1 = |
| base::Milliseconds(2); |
| constexpr base::TimeDelta kTrustedSignalsFetchLatency2 = |
| base::Milliseconds(4); |
| constexpr absl::uint128 kBaseBucket = 100l; |
| constexpr absl::uint128 kOffset = 0l; |
| constexpr double kScale = 1.0; |
| |
| auction_report_buyer_keys_ = {{kBaseBucket}}; |
| auction_report_buyers_ = {{{blink::AuctionConfig::NonSharedParams:: |
| BuyerReportType::kTotalSignalsFetchLatency, |
| {/*bucket=*/kOffset, |
| /*scale=*/kScale}}}}; |
| |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| blink::TestInterestGroupBuilder(kBidder1, kBidder1Name) |
| .SetBiddingUrl(kBidder1Url) |
| .SetTrustedBiddingSignalsUrl(kBidder1TrustedSignalsUrl) |
| .SetTrustedBiddingSignalsKeys({{"k1", "k2"}}) |
| .SetAds({{blink::InterestGroup::Ad(GURL("https://ad1.com"), |
| absl::nullopt)}}) |
| .SetSellerCapabilities({{{url::Origin::Create(kSellerUrl), |
| blink::SellerCapabilities::kLatencyStats}}}) |
| .Build())); |
| bidders.emplace_back(MakeInterestGroup( |
| blink::TestInterestGroupBuilder(kBidder2, kBidder2Name) |
| .SetBiddingUrl(kBidder2Url) |
| .SetTrustedBiddingSignalsUrl(kBidder2TrustedSignalsUrl) |
| .SetTrustedBiddingSignalsKeys({{"k1", "k2"}}) |
| .SetAds({{blink::InterestGroup::Ad(GURL("https://ad1.com"), |
| absl::nullopt)}}) |
| .SetSellerCapabilities({{{url::Origin::Create(kSellerUrl), |
| blink::SellerCapabilities::kLatencyStats}}}) |
| .Build())); |
| |
| RunExtendedPABuyersAuction( |
| bidders, /*trusted_fetch_latency=*/ |
| {kTrustedSignalsFetchLatency1, kTrustedSignalsFetchLatency2}, |
| /*bidding_latency=*/{base::TimeDelta(), base::TimeDelta()}, |
| /*should_bid=*/{true, true}); |
| |
| EXPECT_THAT(private_aggregation_manager_.TakePrivateAggregationRequests(), |
| testing::UnorderedElementsAre(testing::Pair( |
| url::Origin::Create(kSellerUrl), |
| ElementsAreRequests(BuildPrivateAggregationRequest( |
| /*bucket=*/kBaseBucket + kOffset, |
| /*value=*/kTrustedSignalsFetchLatency1.InMilliseconds() * |
| kScale))))); |
| } |
| |
| TEST_F(AuctionRunnerTest, |
| PrivateAggregationBuyersMultipleBiddersSameOwnerTrustedSignalsLatency) { |
| constexpr base::TimeDelta kTrustedSignalsFetchLatency1 = |
| base::Milliseconds(2); |
| constexpr base::TimeDelta kTrustedSignalsFetchLatency2 = |
| base::Milliseconds(3); |
| constexpr absl::uint128 kBaseBucket = 100l; |
| constexpr absl::uint128 kOffset = 0l; |
| constexpr double kScale = 1.0; |
| |
| auction_report_buyer_keys_ = {{kBaseBucket}}; |
| auction_report_buyers_ = {{{blink::AuctionConfig::NonSharedParams:: |
| BuyerReportType::kTotalSignalsFetchLatency, |
| {/*bucket=*/kOffset, |
| /*scale=*/kScale}}}}; |
| // Both interest groups belong to the same bidder. |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| blink::TestInterestGroupBuilder(kBidder1, kBidder1Name) |
| .SetBiddingUrl(kBidder1Url) |
| .SetTrustedBiddingSignalsUrl(kBidder1TrustedSignalsUrl) |
| .SetTrustedBiddingSignalsKeys({{"k1", "k2"}}) |
| .SetAds({{blink::InterestGroup::Ad(GURL("https://ad1.com"), |
| absl::nullopt)}}) |
| .SetSellerCapabilities({{{url::Origin::Create(kSellerUrl), |
| blink::SellerCapabilities::kLatencyStats}}}) |
| .Build())); |
| bidders.emplace_back(MakeInterestGroup( |
| blink::TestInterestGroupBuilder(kBidder1, kBidder1NameAlt) |
| .SetBiddingUrl(kBidder1UrlAlt) |
| .SetTrustedBiddingSignalsUrl(kBidder1TrustedSignalsUrl) |
| .SetTrustedBiddingSignalsKeys({{"k1", "k2"}}) |
| .SetAds({{blink::InterestGroup::Ad(GURL("https://ad1.com"), |
| absl::nullopt)}}) |
| .SetSellerCapabilities({{{url::Origin::Create(kSellerUrl), |
| blink::SellerCapabilities::kLatencyStats}}}) |
| .Build())); |
| |
| RunExtendedPABuyersAuction( |
| bidders, /*trusted_fetch_latency=*/ |
| {kTrustedSignalsFetchLatency1, kTrustedSignalsFetchLatency2}, |
| /*bidding_latency=*/{base::TimeDelta(), base::TimeDelta()}, |
| /*should_bid=*/{true, true}); |
| |
| EXPECT_THAT( |
| private_aggregation_manager_.TakePrivateAggregationRequests(), |
| testing::UnorderedElementsAre(testing::Pair( |
| url::Origin::Create(kSellerUrl), |
| ElementsAreRequests( |
| BuildPrivateAggregationRequest( |
| /*bucket=*/kBaseBucket + kOffset, |
| /*value=*/kTrustedSignalsFetchLatency1.InMilliseconds() * |
| kScale), |
| BuildPrivateAggregationRequest( |
| /*bucket=*/kBaseBucket + kOffset, |
| /*value=*/kTrustedSignalsFetchLatency2.InMilliseconds() * |
| kScale))))); |
| } |
| |
| TEST_F(AuctionRunnerTest, |
| PrivateAggregationBuyersMultipleBiddersSameOwnerBiddingLatency) { |
| constexpr base::TimeDelta kBiddingFetchLatency1 = base::Milliseconds(2); |
| constexpr base::TimeDelta kBiddingFetchLatency2 = base::Milliseconds(3); |
| constexpr absl::uint128 kBaseBucket = 100l; |
| constexpr absl::uint128 kOffset = 0l; |
| constexpr double kScale = 1.0; |
| |
| auction_report_buyer_keys_ = {{kBaseBucket}}; |
| auction_report_buyers_ = {{{blink::AuctionConfig::NonSharedParams:: |
| BuyerReportType::kTotalGenerateBidLatency, |
| {/*bucket=*/kOffset, |
| /*scale=*/kScale}}}}; |
| // Both interest groups belong to the same bidder. |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| blink::TestInterestGroupBuilder(kBidder1, kBidder1Name) |
| .SetBiddingUrl(kBidder1Url) |
| .SetTrustedBiddingSignalsUrl(kBidder1TrustedSignalsUrl) |
| .SetTrustedBiddingSignalsKeys({{"k1", "k2"}}) |
| .SetAds({{blink::InterestGroup::Ad(GURL("https://ad1.com"), |
| absl::nullopt)}}) |
| .SetSellerCapabilities({{{url::Origin::Create(kSellerUrl), |
| blink::SellerCapabilities::kLatencyStats}}}) |
| .Build())); |
| bidders.emplace_back(MakeInterestGroup( |
| blink::TestInterestGroupBuilder(kBidder1, kBidder1NameAlt) |
| .SetBiddingUrl(kBidder1UrlAlt) |
| .SetTrustedBiddingSignalsUrl(kBidder1TrustedSignalsUrl) |
| .SetTrustedBiddingSignalsKeys({{"k1", "k2"}}) |
| .SetAds({{blink::InterestGroup::Ad(GURL("https://ad1.com"), |
| absl::nullopt)}}) |
| .SetSellerCapabilities({{{url::Origin::Create(kSellerUrl), |
| blink::SellerCapabilities::kLatencyStats}}}) |
| .Build())); |
| |
| RunExtendedPABuyersAuction( |
| bidders, /*trusted_fetch_latency=*/ |
| {base::TimeDelta(), base::TimeDelta()}, |
| /*bidding_latency=*/{kBiddingFetchLatency1, kBiddingFetchLatency2}, |
| /*should_bid=*/{true, true}); |
| |
| EXPECT_THAT( |
| private_aggregation_manager_.TakePrivateAggregationRequests(), |
| testing::UnorderedElementsAre(testing::Pair( |
| url::Origin::Create(kSellerUrl), |
| ElementsAreRequests( |
| BuildPrivateAggregationRequest( |
| /*bucket=*/kBaseBucket + kOffset, |
| /*value=*/kBiddingFetchLatency1.InMilliseconds() * kScale), |
| BuildPrivateAggregationRequest( |
| /*bucket=*/kBaseBucket + kOffset, |
| /*value=*/kBiddingFetchLatency2.InMilliseconds() * |
| kScale))))); |
| } |
| |
| TEST_F(AuctionRunnerTest, PrivateAggregationBuyersMultipleStats) { |
| constexpr base::TimeDelta kTrustedSignalsFetchLatency = base::Milliseconds(2); |
| constexpr base::TimeDelta kBiddingFetchLatency = base::Milliseconds(3); |
| constexpr absl::uint128 kBaseBucket = 100l; |
| constexpr absl::uint128 kOffset1 = 0l; |
| constexpr absl::uint128 kOffset2 = 1l; |
| constexpr double kScale = 1.0; |
| |
| auction_report_buyer_keys_ = {{kBaseBucket}}; |
| auction_report_buyers_ = {{{blink::AuctionConfig::NonSharedParams:: |
| BuyerReportType::kTotalSignalsFetchLatency, |
| {/*bucket=*/kOffset1, |
| /*scale=*/kScale}}, |
| {blink::AuctionConfig::NonSharedParams:: |
| BuyerReportType::kTotalGenerateBidLatency, |
| {/*bucket=*/kOffset2, |
| /*scale=*/kScale}}}}; |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| blink::TestInterestGroupBuilder(kBidder1, kBidder1Name) |
| .SetBiddingUrl(kBidder1Url) |
| .SetTrustedBiddingSignalsUrl(kBidder1TrustedSignalsUrl) |
| .SetTrustedBiddingSignalsKeys({{"k1", "k2"}}) |
| .SetAds({{blink::InterestGroup::Ad(GURL("https://ad1.com"), |
| absl::nullopt)}}) |
| .SetSellerCapabilities({{{url::Origin::Create(kSellerUrl), |
| blink::SellerCapabilities::kLatencyStats}}}) |
| .Build())); |
| |
| RunExtendedPABuyersAuction(bidders, /*trusted_fetch_latency=*/ |
| {kTrustedSignalsFetchLatency}, |
| /*bidding_latency=*/{kBiddingFetchLatency}, |
| /*should_bid=*/{true}); |
| |
| EXPECT_THAT( |
| private_aggregation_manager_.TakePrivateAggregationRequests(), |
| testing::UnorderedElementsAre(testing::Pair( |
| url::Origin::Create(kSellerUrl), |
| ElementsAreRequests( |
| BuildPrivateAggregationRequest( |
| /*bucket=*/kBaseBucket + kOffset1, |
| /*value=*/kTrustedSignalsFetchLatency.InMilliseconds() * |
| kScale), |
| BuildPrivateAggregationRequest( |
| /*bucket=*/kBaseBucket + kOffset2, |
| /*value=*/kBiddingFetchLatency.InMilliseconds() * kScale))))); |
| } |
| |
| TEST_F(AuctionRunnerTest, PrivateAggregationBuyersReportBidCount) { |
| constexpr absl::uint128 kBaseBucket1 = 100l; |
| constexpr absl::uint128 kBaseBucket2 = 105l; |
| constexpr absl::uint128 kOffset = 0l; |
| constexpr double kScale = 1.0; |
| |
| auction_report_buyer_keys_ = {{kBaseBucket1, kBaseBucket2}}; |
| auction_report_buyers_ = { |
| {{blink::AuctionConfig::NonSharedParams::BuyerReportType::kBidCount, |
| {/*bucket=*/kOffset, |
| /*scale=*/kScale}}}}; |
| |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| blink::TestInterestGroupBuilder(kBidder1, kBidder1Name) |
| .SetBiddingUrl(kBidder1Url) |
| .SetTrustedBiddingSignalsUrl(kBidder1TrustedSignalsUrl) |
| .SetTrustedBiddingSignalsKeys({{"k1", "k2"}}) |
| .SetAds({{blink::InterestGroup::Ad(GURL("https://ad1.com"), |
| absl::nullopt)}}) |
| .SetSellerCapabilities( |
| {{{url::Origin::Create(kSellerUrl), |
| blink::SellerCapabilities::kInterestGroupCounts}}}) |
| .Build())); |
| bidders.emplace_back(MakeInterestGroup( |
| blink::TestInterestGroupBuilder(kBidder2, kBidder2Name) |
| .SetBiddingUrl(kBidder2Url) |
| .SetTrustedBiddingSignalsUrl(kBidder2TrustedSignalsUrl) |
| .SetTrustedBiddingSignalsKeys({{"k1", "k2"}}) |
| .SetAds({{blink::InterestGroup::Ad(GURL("https://ad1.com"), |
| absl::nullopt)}}) |
| .SetSellerCapabilities( |
| {{{url::Origin::Create(kSellerUrl), |
| blink::SellerCapabilities::kInterestGroupCounts}}}) |
| .Build())); |
| |
| RunExtendedPABuyersAuction( |
| bidders, /*trusted_fetch_latency=*/ |
| {base::TimeDelta(), base::TimeDelta()}, |
| /*bidding_latency=*/{base::TimeDelta(), base::TimeDelta()}, |
| /*should_bid=*/{true, false}); |
| |
| EXPECT_THAT(private_aggregation_manager_.TakePrivateAggregationRequests(), |
| testing::UnorderedElementsAre(testing::Pair( |
| url::Origin::Create(kSellerUrl), |
| ElementsAreRequests(BuildPrivateAggregationRequest( |
| /*bucket=*/kBaseBucket1 + kOffset, |
| /*value=*/1 * kScale), |
| BuildPrivateAggregationRequest( |
| /*bucket=*/kBaseBucket2 + kOffset, |
| /*value=*/0 * kScale))))); |
| } |
| |
| TEST_F(AuctionRunnerTest, PrivateAggregationBuyersReportInterestGroupCount) { |
| constexpr absl::uint128 kBaseBucket1 = 100l; |
| constexpr absl::uint128 kBaseBucket2 = 105l; |
| constexpr absl::uint128 kOffset = 0l; |
| constexpr double kScale = 1.0; |
| |
| auction_report_buyer_keys_ = {{kBaseBucket1, kBaseBucket2}}; |
| auction_report_buyers_ = {{{blink::AuctionConfig::NonSharedParams:: |
| BuyerReportType::kInterestGroupCount, |
| {/*bucket=*/kOffset, |
| /*scale=*/kScale}}}}; |
| |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| blink::TestInterestGroupBuilder(kBidder1, kBidder1Name) |
| .SetBiddingUrl(kBidder1Url) |
| .SetTrustedBiddingSignalsUrl(kBidder1TrustedSignalsUrl) |
| .SetTrustedBiddingSignalsKeys({{"k1", "k2"}}) |
| .SetAds({{blink::InterestGroup::Ad(GURL("https://ad1.com"), |
| absl::nullopt)}}) |
| .SetSellerCapabilities( |
| {{{url::Origin::Create(kSellerUrl), |
| blink::SellerCapabilities::kInterestGroupCounts}}}) |
| .Build())); |
| bidders.emplace_back(MakeInterestGroup( |
| blink::TestInterestGroupBuilder(kBidder2, kBidder2Name) |
| .SetBiddingUrl(kBidder2Url) |
| .SetTrustedBiddingSignalsUrl(kBidder2TrustedSignalsUrl) |
| .SetTrustedBiddingSignalsKeys({{"k1", "k2"}}) |
| .SetAds({{blink::InterestGroup::Ad(GURL("https://ad1.com"), |
| absl::nullopt)}}) |
| .SetSellerCapabilities( |
| {{{url::Origin::Create(kSellerUrl), |
| blink::SellerCapabilities::kInterestGroupCounts}}}) |
| .Build())); |
| |
| RunExtendedPABuyersAuction( |
| bidders, /*trusted_fetch_latency=*/ |
| {base::TimeDelta(), base::TimeDelta()}, |
| /*bidding_latency=*/{base::TimeDelta(), base::TimeDelta()}, |
| /*should_bid=*/{true, false}); |
| |
| EXPECT_THAT(private_aggregation_manager_.TakePrivateAggregationRequests(), |
| testing::UnorderedElementsAre(testing::Pair( |
| url::Origin::Create(kSellerUrl), |
| ElementsAreRequests(BuildPrivateAggregationRequest( |
| /*bucket=*/kBaseBucket1 + kOffset, |
| /*value=*/1 * kScale), |
| BuildPrivateAggregationRequest( |
| /*bucket=*/kBaseBucket2 + kOffset, |
| /*value=*/1 * kScale))))); |
| } |
| |
| // Reported InterestGroupCount is unaffected by the group limit. |
| TEST_F(AuctionRunnerTest, |
| PrivateAggregationBuyersInterestGroupCountUnconstrainedByLimits) { |
| constexpr absl::uint128 kBaseBucket = 100l; |
| constexpr absl::uint128 kOffset = 0l; |
| constexpr double kScale = 1.0; |
| |
| auction_report_buyer_keys_ = {{kBaseBucket}}; |
| auction_report_buyers_ = {{{blink::AuctionConfig::NonSharedParams:: |
| BuyerReportType::kInterestGroupCount, |
| {/*bucket=*/kOffset, |
| /*scale=*/kScale}}}}; |
| all_buyers_group_limit_ = 1; |
| |
| // Both interest groups belong to the same bidder. |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| blink::TestInterestGroupBuilder(kBidder1, kBidder1Name) |
| .SetBiddingUrl(kBidder1Url) |
| .SetTrustedBiddingSignalsUrl(kBidder1TrustedSignalsUrl) |
| .SetTrustedBiddingSignalsKeys({{"k1", "k2"}}) |
| .SetAds({{blink::InterestGroup::Ad(GURL("https://ad1.com"), |
| absl::nullopt)}}) |
| .SetSellerCapabilities( |
| {{{url::Origin::Create(kSellerUrl), |
| blink::SellerCapabilities::kInterestGroupCounts}}}) |
| .Build())); |
| bidders.emplace_back(MakeInterestGroup( |
| blink::TestInterestGroupBuilder(kBidder1, kBidder1NameAlt) |
| .SetBiddingUrl(kBidder1UrlAlt) |
| .SetTrustedBiddingSignalsUrl(kBidder1TrustedSignalsUrl) |
| .SetTrustedBiddingSignalsKeys({{"k1", "k2"}}) |
| .SetAds({{blink::InterestGroup::Ad(GURL("https://ad1.com"), |
| absl::nullopt)}}) |
| .SetSellerCapabilities( |
| {{{url::Origin::Create(kSellerUrl), |
| blink::SellerCapabilities::kInterestGroupCounts}}}) |
| .Build())); |
| |
| RunAuctionAndWait(kSellerUrl, std::move(bidders)); |
| |
| // Only one report comes in, and it indicates there are 2 groups, even though |
| // we apply a group limit of 1. |
| EXPECT_THAT(private_aggregation_manager_.TakePrivateAggregationRequests(), |
| testing::UnorderedElementsAre(testing::Pair( |
| url::Origin::Create(kSellerUrl), |
| ElementsAreRequests(BuildPrivateAggregationRequest( |
| /*bucket=*/kBaseBucket + kOffset, |
| /*value=*/2 * kScale))))); |
| } |
| |
| class RoundingTest : public AuctionRunnerTest, |
| public ::testing::WithParamInterface<size_t> { |
| public: |
| RoundingTest(size_t bid_bits, size_t score_bits, size_t cost_bits) |
| : bid_bits_(bid_bits), score_bits_(score_bits), cost_bits_(cost_bits) { |
| scoped_feature_list_.InitWithFeaturesAndParameters( |
| {{kFledgeRounding, |
| {{kFledgeBidReportingBits.name, base::NumberToString(bid_bits_)}, |
| {kFledgeScoreReportingBits.name, base::NumberToString(score_bits_)}, |
| {kFledgeAdCostReportingBits.name, |
| base::NumberToString(cost_bits_)}}}}, |
| {}); |
| } |
| |
| size_t bid_bits() { return bid_bits_; } |
| size_t score_bits() { return score_bits_; } |
| size_t cost_bits() { return cost_bits_; } |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| size_t bid_bits_, score_bits_, cost_bits_; |
| }; |
| |
| class BidRoundingTest : public RoundingTest { |
| public: |
| BidRoundingTest() : RoundingTest(GetParam(), 8, 8) {} |
| }; |
| |
| class ScoreRoundingTest : public RoundingTest { |
| public: |
| ScoreRoundingTest() : RoundingTest(8, GetParam(), 8) {} |
| }; |
| |
| class CostRoundingTest : public RoundingTest { |
| public: |
| CostRoundingTest() : RoundingTest(8, 8, GetParam()) {} |
| }; |
| |
| TEST_P(CostRoundingTest, AdCostPassed) { |
| const char kBidScript[] = R"( |
| const bid = %d; |
| function generateBid( |
| interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals, |
| browserSignals) { |
| return {bid: bid, |
| render: interestGroup.ads[0].renderUrl, |
| adCost: bid + 1}; |
| } |
| |
| function reportWin( |
| auctionSignals, perBuyerSignals, sellerSignals, browserSignals) { |
| sendReportTo("https://buyer-reporting.example.com/?adCost=" + |
| browserSignals.adCost); |
| } |
| )"; |
| |
| const std::string kSellerScript = R"( |
| function scoreAd(adMetadata, bid, auctionConfig, browserSignals) { |
| return bid; |
| } |
| |
| function reportResult(auctionConfig, browserSignals) { |
| } |
| )"; |
| |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kBidder1Url, |
| base::StringPrintf(kBidScript, 1)); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| kSellerScript); |
| |
| // Only one bidder, to keep things simple. |
| interest_group_buyers_ = {{kBidder1}}; |
| RunStandardAuction(/*request_trusted_bidding_signals=*/false); |
| EXPECT_FALSE(result_.manually_aborted); |
| EXPECT_EQ(kBidder1Key, result_.winning_group_id); |
| EXPECT_EQ(GURL("https://ad1.com/"), result_.ad_descriptor->url); |
| |
| // adCost should be 2. |
| EXPECT_THAT(result_.report_urls, |
| testing::ElementsAre( |
| GURL("https://buyer-reporting.example.com/?adCost=2"))); |
| } |
| |
| TEST_P(CostRoundingTest, AdCostRounded) { |
| const char kBidScript[] = R"( |
| const bid = %f; |
| function generateBid( |
| interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals, |
| browserSignals) { |
| // Return an adCost that requires more bits of precision than allowed. |
| return {bid: bid, |
| render: interestGroup.ads[0].renderUrl, |
| adCost: bid}; |
| } |
| |
| function reportWin( |
| auctionSignals, perBuyerSignals, sellerSignals, browserSignals) { |
| sendReportTo("https://buyer-reporting.example.com/?adCost=" + |
| browserSignals.adCost); |
| } |
| )"; |
| |
| const std::string kSellerScript = R"( |
| function scoreAd(adMetadata, bid, auctionConfig, browserSignals) { |
| return bid; |
| } |
| |
| function reportResult(auctionConfig, browserSignals) { |
| } |
| )"; |
| |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kBidder1Url, |
| base::StringPrintf(kBidScript, 1.99)); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| kSellerScript); |
| |
| // Only one bidder, to keep things simple. |
| interest_group_buyers_ = {{kBidder1}}; |
| RunStandardAuction(/*request_trusted_bidding_signals=*/false); |
| EXPECT_FALSE(result_.manually_aborted); |
| EXPECT_EQ(kBidder1Key, result_.winning_group_id); |
| EXPECT_EQ(GURL("https://ad1.com/"), result_.ad_descriptor->url); |
| |
| switch (GetParam()) { |
| case 8: |
| EXPECT_THAT( |
| result_.report_urls, |
| testing::ElementsAre(testing::AnyOf( |
| GURL("https://buyer-reporting.example.com/?adCost=1.9921875"), |
| GURL("https://buyer-reporting.example.com/?adCost=1.984375")))); |
| break; |
| case 16: |
| EXPECT_THAT( |
| result_.report_urls, |
| testing::ElementsAre(testing::AnyOf( |
| GURL( |
| "https://buyer-reporting.example.com/?adCost=1.990005493164"), |
| GURL("https://buyer-reporting.example.com/" |
| "?adCost=1.989990234375")))); |
| break; |
| case 53: |
| EXPECT_THAT(result_.report_urls, |
| testing::ElementsAre(GURL( |
| "https://buyer-reporting.example.com/?adCost=1.99"))); |
| break; |
| default: |
| // Not a supported test case. |
| ASSERT_TRUE(false); |
| } |
| } |
| |
| TEST_P(CostRoundingTest, AdCostExponentTruncated) { |
| const char kBidScript[] = R"( |
| const bid = %d; |
| function generateBid( |
| interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals, |
| browserSignals) { |
| // Return an adCost that requires more bits of exponent than allowed. |
| return {bid: bid, render: interestGroup.ads[0].renderUrl, adCost: 2**256}; |
| } |
| |
| function reportWin( |
| auctionSignals, perBuyerSignals, sellerSignals, browserSignals) { |
| sendReportTo("https://buyer-reporting.example.com/?adCost=" + |
| browserSignals.adCost); |
| } |
| )"; |
| |
| const std::string kSellerScript = R"( |
| function scoreAd(adMetadata, bid, auctionConfig, browserSignals) { |
| return bid; |
| } |
| |
| function reportResult(auctionConfig, browserSignals) { |
| } |
| )"; |
| |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kBidder1Url, |
| base::StringPrintf(kBidScript, 1)); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| kSellerScript); |
| |
| // Only one bidder, to keep things simple. |
| interest_group_buyers_ = {{kBidder1}}; |
| RunStandardAuction(/*request_trusted_bidding_signals=*/false); |
| EXPECT_FALSE(result_.manually_aborted); |
| EXPECT_EQ(kBidder1Key, result_.winning_group_id); |
| EXPECT_EQ(GURL("https://ad1.com/"), result_.ad_descriptor->url); |
| |
| // adCost should be (Infinity). |
| EXPECT_THAT(result_.report_urls, |
| testing::ElementsAre(GURL( |
| "https://buyer-reporting.example.com/?adCost=Infinity"))); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P( |
| /* no label */, |
| CostRoundingTest, |
| testing::Values(8, 16, 53)); |
| |
| TEST_P(BidRoundingTest, BidRounded) { |
| const char kBidScript[] = R"( |
| function generateBid( |
| interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals, |
| browserSignals) { |
| // Return a bid that requires more bits of precision than allowed. |
| return {bid: %f, |
| render: interestGroup.ads[0].renderUrl}; |
| } |
| |
| function reportWin( |
| auctionSignals, perBuyerSignals, sellerSignals, browserSignals) { |
| sendReportTo("https://buyer-reporting.example.com/?bid=" + |
| browserSignals.bid); |
| } |
| )"; |
| |
| const std::string kSellerScript = R"( |
| function scoreAd(adMetadata, bid, auctionConfig, browserSignals) { |
| return bid; |
| } |
| |
| function reportResult(auctionConfig, browserSignals) { |
| } |
| )"; |
| |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kBidder1Url, |
| base::StringPrintf(kBidScript, 1.99)); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| kSellerScript); |
| |
| // Only one bidder, to keep things simple. |
| interest_group_buyers_ = {{kBidder1}}; |
| RunStandardAuction(/*request_trusted_bidding_signals=*/false); |
| EXPECT_FALSE(result_.manually_aborted); |
| EXPECT_EQ(kBidder1Key, result_.winning_group_id); |
| EXPECT_EQ(GURL("https://ad1.com/"), result_.ad_descriptor->url); |
| |
| switch (GetParam()) { |
| case 8: |
| EXPECT_THAT( |
| result_.report_urls, |
| testing::ElementsAre(testing::AnyOf( |
| GURL("https://buyer-reporting.example.com/?bid=1.9921875"), |
| GURL("https://buyer-reporting.example.com/?bid=1.984375")))); |
| break; |
| case 16: |
| EXPECT_THAT( |
| result_.report_urls, |
| testing::ElementsAre(testing::AnyOf( |
| GURL("https://buyer-reporting.example.com/?bid=1.990005493164"), |
| GURL( |
| "https://buyer-reporting.example.com/?bid=1.989990234375")))); |
| break; |
| case 53: |
| EXPECT_THAT(result_.report_urls, |
| testing::ElementsAre( |
| GURL("https://buyer-reporting.example.com/?bid=1.99"))); |
| break; |
| default: |
| // Not a supported test case. |
| ASSERT_TRUE(false); |
| } |
| } |
| |
| TEST_P(BidRoundingTest, HighestScoringOtherBidRounded) { |
| const char kBidScript[] = R"( |
| function generateBid( |
| interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals, |
| browserSignals) { |
| // Return a bid that requires more bits of precision than allowed. |
| return {bid: %f, |
| render: interestGroup.ads[0].renderUrl}; |
| } |
| |
| function reportWin( |
| auctionSignals, perBuyerSignals, sellerSignals, browserSignals) { |
| sendReportTo("https://buyer-reporting.example.com/?" |
| + "highestScoringOtherBid=" + browserSignals.highestScoringOtherBid); |
| } |
| )"; |
| |
| const std::string kSellerScript = R"( |
| function scoreAd(adMetadata, bid, auctionConfig, browserSignals) { |
| return bid; |
| } |
| |
| function reportResult(auctionConfig, browserSignals) { |
| sendReportTo("https://seller-reporting.example.com/?" |
| + "highestScoringOtherBid=" |
| + browserSignals.highestScoringOtherBid); |
| } |
| )"; |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kBidder2Url, |
| base::StringPrintf(kBidScript, 3.0)); |
| |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kBidder1Url, |
| base::StringPrintf(kBidScript, 1.99)); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| kSellerScript); |
| |
| RunStandardAuction(/*request_trusted_bidding_signals=*/false); |
| EXPECT_FALSE(result_.manually_aborted); |
| EXPECT_EQ(kBidder2Key, result_.winning_group_id); |
| EXPECT_EQ(GURL("https://ad2.com/"), result_.ad_descriptor->url); |
| |
| switch (GetParam()) { |
| case 8: |
| EXPECT_THAT( |
| result_.report_urls, |
| testing::ElementsAre( |
| testing::AnyOf(GURL("https://seller-reporting.example.com/" |
| "?highestScoringOtherBid=1.9921875"), |
| GURL("https://seller-reporting.example.com/" |
| "?highestScoringOtherBid=1.984375")), |
| testing::AnyOf(GURL("https://buyer-reporting.example.com/" |
| "?highestScoringOtherBid=1.9921875"), |
| GURL("https://buyer-reporting.example.com/" |
| "?highestScoringOtherBid=1.984375")))); |
| break; |
| case 16: |
| EXPECT_THAT( |
| result_.report_urls, |
| testing::ElementsAre( |
| testing::AnyOf(GURL("https://seller-reporting.example.com/" |
| "?highestScoringOtherBid=1.990005493164"), |
| GURL("https://seller-reporting.example.com/" |
| "?highestScoringOtherBid=1.989990234375")), |
| testing::AnyOf(GURL("https://buyer-reporting.example.com/" |
| "?highestScoringOtherBid=1.990005493164"), |
| GURL("https://buyer-reporting.example.com/" |
| "?highestScoringOtherBid=1.989990234375")))); |
| break; |
| case 53: |
| EXPECT_THAT( |
| result_.report_urls, |
| testing::ElementsAre(GURL("https://seller-reporting.example.com/" |
| "?highestScoringOtherBid=1.99"), |
| GURL("https://buyer-reporting.example.com/" |
| "?highestScoringOtherBid=1.99"))); |
| break; |
| default: |
| // Not a supported test case. |
| ASSERT_TRUE(false); |
| } |
| } |
| |
| INSTANTIATE_TEST_SUITE_P( |
| /* no label */, |
| BidRoundingTest, |
| ::testing::Values(8, 16, 53)); |
| |
| TEST_P(ScoreRoundingTest, ScoreRounded) { |
| const char kBidScript[] = R"( |
| function generateBid( |
| interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals, |
| browserSignals) { |
| // Return an score that requires more bits of precision than allowed. |
| return {bid: %f, |
| render: interestGroup.ads[0].renderUrl}; |
| } |
| |
| function reportWin( |
| auctionSignals, perBuyerSignals, sellerSignals, browserSignals) { |
| } |
| )"; |
| |
| const std::string kSellerScript = R"( |
| function scoreAd(adMetadata, bid, auctionConfig, browserSignals) { |
| return 1.99; |
| } |
| |
| function reportResult(auctionConfig, browserSignals) { |
| sendReportTo("https://seller-reporting.example.com/?score=" + browserSignals.desirability); |
| } |
| )"; |
| |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kBidder1Url, |
| base::StringPrintf(kBidScript, 1.99)); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| kSellerScript); |
| |
| // Only one bidder, to keep things simple. |
| interest_group_buyers_ = {{kBidder1}}; |
| RunStandardAuction(/*request_trusted_bidding_signals=*/false); |
| EXPECT_FALSE(result_.manually_aborted); |
| EXPECT_EQ(kBidder1Key, result_.winning_group_id); |
| EXPECT_EQ(GURL("https://ad1.com/"), result_.ad_descriptor->url); |
| |
| switch (GetParam()) { |
| case 8: |
| EXPECT_THAT( |
| result_.report_urls, |
| testing::ElementsAre(testing::AnyOf( |
| GURL("https://seller-reporting.example.com/?score=1.9921875"), |
| GURL("https://seller-reporting.example.com/?score=1.984375")))); |
| break; |
| case 16: |
| EXPECT_THAT( |
| result_.report_urls, |
| testing::ElementsAre(testing::AnyOf( |
| GURL( |
| "https://seller-reporting.example.com/?score=1.990005493164"), |
| GURL("https://seller-reporting.example.com/" |
| "?score=1.989990234375")))); |
| break; |
| case 53: |
| EXPECT_THAT(result_.report_urls, |
| testing::ElementsAre(GURL( |
| "https://seller-reporting.example.com/?score=1.99"))); |
| break; |
| default: |
| // Not a supported test case. |
| ASSERT_TRUE(false); |
| } |
| } |
| |
| INSTANTIATE_TEST_SUITE_P( |
| /* no label */, |
| ScoreRoundingTest, |
| ::testing::Values(8, 16, 53)); |
| |
| // Enable and test forDebuggingOnly.reportAdAuctionLoss() and |
| // forDebuggingOnly.reportAdAuctionWin() APIs. |
| class AuctionRunnerBiddingAndScoringDebugReportingAPIEnabledTest |
| : public AuctionRunnerTest { |
| public: |
| AuctionRunnerBiddingAndScoringDebugReportingAPIEnabledTest() { |
| feature_list_.InitAndEnableFeature( |
| blink::features::kBiddingAndScoringDebugReportingAPI); |
| } |
| |
| protected: |
| base::test::ScopedFeatureList feature_list_; |
| }; |
| |
| TEST_F(AuctionRunnerBiddingAndScoringDebugReportingAPIEnabledTest, |
| ForDebuggingOnlyReporting) { |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder1Url, |
| MakeBidScript(kSeller, "1", "https://ad1.com/", /*num_ad_components=*/2, |
| kBidder1, kBidder1Name, |
| /*has_signals=*/false, "k1", "a", |
| /*report_post_auction_signals=*/true, |
| kBidder1DebugLossReportUrl, kBidder1DebugWinReportUrl)); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder2Url, |
| MakeBidScript(kSeller, "2", "https://ad2.com/", /*num_ad_components=*/2, |
| kBidder2, kBidder2Name, |
| /*has_signals=*/false, "l2", "b", |
| /*report_post_auction_signals=*/true, |
| kBidder2DebugLossReportUrl, kBidder2DebugWinReportUrl)); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kSellerUrl, |
| MakeAuctionScript(/*report_post_auction_signals=*/true, |
| GURL("https://adstuff.publisher1.com/auction.js"), |
| kSellerDebugLossReportBaseUrl, |
| kSellerDebugWinReportBaseUrl)); |
| |
| RunStandardAuction(/*request_trusted_bidding_signals=*/false); |
| EXPECT_THAT(result_.errors, testing::UnorderedElementsAre()); |
| |
| // Bidder 2 won the auction. |
| EXPECT_EQ(kBidder2Key, result_.winning_group_id); |
| EXPECT_EQ(GURL("https://ad2.com/"), result_.ad_descriptor->url); |
| |
| EXPECT_EQ(2u, result_.debug_loss_report_urls.size()); |
| // Sellers can get highest scoring other bid, but losing bidders can not. |
| EXPECT_THAT(result_.debug_loss_report_urls, |
| testing::UnorderedElementsAre( |
| DebugReportUrl(kBidder1DebugLossReportUrl, |
| PostAuctionSignals( |
| /*winning_bid=*/2, |
| /*made_winning_bid=*/false, |
| /*highest_scoring_other_bid=*/0, |
| /*made_highest_scoring_other_bid=*/false)), |
| DebugReportUrl(kSellerDebugLossReportBaseUrl, |
| PostAuctionSignals( |
| /*winning_bid=*/2, |
| /*made_winning_bid=*/false, |
| /*highest_scoring_other_bid=*/1, |
| /*made_highest_scoring_other_bid=*/true), |
| /*bid=*/1))); |
| |
| EXPECT_EQ(2u, result_.debug_win_report_urls.size()); |
| // Winning bidders can get highest scoring other bid. |
| EXPECT_THAT(result_.debug_win_report_urls, |
| testing::UnorderedElementsAre( |
| DebugReportUrl(kBidder2DebugWinReportUrl, |
| PostAuctionSignals( |
| /*winning_bid=*/2, |
| /*made_winning_bid=*/true, |
| /*highest_scoring_other_bid=*/1, |
| /*made_highest_scoring_other_bid=*/false)), |
| DebugReportUrl(kSellerDebugWinReportBaseUrl, |
| PostAuctionSignals( |
| /*winning_bid=*/2, |
| /*made_winning_bid=*/true, |
| /*highest_scoring_other_bid=*/1, |
| /*made_highest_scoring_other_bid=*/false), |
| /*bid=*/2))); |
| } |
| |
| // Post auction signals should only be reported through report URL's query |
| // string. Placeholder ${} in a debugging report URL's other parts such as path |
| // will be kept as it is without being replaced with actual signal. |
| TEST_F(AuctionRunnerBiddingAndScoringDebugReportingAPIEnabledTest, |
| PostAuctionSignalsInQueryStringOnly) { |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder1Url, |
| MakeBidScript( |
| kSeller, "1", "https://ad1.com/", /*num_ad_components=*/2, kBidder1, |
| kBidder1Name, |
| /*has_signals=*/false, "k1", "a", |
| /*report_post_auction_signals=*/true, |
| "https://bidder1-debug-loss-reporting.com/winningBid=${winningBid}", |
| "https://bidder1-debug-win-reporting.com/winningBid=${winningBid}")); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder2Url, |
| MakeBidScript( |
| kSeller, "2", "https://ad2.com/", /*num_ad_components=*/2, kBidder2, |
| kBidder2Name, |
| /*has_signals=*/false, "l2", "b", |
| /*report_post_auction_signals=*/true, |
| "https://bidder2-debug-loss-reporting.com/winningBid=${winningBid}", |
| "https://bidder2-debug-win-reporting.com/winningBid=${winningBid}")); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kSellerUrl, |
| MakeAuctionScript( |
| /*report_post_auction_signals=*/true, |
| GURL("https://adstuff.publisher1.com/auction.js"), |
| "https://seller-debug-loss-reporting.com/winningBid=${winningBid}", |
| "https://seller-debug-win-reporting.com/winningBid=${winningBid}")); |
| |
| RunStandardAuction(/*request_trusted_bidding_signals=*/false); |
| EXPECT_THAT(result_.errors, testing::UnorderedElementsAre()); |
| |
| // Bidder 2 won the auction. |
| EXPECT_EQ(kBidder2Key, result_.winning_group_id); |
| EXPECT_EQ(GURL("https://ad2.com/"), result_.ad_descriptor->url); |
| |
| // Placeholder ${winningBid} in a debugging report URL's path will not be |
| // replaced with actual signal. Only those in a debugging report URL's query |
| // param would be replaced. |
| EXPECT_EQ(2u, result_.debug_loss_report_urls.size()); |
| EXPECT_THAT(result_.debug_loss_report_urls, |
| testing::UnorderedElementsAre( |
| DebugReportUrl("https://bidder1-debug-loss-reporting.com/" |
| "winningBid=${winningBid}", |
| PostAuctionSignals( |
| /*winning_bid=*/2, |
| /*made_winning_bid=*/false, |
| /*highest_scoring_other_bid=*/0, |
| /*made_highest_scoring_other_bid=*/false)), |
| DebugReportUrl("https://seller-debug-loss-reporting.com/" |
| "winningBid=${winningBid}", |
| PostAuctionSignals( |
| /*winning_bid=*/2, |
| /*made_winning_bid=*/false, |
| /*highest_scoring_other_bid=*/1, |
| /*made_highest_scoring_other_bid=*/true), |
| /*bid=*/1))); |
| |
| EXPECT_EQ(2u, result_.debug_win_report_urls.size()); |
| EXPECT_THAT( |
| result_.debug_win_report_urls, |
| testing::UnorderedElementsAre( |
| DebugReportUrl("https://bidder2-debug-win-reporting.com/" |
| "winningBid=${winningBid}", |
| PostAuctionSignals( |
| /*winning_bid=*/2, |
| /*made_winning_bid=*/true, |
| /*highest_scoring_other_bid=*/1, |
| /*made_highest_scoring_other_bid=*/false)), |
| DebugReportUrl( |
| "https://seller-debug-win-reporting.com/winningBid=${winningBid}", |
| PostAuctionSignals( |
| /*winning_bid=*/2, |
| /*made_winning_bid=*/true, |
| /*highest_scoring_other_bid=*/1, |
| /*made_highest_scoring_other_bid=*/false), |
| /*bid=*/2))); |
| } |
| |
| // When there are multiple bids getting the highest score, then highest scoring |
| // other bid will be one of them which didn't win the bid. |
| TEST_F(AuctionRunnerBiddingAndScoringDebugReportingAPIEnabledTest, |
| ForDebuggingOnlyReportingMultipleTopBids) { |
| bool seen_ad2_win = false; |
| bool seen_ad3_win = false; |
| |
| while (!seen_ad2_win || !seen_ad3_win) { |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kBidder1Url, |
| MakeBidScriptSupportsTie()); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kBidder2Url, |
| MakeBidScriptSupportsTie()); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| (MakeAuctionScriptSupportsTie())); |
| |
| std::vector<StorageInterestGroup> bidders; |
| // Bid1 from kBidder1 gets second highest score. Bid2 from kBidder1 or bid3 |
| // from kBidder2 wins the auction. Integer values of interest group names |
| // are used as their bid values. |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, /*name=*/"1", kBidder1Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad1.com"))); |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, /*name=*/"3", kBidder1Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad2.com"))); |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder2, /*name=*/"4", kBidder2Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad3.com"))); |
| |
| RunAuctionAndWait(kSellerUrl, std::move(bidders)); |
| |
| EXPECT_EQ(4u, result_.debug_loss_report_urls.size()); |
| EXPECT_EQ(2u, result_.debug_win_report_urls.size()); |
| EXPECT_EQ(2u, result_.report_urls.size()); |
| |
| // Winner has ad2 or ad3. |
| if (result_.ad_descriptor->url == "https://ad2.com/") { |
| seen_ad2_win = true; |
| EXPECT_THAT( |
| result_.debug_loss_report_urls, |
| testing::UnorderedElementsAre( |
| DebugReportUrl( |
| kBidderDebugLossReportBaseUrl, |
| PostAuctionSignals(/*winning_bid=*/3, |
| /*made_winning_bid=*/true, |
| /*highest_scoring_other_bid=*/0, |
| /*made_highest_scoring_other_bid=*/false), |
| /*bid=*/1), |
| DebugReportUrl( |
| kBidderDebugLossReportBaseUrl, |
| PostAuctionSignals(/*winning_bid=*/3, |
| /*made_winning_bid=*/false, |
| /*highest_scoring_other_bid=*/0, |
| /*made_highest_scoring_other_bid=*/false), |
| /*bid=*/4), |
| DebugReportUrl( |
| kSellerDebugLossReportBaseUrl, |
| PostAuctionSignals(/*winning_bid=*/3, |
| /*made_winning_bid=*/true, |
| /*highest_scoring_other_bid=*/4, |
| /*made_highest_scoring_other_bid=*/false), |
| /*bid=*/1), |
| DebugReportUrl( |
| kSellerDebugLossReportBaseUrl, |
| PostAuctionSignals(/*winning_bid=*/3, |
| /*made_winning_bid=*/false, |
| /*highest_scoring_other_bid=*/4, |
| /*made_highest_scoring_other_bid=*/false), |
| /*bid=*/4))); |
| |
| EXPECT_THAT( |
| result_.debug_win_report_urls, |
| testing::UnorderedElementsAre( |
| DebugReportUrl( |
| kBidderDebugWinReportBaseUrl, |
| PostAuctionSignals(/*winning_bid=*/3, |
| /*made_winning_bid=*/true, |
| /*highest_scoring_other_bid=*/4, |
| /*made_highest_scoring_other_bid=*/false), |
| /*bid=*/3), |
| DebugReportUrl( |
| kSellerDebugWinReportBaseUrl, |
| PostAuctionSignals(/*winning_bid=*/3, |
| /*made_winning_bid=*/true, |
| /*highest_scoring_other_bid=*/4, |
| /*made_highest_scoring_other_bid=*/false), |
| /*bid=*/3))); |
| |
| EXPECT_THAT(result_.report_urls, |
| testing::UnorderedElementsAre( |
| GURL("https://reporting.example.com/" |
| "?highestScoringOtherBid=4&bid=3"), |
| ReportWinUrl(/*bid=*/3, /*highest_scoring_other_bid=*/4, |
| /*made_highest_scoring_other_bid=*/false))); |
| } else if (result_.ad_descriptor->url == "https://ad3.com/") { |
| seen_ad3_win = true; |
| EXPECT_THAT( |
| result_.debug_loss_report_urls, |
| testing::UnorderedElementsAre( |
| DebugReportUrl( |
| kBidderDebugLossReportBaseUrl, |
| PostAuctionSignals(/*winning_bid=*/4, |
| /*made_winning_bid=*/false, |
| /*highest_scoring_other_bid=*/0, |
| /*made_highest_scoring_other_bid=*/false), |
| /*bid=*/1), |
| DebugReportUrl( |
| kBidderDebugLossReportBaseUrl, |
| PostAuctionSignals(/*winning_bid=*/4, |
| /*made_winning_bid=*/false, |
| /*highest_scoring_other_bid=*/0, |
| /*made_highest_scoring_other_bid=*/false), |
| /*bid=*/3), |
| DebugReportUrl( |
| kSellerDebugLossReportBaseUrl, |
| PostAuctionSignals(/*winning_bid=*/4, |
| /*made_winning_bid=*/false, |
| /*highest_scoring_other_bid=*/3, |
| /*made_highest_scoring_other_bid=*/false), |
| /*bid=*/1), |
| DebugReportUrl( |
| kSellerDebugLossReportBaseUrl, |
| PostAuctionSignals(/*winning_bid=*/4, |
| /*made_winning_bid=*/false, |
| /*highest_scoring_other_bid=*/3, |
| /*made_highest_scoring_other_bid=*/false), |
| /*bid=*/3))); |
| |
| EXPECT_THAT( |
| result_.debug_win_report_urls, |
| testing::UnorderedElementsAre( |
| DebugReportUrl( |
| kBidderDebugWinReportBaseUrl, |
| PostAuctionSignals(/*winning_bid=*/4, |
| /*made_winning_bid=*/true, |
| /*highest_scoring_other_bid=*/3, |
| /*made_highest_scoring_other_bid=*/false), |
| /*bid=*/4), |
| DebugReportUrl( |
| kSellerDebugWinReportBaseUrl, |
| |
| PostAuctionSignals(/*winning_bid=*/4, |
| /*made_winning_bid=*/true, |
| /*highest_scoring_other_bid=*/3, |
| /*made_highest_scoring_other_bid=*/false), |
| /*bid=*/4))); |
| |
| EXPECT_THAT(result_.report_urls, |
| testing::UnorderedElementsAre( |
| GURL("https://reporting.example.com/" |
| "?highestScoringOtherBid=3&bid=4"), |
| ReportWinUrl(/*bid=*/4, /*highest_scoring_other_bid=*/3, |
| /*made_highest_scoring_other_bid=*/false))); |
| } else { |
| NOTREACHED(); |
| } |
| } |
| } |
| |
| // This is used to test post auction signals when an auction where bidders are |
| // from the same interest group owner. All winning bid and highest scoring other |
| // bids come from the same interest group owner. |
| TEST_F(AuctionRunnerBiddingAndScoringDebugReportingAPIEnabledTest, |
| ForDebuggingOnlyReportingSameOwnerBidders) { |
| // Seen bid1 or bid2 being picked as highest scoring other bid. |
| bool seen_bid1 = false; |
| bool seen_bid2 = false; |
| // Adding these different bidder URLs so that the order of finishes fetch and |
| // starts score is more arbitrary. Because highest scoring other bid picks |
| // the one scored last when there's a tie, so it's more easily and faster to |
| // reach both branches of the test. |
| const GURL kBidder1Url2{"https://adplatform.com/offers2.js"}; |
| const GURL kBidder1Url3{"https://adplatform.com/offers3.js"}; |
| |
| while (!seen_bid1 || !seen_bid2) { |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kBidder1Url, |
| MakeBidScriptSupportsTie()); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kBidder1Url2, |
| MakeBidScriptSupportsTie()); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kBidder1Url3, |
| MakeBidScriptSupportsTie()); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| MakeAuctionScriptSupportsTie()); |
| |
| std::vector<StorageInterestGroup> bidders; |
| // Both bid1 and bid2 from kBidder1 get second highest score. Bid3 from |
| // kBidder1 wins the auction. |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, /*name=*/"1", kBidder1Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad1.com"))); |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, /*name=*/"2", kBidder1Url2, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad2.com"))); |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, /*name=*/"3", kBidder1Url3, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad3.com"))); |
| |
| RunAuctionAndWait(kSellerUrl, std::move(bidders)); |
| |
| double highest_scoring_other_bid = 0.0; |
| if (base::Contains( |
| result_.report_urls, |
| "https://reporting.example.com/?highestScoringOtherBid=1&bid=3", |
| &GURL::spec)) { |
| highest_scoring_other_bid = 1; |
| } else if (base::Contains(result_.report_urls, |
| "https://reporting.example.com/" |
| "?highestScoringOtherBid=2&bid=3", |
| &GURL::spec)) { |
| highest_scoring_other_bid = 2; |
| } |
| |
| EXPECT_EQ(GURL("https://ad3.com/"), result_.ad_descriptor->url); |
| EXPECT_EQ(4u, result_.debug_loss_report_urls.size()); |
| EXPECT_EQ(2u, result_.debug_win_report_urls.size()); |
| EXPECT_EQ(2u, result_.report_urls.size()); |
| |
| if (highest_scoring_other_bid == 1) { |
| seen_bid1 = true; |
| EXPECT_THAT( |
| result_.debug_loss_report_urls, |
| testing::UnorderedElementsAre( |
| DebugReportUrl( |
| kBidderDebugLossReportBaseUrl, |
| PostAuctionSignals(/*winning_bid=*/3, |
| /*made_winning_bid=*/true, |
| /*highest_scoring_other_bid=*/0, |
| /*made_highest_scoring_other_bid=*/false), |
| /*bid=*/1), |
| DebugReportUrl( |
| kBidderDebugLossReportBaseUrl, |
| PostAuctionSignals(/*winning_bid=*/3, |
| /*made_winning_bid=*/true, |
| /*highest_scoring_other_bid=*/0, |
| /*made_highest_scoring_other_bid=*/false), |
| /*bid=*/2), |
| DebugReportUrl( |
| kSellerDebugLossReportBaseUrl, |
| PostAuctionSignals(/*winning_bid=*/3, |
| /*made_winning_bid=*/true, |
| /*highest_scoring_other_bid=*/1, |
| /*made_highest_scoring_other_bid=*/true), |
| /*bid=*/1), |
| DebugReportUrl( |
| kSellerDebugLossReportBaseUrl, |
| PostAuctionSignals(/*winning_bid=*/3, |
| /*made_winning_bid=*/true, |
| /*highest_scoring_other_bid=*/1, |
| /*made_highest_scoring_other_bid=*/true), |
| /*bid=*/2))); |
| |
| EXPECT_THAT( |
| result_.debug_win_report_urls, |
| testing::UnorderedElementsAre( |
| DebugReportUrl(kBidderDebugWinReportBaseUrl, |
| PostAuctionSignals( |
| /*winning_bid=*/3, |
| /*made_winning_bid=*/true, |
| /*highest_scoring_other_bid=*/1, |
| /*made_highest_scoring_other_bid=*/true), |
| /*bid=*/3), |
| DebugReportUrl(kSellerDebugWinReportBaseUrl, |
| PostAuctionSignals( |
| /*winning_bid=*/3, |
| /*made_winning_bid=*/true, |
| /*highest_scoring_other_bid=*/1, |
| /*made_highest_scoring_other_bid=*/true), |
| /*bid=*/3))); |
| |
| EXPECT_THAT(result_.report_urls, |
| testing::UnorderedElementsAre( |
| GURL("https://reporting.example.com/" |
| "?highestScoringOtherBid=1&bid=3"), |
| ReportWinUrl(/*bid=*/3, /*highest_scoring_other_bid=*/1, |
| /*made_highest_scoring_other_bid=*/true))); |
| } else if (highest_scoring_other_bid == 2) { |
| seen_bid2 = true; |
| EXPECT_THAT( |
| result_.debug_loss_report_urls, |
| testing::UnorderedElementsAre( |
| DebugReportUrl( |
| kBidderDebugLossReportBaseUrl, |
| PostAuctionSignals(/*winning_bid=*/3, |
| /*made_winning_bid=*/true, |
| /*highest_scoring_other_bid=*/0, |
| /*made_highest_scoring_other_bid=*/false), |
| /*bid=*/1), |
| DebugReportUrl( |
| kBidderDebugLossReportBaseUrl, |
| PostAuctionSignals(/*winning_bid=*/3, |
| /*made_winning_bid=*/true, |
| /*highest_scoring_other_bid=*/0, |
| /*made_highest_scoring_other_bid=*/false), |
| /*bid=*/2), |
| DebugReportUrl( |
| kSellerDebugLossReportBaseUrl, |
| PostAuctionSignals(/*winning_bid=*/3, |
| /*made_winning_bid=*/true, |
| /*highest_scoring_other_bid=*/2, |
| /*made_highest_scoring_other_bid=*/true), |
| /*bid=*/1), |
| DebugReportUrl( |
| kSellerDebugLossReportBaseUrl, |
| PostAuctionSignals(/*winning_bid=*/3, |
| /*made_winning_bid=*/true, |
| /*highest_scoring_other_bid=*/2, |
| /*made_highest_scoring_other_bid=*/true), |
| /*bid=*/2))); |
| |
| EXPECT_THAT( |
| result_.debug_win_report_urls, |
| testing::UnorderedElementsAre( |
| DebugReportUrl(kBidderDebugWinReportBaseUrl, |
| PostAuctionSignals( |
| /*winning_bid=*/3, |
| /*made_winning_bid=*/true, |
| /*highest_scoring_other_bid=*/2, |
| /*made_highest_scoring_other_bid=*/true), |
| /*bid=*/3), |
| DebugReportUrl(kSellerDebugWinReportBaseUrl, |
| PostAuctionSignals( |
| /*winning_bid=*/3, |
| /*made_winning_bid=*/true, |
| /*highest_scoring_other_bid=*/2, |
| /*made_highest_scoring_other_bid=*/true), |
| /*bid=*/3))); |
| |
| EXPECT_THAT(result_.report_urls, |
| testing::UnorderedElementsAre( |
| GURL("https://reporting.example.com/" |
| "?highestScoringOtherBid=2&bid=3"), |
| ReportWinUrl(/*bid=*/3, /*highest_scoring_other_bid=*/2, |
| /*made_highest_scoring_other_bid=*/true))); |
| } else { |
| NOTREACHED(); |
| } |
| } |
| } |
| |
| // Multiple bids from different interest group owners get the second highest |
| // score, then `${madeHighestScoringOtherBid}` is always false. |
| TEST_F(AuctionRunnerBiddingAndScoringDebugReportingAPIEnabledTest, |
| ForDebuggingOnlyReportingHighestScoringOtherBidFromDifferentOwners) { |
| // Seen bid1 or bid2 being picked as highest scoring other bid. |
| bool seen_bid1 = false; |
| bool seen_bid2 = false; |
| |
| while (!seen_bid1 || !seen_bid2) { |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kBidder1Url, |
| MakeBidScriptSupportsTie()); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kBidder2Url, |
| MakeBidScriptSupportsTie()); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| MakeAuctionScriptSupportsTie()); |
| |
| std::vector<StorageInterestGroup> bidders; |
| // Bidder1 and Bidder2 from different interest group owners both get second |
| // highest score. Bidder3 got the highest score and won the auction. |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, /*name=*/"1", kBidder1Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad1.com"))); |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder2, /*name=*/"2", kBidder2Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad2.com"))); |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, /*name=*/"3", kBidder1Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad3.com"))); |
| |
| RunAuctionAndWait(kSellerUrl, std::move(bidders)); |
| |
| EXPECT_EQ(GURL("https://ad3.com/"), result_.ad_descriptor->url); |
| EXPECT_EQ(4u, result_.debug_loss_report_urls.size()); |
| EXPECT_EQ(2u, result_.debug_win_report_urls.size()); |
| EXPECT_EQ(2u, result_.report_urls.size()); |
| double highest_scoring_other_bid = 0.0; |
| if (base::Contains( |
| result_.report_urls, |
| "https://reporting.example.com/?highestScoringOtherBid=1&bid=3", |
| &GURL::spec)) { |
| highest_scoring_other_bid = 1; |
| } else if (base::Contains(result_.report_urls, |
| "https://reporting.example.com/" |
| "?highestScoringOtherBid=2&bid=3", |
| &GURL::spec)) { |
| highest_scoring_other_bid = 2; |
| } |
| |
| if (highest_scoring_other_bid == 1) { |
| seen_bid1 = true; |
| EXPECT_THAT( |
| result_.debug_loss_report_urls, |
| testing::UnorderedElementsAre( |
| DebugReportUrl( |
| kBidderDebugLossReportBaseUrl, |
| PostAuctionSignals(/*winning_bid=*/3, |
| /*made_winning_bid=*/true, |
| /*highest_scoring_other_bid=*/0, |
| /*made_highest_scoring_other_bid=*/false), |
| /*bid=*/1), |
| DebugReportUrl( |
| kBidderDebugLossReportBaseUrl, |
| PostAuctionSignals(/*winning_bid=*/3, |
| /*made_winning_bid=*/false, |
| /*highest_scoring_other_bid=*/0, |
| /*made_highest_scoring_other_bid=*/false), |
| /*bid=*/2), |
| DebugReportUrl( |
| kSellerDebugLossReportBaseUrl, |
| PostAuctionSignals(/*winning_bid=*/3, |
| /*made_winning_bid=*/true, |
| /*highest_scoring_other_bid=*/1, |
| /*made_highest_scoring_other_bid=*/false), |
| /*bid=*/1), |
| DebugReportUrl( |
| kSellerDebugLossReportBaseUrl, |
| PostAuctionSignals(/*winning_bid=*/3, |
| /*made_winning_bid=*/false, |
| /*highest_scoring_other_bid=*/1, |
| /*made_highest_scoring_other_bid=*/false), |
| /*bid=*/2))); |
| |
| EXPECT_THAT( |
| result_.debug_win_report_urls, |
| testing::UnorderedElementsAre( |
| DebugReportUrl( |
| kBidderDebugWinReportBaseUrl, |
| PostAuctionSignals(/*winning_bid=*/3, |
| /*made_winning_bid=*/true, |
| /*highest_scoring_other_bid=*/1, |
| /*made_highest_scoring_other_bid=*/false), |
| /*bid=*/3), |
| DebugReportUrl( |
| kSellerDebugWinReportBaseUrl, |
| PostAuctionSignals(/*winning_bid=*/3, |
| /*made_winning_bid=*/true, |
| /*highest_scoring_other_bid=*/1, |
| /*made_highest_scoring_other_bid=*/false), |
| /*bid=*/3))); |
| |
| EXPECT_THAT(result_.report_urls, |
| testing::UnorderedElementsAre( |
| GURL("https://reporting.example.com/" |
| "?highestScoringOtherBid=1&bid=3"), |
| ReportWinUrl(/*bid=*/3, /*highest_scoring_other_bid=*/1, |
| /*made_highest_scoring_other_bid=*/false))); |
| } else if (highest_scoring_other_bid == 2) { |
| seen_bid2 = true; |
| EXPECT_THAT( |
| result_.debug_loss_report_urls, |
| testing::UnorderedElementsAre( |
| DebugReportUrl( |
| kBidderDebugLossReportBaseUrl, |
| PostAuctionSignals(/*winning_bid=*/3, |
| /*made_winning_bid=*/true, |
| /*highest_scoring_other_bid=*/0, |
| /*made_highest_scoring_other_bid=*/false), |
| /*bid=*/1), |
| DebugReportUrl( |
| kBidderDebugLossReportBaseUrl, |
| PostAuctionSignals(/*winning_bid=*/3, |
| /*made_winning_bid=*/false, |
| /*highest_scoring_other_bid=*/0, |
| /*made_highest_scoring_other_bid=*/false), |
| /*bid=*/2), |
| DebugReportUrl( |
| kSellerDebugLossReportBaseUrl, |
| PostAuctionSignals(/*winning_bid=*/3, |
| /*made_winning_bid=*/true, |
| /*highest_scoring_other_bid=*/2, |
| /*made_highest_scoring_other_bid=*/false), |
| /*bid=*/1), |
| DebugReportUrl( |
| kSellerDebugLossReportBaseUrl, |
| PostAuctionSignals(/*winning_bid=*/3, |
| /*made_winning_bid=*/false, |
| /*highest_scoring_other_bid=*/2, |
| /*made_highest_scoring_other_bid=*/false), |
| /*bid=*/2))); |
| |
| EXPECT_THAT( |
| result_.debug_win_report_urls, |
| testing::UnorderedElementsAre( |
| DebugReportUrl( |
| kBidderDebugWinReportBaseUrl, |
| PostAuctionSignals(/*winning_bid=*/3, |
| /*made_winning_bid=*/true, |
| /*highest_scoring_other_bid=*/2, |
| /*made_highest_scoring_other_bid=*/false), |
| /*bid=*/3), |
| DebugReportUrl( |
| kSellerDebugWinReportBaseUrl, |
| PostAuctionSignals(/*winning_bid=*/3, |
| /*made_winning_bid=*/true, |
| /*highest_scoring_other_bid=*/2, |
| /*made_highest_scoring_other_bid=*/false), |
| /*bid=*/3))); |
| |
| EXPECT_THAT(result_.report_urls, |
| testing::UnorderedElementsAre( |
| GURL("https://reporting.example.com/" |
| "?highestScoringOtherBid=2&bid=3"), |
| ReportWinUrl(/*bid=*/3, /*highest_scoring_other_bid=*/2, |
| /*made_highest_scoring_other_bid=*/false))); |
| } else { |
| NOTREACHED(); |
| } |
| } |
| } |
| |
| // Should send loss report to seller and bidders when auction fails due to |
| // AllBidsRejected. |
| TEST_F(AuctionRunnerBiddingAndScoringDebugReportingAPIEnabledTest, |
| ForDebuggingOnlyReportingAuctionFailAllBidsRejected) { |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder1Url, |
| MakeBidScript(kSeller, "1", "https://ad1.com/", /*num_ad_components=*/2, |
| kBidder1, kBidder1Name, |
| /*has_signals=*/false, "k1", "a", |
| /*report_post_auction_signals=*/true, |
| kBidder1DebugLossReportUrl, kBidder1DebugWinReportUrl, |
| /*report_reject_reason=*/true)); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder2Url, |
| MakeBidScript(kSeller, "2", "https://ad2.com/", /*num_ad_components=*/2, |
| kBidder2, kBidder2Name, |
| /*has_signals=*/false, "l2", "b", |
| /*report_post_auction_signals=*/true, |
| kBidder2DebugLossReportUrl, kBidder2DebugWinReportUrl, |
| /*report_reject_reason=*/true)); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kSellerUrl, |
| MakeAuctionScriptReject1And2WithDebugReporting( |
| base::StrCat( |
| {kSellerDebugLossReportBaseUrl, kPostAuctionSignalsPlaceholder}), |
| base::StrCat( |
| {kSellerDebugWinReportBaseUrl, kPostAuctionSignalsPlaceholder}))); |
| |
| RunStandardAuction(/*request_trusted_bidding_signals=*/false); |
| EXPECT_THAT(result_.errors, testing::UnorderedElementsAre()); |
| |
| // No winner since both bidders are rejected by seller. |
| EXPECT_FALSE(result_.winning_group_id); |
| EXPECT_FALSE(result_.ad_descriptor); |
| |
| EXPECT_EQ(4u, result_.debug_loss_report_urls.size()); |
| EXPECT_THAT( |
| result_.debug_loss_report_urls, |
| testing::UnorderedElementsAre( |
| DebugReportUrl(kBidder1DebugLossReportUrl, PostAuctionSignals(), |
| /*bid=*/absl::nullopt, "invalid-bid"), |
| DebugReportUrl(kBidder2DebugLossReportUrl, PostAuctionSignals(), |
| /*bid=*/absl::nullopt, "bid-below-auction-floor"), |
| DebugReportUrl(kSellerDebugLossReportBaseUrl, PostAuctionSignals(), |
| /*bid=*/1), |
| DebugReportUrl(kSellerDebugLossReportBaseUrl, PostAuctionSignals(), |
| /*bid=*/2))); |
| |
| EXPECT_EQ(0u, result_.debug_win_report_urls.size()); |
| } |
| |
| // Test win/loss reporting in a component auction with two components with one |
| // bidder each. |
| TEST_F(AuctionRunnerBiddingAndScoringDebugReportingAPIEnabledTest, |
| ForDebuggingOnlyReportingComponentAuctionTwoComponents) { |
| interest_group_buyers_.emplace(); |
| |
| component_auctions_.emplace_back( |
| CreateAuctionConfig(kComponentSeller1Url, {{kBidder1}})); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kComponentSeller1Url, |
| MakeDecisionScript( |
| kComponentSeller1Url, |
| /*send_report_url=*/GURL("https://component1-report.test/"), |
| /*bid_from_component_auction_wins=*/true, |
| /*report_post_auction_signals=*/true, |
| /*debug_loss_report_url=*/"https://component1-loss-reporting.test/", |
| /*debug_win_report_url=*/"https://component1-win-reporting.test/", |
| /*report_top_level_post_auction_signals*/ true)); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder1Url, |
| MakeBidScript(kComponentSeller1, "1", "https://ad1.com/", |
| /*num_ad_components=*/2, kBidder1, kBidder1Name, |
| /*has_signals=*/true, "k1", "a", |
| /*report_post_auction_signals=*/true, |
| kBidder1DebugLossReportUrl, kBidder1DebugWinReportUrl)); |
| auction_worklet::AddBidderJsonResponse( |
| &url_loader_factory_, |
| GURL(kBidder1TrustedSignalsUrl.spec() + |
| "?hostname=publisher1.com&keys=k1,k2" |
| "&interestGroupNames=Ad+Platform"), |
| kBidder1SignalsJson); |
| |
| component_auctions_.emplace_back( |
| CreateAuctionConfig(kComponentSeller2Url, {{kBidder2}})); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kComponentSeller2Url, |
| MakeDecisionScript( |
| kComponentSeller2Url, |
| /*send_report_url=*/GURL("https://component2-report.test/"), |
| /*bid_from_component_auction_wins=*/true, |
| /*report_post_auction_signals=*/true, |
| /*debug_loss_report_url=*/"https://component2-loss-reporting.test/", |
| /*debug_win_report_url=*/"https://component2-win-reporting.test/", |
| /*report_top_level_post_auction_signals*/ true)); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder2Url, |
| MakeBidScript(kComponentSeller2, "2", "https://ad2.com/", |
| /*num_ad_components=*/2, kBidder2, kBidder2Name, |
| /*has_signals=*/true, "l2", "b", |
| /*report_post_auction_signals=*/true, |
| kBidder2DebugLossReportUrl, kBidder2DebugWinReportUrl)); |
| auction_worklet::AddBidderJsonResponse( |
| &url_loader_factory_, |
| GURL(kBidder2TrustedSignalsUrl.spec() + |
| "?hostname=publisher1.com&keys=l1,l2" |
| "&interestGroupNames=Another+Ad+Thing"), |
| kBidder2SignalsJson); |
| |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kSellerUrl, |
| MakeDecisionScript( |
| kSellerUrl, |
| /*send_report_url=*/GURL("https://reporting.example.com"), |
| /*bid_from_component_auction_wins=*/true, |
| /*report_post_auction_signals=*/true, |
| /*debug_loss_report_url=*/"https://top-seller-loss-reporting.test/", |
| /*debug_win_report_url=*/"https://top-seller-win-reporting.test/")); |
| |
| RunStandardAuction(); |
| EXPECT_THAT(result_.errors, testing::UnorderedElementsAre()); |
| |
| // Bidder 2 won the auction. |
| EXPECT_EQ(GURL("https://ad2.com/"), result_.ad_descriptor->url); |
| |
| EXPECT_THAT( |
| result_.debug_loss_report_urls, |
| testing::UnorderedElementsAre( |
| DebugReportUrl(kBidder1DebugLossReportUrl, |
| PostAuctionSignals( |
| /*winning_bid=*/1, |
| /*made_winning_bid=*/true, |
| /*highest_scoring_other_bid=*/0, |
| /*made_highest_scoring_other_bid=*/false)), |
| ComponentSellerDebugReportUrl( |
| "https://component1-loss-reporting.test/", |
| /*signals=*/ |
| PostAuctionSignals(/*winning_bid=*/1, |
| /*made_winning_bid=*/true, |
| /*highest_scoring_other_bid=*/0, |
| /*made_highest_scoring_other_bid=*/false), |
| /*top_level_signals=*/ |
| PostAuctionSignals(/*winning_bid=*/2, |
| /*made_winning_bid=*/false), |
| /*bid=*/1), |
| DebugReportUrl("https://top-seller-loss-reporting.test/", |
| PostAuctionSignals(/*winning_bid=*/2, |
| /*made_winning_bid=*/false), |
| /*bid=*/1))); |
| |
| EXPECT_THAT( |
| result_.debug_win_report_urls, |
| testing::UnorderedElementsAre( |
| DebugReportUrl( |
| kBidder2DebugWinReportUrl, |
| PostAuctionSignals(/*winning_bid=*/2, |
| /*made_winning_bid=*/true, |
| /*highest_scoring_other_bid=*/0, |
| /*made_highest_scoring_other_bid=*/false)), |
| ComponentSellerDebugReportUrl( |
| "https://component2-win-reporting.test/", |
| /*signals=*/ |
| PostAuctionSignals(/*winning_bid=*/2, |
| /*made_winning_bid=*/true, |
| /*highest_scoring_other_bid=*/0, |
| /*made_highest_scoring_other_bid=*/false), |
| /*top_level_signals=*/ |
| PostAuctionSignals(/*winning_bid=*/2, |
| /*made_winning_bid=*/true), |
| /*bid=*/2), |
| DebugReportUrl("https://top-seller-win-reporting.test/", |
| PostAuctionSignals(/*winning_bid=*/2, |
| /*made_winning_bid=*/true), |
| /*bid=*/2))); |
| } |
| |
| // Test debug loss reporting in an auction with no winner. Component bidder 1 is |
| // rejected by component seller, and component bidder 2 is rejected by top-level |
| // seller. Component bidders get component auction's reject reason but not the |
| // top-level auction's. |
| TEST_F(AuctionRunnerBiddingAndScoringDebugReportingAPIEnabledTest, |
| ForDebuggingOnlyReportingComponentAuctionNoWinner) { |
| interest_group_buyers_.emplace(); |
| |
| component_auctions_.emplace_back( |
| CreateAuctionConfig(kComponentSeller1Url, {{kBidder1}})); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kComponentSeller1Url, |
| MakeAuctionScriptReject1And2WithDebugReporting( |
| "https://component1-loss-reporting.test/?", |
| "https://component1-win-reporting.test/?")); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder1Url, |
| MakeBidScript(kComponentSeller1, "1", "https://ad1.com/", |
| /*num_ad_components=*/2, kBidder1, kBidder1Name, |
| /*has_signals=*/true, "k1", "a", |
| /*report_post_auction_signals=*/true, |
| kBidder1DebugLossReportUrl, kBidder1DebugWinReportUrl, |
| /*report_reject_reason=*/true)); |
| auction_worklet::AddBidderJsonResponse( |
| &url_loader_factory_, |
| GURL(kBidder1TrustedSignalsUrl.spec() + |
| "?hostname=publisher1.com&keys=k1,k2" |
| "&interestGroupNames=Ad+Platform"), |
| kBidder1SignalsJson); |
| |
| component_auctions_.emplace_back( |
| CreateAuctionConfig(kComponentSeller2Url, {{kBidder2}})); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kComponentSeller2Url, |
| MakeDecisionScript( |
| kComponentSeller2Url, |
| /*send_report_url=*/GURL("https://component2-report.test/"), |
| /*bid_from_component_auction_wins=*/false, |
| /*report_post_auction_signals=*/true, |
| /*debug_loss_report_url=*/"https://component2-loss-reporting.test/", |
| /*debug_win_report_url=*/"https://component2-win-reporting.test/", |
| /*report_top_level_post_auction_signals*/ true)); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder2Url, |
| MakeBidScript(kComponentSeller2, "2", "https://ad2.com/", |
| /*num_ad_components=*/2, kBidder2, kBidder2Name, |
| /*has_signals=*/true, "l2", "b", |
| /*report_post_auction_signals=*/true, |
| kBidder2DebugLossReportUrl, kBidder2DebugWinReportUrl, |
| /*report_reject_reason=*/true)); |
| auction_worklet::AddBidderJsonResponse( |
| &url_loader_factory_, |
| GURL(kBidder2TrustedSignalsUrl.spec() + |
| "?hostname=publisher1.com&keys=l1,l2" |
| "&interestGroupNames=Another+Ad+Thing"), |
| kBidder2SignalsJson); |
| |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kSellerUrl, |
| base::StringPrintf(R"( |
| function scoreAd(adMetadata, bid, auctionConfig, trustedScoringSignals, |
| browserSignals) { |
| forDebuggingOnly.reportAdAuctionLoss( |
| "https://top-seller-loss-reporting.test/%s&bid=" + bid); |
| forDebuggingOnly.reportAdAuctionWin( |
| "https://top-seller-win-reporting.test/%s&bid=" + bid); |
| // While not setting `allowComponentAuction` will also reject the ad, it |
| // also prevents loss reports and adds an error message, so need to set |
| // it to true. |
| return { |
| desirability: 0, |
| allowComponentAuction: true, |
| rejectReason: "bid-below-auction-floor" |
| }; |
| } |
| )", |
| kPostAuctionSignalsPlaceholder, |
| kPostAuctionSignalsPlaceholder)); |
| |
| RunStandardAuction(); |
| EXPECT_THAT(result_.errors, testing::UnorderedElementsAre()); |
| |
| // No interest group won the auction. |
| EXPECT_FALSE(result_.ad_descriptor); |
| |
| // Component bidder 1 rejected by component auction gets its reject reason |
| // "invalid-bid". Component bidders don't get the top-level auction's reject |
| // reason. |
| EXPECT_THAT( |
| result_.debug_loss_report_urls, |
| testing::UnorderedElementsAre( |
| DebugReportUrl(kBidder1DebugLossReportUrl, |
| PostAuctionSignals( |
| /*winning_bid=*/0, |
| /*made_winning_bid=*/false, |
| /*highest_scoring_other_bid=*/0, |
| /*made_highest_scoring_other_bid=*/false), |
| /*bid=*/absl::nullopt, "invalid-bid"), |
| GURL("https://component1-loss-reporting.test/?&bid=1"), |
| DebugReportUrl(kBidder2DebugLossReportUrl, |
| PostAuctionSignals( |
| /*winning_bid=*/2, |
| /*made_winning_bid=*/true, |
| /*highest_scoring_other_bid=*/0, |
| /*made_highest_scoring_other_bid=*/false), |
| /*bid=*/absl::nullopt, "not-available"), |
| ComponentSellerDebugReportUrl( |
| "https://component2-loss-reporting.test/", |
| /*signals=*/ |
| PostAuctionSignals(/*winning_bid=*/2, |
| /*made_winning_bid=*/true, |
| /*highest_scoring_other_bid=*/0, |
| /*made_highest_scoring_other_bid=*/false), |
| /*top_level_signals=*/ |
| PostAuctionSignals(), |
| /*bid=*/2), |
| DebugReportUrl("https://top-seller-loss-reporting.test/", |
| PostAuctionSignals(), |
| /*bid=*/2))); |
| |
| EXPECT_THAT(result_.debug_win_report_urls, testing::UnorderedElementsAre()); |
| } |
| |
| // Test win/loss reporting in a component auction with one component with two |
| // bidders. |
| TEST_F(AuctionRunnerBiddingAndScoringDebugReportingAPIEnabledTest, |
| ForDebuggingOnlyReportingComponentAuctionOneComponent) { |
| interest_group_buyers_.emplace(); |
| |
| component_auctions_.emplace_back( |
| CreateAuctionConfig(kComponentSeller1Url, {{kBidder1, kBidder2}})); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kComponentSeller1Url, |
| MakeDecisionScript( |
| kComponentSeller1Url, |
| /*send_report_url=*/GURL("https://component-report.test/"), |
| /*bid_from_component_auction_wins=*/true, |
| /*report_post_auction_signals=*/true, |
| /*debug_loss_report_url=*/"https://component-loss-reporting.test/", |
| /*debug_win_report_url=*/"https://component-win-reporting.test/", |
| /*report_top_level_post_auction_signals*/ true)); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder1Url, |
| MakeBidScript(kComponentSeller1, "1", "https://ad1.com/", |
| /*num_ad_components=*/2, kBidder1, kBidder1Name, |
| /*has_signals=*/true, "k1", "a", |
| /*report_post_auction_signals=*/true, |
| kBidder1DebugLossReportUrl, kBidder1DebugWinReportUrl)); |
| auction_worklet::AddBidderJsonResponse( |
| &url_loader_factory_, |
| GURL(kBidder1TrustedSignalsUrl.spec() + |
| "?hostname=publisher1.com&keys=k1,k2" |
| "&interestGroupNames=Ad+Platform"), |
| kBidder1SignalsJson); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder2Url, |
| MakeBidScript(kComponentSeller1, "2", "https://ad2.com/", |
| /*num_ad_components=*/2, kBidder2, kBidder2Name, |
| /*has_signals=*/true, "l2", "b", |
| /*report_post_auction_signals=*/true, |
| kBidder2DebugLossReportUrl, kBidder2DebugWinReportUrl)); |
| auction_worklet::AddBidderJsonResponse( |
| &url_loader_factory_, |
| GURL(kBidder2TrustedSignalsUrl.spec() + |
| "?hostname=publisher1.com&keys=l1,l2" |
| "&interestGroupNames=Another+Ad+Thing"), |
| kBidder2SignalsJson); |
| |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kSellerUrl, |
| MakeDecisionScript( |
| kSellerUrl, |
| /*send_report_url=*/GURL("https://reporting.example.com"), |
| /*bid_from_component_auction_wins=*/true, |
| /*report_post_auction_signals=*/true, |
| /*debug_loss_report_url=*/"https://top-seller-loss-reporting.test/", |
| /*debug_win_report_url=*/"https://top-seller-win-reporting.test/")); |
| |
| RunStandardAuction(); |
| EXPECT_THAT(result_.errors, testing::UnorderedElementsAre()); |
| |
| // Bidder 1 won the auction, since component auctions give lower bidders |
| // higher desireability scores. |
| EXPECT_EQ(GURL("https://ad1.com/"), result_.ad_descriptor->url); |
| |
| EXPECT_THAT( |
| result_.debug_loss_report_urls, |
| testing::UnorderedElementsAre( |
| DebugReportUrl( |
| kBidder2DebugLossReportUrl, |
| PostAuctionSignals(/*winning_bid=*/1, |
| /*made_winning_bid=*/false, |
| /*highest_scoring_other_bid=*/0, |
| /*made_highest_scoring_other_bid=*/false)), |
| ComponentSellerDebugReportUrl( |
| "https://component-loss-reporting.test/", |
| /*signals=*/ |
| PostAuctionSignals(/*winning_bid=*/1, |
| /*made_winning_bid=*/false, |
| /*highest_scoring_other_bid=*/2, |
| /*made_highest_scoring_other_bid=*/true), |
| /*top_level_signals=*/ |
| PostAuctionSignals(/*winning_bid=*/1, |
| /*made_winning_bid=*/false), |
| /*bid=*/2))); |
| |
| EXPECT_THAT( |
| result_.debug_win_report_urls, |
| testing::UnorderedElementsAre( |
| DebugReportUrl(kBidder1DebugWinReportUrl, |
| PostAuctionSignals( |
| /*winning_bid=*/1, |
| /*made_winning_bid=*/true, |
| /*highest_scoring_other_bid=*/2, |
| /*made_highest_scoring_other_bid=*/false)), |
| ComponentSellerDebugReportUrl( |
| "https://component-win-reporting.test/", |
| /*signals=*/ |
| PostAuctionSignals(/*winning_bid=*/1, |
| /*made_winning_bid=*/true, |
| /*highest_scoring_other_bid=*/2, |
| /*made_highest_scoring_other_bid=*/false), |
| /*top_level_signals=*/ |
| PostAuctionSignals(/*winning_bid=*/1, |
| /*made_winning_bid=*/true), |
| /*bid=*/1), |
| DebugReportUrl("https://top-seller-win-reporting.test/", |
| PostAuctionSignals(/*winning_bid=*/1, |
| /*made_winning_bid=*/true), |
| /*bid=*/1))); |
| } |
| |
| // Loss report URLs should be dropped when the seller worklet fails to load. |
| TEST_F(AuctionRunnerBiddingAndScoringDebugReportingAPIEnabledTest, |
| ForDebuggingOnlyReportingSellerWorkletFailToLoad) { |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder1Url, |
| MakeBidScript(kSeller, "1", "https://ad1.com/", /*num_ad_components=*/2, |
| kBidder1, kBidder1Name, |
| /*has_signals=*/false, "k1", "a", |
| /*report_post_auction_signals*/ false, |
| kBidder1DebugLossReportUrl, kBidder1DebugWinReportUrl)); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder2Url, |
| MakeBidScript(kSeller, "2", "https://ad2.com/", /*num_ad_components=*/2, |
| kBidder2, kBidder2Name, |
| /*has_signals=*/false, "l2", "b", |
| /*report_post_auction_signals*/ false, |
| kBidder2DebugLossReportUrl, kBidder2DebugWinReportUrl)); |
| |
| StartStandardAuction(); |
| // Wait for the bids to be generated. |
| task_environment()->RunUntilIdle(); |
| // The seller script fails to load. |
| url_loader_factory_.AddResponse(kSellerUrl.spec(), "", net::HTTP_NOT_FOUND); |
| // Wait for the auction to complete. |
| auction_run_loop_->Run(); |
| |
| EXPECT_THAT(result_.errors, |
| testing::ElementsAre( |
| "Failed to load https://adstuff.publisher1.com/auction.js " |
| "HTTP status = 404 Not Found.")); |
| |
| // There should be no debug win report URLs. |
| EXPECT_EQ(0u, result_.debug_win_report_urls.size()); |
| // Bidders' debug loss report URLs should be dropped as well. |
| EXPECT_EQ(0u, result_.debug_loss_report_urls.size()); |
| } |
| |
| TEST_F(AuctionRunnerBiddingAndScoringDebugReportingAPIEnabledTest, |
| ForDebuggingOnlyReportingBidderBadUrls) { |
| const struct TestCase { |
| const char* expected_error_message; |
| absl::optional<GURL> bidder_debug_loss_report_url; |
| absl::optional<GURL> bidder_debug_win_report_url; |
| } kTestCases[] = { |
| { |
| "Invalid bidder debugging loss report URL", |
| GURL("http://bidder-debug-loss-report.com/"), |
| GURL("http://bidder-debug-win-report.com/"), |
| }, |
| { |
| "Invalid bidder debugging win report URL", |
| GURL("https://bidder-debug-loss-report.com/"), |
| GURL("http://bidder-debug-win-report.com/"), |
| }, |
| { |
| "Invalid bidder debugging loss report URL", |
| GURL("file:///foo/"), |
| GURL("https://bidder-debug-win-report.com/"), |
| }, |
| { |
| "Invalid bidder debugging loss report URL", |
| GURL("Not a URL"), |
| GURL("https://bidder-debug-win-report.com/"), |
| }, |
| }; |
| for (const auto& test_case : kTestCases) { |
| StartStandardAuctionWithMockService(); |
| auto seller_worklet = mock_auction_process_manager_->TakeSellerWorklet(); |
| ASSERT_TRUE(seller_worklet); |
| auto bidder1_worklet = |
| mock_auction_process_manager_->TakeBidderWorklet(kBidder1Url); |
| ASSERT_TRUE(bidder1_worklet); |
| auto bidder2_worklet = |
| mock_auction_process_manager_->TakeBidderWorklet(kBidder2Url); |
| ASSERT_TRUE(bidder2_worklet); |
| |
| // Only Bidder1 bids, to keep things simple. |
| bidder1_worklet->InvokeGenerateBidCallback( |
| /*bid=*/5, blink::AdDescriptor(GURL("https://ad1.com/")), |
| /*mojo_kanon_bid=*/nullptr, |
| /*ad_component_descriptors=*/absl::nullopt, base::TimeDelta(), |
| /*bidding_signals_data_version=*/absl::nullopt, |
| test_case.bidder_debug_loss_report_url, |
| test_case.bidder_debug_win_report_url); |
| bidder2_worklet->InvokeGenerateBidCallback(/*bid=*/absl::nullopt); |
| |
| // Since there's no acceptable bid, the seller worklet is never asked to |
| // score a bid. |
| auction_run_loop_->Run(); |
| EXPECT_EQ(test_case.expected_error_message, TakeBadMessage()); |
| |
| // No bidder won. |
| EXPECT_FALSE(result_.winning_group_id); |
| EXPECT_FALSE(result_.ad_descriptor); |
| EXPECT_THAT(result_.interest_groups_that_bid, |
| testing::UnorderedElementsAre()); |
| |
| EXPECT_EQ(0u, result_.debug_loss_report_urls.size()); |
| EXPECT_EQ(0u, result_.debug_win_report_urls.size()); |
| } |
| } |
| |
| TEST_F(AuctionRunnerBiddingAndScoringDebugReportingAPIEnabledTest, |
| ForDebuggingOnlyReportingSellerBadUrls) { |
| const struct TestCase { |
| const char* expected_error_message; |
| absl::optional<GURL> seller_debug_loss_report_url; |
| absl::optional<GURL> seller_debug_win_report_url; |
| } kTestCases[] = { |
| { |
| "Invalid seller debugging loss report URL", |
| GURL("http://seller-debug-loss-report.com/"), |
| GURL("http://seller-debug-win-report.com/"), |
| }, |
| { |
| "Invalid seller debugging win report URL", |
| GURL("https://seller-debug-loss-report.com/"), |
| GURL("http://seller-debug-win-report.com/"), |
| }, |
| { |
| "Invalid seller debugging loss report URL", |
| GURL("file:///foo/"), |
| GURL("https://seller-debug-win-report.com/"), |
| }, |
| { |
| "Invalid seller debugging loss report URL", |
| GURL("Not a URL"), |
| GURL("https://seller-debug-win-report.com/"), |
| }, |
| }; |
| for (const auto& test_case : kTestCases) { |
| StartStandardAuctionWithMockService(); |
| auto seller_worklet = mock_auction_process_manager_->TakeSellerWorklet(); |
| ASSERT_TRUE(seller_worklet); |
| auto bidder1_worklet = |
| mock_auction_process_manager_->TakeBidderWorklet(kBidder1Url); |
| ASSERT_TRUE(bidder1_worklet); |
| auto bidder2_worklet = |
| mock_auction_process_manager_->TakeBidderWorklet(kBidder2Url); |
| ASSERT_TRUE(bidder2_worklet); |
| |
| // Only Bidder1 bids, to keep things simple. |
| bidder1_worklet->InvokeGenerateBidCallback( |
| /*bid=*/5, blink::AdDescriptor(GURL("https://ad1.com/")), |
| /*mojo_kanon_bid=*/nullptr, |
| /*ad_component_descriptors=*/absl::nullopt, base::TimeDelta(), |
| /*bidding_signals_data_version=*/absl::nullopt, |
| GURL("https://bidder-debug-loss-report.com/"), |
| GURL("https://bidder-debug-win-report.com/")); |
| bidder2_worklet->InvokeGenerateBidCallback(/*bid=*/absl::nullopt); |
| |
| auto score_ad_params = seller_worklet->WaitForScoreAd(); |
| EXPECT_EQ(kBidder1, score_ad_params.interest_group_owner); |
| EXPECT_EQ(5, score_ad_params.bid); |
| mojo::Remote<auction_worklet::mojom::ScoreAdClient>( |
| std::move(score_ad_params.score_ad_client)) |
| ->OnScoreAdComplete( |
| /*score=*/10, |
| /*reject_reason=*/ |
| auction_worklet::mojom::RejectReason::kNotAvailable, |
| auction_worklet::mojom::ComponentAuctionModifiedBidParamsPtr(), |
| /*scoring_signals_data_version=*/0, |
| /*has_scoring_signals_data_version=*/false, |
| test_case.seller_debug_loss_report_url, |
| test_case.seller_debug_win_report_url, /*pa_requests=*/{}, |
| /*errors=*/{}); |
| auction_run_loop_->Run(); |
| EXPECT_EQ(test_case.expected_error_message, TakeBadMessage()); |
| |
| // No bidder won. |
| EXPECT_FALSE(result_.winning_group_id); |
| EXPECT_FALSE(result_.ad_descriptor); |
| EXPECT_THAT(result_.interest_groups_that_bid, |
| testing::UnorderedElementsAre()); |
| |
| EXPECT_EQ(0u, result_.debug_loss_report_urls.size()); |
| EXPECT_EQ(0u, result_.debug_win_report_urls.size()); |
| } |
| } |
| |
| TEST_F(AuctionRunnerBiddingAndScoringDebugReportingAPIEnabledTest, |
| ForDebuggingOnlyReportingGoodAndBadUrl) { |
| StartStandardAuctionWithMockService(); |
| auto seller_worklet = mock_auction_process_manager_->TakeSellerWorklet(); |
| ASSERT_TRUE(seller_worklet); |
| auto bidder1_worklet = |
| mock_auction_process_manager_->TakeBidderWorklet(kBidder1Url); |
| ASSERT_TRUE(bidder1_worklet); |
| auto bidder2_worklet = |
| mock_auction_process_manager_->TakeBidderWorklet(kBidder2Url); |
| ASSERT_TRUE(bidder2_worklet); |
| |
| // Bidder1 returns a bid, which is then scored. |
| bidder1_worklet->InvokeGenerateBidCallback( |
| /*bid=*/5, blink::AdDescriptor(GURL("https://ad1.com/")), |
| /*mojo_kanon_bid=*/nullptr, |
| /*ad_component_descriptors=*/absl::nullopt, base::TimeDelta(), |
| /*bidding_signals_data_version=*/absl::nullopt, |
| GURL(kBidder1DebugLossReportUrl), GURL(kBidder1DebugWinReportUrl)); |
| // The bidder pipe should be closed after it bids. |
| EXPECT_TRUE(bidder1_worklet->PipeIsClosed()); |
| bidder1_worklet.reset(); |
| EXPECT_EQ("", TakeBadMessage()); |
| |
| // Bidder2 returns a bid with an invalid debug report url. This could only |
| // happen when the bidder worklet is compromised. It will be filtered out |
| // and not be scored. |
| bidder2_worklet->InvokeGenerateBidCallback( |
| /*bid=*/10, blink::AdDescriptor(GURL("https://ad2.com/")), |
| /*mojo_kanon_bid=*/nullptr, |
| /*ad_component_descriptors=*/absl::nullopt, base::TimeDelta(), |
| /*bidding_signals_data_version=*/absl::nullopt, |
| GURL("http://not-https.com/"), GURL(kBidder2DebugWinReportUrl)); |
| // The bidder pipe should be closed after it bids. |
| EXPECT_TRUE(bidder2_worklet->PipeIsClosed()); |
| bidder2_worklet.reset(); |
| EXPECT_EQ("Invalid bidder debugging loss report URL", TakeBadMessage()); |
| |
| auto score_ad_params = seller_worklet->WaitForScoreAd(); |
| EXPECT_EQ(kBidder1, score_ad_params.interest_group_owner); |
| EXPECT_EQ(5, score_ad_params.bid); |
| mojo::Remote<auction_worklet::mojom::ScoreAdClient>( |
| std::move(score_ad_params.score_ad_client)) |
| ->OnScoreAdComplete( |
| /*score=*/10, |
| /*reject_reason=*/ |
| auction_worklet::mojom::RejectReason::kNotAvailable, |
| auction_worklet::mojom::ComponentAuctionModifiedBidParamsPtr(), |
| /*scoring_signals_data_version=*/0, |
| /*has_scoring_signals_data_version=*/false, |
| GURL("https://seller-debug-loss-reporting.com/1"), |
| GURL("https://seller-debug-win-reporting.com/1"), /*pa_requests=*/{}, |
| /*errors=*/{}); |
| |
| seller_worklet->WaitForReportResult(); |
| seller_worklet->InvokeReportResultCallback(); |
| mock_auction_process_manager_->WaitForWinningBidderReload(); |
| bidder1_worklet = |
| mock_auction_process_manager_->TakeBidderWorklet(kBidder1Url); |
| bidder1_worklet->WaitForReportWin(); |
| bidder1_worklet->InvokeReportWinCallback(); |
| auction_run_loop_->Run(); |
| |
| // Bidder1 won. Bidder2 was filtered out as an invalid bid because its debug |
| // loss report url is not a valid HTTPS URL. |
| EXPECT_EQ(kBidder1Key, result_.winning_group_id); |
| EXPECT_EQ(GURL("https://ad1.com/"), result_.ad_descriptor->url); |
| EXPECT_THAT(result_.interest_groups_that_bid, |
| testing::UnorderedElementsAre(kBidder1Key)); |
| EXPECT_EQ(R"({"render_url":"https://ad1.com/","metadata":{"ads": true}})", |
| result_.winning_group_ad_metadata); |
| |
| // Bidder2 lost, but debug_loss_report_urls is empty because bidder2's |
| // `debug_loss_report_url` is not a valid HTTPS URL. There's no seller debug |
| // loss report url neither because bidder2 was filtered out and its bid was |
| // not scored by seller. |
| EXPECT_EQ(0u, result_.debug_loss_report_urls.size()); |
| EXPECT_EQ(2u, result_.debug_win_report_urls.size()); |
| EXPECT_THAT(result_.debug_win_report_urls, |
| testing::UnorderedElementsAre( |
| kBidder1DebugWinReportUrl, |
| "https://seller-debug-win-reporting.com/1")); |
| } |
| |
| // This tests the component auction state machine in the case of a large |
| // component auction. It uses the debug reporting API just to make sure all |
| // scripts were run to completion. The main thing this test serves to do is to |
| // validate the component auction state machinery works (Waits for all bids to |
| // be generated/scored, doesn't abort them early, doesn't wait for extra bids). |
| TEST_F(AuctionRunnerBiddingAndScoringDebugReportingAPIEnabledTest, |
| LargeComponentAuction) { |
| const GURL kComponentSeller3Url{"https://component.seller3.test/baz.js"}; |
| |
| // Seller URLs and number of bidders for each Auction. |
| const struct { |
| GURL seller_url; |
| int num_bidders; |
| } kSellerInfo[] = { |
| // Top-level seller can't have any bidders. |
| {kSellerUrl, 0}, |
| {kComponentSeller1Url, 3}, |
| {kComponentSeller2Url, 5}, |
| {kComponentSeller3Url, 7}, |
| }; |
| |
| // Set up auction, including bidder and seller Javascript responses, |
| // AuctionConfig fields, etc. |
| size_t bidder_index = 1; |
| std::vector<StorageInterestGroup> all_bidders; |
| for (size_t i = 0; i < std::size(kSellerInfo); ++i) { |
| url::Origin seller = url::Origin::Create(kSellerInfo[i].seller_url); |
| GURL send_report_url = |
| GURL(base::StringPrintf("https://seller%zu.test/report/", i)); |
| GURL debug_loss_report_url = |
| GURL(base::StringPrintf("https://seller%zu.test/loss/", i)); |
| GURL debug_win_report_url = |
| GURL(base::StringPrintf("https://seller%zu.test/win/", i)); |
| |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kSellerInfo[i].seller_url, |
| MakeDecisionScript(kSellerInfo[i].seller_url, send_report_url, |
| /*bid_from_component_auction_wins=*/true, |
| /*report_post_auction_signals=*/false, |
| debug_loss_report_url.spec(), |
| debug_win_report_url.spec())); |
| |
| std::vector<url::Origin> bidders; |
| for (int j = 0; j < kSellerInfo[i].num_bidders; ++j, ++bidder_index) { |
| GURL bidder_url = GURL( |
| base::StringPrintf("https://bidder%zu.test/script.js", bidder_index)); |
| url::Origin bidder = url::Origin::Create(bidder_url); |
| GURL ad_url = |
| GURL(base::StringPrintf("https://bidder%zu.ad.test/", bidder_index)); |
| GURL bidder_debug_loss_report_url = GURL( |
| base::StringPrintf("https://bidder%zu.test/loss/", bidder_index)); |
| GURL bidder_debug_win_report_url = |
| GURL(base::StringPrintf("https://bidder%zu.test/win/", bidder_index)); |
| |
| all_bidders.emplace_back(MakeInterestGroup( |
| bidder, /*name=*/base::NumberToString(bidder_index), bidder_url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, ad_url, |
| /*ad_component_urls=*/absl::nullopt)); |
| |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, bidder_url, |
| MakeBidScript( |
| seller, /*bid=*/base::NumberToString(bidder_index), ad_url.spec(), |
| /*num_ad_components=*/0, bidder, |
| /*interest_group_name=*/base::NumberToString(bidder_index), |
| /*has_signals=*/false, /*signal_key=*/"", /*signal_val=*/"", |
| /*report_post_auction_signals=*/false, |
| bidder_debug_loss_report_url.spec(), |
| bidder_debug_win_report_url.spec())); |
| |
| bidders.push_back(bidder); |
| } |
| |
| // For the top-most auction, only need to set `interest_group_buyers_`. For |
| // others, need to append to `component_auctions_`. |
| if (kSellerInfo[i].seller_url == kSellerUrl) { |
| interest_group_buyers_ = std::move(bidders); |
| } else { |
| component_auctions_.emplace_back( |
| CreateAuctionConfig(kSellerInfo[i].seller_url, std::move(bidders))); |
| } |
| } |
| |
| StartAuction(kSellerInfo[0].seller_url, std::move(all_bidders)); |
| auction_run_loop_->Run(); |
| |
| EXPECT_THAT(result_.errors, testing::UnorderedElementsAre()); |
| |
| // Bidder 9 won - the first bidder for the third component auction. Higher |
| // bidders bid more, but component sellers use a script that favors lower |
| // bidders, while the top-level seller favors higher bidders. |
| EXPECT_EQ(GURL("https://bidder9.ad.test/"), result_.ad_descriptor->url); |
| |
| // Top seller doesn't report a loss, since it never saw the bid from the |
| // second bidder. |
| EXPECT_THAT(result_.debug_loss_report_urls, |
| testing::UnorderedElementsAre( |
| // kComponentSeller1's bidders. The first makes it to the |
| // top-level auction, the others do not. |
| GURL("https://bidder1.test/loss/"), |
| GURL("https://seller1.test/loss/1"), |
| GURL("https://seller0.test/loss/1"), |
| GURL("https://bidder2.test/loss/"), |
| GURL("https://seller1.test/loss/2"), |
| GURL("https://bidder3.test/loss/"), |
| GURL("https://seller1.test/loss/3"), |
| // kComponentSeller2's bidders. The first makes it to the |
| // top-level auction, the others do not. |
| GURL("https://bidder4.test/loss/"), |
| GURL("https://seller2.test/loss/4"), |
| GURL("https://seller0.test/loss/4"), |
| GURL("https://bidder5.test/loss/"), |
| GURL("https://seller2.test/loss/5"), |
| GURL("https://bidder6.test/loss/"), |
| GURL("https://seller2.test/loss/6"), |
| GURL("https://bidder7.test/loss/"), |
| GURL("https://seller2.test/loss/7"), |
| GURL("https://bidder8.test/loss/"), |
| GURL("https://seller2.test/loss/8"), |
| // kComponentSeller3's bidders. Bidder 9 won the entire |
| // auction, all the others lose component seller 3's auction. |
| GURL("https://bidder10.test/loss/"), |
| GURL("https://seller3.test/loss/10"), |
| GURL("https://bidder11.test/loss/"), |
| GURL("https://seller3.test/loss/11"), |
| GURL("https://bidder12.test/loss/"), |
| GURL("https://seller3.test/loss/12"), |
| GURL("https://bidder13.test/loss/"), |
| GURL("https://seller3.test/loss/13"), |
| GURL("https://bidder14.test/loss/"), |
| GURL("https://seller3.test/loss/14"), |
| GURL("https://bidder15.test/loss/"), |
| GURL("https://seller3.test/loss/15"))); |
| |
| EXPECT_THAT( |
| result_.debug_win_report_urls, |
| testing::UnorderedElementsAre(GURL("https://bidder9.test/win/"), |
| GURL("https://seller3.test/win/9"), |
| GURL("https://seller0.test/win/9"))); |
| } |
| |
| // Reject reason returned by scoreAd() for a rejected bid can be reported to the |
| // bidder through its debug loss report URL. |
| TEST_F(AuctionRunnerBiddingAndScoringDebugReportingAPIEnabledTest, |
| RejectedBidGetsRejectReason) { |
| for (const std::string& reject_reason : |
| {"not-available", "invalid-bid", "bid-below-auction-floor", |
| "pending-approval-by-exchange", "disapproved-by-exchange", |
| "blocked-by-publisher", "language-exclusions", "category-exclusions"}) { |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder1Url, |
| MakeBidScript(kSeller, "1", "https://ad1.com/", /*num_ad_components=*/2, |
| kBidder1, kBidder1Name, |
| /*has_signals=*/false, "k1", "a", |
| /*report_post_auction_signals=*/false, |
| kBidder1DebugLossReportUrl, kBidder1DebugWinReportUrl, |
| /*report_reject_reason=*/true)); |
| // Bidder 2 will get a negative score from scoreAd(). |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder2Url, |
| MakeBidScript(kSeller, "2", "https://ad2.com/", /*num_ad_components=*/2, |
| kBidder2, kBidder2Name, |
| /*has_signals=*/false, "l2", "b", |
| /*report_post_auction_signals=*/false, |
| kBidder2DebugLossReportUrl, kBidder2DebugWinReportUrl, |
| /*report_reject_reason=*/true)); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kSellerUrl, |
| MakeAuctionScriptReject2(reject_reason)); |
| |
| RunStandardAuction(/*request_trusted_bidding_signals=*/false); |
| EXPECT_THAT(result_.errors, testing::UnorderedElementsAre()); |
| |
| // Bidder 1 won the auction. |
| EXPECT_EQ(kBidder1Key, result_.winning_group_id); |
| EXPECT_EQ(GURL("https://ad1.com/"), result_.ad_descriptor->url); |
| |
| EXPECT_EQ(1u, result_.debug_loss_report_urls.size()); |
| // Seller rejected bidder 2 and returned the reject reason which were then |
| // reported to bidder 2 through its loss report URL. |
| EXPECT_THAT(result_.debug_loss_report_urls, |
| testing::UnorderedElementsAre(base::StringPrintf( |
| "https://bidder2-debug-loss-reporting.com/" |
| "?rejectReason=%s", |
| reject_reason.c_str()))); |
| |
| EXPECT_EQ(1u, result_.debug_win_report_urls.size()); |
| EXPECT_THAT(result_.debug_win_report_urls, |
| testing::UnorderedElementsAre(kBidder1DebugWinReportUrl)); |
| } |
| } |
| |
| // Reject reason returned by scoreAd() for a bid whose score is positive is |
| // ignored and will not be reported to the bidder. |
| TEST_F(AuctionRunnerBiddingAndScoringDebugReportingAPIEnabledTest, |
| RejectReasonIgnoredForPositiveBid) { |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder1Url, |
| MakeBidScript(kSeller, "1", "https://ad1.com/", /*num_ad_components=*/2, |
| kBidder1, kBidder1Name, |
| /*has_signals=*/false, "k1", "a", |
| /*report_post_auction_signals=*/false, |
| kBidder1DebugLossReportUrl, kBidder1DebugWinReportUrl, |
| /*report_reject_reason=*/true)); |
| |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder2Url, |
| MakeBidScript(kSeller, "3", "https://ad2.com/", /*num_ad_components=*/2, |
| kBidder2, kBidder2Name, |
| /*has_signals=*/false, "l2", "b", |
| /*report_post_auction_signals=*/false, |
| kBidder2DebugLossReportUrl, kBidder2DebugWinReportUrl, |
| /*report_reject_reason=*/true)); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| MakeAuctionScriptReject2()); |
| |
| RunStandardAuction(/*request_trusted_bidding_signals=*/false); |
| EXPECT_THAT(result_.errors, testing::UnorderedElementsAre()); |
| |
| // Bidder 2 won the auction. |
| EXPECT_EQ(kBidder2Key, result_.winning_group_id); |
| EXPECT_EQ(GURL("https://ad2.com/"), result_.ad_descriptor->url); |
| |
| EXPECT_EQ(1u, result_.debug_loss_report_urls.size()); |
| // Reject reason returned by scoreAd() for bidder 1 should be ignored and |
| // reported as "not-available" in debug loss report URL, because the bid gets |
| // a positive score thus not rejected by seller. |
| EXPECT_THAT( |
| result_.debug_loss_report_urls, |
| testing::UnorderedElementsAre("https://bidder1-debug-loss-reporting.com/" |
| "?rejectReason=not-available")); |
| |
| EXPECT_EQ(1u, result_.debug_win_report_urls.size()); |
| EXPECT_THAT(result_.debug_win_report_urls, |
| testing::UnorderedElementsAre(kBidder2DebugWinReportUrl)); |
| } |
| |
| // Only bidders' debug loss report URLs support macro ${rejectReason}. |
| // Bidders' debug win report URLs and sellers' debug loss/win report URLs does |
| // not. |
| TEST_F(AuctionRunnerBiddingAndScoringDebugReportingAPIEnabledTest, |
| RejectReasonInBidderDebugLossReportOnly) { |
| const char kBidder1Script[] = R"( |
| function generateBid(interestGroup, auctionSignals, perBuyerSignals, |
| trustedBiddingSignals, browserSignals) { |
| forDebuggingOnly.reportAdAuctionLoss( |
| 'https://bidder1-debug-loss-reporting.com/?reason=${rejectReason}'); |
| forDebuggingOnly.reportAdAuctionWin( |
| 'https://bidder1-debug-win-reporting.com/?reason=${rejectReason}'); |
| return { |
| bid: 1, |
| render: interestGroup.ads[0].renderUrl |
| }; |
| } |
| |
| // Prevent an error about this method not existing. |
| function reportWin() {} |
| )"; |
| |
| const char kBidder2Script[] = R"( |
| function generateBid(interestGroup, auctionSignals, perBuyerSignals, |
| trustedBiddingSignals, browserSignals) { |
| forDebuggingOnly.reportAdAuctionLoss( |
| 'https://bidder2-debug-loss-reporting.com/?reason=${rejectReason}'); |
| forDebuggingOnly.reportAdAuctionWin( |
| 'https://bidder2-debug-win-reporting.com/?reason=${rejectReason}'); |
| return { |
| bid: 2, |
| render: interestGroup.ads[0].renderUrl |
| }; |
| } |
| |
| // Prevent an error about this method not existing. |
| function reportWin() {} |
| )"; |
| |
| // Desirability is -1 if bid is 1, otherwise is bid. |
| const char kSellerScript[] = R"( |
| function scoreAd(adMetadata, bid, auctionConfig, trustedScoringSignals, |
| browserSignals) { |
| forDebuggingOnly.reportAdAuctionLoss( |
| 'https://seller-debug-loss-reporting.com/?reason=${rejectReason}'); |
| forDebuggingOnly.reportAdAuctionWin( |
| 'https://seller-debug-win-reporting.com/?reason=${rejectReason}'); |
| if (bid == 1) { |
| return {desirability: -1, rejectReason: 'invalid-bid'} |
| } else { |
| return bid; |
| } |
| } |
| |
| // Prevent an error about this method not existing. |
| function reportResult() {} |
| )"; |
| |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kBidder1Url, |
| kBidder1Script); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kBidder2Url, |
| kBidder2Script); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| kSellerScript); |
| |
| RunStandardAuction(/*request_trusted_bidding_signals=*/false); |
| EXPECT_THAT(result_.errors, testing::UnorderedElementsAre()); |
| |
| // Bidder 2 won the auction. |
| EXPECT_EQ(kBidder2Key, result_.winning_group_id); |
| EXPECT_EQ(GURL("https://ad2.com/"), result_.ad_descriptor->url); |
| |
| // Only bidder's debug loss report supports macro ${rejectReason}. |
| EXPECT_EQ(2u, result_.debug_loss_report_urls.size()); |
| EXPECT_THAT( |
| result_.debug_loss_report_urls, |
| testing::UnorderedElementsAre( |
| "https://bidder1-debug-loss-reporting.com/?reason=invalid-bid", |
| "https://seller-debug-loss-reporting.com/" |
| "?reason=${rejectReason}")); |
| EXPECT_EQ(2u, result_.debug_win_report_urls.size()); |
| EXPECT_THAT( |
| result_.debug_win_report_urls, |
| testing::UnorderedElementsAre("https://bidder2-debug-win-reporting.com/" |
| "?reason=${rejectReason}", |
| "https://seller-debug-win-reporting.com/" |
| "?reason=${rejectReason}")); |
| } |
| |
| // When scoreAd() does not return a reject reason, report it as "not-available" |
| // in bidder's loss report URL as default. |
| TEST_F(AuctionRunnerBiddingAndScoringDebugReportingAPIEnabledTest, |
| SellerNotReturningRejectReason) { |
| interest_group_buyers_ = {{kBidder1}}; |
| |
| const char kBidderScript[] = R"( |
| function generateBid(interestGroup, auctionSignals, perBuyerSignals, |
| trustedBiddingSignals, browserSignals) { |
| forDebuggingOnly.reportAdAuctionLoss( |
| 'https://bidder-debug-loss-reporting.com/?reason=${rejectReason}'); |
| return { |
| bid: 1, |
| render: interestGroup.ads[0].renderUrl |
| }; |
| } |
| |
| // Prevent an error about this method not existing. |
| function reportWin() {} |
| )"; |
| |
| const char kSellerScript[] = R"( |
| function scoreAd(adMetadata, bid, auctionConfig, trustedScoringSignals, |
| browserSignals) { |
| return {desirability: -1}; |
| } |
| |
| // Prevent an error about this method not existing. |
| function reportResult() {} |
| )"; |
| |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kBidder1Url, |
| kBidderScript); |
| auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl, |
| kSellerScript); |
| |
| RunStandardAuction(/*request_trusted_bidding_signals=*/false); |
| EXPECT_THAT(result_.errors, testing::UnorderedElementsAre()); |
| |
| EXPECT_EQ(1u, result_.debug_loss_report_urls.size()); |
| EXPECT_THAT( |
| result_.debug_loss_report_urls, |
| testing::UnorderedElementsAre("https://bidder-debug-loss-reporting.com/" |
| "?reason=not-available")); |
| EXPECT_EQ(0u, result_.debug_win_report_urls.size()); |
| } |
| |
| // Disable private aggregation API. |
| class AuctionRunnerPrivateAggregationAPIDisabledTest |
| : public AuctionRunnerTest { |
| public: |
| AuctionRunnerPrivateAggregationAPIDisabledTest() |
| : AuctionRunnerTest(/*should_enable_private_aggregation=*/false) {} |
| }; |
| |
| TEST_F(AuctionRunnerPrivateAggregationAPIDisabledTest, ReportsNotSent) { |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder1Url, |
| MakeBidScript(kSeller, "1", "https://ad1.com/", /*num_ad_components=*/2, |
| kBidder1, kBidder1Name, |
| /*has_signals=*/true, "k1", "a", |
| /*report_post_auction_signals=*/true)); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder2Url, |
| MakeBidScript(kSeller, "2", "https://ad2.com/", /*num_ad_components=*/2, |
| kBidder2, kBidder2Name, |
| /*has_signals=*/true, "l2", "b", |
| /*report_post_auction_signals=*/true)); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kSellerUrl, |
| MakeAuctionScript(/*report_post_auction_signals=*/true)); |
| auction_worklet::AddBidderJsonResponse( |
| &url_loader_factory_, |
| GURL(kBidder1TrustedSignalsUrl.spec() + |
| "?hostname=publisher1.com&keys=k1,k2"), |
| kBidder1SignalsJson); |
| auction_worklet::AddBidderJsonResponse( |
| &url_loader_factory_, |
| GURL(kBidder2TrustedSignalsUrl.spec() + |
| "?hostname=publisher1.com&keys=l1,l2"), |
| kBidder2SignalsJson); |
| |
| RunStandardAuction(); |
| EXPECT_TRUE( |
| private_aggregation_manager_.TakePrivateAggregationRequests().empty()); |
| EXPECT_TRUE(result_.private_aggregation_event_map.empty()); |
| } |
| |
| class AuctionRunnerKAnonTest : public AuctionRunnerTest, |
| public ::testing::WithParamInterface< |
| auction_worklet::mojom::KAnonymityBidMode> { |
| public: |
| AuctionRunnerKAnonTest() |
| : AuctionRunnerTest( |
| /*should_enable_private_aggregation=*/true, |
| /*should_enable_private_aggregation_fledge_extension=*/true, |
| kanon_mode()) {} |
| |
| auction_worklet::mojom::KAnonymityBidMode kanon_mode() { return GetParam(); } |
| }; |
| |
| TEST_P(AuctionRunnerKAnonTest, SingleNonKAnon) { |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder1Url, |
| MakeConstBidScript(1, "https://ad1.com") + kReportWinNoUrl); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kSellerUrl, |
| std::string(kMinimumDecisionScript) + kBasicReportResult); |
| |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, kBidder1Name, kBidder1Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad1.com"))); |
| |
| // No k-anon authorizations. |
| StartAuction(kSellerUrl, bidders); |
| auction_run_loop_->Run(); |
| // Have to spin all message loops to flush any k-anon set join events. |
| task_environment()->RunUntilIdle(); |
| EXPECT_THAT(interest_group_manager_->TakeJoinedKAnonSets(), |
| testing::UnorderedElementsAre( |
| blink::KAnonKeyForAdBid( |
| bidders[0].interest_group, |
| bidders[0].interest_group.ads.value()[0].render_url), |
| blink::KAnonKeyForAdNameReporting( |
| bidders[0].interest_group, |
| bidders[0].interest_group.ads.value()[0]))); |
| histogram_tester_->ExpectUniqueSample( |
| "Ads.InterestGroup.Auction.NonKAnonWinnerIsKAnon", false, 1); |
| switch (kanon_mode()) { |
| case auction_worklet::mojom::KAnonymityBidMode::kNone: |
| ASSERT_TRUE(result_.ad_descriptor.has_value()); |
| EXPECT_EQ(GURL("https://ad1.com"), result_.ad_descriptor->url); |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| break; |
| |
| case auction_worklet::mojom::KAnonymityBidMode::kEnforce: |
| EXPECT_FALSE(result_.ad_descriptor.has_value()); |
| EXPECT_THAT( |
| result_.errors, |
| testing::ElementsAre( |
| "https://adplatform.com/offers.js generateBid() bid render URL " |
| "'https://ad1.com/' isn't one of the registered creative URLs.")); |
| break; |
| |
| case auction_worklet::mojom::KAnonymityBidMode::kSimulate: |
| ASSERT_TRUE(result_.ad_descriptor.has_value()); |
| EXPECT_EQ(GURL("https://ad1.com"), result_.ad_descriptor->url); |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| break; |
| } |
| } |
| |
| TEST_P(AuctionRunnerKAnonTest, SingleKAnon) { |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder1Url, |
| MakeConstBidScript(1, "https://ad1.com") + kReportWinNoUrl); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kSellerUrl, |
| std::string(kMinimumDecisionScript) + kBasicReportResult); |
| |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, kBidder1Name, kBidder1Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad1.com/"))); |
| |
| // Authorize the ad. |
| AuthorizeKAnonAd(bidders[0].interest_group.ads.value()[0], "https://ad1.com/", |
| bidders[0]); |
| |
| StartAuction(kSellerUrl, bidders); |
| auction_run_loop_->Run(); |
| |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| ASSERT_TRUE(result_.ad_descriptor.has_value()); |
| EXPECT_EQ(GURL("https://ad1.com"), result_.ad_descriptor->url); |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| // Have to spin all message loops to flush any k-anon set join events. |
| task_environment()->RunUntilIdle(); |
| EXPECT_THAT(interest_group_manager_->TakeJoinedKAnonSets(), |
| testing::UnorderedElementsAre( |
| blink::KAnonKeyForAdBid( |
| bidders[0].interest_group, |
| bidders[0].interest_group.ads.value()[0].render_url), |
| blink::KAnonKeyForAdNameReporting( |
| bidders[0].interest_group, |
| bidders[0].interest_group.ads.value()[0]))); |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| histogram_tester_->ExpectUniqueSample( |
| "Ads.InterestGroup.Auction.NonKAnonWinnerIsKAnon", |
| kanon_mode() != auction_worklet::mojom::KAnonymityBidMode::kNone, 1); |
| } |
| |
| // Test that k-anonymity for ads with ad components is handled correctly: |
| // - All components must be k-anonymous to be eligible. |
| // - All components of the winner will be reported as joined. |
| // Runs an auction with two groups where each gives a bid with two component ads |
| // and all ad URLs except one component ad URL of the second bidder are |
| // k-anonymous. When k-anonymity is enforced the first interest group should |
| // win, despite having a lower bid. |
| TEST_P(AuctionRunnerKAnonTest, ComponentURLs) { |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder1Url, |
| MakeFilteringBidScript(1) + kSimpleReportWin); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder2Url, |
| MakeFilteringBidScript(2) + kSimpleReportWin); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kSellerUrl, |
| std::string(kMinimumDecisionScript) + kBasicReportResult); |
| |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, kBidder1Name, kBidder1Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad1.com"), |
| absl::make_optional(std::vector<GURL>( |
| {GURL("https://ad1.com/1"), GURL("https://ad1.com/2")})))); |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder2, kBidder2Name, kBidder2Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad2.com"), |
| absl::make_optional(std::vector<GURL>( |
| {GURL("https://ad2.com/1"), GURL("https://ad2.com/2")})))); |
| |
| // Authorize everything except for one of the components in ad2. |
| AuthorizeKAnonAd(bidders[0].interest_group.ads.value()[0], "https://ad1.com/", |
| bidders[0]); |
| AuthorizeKAnonAdComponent(bidders[0].interest_group.ad_components.value()[0], |
| "https://ad1.com/1", bidders[0]); |
| AuthorizeKAnonAdComponent(bidders[0].interest_group.ad_components.value()[1], |
| "https://ad1.com/2", bidders[0]); |
| AuthorizeKAnonAd(bidders[1].interest_group.ads.value()[0], "https://ad2.com/", |
| bidders[1]); |
| AuthorizeKAnonAdComponent(bidders[1].interest_group.ad_components.value()[0], |
| "https://ad2.com/1", bidders[1]); |
| |
| std::vector<std::string> ad1_k_anon_keys = { |
| blink::KAnonKeyForAdBid( |
| bidders[0].interest_group, |
| bidders[0].interest_group.ads.value()[0].render_url), |
| blink::KAnonKeyForAdNameReporting( |
| bidders[0].interest_group, bidders[0].interest_group.ads.value()[0]), |
| blink::KAnonKeyForAdComponentBid( |
| bidders[0].interest_group.ad_components.value()[0].render_url), |
| blink::KAnonKeyForAdComponentBid( |
| bidders[0].interest_group.ad_components.value()[1].render_url), |
| }; |
| |
| std::vector<std::string> ad2_k_anon_keys = { |
| blink::KAnonKeyForAdBid( |
| bidders[1].interest_group, |
| bidders[1].interest_group.ads.value()[0].render_url), |
| blink::KAnonKeyForAdNameReporting( |
| bidders[1].interest_group, bidders[1].interest_group.ads.value()[0]), |
| blink::KAnonKeyForAdComponentBid( |
| bidders[1].interest_group.ad_components.value()[0].render_url), |
| blink::KAnonKeyForAdComponentBid( |
| bidders[1].interest_group.ad_components.value()[1].render_url), |
| }; |
| |
| for (bool run_as_component : {false, true}) { |
| SCOPED_TRACE(run_as_component); |
| |
| if (run_as_component) { |
| component_auctions_.emplace_back( |
| CreateAuctionConfig(kSellerUrl, {{kBidder1, kBidder2}})); |
| interest_group_buyers_->clear(); |
| } else { |
| DCHECK(!interest_group_buyers_->empty()); |
| } |
| |
| StartAuction(kSellerUrl, bidders); |
| auction_run_loop_->Run(); |
| ASSERT_TRUE(result_.ad_descriptor.has_value()); |
| |
| GURL expected_seller_report_url; |
| std::vector<GURL> expected_report_urls; |
| base::flat_set<std::string> expected_k_anon_keys_to_join; |
| histogram_tester_->ExpectUniqueSample( |
| "Ads.InterestGroup.Auction.NonKAnonWinnerIsKAnon", false, 1); |
| switch (kanon_mode()) { |
| case auction_worklet::mojom::KAnonymityBidMode::kNone: |
| // k-anon support is turned off entirely, so ad2 wins, and no other URLs |
| // are set. |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| EXPECT_EQ(GURL("https://ad2.com"), result_.ad_descriptor->url); |
| EXPECT_THAT(result_.ad_component_descriptors, |
| testing::UnorderedElementsAre( |
| blink::AdDescriptor(GURL("https://ad2.com/1")), |
| blink::AdDescriptor(GURL("https://ad2.com/2")))); |
| // Only join for ad2 |
| expected_k_anon_keys_to_join.insert(ad2_k_anon_keys.begin(), |
| ad2_k_anon_keys.end()); |
| |
| expected_seller_report_url = GURL("https://reporting.example.com/2"); |
| expected_report_urls.push_back( |
| ReportWinUrl(/*bid=*/2, /*highest_scoring_other_bid=*/1, |
| /*made_highest_scoring_other_bid=*/false)); |
| break; |
| |
| case auction_worklet::mojom::KAnonymityBidMode::kEnforce: |
| // k-anon requirement means ad1 wins, but we also report ad2 as what |
| // would have won had it been authorized. |
| EXPECT_THAT(result_.errors, |
| testing::ElementsAre( |
| "https://anotheradthing.com/bids.js generateBid() bid " |
| "adComponents URL 'https://ad2.com/2' isn't one of the " |
| "registered creative URLs.")); |
| EXPECT_EQ(GURL("https://ad1.com"), result_.ad_descriptor->url); |
| EXPECT_THAT(result_.ad_component_descriptors, |
| testing::UnorderedElementsAre( |
| blink::AdDescriptor(GURL("https://ad1.com/1")), |
| blink::AdDescriptor(GURL("https://ad1.com/2")))); |
| |
| expected_k_anon_keys_to_join.insert(ad1_k_anon_keys.begin(), |
| ad1_k_anon_keys.end()); |
| expected_k_anon_keys_to_join.insert(ad2_k_anon_keys.begin(), |
| ad2_k_anon_keys.end()); |
| expected_seller_report_url = GURL("https://reporting.example.com/1"); |
| expected_report_urls.push_back( |
| ReportWinUrl(/*bid=*/1, /*highest_scoring_other_bid=*/0, |
| /*made_highest_scoring_other_bid=*/false)); |
| break; |
| |
| case auction_worklet::mojom::KAnonymityBidMode::kSimulate: |
| // Winner is ad2.com, disregarding k-anonymity, but we also report that |
| // if we did care about it, ad1.com would have won. |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| EXPECT_EQ(GURL("https://ad2.com"), result_.ad_descriptor->url); |
| EXPECT_THAT(result_.ad_component_descriptors, |
| testing::UnorderedElementsAre( |
| blink::AdDescriptor(GURL("https://ad2.com/1")), |
| blink::AdDescriptor(GURL("https://ad2.com/2")))); |
| |
| expected_k_anon_keys_to_join.insert(ad1_k_anon_keys.begin(), |
| ad1_k_anon_keys.end()); |
| expected_k_anon_keys_to_join.insert(ad2_k_anon_keys.begin(), |
| ad2_k_anon_keys.end()); |
| expected_seller_report_url = GURL("https://reporting.example.com/2"); |
| expected_report_urls.push_back( |
| ReportWinUrl(/*bid=*/2, /*highest_scoring_other_bid=*/1, |
| /*made_highest_scoring_other_bid=*/false)); |
| break; |
| } |
| |
| // Have to spin all message loops to flush any k-anon set join events. |
| task_environment()->RunUntilIdle(); |
| EXPECT_THAT( |
| interest_group_manager_->TakeJoinedKAnonSets(), |
| testing::UnorderedElementsAreArray(expected_k_anon_keys_to_join)); |
| |
| expected_report_urls.push_back(expected_seller_report_url); |
| if (run_as_component) { |
| // Both top-level and component auction report this. |
| expected_report_urls.push_back(expected_seller_report_url); |
| } |
| EXPECT_THAT(result_.report_urls, |
| testing::UnorderedElementsAreArray(expected_report_urls)); |
| } |
| } |
| |
| // Test that if there are two ads, one k-anonymous and one not k-anonymous that |
| // the correct ad is the winner (depends on `kanon_mode()`). Note that the |
| // non-k-anonymous ad bids higher so that it wins when k-anonymity is not |
| // enforced. |
| TEST_P(AuctionRunnerKAnonTest, Basic) { |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder1Url, |
| MakeFilteringBidScript(1) + kSimpleReportWin); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder2Url, |
| MakeFilteringBidScript(2) + kSimpleReportWin); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kSellerUrl, |
| std::string(kMinimumDecisionScript) + kBasicReportResult); |
| |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, kBidder1Name, kBidder1Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad1.com"))); |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder2, kBidder2Name, kBidder2Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad2.com"))); |
| |
| // Authorize only ad 1. |
| AuthorizeKAnonAd(bidders[0].interest_group.ads.value()[0], "https://ad1.com/", |
| bidders[0]); |
| |
| std::vector<std::string> ad1_k_anon_keys = { |
| blink::KAnonKeyForAdBid( |
| bidders[0].interest_group, |
| bidders[0].interest_group.ads.value()[0].render_url), |
| blink::KAnonKeyForAdNameReporting( |
| bidders[0].interest_group, bidders[0].interest_group.ads.value()[0]), |
| }; |
| std::vector<std::string> ad2_k_anon_keys = { |
| blink::KAnonKeyForAdBid( |
| bidders[1].interest_group, |
| bidders[1].interest_group.ads.value()[0].render_url), |
| blink::KAnonKeyForAdNameReporting( |
| bidders[1].interest_group, bidders[1].interest_group.ads.value()[0]), |
| }; |
| |
| for (bool run_as_component : {false, true}) { |
| SCOPED_TRACE(run_as_component); |
| |
| if (run_as_component) { |
| component_auctions_.emplace_back( |
| CreateAuctionConfig(kSellerUrl, {{kBidder1, kBidder2}})); |
| interest_group_buyers_->clear(); |
| } else { |
| DCHECK(!interest_group_buyers_->empty()); |
| } |
| |
| StartAuction(kSellerUrl, bidders); |
| auction_run_loop_->Run(); |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| ASSERT_TRUE(result_.ad_descriptor.has_value()); |
| histogram_tester_->ExpectUniqueSample( |
| "Ads.InterestGroup.Auction.NonKAnonWinnerIsKAnon", false, 1); |
| |
| base::flat_set<std::string> expected_k_anon_keys_to_join; |
| GURL expected_seller_report_url; |
| std::vector<GURL> expected_report_urls; |
| switch (kanon_mode()) { |
| case auction_worklet::mojom::KAnonymityBidMode::kNone: |
| // k-anon support is turned off entirely, so ad2 wins, and no other URLs |
| // are set. |
| EXPECT_EQ(GURL("https://ad2.com"), result_.ad_descriptor->url); |
| expected_k_anon_keys_to_join.insert(ad2_k_anon_keys.begin(), |
| ad2_k_anon_keys.end()); |
| expected_seller_report_url = GURL("https://reporting.example.com/2"); |
| expected_report_urls.push_back( |
| ReportWinUrl(/*bid=*/2, /*highest_scoring_other_bid=*/1, |
| /*made_highest_scoring_other_bid=*/false)); |
| break; |
| |
| case auction_worklet::mojom::KAnonymityBidMode::kEnforce: |
| // k-anon requirement meands ad1 wins, but we also report ad2 as what |
| // would have won had it been authorized. |
| EXPECT_EQ(GURL("https://ad1.com"), result_.ad_descriptor->url); |
| expected_k_anon_keys_to_join.insert(ad1_k_anon_keys.begin(), |
| ad1_k_anon_keys.end()); |
| expected_k_anon_keys_to_join.insert(ad2_k_anon_keys.begin(), |
| ad2_k_anon_keys.end()); |
| expected_seller_report_url = GURL("https://reporting.example.com/1"); |
| expected_report_urls.push_back( |
| ReportWinUrl(/*bid=*/1, /*highest_scoring_other_bid=*/0, |
| /*made_highest_scoring_other_bid=*/false)); |
| break; |
| |
| case auction_worklet::mojom::KAnonymityBidMode::kSimulate: |
| // Winner is ad2.com, disregarding k-anonymity, but we also report that |
| // if we did care about it, ad1.com would have won. |
| EXPECT_EQ(GURL("https://ad2.com"), result_.ad_descriptor->url); |
| expected_k_anon_keys_to_join.insert(ad1_k_anon_keys.begin(), |
| ad1_k_anon_keys.end()); |
| expected_k_anon_keys_to_join.insert(ad2_k_anon_keys.begin(), |
| ad2_k_anon_keys.end()); |
| expected_seller_report_url = GURL("https://reporting.example.com/2"); |
| expected_report_urls.push_back( |
| ReportWinUrl(/*bid=*/2, /*highest_scoring_other_bid=*/1, |
| /*made_highest_scoring_other_bid=*/false)); |
| break; |
| } |
| // Have to spin all message loops to flush any k-anon set join events. |
| task_environment()->RunUntilIdle(); |
| EXPECT_THAT( |
| interest_group_manager_->TakeJoinedKAnonSets(), |
| testing::UnorderedElementsAreArray(expected_k_anon_keys_to_join)); |
| |
| expected_report_urls.push_back(expected_seller_report_url); |
| if (run_as_component) { |
| // Both top-level and component auction report this. |
| expected_report_urls.push_back(expected_seller_report_url); |
| } |
| EXPECT_THAT(result_.report_urls, |
| testing::UnorderedElementsAreArray(expected_report_urls)); |
| } |
| } |
| |
| // Test where the k-anon ad has a higher bid. |
| TEST_P(AuctionRunnerKAnonTest, KAnonHigher) { |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder1Url, |
| MakeFilteringBidScript(2) + kSimpleReportWin); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder2Url, |
| MakeFilteringBidScript(1) + kSimpleReportWin); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kSellerUrl, |
| std::string(kMinimumDecisionScript) + kBasicReportResult); |
| |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, kBidder1Name, kBidder1Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad1.com"))); |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder2, kBidder2Name, kBidder2Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad2.com"))); |
| |
| // Authorize only ad 1. |
| AuthorizeKAnonAd(bidders[0].interest_group.ads.value()[0], "https://ad1.com/", |
| bidders[0]); |
| |
| std::vector<std::string> ad1_k_anon_keys = { |
| blink::KAnonKeyForAdBid( |
| bidders[0].interest_group, |
| bidders[0].interest_group.ads.value()[0].render_url), |
| blink::KAnonKeyForAdNameReporting( |
| bidders[0].interest_group, bidders[0].interest_group.ads.value()[0]), |
| }; |
| |
| StartAuction(kSellerUrl, bidders); |
| auction_run_loop_->Run(); |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| ASSERT_TRUE(result_.ad_descriptor.has_value()); |
| EXPECT_EQ(GURL("https://ad1.com"), result_.ad_descriptor->url); |
| // Have to spin all message loops to flush any k-anon set join events. |
| task_environment()->RunUntilIdle(); |
| EXPECT_THAT(interest_group_manager_->TakeJoinedKAnonSets(), |
| testing::UnorderedElementsAreArray(ad1_k_anon_keys)); |
| |
| std::vector<GURL> expected_report_urls; |
| expected_report_urls.emplace_back("https://reporting.example.com/2"); |
| switch (kanon_mode()) { |
| case auction_worklet::mojom::KAnonymityBidMode::kNone: |
| // k-anon support is turned off entirely, so no other URLs |
| // are set. |
| histogram_tester_->ExpectUniqueSample( |
| "Ads.InterestGroup.Auction.NonKAnonWinnerIsKAnon", false, 1); |
| expected_report_urls.push_back( |
| ReportWinUrl(/*bid=*/2, /*highest_scoring_other_bid=*/1, |
| /*made_highest_scoring_other_bid=*/false)); |
| break; |
| |
| case auction_worklet::mojom::KAnonymityBidMode::kEnforce: |
| // The enforced winner is the same, but there is no runner-up. |
| histogram_tester_->ExpectUniqueSample( |
| "Ads.InterestGroup.Auction.NonKAnonWinnerIsKAnon", true, 1); |
| expected_report_urls.push_back( |
| ReportWinUrl(/*bid=*/2, /*highest_scoring_other_bid=*/0, |
| /*made_highest_scoring_other_bid=*/false)); |
| break; |
| |
| case auction_worklet::mojom::KAnonymityBidMode::kSimulate: |
| // ad1.com also wins in the simulated mode. |
| histogram_tester_->ExpectUniqueSample( |
| "Ads.InterestGroup.Auction.NonKAnonWinnerIsKAnon", true, 1); |
| expected_report_urls.push_back( |
| ReportWinUrl(/*bid=*/2, /*highest_scoring_other_bid=*/1, |
| /*made_highest_scoring_other_bid=*/false)); |
| break; |
| } |
| EXPECT_THAT(result_.report_urls, |
| testing::UnorderedElementsAreArray(expected_report_urls)); |
| } |
| |
| // Test for where the same IG makes different bids based on k-anon enforcement, |
| // rather than potentially not bidding at all. The non-k-anon bid is higher. |
| TEST_P(AuctionRunnerKAnonTest, DifferentBids) { |
| // A simple bid script that returns the last ad in the input and the length of |
| // ads array as the bid. |
| const char kAdsArraySensitiveBidScript[] = R"( |
| function generateBid(interestGroup, auctionSignals, perBuyerSignals, |
| trustedBiddingSignals, browserSignals) { |
| return {ad: {}, |
| bid: interestGroup.ads.length, |
| render: interestGroup.ads.pop().renderUrl, |
| allowComponentAuction: true}; |
| } |
| )"; |
| |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder1Url, |
| std::string(kAdsArraySensitiveBidScript) + kReportWinNoUrl); |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kSellerUrl, |
| std::string(kMinimumDecisionScript) + kBasicReportResult); |
| |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, kBidder1Name, kBidder1Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad1.com"))); |
| bidders.back().interest_group.ads->emplace_back(GURL("https://ad2.com"), |
| /*metadata=*/absl::nullopt); |
| |
| // Authorize only ad 1. |
| AuthorizeKAnonAd(bidders[0].interest_group.ads.value()[0], "https://ad1.com/", |
| bidders[0]); |
| |
| std::vector<std::string> ad1_k_anon_keys = { |
| blink::KAnonKeyForAdBid( |
| bidders[0].interest_group, |
| bidders[0].interest_group.ads.value()[0].render_url), |
| blink::KAnonKeyForAdNameReporting( |
| bidders[0].interest_group, bidders[0].interest_group.ads.value()[0]), |
| }; |
| std::vector<std::string> ad2_k_anon_keys = { |
| blink::KAnonKeyForAdBid( |
| bidders[0].interest_group, |
| bidders[0].interest_group.ads.value()[1].render_url), |
| blink::KAnonKeyForAdNameReporting( |
| bidders[0].interest_group, bidders[0].interest_group.ads.value()[1]), |
| }; |
| |
| StartAuction(kSellerUrl, bidders); |
| auction_run_loop_->Run(); |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| ASSERT_TRUE(result_.ad_descriptor.has_value()); |
| histogram_tester_->ExpectUniqueSample( |
| "Ads.InterestGroup.Auction.NonKAnonWinnerIsKAnon", false, 1); |
| |
| base::flat_set<std::string> expected_k_anon_keys_to_join; |
| switch (kanon_mode()) { |
| case auction_worklet::mojom::KAnonymityBidMode::kNone: |
| // Don't care about k-anonymity: ad2 wins, nothing else is reporter. |
| EXPECT_EQ(GURL("https://ad2.com"), result_.ad_descriptor->url); |
| expected_k_anon_keys_to_join.insert(ad2_k_anon_keys.begin(), |
| ad2_k_anon_keys.end()); |
| EXPECT_THAT(result_.report_urls, |
| testing::ElementsAre("https://reporting.example.com/2")); |
| break; |
| |
| case auction_worklet::mojom::KAnonymityBidMode::kEnforce: |
| // Ad 2 is what got blocked by enforcement --- if it were authorized, it |
| // would win. |
| EXPECT_EQ(GURL("https://ad1.com"), result_.ad_descriptor->url); |
| expected_k_anon_keys_to_join.insert(ad1_k_anon_keys.begin(), |
| ad1_k_anon_keys.end()); |
| expected_k_anon_keys_to_join.insert(ad2_k_anon_keys.begin(), |
| ad2_k_anon_keys.end()); |
| EXPECT_THAT(result_.report_urls, |
| testing::ElementsAre("https://reporting.example.com/1")); |
| break; |
| |
| case auction_worklet::mojom::KAnonymityBidMode::kSimulate: |
| // Winner is ad2.com, disregarding k-anonymity, but we also report that |
| // if we did care about it, ad1.com would have won. |
| EXPECT_EQ(GURL("https://ad2.com"), result_.ad_descriptor->url); |
| expected_k_anon_keys_to_join.insert(ad1_k_anon_keys.begin(), |
| ad1_k_anon_keys.end()); |
| expected_k_anon_keys_to_join.insert(ad2_k_anon_keys.begin(), |
| ad2_k_anon_keys.end()); |
| EXPECT_THAT(result_.report_urls, |
| testing::ElementsAre("https://reporting.example.com/2")); |
| break; |
| } |
| // Have to spin all message loops to flush any k-anon set join events. |
| task_environment()->RunUntilIdle(); |
| EXPECT_THAT(interest_group_manager_->TakeJoinedKAnonSets(), |
| testing::UnorderedElementsAreArray(expected_k_anon_keys_to_join)); |
| } |
| |
| // Test to make sure that k-anon info doesn't get incorrectly reported when |
| // an auction gets interrupted. |
| TEST_P(AuctionRunnerKAnonTest, FailureHandling) { |
| // As in DifferentBids, this script produces different k-anon and n-k-anon |
| // bids; it's helpful for this test since |
| const char kAdsArraySensitiveBidScript[] = R"( |
| function generateBid(interestGroup, auctionSignals, perBuyerSignals, |
| trustedBiddingSignals, browserSignals) { |
| return {ad: {}, |
| bid: interestGroup.ads.length, |
| render: interestGroup.ads.pop().renderUrl, |
| allowComponentAuction: true}; |
| } |
| )"; |
| |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kBidder1Url, |
| std::string(kAdsArraySensitiveBidScript) + kSimpleReportWin); |
| // No script for bidder 2, so it never finishes. |
| auction_worklet::AddJavascriptResponse( |
| &url_loader_factory_, kSellerUrl, |
| std::string(kMinimumDecisionScript) + kBasicReportResult); |
| |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, kBidder1Name, kBidder1Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad1.com"))); |
| bidders.back().interest_group.ads->emplace_back(GURL("https://ad2.com"), |
| /*metadata=*/absl::nullopt); |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder2, kBidder2Name, kBidder2Url, |
| /*trusted_bidding_signals_url=*/absl::nullopt, |
| /*trusted_bidding_signals_keys=*/{}, GURL("https://ad3.com"))); |
| |
| // Authorize only ad 1. |
| AuthorizeKAnonAd(bidders[0].interest_group.ads.value()[0], "https://ad1.com/", |
| bidders[0]); |
| |
| // Run the auction, and simulate it being interrupted by navigating away. |
| StartAuction(kSellerUrl, bidders); |
| task_environment()->RunUntilIdle(); |
| auction_runner_->FailAuction(/*manually_aborted=*/false); |
| |
| EXPECT_THAT(result_.errors, testing::ElementsAre()); |
| |
| // Should not have anything to report. |
| EXPECT_FALSE(result_.ad_descriptor.has_value()); |
| // Have to spin all message loops to flush any k-anon set join events. |
| task_environment()->RunUntilIdle(); |
| EXPECT_THAT(interest_group_manager_->TakeJoinedKAnonSets(), |
| testing::ElementsAre()); |
| histogram_tester_->ExpectUniqueSample( |
| "Ads.InterestGroup.Auction.NonKAnonWinnerIsKAnon", false, 0); |
| } |
| |
| TEST_P(AuctionRunnerKAnonTest, MojoValidation) { |
| const struct TestCase { |
| std::set<auction_worklet::mojom::KAnonymityBidMode> run_in_modes; |
| const char* expected_error_message; |
| blink::AdDescriptor ad_descriptor; |
| auction_worklet::mojom::BidderWorkletKAnonEnforcedBidPtr mojo_bid; |
| bool expect_winner; |
| } kTestCases[] = { |
| // Sending a k-anon enforced bid when it should just match the |
| // non-enforced bid. |
| {{auction_worklet::mojom::KAnonymityBidMode::kEnforce, |
| auction_worklet::mojom::KAnonymityBidMode::kSimulate}, |
| "Received different k-anon bid when unenforced bid already k-anon", |
| blink::AdDescriptor(GURL("https://ad1.com")), |
| auction_worklet::mojom::BidderWorkletKAnonEnforcedBid::NewBid( |
| auction_worklet::mojom::BidderWorkletBid::New( |
| "ad", 5.0, /*ad_cost=*/absl::nullopt, |
| blink::AdDescriptor(GURL("https://ad2.com")), |
| /*ad_component_urls=*/absl::nullopt, base::TimeDelta())), |
| /*expect_winner=*/true}, |
| // A non-k-anon bid as k-anon one. Enforced, so auction fails. |
| { |
| {auction_worklet::mojom::KAnonymityBidMode::kEnforce}, |
| "Bid render ad must have a valid URL and size (if specified)", |
| blink::AdDescriptor(GURL("https://ad2.com")), |
| auction_worklet::mojom::BidderWorkletKAnonEnforcedBid::NewBid( |
| auction_worklet::mojom::BidderWorkletBid::New( |
| "ad", 5.0, /*ad_cost=*/absl::nullopt, |
| blink::AdDescriptor(GURL("https://ad2.com")), |
| /*ad_component_urls=*/absl::nullopt, base::TimeDelta())), |
| /*expect_winner=*/false, |
| }, |
| // A non-k-anon bid as k-anon one. Simulate, so auction succeeds. |
| { |
| {auction_worklet::mojom::KAnonymityBidMode::kSimulate}, |
| "Bid render ad must have a valid URL and size (if specified)", |
| blink::AdDescriptor(GURL("https://ad2.com")), |
| auction_worklet::mojom::BidderWorkletKAnonEnforcedBid::NewBid( |
| auction_worklet::mojom::BidderWorkletBid::New( |
| "ad", 5.0, /*ad_cost=*/absl::nullopt, |
| blink::AdDescriptor(GURL("https://ad2.com")), |
| /*ad_component_urls=*/absl::nullopt, base::TimeDelta())), |
| /*expect_winner=*/true, |
| }, |
| // Sending k-anon data when it's not even on. |
| { |
| {auction_worklet::mojom::KAnonymityBidMode::kNone}, |
| "Received k-anon bid data when not considering k-anon", |
| blink::AdDescriptor(GURL("https://ad1.com")), |
| auction_worklet::mojom::BidderWorkletKAnonEnforcedBid:: |
| NewSameAsNonEnforced(nullptr), |
| /*expect_winner=*/true, |
| }}; |
| |
| std::vector<StorageInterestGroup> bidders; |
| bidders.emplace_back(MakeInterestGroup( |
| kBidder1, kBidder1Name, kBidder1Url, kBidder1TrustedSignalsUrl, |
| /*trusted_bidding_signals_keys=*/{"k1", "k2"}, GURL("https://ad1.com"))); |
| bidders.back().interest_group.ads->emplace_back(GURL("https://ad2.com"), |
| /*metadata=*/absl::nullopt); |
| // Authorize only ad 1. |
| AuthorizeKAnonAd(bidders[0].interest_group.ads.value()[0], "https://ad1.com/", |
| bidders[0]); |
| |
| for (const auto& test_case : kTestCases) { |
| SCOPED_TRACE(test_case.expected_error_message); |
| if (test_case.run_in_modes.find(kanon_mode()) == |
| test_case.run_in_modes.end()) |
| continue; |
| |
| UseMockWorkletService(); |
| StartAuction(kSellerUrl, bidders); |
| mock_auction_process_manager_->WaitForWorklets( |
| /*num_bidders=*/1, /*num_sellers=*/1); |
| auto seller_worklet = mock_auction_process_manager_->TakeSellerWorklet(); |
| ASSERT_TRUE(seller_worklet); |
| auto bidder1_worklet = |
| mock_auction_process_manager_->TakeBidderWorklet(kBidder1Url); |
| bidder1_worklet->InvokeGenerateBidCallback(1.0, test_case.ad_descriptor, |
| test_case.mojo_bid.Clone()); |
| |
| // All of these tests only get one scoreAd, since k-anon bid is invalid. |
| auto score_ad_params = seller_worklet->WaitForScoreAd(); |
| EXPECT_EQ(kBidder1, score_ad_params.interest_group_owner); |
| EXPECT_EQ(1, score_ad_params.bid); |
| mojo::Remote<auction_worklet::mojom::ScoreAdClient>( |
| std::move(score_ad_params.score_ad_client)) |
| ->OnScoreAdComplete( |
| /*score=*/11, |
| /*reject_reason=*/ |
| auction_worklet::mojom::RejectReason::kNotAvailable, |
| auction_worklet::mojom::ComponentAuctionModifiedBidParamsPtr(), |
| /*scoring_signals_data_version=*/0, |
| /*has_scoring_signals_data_version=*/false, |
| /*debug_loss_report_url=*/absl::nullopt, |
| /*debug_win_report_url=*/absl::nullopt, /*pa_requests=*/{}, |
| /*errors=*/{}); |
| |
| // Finish the auction. |
| if (test_case.expect_winner) { |
| seller_worklet->WaitForReportResult(); |
| seller_worklet->InvokeReportResultCallback(); |
| mock_auction_process_manager_->WaitForWinningBidderReload(); |
| bidder1_worklet = |
| mock_auction_process_manager_->TakeBidderWorklet(kBidder1Url); |
| bidder1_worklet->WaitForReportWin(); |
| bidder1_worklet->InvokeReportWinCallback(); |
| } |
| auction_run_loop_->Run(); |
| |
| EXPECT_EQ(test_case.expected_error_message, TakeBadMessage()); |
| EXPECT_EQ(test_case.expect_winner, result_.ad_descriptor.has_value()); |
| } |
| } |
| |
| INSTANTIATE_TEST_SUITE_P( |
| /* no label */, |
| AuctionRunnerKAnonTest, |
| ::testing::Values(auction_worklet::mojom::KAnonymityBidMode::kNone, |
| auction_worklet::mojom::KAnonymityBidMode::kEnforce, |
| auction_worklet::mojom::KAnonymityBidMode::kSimulate)); |
| |
| } // namespace |
| } // namespace content |