blob: 86f94fc8597dec96193ac68fe850eba52891fa7b [file] [log] [blame]
// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "third_party/blink/public/common/interest_group/interest_group.h"
#include <stdint.h>
#include <algorithm>
#include <array>
#include <cmath>
#include <cstddef>
#include <optional>
#include <sstream>
#include <string>
#include <string_view>
#include <tuple>
#include <utility>
#include <variant>
#include <vector>
#include "base/base64.h"
#include "base/base64url.h"
#include "base/command_line.h"
#include "base/containers/contains.h"
#include "base/containers/flat_map.h"
#include "base/containers/flat_set.h"
#include "base/containers/span_reader.h"
#include "base/feature_list.h"
#include "base/functional/callback.h"
#include "base/functional/callback_forward.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "base/json/string_escape.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/scoped_refptr.h"
#include "base/run_loop.h"
#include "base/strings/escape.h"
#include "base/strings/pattern.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/strings/string_view_util.h"
#include "base/strings/stringprintf.h"
#include "base/synchronization/lock.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/mock_callback.h"
#include "base/test/scoped_command_line.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "base/test/test_timeouts.h"
#include "base/test/values_test_util.h"
#include "base/test/with_feature_override.h"
#include "base/thread_annotations.h"
#include "base/time/time.h"
#include "base/trace_event/trace_config.h"
#include "base/values.h"
#include "build/build_config.h"
#include "components/aggregation_service/aggregation_coordinator_utils.h"
#include "components/cbor/reader.h"
#include "components/network_session_configurator/common/network_switches.h"
#include "components/web_package/web_bundle_builder.h"
#include "content/browser/aggregation_service/aggregatable_report.h"
#include "content/browser/fenced_frame/fenced_document_data.h"
#include "content/browser/fenced_frame/fenced_frame.h"
#include "content/browser/fenced_frame/fenced_frame_url_mapping.h"
#include "content/browser/interest_group/ad_auction_page_data.h"
#include "content/browser/interest_group/ad_auction_service_impl.h"
#include "content/browser/interest_group/additional_bids_test_util.h"
#include "content/browser/interest_group/interest_group_features.h"
#include "content/browser/interest_group/interest_group_manager_impl.h"
#include "content/browser/interest_group/test_interest_group_observer.h"
#include "content/browser/private_aggregation/private_aggregation_caller_api.h"
#include "content/browser/private_aggregation/private_aggregation_manager_impl.h"
#include "content/browser/private_aggregation/private_aggregation_pending_contributions.h"
#include "content/browser/private_aggregation/private_aggregation_test_utils.h"
#include "content/browser/renderer_host/page_impl.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/storage_partition_impl.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/common/features.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/site_isolation_policy.h"
#include "content/public/browser/tracing_controller.h"
#include "content/public/common/content_client.h"
#include "content/public/common/content_features.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/back_forward_cache_util.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_content_browser_client.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/public/test/privacy_sandbox_coordinator_test_util.h"
#include "content/public/test/test_frame_navigation_observer.h"
#include "content/public/test/test_navigation_observer.h"
#include "content/public/test/url_loader_monitor.h"
#include "content/services/auction_worklet/public/cpp/auction_worklet_features.h"
#include "content/services/auction_worklet/public/cpp/cbor_test_util.h"
#include "content/services/auction_worklet/public/mojom/bidder_worklet.mojom.h"
#include "content/shell/browser/shell.h"
#include "content/test/content_browser_test_utils_internal.h"
#include "content/test/fenced_frame_test_utils.h"
#include "net/base/isolation_info.h"
#include "net/base/network_isolation_key.h"
#include "net/dns/mock_host_resolver.h"
#include "net/http/http_request_headers.h"
#include "net/http/http_status_code.h"
#include "net/test/embedded_test_server/controllable_http_response.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "net/test/embedded_test_server/http_request.h"
#include "net/test/embedded_test_server/http_response.h"
#include "net/test/embedded_test_server/request_handler_util.h"
#include "net/third_party/quiche/src/quiche/oblivious_http/oblivious_http_gateway.h"
#include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
#include "services/data_decoder/public/cpp/test_support/in_process_data_decoder.h"
#include "services/network/public/cpp/features.h"
#include "services/network/public/cpp/network_switches.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/mojom/fetch_api.mojom.h"
#include "services/network/public/mojom/ip_address_space.mojom.h"
#include "services/network/test/test_network_context.h"
#include "services/network/test/test_utils.h"
#include "testing/gmock/include/gmock/gmock-matchers.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/common/features_generated.h"
#include "third_party/blink/public/common/interest_group/ad_auction_constants.h"
#include "third_party/blink/public/common/interest_group/ad_display_size_utils.h"
#include "third_party/blink/public/common/interest_group/test/interest_group_test_utils.h"
#include "third_party/blink/public/common/interest_group/test_interest_group_builder.h"
#include "third_party/blink/public/mojom/aggregation_service/aggregatable_report.mojom.h"
#include "third_party/blink/public/mojom/interest_group/ad_auction_service.mojom.h"
#include "third_party/blink/public/mojom/interest_group/interest_group_types.mojom.h"
#include "third_party/blink/public/mojom/private_aggregation/private_aggregation_host.mojom.h"
#include "ui/display/screen.h"
#include "url/gurl.h"
#include "url/origin.h"
#include "url/url_constants.h"
namespace content {
namespace {
using ::blink::IgExpectEqualsForTesting;
using ::blink::IgExpectNotEqualsForTesting;
using ::testing::Eq;
using ::testing::HasSubstr;
using ::testing::Optional;
constexpr char kLegitimateAdAuctionResponse[] =
"ungWv48Bz-pBQUDeXa4iI7ADYaOWF3qctBD_YfIAFa0=";
constexpr char kLegitimateAdAuctionSignals[] =
R"([{"adSlot":"slot1", "sellerSignals":{"signal1":"value1"}}])";
// Returned by test Javascript code when join or leave promises complete without
// throwing an exception.
const char kSuccess[] = "success";
// A path for an update registered by `RegisterNoOpUpdate()` that returns an
// empty dict, which is a valid update, but doesn't change the interest group.
// Can be used with
// WaitForInterestGroupsSatisfyingInvalidatingCacheByUpdating().
constexpr char kNoOpUpdatePath[] = "/interest_group/no_op_update_path.json";
// A path for a bidding script that validates view and click counts, as used by
// ValidateViewClickCountsInGenerateBid().
constexpr char kValidateViewClickBiddingLogicPath[] =
"/interest_group/bidding_view_click_validator.js";
// An example ad path, as used by ValidateViewClickCountsInGenerateBid().
constexpr char kAdURL[] = "https://example.test/render";
// Returns a string that declares a "maybePromise()" Javascript function, which
// takes an argument and either returns it (if `use_promise` is false) or
// returns a promise that will be resolved with that value in a millisecond (if
// `use_promise` is true).
std::string MaybePromiseFunction(bool use_promise) {
if (use_promise) {
return R"(
function maybePromise(val, delay = 1) {
return new Promise((resolve, reject) => {
setTimeout(() => { resolve(val); }, delay);
});
}
)";
}
return R"(
function maybePromise(val) {
return val;
}
)";
}
const blink::InterestGroup::AdditionalBidKey kPublicKey1 = {
0xc7, 0xd1, 0x8d, 0x16, 0x57, 0x5c, 0xe7, 0x3a, 0x2c, 0x60, 0x22,
0xfb, 0x44, 0xe4, 0xc8, 0x5a, 0xb5, 0x41, 0xee, 0xf9, 0x34, 0xee,
0xf8, 0x11, 0x00, 0xf7, 0xfa, 0xf0, 0x7f, 0xfa, 0x30, 0x6f};
const char kBase64PublicKey1[] = "x9GNFldc5zosYCL7ROTIWrVB7vk07vgRAPf68H/6MG8=";
const uint8_t kPrivateKey1[] = {
0x44, 0x0a, 0xb0, 0x1f, 0xec, 0x87, 0x57, 0xce, 0x42, 0x17, 0x91,
0xa3, 0x67, 0xa4, 0x3f, 0x2b, 0xf6, 0x2c, 0xf5, 0xe3, 0x0a, 0x88,
0x4e, 0x22, 0x94, 0xf0, 0x59, 0x01, 0x1d, 0x53, 0x2b, 0xa0, 0xc7,
0xd1, 0x8d, 0x16, 0x57, 0x5c, 0xe7, 0x3a, 0x2c, 0x60, 0x22, 0xfb,
0x44, 0xe4, 0xc8, 0x5a, 0xb5, 0x41, 0xee, 0xf9, 0x34, 0xee, 0xf8,
0x11, 0x00, 0xf7, 0xfa, 0xf0, 0x7f, 0xfa, 0x30, 0x6f};
const blink::InterestGroup::AdditionalBidKey kPublicKey2 = {
0xa6, 0x34, 0xb2, 0xd9, 0x67, 0x01, 0xc9, 0x8b, 0x19, 0xfc, 0x0a,
0xa8, 0x9a, 0x8f, 0x3c, 0xb6, 0x2a, 0xad, 0x14, 0xea, 0x4f, 0xa5,
0x17, 0x7e, 0xe3, 0x04, 0xed, 0x8d, 0x0d, 0xaf, 0x66, 0x11};
const char kBase64PublicKey2[] = "pjSy2WcByYsZ/Aqomo88tiqtFOpPpRd+4wTtjQ2vZhE=";
const uint8_t kPrivateKey2[] = {
0x6b, 0x42, 0xed, 0x42, 0x2e, 0x8d, 0x01, 0xf7, 0x6a, 0xc5, 0xc6,
0x26, 0x4a, 0xeb, 0xfb, 0xe5, 0xbc, 0x31, 0xd2, 0x49, 0x19, 0x92,
0x63, 0x5e, 0x95, 0x1c, 0x99, 0x7f, 0xf2, 0xd2, 0x77, 0x87, 0xa6,
0x34, 0xb2, 0xd9, 0x67, 0x01, 0xc9, 0x8b, 0x19, 0xfc, 0x0a, 0xa8,
0x9a, 0x8f, 0x3c, 0xb6, 0x2a, 0xad, 0x14, 0xea, 0x4f, 0xa5, 0x17,
0x7e, 0xe3, 0x04, 0xed, 0x8d, 0x0d, 0xaf, 0x66, 0x11};
const blink::InterestGroup::AdditionalBidKey kPublicKeyWithNoMatchingSignature =
{0xf8, 0x11, 0x00, 0xf7, 0xfa, 0xf0, 0x7f, 0xfa, 0x30, 0x6f, 0xc7,
0xd1, 0x8d, 0x16, 0x57, 0x5c, 0xe7, 0x3a, 0x2c, 0x60, 0x22, 0xfb,
0x44, 0xe4, 0xc8, 0x5a, 0xb5, 0x41, 0xee, 0xf9, 0x34, 0xee};
constexpr char kNoContextualDataValue[] = "\"no contextual data\"";
std::string base64Decode(std::string_view input) {
std::string bytes;
CHECK(base::Base64UrlDecode(
input, base::Base64UrlDecodePolicy::IGNORE_PADDING, &bytes));
return bytes;
}
// Convenience helper to parse JSON to a base::Value. CHECKs on failure, rather
// than letting callers handle it.
base::Value JsonToValue(const std::string& json) {
std::optional<base::Value> metadata =
base::JSONReader::Read(json, base::JSON_PARSE_RFC);
CHECK(metadata);
return std::move(metadata).value();
}
base::Value::List AdAllowedReportingOriginsToList(
std::vector<url::Origin> origins) {
base::Value::List allowed_reporting_origins;
for (const auto& origin : origins) {
allowed_reporting_origins.Append(origin.Serialize());
}
return allowed_reporting_origins;
}
// Creates base::Value representations of ads and adComponents arrays from the
// provided InterestGroup::Ads.
base::Value::List MakeAdsValue(
const std::vector<blink::InterestGroup::Ad>& ads) {
base::Value::List list;
for (const auto& ad : ads) {
base::Value::Dict entry;
entry.Set("renderURL", ad.render_url());
if (ad.size_group) {
entry.Set("sizeGroup", std::move(ad.size_group.value()));
}
if (ad.buyer_reporting_id) {
entry.Set("buyerReportingId", *ad.buyer_reporting_id);
}
if (ad.buyer_and_seller_reporting_id) {
entry.Set("buyerAndSellerReportingId", *ad.buyer_and_seller_reporting_id);
}
if (ad.selectable_buyer_and_seller_reporting_ids) {
base::Value::List selectable_buyer_and_seller_reporting_ids;
for (std::string id : *ad.selectable_buyer_and_seller_reporting_ids) {
selectable_buyer_and_seller_reporting_ids.Append(id);
}
entry.Set("selectableBuyerAndSellerReportingIds",
std::move(selectable_buyer_and_seller_reporting_ids));
}
if (ad.metadata) {
entry.Set("metadata", JsonToValue(*ad.metadata));
}
if (ad.ad_render_id) {
entry.Set("adRenderId", std::move(ad.ad_render_id.value()));
}
if (ad.allowed_reporting_origins) {
entry.Set("allowedReportingOrigins",
AdAllowedReportingOriginsToList(
ad.allowed_reporting_origins.value()));
}
if (ad.creative_scanning_metadata) {
entry.Set("creativeScanningMetadata", *ad.creative_scanning_metadata);
}
list.Append(std::move(entry));
}
return list;
}
base::Value::Dict StringDoubleMapToDict(
const base::flat_map<std::string, double>& map) {
base::Value::Dict dict;
for (const auto& pair : map) {
dict.Set(pair.first, pair.second);
}
return dict;
}
base::Value::List SellerCapabilitiesToList(
blink::SellerCapabilitiesType capabilities) {
base::Value::List list;
for (blink::SellerCapabilities capability : capabilities) {
if (capability == blink::SellerCapabilities::kInterestGroupCounts) {
list.Append("interest-group-counts");
} else if (capability == blink::SellerCapabilities::kLatencyStats) {
list.Append("latency-stats");
} else {
ADD_FAILURE() << "Unknown seller capability "
<< static_cast<uint32_t>(capability);
}
}
return list;
}
base::Value::Dict SellerCapabilitiesToDict(
const std::optional<
base::flat_map<url::Origin, blink::SellerCapabilitiesType>>& map,
blink::SellerCapabilitiesType all_sellers_capabilities) {
base::Value::Dict dict;
if (map) {
for (const auto& [origin, capabilities] : *map) {
dict.Set(origin.Serialize(), SellerCapabilitiesToList(capabilities));
}
}
if (!all_sellers_capabilities.empty()) {
dict.Set("*", SellerCapabilitiesToList(all_sellers_capabilities));
}
return dict;
}
base::Value::Dict InterestGroupSizeToDict(const blink::AdSize& size) {
base::Value::Dict output;
output.Set("width", base::NumberToString(size.width) +
blink::ConvertAdSizeUnitToString(size.width_units));
output.Set("height", base::NumberToString(size.height) +
blink::ConvertAdSizeUnitToString(size.height_units));
return output;
}
base::Value::Dict AdSizesToDict(
const base::flat_map<std::string, blink::AdSize>& map) {
base::Value::Dict dict;
for (const auto& [size_name, size] : map) {
dict.Set(size_name, InterestGroupSizeToDict(size));
}
return dict;
}
base::Value::Dict SizeGroupsToDict(
const base::flat_map<std::string, std::vector<std::string>>& map) {
base::Value::Dict dict;
for (const auto& [group_name, group] : map) {
base::Value::List size_list;
for (const std::string& size : group) {
size_list.Append(size);
}
dict.Set(group_name, std::move(size_list));
}
return dict;
}
base::Value::List AuctionServerRequestFlagsToList(
const blink::AuctionServerRequestFlags& flags) {
base::Value::List result;
if (flags.Has(blink::AuctionServerRequestFlagsEnum::kOmitAds)) {
result.Append("omit-ads");
}
if (flags.Has(blink::AuctionServerRequestFlagsEnum::kIncludeFullAds)) {
result.Append("include-full-ads");
}
if (flags.Has(
blink::AuctionServerRequestFlagsEnum::kOmitUserBiddingSignals)) {
result.Append("omit-user-bidding-signals");
}
return result;
}
bool IsErrorMessage(const content::WebContentsConsoleObserver::Message& msg) {
return msg.log_level == blink::mojom::ConsoleMessageLevel::kError;
}
// This convoluted way to validate the UUID is necessary because MatchesRegex
// only support simple regular expressions, not PCRE.
std::string ConvertUuidWithOnlyZeros(const std::string& uuid) {
std::string all_zeros;
base::ReplaceChars(uuid, "1234567890abcdef", "0", &all_zeros);
return all_zeros;
}
class AllowlistedOriginContentBrowserClient
: public ContentBrowserTestContentBrowserClient {
public:
explicit AllowlistedOriginContentBrowserClient() = default;
AllowlistedOriginContentBrowserClient(
const AllowlistedOriginContentBrowserClient&) = delete;
AllowlistedOriginContentBrowserClient& operator=(
const AllowlistedOriginContentBrowserClient&) = delete;
void SetAllowList(base::flat_set<url::Origin>&& allow_list) {
allow_list_ = allow_list;
}
void AddToAllowList(const std::vector<url::Origin>& add_to_allow_list) {
allow_list_.insert(add_to_allow_list.begin(), add_to_allow_list.end());
}
// ContentBrowserClient overrides:
bool IsInterestGroupAPIAllowed(
content::BrowserContext* browser_context,
content::RenderFrameHost* render_frame_host,
ContentBrowserClient::InterestGroupApiOperation operation,
const url::Origin& top_frame_origin,
const url::Origin& api_origin) override {
checked_top_frame_origins_.insert(top_frame_origin);
return allow_list_.contains(top_frame_origin) &&
allow_list_.contains(api_origin);
}
bool IsPrivacySandboxReportingDestinationAttested(
content::BrowserContext* browser_context,
const url::Origin& destination_origin,
content::PrivacySandboxInvokingAPI invoking_api) override {
return allow_list_.contains(destination_origin);
}
const std::set<url::Origin>& checked_top_frame_origins() const {
return checked_top_frame_origins_;
}
MOCK_METHOD(void,
LogWebFeatureForCurrentPage,
(content::RenderFrameHost*, blink::mojom::WebFeature),
(override));
private:
std::set<url::Origin> checked_top_frame_origins_;
base::flat_set<url::Origin> allow_list_;
};
// A special path for updates that allows deferring the server response. Only
// update requests to this path can be deferred, because the path must be
// registered before the EmbeddedTestServer starts.
constexpr char kDeferredUpdateResponsePath[] =
"/interest_group/update_deferred.json";
constexpr char kFledgeHeader[] = "Ad-Auction-Allowed";
// Allows registering responses to network requests.
class NetworkResponder {
public:
using ResponseHeaders = std::vector<std::pair<std::string, std::string>>;
explicit NetworkResponder(
net::EmbeddedTestServer& server,
const std::string& relative_url = kDeferredUpdateResponsePath)
: controllable_response_(&server, relative_url) {
RegisterWithServer(server);
}
NetworkResponder(const NetworkResponder&) = delete;
NetworkResponder& operator=(const NetworkResponder&) = delete;
// Registers the NetworkResponder with the specified server. Allows one
// responder to work with multiple servers. Doesn't work with the
// `relative_url` passed in during construction.
void RegisterWithServer(net::EmbeddedTestServer& server) {
server.RegisterRequestHandler(base::BindRepeating(
&NetworkResponder::RequestHandler, base::Unretained(this)));
}
void RegisterNetworkResponse(const std::string& url_path,
std::string_view body,
std::string_view mime_type = "application/json",
ResponseHeaders extra_response_headers = {},
net::HttpStatusCode code = net::HTTP_OK) {
base::AutoLock auto_lock(response_map_lock_);
Response response;
response.body = body;
response.mime_type = mime_type;
response.extra_response_headers = std::move(extra_response_headers);
response.code = code;
response_map_[url_path] = std::move(response);
}
struct SubresourceResponse {
SubresourceResponse(const std::string& subresource_url,
const std::string& payload,
const std::string& content_type = "application/json")
: subresource_url(subresource_url),
payload(payload),
content_type(content_type) {}
std::string subresource_url;
std::string payload;
std::string content_type;
};
static SubresourceResponse DirectFromSellerPerBuyerSignals(
const url::Origin& buyer_origin,
const std::string& payload,
const std::string& prefix = "/direct_from_seller_signals") {
return NetworkResponder::SubresourceResponse(
/*subresource_url=*/base::StringPrintf(
"%s?perBuyerSignals=%s", prefix.c_str(),
base::EscapeQueryParamValue(buyer_origin.Serialize(),
/*use_plus=*/false)
.c_str()),
/*payload=*/
payload);
}
struct SubresourceBundle {
SubresourceBundle(const GURL& bundle_url,
const std::vector<SubresourceResponse>& subresources)
: bundle_url(bundle_url.spec()), subresources(subresources) {}
std::string bundle_url;
std::vector<SubresourceResponse> subresources;
};
// Serves DirectFromSellerSignals subresource bundles from `bundles` with the
// Access-Control-Allow-Origin (on both bundle and subresources) set to
// `allow_origin`.
void RegisterDirectFromSellerSignalsResponse(
const std::vector<SubresourceBundle>& bundles,
const std::string& allow_origin) {
for (const SubresourceBundle& bundle : bundles) {
web_package::WebBundleBuilder builder;
for (const SubresourceResponse& response : bundle.subresources) {
// NOTE: Upper-case characters are *not* allowed.
builder.AddExchange(response.subresource_url,
{{":status", "200"},
{"content-type", response.content_type},
{"ad-auction-allowed", "true"},
{"ad-auction-only", "true"},
{"access-control-allow-credentials", "true"},
{"access-control-allow-origin", allow_origin}},
response.payload);
}
std::vector<uint8_t> bundle_bytes = builder.CreateBundle();
std::string body(reinterpret_cast<const char*>(bundle_bytes.data()),
bundle_bytes.size());
RegisterNetworkResponse(GURL(bundle.bundle_url).path(), body,
/*mime_type=*/"application/webbundle",
/*extra_response_headers=*/
{{"X-Content-Type-Options", "nosniff"},
{"Access-Control-Allow-Credentials", "true"},
{"Access-Control-Allow-Origin", allow_origin}});
}
}
static std::string ProduceHtmlWithSubresourceBundles(
const std::vector<SubresourceBundle>& bundles) {
constexpr char kHtmlTemplate[] = R"(<!DOCTYPE html>
<meta charset="utf-8">
<title>Page with subresource bundle directFromSellerSignals</title>
%s
<body>
<p>This page has a subresource bundle for passing directFromSellerSignals to
navigator.runAdAuction().</p>
</body>)";
// Include credentials to test that this works with DirectFromSellerSignals.
constexpr char kScriptWebBundleTemplate[] = R"(
<script type="webbundle">
{
"source": $1,
"credentials": "include",
"resources": $2
}
</script>
)";
std::string script_tags;
for (const SubresourceBundle& bundle : bundles) {
base::Value::List subresources;
for (const SubresourceResponse& subresource : bundle.subresources) {
subresources.Append(subresource.subresource_url);
}
script_tags += JsReplace(kScriptWebBundleTemplate, bundle.bundle_url,
std::move(subresources));
}
return base::StringPrintf(kHtmlTemplate, script_tags.c_str());
}
// Registers an HTML response at `page_url` that inclues <script
// type="webbundle"> tags for each fo the SubresourceBundles in `bundles`.
// Each bundle is loaded using credentials.
void RegisterHtmlWithSubresourceBundles(
const std::vector<SubresourceBundle>& bundles,
std::string page_url) {
RegisterNetworkResponse(page_url,
ProduceHtmlWithSubresourceBundles(bundles),
/*mime_type=*/"text/html");
}
// Register a response that's a bidder script. Takes the body of the
// generateBid() method.
void RegisterBidderScript(const std::string& url_path,
const std::string& generate_bid_body) {
std::string script = base::StringPrintf(R"(
function generateBid(
interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals,
unusedBrowserSignals) {
%s
})",
generate_bid_body.c_str());
RegisterNetworkResponse(url_path, script, "application/javascript");
}
// Perform the deferred response -- the test hangs if the client isn't waiting
// on a response to kDeferredUpdateResponsePath.
void DoDeferredUpdateResponse(
const std::string& response,
const std::string& content_type = "application/json") {
controllable_response_.WaitForRequest();
controllable_response_.Send(net::HTTP_OK, content_type, response,
/*cookies=*/{},
/*extra_headers=*/{std::string(kFledgeHeader)});
controllable_response_.Done();
}
// Wait for and get the received request.
const net::test_server::HttpRequest* GetRequest() {
controllable_response_.WaitForRequest();
return controllable_response_.http_request();
}
bool HasReceivedRequest() {
return controllable_response_.has_received_request();
}
private:
struct Response {
std::string body;
std::string mime_type;
ResponseHeaders extra_response_headers;
net::HttpStatusCode code;
};
std::unique_ptr<net::test_server::HttpResponse> RequestHandler(
const net::test_server::HttpRequest& request) {
base::AutoLock auto_lock(response_map_lock_);
const auto it = response_map_.find(request.GetURL().path());
if (it == response_map_.end()) {
return nullptr;
}
auto response = std::make_unique<net::test_server::BasicHttpResponse>();
response->AddCustomHeader(kFledgeHeader, "true");
response->set_code(it->second.code);
response->set_content(it->second.body);
response->set_content_type(it->second.mime_type);
for (const auto& header : it->second.extra_response_headers) {
response->AddCustomHeader(header.first, header.second);
}
return std::move(response);
}
// EmbeddedTestServer RequestHandlers can't be added after the server has
// started, but tests may want to specify network responses after the server
// starts in the fixture. A handler is therefore registered that uses
// `response_map_` to serve network responses.
base::Lock response_map_lock_;
// For each HTTPS request, we see if any path in the map matches the request
// path. If so, the server returns the mapped value string as the response.
base::flat_map<std::string, Response> response_map_
GUARDED_BY(response_map_lock_);
net::test_server::ControllableHttpResponse controllable_response_;
};
// Handle well-known requests. Frame origins are expected to be of the form
// "allow-join...", "allow-leave...", or "no-cors...".
std::unique_ptr<net::test_server::HttpResponse> HandleWellKnownRequest(
const net::test_server::HttpRequest& request) {
if (!base::StartsWith(request.relative_url,
"/.well-known/interest-group/permissions/?origin=")) {
return nullptr;
}
// .well-known requests should advertise they accept JSON responses.
const auto accept_header =
request.headers.find(net::HttpRequestHeaders::kAccept);
CHECK(accept_header != request.headers.end());
EXPECT_EQ(accept_header->second, "application/json");
auto response = std::make_unique<net::test_server::BasicHttpResponse>();
response->set_content_type("application/json");
response->set_content("{}");
const auto host_header = request.headers.find(net::HttpRequestHeaders::kHost);
CHECK(host_header != request.headers.end());
if (base::StartsWith(host_header->second, "allow-join.")) {
response->set_content(R"({"joinAdInterestGroup" : true})");
response->AddCustomHeader("Access-Control-Allow-Origin", "*");
} else if (base::StartsWith(host_header->second, "allow-leave.")) {
response->set_content(R"({"leaveAdInterestGroup" : true})");
response->AddCustomHeader("Access-Control-Allow-Origin", "*");
} else if (base::StartsWith(host_header->second, "no-cors.")) {
response->set_content(
R"({"joinAdInterestGroup" : true, "leaveAdInterestGroup" : true})");
} else {
NOTREACHED() << "Unexpected host_header: " << host_header->second;
}
return response;
}
std::unique_ptr<net::test_server::HttpResponse> HandleAdditionalBids(
const net::test_server::HttpRequest& request) {
if (!base::StartsWith(request.relative_url, "/additionalBidsHandler?")) {
return nullptr;
}
std::vector<std::string> pieces =
base::SplitString(request.GetURL().query_piece(), "&",
base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
if (pieces.size() < 2u) {
return nullptr;
}
auto response = std::make_unique<net::test_server::BasicHttpResponse>();
response->set_content_type("application/json");
response->set_content("{}");
const std::string& nonce = pieces[0];
const std::string& fault_str = pieces[1];
SignedAdditionalBidFault fault = SignedAdditionalBidFault::kNone;
if (fault_str == "invalid-signed-base-64") {
fault = SignedAdditionalBidFault::kInvalidSignedBase64;
} else if (fault_str == "invalid-signed-json") {
fault = SignedAdditionalBidFault::kInvalidSignedJson;
} else if (fault_str == "invalid-signed-struct") {
fault = SignedAdditionalBidFault::kInvalidSignedBidStructure;
} else if (fault_str == "invalid-signature") {
fault = SignedAdditionalBidFault::kInvalidSignature;
} else if (fault_str == "one-invalid-signature") {
fault = SignedAdditionalBidFault::kOneInvalidSignature;
} else {
DCHECK_EQ(fault_str, "none");
}
for (size_t i = 2; i < pieces.size(); ++i) {
std::string bid;
bool ok = base::Base64Decode(pieces[i], &bid,
base::Base64DecodePolicy::kForgiving);
CHECK(ok);
response->AddCustomHeader(
"Ad-Auction-Additional-Bid",
GenerateSignedAdditionalBidHeader(
fault, nonce, bid, {kPrivateKey1, kPrivateKey2},
{kBase64PublicKey1, kBase64PublicKey2}));
}
return response;
}
class InterestGroupBrowserTest : public ContentBrowserTest {
public:
InterestGroupBrowserTest() {
feature_list_.InitWithFeaturesAndParameters(
/*enabled_features=*/
{{network::features::kInterestGroupStorage, {}},
{blink::features::kFledgeBiddingAndAuctionServer, {}},
{features::kPrivacySandboxAdsAPIsOverride, {}},
{blink::features::kAdInterestGroupAPI, {}},
{blink::features::kParakeet, {}},
{blink::features::kFledge, {}},
{blink::features::kAllowURNsInIframes, {}},
{blink::features::kFledgeDirectFromSellerSignalsHeaderAdSlot, {}},
{features::kBackForwardCache, {}},
{blink::features::kFencedFramesLocalUnpartitionedDataAccess, {}},
{blink::features::kFledgeDeprecatedRenderURLReplacements, {}},
{blink::features::kFledgeMultiBid, {}},
{blink::features::kFledgeSampleDebugReports, {}},
{blink::features::kFledgeEnableSampleDebugReportOnCookieSetting, {}},
{blink::features::kFledgeSellerScriptExecutionMode, {}},
// These are in field trial config, but we want this consistent among
// bots.
{blink::features::kFledgeCustomMaxAuctionAdComponents,
{{"FledgeAdComponentLimit", "40"}}},
{blink::features::kFledgeAuctionDealSupport, {}},
// TODO(crrev.com/c/6096602): Remove once implementation is removed.
{blink::features::kFledgeDirectFromSellerSignalsWebBundles, {}},
{blink::features::kFledgeTrustedSignalsKVv1CreativeScanning, {}},
{blink::features::kFledgeTrustedSignalsKVv2ContextualData, {}},
{features::kFledgeTextConversionHelpers, {}},
{network::features::kAdAuctionEventRegistration, {}},
{blink::features::kFledgeClickiness, {}},
{network::features::kPopulatePermissionsPolicyOnRequest, {}}},
/*disabled_features=*/
{blink::features::kFencedFrames,
blink::features::kFledgeEnforceKAnonymity,
blink::features::kFledgeRealTimeReporting,
features::kCookieDeprecationFacilitatedTesting});
}
~InterestGroupBrowserTest() override { content_browser_client_.reset(); }
void SetUpOnMainThread() override {
ContentBrowserTest::SetUpOnMainThread();
host_resolver()->AddRule("*", "127.0.0.1");
ASSERT_TRUE(embedded_test_server()->Start());
embedded_https_test_server().SetSSLConfig(
net::EmbeddedTestServer::CERT_TEST_NAMES);
embedded_https_test_server().RegisterRequestHandler(
base::BindRepeating(&HandleWellKnownRequest));
embedded_https_test_server().RegisterRequestHandler(
base::BindRepeating(&HandleAdditionalBids));
embedded_https_test_server().AddDefaultHandlers(GetTestDataFilePath());
embedded_https_test_server().RegisterRequestMonitor(base::BindRepeating(
&InterestGroupBrowserTest::OnHttpsTestServerRequestMonitor,
base::Unretained(this)));
network_responder_ = CreateNetworkResponder();
ASSERT_TRUE(embedded_https_test_server().Start());
manager_ = static_cast<InterestGroupManagerImpl*>(
shell()
->web_contents()
->GetBrowserContext()
->GetDefaultStoragePartition()
->GetInterestGroupManager());
content_browser_client_ =
std::make_unique<AllowlistedOriginContentBrowserClient>();
content_browser_client_->SetAllowList(
{embedded_https_test_server().GetOrigin("a.test"),
embedded_https_test_server().GetOrigin("b.test"),
embedded_https_test_server().GetOrigin("c.test"),
// Magic interest group origins used in cross-site join/leave tests.
embedded_https_test_server().GetOrigin("allow-join.a.test"),
embedded_https_test_server().GetOrigin("allow-leave.a.test"),
embedded_https_test_server().GetOrigin("no-cors.a.test"),
// Magic interest group origins used in other tests where we need
// InterestGroups from multiple join origins.
embedded_https_test_server().GetOrigin("allow-join.b.test"),
embedded_https_test_server().GetOrigin("allow-leave.b.test"),
embedded_https_test_server().GetOrigin("no-cors.b.test"),
embedded_https_test_server().GetOrigin("allow-join.c.test"),
embedded_https_test_server().GetOrigin("allow-leave.c.test"),
embedded_https_test_server().GetOrigin("no-cors.c.test"),
// HTTP origins like those below aren't supported for FLEDGE -- some
// tests verify that HTTP origins are rejected, even if somehow they
// are allowed by the allowlist.
embedded_test_server()->GetOrigin("a.test"),
embedded_test_server()->GetOrigin("b.test"),
embedded_test_server()->GetOrigin("c.test")});
}
void TearDownOnMainThread() override {
manager_ = nullptr; // don't dangle once StoragePartition cleans it up.
ContentBrowserTest::TearDownOnMainThread();
}
virtual std::unique_ptr<NetworkResponder> CreateNetworkResponder() {
return std::make_unique<NetworkResponder>(embedded_https_test_server());
}
// Attempts to join the specified interest group. Returns kSuccess if the
// operation claims to have succeeded, and the exception message on failure.
//
// If `execution_target` is non-null, uses it as the target. Otherwise, uses
// shell().
[[nodiscard]] std::string JoinInterestGroup(
url::Origin owner,
std::string name,
std::optional<ToRenderFrameHost> execution_target = std::nullopt) {
return EvalJs(execution_target ? *execution_target : shell(),
JsReplace(R"(
(async function() {
try {
await navigator.joinAdInterestGroup(
{name: $1, owner: $2}, /*joinDurationSec=*/ 300);
return 'success';
} catch (e) {
return e.toString();
}
})())",
name, owner))
.ExtractString();
}
// Just like JoinInterestGroup() above, but also verifies that the interest
// group was joined or not, depending on the return value.
[[nodiscard]] std::string JoinInterestGroupAndVerify(
const url::Origin& owner,
const std::string& name,
std::optional<ToRenderFrameHost> execution_target = std::nullopt) {
std::optional<SingleStorageInterestGroup> initial_interest_group =
GetInterestGroup(owner, name);
std::string result = JoinInterestGroup(owner, name, execution_target);
std::optional<SingleStorageInterestGroup> final_interest_group =
GetInterestGroup(owner, name);
if (result == kSuccess) {
// On or after success, the user should have joined the interest group,
// which should have been overwritten with the new interest group.
while (!final_interest_group) {
base::RunLoop().RunUntilIdle();
final_interest_group = GetInterestGroup(owner, name);
}
if (final_interest_group) {
if (!initial_interest_group) {
EXPECT_EQ(1, final_interest_group.value()
->bidding_browser_signals->join_count);
} else {
EXPECT_EQ(initial_interest_group.value()
->bidding_browser_signals->join_count +
1,
final_interest_group.value()
->bidding_browser_signals->join_count);
}
// Check that the interest group is as expected.
blink::InterestGroup expected_group;
expected_group.owner = owner;
expected_group.name = name;
expected_group.priority = 0;
// Don't compare the expiration.
expected_group.expiry =
final_interest_group.value()->interest_group.expiry;
IgExpectEqualsForTesting(
/*actual=*/final_interest_group.value()->interest_group,
/*expected=*/expected_group);
}
} else {
// On failure, nothing should have changed.
if (!initial_interest_group) {
EXPECT_FALSE(final_interest_group);
} else {
EXPECT_EQ(
initial_interest_group.value()->bidding_browser_signals->join_count,
final_interest_group.value()->bidding_browser_signals->join_count);
IgExpectEqualsForTesting(
/*actual=*/final_interest_group.value()->interest_group,
/*expected=*/initial_interest_group.value()->interest_group);
}
}
return result;
}
// The `trusted_bidding_signals_keys` and `ads` fields of `group` will be
// ignored in favor of the passed in values.
// If `execution_target` is non-null, uses it as the target. Otherwise, uses
// shell().
[[nodiscard]] std::string JoinInterestGroup(
const blink::InterestGroup& group,
const std::optional<ToRenderFrameHost> execution_target = std::nullopt) {
base::Value::Dict dict;
dict.Set("name", group.name);
dict.Set("owner", group.owner.Serialize());
dict.Set("priority", group.priority);
dict.Set("enableBiddingSignalsPrioritization",
group.enable_bidding_signals_prioritization);
if (group.priority_vector) {
dict.Set("priorityVector", StringDoubleMapToDict(*group.priority_vector));
}
if (group.priority_signals_overrides) {
dict.Set("prioritySignalsOverrides",
StringDoubleMapToDict(*group.priority_signals_overrides));
}
dict.Set("sellerCapabilities",
SellerCapabilitiesToDict(group.seller_capabilities,
group.all_sellers_capabilities));
if (group.bidding_url) {
dict.Set("biddingLogicURL", group.bidding_url->spec());
}
if (group.bidding_wasm_helper_url) {
dict.Set("biddingWasmHelperURL", group.bidding_wasm_helper_url->spec());
}
if (group.update_url) {
// It doesn't really make sense to set `update_url` without one of these
// being true.
DCHECK(set_update_url_ || set_daily_update_url_);
if (set_update_url_) {
dict.Set("updateURL", group.update_url->spec());
}
if (set_daily_update_url_) {
dict.Set("dailyUpdateUrl", group.update_url->spec());
}
}
if (group.trusted_bidding_signals_url) {
dict.Set("trustedBiddingSignalsURL",
group.trusted_bidding_signals_url->spec());
}
if (group.trusted_bidding_signals_coordinator) {
dict.Set("trustedBiddingSignalsCoordinator",
group.trusted_bidding_signals_coordinator->Serialize());
}
if (group.view_and_click_counts_providers) {
base::Value::List providers;
for (const url::Origin& provider :
*group.view_and_click_counts_providers) {
providers.Append(provider.Serialize());
}
dict.Set("viewAndClickCountsProviders", std::move(providers));
}
if (group.user_bidding_signals) {
dict.Set("userBiddingSignals", JsonToValue(*group.user_bidding_signals));
}
if (group.trusted_bidding_signals_keys) {
base::Value::List keys;
for (const auto& key : *group.trusted_bidding_signals_keys) {
keys.Append(key);
}
dict.Set("trustedBiddingSignalsKeys", std::move(keys));
}
if (group.ads) {
dict.Set("ads", MakeAdsValue(*group.ads));
}
if (group.ad_components) {
dict.Set("adComponents", MakeAdsValue(*group.ad_components));
}
if (group.ad_sizes) {
dict.Set("adSizes", AdSizesToDict(*group.ad_sizes));
}
if (group.size_groups) {
dict.Set("sizeGroups", SizeGroupsToDict(*group.size_groups));
}
if (!group.auction_server_request_flags.empty()) {
dict.Set(
"auctionServerRequestFlags",
AuctionServerRequestFlagsToList(group.auction_server_request_flags));
}
switch (group.execution_mode) {
case blink::InterestGroup::ExecutionMode::kCompatibilityMode:
dict.Set("executionMode", "compatibility");
break;
case blink::InterestGroup::ExecutionMode::kGroupedByOriginMode:
dict.Set("executionMode", "group-by-origin");
break;
case blink::InterestGroup::ExecutionMode::kFrozenContext:
dict.Set("executionMode", "frozenContext");
break;
}
if (group.additional_bid_key) {
dict.Set("additionalBidKey",
base::Base64Encode(*group.additional_bid_key));
}
std::string interest_group_string;
CHECK(base::JSONWriter::Write(dict, &interest_group_string));
return EvalJs(execution_target ? *execution_target : shell(),
base::StringPrintf(R"(
(async function() {
try {
await navigator.joinAdInterestGroup(
%s, /*join_duration_sec=*/ 300);
return 'success';
} catch (e) {
return e.toString();
}
})())",
interest_group_string.c_str()))
.ExtractString();
}
// If `execution_target` is non-null, uses it as the target. Otherwise, uses
// shell().
EvalJsResult UpdateInterestGroupsInJS(
const std::optional<ToRenderFrameHost> execution_target = std::nullopt) {
return EvalJs(execution_target ? *execution_target : shell(), R"(
(function() {
try {
navigator.updateAdInterestGroups();
} catch (e) {
return e.toString();
}
return 'done';
})())");
}
// Attempts to leave the specified interest group. Returns kSuccess if the
// operation claims to have succeeded, and the exception message on failure.
//
// If `execution_target` is non-null, uses it as the target. Otherwise, uses
// shell().
[[nodiscard]] std::string LeaveInterestGroup(
url::Origin owner,
std::string name,
const std::optional<ToRenderFrameHost> execution_target = std::nullopt) {
return EvalJs(execution_target ? *execution_target : shell(),
JsReplace(R"(
(async function() {
try {
await navigator.leaveAdInterestGroup({name: $1, owner: $2});
return 'success';
} catch (e) {
return e.toString();
}
})())",
name, owner))
.ExtractString();
}
// Just like LeaveInterestGroupInJS(), but also verifies that the interest
// group was left or not, depending on the return value.
[[nodiscard]] std::string LeaveInterestGroupAndVerify(
const url::Origin& owner,
const std::string& name,
const std::optional<ToRenderFrameHost> execution_target = std::nullopt) {
int initial_count = GetJoinCount(owner, name);
std::string result = LeaveInterestGroup(owner, name, execution_target);
int final_count = GetJoinCount(owner, name);
if (result == kSuccess) {
// On or after success, the user should no longer be in the interest
// group.
while (final_count > 0) {
base::RunLoop().RunUntilIdle();
final_count = GetJoinCount(owner, name);
}
EXPECT_EQ(0, final_count);
} else {
// On failure, nothing should have changed.
EXPECT_EQ(initial_count, final_count);
}
return result;
}
// Attempts to leave all interest groups joined by the current frame owned be
// `owner` except for `groups_to_keep`. Returns kSuccess if the operation
// claims to have succeeded, and the exception message on failure.
//
// If `execution_target` is non-null, uses it as the target. Otherwise, uses
// shell().
[[nodiscard]] std::string ClearOriginJoinedInterestGroups(
const url::Origin& owner,
const std::optional<std::vector<std::string>>& groups_to_keep =
std::nullopt,
const std::optional<ToRenderFrameHost> execution_target = std::nullopt) {
ToRenderFrameHost target = execution_target ? *execution_target : shell();
if (!groups_to_keep) {
return EvalJs(target, JsReplace(R"(
(async function() {
try {
await navigator.clearOriginJoinedAdInterestGroups($1);
return 'success';
} catch (e) {
return e.toString();
}
})())",
owner))
.ExtractString();
}
base::Value::List name_list;
for (const auto& group : *groups_to_keep) {
name_list.Append(group);
}
return EvalJs(target, JsReplace(R"(
(async function() {
try {
await navigator.clearOriginJoinedAdInterestGroups($1, $2);
return 'success';
} catch (e) {
return e.toString();
}
})())",
owner, base::Value(std::move(name_list))))
.ExtractString();
}
// Wrapper around ClearOriginJoinedInterestGroups() that also checks that the
// correct set of interest groups were left.
[[nodiscard]] std::string ClearOriginJoinedInterestGroupsAndVerify(
const url::Origin& owner,
const std::optional<std::vector<std::string>>& groups_to_keep =
std::nullopt,
const std::optional<ToRenderFrameHost> execution_target = std::nullopt) {
ToRenderFrameHost target = execution_target ? *execution_target : shell();
content::RenderFrameHost* main_frame =
content::WebContents::FromRenderFrameHost(target.render_frame_host())
->GetPrimaryMainFrame();
url::Origin main_frame_origin = main_frame->GetLastCommittedOrigin();
const auto initial_groups = GetAllInterestGroupDetails();
std::string result = ClearOriginJoinedInterestGroups(owner, groups_to_keep,
execution_target);
bool success = (result == kSuccess);
for (const auto& group : initial_groups) {
const std::string& name = group->interest_group.name;
int final_join_count = GetJoinCount(group->interest_group.owner, name);
// If the leave failed, nothing should have changed.
if (!success) {
EXPECT_EQ(group->bidding_browser_signals->join_count, final_join_count);
continue;
}
if (owner != group->interest_group.owner ||
main_frame_origin != group->joining_origin) {
// Groups with different origins or joined by different origins should
// not be modified in any way.
EXPECT_EQ(group->bidding_browser_signals->join_count, final_join_count);
} else if (groups_to_keep && base::Contains(*groups_to_keep, name)) {
// Interest groups that are excluded by name also should not be
// modified.
EXPECT_EQ(group->bidding_browser_signals->join_count, final_join_count);
} else {
// Other interest groups should have been left.
EXPECT_EQ(0, final_join_count);
}
}
return result;
}
std::vector<url::Origin> GetAllInterestGroupsOwners() {
std::vector<url::Origin> interest_group_owners;
base::RunLoop run_loop;
manager_->GetAllInterestGroupOwners(base::BindLambdaForTesting(
[&run_loop, &interest_group_owners](std::vector<url::Origin> owners) {
interest_group_owners = std::move(owners);
run_loop.Quit();
}));
run_loop.Run();
return interest_group_owners;
}
scoped_refptr<StorageInterestGroups> GetInterestGroupsForOwner(
const url::Origin& owner) {
scoped_refptr<StorageInterestGroups> result;
base::RunLoop run_loop;
manager_->GetInterestGroupsForOwner(
/*devtools_auction_id=*/std::nullopt, owner,
base::BindLambdaForTesting(
[&result, &run_loop](scoped_refptr<StorageInterestGroups> groups) {
result = std::move(groups);
run_loop.Quit();
}));
run_loop.Run();
return result;
}
std::vector<SingleStorageInterestGroup> GetAllInterestGroupDetails() {
std::vector<SingleStorageInterestGroup> interest_groups;
for (const auto& owner : GetAllInterestGroupsOwners()) {
scoped_refptr<StorageInterestGroups> owner_groups =
GetInterestGroupsForOwner(owner);
for (const SingleStorageInterestGroup& group :
owner_groups->GetInterestGroups()) {
interest_groups.push_back(group);
}
}
return interest_groups;
}
std::vector<blink::InterestGroupKey> GetAllInterestGroups() {
std::vector<blink::InterestGroupKey> interest_groups;
for (const auto& storage_group : GetAllInterestGroupDetails()) {
interest_groups.emplace_back(storage_group->interest_group.owner,
storage_group->interest_group.name);
}
return interest_groups;
}
std::optional<SingleStorageInterestGroup> GetInterestGroup(
const url::Origin& owner,
const std::string& name) {
std::optional<SingleStorageInterestGroup> result;
base::RunLoop run_loop;
manager_->GetInterestGroup(
owner, name,
base::BindLambdaForTesting(
[&run_loop,
&result](std::optional<SingleStorageInterestGroup> group) {
result = std::move(group);
run_loop.Quit();
}));
run_loop.Run();
return result;
}
int GetJoinCount(const url::Origin& owner, const std::string& name) {
std::optional<SingleStorageInterestGroup> group =
GetInterestGroup(owner, name);
if (!group) {
return 0;
}
return group.value()->bidding_browser_signals->join_count;
}
// Typically, even tests get view and click data by loading an interest group
// and observing the events in that group's browser signals. However,
// sometimes joining an interest group in a test might not be possible, so
// this method allows checking whether raw click and view data exists given
// `provider_origin` and `eligible_origin`
std::optional<bool> CheckViewClickInfoInDb(url::Origin provider_origin,
url::Origin eligible_origin) {
base::test::TestFuture<std::optional<bool>> future;
manager_->CheckViewClickInfoInDbForTesting(std::move(provider_origin),
std::move(eligible_origin),
future.GetCallback());
return future.Take();
}
// If `execution_target` is non-null, uses it as the target. Otherwise, uses
// shell().
[[nodiscard]] std::string JoinInterestGroupAndVerify(
const blink::InterestGroup& group,
const std::optional<ToRenderFrameHost> execution_target = std::nullopt) {
std::optional<SingleStorageInterestGroup> initial_interest_group =
GetInterestGroup(group.owner, group.name);
std::string result = JoinInterestGroup(group, execution_target);
std::optional<SingleStorageInterestGroup> final_interest_group =
GetInterestGroup(group.owner, group.name);
if (result == kSuccess) {
// On success, the user should have joined the interest group, which
// should have been overwritten with `group`.
EXPECT_TRUE(final_interest_group);
if (final_interest_group) {
if (!initial_interest_group) {
EXPECT_EQ(1, final_interest_group.value()
->bidding_browser_signals->join_count);
} else {
EXPECT_EQ(initial_interest_group.value()
->bidding_browser_signals->join_count +
1,
final_interest_group.value()
->bidding_browser_signals->join_count);
}
// Check that the interest group in the store matches `group`, except
// the expiration.
blink::InterestGroup expected_group = group;
expected_group.expiry =
final_interest_group.value()->interest_group.expiry;
IgExpectEqualsForTesting(
/*actual=*/final_interest_group.value()->interest_group,
/*expected=*/expected_group);
}
} else {
// On failure, nothing should have changed.
if (!initial_interest_group) {
EXPECT_FALSE(final_interest_group);
} else {
EXPECT_EQ(
initial_interest_group.value()->bidding_browser_signals->join_count,
final_interest_group.value()->bidding_browser_signals->join_count);
IgExpectEqualsForTesting(
/*actual=*/final_interest_group.value()->interest_group,
/*expected=*/initial_interest_group.value()->interest_group);
}
}
return result;
}
// Simplified method to join an interest group for tests that only care about
// a few fields.
[[nodiscard]] std::string JoinInterestGroupAndVerify(
const url::Origin& owner,
const std::string& name,
double priority,
blink::InterestGroup::ExecutionMode execution_mode =
blink::InterestGroup::ExecutionMode::kCompatibilityMode,
std::optional<GURL> bidding_url = std::nullopt,
std::optional<std::vector<blink::InterestGroup::Ad>> ads = std::nullopt,
std::optional<std::vector<blink::InterestGroup::Ad>> ad_components =
std::nullopt,
std::optional<ToRenderFrameHost> execution_target = std::nullopt) {
return JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(owner, name)
.SetPriority(priority)
.SetExecutionMode(execution_mode)
.SetBiddingUrl(std::move(bidding_url))
.SetAds(std::move(ads))
.SetAdComponents(std::move(ad_components))
.Build(),
execution_target);
}
[[nodiscard]] std::string CreateAuctionNonceAndWait() {
std::string auction_nonce =
EvalJs(shell(), "navigator.createAuctionNonce()").ExtractString();
// Validate nonce is in the correct format.
EXPECT_EQ("00000000-0000-0000-0000-000000000000",
ConvertUuidWithOnlyZeros(auction_nonce));
return auction_nonce;
}
// If `execution_target` is non-null, uses it as the target. Otherwise, uses
// shell().
[[nodiscard]] content::EvalJsResult RunAuctionAndWait(
const std::string& auction_config_json,
const std::optional<ToRenderFrameHost> execution_target = std::nullopt) {
return EvalJs(execution_target ? *execution_target : shell(),
base::StringPrintf(
R"(
// Helper for setting up additional bids.
// It returns a promise that resolves when a network request that receives the
// additional bids as a header is received. additionalBids in an AuctionConfig
// can be set to the returned value.
function provideAdditionalBids(seller, nonce, bidStringList,
injectFault = "none") {
let url = seller + "/additionalBidsHandler?" + nonce + "&" + injectFault;
for (bidString of bidStringList) {
url += "&" + btoa(bidString);
}
return fetch(url, {adAuctionHeaders: true});
}
(async function() {
let auctionConfig = %s;
// Our test bots can get kinda slow, so bump script execution time limits
// in tests that don't specifically configure them.
let defaultTimeout = %s;
if (!("perBuyerTimeouts" in auctionConfig)) {
auctionConfig.perBuyerTimeouts = { '*': defaultTimeout };
}
if (!("sellerTimeout" in auctionConfig)) {
auctionConfig.sellerTimeout = defaultTimeout;
}
if (!("reportingTimeout" in auctionConfig)) {
auctionConfig.reportingTimeout = defaultTimeout;
}
try {
return await navigator.runAdAuction(auctionConfig);
} catch (e) {
return e.toString();
}
})())",
auction_config_json.c_str(),
base::NumberToString(
TestTimeouts::action_max_timeout().InMilliseconds())
.c_str()));
}
// Wrapper around RunAuctionAndWait that assumes the result is a URN URL and
// returns the mapped URL.
[[nodiscard]] std::string RunAuctionAndWaitForUrl(
const std::string& auction_config_json,
const std::optional<ToRenderFrameHost> execution_target = std::nullopt) {
auto result = RunAuctionAndWait(auction_config_json, execution_target);
GURL urn_url = GURL(result.ExtractString());
EXPECT_TRUE(urn_url.is_valid());
EXPECT_EQ(url::kUrnScheme, urn_url.scheme_piece());
TestFencedFrameURLMappingResultObserver observer;
ConvertFencedFrameURNToURL(urn_url, &observer);
EXPECT_TRUE(observer.mapped_url()) << urn_url;
return observer.mapped_url()->spec();
}
// Navigates an iframe with the id="test_iframe" to the provided URL and
// checks that the last committed url is the expected url. There must only be
// one iframe in the main document.
void NavigateIframeAndCheckURL(WebContents* web_contents,
const GURL& url,
const GURL& expected_commit_url) {
FrameTreeNode* parent =
FrameTreeNode::From(web_contents->GetPrimaryMainFrame());
CHECK(parent->child_count() > 0u);
FrameTreeNode* iframe = parent->child_at(0);
TestFrameNavigationObserver nav_observer(iframe->current_frame_host());
const std::string kIframeId = "test_iframe";
EXPECT_TRUE(BeginNavigateIframeToURL(web_contents, kIframeId, url));
nav_observer.Wait();
EXPECT_EQ(expected_commit_url, nav_observer.last_committed_url());
EXPECT_TRUE(nav_observer.last_navigation_succeeded());
}
// Wrapper around RunAuctionAndWait that assumes the result is a URN URL and
// tries to navigate to it. Checks that the mapped URL equals `expected_url`.
void RunAuctionAndWaitForURLAndNavigateIframe(
const std::string& auction_config_json,
GURL expected_url) {
auto result = RunAuctionAndWait(auction_config_json,
/*execution_target=*/std::nullopt);
GURL urn_url = GURL(result.ExtractString());
EXPECT_TRUE(urn_url.is_valid()) << result;
EXPECT_EQ(url::kUrnScheme, urn_url.scheme_piece()) << result;
TestFencedFrameURLMappingResultObserver observer;
ConvertFencedFrameURNToURL(urn_url, &observer);
EXPECT_TRUE(observer.mapped_url()) << urn_url;
NavigateIframeAndCheckURL(web_contents(), urn_url, expected_url);
EXPECT_EQ(expected_url, observer.mapped_url());
}
// If `execution_target` is non-null, uses it as the target. Otherwise, uses
// shell().
[[nodiscard]] content::EvalJsResult CreateAdRequestAndWait(
const std::optional<ToRenderFrameHost> execution_target = std::nullopt) {
return EvalJs(execution_target ? *execution_target : shell(),
R"(
(async function() {
try {
return await navigator.createAdRequest({
adRequestUrl: "https://example.site",
adProperties: [
{ width: "24", height: "48", slot: "first",
lang: "en-us", adType: "test-ad2", bidFloor: 42.0 }],
publisherCode: "pubCode123",
targeting: { interests: ["interest1", "interest2"] },
anonymizedProxiedSignals: [],
fallbackSource: "https://fallback.site"
});
} catch (e) {
return e.toString();
}
})())");
}
// If `execution_target` is non-null, uses it as the target. Otherwise, uses
// shell().
[[nodiscard]] content::EvalJsResult FinalizeAdAndWait(
const std::optional<ToRenderFrameHost> execution_target = std::nullopt) {
return EvalJs(execution_target ? *execution_target : shell(),
R"(
(async function() {
try {
return await navigator.createAdRequest({
adRequestUrl: "https://example.site",
adProperties: [
{ width: "24", height: "48", slot: "first",
lang: "en-us", adType: "test-ad2", bidFloor: 42.0 }],
publisherCode: "pubCode123",
targeting: { interests: ["interest1", "interest2"] },
anonymizedProxiedSignals: [],
fallbackSource: "https://fallback.site"
}).then(ads => {
return navigator.finalizeAd(ads, {
seller: "https://example.site",
decisionLogicURL: "https://example.site/script.js",
perBuyerSignals: {"example.site": { randomParam: "value1" }},
auctionSignals: "pubCode123",
sellerSignals: { someKey: "sellerValue" }
});
});
} catch (e) {
return e.toString();
}
})())");
}
// Waits until the `condition` callback over the interest groups returns true.
void WaitForInterestGroupsSatisfying(
const url::Origin& owner,
base::RepeatingCallback<bool(scoped_refptr<StorageInterestGroups>)>
condition) {
while (true) {
if (condition.Run(GetInterestGroupsForOwner(owner))) {
break;
}
}
}
// Waits until the `condition` callback over the interest groups returns
// true, running an update on the owner's groups in order to force interest
// group cache invalidation.
//
// This can be useful for loading things like clickiness data, whose updates
// intentionally don't invalidate cache.
//
// **REQUIREMENT**: At least one interest group owned by `owner` must have a
// valid updateURL and response registered, but the update response can be an
// empty dict. RegisterNoOpUpdate() and kNoOpUpdatePath can be used for this.
//
// Also, the current top-level page origin should be the same as `owner`.
void WaitForInterestGroupsSatisfyingInvalidatingCacheByUpdating(
const url::Origin& owner,
base::RepeatingCallback<bool(scoped_refptr<StorageInterestGroups>)>
condition) {
ASSERT_EQ(owner, shell()
->web_contents()
->GetPrimaryMainFrame()
->GetLastCommittedOrigin());
while (true) {
EXPECT_EQ("done", UpdateInterestGroupsInJS());
if (condition.Run(GetInterestGroupsForOwner(owner))) {
break;
}
}
}
// Registers an update at kNoOpUpdatePath that returns an empty dict, which
// is a valid update, but doesn't change the interest group. Can be used with
// WaitForInterestGroupsSatisfyingInvalidatingCacheByUpdating().
void RegisterNoOpUpdate() {
network_responder_->RegisterNetworkResponse(kNoOpUpdatePath, "{}");
}
// Waits for `url` to be requested by `embedded_https_test_server()`, or any
// other server that OnHttpsTestServerRequestMonitor() has been configured to
// monitor. `url`'s hostname is replaced with "127.0.0.1", since the embedded
// test server always claims requests were for 127.0.0.1, rather than
// revealing the hostname that was actually associated with a request.
void WaitForUrl(const GURL& url) {
GURL::Replacements replacements;
replacements.SetHostStr("127.0.0.1");
GURL wait_for_url = url.ReplaceComponents(replacements);
{
base::AutoLock auto_lock(requests_lock_);
if (received_https_test_server_requests_.count(wait_for_url) > 0u) {
return;
}
wait_for_url_ = wait_for_url;
request_run_loop_ = std::make_unique<base::RunLoop>();
}
request_run_loop_->Run();
request_run_loop_.reset();
}
void OnHttpsTestServerRequestMonitor(
const net::test_server::HttpRequest& request) {
base::AutoLock auto_lock(requests_lock_);
auto ad_auction_header = request.headers.find("Sec-Ad-Auction-Fetch");
if (ad_auction_header != request.headers.end()) {
request_path_ad_auction_header_map_[request.GetURL().path()] =
ad_auction_header->second;
}
received_https_test_server_requests_.insert(request.GetURL());
if (wait_for_url_ == request.GetURL()) {
wait_for_url_ = GURL();
request_run_loop_->Quit();
}
}
std::optional<std::string> GetAdAuctionHeaderForRequestPath(
const std::string& request_path) {
base::AutoLock auto_lock(requests_lock_);
auto it = request_path_ad_auction_header_map_.find(request_path);
if (it == request_path_ad_auction_header_map_.end()) {
return std::nullopt;
}
return it->second;
}
bool WitnessedAuctionResultForOrigin(const url::Origin& origin,
const std::string& response) {
Page& page = web_contents()->GetPrimaryPage();
AdAuctionPageData* ad_auction_page_data =
PageUserData<AdAuctionPageData>::GetOrCreateForPage(page);
return ad_auction_page_data->WitnessedAuctionResultForOrigin(origin,
response);
}
const scoped_refptr<HeaderDirectFromSellerSignals::Result>
ParseAndFindAdAuctionSignals(const url::Origin& origin,
const std::string& ad_slot) {
Page& page = web_contents()->GetPrimaryPage();
AdAuctionPageData* ad_auction_page_data =
PageUserData<AdAuctionPageData>::GetOrCreateForPage(page);
base::RunLoop run_loop;
scoped_refptr<HeaderDirectFromSellerSignals::Result> my_result;
ad_auction_page_data->ParseAndFindAdAuctionSignals(
origin, ad_slot,
base::BindLambdaForTesting(
[&run_loop, &my_result](
scoped_refptr<HeaderDirectFromSellerSignals::Result> result) {
my_result = std::move(result);
run_loop.Quit();
}));
run_loop.Run();
return my_result;
}
std::vector<SignedAdditionalBidWithMetadata>
TakeAuctionAdditionalBidsForOriginAndNonce(const url::Origin& origin,
const std::string& nonce) {
Page& page = web_contents()->GetPrimaryPage();
AdAuctionPageData* ad_auction_page_data =
PageUserData<AdAuctionPageData>::GetOrCreateForPage(page);
return ad_auction_page_data->TakeAuctionAdditionalBidsForOriginAndNonce(
origin, nonce);
}
void ClearReceivedRequests() {
base::AutoLock auto_lock(requests_lock_);
received_https_test_server_requests_.clear();
}
// View / click counts, as used by ValidateViewClickCountsInGenerateBid().
struct ViewOrClickCounts {
int32_t past_hour = 0;
int32_t past_day = 0;
int32_t past_week = 0;
int32_t past_30_days = 0;
int32_t past_90_days = 0;
};
// Runs an auction where generateBid() expects that the view and click counts
// are `expected_view_counts` and `expected_click_counts`, respectively.
//
// NOTE: The interest group to be checked must use
// kValidateViewClickBiddingLogicPath for the bidding logic path, and kAdURL
// for the first ad URL. Also, will use the current top-frame origin as the
// seller origin and the buyer origin, for simplicity.
void ValidateViewClickCountsInGenerateBid(
const ViewOrClickCounts& expected_view_counts,
const ViewOrClickCounts& expected_click_counts) {
constexpr char kBiddingLogicScriptTemplate[] = R"(
function generateBid(
interestGroup, unusedAuctionSignals, unusedPerBuyerSignals,
unusedTrustedBiddingSignals, browserSignals) {
if (!('viewCounts' in browserSignals)) {
throw 'viewCounts unexpectedly not in browserSignals';
} else if (!('clickCounts' in browserSignals)) {
throw 'clickCounts unexpectedly not in browserSignals';
}
%s
const ad = interestGroup.ads[0];
return {'ad': ad, 'bid': 1, 'render': ad.renderURL};
})";
auto check_field = [](std::string_view field, int32_t expected_value) {
constexpr char kCheckFieldTemplate[] = R"(
if (browserSignals.%s !== %d) {
throw 'Wrong browserSignals.%s, expected ' + %d + ', got ' +
browserSignals.%s;
}
)";
return base::StringPrintf(kCheckFieldTemplate, field, expected_value,
field, expected_value, field);
};
std::string checking_logic = base::StrCat({
check_field("viewCounts.pastHour", expected_view_counts.past_hour),
check_field("viewCounts.pastDay", expected_view_counts.past_day),
check_field("viewCounts.pastWeek", expected_view_counts.past_week),
check_field("viewCounts.past30Days", expected_view_counts.past_30_days),
check_field("viewCounts.past90Days", expected_view_counts.past_90_days),
check_field("clickCounts.pastHour", expected_click_counts.past_hour),
check_field("clickCounts.pastDay", expected_click_counts.past_day),
check_field("clickCounts.pastWeek", expected_click_counts.past_week),
check_field("clickCounts.past30Days",
expected_click_counts.past_30_days),
check_field("clickCounts.past90Days",
expected_click_counts.past_90_days),
});
const std::string bidding_script =
base::StringPrintf(kBiddingLogicScriptTemplate, checking_logic);
network_responder_->RegisterNetworkResponse(
kValidateViewClickBiddingLogicPath, bidding_script,
"application/javascript");
// For simplicity, both the buyer and seller are the current top-frame
// origin.
url::Origin buyer_and_seller_origin = shell()
->web_contents()
->GetPrimaryMainFrame()
->GetLastCommittedOrigin();
EXPECT_EQ(GURL(kAdURL), RunAuctionAndWaitForUrl(JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1]
})",
buyer_and_seller_origin,
embedded_https_test_server().GetURL(
buyer_and_seller_origin.host(),
"/interest_group/decision_logic.js"))));
}
bool HasServerSeenUrl(const GURL& url) {
GURL::Replacements replacements;
replacements.SetHostStr("127.0.0.1");
GURL look_for_url = url.ReplaceComponents(replacements);
base::AutoLock auto_lock(requests_lock_);
return received_https_test_server_requests_.find(look_for_url) !=
received_https_test_server_requests_.end();
}
// Returns URLs seen by the test server.
std::set<GURL> SeenUrls() {
base::AutoLock auto_lock(requests_lock_);
return received_https_test_server_requests_;
}
bool HasServerSeenUrls(const std::vector<GURL>& urls) {
for (const auto& url : urls) {
if (!HasServerSeenUrl(url)) {
return false;
}
}
return true;
}
void ExpectNotAllowedToJoinOrUpdateInterestGroup(
const url::Origin& origin,
RenderFrameHost* execution_target) {
EXPECT_EQ(
"NotAllowedError: Failed to execute 'joinAdInterestGroup' on "
"'Navigator': Feature join-ad-interest-group is not enabled by "
"Permissions Policy",
EvalJs(execution_target, JsReplace(
R"(
(async function() {
try {
await navigator.joinAdInterestGroup(
{
name: 'cars',
owner: $1,
},
/*joinDurationSec=*/1000);
} catch (e) {
return e.toString();
}
return 'done';
})())",
origin)));
EXPECT_EQ(
"NotAllowedError: Failed to execute 'updateAdInterestGroups' on "
"'Navigator': Feature join-ad-interest-group is not enabled by "
"Permissions Policy",
UpdateInterestGroupsInJS(execution_target));
}
// If `execution_target` is non-null, uses it as the target. Otherwise, uses
// shell().
void ExpectNotAllowedToLeaveInterestGroup(const url::Origin& origin,
std::string name,
RenderFrameHost* execution_target) {
EXPECT_EQ(
"NotAllowedError: Failed to execute 'leaveAdInterestGroup' on "
"'Navigator': Feature join-ad-interest-group is not enabled by "
"Permissions Policy",
EvalJs(execution_target,
base::StringPrintf(R"(
(async function() {
try {
await navigator.leaveAdInterestGroup({name: '%s', owner: '%s'});
} catch (e) {
return e.toString();
}
return 'done';
})())",
name.c_str(), origin.Serialize().c_str())));
}
// Expects that permission policy will prevent
// clearOriginJoinedAdInterestGroups() from being invoked. Checks both the 1-
// and 2-argument overloads.
void ExpectNotAllowedToClearOriginJoinedInterestGroups(
const url::Origin& origin,
RenderFrameHost* execution_target) {
const char kExpectedError[] =
"NotAllowedError: Failed to execute "
"'clearOriginJoinedAdInterestGroups' on 'Navigator': Feature "
"join-ad-interest-group is not enabled by Permissions Policy";
EXPECT_EQ(kExpectedError, EvalJs(execution_target, JsReplace(R"(
(async function() {
try {
await navigator.clearOriginJoinedAdInterestGroups($1);
} catch (e) {
return e.toString();
}
return 'done';
})())",
origin)));
EXPECT_EQ(kExpectedError, EvalJs(execution_target, JsReplace(R"(
(async function() {
try {
await navigator.clearOriginJoinedAdInterestGroups($1, ['name1', 'name2']);
} catch (e) {
return e.toString();
}
return 'done';
})())",
origin)));
}
void ExpectNotAllowedToRunAdAuction(const url::Origin& origin,
const GURL& url,
RenderFrameHost* execution_target) {
EXPECT_EQ(
"NotAllowedError: Failed to execute 'runAdAuction' on 'Navigator': "
"Feature run-ad-auction is not enabled by Permissions Policy",
RunAuctionAndWait(JsReplace(
R"(
{
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
}
)",
origin, url),
execution_target));
}
std::string WarningPermissionsPolicy(std::string feature, std::string api) {
return base::StringPrintf(
"In the future, Permissions Policy feature %s will not be enabled by "
"default in cross-origin iframes or same-origin iframes nested in "
"cross-origin iframes. Calling %s will be rejected with "
"NotAllowedError if it is not explicitly enabled",
feature.c_str(), api.c_str());
}
void ConvertFencedFrameURNToURL(
const GURL& urn_url,
TestFencedFrameURLMappingResultObserver* observer,
const std::optional<ToRenderFrameHost> execution_target = std::nullopt) {
ToRenderFrameHost adapter(execution_target ? *execution_target : shell());
FencedFrameURLMapping& fenced_frame_urls_map =
static_cast<RenderFrameHostImpl*>(adapter.render_frame_host())
->GetPage()
.fenced_frame_urls_map();
fenced_frame_urls_map.ConvertFencedFrameURNToURL(urn_url, observer);
}
std::optional<GURL> ConvertFencedFrameURNToURLInJS(
const GURL& urn_url,
bool send_reports = false,
const std::optional<ToRenderFrameHost> execution_target = std::nullopt) {
ToRenderFrameHost adapter(execution_target ? *execution_target : shell());
EvalJsResult result =
EvalJs(adapter, JsReplace("navigator.deprecatedURNToURL($1, $2)",
urn_url, send_reports));
if (!result.is_ok() || result == base::Value()) {
return std::nullopt;
}
return GURL(result.ExtractString());
}
bool ReplaceInURNInJS(
const GURL& urn_url,
const base::flat_map<std::string, std::string> replacements,
std::string* error_out = nullptr) {
base::Value::Dict replacement_value;
for (const auto& replacement : replacements) {
replacement_value.Set(replacement.first, replacement.second);
}
EvalJsResult result = EvalJs(
shell(), JsReplace(R"(
(async function() {
await navigator.deprecatedReplaceInURN($1, $2);
return 'done';
})())",
urn_url, base::Value(std::move(replacement_value))));
if (error_out != nullptr) {
*error_out = result.is_ok() ? "" : result.ExtractError();
}
return result == "done";
}
void AttachInterestGroupObserver() {
if (observer_) {
manager_->RemoveInterestGroupObserver(observer_.get());
}
observer_ = std::make_unique<TestInterestGroupObserver>();
manager_->AddInterestGroupObserver(observer_.get());
}
void WaitForAccessObserved(
const std::vector<TestInterestGroupObserver::Entry>& expected) {
observer_->WaitForAccesses(expected);
}
void WaitForAccessObservedInOrder(
const std::vector<TestInterestGroupObserver::Entry>& expected) {
observer_->WaitForAccessesInOrder(expected);
}
WebContentsImpl* web_contents() const {
return static_cast<WebContentsImpl*>(shell()->web_contents());
}
protected:
base::test::ScopedFeatureList feature_list_;
std::unique_ptr<AllowlistedOriginContentBrowserClient>
content_browser_client_;
std::unique_ptr<TestInterestGroupObserver> observer_;
raw_ptr<InterestGroupManagerImpl> manager_;
base::Lock requests_lock_;
std::set<GURL> received_https_test_server_requests_
GUARDED_BY(requests_lock_);
std::map<std::string, std::string> request_path_ad_auction_header_map_
GUARDED_BY(requests_lock_);
std::unique_ptr<base::RunLoop> request_run_loop_;
GURL wait_for_url_ GUARDED_BY(requests_lock_);
std::unique_ptr<NetworkResponder> network_responder_;
// These determine, when joining an interest group in Javascript using a
// blink::InterestGroup with a non-null update_url field, whether `updateURL`,
// `dailyUpdateUrl`, or both are set on the Javascript `interestGroup` object.
//
// TODO(crbug.com/40258629): Remove these once support for
// `dailyUpdateUrl` has been removed, and always set `updateURL` only.
bool set_update_url_ = true;
bool set_daily_update_url_ = false;
};
// At the moment, InterestGroups use either:
// a. Web-exposed `FencedFrameConfig` objects or URN urls, when fenced frames
// are enabled
// b. Normal URLs when fenced frames are not enabled
// This means they require ads be loaded in fenced frames when Chrome is running
// with the option enabled. This fixture is parameterized over whether the test
// should call `navigator.runAdAuction()` with a request to have the promise
// resolve to a JS `FencedFrameConfig` object or a URN.
class InterestGroupFencedFrameBrowserTest : public InterestGroupBrowserTest {
public:
InterestGroupFencedFrameBrowserTest() {
feature_list_.InitWithFeaturesAndParameters(
{{blink::features::kFencedFrames, {}},
{features::kPrivacySandboxAdsAPIsOverride, {}},
{blink::features::kPrivateAggregationApi, {}},
// This feature allows `runAdAuction()`'s promise to resolve to a
// `FencedFrameConfig` object upon developer request.
{blink::features::kFencedFramesAPIChanges, {}},
{blink::features::kFencedFramesDefaultMode, {}},
{blink::features::kFencedFramesCrossOriginAutomaticBeaconData, {}}},
/*disabled_features=*/{});
}
~InterestGroupFencedFrameBrowserTest() override = default;
// Runs the specified auction using RunAuctionAndWait(), expecting a success
// resulting in a URN URL. Then navigates a pre-existing fenced frame to that
// URL, expecting `expected_ad_url` to be loaded in the fenced frame.
//
// If `execution_target` is non-null, uses it as the target. Otherwise, uses
// shell().
//
// The target must already contain a single fenced frame.
void RunAuctionAndNavigateFencedFrame(
const GURL& expected_ad_url,
const std::string& auction_config_json,
std::optional<ToRenderFrameHost> execution_target = std::nullopt) {
if (!execution_target) {
execution_target = shell();
}
// For this test, we:
// 1. Run the ad auction, specifically requesting that the returned
// promise resolve to a config object that we immediately navigate to
// 2. Wait for the navigation to finish
TestFrameNavigationObserver observer(
GetFencedFrameRenderFrameHost(*execution_target));
content::EvalJsResult eval_result =
EvalJs(execution_target ? *execution_target : shell(),
base::StringPrintf(
R"(
(async function() {
try {
const auctionConfig = %s;
auctionConfig.resolveToConfig = true;
// Our test bots can get kinda slow, so bump script execution time limits
// in tests that don't specifically configure them.
let defaultTimeout = %s;
if (!("perBuyerTimeouts" in auctionConfig)) {
auctionConfig.perBuyerTimeouts = { '*': defaultTimeout };
}
if (!("sellerTimeout" in auctionConfig)) {
auctionConfig.sellerTimeout = defaultTimeout;
}
if (!("reportingTimeout" in auctionConfig)) {
auctionConfig.reportingTimeout = defaultTimeout;
}
const fencedFrameConfig = await navigator.runAdAuction(auctionConfig);
if (!(fencedFrameConfig instanceof FencedFrameConfig)) {
throw new Error('runAdAuction() did not return a FencedFrameConfig');
}
document.querySelector('fencedframe').config = fencedFrameConfig;
} catch (e) {
return e.toString();
}
})())",
auction_config_json.c_str(),
base::NumberToString(
TestTimeouts::action_max_timeout().InMilliseconds())
.c_str()));
ASSERT_THAT(
eval_result,
AnyOf(EvalJsResult::IsError(),
EvalJsResult::IsOkAndHolds(base::test::IsJson(base::Value()))))
<< "Expected string, but got " << eval_result;
WaitForFencedFrameNavigation(expected_ad_url, *execution_target, observer);
}
// Navigates the only fenced frame in `execution_target` to `url` and invokes
// `WaitForFencedFrameNavigation()`.
void NavigateFencedFrameAndWait(const GURL& url,
const GURL& expected_url,
const ToRenderFrameHost& execution_target) {
TestFrameNavigationObserver observer(
GetFencedFrameRenderFrameHost(execution_target));
EXPECT_TRUE(ExecJs(execution_target,
JsReplace("document.querySelector('fencedframe').config "
"= new FencedFrameConfig($1);",
url)));
WaitForFencedFrameNavigation(expected_url, execution_target, observer);
}
// Waits for a fenced frame navigation to complete in `execution_target`,
// expecting the frame to navigate to `expected_url`. Also checks that the URL
// is actually requested from the test server if `expected_url` is an HTTPS
// URL. `observer` must be set up before the navigation-initiating code is
// run. We wait on it in this method.
void WaitForFencedFrameNavigation(const GURL& expected_url,
const ToRenderFrameHost& execution_target,
TestFrameNavigationObserver& observer) {
// If the URL is HTTPS, wait for the URL to be requested, to make sure the
// fenced frame actually made the request and, in the MPArch case, to make
// sure the load actually started. On regression, this is likely to hang.
if (expected_url.SchemeIs(url::kHttpsScheme)) {
WaitForUrl(expected_url);
} else {
// The only other URLs this should be used with are about:blank URLs or
// loopback http URLs.
if (expected_url.SchemeIs(url::kHttpScheme)) {
EXPECT_EQ("127.0.0.1", expected_url.host());
} else {
ASSERT_EQ(GURL(url::kAboutBlankURL), expected_url);
}
}
// Wait for the load to complete.
observer.Wait();
RenderFrameHost* fenced_frame_host =
GetFencedFrameRenderFrameHost(execution_target);
// Verify that the URN was resolved to the correct URL.
EXPECT_EQ(expected_url, fenced_frame_host->GetLastCommittedURL());
// Make sure the URL was successfully committed. If the page failed to load
// the URL will be `expected_url`, but IsErrorDocument() will be true, and
// the last committed origin will be opaque.
EXPECT_FALSE(fenced_frame_host->IsErrorDocument());
// If scheme is HTTP or HTTPS, check the last committed origin here. If
// scheme is about:blank, don't do so, since url::Origin::Create() will
// return an opaque origin in that case.
if (expected_url.SchemeIsHTTPOrHTTPS()) {
EXPECT_EQ(url::Origin::Create(expected_url),
fenced_frame_host->GetLastCommittedOrigin());
}
}
// Returns the RenderFrameHostImpl for a fenced frame in `execution_target`,
// which is assumed to contain only one fenced frame and no iframes.
RenderFrameHostImpl* GetFencedFrameRenderFrameHost(
const ToRenderFrameHost& execution_target) {
return GetFencedFrame(execution_target)->GetInnerRoot();
}
// Returns FencedFrame in `execution_target` frame. Requires that
// `execution_target` have one and only one FencedFrame. MPArch only, as the
// ShadowDOM implementation doesn't use the FencedFrame class.
FencedFrame* GetFencedFrame(const ToRenderFrameHost& execution_target) {
std::vector<FencedFrame*> fenced_frames =
static_cast<RenderFrameHostImpl*>(execution_target.render_frame_host())
->GetFencedFrames();
CHECK_EQ(1u, fenced_frames.size());
return fenced_frames[0];
}
// When using default bidding and decision logic:
// Navigates the main frame, adds an interest group with a single component
// URL, and runs an auction where an ad with that component URL wins.
// Navigates a fenced frame to the winning render URL (which contains a nested
// fenced frame), and navigates that fenced frame to the component ad URL.
// Provides a common starting state for testing behavior of component ads and
// fenced frames.
//
// Writes URN for the component ad to `component_ad_urn`, if non-null.
void RunBasicAuctionWithAdComponents(
const GURL& ad_component_url,
GURL* component_ad_urn = nullptr,
std::string bidding_logic = "bidding_logic.js",
std::string decision_logic = "decision_logic.js") {
GURL test_url = embedded_https_test_server().GetURL(
"a.test", "/fenced_frames/basic.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
GURL ad_url = embedded_https_test_server().GetURL(
"c.test", "/fenced_frames/basic.html");
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
/*owner=*/url::Origin::Create(test_url),
/*name=*/"cars",
/*priority=*/0.0, /*execution_mode=*/
blink::InterestGroup::ExecutionMode::kCompatibilityMode,
/*bidding_url=*/
embedded_https_test_server().GetURL(
"a.test", "/interest_group/" + bidding_logic),
/*ads=*/{{{ad_url, /*metadata=*/std::nullopt}}},
/*ad_components=*/
{{{ad_component_url, /*metadata=*/std::nullopt}}}));
ASSERT_NO_FATAL_FAILURE(RunAuctionAndNavigateFencedFrame(
ad_url,
JsReplace(R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1]
})",
url::Origin::Create(test_url),
embedded_https_test_server().GetURL(
"a.test", "/interest_group/" + decision_logic)),
/*execution_target=*/std::nullopt));
// Get first component URL from the fenced frame.
RenderFrameHost* ad_frame = GetFencedFrameRenderFrameHost(shell());
std::optional<std::vector<GURL>> components =
GetAdAuctionComponentsInJS(ad_frame, 1);
ASSERT_TRUE(components);
ASSERT_EQ(1u, components->size());
EXPECT_EQ(url::kUrnScheme, (*components)[0].scheme_piece());
if (component_ad_urn) {
*component_ad_urn = (*components)[0];
}
// Load the ad component in the nested fenced frame. The load should
// succeed.
NavigateFencedFrameAndWait((*components)[0], ad_component_url, ad_frame);
}
std::optional<std::vector<GURL>> GetAdAuctionComponentsInJS(
const ToRenderFrameHost& execution_target,
int num_params) {
auto result = EvalJs(
execution_target,
base::StringPrintf("navigator.adAuctionComponents(%i)", num_params));
// Return nullopt if an exception was thrown, as should be the case for
// loading pages that are not the result of an auction.
if (!result.is_ok()) {
return std::nullopt;
}
// Otherwise, adAuctionComponents should always return a list, since it
// forces its input to be a number, and clamps it to the expected range.
EXPECT_TRUE(result.is_list());
if (!result.is_list()) {
return std::nullopt;
}
std::vector<GURL> out;
for (const auto& value : result.ExtractList()) {
if (!value.is_string()) {
ADD_FAILURE() << "Expected string: " << value;
return std::vector<GURL>();
}
GURL url(value.GetString());
if (!url.is_valid() || !url.SchemeIs(url::kUrnScheme)) {
ADD_FAILURE() << "Expected valid URN URL: " << value;
return std::vector<GURL>();
}
out.emplace_back(std::move(url));
}
return out;
}
// Validates that navigator.adAuctionComponents() returns URNs that map to
// `expected_ad_component_urls`. `expected_ad_component_urls` is padded with
// about:blank URLs up to blink::MaxAdAuctionAdComponents(). Calls
// adAuctionComponents() with a number of different input parameters to get a
// list of URNs and checks them against FencedFrameURLMapping to make sure
// they're mapped to `expected_ad_component_urls`, and in the same order.
void CheckAdComponents(std::vector<GURL> expected_ad_component_urls,
RenderFrameHostImpl* render_frame_host) {
size_t kMaxAdAuctionAdComponents = blink::MaxAdAuctionAdComponents();
while (expected_ad_component_urls.size() < kMaxAdAuctionAdComponents) {
expected_ad_component_urls.emplace_back(url::kAboutBlankURL);
}
std::optional<std::vector<GURL>> all_component_urls =
GetAdAuctionComponentsInJS(render_frame_host,
kMaxAdAuctionAdComponents);
ASSERT_TRUE(all_component_urls);
ASSERT_EQ(kMaxAdAuctionAdComponents, all_component_urls->size());
for (size_t i = 0; i < all_component_urls->size(); ++i) {
// All ad component URLs should use the URN scheme.
EXPECT_EQ(url::kUrnScheme, (*all_component_urls)[i].scheme_piece());
// All ad component URLs should be unique.
for (size_t j = 0; j < i; ++j) {
EXPECT_NE((*all_component_urls)[i], (*all_component_urls)[j]);
}
// Check URNs are mapped to the values in `expected_ad_component_urls`.
TestFencedFrameURLMappingResultObserver observer;
ConvertFencedFrameURNToURL((*all_component_urls)[i], &observer,
render_frame_host);
EXPECT_TRUE(observer.mapped_url());
EXPECT_EQ(expected_ad_component_urls[i], observer.mapped_url());
}
// Make sure smaller values passed to GetAdAuctionComponentsInJS() return
// the first elements of the full kMaxAdAuctionAdComponents element list
// retrieved above.
for (size_t i = 0; i < kMaxAdAuctionAdComponents; ++i) {
std::optional<std::vector<GURL>> component_urls =
GetAdAuctionComponentsInJS(render_frame_host, i);
ASSERT_TRUE(component_urls);
EXPECT_THAT(*component_urls,
testing::ElementsAreArray(all_component_urls->begin(),
all_component_urls->begin() + i));
}
// Test clamping behavior.
EXPECT_EQ(std::vector<GURL>(),
GetAdAuctionComponentsInJS(render_frame_host, -32769));
EXPECT_EQ(std::vector<GURL>(),
GetAdAuctionComponentsInJS(render_frame_host, -2));
EXPECT_EQ(std::vector<GURL>(),
GetAdAuctionComponentsInJS(render_frame_host, -1));
EXPECT_EQ(all_component_urls,
GetAdAuctionComponentsInJS(render_frame_host,
kMaxAdAuctionAdComponents + 1));
EXPECT_EQ(all_component_urls,
GetAdAuctionComponentsInJS(render_frame_host,
kMaxAdAuctionAdComponents + 2));
EXPECT_EQ(all_component_urls,
GetAdAuctionComponentsInJS(render_frame_host, 32768));
}
// Send a request that has the content "Basic request data" to the reporting
// destination. This function is used in negative test cases where a reporting
// beacon is expected not to be sent. For example:
// 1. A click without user gesture happened on ad component fenced frame, the
// reporting beacon should not be sent.
// 2. Call SendBasicRequest().
// 3. Verify the received request's content is from SendBasicRequest().
// This works because `ControllableHttpResponse` only handles one request.
void SendBasicRequest(GURL url) {
// Construct the resource request.
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory =
web_contents()
->GetPrimaryMainFrame()
->GetStoragePartition()
->GetURLLoaderFactoryForBrowserProcess();
auto request = std::make_unique<network::ResourceRequest>();
request->url = url;
request->credentials_mode = network::mojom::CredentialsMode::kOmit;
request->method = net::HttpRequestHeaders::kPostMethod;
request->trusted_params = network::ResourceRequest::TrustedParams();
request->trusted_params->isolation_info =
net::IsolationInfo::CreateTransient(/*nonce=*/std::nullopt);
std::unique_ptr<network::SimpleURLLoader> simple_url_loader =
network::SimpleURLLoader::Create(std::move(request),
TRAFFIC_ANNOTATION_FOR_TESTS);
simple_url_loader->AttachStringForUpload(
"Basic request data",
/*upload_content_type=*/"text/plain;charset=UTF-8");
network::SimpleURLLoader* simple_url_loader_ptr = simple_url_loader.get();
// Send out the beacon.
simple_url_loader_ptr->DownloadHeadersOnly(
url_loader_factory.get(),
base::DoNothingWithBoundArgs(std::move(simple_url_loader)));
}
std::unique_ptr<NetworkResponder> CreateNetworkResponder() override {
// Fenced frame window.fence.reportEvent API requires a responder that
// handles beacons sent to the reporting url.
return std::make_unique<NetworkResponder>(embedded_https_test_server(),
"/echoall?report_win_beacon");
}
protected:
base::test::ScopedFeatureList feature_list_;
};
// Make sure that FLEDGE has protections against making local network requests..
class InterestGroupPrivateNetworkBrowserTest : public InterestGroupBrowserTest {
protected:
InterestGroupPrivateNetworkBrowserTest()
: remote_test_server_(net::test_server::EmbeddedTestServer::TYPE_HTTPS) {
base::FieldTrialParams params;
params["LocalNetworkAccessChecksWarn"] = "false";
feature_list_.InitAndEnableFeatureWithParameters(
network::features::kLocalNetworkAccessChecks, params);
remote_test_server_.SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
remote_test_server_.AddDefaultHandlers(GetTestDataFilePath());
remote_test_server_.RegisterRequestMonitor(base::BindRepeating(
&InterestGroupBrowserTest::OnHttpsTestServerRequestMonitor,
base::Unretained(this)));
// Need to bind the remote test server's socket so can get port when setting
// up the command line, but can't have the test server start accepting
// connections yet, so the network responder can be set up to start handling
// requests to it in CreateNetworkResponder(), which is called from
// InterestGroupBrowserTest::SetUpOnMainThread().
EXPECT_TRUE(remote_test_server_.InitializeAndListen());
}
void SetUpCommandLine(base::CommandLine* command_line) override {
command_line->AppendSwitchASCII(
network::switches::kIpAddressSpaceOverrides,
base::StringPrintf(
"%s=public",
remote_test_server_.host_port_pair().ToString().c_str()));
}
void SetUpOnMainThread() override {
InterestGroupBrowserTest::SetUpOnMainThread();
remote_test_server_.StartAcceptingConnections();
// Extend allow list to include the remote server.
content_browser_client_->AddToAllowList(
{remote_test_server_.GetOrigin("a.test"),
remote_test_server_.GetOrigin("b.test"),
remote_test_server_.GetOrigin("c.test")});
}
std::unique_ptr<NetworkResponder> CreateNetworkResponder() override {
auto network_responder = InterestGroupBrowserTest::CreateNetworkResponder();
network_responder->RegisterWithServer(remote_test_server_);
return network_responder;
}
protected:
// Test server which is treated as being from a public IP address space
// (IPAddressSpace::kPublic), despite being served from a localhost IP
// address. This is configured in the SetUpCommandLine() call above, which
// makes the network service consider all connections to its IP+port to be be
// to a IPAddressSpace::kPublic address. Can't use the more official
// "Content-Security-Policy: treat-as-public-address", because that's not
// implemented in the network stack, and is only applied after getting the
// response (i.e., not when establishing a new connection and deciding if it
// needs PNA preflights).
//
// embedded_https_test_server() is also served from a localhost IP address,
// but without the command line switch, that's considered
// IPAddressSpace::kLoopback, using the standard IP to IPAddressSpace mapping
// logic.
//
// These are are based around interactions between the "loopback"
// `embedded_https_test_server()` and the "public" `remote_test_server_` and
// the Private Network Access preflight logic.
net::test_server::EmbeddedTestServer remote_test_server_;
base::test::ScopedFeatureList feature_list_;
};
// More restricted Permissions Policy is set for features join-ad-interest-group
// and run-ad-auction (EnableForSelf instead of EnableForAll) when runtime flag
// kAdInterestGroupAPIRestrictedPolicyByDefault is enabled.
class InterestGroupRestrictedPermissionsPolicyBrowserTest
: public InterestGroupBrowserTest {
public:
InterestGroupRestrictedPermissionsPolicyBrowserTest() {
feature_list_.InitAndEnableFeature(
network::features::kAdInterestGroupAPIRestrictedPolicyByDefault);
}
protected:
base::test::ScopedFeatureList feature_list_;
};
class InterestGroupMultiBidBrowserTest : public InterestGroupBrowserTest {
public:
InterestGroupMultiBidBrowserTest() {
feature_list_.InitAndEnableFeature(blink::features::kFledgeMultiBid);
}
protected:
base::test::ScopedFeatureList feature_list_;
};
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
SameOriginJoinLeaveInterestGroup) {
GURL test_url_a = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin test_origin_a = url::Origin::Create(test_url_a);
ASSERT_TRUE(test_url_a.SchemeIs(url::kHttpsScheme));
ASSERT_TRUE(NavigateToURL(shell(), test_url_a));
// This join should succeed.
EXPECT_EQ(kSuccess, JoinInterestGroupAndVerify(test_origin_a, "cars"));
// This join should fail and throw an exception since a.test is not the same
// origin as the bidding_url, bid.a.test.
EXPECT_EQ(
base::StringPrintf(
"TypeError: Failed to execute 'joinAdInterestGroup' on 'Navigator': "
"biddingLogicURL 'https://bid.a.test/' for AuctionAdInterestGroup "
"with owner '%s' and name 'bicycles' biddingLogicURL must have the "
"same origin as the InterestGroup owner and have no fragment "
"identifier or embedded credentials.",
test_origin_a.Serialize().c_str()),
JoinInterestGroupAndVerify(blink::TestInterestGroupBuilder(
/*owner=*/test_origin_a,
/*name=*/"bicycles")
.SetBiddingUrl(GURL("https://bid.a.test"))
.Build()));
// This join should fail and throw an exception since a.test is not the same
// origin as the update_url, update.a.test.
EXPECT_EQ(
base::StringPrintf(
"TypeError: Failed to execute 'joinAdInterestGroup' on 'Navigator': "
"updateURL 'https://update.a.test/' for AuctionAdInterestGroup with "
"owner '%s' and name 'tricycles' updateURL must have the same origin "
"as the InterestGroup owner and have no fragment identifier or "
"embedded credentials.",
test_origin_a.Serialize().c_str()),
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin_a,
/*name=*/"tricycles")
.SetUpdateUrl(GURL("https://update.a.test"))
.Build()));
// This join should silently fail since d.test is not allowlisted for the API,
// and allowlist checks only happen in the browser process, so don't throw an
// exception. Can't use JoinInterestGroupAndVerify() because of the silent
// failure.
GURL test_url_d = embedded_https_test_server().GetURL("d.test", "/echo");
url::Origin test_origin_d = url::Origin::Create(test_url_d);
ASSERT_TRUE(NavigateToURL(shell(), test_url_d));
EXPECT_EQ(kSuccess, JoinInterestGroup(test_origin_d, "toys"));
// Another successful join.
GURL test_url_b = embedded_https_test_server().GetURL("b.test", "/echo");
url::Origin test_origin_b = url::Origin::Create(test_url_b);
ASSERT_TRUE(NavigateToURL(shell(), test_url_b));
EXPECT_EQ(kSuccess, JoinInterestGroupAndVerify(test_origin_b, "trucks"));
// Check that only the a.test and b.test interest groups were added to
// the database.
std::vector<blink::InterestGroupKey> expected_groups = {
{test_origin_a, "cars"}, {test_origin_b, "trucks"}};
std::vector<blink::InterestGroupKey> received_groups;
received_groups = GetAllInterestGroups();
EXPECT_THAT(received_groups,
testing::UnorderedElementsAreArray(expected_groups));
// Now test leaving
// Test that we can't leave an interest group from a site not allowedlisted
// for the API.
// Inject an interest group into the DB for that site so we can try
// to remove it.
manager_->JoinInterestGroup(blink::TestInterestGroupBuilder(
/*owner=*/test_origin_d,
/*name=*/"candy")
.Build(),
test_origin_d.GetURL());
ASSERT_TRUE(NavigateToURL(shell(), test_url_d));
// This leave should do nothing because `origin_d` is not allowed by privacy
// sandbox. Can't use LeaveInterestGroupAndVerify() because it returns "true"
// but doesn't actually leave the interest group.
EXPECT_EQ(kSuccess, LeaveInterestGroup(test_origin_d, "candy"));
ASSERT_TRUE(NavigateToURL(shell(), test_url_b));
// This leave should do nothing because there is not interest group of that
// name.
EXPECT_EQ(kSuccess, LeaveInterestGroupAndVerify(test_origin_b, "cars"));
// This leave should succeed.
ASSERT_TRUE(NavigateToURL(shell(), test_url_a));
EXPECT_EQ(kSuccess, LeaveInterestGroupAndVerify(test_origin_a, "cars"));
// We expect that `test_origin_b` and the (injected) `test_origin_d` interest
// groups remain.
expected_groups = {{test_origin_b, "trucks"}, {test_origin_d, "candy"}};
received_groups = GetAllInterestGroups();
EXPECT_THAT(received_groups,
testing::UnorderedElementsAreArray(expected_groups));
}
IN_PROC_BROWSER_TEST_F(
InterestGroupBrowserTest,
SameOriginClearOriginJoinedInterestGroupsWithoutGroupsToKeep) {
GURL test_url_a = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin test_origin_a = url::Origin::Create(test_url_a);
ASSERT_TRUE(test_url_a.SchemeIs(url::kHttpsScheme));
ASSERT_TRUE(NavigateToURL(shell(), test_url_a));
// These joins should all succeed.
EXPECT_EQ(kSuccess, JoinInterestGroupAndVerify(test_origin_a, "name1"));
EXPECT_EQ(kSuccess, JoinInterestGroupAndVerify(test_origin_a, "name2"));
EXPECT_EQ(kSuccess, JoinInterestGroupAndVerify(test_origin_a, "name3"));
// Directly join an interest group from a different main frame origin. This
// should not be left by the clear call.
manager_->JoinInterestGroup(
blink::TestInterestGroupBuilder(test_origin_a, "name4").Build(),
/*joining_url=*/embedded_https_test_server().GetURL("b.test", "/echo"));
EXPECT_EQ(kSuccess, ClearOriginJoinedInterestGroupsAndVerify(test_origin_a));
}
IN_PROC_BROWSER_TEST_F(
InterestGroupBrowserTest,
SameOriginClearOriginJoinedInterestGroupsWithGroupsToKeep) {
const char kName1[] = "name1";
const char kName2[] = "name2";
const char kName3[] = "name3";
const char kName4[] = "name4";
GURL test_url_a = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin test_origin_a = url::Origin::Create(test_url_a);
ASSERT_TRUE(test_url_a.SchemeIs(url::kHttpsScheme));
ASSERT_TRUE(NavigateToURL(shell(), test_url_a));
// These joins should all succeed.
EXPECT_EQ(kSuccess, JoinInterestGroupAndVerify(test_origin_a, kName1));
EXPECT_EQ(kSuccess, JoinInterestGroupAndVerify(test_origin_a, kName2));
EXPECT_EQ(kSuccess, JoinInterestGroupAndVerify(test_origin_a, kName3));
// Directly join an interest group from a different main frame origin. This
// should not be left by the clear call.
manager_->JoinInterestGroup(
blink::TestInterestGroupBuilder(test_origin_a, kName4).Build(),
embedded_https_test_server().GetURL("b.test", "/echo"));
// Progressively reduce the number of interest groups to keep. Each request
// should succeed. These calls should all check that the right set of interest
// groups were left.
EXPECT_EQ(kSuccess,
ClearOriginJoinedInterestGroupsAndVerify(
test_origin_a, /*groups_to_keep=*/{{kName1, kName2, kName3}}));
EXPECT_EQ(kSuccess,
ClearOriginJoinedInterestGroupsAndVerify(
test_origin_a, /*groups_to_keep=*/{{kName1, kName3}}));
EXPECT_EQ(kSuccess, ClearOriginJoinedInterestGroupsAndVerify(
test_origin_a, /*groups_to_keep=*/{{}}));
}
// Make sure that ClearOriginJoinedInterestGroups() sends leave notifications
// for the right set of interest groups. This is separate from the above tests
// because the *AndVerify() series of methods results in extra notifications.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
SameOriginClearOriginJoinedInterestGroupsNotifications) {
const char kName1[] = "name1";
const char kName2[] = "name2";
const char kName3[] = "name3";
const char kName4[] = "name4";
AttachInterestGroupObserver();
GURL test_url_a = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin test_origin_a = url::Origin::Create(test_url_a);
ASSERT_TRUE(test_url_a.SchemeIs(url::kHttpsScheme));
ASSERT_TRUE(NavigateToURL(shell(), test_url_a));
// These joins should all succeed.
EXPECT_EQ(kSuccess, JoinInterestGroup(test_origin_a, kName1));
EXPECT_EQ(kSuccess, JoinInterestGroup(test_origin_a, kName2));
EXPECT_EQ(kSuccess, JoinInterestGroup(test_origin_a, kName3));
// Directly join an interest group from a different main frame origin. This
// should not be left by the clear call.
manager_->JoinInterestGroup(
blink::TestInterestGroupBuilder(test_origin_a, kName4).Build(),
/*joining_url=*/embedded_https_test_server().GetURL("b.test", "/echo"));
WaitForAccessObserved(
{{"global", TestInterestGroupObserver::kJoin, test_origin_a, kName1},
{"global", TestInterestGroupObserver::kJoin, test_origin_a, kName2},
{"global", TestInterestGroupObserver::kJoin, test_origin_a, kName3},
{"global", TestInterestGroupObserver::kJoin, test_origin_a, kName4}});
EXPECT_EQ(kSuccess, ClearOriginJoinedInterestGroups(
test_origin_a, /*groups_to_keep=*/{{kName2}}));
// kName2 and kName4 should not be cleared. Don't check exact order, as
// there's no guarantee about the order groups will be listed in.
WaitForAccessObserved(
{{"global", TestInterestGroupObserver::kClear, test_origin_a, kName1},
{"global", TestInterestGroupObserver::kClear, test_origin_a, kName3}});
}
// Can't join or leave interest groups from http://localhost, even though it's
// a "secure context" (since it's potentially trustworthy).
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, CantJoinLeaveHttpLocalhost) {
GURL test_url = embedded_test_server()->GetURL("/echo");
ASSERT_TRUE(test_url.SchemeIs(url::kHttpScheme));
ASSERT_TRUE(NavigateToURL(shell(), test_url));
EXPECT_EQ(
"SecurityError: Failed to execute 'joinAdInterestGroup' on 'Navigator': "
"May only joinAdInterestGroup from an https origin.",
JoinInterestGroup(url::Origin::Create(GURL("https://example.org/")),
"cars"));
EXPECT_EQ(
"SecurityError: Failed to execute 'leaveAdInterestGroup' on 'Navigator': "
"May only leaveAdInterestGroup from an https origin.",
LeaveInterestGroup(url::Origin::Create(GURL("https://example.org/")),
"cars"));
EXPECT_EQ(
"SecurityError: Failed to execute 'clearOriginJoinedAdInterestGroups' on "
"'Navigator': May only clearOriginJoinedAdInterestGroups from an https "
"origin.",
ClearOriginJoinedInterestGroups(
url::Origin::Create(GURL("https://example.org/"))));
}
// Test the case of a cross-origin iframe joining and leaving same-origin
// interest groups. This should succeed without any .well-known fetches needed.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
SameOriginIframeJoinLeaveInterestGroup) {
const char kGroup[] = "goats";
// b.test iframes a.test. The iframe should be able to successfully join and
// leave a.test interest group without needing any .well-known fetches.
GURL main_url = embedded_https_test_server().GetURL(
"b.test",
"/cross_site_iframe_factory.html?b.test("
"a.test{allow-join-ad-interest-group}"
")");
ASSERT_TRUE(NavigateToURL(shell(), main_url));
url::Origin group_origin = embedded_https_test_server().GetOrigin("a.test");
FrameTreeNode* parent =
FrameTreeNode::From(web_contents()->GetPrimaryMainFrame());
ASSERT_GT(parent->child_count(), 0u);
RenderFrameHost* iframe = parent->child_at(0)->current_frame_host();
// Both joining and leaving should work.
EXPECT_EQ(kSuccess, JoinInterestGroupAndVerify(group_origin, kGroup, iframe));
EXPECT_EQ(kSuccess,
LeaveInterestGroupAndVerify(group_origin, kGroup, iframe));
// clearOriginJoinedAdInterestGroups() should also work.
EXPECT_EQ(kSuccess, JoinInterestGroupAndVerify(group_origin, kGroup, iframe));
EXPECT_EQ(kSuccess,
ClearOriginJoinedInterestGroupsAndVerify(
group_origin, /*groups_to_keep=*/std::nullopt, iframe));
}
// Test cross-origin joining/leaving of interest groups, in the case an IG owner
// only allows cross-origin joining. Only allow joining to make sure join and
// leave permissions are tracked separately.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
CrossOriginJoinAllowedByWellKnownFetch) {
const char kGroup[] = "aardvarks";
url::Origin allow_join_origin = url::Origin::Create(
GURL(embedded_https_test_server().GetURL("allow-join.a.test", "/")));
// Navigate to a cross-origin URL.
ASSERT_TRUE(NavigateToURL(
shell(), embedded_https_test_server().GetURL("b.test", "/echo")));
// Joining a group cross-origin should succeed.
EXPECT_EQ(kSuccess, JoinInterestGroupAndVerify(allow_join_origin, kGroup));
// Leaving the group should fail.
EXPECT_EQ("NotAllowedError: Permission to leave interest group denied.",
LeaveInterestGroupAndVerify(allow_join_origin, kGroup));
EXPECT_EQ("NotAllowedError: Permission to leave interest groups denied.",
ClearOriginJoinedInterestGroupsAndVerify(
url::Origin::Create(GURL("https://example.org/"))));
}
// Test cross-origin joining/leaving of interest groups, in the case an IG owner
// only allows cross-origin leaving. Only allow leaving to make sure leave and
// leave permissions are tracked separately.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
CrossOriginLeaveAllowedByWellKnownFetch) {
// Group that's joined with a same-origin request.
const char kJoinSucceedsGroup[] = "join-succeeds-group";
// Group where joining fails due to the request being cross-origin, and
// cross-origin joins being blocked.
const char kJoinFailsGroup[] = "join-fails-group";
GURL allow_leave_url =
GURL(embedded_https_test_server().GetURL("allow-leave.a.test", "/echo"));
url::Origin allow_leave_origin = url::Origin::Create(allow_leave_url);
// Navigate to the origin that allows leaving only, and join one of its
// groups, which should succeed.
ASSERT_TRUE(NavigateToURL(shell(), allow_leave_url));
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(allow_leave_origin, kJoinSucceedsGroup));
// Navigate to a cross-origin URL.
GURL cross_origin_url =
embedded_https_test_server().GetURL("b.test", "/echo");
ASSERT_TRUE(NavigateToURL(shell(), cross_origin_url));
// Try to join an `allow_leave_origin` group, which should fail.
EXPECT_EQ("NotAllowedError: Permission to join interest group denied.",
JoinInterestGroupAndVerify(allow_leave_origin, kJoinFailsGroup));
// Leaving the group that was successfully joined earlier should succeed.
EXPECT_EQ(kSuccess, LeaveInterestGroupAndVerify(allow_leave_origin,
kJoinSucceedsGroup));
// Clearing interest groups joined from the current origin should succeed. To
// test, join a URL from both navigate to origins directly (bypassing
// .well-known checks), and then call
// ClearOriginJoinedInterestGroupsAndVerify(), which should only leave the one
// joined from the currently navigated to URL.
manager_->JoinInterestGroup(
blink::TestInterestGroupBuilder(allow_leave_origin, kJoinSucceedsGroup)
.Build(),
/*joining_url=*/allow_leave_url.EmptyGURL());
manager_->JoinInterestGroup(
blink::TestInterestGroupBuilder(allow_leave_origin, kJoinFailsGroup)
.Build(),
/*joining_url=*/cross_origin_url.EmptyGURL());
EXPECT_EQ(kSuccess,
ClearOriginJoinedInterestGroupsAndVerify(allow_leave_origin));
}
// Test cross-origin joining/leaving of interest groups from an iframe, in the
// case an IG owner only allows cross-origin joining. Only allow joining to make
// sure join and leave permissions are tracked separately.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
CrossOriginIframeJoinAllowedByWellKnownFetch) {
const char kGroup[] = "aardvarks";
url::Origin allow_join_origin = url::Origin::Create(
GURL(embedded_https_test_server().GetURL("allow-join.a.test", "/")));
// allow-join.a.test iframes b.test. The iframe should require preflights to
// be able to join or leave allow-join.a.test's interest groups. In this test,
// the preflights only allow joins.
GURL main_url = embedded_https_test_server().GetURL(
"allow-join.a.test",
"/cross_site_iframe_factory.html?allow-join.a.test("
"b.test{allow-join-ad-interest-group}"
")");
ASSERT_TRUE(NavigateToURL(shell(), main_url));
FrameTreeNode* parent =
FrameTreeNode::From(web_contents()->GetPrimaryMainFrame());
ASSERT_GT(parent->child_count(), 0u);
RenderFrameHost* iframe = parent->child_at(0)->current_frame_host();
// Joining a group cross-origin should succeed.
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
allow_join_origin, kGroup,
/*priority=*/0, /*execution_mode=*/
blink::InterestGroup::ExecutionMode::kCompatibilityMode,
/*bidding_url=*/std::nullopt,
/*ads=*/std::nullopt,
/*ad_components=*/std::nullopt, iframe));
// Leaving the group should fail.
EXPECT_EQ("NotAllowedError: Permission to leave interest group denied.",
LeaveInterestGroupAndVerify(allow_join_origin, kGroup, iframe));
// Clearing should fail as well.
EXPECT_EQ("NotAllowedError: Permission to leave interest groups denied.",
ClearOriginJoinedInterestGroupsAndVerify(
allow_join_origin, /*groups_to_keep=*/std::nullopt, iframe));
}
// Test cross-origin joining/leaving of interest groups from an iframe, in the
// case an IG owner only allows cross-origin leaving. Only allow leaving to make
// sure join and leave permissions are tracked separately.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
CrossOriginIframeLeaveAllowedByWellKnownFetch) {
// Group that's joined with a same-origin request.
const char kJoinSucceedsGroup[] = "join-succeeds-group";
// Group where joining fails due to the request being cross-origin, and
// cross-origin joins being blocked.
const char kJoinFailsGroup[] = "join-fails-group";
url::Origin allow_leave_origin =
embedded_https_test_server().GetOrigin("allow-leave.a.test");
// allow-leave.a.test iframes b.test. The iframe should require preflights to
// be able to join or leave allow-leave.a.test's interest groups. In this
// test, the preflights only allow leaves.
GURL main_url = embedded_https_test_server().GetURL(
"allow-leave.a.test",
"/cross_site_iframe_factory.html?allow-leave.a.test("
"b.test{allow-join-ad-interest-group}"
")");
ASSERT_TRUE(NavigateToURL(shell(), main_url));
// The main frame joins kJoinSucceedsGroup, which should succeed.
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(allow_leave_origin, kJoinSucceedsGroup));
FrameTreeNode* parent =
FrameTreeNode::From(web_contents()->GetPrimaryMainFrame());
ASSERT_GT(parent->child_count(), 0u);
RenderFrameHost* iframe = parent->child_at(0)->current_frame_host();
// Try to join an `allow_leave_origin` group from the iframe, which should
// fail.
EXPECT_EQ("NotAllowedError: Permission to join interest group denied.",
JoinInterestGroupAndVerify(
allow_leave_origin, kJoinFailsGroup,
/*priority=*/0.0, /*execution_mode=*/
blink::InterestGroup::ExecutionMode::kCompatibilityMode,
/*bidding_url=*/std::nullopt,
/*ads=*/std::nullopt,
/*ad_components=*/
std::nullopt,
/*execution_target=*/iframe));
// Leaving the group from the iframe that was successfully joined earlier
// should succeed.
EXPECT_EQ(kSuccess, LeaveInterestGroupAndVerify(allow_leave_origin,
kJoinSucceedsGroup, iframe));
}
// Test the case cross-origin joining/leaving of interest groups is blocked by
// the ContentBrowserClient, but allowed by the .well-known URL. In this case,
// the .well-known URL should be fetched, and the return value should be derived
// from that fetch returned, but the database should not updated, regardless of
// whether the .well-known URL allows it. This can happen if, for example,
// cookies blocking is enabled for a site.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
CrossOriginJoinLeaveBlockedByContentBrowserClient) {
const char kGroup1[] = "aardvarks";
const char kGroup2[] = "wombats";
const char kGroup3[] = "wallabies";
// Interest groups operations are not allowed on "*.d.test" by the
// ContentBrowserClient. One allows only joins, one only leaves, which should
// affect return values, but not whether the page can actually join or leave
// cross-origin interest groups.
url::Origin allow_join_origin =
embedded_https_test_server().GetOrigin("allow-join.d.test");
url::Origin allow_leave_origin =
embedded_https_test_server().GetOrigin("allow-leave.d.test");
// Join kGroup2 directly for both origins, so can check leave calls have no
// effect.
blink::InterestGroup interest_group;
interest_group.owner = allow_join_origin;
interest_group.name = kGroup2;
interest_group.expiry = base::Time::Now() + base::Days(1);
// The joining URL doesn't actually matter.
manager_->JoinInterestGroup(
interest_group,
/*joining_url=*/embedded_https_test_server().GetURL("allow-join.d.test",
"/"));
interest_group.owner = allow_leave_origin;
manager_->JoinInterestGroup(
interest_group,
/*joining_url=*/embedded_https_test_server().GetURL("allow-leave.d.test",
"/"));
// Navigate to a cross-origin URL.
GURL cross_origin_url =
embedded_https_test_server().GetURL("b.test", "/echo");
ASSERT_TRUE(NavigateToURL(shell(), cross_origin_url));
// Join/leave calls for `allow_join_origin` should claim joining succeeded,
// and leaving failed, but neither call should actually affect what interest
// groups the user is in.
EXPECT_EQ(kSuccess, JoinInterestGroup(allow_join_origin, kGroup1));
EXPECT_EQ("NotAllowedError: Permission to leave interest group denied.",
LeaveInterestGroup(allow_join_origin, kGroup2));
// Join/leave calls for `allow_leave_origin` should claim joining failed, and
// leaving succeeded, but neither call should actually affect what interest
// groups the user is in.
EXPECT_EQ("NotAllowedError: Permission to join interest group denied.",
JoinInterestGroup(allow_leave_origin, kGroup1));
EXPECT_EQ(kSuccess, LeaveInterestGroup(allow_leave_origin, kGroup2));
// The user should still be in kGroup2, but not kGroup1, for both origins.
std::vector<blink::InterestGroupKey> expected_groups = {
{allow_join_origin, kGroup2}, {allow_leave_origin, kGroup2}};
EXPECT_THAT(GetAllInterestGroups(),
testing::UnorderedElementsAreArray(expected_groups));
// Clearing should claim to succeed as as well, but do nothing. Join an
// interest group directly from the current page, bypassing checks, so there's
// an interest group that should not be left to check.
manager_->JoinInterestGroup(blink::TestInterestGroupBuilder(
/*owner=*/allow_leave_origin,
/*name=*/kGroup3)
.Build(),
/*joining_url=*/cross_origin_url);
EXPECT_EQ(kSuccess, ClearOriginJoinedInterestGroups(allow_leave_origin));
expected_groups.emplace_back(allow_leave_origin, kGroup3);
EXPECT_THAT(GetAllInterestGroups(),
testing::UnorderedElementsAreArray(expected_groups));
}
// Test cross-origin joining of interest groups requires CORS.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, CrossOriginJoinNoCors) {
const char kGroup[] = "aardvarks";
url::Origin no_cors_origin = url::Origin::Create(
GURL(embedded_https_test_server().GetURL("no-cors.a.test", "/")));
// Navigate to a cross-origin URL.
ASSERT_TRUE(NavigateToURL(
shell(), embedded_https_test_server().GetURL("b.test", "/echo")));
// Joining a group should fail.
EXPECT_EQ("NotAllowedError: Permission to join interest group denied.",
JoinInterestGroupAndVerify(no_cors_origin, kGroup));
}
// Test cross-origin leaving of interest groups requires CORS.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, CrossOriginLeaveNoCors) {
const char kGroup[] = "aardvarks";
GURL no_cors_url =
GURL(embedded_https_test_server().GetURL("no-cors.a.test", "/echo"));
url::Origin no_cors_origin = url::Origin::Create(no_cors_url);
// Navigate to `no_cors_url` and join an IG, which should succeed, since it's
// a same-origin join.
ASSERT_TRUE(NavigateToURL(shell(), no_cors_url));
EXPECT_EQ(kSuccess, JoinInterestGroupAndVerify(no_cors_origin, kGroup));
// Navigate to a cross-origin URL.
ASSERT_TRUE(NavigateToURL(
shell(), embedded_https_test_server().GetURL("b.test", "/echo")));
// Leaving the group should fail.
EXPECT_EQ("NotAllowedError: Permission to leave interest group denied.",
LeaveInterestGroupAndVerify(no_cors_origin, kGroup));
}
// Test the renderer restricting the number of active cross-origin joins per
// frame. One page tries to join kMaxActiveCrossSiteJoins+1 cross-origin
// interest groups from 3 different origins, the last two requests are to two
// distinct origins. All but the last are sent to the browser process. The
// results in two .well-known permissions requests. While those two requests are
// pending, the last request is held back in the renderer.
//
// Then the site joins and leaves a same-origin interest group, which should
// bypass the queue. Then one of the hung .well-known requests completes, which
// should allow the final cross-origin join to send out its .well-known request.
//
// Then a cross-origin leave request is issued for the group just joined, which
// should not wait before sending the request to the browser process, since
// leaves and joins are throttled separately. The browser process then leaves
// the group immediately, using the cached result of the previous .well-known
// fetch.
//
// The remaining two .well-known requests for the joins are then completed,
// which should result in all pending joins completing successfully.
//
// The title of the page is updated when each promise completes successfully, to
// allow waiting on promises that were created earlier in the test run.
//
// Only 3 cross-site origins are used to limit the test to 3 simultaneous
// .well-known requests. Using too many would run into the network stack's
// request throttling code.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, CrossOriginJoinQueue) {
// This matches the value in navigator_auction.cc in blink/.
const int kMaxActiveCrossSiteJoins = 20;
// Since this is using another port from `cross_origin_server` below, the
// hostname doesn't matter, but use a different one, just in case.
GURL main_url = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin main_origin = url::Origin::Create(main_url);
std::vector<std::unique_ptr<net::test_server::ControllableHttpResponse>>
permissions_responses;
net::EmbeddedTestServer cross_origin_server(
net::test_server::EmbeddedTestServer::TYPE_HTTPS);
cross_origin_server.SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
// There should be 3 .well-known requests for the cross-origin joins. The
// cross-origin leave should use a cached result.
for (int i = 0; i < 3; ++i) {
permissions_responses.emplace_back(
std::make_unique<net::test_server::ControllableHttpResponse>(
&cross_origin_server,
"/.well-known/interest-group/permissions/?origin=" +
base::EscapeQueryParamValue(main_origin.Serialize(),
/*use_plus=*/false)));
}
ASSERT_TRUE(cross_origin_server.Start());
// Navigate to a cross-origin URL.
ASSERT_TRUE(NavigateToURL(shell(), main_url));
for (int i = 0; i < kMaxActiveCrossSiteJoins + 1; ++i) {
const char* other_origin_host = "0.b.test";
if (i == kMaxActiveCrossSiteJoins - 1) {
other_origin_host = "1.b.test";
} else if (i == kMaxActiveCrossSiteJoins) {
other_origin_host = "2.b.test";
}
url::Origin other_origin = cross_origin_server.GetOrigin(other_origin_host);
content_browser_client_->AddToAllowList({other_origin});
ExecuteScriptAsync(shell(),
JsReplace(R"(
navigator.joinAdInterestGroup(
{name: $1, owner: $2}, /*joinDurationSec=*/ 300)
.then(() => {
// Append the first character of the owner's host to the title.
document.title += (new URL($2)).host[0];
});)",
base::NumberToString(i), other_origin));
// Wait for .well-known requests to be made for "0.b.test" and "1.b.test".
// Need to wait for them immediately after the Javascript calls that should
// trigger the requests to prevent their order from being racily reversed at
// the network layer.
if (i == 0) {
permissions_responses[0]->WaitForRequest();
EXPECT_TRUE(base::StartsWith(
permissions_responses[0]->http_request()->headers.at("Host"),
"0.b.test"));
} else if (i == kMaxActiveCrossSiteJoins - 1) {
permissions_responses[1]->WaitForRequest();
EXPECT_TRUE(base::StartsWith(
permissions_responses[1]->http_request()->headers.at("Host"),
"1.b.test"));
}
}
// Clear title, as each successful join modifies the title, so need a basic
// title to start with. Can't set an empty title, so use "_" instead.
ExecuteScriptAsync(shell(), "document.title='_'");
// Joining and leaving a same-origin interest group should not be throttled.
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(main_origin, "helmets for unicorns"));
EXPECT_EQ(kSuccess,
LeaveInterestGroupAndVerify(main_origin, "helmets for unicorns"));
// The "2.b.test" cross-origin join should still be waiting for one of the
// other cross-site joins to complete.
EXPECT_FALSE(permissions_responses[2]->has_received_request());
// Complete the "1.b.test" .well-known request, which should cause the
// "2.b.test" join request to be sent to the browser, which should issue
// another .well-known request.
TitleWatcher title_watcher1(web_contents(), u"_1");
permissions_responses[1]->Send(
net::HttpStatusCode::HTTP_OK,
/*content_type=*/"application/json",
/*content=*/
R"({"joinAdInterestGroup" : true, "leaveAdInterestGroup" : true})",
/*cookies=*/{},
/*extra_headers=*/{"Access-Control-Allow-Origin: *"});
permissions_responses[1]->Done();
EXPECT_EQ(u"_1", title_watcher1.WaitAndGetTitle());
// The "2.b.test" cross-origin join should advance out of the queue and send a
// .well-known request.
permissions_responses[2]->WaitForRequest();
EXPECT_TRUE(base::StartsWith(
permissions_responses[2]->http_request()->headers.at("Host"),
"2.b.test"));
// A new cross-origin leave should bypass the join queue, and start
// immediately, retrieving the previous .well-known response from the cache.
EXPECT_EQ(kSuccess,
LeaveInterestGroupAndVerify(
/*owner=*/cross_origin_server.GetOrigin("1.b.test"),
/*name=*/base::NumberToString(kMaxActiveCrossSiteJoins)));
// Complete the "2.b.test" join's .well-known request.
TitleWatcher title_watcher2(web_contents(), u"_12");
permissions_responses[2]->Send(
net::HttpStatusCode::HTTP_OK,
/*content_type=*/"application/json",
/*content=*/R"({"joinAdInterestGroup" : true})",
/*cookies=*/{},
/*extra_headers=*/{"Access-Control-Allow-Origin: *"});
permissions_responses[2]->Done();
EXPECT_EQ(u"_12", title_watcher2.WaitAndGetTitle());
// Complete the "0.b.test" joins' .well-known request.
std::u16string final_title =
u"_12" + std::u16string(kMaxActiveCrossSiteJoins - 1, u'0');
TitleWatcher title_watcher3(web_contents(), final_title);
permissions_responses[0]->Send(
net::HttpStatusCode::HTTP_OK,
/*content_type=*/"application/json",
/*content=*/R"({"joinAdInterestGroup" : true})",
/*cookies=*/{},
/*extra_headers=*/{"Access-Control-Allow-Origin: *"});
permissions_responses[0]->Done();
EXPECT_EQ(final_title, title_watcher3.WaitAndGetTitle());
}
// The inverse of CrossOriginJoinQueue. Unlike most leave tests, leaves interest
// groups the user isn't actually in.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, CrossOriginLeaveQueue) {
// This matches the value in navigator_auction.cc in blink/.
const int kMaxActiveCrossSiteLeaves = 20;
// Since this is using another port from `cross_origin_server` below, the
// hostname doesn't matter, but use a different one, just in case.
GURL main_url = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin main_origin = url::Origin::Create(main_url);
std::vector<std::unique_ptr<net::test_server::ControllableHttpResponse>>
permissions_responses;
net::EmbeddedTestServer cross_origin_server(
net::test_server::EmbeddedTestServer::TYPE_HTTPS);
cross_origin_server.SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
// There should be 3 .well-known requests for the cross-origin leaves. The
// cross-origin join should use a cached result.
for (int i = 0; i < 3; ++i) {
permissions_responses.emplace_back(
std::make_unique<net::test_server::ControllableHttpResponse>(
&cross_origin_server,
"/.well-known/interest-group/permissions/?origin=" +
base::EscapeQueryParamValue(main_origin.Serialize(),
/*use_plus=*/false)));
}
ASSERT_TRUE(cross_origin_server.Start());
// Navigate to a cross-origin URL.
ASSERT_TRUE(NavigateToURL(shell(), main_url));
for (int i = 0; i < kMaxActiveCrossSiteLeaves + 1; ++i) {
const char* other_origin_host = "0.b.test";
if (i == kMaxActiveCrossSiteLeaves - 1) {
other_origin_host = "1.b.test";
} else if (i == kMaxActiveCrossSiteLeaves) {
other_origin_host = "2.b.test";
}
url::Origin other_origin = cross_origin_server.GetOrigin(other_origin_host);
content_browser_client_->AddToAllowList({other_origin});
ExecuteScriptAsync(shell(),
JsReplace(R"(
navigator.leaveAdInterestGroup({name: $1, owner: $2})
.then(() => {
// Append the first character of the owner's host to the title.
document.title += (new URL($2)).host[0];
});)",
base::NumberToString(i), other_origin));
// Wait for .well-known requests to be made for "0.b.test" and "1.b.test".
// Need to wait for them immediately after the Javascript calls that should
// trigger the requests to prevent their order from being racily reversed at
// the network layer.
if (i == 0) {
permissions_responses[0]->WaitForRequest();
EXPECT_TRUE(base::StartsWith(
permissions_responses[0]->http_request()->headers.at("Host"),
"0.b.test"));
} else if (i == kMaxActiveCrossSiteLeaves - 1) {
permissions_responses[1]->WaitForRequest();
EXPECT_TRUE(base::StartsWith(
permissions_responses[1]->http_request()->headers.at("Host"),
"1.b.test"));
}
}
// Clear title, as each successful leave modifies the title, so need a basic
// title to start with. Can't set an empty title, so use "_" instead.
ExecuteScriptAsync(shell(), "document.title='_'");
// Joining and leaving a same-origin interest group should not be throttled.
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(main_origin, "helmets for unicorns"));
EXPECT_EQ(kSuccess,
LeaveInterestGroupAndVerify(main_origin, "helmets for unicorns"));
// The "2.b.test" cross-origin leave should still be waiting for one of the
// other cross-site leaves to complete.
EXPECT_FALSE(permissions_responses[2]->has_received_request());
// Complete the "1.b.test" .well-known request, which should cause the
// "2.b.test" leave request to be sent to the browser, which should issue
// another .well-known request.
TitleWatcher title_watcher1(web_contents(), u"_1");
permissions_responses[1]->Send(
net::HttpStatusCode::HTTP_OK,
/*content_type=*/"application/json",
/*content=*/
R"({"joinAdInterestGroup" : true, "leaveAdInterestGroup" : true})",
/*cookies=*/{},
/*extra_headers=*/{"Access-Control-Allow-Origin: *"});
permissions_responses[1]->Done();
EXPECT_EQ(u"_1", title_watcher1.WaitAndGetTitle());
// The "2.b.test" cross-origin leave should advance out of the queue and send
// a .well-known request.
permissions_responses[2]->WaitForRequest();
EXPECT_TRUE(base::StartsWith(
permissions_responses[2]->http_request()->headers.at("Host"),
"2.b.test"));
// A new cross-origin join should bypass the leave queue, and start
// immediately, retrieving the previous .well-known response from the cache.
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
/*owner=*/cross_origin_server.GetOrigin("1.b.test"),
/*name=*/base::NumberToString(kMaxActiveCrossSiteLeaves),
/*priority=*/0.0));
// Complete the "2.b.test" leave's .well-known request.
TitleWatcher title_watcher2(web_contents(), u"_12");
permissions_responses[2]->Send(
net::HttpStatusCode::HTTP_OK,
/*content_type=*/"application/json",
/*content=*/R"({"leaveAdInterestGroup" : true})",
/*cookies=*/{},
/*extra_headers=*/{"Access-Control-Allow-Origin: *"});
permissions_responses[2]->Done();
EXPECT_EQ(u"_12", title_watcher2.WaitAndGetTitle());
// Complete the "0.b.test" leaves' .well-known request.
std::u16string final_title =
u"_12" + std::u16string(kMaxActiveCrossSiteLeaves - 1, u'0');
TitleWatcher title_watcher3(web_contents(), final_title);
permissions_responses[0]->Send(
net::HttpStatusCode::HTTP_OK,
/*content_type=*/"application/json",
/*content=*/R"({"leaveAdInterestGroup" : true})",
/*cookies=*/{},
/*extra_headers=*/{"Access-Control-Allow-Origin: *"});
permissions_responses[0]->Done();
EXPECT_EQ(final_title, title_watcher3.WaitAndGetTitle());
}
// Much like CrossOriginJoinQueue, but navigates the page when the queue is
// full. Makes sure started joins complete successfully, and a join that was
// still queued when the frame was navigated away is dropped.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
CrossOriginJoinAndNavigateAway) {
// This matches the value in navigator_auction.cc in blink/.
const int kMaxActiveCrossSiteJoins = 20;
// Since this is using another port from `cross_origin_server` below, the
// hostname doesn't matter, but use a different one, just in case.
GURL main_url = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin main_origin = url::Origin::Create(main_url);
// URL with same origin as `main_url` to navigate to afterwards. Same origin
// so that the renderer process will be shared.
GURL same_origin_url =
embedded_https_test_server().GetURL("a.test", "/echo?2");
net::EmbeddedTestServer cross_origin_server(
net::test_server::EmbeddedTestServer::TYPE_HTTPS);
cross_origin_server.SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
std::vector<std::unique_ptr<net::test_server::ControllableHttpResponse>>
permissions_responses;
// While there should only be 2 .well-known requests in this test, create an
// extra ControllableHttpResponse so can make sure it never sees a request.
for (int i = 0; i < 3; ++i) {
permissions_responses.emplace_back(
std::make_unique<net::test_server::ControllableHttpResponse>(
&cross_origin_server,
"/.well-known/interest-group/permissions/?origin=" +
base::EscapeQueryParamValue(main_origin.Serialize(),
/*use_plus=*/false)));
}
ASSERT_TRUE(cross_origin_server.Start());
// Navigate to a cross-origin URL.
ASSERT_TRUE(NavigateToURL(shell(), main_url));
for (int i = 0; i < kMaxActiveCrossSiteJoins + 1; ++i) {
const char* other_origin_host = "0.b.test";
if (i == kMaxActiveCrossSiteJoins - 1) {
other_origin_host = "1.b.test";
} else if (i == kMaxActiveCrossSiteJoins) {
other_origin_host = "2.b.test";
}
url::Origin other_origin = cross_origin_server.GetOrigin(other_origin_host);
content_browser_client_->AddToAllowList({other_origin});
ExecuteScriptAsync(shell(),
JsReplace(R"(
navigator.joinAdInterestGroup(
{name: $1, owner: $2}, /*joinDurationSec=*/ 300);)",
base::NumberToString(i), other_origin));
// Wait for .well-known requests to be made for "0.b.test" and "1.b.test".
// Need to wait for them immediately after the Javascript calls that should
// trigger the requests to prevent their order from being racily reversed at
// the network layer.
//
// Also need to wait for them to make sure the requests reach the browser
// process before the frame is navigated.
if (i == 0) {
permissions_responses[0]->WaitForRequest();
EXPECT_TRUE(base::StartsWith(
permissions_responses[0]->http_request()->headers.at("Host"),
"0.b.test"));
} else if (i == kMaxActiveCrossSiteJoins - 1) {
permissions_responses[1]->WaitForRequest();
EXPECT_TRUE(base::StartsWith(
permissions_responses[1]->http_request()->headers.at("Host"),
"1.b.test"));
}
}
// Navigate the frame.
ASSERT_TRUE(NavigateToURL(shell(), same_origin_url));
// Complete the "1.b.test" .well-known request.
permissions_responses[1]->Send(
net::HttpStatusCode::HTTP_OK,
/*content_type=*/"application/json",
/*content=*/R"({"joinAdInterestGroup" : true})",
/*cookies=*/{},
/*extra_headers=*/{"Access-Control-Allow-Origin: *"});
permissions_responses[1]->Done();
// Wait for the "1.b.test" group to be joined successfully.
while (GetJoinCount(cross_origin_server.GetOrigin("1.b.test"),
base::NumberToString(kMaxActiveCrossSiteJoins - 1)) !=
1) {
continue;
}
// Complete the "0.b.test" .well-known request.
permissions_responses[0]->Send(
net::HttpStatusCode::HTTP_OK,
/*content_type=*/"application/json",
/*content=*/R"({"joinAdInterestGroup" : true})",
/*cookies=*/{},
/*extra_headers=*/{"Access-Control-Allow-Origin: *"});
permissions_responses[0]->Done();
// Wait for two of the "0.b.test" groups to be joined successfully.
while (GetJoinCount(cross_origin_server.GetOrigin("0.b.test"),
base::NumberToString(0)) != 1) {
continue;
}
while (GetJoinCount(cross_origin_server.GetOrigin("0.b.test"),
base::NumberToString(kMaxActiveCrossSiteJoins - 2)) !=
1) {
continue;
}
// The "2.b.test" cross-origin join should never have made it to the browser
// process, let alone to the test server.
EXPECT_FALSE(permissions_responses[2]->has_received_request());
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
JoinInterestGroupUnsupportedFields) {
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin test_origin = url::Origin::Create(url);
ASSERT_TRUE(NavigateToURL(shell(), url));
AttachInterestGroupObserver();
EXPECT_EQ("done", EvalJs(shell(), JsReplace(R"(
(async function() {
try {
await navigator.joinAdInterestGroup(
{
name: 'cars',
owner: $1,
unsupportedField: 'In group',
ads: [{
renderURL: $2,
unsupportedField: 'In ad',
}],
adComponents: [{
renderURL: $2,
unsupportedField: 'In ad component',
}]
},
/*joinDurationSec=*/1000);
} catch (e) {
return e.toString();
}
return 'done';
})())",
test_origin, url)));
WaitForAccessObserved({
{"global", TestInterestGroupObserver::kJoin, test_origin, "cars"},
});
auto storage_groups = GetInterestGroupsForOwner(test_origin);
ASSERT_EQ(storage_groups->size(), 1u);
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
JoinInterestGroupMissingLifetimeMs) {
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
ASSERT_TRUE(NavigateToURL(shell(), url));
AttachInterestGroupObserver();
EXPECT_EQ(
"TypeError: Failed to execute 'joinAdInterestGroup' on 'Navigator': "
"Missing required field lifetimeMs",
EvalJs(shell(), JsReplace(R"(
(async function() {
try {
await navigator.joinAdInterestGroup(
{
name: 'cars',
owner: $1,
});
} catch (e) {
return e.toString();
}
return 'done';
})())",
url::Origin::Create(url))));
WaitForAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, JoinInterestGroupLifetimeMs) {
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin test_origin = url::Origin::Create(url);
ASSERT_TRUE(NavigateToURL(shell(), url));
AttachInterestGroupObserver();
EXPECT_EQ("done", EvalJs(shell(), JsReplace(R"(
(async function() {
try {
await navigator.joinAdInterestGroup(
{
name: 'cars',
owner: $1,
lifetimeMs: 1000000
});
} catch (e) {
return e.toString();
}
return 'done';
})())",
test_origin)));
WaitForAccessObserved({
{"global", TestInterestGroupObserver::kJoin, test_origin, "cars"},
});
auto storage_groups = GetInterestGroupsForOwner(test_origin);
ASSERT_EQ(storage_groups->size(), 1u);
// Unfortunately, we can't use MOCK_TIME in browser tests, and blink code
// doesn't run in unit tests, so we can't verify the expiry time, since
// base::Time clocks can skew backwards.
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
JoinInterestGroupLifetimeMsAndDeprecatedDuration) {
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin test_origin = url::Origin::Create(url);
ASSERT_TRUE(NavigateToURL(shell(), url));
AttachInterestGroupObserver();
EXPECT_EQ("done", EvalJs(shell(), JsReplace(R"(
(async function() {
try {
await navigator.joinAdInterestGroup(
{
name: 'cars',
owner: $1,
lifetimeMs: 1000000
},
/*joinDurationSec=*/1000);
} catch (e) {
return e.toString();
}
return 'done';
})())",
test_origin)));
WaitForAccessObserved({
{"global", TestInterestGroupObserver::kJoin, test_origin, "cars"},
});
auto storage_groups = GetInterestGroupsForOwner(test_origin);
ASSERT_EQ(storage_groups->size(), 1u);
// Unfortunately, we can't use MOCK_TIME in browser tests, and blink code
// doesn't run in unit tests, so we can't verify the expiry time, since
// base::Time clocks can skew backwards.
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
JoinInterestGroupInvalidOwner) {
ASSERT_TRUE(NavigateToURL(
shell(), embedded_https_test_server().GetURL("a.test", "/echo")));
AttachInterestGroupObserver();
EXPECT_EQ(
"TypeError: Failed to execute 'joinAdInterestGroup' on 'Navigator': "
"owner 'https://invalid^&' for AuctionAdInterestGroup with name 'cars' "
"must be a valid https origin.",
EvalJs(shell(), R"(
(async function() {
try {
await navigator.joinAdInterestGroup(
{
name: 'cars',
owner: 'https://invalid^&',
},
/*joinDurationSec=*/1000);
} catch (e) {
return e.toString();
}
return 'done';
})())"));
WaitForAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
JoinInterestGroupInvalidPrioritySignalsOverrides) {
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
std::string origin_string = url::Origin::Create(url).Serialize();
ASSERT_TRUE(NavigateToURL(shell(), url));
AttachInterestGroupObserver();
EXPECT_EQ(
"TypeError: Failed to execute 'joinAdInterestGroup' on 'Navigator': "
"Failed to read the 'prioritySignalsOverrides' property from "
"'AuctionAdInterestGroup': Only objects can be converted to record<K,V> "
"types",
EvalJs(shell(), JsReplace(R"(
(async function() {
try {
await navigator.joinAdInterestGroup(
{
name: 'cars',
owner: $1,
prioritySignalsOverrides: "Not an object",
},
/*joinDurationSec=*/1000);
} catch (e) {
return e.toString();
}
return 'done';
})())",
origin_string.c_str())));
WaitForAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
JoinInterestGroupSupportsDeprecatedNames) {
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
std::string origin_string = url::Origin::Create(url).Serialize();
ASSERT_TRUE(NavigateToURL(shell(), url));
bool logged_interest_group_counts = false, logged_latency_stats = false,
logged_group_by_origin = false;
WebContentsConsoleObserver console_observer(shell()->web_contents());
console_observer.SetFilter(base::BindLambdaForTesting(
[&logged_interest_group_counts, &logged_latency_stats,
&logged_group_by_origin](
const WebContentsConsoleObserver::Message& message) {
const std::u16string& text = message.message;
if (text ==
u"Enum SellerCapabilities used deprecated value "
u"interestGroupCounts -- \"dashed-naming\" should be used instead "
u"of \"camelCase\".") {
logged_interest_group_counts = true;
} else if (text ==
u"Enum SellerCapabilities used deprecated value "
u"latencyStats -- \"dashed-naming\" should be used instead "
u"of \"camelCase\".") {
logged_latency_stats = true;
} else if (text ==
u"Enum executionMode used deprecated value groupByOrigin "
u"-- \"dashed-naming\" should be used instead of "
u"\"camelCase\".") {
logged_group_by_origin = true;
}
return logged_interest_group_counts && logged_latency_stats &&
logged_group_by_origin;
}));
EXPECT_EQ("done", EvalJs(shell(), JsReplace(R"(
(async function() {
try {
await navigator.joinAdInterestGroup(
{
name: 'cars',
owner: $1,
sellerCapabilities: {'*': ['interestGroupCounts', 'latencyStats']},
executionMode: 'groupByOrigin',
},
/*joinDurationSec=*/1000);
} catch (e) {
return e.toString();
}
return 'done';
})())",
origin_string.c_str())));
EXPECT_TRUE(console_observer.Wait());
WaitForInterestGroupsSatisfying(
url::Origin::Create(url),
base::BindLambdaForTesting([](scoped_refptr<StorageInterestGroups>
groups) {
if (groups->size() != 1) {
return false;
}
const auto& group = groups->GetInterestGroups()[0]->interest_group;
return group.all_sellers_capabilities ==
blink::SellerCapabilitiesType(
{blink::SellerCapabilities::kInterestGroupCounts,
blink::SellerCapabilities::kLatencyStats}) &&
group.execution_mode ==
blink::InterestGroup::ExecutionMode::kGroupedByOriginMode;
}));
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
JoinInterestGroupInvalidEnumFieldsIgnored) {
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
std::string origin_string = url::Origin::Create(url).Serialize();
ASSERT_TRUE(NavigateToURL(shell(), url));
EXPECT_EQ("done", EvalJs(shell(), JsReplace(R"(
(async function() {
try {
await navigator.joinAdInterestGroup(
{
name: 'cars',
owner: $1,
sellerCapabilities: {'https://example.test': ['non-valid-capability']},
executionMode: 'non-valid-execution-mode',
},
/*joinDurationSec=*/1000);
} catch (e) {
return e.toString();
}
return 'done';
})())",
origin_string.c_str())));
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
JoinInterestGroupValidSellerCapabilities) {
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
auto origin = url::Origin::Create(url);
ASSERT_TRUE(NavigateToURL(shell(), url));
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/origin,
/*name=*/"cars")
.SetSellerCapabilities(
{{{url::Origin::Create(GURL("https://example.test")),
{blink::SellerCapabilities::kInterestGroupCounts}}}})
.SetAllSellersCapabilities(
{blink::SellerCapabilities::kLatencyStats})
.Build()));
scoped_refptr<StorageInterestGroups> groups =
GetInterestGroupsForOwner(origin);
ASSERT_EQ(groups->size(), 1u);
const blink::InterestGroup& group =
groups->GetInterestGroups()[0]->interest_group;
EXPECT_EQ(group.all_sellers_capabilities,
blink::SellerCapabilitiesType(
{blink::SellerCapabilities::kLatencyStats}));
ASSERT_TRUE(group.seller_capabilities);
ASSERT_EQ(group.seller_capabilities->size(), 1u);
EXPECT_EQ(group.seller_capabilities->at(
url::Origin::Create(GURL("https://example.test"))),
blink::SellerCapabilitiesType(
{blink::SellerCapabilities::kInterestGroupCounts}));
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
JoinInterestGroupValidSizeFields) {
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
auto origin = url::Origin::Create(url);
ASSERT_TRUE(NavigateToURL(shell(), url));
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(origin, "cars")
.SetAds({{{GURL("https://example.com/render"),
/*metadata=*/std::nullopt, /*size_group=*/"group_1"}}})
.SetAdComponents(
{{{GURL("https://example.com/component"),
/*metadata=*/std::nullopt, /*size_group=*/"group_1"}}})
.SetAdSizes(
{{{"size_1",
blink::AdSize(150, blink::AdSize::LengthUnit::kPixels, 75,
blink::AdSize::LengthUnit::kPixels)}}})
.SetSizeGroups({{{"group_1", {"size_1"}}}})
.Build()));
scoped_refptr<StorageInterestGroups> groups =
GetInterestGroupsForOwner(origin);
ASSERT_EQ(groups->size(), 1u);
const blink::InterestGroup& group =
groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(group.ads.has_value());
ASSERT_EQ(group.ads->size(), 1u);
EXPECT_EQ(group.ads.value()[0].render_url(),
GURL("https://example.com/render"));
ASSERT_TRUE(group.ads.value()[0].size_group.has_value());
EXPECT_EQ(group.ads.value()[0].size_group, "group_1");
ASSERT_EQ(group.ad_components->size(), 1u);
EXPECT_EQ(group.ad_components.value()[0].render_url(),
GURL("https://example.com/component"));
ASSERT_TRUE(group.ad_components.value()[0].size_group.has_value());
EXPECT_EQ(group.ad_components.value()[0].size_group, "group_1");
EXPECT_EQ(group.ad_sizes->size(), 1u);
ASSERT_EQ(group.ad_sizes->at("size_1"),
blink::AdSize(150, blink::AdSize::LengthUnit::kPixels, 75,
blink::AdSize::LengthUnit::kPixels));
EXPECT_EQ(group.size_groups->size(), 1u);
ASSERT_EQ(group.size_groups->at("group_1").size(), 1u);
ASSERT_EQ(group.size_groups->at("group_1").at(0), "size_1");
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
JoinInterestGroupWithAuctionServerRequestFlags) {
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
auto origin = url::Origin::Create(url);
ASSERT_TRUE(NavigateToURL(shell(), url));
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/origin,
/*name=*/"cars")
.SetAuctionServerRequestFlags(
{blink::AuctionServerRequestFlagsEnum::kOmitAds,
blink::AuctionServerRequestFlagsEnum::kIncludeFullAds,
blink::AuctionServerRequestFlagsEnum::
kOmitUserBiddingSignals})
.Build()));
scoped_refptr<StorageInterestGroups> groups =
GetInterestGroupsForOwner(origin);
ASSERT_EQ(groups->size(), 1u);
const blink::InterestGroup& group =
groups->GetInterestGroups()[0]->interest_group;
EXPECT_TRUE(group.auction_server_request_flags.Has(
blink::AuctionServerRequestFlagsEnum::kOmitAds));
EXPECT_TRUE(group.auction_server_request_flags.Has(
blink::AuctionServerRequestFlagsEnum::kIncludeFullAds));
EXPECT_TRUE(group.auction_server_request_flags.Has(
blink::AuctionServerRequestFlagsEnum::kOmitUserBiddingSignals));
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
JoinInterestGroupValidReportingIds) {
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
auto origin = url::Origin::Create(url);
const std::vector<std::string> kSelectableBuyerAndSellerReportingIds = {
"selectable_id1", "selectable_id2"};
ASSERT_TRUE(NavigateToURL(shell(), url));
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(origin, "cars")
.SetAds(
{{{GURL("https://example.com/render"),
/*metadata=*/std::nullopt, /*size_group=*/std::nullopt,
/*buyer_reporting_id=*/"brid1",
/*buyer_and_seller_reporting_id=*/"sh1",
/*selectable_buyer_and_seller_reporting_ids=*/
std::vector<std::string>{"selectable_id1",
"selectable_id2"},
/*ad_render_id=*/std::nullopt},
{GURL("https://example.com/render2"),
/*metadata=*/std::nullopt, /*size_group=*/std::nullopt,
/*buyer_reporting_id=*/std::nullopt,
/*buyer_and_seller_reporting_id=*/"sh2",
/*selectable_buyer_and_seller_reporting_ids=*/std::nullopt,
/*ad_render_id=*/std::nullopt}}})
.Build()));
scoped_refptr<StorageInterestGroups> groups =
GetInterestGroupsForOwner(origin);
ASSERT_EQ(groups->size(), 1u);
const blink::InterestGroup& group =
groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(group.ads.has_value());
ASSERT_EQ(group.ads->size(), 2u);
EXPECT_EQ(group.ads.value()[0].render_url(),
GURL("https://example.com/render"));
EXPECT_EQ(group.ads.value()[0].buyer_reporting_id, "brid1");
EXPECT_EQ(group.ads.value()[0].buyer_and_seller_reporting_id, "sh1");
ASSERT_TRUE(group.ads.value()[0]
.selectable_buyer_and_seller_reporting_ids.has_value());
EXPECT_THAT(*group.ads.value()[0].selectable_buyer_and_seller_reporting_ids,
::testing::ElementsAre("selectable_id1", "selectable_id2"));
EXPECT_EQ(group.ads.value()[1].render_url(),
GURL("https://example.com/render2"));
EXPECT_FALSE(group.ads.value()[1].buyer_reporting_id.has_value());
EXPECT_EQ(group.ads.value()[1].buyer_and_seller_reporting_id, "sh2");
EXPECT_FALSE(group.ads.value()[1]
.selectable_buyer_and_seller_reporting_ids.has_value());
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
JoinInterestGroupValidCreativeScanningMetadata) {
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
auto origin = url::Origin::Create(url);
ASSERT_TRUE(NavigateToURL(shell(), url));
blink::InterestGroup ig =
blink::TestInterestGroupBuilder(origin, "cars")
.SetAds({{{GURL("https://example.com/render"),
/*metadata=*/std::nullopt},
{GURL("https://example.com/render2"),
/*metadata=*/std::nullopt}}})
.SetAdComponents({{{GURL("https://example.com/component"),
/*metadata=*/std::nullopt}}})
.Build();
ig.ads.value()[1].creative_scanning_metadata = "scan me";
ig.ad_components.value()[0].creative_scanning_metadata = "me too";
EXPECT_EQ(kSuccess, JoinInterestGroupAndVerify(ig));
scoped_refptr<StorageInterestGroups> groups =
GetInterestGroupsForOwner(origin);
ASSERT_EQ(groups->size(), 1u);
const blink::InterestGroup& group =
groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(group.ads.has_value());
ASSERT_EQ(group.ads->size(), 2u);
EXPECT_FALSE(group.ads.value()[0].creative_scanning_metadata.has_value());
ASSERT_TRUE(group.ads.value()[1].creative_scanning_metadata.has_value());
EXPECT_EQ("scan me", *group.ads.value()[1].creative_scanning_metadata);
ASSERT_TRUE(group.ad_components.has_value());
ASSERT_EQ(group.ad_components->size(), 1u);
ASSERT_TRUE(
group.ad_components.value()[0].creative_scanning_metadata.has_value());
EXPECT_EQ("me too",
*group.ad_components.value()[0].creative_scanning_metadata);
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
JoinInterestGroupValidAdRenderId) {
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
auto origin = url::Origin::Create(url);
ASSERT_TRUE(NavigateToURL(shell(), url));
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/origin,
/*name=*/"cars")
.SetAds(
{{{GURL("https://example.com/render"),
/*metadata=*/std::nullopt,
/*size_group=*/std::nullopt,
/*buyer_reporting_id=*/std::nullopt,
/*buyer_and_seller_reporting_id=*/std::nullopt,
/*selectable_buyer_and_seller_reporting_ids=*/std::nullopt,
/*ad_render_id=*/"123abc"}}})
.SetAdComponents(
{{{GURL("https://example.com/component"),
/*metadata=*/std::nullopt,
/*size_group=*/std::nullopt,
/*buyer_reporting_id=*/std::nullopt,
/*buyer_and_seller_reporting_id=*/std::nullopt,
/*selectable_buyer_and_seller_reporting_ids=*/std::nullopt,
/*ad_render_id=*/"456def"}}})
.Build()));
scoped_refptr<StorageInterestGroups> groups =
GetInterestGroupsForOwner(origin);
ASSERT_EQ(groups->size(), 1u);
const blink::InterestGroup& group =
groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(group.ads.has_value());
ASSERT_EQ(group.ads->size(), 1u);
EXPECT_EQ(group.ads.value()[0].render_url(),
GURL("https://example.com/render"));
EXPECT_EQ(group.ads.value()[0].ad_render_id, "123abc");
ASSERT_EQ(group.ad_components->size(), 1u);
EXPECT_EQ(group.ad_components.value()[0].render_url(),
GURL("https://example.com/component"));
EXPECT_EQ(group.ad_components.value()[0].ad_render_id, "456def");
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
JoinInterestGroupValidAttestedAllowedReportingOrigins) {
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
auto origin = url::Origin::Create(url);
ASSERT_TRUE(NavigateToURL(shell(), url));
url::Origin other_origin = url::Origin::Create(
embedded_https_test_server().GetURL("b.test", "/echo"));
content_browser_client_->AddToAllowList({other_origin});
std::vector<url::Origin> allowed_reporting_origins = {other_origin};
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/origin,
/*name=*/"cars")
.SetAds(
{{{GURL("https://example.com/render"),
/*metadata=*/std::nullopt,
/*size_group=*/std::nullopt,
/*buyer_reporting_id=*/std::nullopt,
/*buyer_and_seller_reporting_id=*/std::nullopt,
/*selectable_buyer_and_seller_reporting_ids=*/std::nullopt,
/*ad_render_id=*/"123abc",
/*allowed_reporting_origins=*/allowed_reporting_origins}}})
.Build()));
scoped_refptr<StorageInterestGroups> groups =
GetInterestGroupsForOwner(origin);
ASSERT_EQ(groups->size(), 1u);
const blink::InterestGroup& group =
groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(group.ads.has_value());
ASSERT_EQ(group.ads->size(), 1u);
EXPECT_EQ(group.ads.value()[0].render_url(),
GURL("https://example.com/render"));
EXPECT_EQ(group.ads.value()[0].ad_render_id, "123abc");
EXPECT_EQ(group.ads.value()[0].allowed_reporting_origins,
allowed_reporting_origins);
}
// Some of ad's fields are ignored in adComponents, such as
// allowedReportingOrigins.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
JoinInterestGroupAdComponentsIgnoredFields) {
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin origin = url::Origin::Create(url);
ASSERT_TRUE(NavigateToURL(shell(), url));
AttachInterestGroupObserver();
EXPECT_EQ("done", EvalJs(shell(), JsReplace(R"(
(async function() {
try {
await navigator.joinAdInterestGroup(
{
name: 'cars',
owner: $1,
lifetimeMs: 1000000,
ads: [{
renderURL: 'https://ad.com/render'
}],
adComponents: [{
renderURL: 'https://ad-components.com/render',
adRenderId: '123abc',
buyerReportingId: 'ignored',
buyerAndSellerReportingId: 'ignored_2',
allowedReportingOrigins: ['https://ignored.com']
}],
});
} catch (e) {
return e.toString();
}
return 'done';
})())",
origin)));
WaitForAccessObserved({
{"global", TestInterestGroupObserver::kJoin, origin, "cars"},
});
scoped_refptr<StorageInterestGroups> groups =
GetInterestGroupsForOwner(origin);
ASSERT_EQ(groups->size(), 1u);
const blink::InterestGroup& group =
groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(group.ad_components.has_value());
ASSERT_EQ(group.ad_components->size(), 1u);
const auto& ad_component = group.ad_components.value()[0];
EXPECT_EQ(ad_component.render_url(),
GURL("https://ad-components.com/render"));
EXPECT_EQ(ad_component.ad_render_id, "123abc");
EXPECT_FALSE(ad_component.buyer_reporting_id.has_value());
EXPECT_FALSE(ad_component.buyer_and_seller_reporting_id.has_value());
EXPECT_FALSE(ad_component.allowed_reporting_origins.has_value());
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
JoinInterestGroupInvalidBiddingLogicUrl) {
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
std::string origin_string = url::Origin::Create(url).Serialize();
ASSERT_TRUE(NavigateToURL(shell(), url));
AttachInterestGroupObserver();
EXPECT_EQ(
base::StringPrintf(
"TypeError: Failed to execute 'joinAdInterestGroup' on 'Navigator': "
"biddingLogicURL 'https://invalid^&' for AuctionAdInterestGroup with "
"owner '%s' and name 'cars' cannot be resolved to a valid URL.",
origin_string.c_str()),
EvalJs(shell(), JsReplace(R"(
(async function() {
try {
await navigator.joinAdInterestGroup(
{
name: 'cars',
owner: $1,
biddingLogicURL: 'https://invalid^&',
},
/*joinDurationSec=*/1000);
} catch (e) {
return e.toString();
}
return 'done';
})())",
origin_string.c_str())));
WaitForAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
JoinInterestGroupInvalidBiddingWasmHelperUrl) {
const char kScriptTemplate[] = R"(
(async function() {
try {
await navigator.joinAdInterestGroup(
{
name: 'cars',
owner: $1,
biddingWasmHelperURL: 'https://invalid^&',
},
/*joinDurationSec=*/1000);
} catch (e) {
return e.toString();
}
return 'done';
})())";
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
std::string origin_string = url::Origin::Create(url).Serialize();
ASSERT_TRUE(NavigateToURL(shell(), url));
EXPECT_EQ(
base::StringPrintf(
"TypeError: Failed to execute 'joinAdInterestGroup' on 'Navigator': "
"biddingWasmHelperURL 'https://invalid^&' for AuctionAdInterestGroup "
"with owner '%s' and name 'cars' cannot be resolved to a valid URL.",
origin_string.c_str()),
EvalJs(shell(), JsReplace(kScriptTemplate, origin_string.c_str())));
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
JoinInterestGroupInvalidUpdateUrl) {
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
std::string origin_string = url::Origin::Create(url).Serialize();
ASSERT_TRUE(NavigateToURL(shell(), url));
AttachInterestGroupObserver();
EXPECT_EQ(
base::StringPrintf(
"TypeError: Failed to execute 'joinAdInterestGroup' on 'Navigator': "
"updateURL 'https://invalid^&' for AuctionAdInterestGroup with "
"owner '%s' and name 'cars' cannot be resolved to a valid URL.",
origin_string.c_str()),
EvalJs(shell(), JsReplace(R"(
(async function() {
try {
await navigator.joinAdInterestGroup(
{
name: 'cars',
owner: $1,
updateURL: 'https://invalid^&',
},
/*joinDurationSec=*/1000);
} catch (e) {
return e.toString();
}
return 'done';
})())",
origin_string.c_str())));
WaitForAccessObserved({});
}
// TODO(crbug.com/40258629): Remove one support for `dailyUpdateUrl` has
// been removed.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
JoinInterestGroupInvalidDailyUpdateUrl) {
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
std::string origin_string = url::Origin::Create(url).Serialize();
ASSERT_TRUE(NavigateToURL(shell(), url));
AttachInterestGroupObserver();
EXPECT_EQ(
base::StringPrintf(
"TypeError: Failed to execute 'joinAdInterestGroup' on 'Navigator': "
"dailyUpdateUrl 'https://invalid^&' for AuctionAdInterestGroup with "
"owner '%s' and name 'cars' cannot be resolved to a valid URL.",
origin_string.c_str()),
EvalJs(shell(), JsReplace(R"(
(async function() {
try {
await navigator.joinAdInterestGroup(
{
name: 'cars',
owner: $1,
dailyUpdateUrl: 'https://invalid^&',
},
/*joinDurationSec=*/1000);
} catch (e) {
return e.toString();
}
return 'done';
})())",
origin_string.c_str())));
WaitForAccessObserved({});
}
// TODO(crbug.com/40258629): Remove one support for `dailyUpdateUrl` has
// been removed.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
JoinInterestGroupDifferentUpdateURLAndDailyUpdateUrl) {
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
std::string origin_string = url::Origin::Create(url).Serialize();
ASSERT_TRUE(NavigateToURL(shell(), url));
AttachInterestGroupObserver();
EXPECT_EQ(
base::StringPrintf(
"TypeError: Failed to execute 'joinAdInterestGroup' on 'Navigator': "
"updateURL '%s' for AuctionAdInterestGroup with owner '%s' and name "
"'cars' must match dailyUpdateUrl, when both are present.",
(origin_string + "/foo").c_str(), origin_string.c_str()),
EvalJs(shell(), JsReplace(R"(
(async function() {
try {
await navigator.joinAdInterestGroup(
{
name: 'cars',
owner: $1,
updateURL: $1 + '/foo',
dailyUpdateUrl: $1 + '/bar',
},
/*joinDurationSec=*/1000);
} catch (e) {
return e.toString();
}
return 'done';
})())",
origin_string.c_str())));
WaitForAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
JoinInterestGroupInvalidTrustedBiddingSignalsUrl) {
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
std::string origin_string = url::Origin::Create(url).Serialize();
ASSERT_TRUE(NavigateToURL(shell(), url));
AttachInterestGroupObserver();
EXPECT_EQ(base::StringPrintf(
"TypeError: Failed to execute 'joinAdInterestGroup' on "
"'Navigator': trustedBiddingSignalsURL 'https://invalid^&' for "
"AuctionAdInterestGroup with owner '%s' and name 'cars' cannot "
"be resolved to a valid URL.",
origin_string.c_str()),
EvalJs(shell(), JsReplace(R"(
(async function() {
try {
await navigator.joinAdInterestGroup(
{
name: 'cars',
owner: $1,
trustedBiddingSignalsURL: 'https://invalid^&',
},
/*joinDurationSec=*/1000);
} catch (e) {
return e.toString();
}
return 'done';
})())",
origin_string.c_str())));
WaitForAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
JoinInterestGroupInvalidUserBiddingSignals) {
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
std::string origin_string = url::Origin::Create(url).Serialize();
ASSERT_TRUE(NavigateToURL(shell(), url));
AttachInterestGroupObserver();
EXPECT_EQ(
base::StringPrintf(
"TypeError: Failed to execute 'joinAdInterestGroup' on 'Navigator': "
"userBiddingSignals for AuctionAdInterestGroup with owner '%s' and "
"name 'cars' must be a JSON-serializable object.",
origin_string.c_str()),
EvalJs(shell(), JsReplace(R"(
(async function() {
try {
await navigator.joinAdInterestGroup(
{
name: 'cars',
owner: $1,
userBiddingSignals: function() {},
},
/*joinDurationSec=*/1000);
} catch (e) {
return e.toString();
}
return 'done';
})())",
origin_string.c_str())));
WaitForAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
JoinInterestGroupInvalidAdUrl) {
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
std::string origin_string = url::Origin::Create(url).Serialize();
ASSERT_TRUE(NavigateToURL(shell(), url));
AttachInterestGroupObserver();
EXPECT_EQ(
base::StringPrintf(
"TypeError: Failed to execute 'joinAdInterestGroup' on 'Navigator': "
"ad renderURL 'https://invalid^&' for AuctionAdInterestGroup with "
"owner '%s' and name 'cars' cannot be resolved to a valid URL.",
origin_string.c_str()),
EvalJs(shell(), JsReplace(R"(
(async function() {
try {
await navigator.joinAdInterestGroup(
{
name: 'cars',
owner: $1,
ads: [{renderURL:"https://invalid^&"}],
},
/*joinDurationSec=*/1000);
} catch (e) {
return e.toString();
}
return 'done';
})())",
origin_string.c_str())));
WaitForAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
JoinInterestGroupInvalidAdMetadata) {
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
std::string origin_string = url::Origin::Create(url).Serialize();
ASSERT_TRUE(NavigateToURL(shell(), url));
AttachInterestGroupObserver();
EXPECT_EQ(
base::StringPrintf(
"TypeError: Failed to execute 'joinAdInterestGroup' on "
"'Navigator': ad metadata for AuctionAdInterestGroup with "
"owner '%s' and name 'cars' must be a JSON-serializable object.",
origin_string.c_str()),
EvalJs(shell(), JsReplace(R"(
(async function() {
let x = {};
let y = {};
x.a = y;
y.a = x;
try {
await navigator.joinAdInterestGroup(
{
name: 'cars',
owner: $1,
ads: [{renderURL:"https://test.com", metadata:x}],
},
/*joinDurationSec=*/1000);
} catch (e) {
return e.toString();
}
return 'done';
})())",
origin_string.c_str())));
WaitForAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
LeaveInterestGroupInvalidOwner) {
ASSERT_TRUE(NavigateToURL(
shell(), embedded_https_test_server().GetURL("a.test", "/echo")));
AttachInterestGroupObserver();
EXPECT_EQ(
"TypeError: Failed to execute 'leaveAdInterestGroup' on 'Navigator': "
"owner 'https://invalid^&' for AuctionAdInterestGroup with name 'cars' "
"must be a valid https origin.",
EvalJs(shell(), R"(
(async function() {
try {
await navigator.leaveAdInterestGroup(
{
name: 'cars',
owner: 'https://invalid^&',
});
} catch (e) {
return e.toString();
}
return 'done';
})())"));
WaitForAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
ClearOriginJoinedInterestGroupsInvalidOwner) {
ASSERT_TRUE(NavigateToURL(
shell(), embedded_https_test_server().GetURL("a.test", "/echo")));
AttachInterestGroupObserver();
EXPECT_EQ(
"TypeError: Failed to execute 'clearOriginJoinedAdInterestGroups' on "
"'Navigator': owner 'https://invalid^&' must be a valid https origin.",
EvalJs(shell(), R"(
(async function() {
try {
await navigator.clearOriginJoinedAdInterestGroups('https://invalid^&');
} catch (e) {
return e.toString();
}
return 'done';
})())"));
WaitForAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
JoinInterestGroupInvalidAdSizeGroupEmptyName) {
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
std::string origin_string = url::Origin::Create(url).Serialize();
ASSERT_TRUE(NavigateToURL(shell(), url));
AttachInterestGroupObserver();
EXPECT_EQ(
base::StringPrintf(
"TypeError: Failed to execute 'joinAdInterestGroup' on 'Navigator': "
"ads[0].sizeGroup '' for AuctionAdInterestGroup with owner '%s' and "
"name 'cars' Size group name cannot be empty.",
origin_string.c_str()),
EvalJs(shell(), JsReplace(R"(
(async function() {
try {
await navigator.joinAdInterestGroup(
{
name: 'cars',
owner: $1,
ads: [{renderURL: "https://test.com", sizeGroup: ""}],
},
/*joinDurationSec=*/1000);
} catch (e) {
return e.toString();
}
return 'done';
})())",
origin_string.c_str())));
WaitForAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
JoinInterestGroupInvalidAdSizeGroupNoSizeGroups) {
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
std::string origin_string = url::Origin::Create(url).Serialize();
ASSERT_TRUE(NavigateToURL(shell(), url));
AttachInterestGroupObserver();
EXPECT_EQ(
base::StringPrintf(
"TypeError: Failed to execute 'joinAdInterestGroup' on 'Navigator': "
"ads[0].sizeGroup 'nonexistent' for AuctionAdInterestGroup with "
"owner '%s' and name 'cars' The assigned size group does not exist "
"in sizeGroups map.",
origin_string.c_str()),
EvalJs(shell(), JsReplace(R"(
(async function() {
try {
await navigator.joinAdInterestGroup(
{
name: 'cars',
owner: $1,
ads: [{renderURL: "https://test.com", sizeGroup: "nonexistent"}],
},
/*joinDurationSec=*/1000);
} catch (e) {
return e.toString();
}
return 'done';
})())",
origin_string.c_str())));
WaitForAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(
InterestGroupBrowserTest,
JoinInterestGroupInvalidAdSizeGroupNotContainedInSizeGroups) {
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
std::string origin_string = url::Origin::Create(url).Serialize();
ASSERT_TRUE(NavigateToURL(shell(), url));
AttachInterestGroupObserver();
EXPECT_EQ(
base::StringPrintf(
"TypeError: Failed to execute 'joinAdInterestGroup' on 'Navigator': "
"ads[0].sizeGroup 'nonexistent' for AuctionAdInterestGroup with "
"owner '%s' and name 'cars' The assigned size group does not exist "
"in sizeGroups map.",
origin_string.c_str()),
EvalJs(shell(), JsReplace(R"(
(async function() {
try {
await navigator.joinAdInterestGroup(
{
name: 'cars',
owner: $1,
ads: [{renderURL: "https://test.com", sizeGroup: "nonexistent"}],
adSizes: {"size_1": {"width": "50px", "height": "50px"}},
sizeGroups: {"group_1": ["size_1"]},
},
/*joinDurationSec=*/1000);
} catch (e) {
return e.toString();
}
return 'done';
})())",
origin_string.c_str())));
WaitForAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
JoinInterestGroupInvalidAdSizeGroupNoAdSize) {
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
std::string origin_string = url::Origin::Create(url).Serialize();
ASSERT_TRUE(NavigateToURL(shell(), url));
AttachInterestGroupObserver();
EXPECT_EQ(
base::StringPrintf(
"TypeError: Failed to execute 'joinAdInterestGroup' on 'Navigator': "
"sizeGroups '' for AuctionAdInterestGroup with owner '%s' and name "
"'cars' An adSizes map must exist for sizeGroups to work.",
origin_string.c_str()),
EvalJs(shell(), JsReplace(R"(
(async function() {
try {
await navigator.joinAdInterestGroup(
{
name: 'cars',
owner: $1,
ads: [{renderURL: "https://test.com", sizeGroup: "group_1"}],
sizeGroups: {"group_1": ["nonexistent"]},
},
/*joinDurationSec=*/1000);
} catch (e) {
return e.toString();
}
return 'done';
})())",
origin_string.c_str())));
WaitForAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
JoinInterestGroupInvalidAdComponentSizeGroupEmptyName) {
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
std::string origin_string = url::Origin::Create(url).Serialize();
ASSERT_TRUE(NavigateToURL(shell(), url));
AttachInterestGroupObserver();
EXPECT_EQ(
base::StringPrintf(
"TypeError: Failed to execute 'joinAdInterestGroup' on 'Navigator': "
"adComponents[0].sizeGroup '' for AuctionAdInterestGroup with owner "
"'%s' and name 'cars' Size group name cannot be empty.",
origin_string.c_str()),
EvalJs(shell(), JsReplace(R"(
(async function() {
try {
await navigator.joinAdInterestGroup(
{
name: 'cars',
owner: $1,
adComponents: [{renderURL: "https://test.com", sizeGroup: ""}],
},
/*joinDurationSec=*/1000);
} catch (e) {
return e.toString();
}
return 'done';
})())",
origin_string.c_str())));
WaitForAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(
InterestGroupBrowserTest,
JoinInterestGroupInvalidAdComponentSizeGroupNoSizeGroups) {
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
std::string origin_string = url::Origin::Create(url).Serialize();
ASSERT_TRUE(NavigateToURL(shell(), url));
AttachInterestGroupObserver();
EXPECT_EQ(
base::StringPrintf(
"TypeError: Failed to execute 'joinAdInterestGroup' on 'Navigator': "
"adComponents[0].sizeGroup 'nonexistent' for AuctionAdInterestGroup "
"with owner '%s' and name 'cars' The assigned size group does not "
"exist in sizeGroups map.",
origin_string.c_str()),
EvalJs(shell(), JsReplace(R"(
(async function() {
try {
await navigator.joinAdInterestGroup(
{
name: 'cars',
owner: $1,
adComponents: [{renderURL: "https://test.com", sizeGroup: "nonexistent"}],
},
/*joinDurationSec=*/1000);
} catch (e) {
return e.toString();
}
return 'done';
})())",
origin_string.c_str())));
WaitForAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(
InterestGroupBrowserTest,
JoinInterestGroupInvalidAdComponentSizeGroupNotContainedInSizeGroups) {
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
std::string origin_string = url::Origin::Create(url).Serialize();
ASSERT_TRUE(NavigateToURL(shell(), url));
AttachInterestGroupObserver();
EXPECT_EQ(
base::StringPrintf(
"TypeError: Failed to execute 'joinAdInterestGroup' on 'Navigator': "
"adComponents[0].sizeGroup 'nonexistent' for AuctionAdInterestGroup "
"with owner '%s' and name 'cars' The assigned size group does not "
"exist in sizeGroups map.",
origin_string.c_str()),
EvalJs(shell(), JsReplace(R"(
(async function() {
try {
await navigator.joinAdInterestGroup(
{
name: 'cars',
owner: $1,
adComponents: [{renderURL: "https://test.com", sizeGroup: "nonexistent"}],
adSizes: {"size_1": {"width": "50px", "height": "50px"}},
sizeGroups: {"group_1": ["size_1"]},
},
/*joinDurationSec=*/1000);
} catch (e) {
return e.toString();
}
return 'done';
})())",
origin_string.c_str())));
WaitForAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
JoinInterestGroupInvalidAdComponentSizeGroupNoAdSizes) {
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
std::string origin_string = url::Origin::Create(url).Serialize();
ASSERT_TRUE(NavigateToURL(shell(), url));
AttachInterestGroupObserver();
EXPECT_EQ(
base::StringPrintf(
"TypeError: Failed to execute 'joinAdInterestGroup' on 'Navigator': "
"sizeGroups '' for AuctionAdInterestGroup with owner '%s' and name "
"'cars' An adSizes map must exist for sizeGroups to work.",
origin_string.c_str()),
EvalJs(shell(), JsReplace(R"(
(async function() {
try {
await navigator.joinAdInterestGroup(
{
name: 'cars',
owner: $1,
adComponents: [{renderURL: "https://test.com", sizeGroup: "group_1"}],
sizeGroups: {"group_1": ["nonexistent"]},
},
/*joinDurationSec=*/1000);
} catch (e) {
return e.toString();
}
return 'done';
})())",
origin_string.c_str())));
WaitForAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
JoinInterestGroupInvalidAdSize) {
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
std::string origin_string = url::Origin::Create(url).Serialize();
ASSERT_TRUE(NavigateToURL(shell(), url));
AttachInterestGroupObserver();
EXPECT_EQ(
base::StringPrintf(
"TypeError: Failed to execute 'joinAdInterestGroup' on 'Navigator': "
"adSizes '0.000000 x 50.000000' for AuctionAdInterestGroup with "
"owner '%s' and name 'cars' Ad sizes must have a valid "
"(non-zero/non-infinite) width and height.",
origin_string.c_str()),
EvalJs(shell(), JsReplace(R"(
(async function() {
try {
await navigator.joinAdInterestGroup(
{
name: 'cars',
owner: $1,
adSizes: {"my_size": {"width": "0px", "height": "50px"}},
},
/*joinDurationSec=*/1000);
} catch (e) {
return e.toString();
}
return 'done';
})())",
origin_string.c_str())));
WaitForAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
JoinInterestGroupInvalidAdSizeUnits) {
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
std::string origin_string = url::Origin::Create(url).Serialize();
ASSERT_TRUE(NavigateToURL(shell(), url));
AttachInterestGroupObserver();
EXPECT_EQ(
base::StringPrintf(
"TypeError: Failed to execute 'joinAdInterestGroup' on 'Navigator': "
"adSizes '' for AuctionAdInterestGroup with owner '%s' and name "
"'cars' Ad size dimensions must be a valid number either in pixels "
"(px) or screen width (sw).",
origin_string.c_str()),
EvalJs(shell(), JsReplace(R"(
(async function() {
try {
await navigator.joinAdInterestGroup(
{
name: 'cars',
owner: $1,
adSizes: {"my_size": {"width": "500px", "height": "400bad"}},
},
/*joinDurationSec=*/1000);
} catch (e) {
return e.toString();
}
return 'done';
})())",
origin_string.c_str())));
WaitForAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
JoinInterestGroupInvalidAdSizeNoNumber) {
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
std::string origin_string = url::Origin::Create(url).Serialize();
ASSERT_TRUE(NavigateToURL(shell(), url));
AttachInterestGroupObserver();
EXPECT_EQ(
base::StringPrintf(
"TypeError: Failed to execute 'joinAdInterestGroup' on 'Navigator': "
"adSizes '' for AuctionAdInterestGroup with "
"owner '%s' and name 'cars' Ad size dimensions must be a valid "
"number either in pixels (px) or screen width (sw).",
origin_string.c_str()),
EvalJs(shell(), JsReplace(R"(
(async function() {
try {
await navigator.joinAdInterestGroup(
{
name: 'cars',
owner: $1,
adSizes: {"my_size": {"width": "500px", "height": "px"}},
},
/*joinDurationSec=*/1000);
} catch (e) {
return e.toString();
}
return 'done';
})())",
origin_string.c_str())));
WaitForAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
JoinInterestGroupInvalidSizeGroup) {
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
std::string origin_string = url::Origin::Create(url).Serialize();
ASSERT_TRUE(NavigateToURL(shell(), url));
AttachInterestGroupObserver();
EXPECT_EQ(
base::StringPrintf(
"TypeError: Failed to execute 'joinAdInterestGroup' on 'Navigator': "
"sizeGroups '' for AuctionAdInterestGroup with owner '%s' and name "
"'cars' Size groups cannot map from an empty group name.",
origin_string.c_str()),
EvalJs(shell(), JsReplace(R"(
(async function() {
try {
await navigator.joinAdInterestGroup(
{
name: 'cars',
owner: $1,
adSizes: {"size_1": {"width": "300px", "height": "150px"}},
sizeGroups: {"": ["size_1"]},
},
/*joinDurationSec=*/1000);
} catch (e) {
return e.toString();
}
return 'done';
})())",
origin_string.c_str())));
WaitForAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
JoinInterestGroupInvalidSizeGroupSize) {
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
std::string origin_string = url::Origin::Create(url).Serialize();
ASSERT_TRUE(NavigateToURL(shell(), url));
AttachInterestGroupObserver();
EXPECT_EQ(
base::StringPrintf(
"TypeError: Failed to execute 'joinAdInterestGroup' on 'Navigator': "
"sizeGroups 'nonexistant' for AuctionAdInterestGroup with owner '%s' "
"and name 'cars' Size does not exist in adSizes map.",
origin_string.c_str()),
EvalJs(shell(), JsReplace(R"(
(async function() {
try {
await navigator.joinAdInterestGroup(
{
name: 'cars',
owner: $1,
adSizes: {"size_1": {"width": "300px", "height": "150px"}},
sizeGroups: {"my_group": ["nonexistant"]},
},
/*joinDurationSec=*/1000);
} catch (e) {
return e.toString();
}
return 'done';
})())",
origin_string.c_str())));
WaitForAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
JoinInterestGroupInvalidAdRenderId) {
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
std::string origin_string = url::Origin::Create(url).Serialize();
ASSERT_TRUE(NavigateToURL(shell(), url));
AttachInterestGroupObserver();
EXPECT_EQ(
base::StringPrintf(
"TypeError: Failed to execute 'joinAdInterestGroup' on 'Navigator': "
"ads[0].adRenderId 'ThisIsTooLong' for AuctionAdInterestGroup with "
"owner '%s' and name 'cars' The adRenderId is too long.",
origin_string.c_str()),
EvalJs(shell(), JsReplace(R"(
(async function() {
try {
await navigator.joinAdInterestGroup(
{
name: 'cars',
owner: $1,
ads: [{renderURL: "https://test.com", adRenderId: "ThisIsTooLong"}],
},
/*joinDurationSec=*/1000);
} catch (e) {
return e.toString();
}
return 'done';
})())",
origin_string.c_str())));
WaitForAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
JoinInterestGroupNonHttpsAllowedReportingOrigins) {
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
std::string origin_string = url::Origin::Create(url).Serialize();
ASSERT_TRUE(NavigateToURL(shell(), url));
AttachInterestGroupObserver();
EXPECT_EQ(
base::StringPrintf(
"TypeError: Failed to execute 'joinAdInterestGroup' on 'Navigator': "
"ad allowedReportingOrigins '' for AuctionAdInterestGroup with "
"owner '%s' and name 'cars' must all be https origins.",
origin_string.c_str()),
EvalJs(shell(), JsReplace(R"(
(async function() {
try {
await navigator.joinAdInterestGroup(
{
name: 'cars',
owner: $1,
ads: [{
renderURL: 'https://test.com',
adRenderId: '123abc',
allowedReportingOrigins: ['http://example.com']
}],
},
/*joinDurationSec=*/ 1);
} catch (e) {
return e.toString();
}
return 'done';
})())",
origin_string.c_str())));
WaitForAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
JoinInterestGroupTooManyAllowedReportingOrigins) {
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
std::string origin_string = url::Origin::Create(url).Serialize();
ASSERT_TRUE(NavigateToURL(shell(), url));
AttachInterestGroupObserver();
EXPECT_EQ(
base::StringPrintf(
"TypeError: Failed to execute 'joinAdInterestGroup' on 'Navigator': "
"ads[0].allowedReportingOrigins '' for AuctionAdInterestGroup with "
"owner '%s' and name 'cars' allowedReportingOrigins cannot have more "
"than 10 elements.",
origin_string.c_str()),
EvalJs(shell(), JsReplace(R"(
(async function() {
try {
await navigator.joinAdInterestGroup(
{
name: 'cars',
owner: $1,
ads: [{
renderURL: 'https://test.com',
adRenderId: '123abc',
allowedReportingOrigins: [
'https://1', 'https://2', 'https://3', 'https://4', 'https://5',
'https://6', 'https://7', 'https://8', 'https://9', 'https://10',
'https://11'
]
}],
},
/*joinDurationSec=*/ 1);
} catch (e) {
return e.toString();
}
return 'done';
})())",
origin_string.c_str())));
WaitForAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
JoinInterestGroupWithValidAdditionalBidKey) {
constexpr blink::InterestGroup::AdditionalBidKey kAdditionalBidKey = {
0x10, 0x0f, 0xdf, 0x47, 0xfb, 0x94, 0xf1, 0x53, 0x6a, 0x4f, 0x7c,
0x3f, 0xda, 0x27, 0x38, 0x3f, 0xa0, 0x33, 0x75, 0xa8, 0xf5, 0x27,
0xc5, 0x37, 0xe6, 0xf1, 0x70, 0x3c, 0x47, 0xf9, 0x4f, 0x86};
constexpr char kAdditionalBidKeyBase64[] =
"EA/fR/uU8VNqT3w/2ic4P6Azdaj1J8U35vFwPEf5T4Y=";
constexpr char kAdditionalBidKeySloppyBase64[] =
" EA/fR/uU8VNqT3w/2ic4P6Azdaj1J8U35vFwPEf5T4Y";
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin origin = url::Origin::Create(url);
for (bool require_forgiving_base64 : {false, true}) {
SCOPED_TRACE(require_forgiving_base64);
AttachInterestGroupObserver();
ASSERT_TRUE(NavigateToURL(shell(), url));
EXPECT_EQ("done",
EvalJs(shell(), JsReplace(R"(
(async function() {
try {
await navigator.joinAdInterestGroup(
{
name: 'cars',
owner: $1,
additionalBidKey: $2,
},
/*joinDurationSec=*/ 1000);
} catch (e) {
return e.toString();
}
return 'done';
})())",
origin,
require_forgiving_base64
? kAdditionalBidKeySloppyBase64
: kAdditionalBidKeyBase64)));
WaitForAccessObserved({
{"global", TestInterestGroupObserver::kJoin, origin, "cars"},
});
scoped_refptr<StorageInterestGroups> groups =
GetInterestGroupsForOwner(origin);
ASSERT_EQ(groups->size(), 1u);
const blink::InterestGroup& group =
groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(group.additional_bid_key.has_value());
EXPECT_EQ(*group.additional_bid_key, kAdditionalBidKey);
}
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
JoinInterestGroupAdditionalBidKeyNotValidBase64) {
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
std::string origin_string = url::Origin::Create(url).Serialize();
ASSERT_TRUE(NavigateToURL(shell(), url));
AttachInterestGroupObserver();
EXPECT_EQ(
base::StringPrintf(
"TypeError: Failed to execute 'joinAdInterestGroup' on 'Navigator': "
"additionalBidKey 'EA/fR/uU8VNqT3w/2ic4P6Azdaj1J8U35vFwPEf5T4Y?' for "
"AuctionAdInterestGroup with owner '%s' and name 'cars' "
"cannot be base64 decoded.",
origin_string.c_str()),
EvalJs(shell(), JsReplace(R"(
(async function() {
try {
await navigator.joinAdInterestGroup(
{
name: 'cars',
owner: $1,
additionalBidKey: 'EA/fR/uU8VNqT3w/2ic4P6Azdaj1J8U35vFwPEf5T4Y?'
},
/*joinDurationSec=*/ 1000);
} catch (e) {
return e.toString();
}
return 'done';
})())",
origin_string.c_str())));
WaitForAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
JoinInterestGroupAdditionalBidKeyInvalidLength) {
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
std::string origin_string = url::Origin::Create(url).Serialize();
ASSERT_TRUE(NavigateToURL(shell(), url));
AttachInterestGroupObserver();
EXPECT_EQ(
base::StringPrintf(
"TypeError: Failed to execute 'joinAdInterestGroup' on 'Navigator': "
"additionalBidKey '2ic4P6Az' for AuctionAdInterestGroup with owner "
"'%s' and name 'cars' must be exactly 32' bytes, was 6.",
origin_string.c_str()),
EvalJs(shell(), JsReplace(R"(
(async function() {
try {
await navigator.joinAdInterestGroup(
{
name: 'cars',
owner: $1,
additionalBidKey: '2ic4P6Az'
},
/*joinDurationSec=*/ 1000);
} catch (e) {
return e.toString();
}
return 'done';
})())",
origin_string.c_str())));
WaitForAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
JoinInterestGroupWithBothAdditionalBidKeyAndAdsFails) {
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
std::string origin_string = url::Origin::Create(url).Serialize();
ASSERT_TRUE(NavigateToURL(shell(), url));
AttachInterestGroupObserver();
constexpr char kAdditionalBidKeyBase64[] =
"EA/fR/uU8VNqT3w/2ic4P6Azdaj1J8U35vFwPEf5T4Y=";
EXPECT_EQ(
base::StringPrintf(
"TypeError: Failed to execute 'joinAdInterestGroup' on 'Navigator': "
"AuctionAdInterestGroup with owner '%s' and name 'cars' "
"Interest groups that provide a value of additionalBidKey for "
"negative targeting must not provide a value for ads.",
origin_string.c_str()),
EvalJs(shell(), JsReplace(R"(
(async function() {
try {
await navigator.joinAdInterestGroup(
{
name: 'cars',
owner: $1,
additionalBidKey: $2,
ads: [{renderURL:"https://example.com/render", metadata:2}]
},
/*joinDurationSec=*/ 1000);
} catch (e) {
return e.toString();
}
return 'done';
})())",
origin_string, kAdditionalBidKeyBase64)));
WaitForAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
NegativeInterestGroupsMustNotHaveUpdateURL) {
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
std::string origin_string = url::Origin::Create(url).Serialize();
ASSERT_TRUE(NavigateToURL(shell(), url));
AttachInterestGroupObserver();
constexpr char kAdditionalBidKeyBase64[] =
"EA/fR/uU8VNqT3w/2ic4P6Azdaj1J8U35vFwPEf5T4Y=";
EXPECT_EQ(
base::StringPrintf(
"TypeError: Failed to execute 'joinAdInterestGroup' on 'Navigator': "
"AuctionAdInterestGroup with owner '%s' and name 'cars' "
"Interest groups that provide a value of additionalBidKey for "
"negative targeting must not provide an updateURL.",
origin_string.c_str()),
EvalJs(shell(), JsReplace(R"(
(async function() {
try {
await navigator.joinAdInterestGroup(
{
name: 'cars',
owner: $1,
additionalBidKey: $2,
updateURL: $3
},
/*joinDurationSec=*/ 1000);
} catch (e) {
return e.toString();
}
return 'done';
})())",
origin_string, kAdditionalBidKeyBase64,
embedded_https_test_server().GetURL(
"a.test", "/update"))));
WaitForAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
JoinInterestGroupRenamedFields) {
const GURL kAdUrl("https://example.com/render");
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin origin = url::Origin::Create(url);
std::string origin_string = origin.Serialize();
ASSERT_TRUE(NavigateToURL(shell(), url));
const blink::InterestGroup kExpectedGroupAds =
blink::TestInterestGroupBuilder(/*owner=*/origin, /*name=*/"cars")
.SetAds({{{kAdUrl, /*metadata=*/std::nullopt}}})
.Build();
const blink::InterestGroup kExpectedGroupAdComponents =
blink::TestInterestGroupBuilder(/*owner=*/origin, /*name=*/"cars")
.SetAdComponents({{{kAdUrl, /*metadata=*/std::nullopt}}})
.Build();
const blink::InterestGroup kExpectedGroupBiddingLogicUrl =
blink::TestInterestGroupBuilder(/*owner=*/origin, /*name=*/"cars")
.SetBiddingUrl(
GURL(base::StringPrintf("%s/bidding.js", origin_string.c_str())))
.Build();
const blink::InterestGroup kExpectedGroupBiddingWasmHelperUrl =
blink::TestInterestGroupBuilder(/*owner=*/origin, /*name=*/"cars")
.SetBiddingWasmHelperUrl(GURL(
base::StringPrintf("%s/bidding.wasm", origin_string.c_str())))
.Build();
const blink::InterestGroup kExpectedGroupUpdateUrl =
blink::TestInterestGroupBuilder(/*owner=*/origin, /*name=*/"cars")
.SetUpdateUrl(
GURL(base::StringPrintf("%s/update.json", origin_string.c_str())))
.Build();
const blink::InterestGroup kExpectedGroupTrustedBiddingSignalsUrl =
blink::TestInterestGroupBuilder(/*owner=*/origin, /*name=*/"cars")
.SetTrustedBiddingSignalsUrl(GURL(
base::StringPrintf("%s/signals.json", origin_string.c_str())))
.Build();
struct TestCases {
const std::string join_dict_contents;
const std::string result;
const std::optional<blink::InterestGroup> expected_group;
} kTestCases[] = {
// ***
// ads renderURL
// ***
{R"(ads: [{renderUrl: 'https://example.com/render'}])", "done",
kExpectedGroupAds},
{R"(ads: [{renderUrl: 'https://example.com/render',
renderURL: 'https://example.com/render'}])",
"done", kExpectedGroupAds},
{R"(ads: [{renderUrl: 'https://example.com/render',
renderURL: 'https://example.com/render2'}])",
"TypeError: Failed to execute 'joinAdInterestGroup' on 'Navigator': ad "
"renderUrl doesn't have the same value as ad renderURL ("
"'https://example.com/render' vs 'https://example.com/render2')",
std::nullopt},
{R"(ads: [{}])",
"TypeError: Failed to execute 'joinAdInterestGroup' on "
"'Navigator': Missing required field ad renderURL",
std::nullopt},
// ***
// adComponents renderURL
// ***
{R"(adComponents: [{renderUrl: 'https://example.com/render'}])", "done",
kExpectedGroupAdComponents},
{R"(adComponents: [{renderUrl: 'https://example.com/render',
renderURL: 'https://example.com/render'}])",
"done", kExpectedGroupAdComponents},
{R"(adComponents: [{renderUrl: 'https://example.com/render',
renderURL: 'https://example.com/render2'}])",
"TypeError: Failed to execute 'joinAdInterestGroup' on 'Navigator': ad "
"component renderUrl doesn't have the same value as ad component "
"renderURL ('https://example.com/render' vs "
"'https://example.com/render2')",
std::nullopt},
{R"(adComponents: [{}])",
"TypeError: Failed to execute 'joinAdInterestGroup' on "
"'Navigator': Missing required field ad component renderURL",
std::nullopt},
// ***
// biddingLogicURL
// ***
{base::StringPrintf(R"(biddingLogicUrl: '%s/bidding.js')",
origin_string.c_str()),
"done", kExpectedGroupBiddingLogicUrl},
{base::StringPrintf(R"(biddingLogicURL: '%s/bidding.js')",
origin_string.c_str()),
"done", kExpectedGroupBiddingLogicUrl},
{base::StringPrintf(R"(biddingLogicUrl: '%s/bidding.js',)"
R"(biddingLogicURL: '%s/bidding.js')",
origin_string.c_str(), origin_string.c_str()),
"done", kExpectedGroupBiddingLogicUrl},
{base::StringPrintf(R"(biddingLogicUrl: '%s/bidding.js',)"
R"(biddingLogicURL: '%s/bidding2.js')",
origin_string.c_str(), origin_string.c_str()),
base::StringPrintf(
"TypeError: Failed to execute 'joinAdInterestGroup' on 'Navigator': "
"interest group biddingLogicUrl doesn't have the same value as "
"interest group biddingLogicURL ('%s/bidding.js' vs "
"'%s/bidding2.js')",
origin_string.c_str(), origin_string.c_str()),
std::nullopt},
// ***
// biddingWasmHelperURL
// ***
{base::StringPrintf(R"(biddingWasmHelperUrl: '%s/bidding.wasm')",
origin_string.c_str()),
"done", kExpectedGroupBiddingWasmHelperUrl},
{base::StringPrintf(R"(biddingWasmHelperURL: '%s/bidding.wasm')",
origin_string.c_str()),
"done", kExpectedGroupBiddingWasmHelperUrl},
{base::StringPrintf(R"(biddingWasmHelperUrl: '%s/bidding.wasm',)"
R"(biddingWasmHelperURL: '%s/bidding.wasm')",
origin_string.c_str(), origin_string.c_str()),
"done", kExpectedGroupBiddingWasmHelperUrl},
{base::StringPrintf(R"(biddingWasmHelperUrl: '%s/bidding.wasm',)"
R"(biddingWasmHelperURL: '%s/bidding2.wasm')",
origin_string.c_str(), origin_string.c_str()),
base::StringPrintf(
"TypeError: Failed to execute 'joinAdInterestGroup' on 'Navigator': "
"interest group biddingWasmHelperUrl doesn't have the same value as "
"interest group biddingWasmHelperURL ('%s/bidding.wasm' vs "
"'%s/bidding2.wasm')",
origin_string.c_str(), origin_string.c_str()),
std::nullopt},
// ***
// updateURL
// ***
{base::StringPrintf(R"(updateUrl: '%s/update.json')",
origin_string.c_str()),
"done", kExpectedGroupUpdateUrl},
{base::StringPrintf(R"(updateURL: '%s/update.json')",
origin_string.c_str()),
"done", kExpectedGroupUpdateUrl},
{base::StringPrintf(R"(updateUrl: '%s/update.json',)"
R"(updateURL: '%s/update.json')",
origin_string.c_str(), origin_string.c_str()),
"done", kExpectedGroupUpdateUrl},
{base::StringPrintf(R"(updateUrl: '%s/update.json',)"
R"(updateURL: '%s/update2.json')",
origin_string.c_str(), origin_string.c_str()),
base::StringPrintf(
"TypeError: Failed to execute 'joinAdInterestGroup' on 'Navigator': "
"interest group updateUrl doesn't have the same value as "
"interest group updateURL ('%s/update.json' vs "
"'%s/update2.json')",
origin_string.c_str(), origin_string.c_str()),
std::nullopt},
// ***
// trustedBiddingSignalsURL
// ***
{base::StringPrintf(R"(trustedBiddingSignalsUrl: '%s/signals.json')",
origin_string.c_str()),
"done", kExpectedGroupTrustedBiddingSignalsUrl},
{base::StringPrintf(R"(trustedBiddingSignalsURL: '%s/signals.json')",
origin_string.c_str()),
"done", kExpectedGroupTrustedBiddingSignalsUrl},
{base::StringPrintf(R"(trustedBiddingSignalsUrl: '%s/signals.json',)"
R"(trustedBiddingSignalsURL: '%s/signals.json')",
origin_string.c_str(), origin_string.c_str()),
"done", kExpectedGroupTrustedBiddingSignalsUrl},
{base::StringPrintf(R"(trustedBiddingSignalsUrl: '%s/signals.json',)"
R"(trustedBiddingSignalsURL: '%s/signals2.json')",
origin_string.c_str(), origin_string.c_str()),
base::StringPrintf(
"TypeError: Failed to execute 'joinAdInterestGroup' on 'Navigator': "
"interest group trustedBiddingSignalsUrl doesn't have the same "
"value as interest group trustedBiddingSignalsURL "
"('%s/signals.json' "
"vs '%s/signals2.json')",
origin_string.c_str(), origin_string.c_str()),
std::nullopt},
};
for (const auto& test_case : kTestCases) {
SCOPED_TRACE(test_case.join_dict_contents);
EXPECT_EQ(test_case.result,
EvalJs(shell(),
base::StringPrintf(R"(
(async function() {
try {
await navigator.joinAdInterestGroup(
{
name: 'cars',
owner: '%s',
%s
},
/*joinDurationSec=*/1000);
} catch (e) {
return e.toString();
}
return 'done';
})())",
origin_string.c_str(),
test_case.join_dict_contents.c_str())));
// Check that the database has also been updated.
std::optional<SingleStorageInterestGroup> maybe_interest_group =
GetInterestGroup(/*owner=*/origin,
/*name=*/"cars");
if (test_case.expected_group) {
ASSERT_TRUE(maybe_interest_group);
blink::InterestGroup expected_group = test_case.expected_group.value();
expected_group.expiry =
maybe_interest_group.value()->interest_group.expiry;
IgExpectEqualsForTesting(
/*actual=*/maybe_interest_group.value()->interest_group,
/*expected=*/expected_group);
} else {
EXPECT_FALSE(maybe_interest_group);
}
// Cleanup between runs.
EXPECT_EQ(kSuccess, LeaveInterestGroupAndVerify(/*owner=*/origin,
/*name=*/"cars"));
}
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
JoinInterestGroupInvalidAggregationCoordinatorOrigin) {
const char kScriptTemplate[] = R"(
(async function() {
try {
await navigator.joinAdInterestGroup(
{
name: 'cars',
owner: $1,
privateAggregationConfig: {
aggregationCoordinatorOrigin: 'https://invalid^&'
}
},
/*joinDurationSec=*/1000);
} catch (e) {
return e.toString();
}
return 'done';
})())";
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
std::string origin_string = url::Origin::Create(url).Serialize();
ASSERT_TRUE(NavigateToURL(shell(), url));
EXPECT_EQ(
"SyntaxError: Failed to execute 'joinAdInterestGroup' on 'Navigator': "
"aggregationCoordinatorOrigin 'https://invalid^&' must be a valid https "
"origin.",
EvalJs(shell(), JsReplace(kScriptTemplate, origin_string.c_str())));
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
JoinInterestGroupValidAggregationCoordinatorOrigin) {
const char kScriptTemplate[] = R"(
(async function() {
try {
await navigator.joinAdInterestGroup(
{
name: 'cars',
owner: $1,
privateAggregationConfig: {
aggregationCoordinatorOrigin: $2,
}
},
/*joinDurationSec=*/1000);
} catch (e) {
return e.toString();
}
return 'done';
})())";
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
std::string origin_string = url::Origin::Create(url).Serialize();
ASSERT_TRUE(NavigateToURL(shell(), url));
EXPECT_EQ(
"done",
EvalJs(shell(),
JsReplace(
kScriptTemplate, origin_string.c_str(),
aggregation_service::kDefaultAggregationCoordinatorAwsCloud)));
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
JoinInterestGroupNonHTTPSAggregationCoordinatorOrigin) {
const char kScriptTemplate[] = R"(
(async function() {
try {
await navigator.joinAdInterestGroup(
{
name: 'cars',
owner: $1,
privateAggregationConfig: {
aggregationCoordinatorOrigin: 'http://coordinator.test/',
}
},
/*joinDurationSec=*/1000);
} catch (e) {
return e.toString();
}
return 'done';
})())";
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
std::string origin_string = url::Origin::Create(url).Serialize();
ASSERT_TRUE(NavigateToURL(shell(), url));
EXPECT_EQ(
"SyntaxError: Failed to execute 'joinAdInterestGroup' on 'Navigator': "
"aggregationCoordinatorOrigin 'http://coordinator.test/' must be a valid "
"https origin.",
EvalJs(shell(), JsReplace(kScriptTemplate, origin_string.c_str())));
}
IN_PROC_BROWSER_TEST_F(
InterestGroupBrowserTest,
JoinInterestGroupUnsupportedAggregationCoordinatorOrigin) {
const char kScriptTemplate[] = R"(
(async function() {
try {
await navigator.joinAdInterestGroup(
{
name: 'cars',
owner: $1,
privateAggregationConfig: {
aggregationCoordinatorOrigin: 'https://coordinator.test/',
}
},
/*joinDurationSec=*/1000);
} catch (e) {
return e.toString();
}
return 'done';
})())";
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
std::string origin_string = url::Origin::Create(url).Serialize();
ASSERT_TRUE(NavigateToURL(shell(), url));
EXPECT_EQ(
"DataError: Failed to execute 'joinAdInterestGroup' on 'Navigator': "
"aggregationCoordinatorOrigin 'https://coordinator.test/' is not a "
"recognized coordinator origin.",
EvalJs(shell(), JsReplace(kScriptTemplate, origin_string.c_str())));
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
JoinInterestGroupNonOriginViewAndClickCountsProviders) {
const char kScriptTemplate[] = R"(
(async function() {
try {
await navigator.joinAdInterestGroup(
{
name: 'cars',
owner: $1,
viewAndClickCountsProviders: ['hi'],
},
/*joinDurationSec=*/10000);
} catch (e) {
return e.toString();
}
return 'done';
})())";
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
std::string origin_string = url::Origin::Create(url).Serialize();
ASSERT_TRUE(NavigateToURL(shell(), url));
EXPECT_EQ(
"TypeError: Failed to execute 'joinAdInterestGroup' on 'Navigator': "
"viewAndClickCountsProviders 'hi' for AuctionAdInterestGroup with name "
"'cars' must be a valid https origin.",
EvalJs(shell(), JsReplace(kScriptTemplate, origin_string.c_str())));
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, RunAdAuctionInvalidSeller) {
ASSERT_TRUE(NavigateToURL(
shell(), embedded_https_test_server().GetURL("a.test", "/echo")));
AttachInterestGroupObserver();
EXPECT_EQ(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': seller "
"'https://invalid^&' for AuctionAdConfig must be a valid https origin.",
RunAuctionAndWait(R"({
seller: 'https://invalid^&',
decisionLogicURL: 'https://test.com/decision_logic'
})"));
WaitForAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, RunAdAuctionHttpSeller) {
ASSERT_TRUE(NavigateToURL(
shell(), embedded_https_test_server().GetURL("a.test", "/echo")));
AttachInterestGroupObserver();
EXPECT_EQ(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': seller "
"'http://test.com' for AuctionAdConfig must be a valid https origin.",
RunAuctionAndWait(R"({
seller: 'http://test.com',
decisionLogicURL: 'https://test.com/decision_logic'
})"));
WaitForAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionInvalidDecisionLogicUrl) {
ASSERT_TRUE(NavigateToURL(
shell(), embedded_https_test_server().GetURL("a.test", "/echo")));
AttachInterestGroupObserver();
EXPECT_EQ(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': "
"decisionLogicURL 'https://invalid^&' for AuctionAdConfig with seller "
"'https://test.com' cannot be resolved to a valid URL.",
RunAuctionAndWait(R"({
seller: 'https://test.com',
decisionLogicURL: 'https://invalid^&'
})"));
WaitForAccessObserved({});
}
// TODO(crbug.com/40266734): Remove test when old names are no longer supported.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionMissingDecisionLogicUrl) {
ASSERT_TRUE(NavigateToURL(
shell(), embedded_https_test_server().GetURL("a.test", "/echo")));
AttachInterestGroupObserver();
EXPECT_EQ(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': Missing "
"required field ad auction config decisionLogicURL or serverResponse",
RunAuctionAndWait(R"({
seller: 'https://test.com',
})"));
WaitForAccessObserved({});
}
// TODO(crbug.com/40266734): Remove test when old names are no longer supported.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionDecisionLogicUrlOldAndNewNamesDontMatch) {
ASSERT_TRUE(NavigateToURL(
shell(), embedded_https_test_server().GetURL("a.test", "/echo")));
AttachInterestGroupObserver();
EXPECT_EQ(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': ad auction "
"config decisionLogicUrl doesn't have the same value as ad auction "
"config decisionLogicURL ('https://test.com/decision2' vs "
"'https://test.com/decision1')",
RunAuctionAndWait(R"({
seller: 'https://test.com',
decisionLogicURL: 'https://test.com/decision1',
decisionLogicUrl: 'https://test.com/decision2'
})"));
WaitForAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionInvalidTrustedScoringSignalsUrl) {
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin origin = url::Origin::Create(url);
ASSERT_TRUE(NavigateToURL(shell(), url));
AttachInterestGroupObserver();
EXPECT_EQ(
base::StringPrintf(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': "
"trustedScoringSignalsURL 'https://invalid^&' for AuctionAdConfig "
"with seller '%s' cannot be resolved to a valid URL.",
origin.Serialize().c_str()),
RunAuctionAndWait(JsReplace(R"({
seller: $1,
decisionLogicURL: $2,
trustedScoringSignalsURL: 'https://invalid^&'
})",
origin, url)));
WaitForAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionInvalidTrustedScoringSignalsUrl2) {
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin origin = url::Origin::Create(url);
ASSERT_TRUE(NavigateToURL(shell(), url));
AttachInterestGroupObserver();
// Various trailing URL fields that can make it invalid
const char* kTests[] = {"?foo", "#foo", "?", "#"};
const char kAuctionConfigTemplate[] = R"({
seller: $1,
decisionLogicURL: $2,
trustedScoringSignalsURL: $1 + $3
})";
for (const char* test : kTests) {
SCOPED_TRACE(test);
EXPECT_EQ(base::StringPrintf(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': "
"trustedScoringSignalsURL '%s%s' for AuctionAdConfig with "
"seller '%s' must not include a query, a fragment string, or "
"embedded credentials.",
origin.Serialize().c_str(), test, origin.Serialize().c_str()),
RunAuctionAndWait(
JsReplace(kAuctionConfigTemplate, origin, url, test)));
WaitForAccessObserved({});
}
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionInvalidTrustedScoringSignalsUrl3) {
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin origin = url::Origin::Create(url);
ASSERT_TRUE(NavigateToURL(shell(), url));
AttachInterestGroupObserver();
GURL::Replacements replacements;
replacements.SetUsernameStr("user");
replacements.SetPasswordStr("pass");
replacements.SetPathStr("");
GURL url_with_pass = url.ReplaceComponents(replacements);
const char kAuctionConfigTemplate[] = R"({
seller: $1,
decisionLogicURL: $2,
trustedScoringSignalsURL: $3
})";
EXPECT_EQ(base::StringPrintf(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': "
"trustedScoringSignalsURL '%s' for AuctionAdConfig with "
"seller '%s' must not include a query, a fragment string, or "
"embedded credentials.",
url_with_pass.spec().c_str(), origin.Serialize().c_str()),
RunAuctionAndWait(
JsReplace(kAuctionConfigTemplate, origin, url, url_with_pass)));
WaitForAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAuctionWithTooLongDecisionLogicUrl) {
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin origin = url::Origin::Create(url);
// Create almost-too-long and too-long seller script URLs.
std::string almost_too_long_seller_url_base = origin.GetURL().spec();
GURL almost_too_long_seller_url = GURL(
almost_too_long_seller_url_base +
std::string(url::kMaxURLChars - almost_too_long_seller_url_base.size(),
'1'));
GURL too_long_seller_url = GURL(almost_too_long_seller_url.spec() + "2");
ASSERT_EQ(too_long_seller_url.spec().size(), url::kMaxURLChars + 1);
ASSERT_TRUE(NavigateToURL(shell(), url));
// Join an interest group. All that matters is it will bid, and has an ad.
ASSERT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
origin.host(), "/interest_group/bidding_logic.js"))
.SetAds({{{url, /*metadata=*/std::nullopt}}})
.Build()));
// Register a response for both the almost too long seller path and the too
// long seller path. Latter should never be requested, but if it is, the
// server should respond with a valid script, to make this test fail if that
// script is ever unexpectedly download and run.
const std::string_view kDecisionLogicScript = R"(
function scoreAd(adMetadata, bid, auctionConfig, trustedScoringSignals,
browserSignals) {
return bid;
};)";
network_responder_->RegisterNetworkResponse(almost_too_long_seller_url.path(),
kDecisionLogicScript,
"application/javascript");
network_responder_->RegisterNetworkResponse(too_long_seller_url.path(),
kDecisionLogicScript,
"application/javascript");
EXPECT_EQ(url, RunAuctionAndWaitForUrl(
JsReplace(R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1]
})",
origin, almost_too_long_seller_url)));
EXPECT_EQ(base::Value(),
RunAuctionAndWait(JsReplace(R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1]
})",
origin, too_long_seller_url)));
}
// Run two multi-seller auctions with two components. In the first auction, both
// components have too-long decision logic URLs, and there should be no winner.
// In the second auction, one component has a too-long URL. There should be a
// winner.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunComponentAuctionWithTooLongDecisionLogicUrl) {
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin origin = url::Origin::Create(url);
// Create too-long seller script URL.
std::string too_long_seller_url_base = origin.GetURL().spec();
GURL too_long_seller_url =
GURL(too_long_seller_url_base +
std::string(url::kMaxURLChars - too_long_seller_url_base.size() + 1,
'1'));
ASSERT_EQ(too_long_seller_url.spec().size(), url::kMaxURLChars + 1);
ASSERT_TRUE(NavigateToURL(shell(), url));
// Join an interest group. All that matters is it will bid, and has an ad.
ASSERT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
origin.host(), "/interest_group/bidding_logic.js"))
.SetAds({{{url, /*metadata=*/std::nullopt}}})
.Build()));
// Run an auction where both component auctions have too-long decision logic
// URLs. There should be no winner.
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
auctionSignals: "bidderAllowsComponentAuction,"+
"sellerAllowsComponentAuction",
componentAuctions:
[{
seller: $1,
decisionLogicURL: $3,
interestGroupBuyers: [$1],
auctionSignals: "bidderAllowsComponentAuction,"+
"sellerAllowsComponentAuction"
},
{
seller: $1,
decisionLogicURL: $3,
interestGroupBuyers: [$1],
auctionSignals: "bidderAllowsComponentAuction,"+
"sellerAllowsComponentAuction"
}]
})",
origin,
embedded_https_test_server().GetURL(origin.host(),
"/interest_group/decision_logic.js"),
too_long_seller_url);
EXPECT_EQ(base::Value(), RunAuctionAndWait(auction_config));
// Run an auction where only one component auction has a too-long decision
// logic URLs. There should be a winner from the other component auction.
std::string auction_config2 = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
auctionSignals: "bidderAllowsComponentAuction,"+
"sellerAllowsComponentAuction",
componentAuctions:
[{
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
auctionSignals: "bidderAllowsComponentAuction,"+
"sellerAllowsComponentAuction"
},
{
seller: $1,
decisionLogicURL: $3,
interestGroupBuyers: [$1],
auctionSignals: "bidderAllowsComponentAuction,"+
"sellerAllowsComponentAuction"
}]
})",
origin,
embedded_https_test_server().GetURL(origin.host(),
"/interest_group/decision_logic.js"),
too_long_seller_url);
EXPECT_EQ(url, RunAuctionAndWaitForUrl(auction_config2));
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAuctionWithTooLongTrustedScoringSignalsUrl) {
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin origin = url::Origin::Create(url);
GURL decision_url = embedded_https_test_server().GetURL(
origin.host(), "/interest_group/decision_logic.js");
// Note that the almost-too-long URL will be turned into a too-long URL once
// the query param is attached.
std::string almost_too_long_url_base = origin.GetURL().spec();
GURL almost_too_long_trusted_scoring_signals_url = GURL(
almost_too_long_url_base +
std::string(url::kMaxURLChars - almost_too_long_url_base.size(), '1'));
GURL too_long_trusted_scoring_signals_url =
GURL(almost_too_long_trusted_scoring_signals_url.spec() + "2");
ASSERT_EQ(too_long_trusted_scoring_signals_url.spec().size(),
url::kMaxURLChars + 1);
ASSERT_TRUE(NavigateToURL(shell(), url));
// Join an interest group. All that matters is it will bid, and has an ad.
ASSERT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
origin.host(), "/interest_group/bidding_logic.js"))
.SetAds({{{url, /*metadata=*/std::nullopt}}})
.Build()));
// Using either signals URL should result in a winner.
EXPECT_EQ(url, RunAuctionAndWaitForUrl(
JsReplace(R"({
seller: $1,
decisionLogicURL: $2,
trustedScoringSignalsURL: $3,
interestGroupBuyers: [$1]
})",
origin, decision_url,
almost_too_long_trusted_scoring_signals_url)));
// This should fail with an attestation failure, since the too-long URL will
// be passed as a null URL to the cross-origin attestion check, which will
// fail on a null URL.
EXPECT_EQ(base::Value(),
RunAuctionAndWait(JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
trustedScoringSignalsURL: $3,
interestGroupBuyers: [$1]
})",
origin, decision_url, too_long_trusted_scoring_signals_url)));
// No observed requests should start with `almost_too_long_url_base` - all
// trusted scoring signals request URLs should exceed the length limit, so
// result in fetch failures without being sent over the network.
for (const GURL& seen_url : SeenUrls()) {
EXPECT_FALSE(base::StartsWith(seen_url.spec(), almost_too_long_url_base));
}
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionPositiveMaxTrustedScoringSignalsURLLength) {
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin origin = url::Origin::Create(url);
ASSERT_TRUE(NavigateToURL(shell(), url));
AttachInterestGroupObserver();
EXPECT_EQ(base::Value(), RunAuctionAndWait(JsReplace(R"({
seller: $1,
decisionLogicURL: $2,
maxTrustedScoringSignalsURLLength: 1000
})",
origin, url)));
WaitForAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionZeroMaxTrustedScoringSignalsURLLength) {
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin origin = url::Origin::Create(url);
ASSERT_TRUE(NavigateToURL(shell(), url));
AttachInterestGroupObserver();
EXPECT_EQ(base::Value(), RunAuctionAndWait(JsReplace(R"({
seller: $1,
decisionLogicURL: $2,
maxTrustedScoringSignalsURLLength: 0
})",
origin, url)));
WaitForAccessObserved({});
}
class AuctionConfigExecutionModeBrowserTest : public InterestGroupBrowserTest {
public:
AuctionConfigExecutionModeBrowserTest() {
feature_list_.InitWithFeatures({/*enabled_features=*/blink::features::
kFledgeSellerScriptExecutionMode},
/*disabled_features=*/{});
}
protected:
base::test::ScopedFeatureList feature_list_;
};
// Verify that we handle converting the executionMode from the auction config's
// idl to mojo properly. Covers all the modes and an invalid mode as well.
IN_PROC_BROWSER_TEST_F(AuctionConfigExecutionModeBrowserTest,
AuctionConfigExecutionModes) {
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
ASSERT_TRUE(NavigateToURL(shell(), url));
url::Origin origin = url::Origin::Create(url);
constexpr char kDecisionLogicPath[] =
R"(/interest_group/decision_logic_$1.js)";
constexpr char kBiddingLogicPath[] =
"/interest_group/test_generated_bidding_argument_validator.js";
constexpr char kBiddingLogicScript[] = R"(
function generateBid(
interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals,
unusedBrowserSignals) {
const ad = interestGroup.ads[0];
return {'ad': ad, 'bid': 1, 'render': ad.renderURL};
}
)";
constexpr char kDecisionLogicScript[] = R"(
function scoreAd(
adMetadata, bid, auctionConfig, unusedTrustedScoringSignals,
unusedBrowserSignals) {
validateAuctionConfig(auctionConfig);
return bid;
}
function validateAuctionConfig(auctionConfig) {
const executionMode = auctionConfig.executionMode;
if (executionMode !== $1){
throw 'Execution mode should be $1, but was: ' + executionMode;
}
}
)";
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
origin, "cars",
/*priority=*/0, /*execution_mode=*/
blink::InterestGroup::ExecutionMode::kCompatibilityMode,
/*bidding_url=*/
embedded_https_test_server().GetURL("a.test", kBiddingLogicPath),
/*ads=*/
{{{GURL("https://example.com/render"),
/*metadata=*/std::nullopt}}},
/*ad_components=*/std::nullopt, std::nullopt));
const struct TestCases {
std::string auction_config_execution_mode;
std::string expected_execution_mode;
} kTestCases[] = {
{/*auction_config_execution_mode=*/"compatibility",
/*expected_execution_mode=*/"compatibility"},
{/*auction_config_execution_mode=*/"group-by-origin",
/*expected_execution_mode=*/"group-by-origin"},
{/*auction_config_execution_mode=*/"frozen-context",
/*expected_execution_mode=*/"frozen-context"},
{/*auction_config_execution_mode=*/"invalid-mode",
/*expected_execution_mode=*/"compatibility"},
};
// Used to make different decision logic urls, since caching causes flaky
// tests.
int url_count = 0;
for (const auto& test_case : kTestCases) {
url_count++;
auto current_decision_url = JsReplace(kDecisionLogicPath, url_count);
network_responder_->RegisterNetworkResponse(
kBiddingLogicPath, kBiddingLogicScript, "application/javascript");
network_responder_->RegisterNetworkResponse(
current_decision_url,
JsReplace(kDecisionLogicScript, test_case.expected_execution_mode),
"application/javascript");
EXPECT_EQ(
"https://example.com/render",
RunAuctionAndWaitForUrl(JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
executionMode: $3,
interestGroupBuyers: [$1]
})",
origin,
embedded_https_test_server().GetURL("a.test", current_decision_url),
test_case.auction_config_execution_mode)));
}
}
class AuctionConfigExecutionModeDisabledBrowserTest
: public InterestGroupBrowserTest {
public:
AuctionConfigExecutionModeDisabledBrowserTest() {
feature_list_.InitWithFeatures(
{/*enabled_features=*/},
/*disabled_features=*/{
blink::features::kFledgeSellerScriptExecutionMode});
}
protected:
base::test::ScopedFeatureList feature_list_;
};
// Verify that when the feature is disabled, execution mode does not get passed
// to the seller worklet.
IN_PROC_BROWSER_TEST_F(AuctionConfigExecutionModeDisabledBrowserTest,
AuctionConfigExecutionModes) {
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
ASSERT_TRUE(NavigateToURL(shell(), url));
url::Origin origin = url::Origin::Create(url);
constexpr char kDecisionLogicPath[] =
"/interest_group/non_object_decision_argument_validator.js";
constexpr char kBiddingLogicPath[] =
"/interest_group/test_generated_bidding_argument_validator.js";
constexpr char kBiddingLogicScript[] = R"(
function generateBid(
interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals,
unusedBrowserSignals) {
const ad = interestGroup.ads[0];
return {'ad': ad, 'bid': 1, 'render': ad.renderURL};
}
)";
constexpr char kDecisionLogicScript[] = R"(
function scoreAd(
adMetadata, bid, auctionConfig, unusedTrustedScoringSignals,
unusedBrowserSignals) {
validateAuctionConfig(auctionConfig);
return bid;
}
function validateAuctionConfig(auctionConfig) {
const executionMode = auctionConfig.executionMode;
if (executionMode !== undefined) {
throw 'Execution mode should be undefined, but was: ' + executionMode;
}
}
)";
const std::string kTestCases[] = {
"compatibility",
"group-by-origin",
"frozen-context",
"invalid-mode",
};
for (const auto& execution_mode : kTestCases) {
network_responder_->RegisterNetworkResponse(
kBiddingLogicPath, kBiddingLogicScript, "application/javascript");
network_responder_->RegisterNetworkResponse(
kDecisionLogicPath, kDecisionLogicScript, "application/javascript");
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
origin, "cars",
/*priority=*/0, /*execution_mode=*/
blink::InterestGroup::ExecutionMode::kCompatibilityMode,
/*bidding_url=*/
embedded_https_test_server().GetURL("a.test", kBiddingLogicPath),
/*ads=*/
{{{GURL("https://example.com/render"),
/*metadata=*/std::nullopt}}},
/*ad_components=*/std::nullopt, std::nullopt));
EXPECT_EQ(
"https://example.com/render",
RunAuctionAndWaitForUrl(JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
executionMode: $3,
interestGroupBuyers: [$1]
})",
origin,
embedded_https_test_server().GetURL("a.test", kDecisionLogicPath),
execution_mode)));
}
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionNegativeMaxTrustedScoringSignalsURLLength) {
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin origin = url::Origin::Create(url);
ASSERT_TRUE(NavigateToURL(shell(), url));
AttachInterestGroupObserver();
EXPECT_EQ(
base::StringPrintf(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': "
"maxTrustedScoringSignalsURLLength '-1000' for AuctionAdConfig with "
"seller '%s' must not be negative.",
origin.Serialize().c_str()),
RunAuctionAndWait(JsReplace(R"({
seller: $1,
decisionLogicURL: $2,
maxTrustedScoringSignalsURLLength: -1000
})",
origin, url)));
WaitForAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionValidTrustedScoringSignalsCoordinator) {
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin origin = url::Origin::Create(url);
ASSERT_TRUE(NavigateToURL(shell(), url));
AttachInterestGroupObserver();
EXPECT_EQ(base::Value(), RunAuctionAndWait(JsReplace(R"({
seller: $1,
decisionLogicURL: $2,
trustedScoringSignalsCoordinator: "https://example.test"
})",
origin, url)));
WaitForAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionHTTPSchemeTrustedScoringSignalsCoordinator) {
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin origin = url::Origin::Create(url);
ASSERT_TRUE(NavigateToURL(shell(), url));
AttachInterestGroupObserver();
EXPECT_EQ(
base::StringPrintf(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': "
"trustedScoringSignalsCoordinator 'http://example.test' for "
"AuctionAdConfig with seller '%s' must be a valid https origin.",
origin.Serialize().c_str()),
RunAuctionAndWait(JsReplace(R"({
seller: $1,
decisionLogicURL: $2,
trustedScoringSignalsCoordinator: "http://example.test"
})",
origin, url)));
WaitForAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionOpaqueTrustedScoringSignalsCoordinator) {
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin origin = url::Origin::Create(url);
ASSERT_TRUE(NavigateToURL(shell(), url));
AttachInterestGroupObserver();
EXPECT_EQ(
base::StringPrintf(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': "
"trustedScoringSignalsCoordinator 'data:,foo' for "
"AuctionAdConfig with seller '%s' must be a valid https origin.",
origin.Serialize().c_str()),
RunAuctionAndWait(JsReplace(R"({
seller: $1,
decisionLogicURL: $2,
trustedScoringSignalsCoordinator: "data:,foo"
})",
origin, url)));
WaitForAccessObserved({});
}
// TODO(crbug.com/40266734): Remove test when old names are no longer supported.
IN_PROC_BROWSER_TEST_F(
InterestGroupBrowserTest,
RunAdAuctionTrustedScoringSignalsUrlOldAndNewNamesDontMatch) {
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin origin = url::Origin::Create(url);
ASSERT_TRUE(NavigateToURL(shell(), url));
AttachInterestGroupObserver();
EXPECT_EQ(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': ad auction "
"config trustedScoringSignalsUrl doesn't have the same value as ad "
"auction config trustedScoringSignalsURL ('https://test.com/scoring2' vs "
"'https://test.com/scoring1')",
RunAuctionAndWait(JsReplace(R"({
seller: $1,
decisionLogicURL: $2,
trustedScoringSignalsURL: 'https://test.com/scoring1',
trustedScoringSignalsUrl: 'https://test.com/scoring2'
})",
origin, url)));
WaitForAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionDecisionLogicUrlDifferentFromSeller) {
GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
AttachInterestGroupObserver();
EXPECT_EQ(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': "
"decisionLogicURL 'https://b.test/foo' for AuctionAdConfig with seller "
"'https://a.test/' must match seller origin.",
RunAuctionAndWait(R"({
seller: "https://a.test/",
decisionLogicURL: "https://b.test/foo",
interestGroupBuyers: ["https://c.test/"],
})"));
WaitForAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionInvalidInterestGroupBuyers) {
ASSERT_TRUE(NavigateToURL(
shell(), embedded_https_test_server().GetURL("a.test", "/echo")));
AttachInterestGroupObserver();
EXPECT_EQ(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': "
"interestGroupBuyers buyer 'https://invalid^&' for AuctionAdConfig "
"with seller 'https://test.com' must be a valid https origin.",
RunAuctionAndWait(R"({
seller: 'https://test.com',
decisionLogicURL: 'https://test.com',
interestGroupBuyers: ['https://invalid^&'],
})"));
WaitForAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionInvalidInterestGroupBuyersStr) {
ASSERT_TRUE(NavigateToURL(
shell(), embedded_https_test_server().GetURL("a.test", "/echo")));
AttachInterestGroupObserver();
EXPECT_EQ(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': Failed to "
"read the 'interestGroupBuyers' property from 'AuctionAdConfig': The "
"provided value cannot be converted to a sequence.",
RunAuctionAndWait(R"({
seller: 'https://test.com',
decisionLogicURL: 'https://test.com',
interestGroupBuyers: 'not an array',
})"));
WaitForAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionNoInterestGroupBuyers) {
ASSERT_TRUE(NavigateToURL(
shell(), embedded_https_test_server().GetURL("a.test", "/echo")));
AttachInterestGroupObserver();
base::HistogramTester histogram_tester;
EXPECT_EQ(base::Value(), RunAuctionAndWait(R"({
seller: 'https://test.com',
decisionLogicURL: 'https://test.com',
})"));
WaitForAccessObserved({});
content::FetchHistogramsFromChildProcesses();
// Make sure the right histogram was logged (the histogram for on-device
// auctions).
histogram_tester.ExpectTotalCount(
"Ads.InterestGroup.ServerAuction.TimeToResolve", 0);
histogram_tester.ExpectTotalCount("Ads.InterestGroup.Auction.TimeToResolve",
1);
histogram_tester.ExpectTotalCount(
"Ads.InterestGroup.ServerAuction.TimeFromInputsResolvedToAuctionResolved",
0);
histogram_tester.ExpectTotalCount(
"Ads.InterestGroup.Auction.TimeFromInputsResolvedToAuctionResolved", 1);
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionEmptyInterestGroupBuyers) {
ASSERT_TRUE(NavigateToURL(
shell(), embedded_https_test_server().GetURL("a.test", "/echo")));
AttachInterestGroupObserver();
EXPECT_EQ(base::Value(), RunAuctionAndWait(R"({
seller: 'https://test.com',
decisionLogicURL: 'https://test.com',
interestGroupBuyers: [],
})"));
WaitForAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionInvalidAuctionSignals) {
GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin test_origin = url::Origin::Create(test_url);
GURL decision_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
console_observer.SetPattern(
"Uncaught (in promise) TypeError: Failed to execute 'runAdAuction' on "
"'NavigatorAuction': auctionSignals for AuctionAdConfig with seller "
"'https://a.test:*' must be a JSON-serializable object.");
base::HistogramTester histogram_tester;
content::FetchHistogramsFromChildProcesses();
EXPECT_EQ(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': Promise "
"argument rejected or resolved to invalid value.",
RunAuctionAndWait(JsReplace(R"({
seller: $1,
decisionLogicURL: $2,
auctionSignals: alert,
interestGroupBuyers: []
})",
test_origin, decision_url)));
EXPECT_TRUE(console_observer.Wait());
// Make sure the metrics reflect that this promise failed to resolve.
histogram_tester.ExpectTotalCount(
"Ads.InterestGroup.ServerAuction.TimeToResolve", 0);
histogram_tester.ExpectTotalCount("Ads.InterestGroup.Auction.TimeToResolve",
0);
histogram_tester.ExpectTotalCount(
"Ads.InterestGroup.ServerAuction.TimeFromInputsResolvedToAuctionResolved",
0);
histogram_tester.ExpectTotalCount(
"Ads.InterestGroup.Auction.TimeFromInputsResolvedToAuctionResolved", 0);
}
// Exercise rejection path in the renderer for promise-delivered auction
// signals.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionRejectPromiseAuctionSignals) {
GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin test_origin = url::Origin::Create(test_url);
ASSERT_TRUE(NavigateToURL(shell(), test_url));
GURL decision_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js");
const char kAuctionConfigTemplate[] = R"({
seller: $1,
decisionLogicURL: $2,
auctionSignals: new Promise((resolve, reject) => { setTimeout(
() => { reject('boo'); }, 10) }),
interestGroupBuyers: []
})";
EXPECT_EQ(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': Promise "
"argument rejected or resolved to invalid value.",
RunAuctionAndWait(
JsReplace(kAuctionConfigTemplate, test_origin, decision_url)));
}
// Exercise error-handling path in the renderer for promise-delivered auction
// signals.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionResolvePromiseInvalidAuctionSignals) {
GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin test_origin = url::Origin::Create(test_url);
ASSERT_TRUE(NavigateToURL(shell(), test_url));
GURL decision_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js");
const char kAuctionConfigTemplate[] = R"({
seller: $1,
decisionLogicURL: $2,
auctionSignals: new Promise((resolve, reject) => { setTimeout(
() => { resolve(function() {}); }, 10) }),
interestGroupBuyers: []
})";
WebContentsConsoleObserver console_observer(shell()->web_contents());
console_observer.SetPattern(
"Uncaught (in promise) TypeError: Failed to execute 'runAdAuction' on "
"'NavigatorAuction': auctionSignals for AuctionAdConfig with seller "
"'https://a.test:*' must be a JSON-serializable object.");
EXPECT_EQ(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': Promise "
"argument rejected or resolved to invalid value.",
RunAuctionAndWait(
JsReplace(kAuctionConfigTemplate, test_origin, decision_url)));
EXPECT_TRUE(console_observer.Wait());
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionInvalidSellerSignals) {
GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin test_origin = url::Origin::Create(test_url);
GURL decision_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
console_observer.SetPattern(
"Uncaught (in promise) TypeError: Failed to execute 'runAdAuction' on "
"'NavigatorAuction': sellerSignals for AuctionAdConfig with seller "
"'https://a.test:*' must be a JSON-serializable object.");
EXPECT_EQ(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': Promise "
"argument rejected or resolved to invalid value.",
RunAuctionAndWait(JsReplace(R"({
seller: $1,
decisionLogicURL: $2,
sellerSignals: function() {},
interestGroupBuyers: []
})",
test_origin, decision_url)));
EXPECT_TRUE(console_observer.Wait());
}
// Exercise rejection path in the renderer for promise-delivered seller signals.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionRejectPromiseSellerSignals) {
GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin test_origin = url::Origin::Create(test_url);
ASSERT_TRUE(NavigateToURL(shell(), test_url));
GURL decision_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js");
const char kAuctionConfigTemplate[] = R"({
seller: $1,
decisionLogicURL: $2,
sellerSignals: new Promise((resolve, reject) => { setTimeout(
() => { reject('boo'); }, 10) }),
interestGroupBuyers: []
})";
EXPECT_EQ(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': Promise "
"argument rejected or resolved to invalid value.",
RunAuctionAndWait(
JsReplace(kAuctionConfigTemplate, test_origin, decision_url)));
}
// Exercise error-handling path in the renderer for promise-delivered seller
// signals.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionResolvePromiseInvalidSellerSignals) {
GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin test_origin = url::Origin::Create(test_url);
ASSERT_TRUE(NavigateToURL(shell(), test_url));
GURL decision_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js");
const char kAuctionConfigTemplate[] = R"({
seller: $1,
decisionLogicURL: $2,
sellerSignals: new Promise((resolve, reject) => { setTimeout(
() => { resolve(function() {}); }, 10) }),
interestGroupBuyers: []
})";
WebContentsConsoleObserver console_observer(shell()->web_contents());
console_observer.SetPattern(
"Uncaught (in promise) TypeError: Failed to execute 'runAdAuction' on "
"'NavigatorAuction': sellerSignals for AuctionAdConfig with seller "
"'https://a.test:*' must be a JSON-serializable object.");
EXPECT_EQ(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': Promise "
"argument rejected or resolved to invalid value.",
RunAuctionAndWait(
JsReplace(kAuctionConfigTemplate, test_origin, decision_url)));
EXPECT_TRUE(console_observer.Wait());
}
// Test rejection path in the renderer for promise-delivered perBuyerSignals.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionRejectPromisePerBuyerSignals) {
GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin test_origin = url::Origin::Create(test_url);
ASSERT_TRUE(NavigateToURL(shell(), test_url));
GURL decision_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js");
const char kAuctionConfigTemplate[] = R"({
seller: $1,
decisionLogicURL: $2,
perBuyerSignals: new Promise((resolve, reject) => { setTimeout(
() => { reject('boo'); }, 10) }),
interestGroupBuyers: []
})";
EXPECT_EQ(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': Promise "
"argument rejected or resolved to invalid value.",
RunAuctionAndWait(
JsReplace(kAuctionConfigTemplate, test_origin, decision_url)));
}
// Exercise error-handling path in the renderer for promise-delivered
// perBuyerSignals.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionResolvePromiseInvalidPerBuyerSignals) {
GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin test_origin = url::Origin::Create(test_url);
ASSERT_TRUE(NavigateToURL(shell(), test_url));
GURL decision_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js");
const char kAuctionConfigTemplate[] = R"({
seller: $1,
decisionLogicURL: $2,
perBuyerSignals: new Promise((resolve, reject) => { setTimeout(
() => { resolve(52); }, 10) }),
interestGroupBuyers: []
})";
WebContentsConsoleObserver console_observer(shell()->web_contents());
console_observer.SetPattern(
"Uncaught (in promise) TypeError: Failed to execute 'runAdAuction' on "
"'NavigatorAuction': Only objects can be converted to record<K,V> types");
EXPECT_EQ(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': Promise "
"argument rejected or resolved to invalid value.",
RunAuctionAndWait(
JsReplace(kAuctionConfigTemplate, test_origin, decision_url)));
EXPECT_TRUE(console_observer.Wait());
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionInvalidPerBuyerSignalsOrigin) {
GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin test_origin = url::Origin::Create(test_url);
GURL decision_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
console_observer.SetPattern(
"Uncaught (in promise) TypeError: Failed to execute 'runAdAuction' on "
"'NavigatorAuction': perBuyerSignals buyer 'https://invalid^&' for "
"AuctionAdConfig with seller 'https://a.test:*' must be a valid https "
"origin.");
EXPECT_EQ(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': Promise "
"argument rejected or resolved to invalid value.",
RunAuctionAndWait(JsReplace(R"({
seller: $1,
decisionLogicURL: $2,
perBuyerSignals: {'https://invalid^&': {a:1}},
interestGroupBuyers: []
})",
test_origin, decision_url)));
EXPECT_TRUE(console_observer.Wait());
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionInvalidPerBuyerTKVSignalsOrigin) {
GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin test_origin = url::Origin::Create(test_url);
GURL decision_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
AttachInterestGroupObserver();
EXPECT_EQ(
base::StringPrintf(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': "
"perBuyerTKVSignals buyer 'https://invalid^&' for AuctionAdConfig "
"with seller '%s' must be a valid https origin.",
test_origin.Serialize().c_str()),
RunAuctionAndWait(JsReplace(R"({
seller: $1,
decisionLogicURL: $2,
perBuyerTKVSignals: {'https://invalid^&': {a:1}},
interestGroupBuyers: []
})",
test_origin, decision_url)));
WaitForAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionInvalidPerBuyerTKVSignals) {
GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin test_origin = url::Origin::Create(test_url);
GURL decision_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
AttachInterestGroupObserver();
EXPECT_EQ(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': Failed to "
"read the 'perBuyerTKVSignals' property from 'AuctionAdConfig': Only "
"objects can be converted to record<K,V> types",
RunAuctionAndWait(JsReplace(R"({
seller: $1,
decisionLogicURL: $2,
perBuyerTKVSignals: 52,
interestGroupBuyers: []
})",
test_origin, decision_url)));
WaitForAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionInvalidSellerTKVSignals) {
GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin test_origin = url::Origin::Create(test_url);
GURL decision_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
console_observer.SetPattern(
"Uncaught (in promise) TypeError: Failed to execute 'runAdAuction' on "
"'NavigatorAuction': sellerTKVSignals for AuctionAdConfig with seller "
"'https://a.test:*' must be a JSON-serializable object.");
EXPECT_EQ(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': Promise "
"argument rejected or resolved to invalid value.",
RunAuctionAndWait(JsReplace(R"({
seller: $1,
decisionLogicURL: $2,
sellerTKVSignals: function() {},
interestGroupBuyers: []
})",
test_origin, decision_url)));
EXPECT_TRUE(console_observer.Wait());
}
// Exercise a rejection path in the renderer for promise-delivered
// sellerTKVSignals.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionThenRejectPromiseSellerTKVSignals) {
GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin test_origin = url::Origin::Create(test_url);
ASSERT_TRUE(NavigateToURL(shell(), test_url));
GURL decision_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js");
const char kAuctionConfigTemplate[] = R"({
seller: $1,
decisionLogicURL: $2,
sellerTKVSignals: new Promise((resolve, reject) => { setTimeout(
() => { reject('boo'); }, 10) }),
interestGroupBuyers: []
})";
EXPECT_EQ(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': Promise "
"argument rejected or resolved to invalid value.",
RunAuctionAndWait(
JsReplace(kAuctionConfigTemplate, test_origin, decision_url)));
}
// Exercise an initial rejection path in the renderer for promise-delivered
// sellerTKVSignals.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionAlreadyRejectPromiseSellerTKVSignals) {
GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin test_origin = url::Origin::Create(test_url);
ASSERT_TRUE(NavigateToURL(shell(), test_url));
GURL decision_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js");
const char kAuctionConfigTemplate[] = R"({
seller: $1,
decisionLogicURL: $2,
sellerTKVSignals: new Promise((resolve, reject) => { reject('boo'); }),
interestGroupBuyers: []
})";
EXPECT_EQ(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': Promise "
"argument rejected or resolved to invalid value.",
RunAuctionAndWait(
JsReplace(kAuctionConfigTemplate, test_origin, decision_url)));
}
// Exercise error-handling path in the renderer for promise-delivered
// sellerTKVSignals.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionResolvePromiseInvalidSellerTKVSignals) {
GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin test_origin = url::Origin::Create(test_url);
ASSERT_TRUE(NavigateToURL(shell(), test_url));
GURL decision_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js");
const char kAuctionConfigTemplate[] = R"({
seller: $1,
decisionLogicURL: $2,
sellerTKVSignals: new Promise((resolve, reject) => { setTimeout(
() => { resolve(function() {}); }, 10) }),
interestGroupBuyers: []
})";
WebContentsConsoleObserver console_observer(shell()->web_contents());
console_observer.SetPattern(
"Uncaught (in promise) TypeError: Failed to execute 'runAdAuction' on "
"'NavigatorAuction': sellerTKVSignals for AuctionAdConfig with seller "
"'https://a.test:*' must be a JSON-serializable object.");
EXPECT_EQ(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': Promise "
"argument rejected or resolved to invalid value.",
RunAuctionAndWait(
JsReplace(kAuctionConfigTemplate, test_origin, decision_url)));
EXPECT_TRUE(console_observer.Wait());
}
// Test rejection path in the renderer for promise-delivered perBuyerTimeouts.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionRejectPromisePerBuyerTimeouts) {
GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin test_origin = url::Origin::Create(test_url);
ASSERT_TRUE(NavigateToURL(shell(), test_url));
GURL decision_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js");
const char kAuctionConfigTemplate[] = R"({
seller: $1,
decisionLogicURL: $2,
perBuyerTimeouts: new Promise((resolve, reject) => { setTimeout(
() => { reject('boo'); }, 10) }),
interestGroupBuyers: []
})";
EXPECT_EQ(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': Promise "
"argument rejected or resolved to invalid value.",
RunAuctionAndWait(
JsReplace(kAuctionConfigTemplate, test_origin, decision_url)));
}
// Exercise error-handling path in the renderer for promise-delivered
// perBuyerTimeouts.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionResolvePromiseInvalidPerBuyerTimeouts) {
GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin test_origin = url::Origin::Create(test_url);
ASSERT_TRUE(NavigateToURL(shell(), test_url));
GURL decision_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js");
const char kAuctionConfigTemplate[] = R"({
seller: $1,
decisionLogicURL: $2,
perBuyerTimeouts: new Promise((resolve, reject) => { setTimeout(
() => { resolve({'http://b.com': 52}); }, 10) }),
interestGroupBuyers: []
})";
WebContentsConsoleObserver console_observer(shell()->web_contents());
console_observer.SetPattern(
"Uncaught (in promise) TypeError: Failed to execute 'runAdAuction' on "
"'NavigatorAuction': perBuyerTimeouts buyer 'http://b.com' for "
"AuctionAdConfig with seller 'https://a.test:*' must be \"*\" (wildcard) "
"or a valid https origin.");
EXPECT_EQ(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': Promise "
"argument rejected or resolved to invalid value.",
RunAuctionAndWait(
JsReplace(kAuctionConfigTemplate, test_origin, decision_url)));
EXPECT_TRUE(console_observer.Wait());
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionInvalidPerBuyerTimeoutsOrigin) {
GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin test_origin = url::Origin::Create(test_url);
GURL decision_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
console_observer.SetPattern(
"Uncaught (in promise) TypeError: Failed to execute 'runAdAuction' on "
"'NavigatorAuction': perBuyerTimeouts buyer 'https://invalid^&' for "
"AuctionAdConfig with seller 'https://a.test:*' must be \"*\" (wildcard) "
"or a valid https origin.");
EXPECT_EQ(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': Promise "
"argument rejected or resolved to invalid value.",
RunAuctionAndWait(JsReplace(R"({
seller: $1,
decisionLogicURL: $2,
perBuyerTimeouts: {'https://invalid^&': 100},
interestGroupBuyers: []
})",
test_origin, decision_url)));
EXPECT_TRUE(console_observer.Wait());
}
// Test rejection path in the renderer for promise-delivered
// perBuyerCumulativeTimeouts.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionRejectPromisePerBuyerCumulativeTimeouts) {
GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin test_origin = url::Origin::Create(test_url);
ASSERT_TRUE(NavigateToURL(shell(), test_url));
GURL decision_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js");
const char kAuctionConfigTemplate[] = R"({
seller: $1,
decisionLogicURL: $2,
perBuyerCumulativeTimeouts: new Promise((resolve, reject) => { setTimeout(
() => { reject('boo'); }, 10) }),
interestGroupBuyers: []
})";
EXPECT_EQ(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': Promise "
"argument rejected or resolved to invalid value.",
RunAuctionAndWait(
JsReplace(kAuctionConfigTemplate, test_origin, decision_url)));
}
// Exercise error-handling path in the renderer for promise-delivered
// perBuyerCumulativeTimeouts.
IN_PROC_BROWSER_TEST_F(
InterestGroupBrowserTest,
RunAdAuctionResolvePromiseInvalidPerBuyerCumulativeTimeouts) {
GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin test_origin = url::Origin::Create(test_url);
ASSERT_TRUE(NavigateToURL(shell(), test_url));
GURL decision_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js");
const char kAuctionConfigTemplate[] = R"({
seller: $1,
decisionLogicURL: $2,
perBuyerCumulativeTimeouts: new Promise((resolve, reject) => { setTimeout(
() => { resolve({'http://b.com': 52}); }, 10) }),
interestGroupBuyers: []
})";
WebContentsConsoleObserver console_observer(shell()->web_contents());
console_observer.SetPattern(
"Uncaught (in promise) TypeError: Failed to execute 'runAdAuction' on "
"'NavigatorAuction': perBuyerCumulativeTimeouts buyer 'http://b.com' for "
"AuctionAdConfig with seller 'https://a.test:*' must be \"*\" (wildcard) "
"or a valid https origin.");
EXPECT_EQ(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': Promise "
"argument rejected or resolved to invalid value.",
RunAuctionAndWait(
JsReplace(kAuctionConfigTemplate, test_origin, decision_url)));
EXPECT_TRUE(console_observer.Wait());
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionInvalidPerBuyerCumulativeTimeoutsOrigin) {
GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin test_origin = url::Origin::Create(test_url);
GURL decision_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
console_observer.SetPattern(
"Uncaught (in promise) TypeError: Failed to execute 'runAdAuction' on "
"'NavigatorAuction': perBuyerCumulativeTimeouts buyer "
"'https://invalid^&' for AuctionAdConfig with seller 'https://a.test:*' "
"must be \"*\" (wildcard) or a valid https origin.");
EXPECT_EQ(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': Promise "
"argument rejected or resolved to invalid value.",
RunAuctionAndWait(JsReplace(R"({
seller: $1,
decisionLogicURL: $2,
perBuyerCumulativeTimeouts: {'https://invalid^&': 100},
interestGroupBuyers: []
})",
test_origin, decision_url)));
EXPECT_TRUE(console_observer.Wait());
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionInvalidPerBuyerCurrenciesOrigin) {
GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin test_origin = url::Origin::Create(test_url);
GURL decision_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
console_observer.SetPattern(
"Uncaught (in promise) TypeError: Failed to execute 'runAdAuction' on "
"'NavigatorAuction': perBuyerCurrencies buyer 'https://invalid^&' for "
"AuctionAdConfig with seller 'https://a.test:*' must be \"*\" (wildcard) "
"or a valid https origin.");
EXPECT_EQ(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': Promise "
"argument rejected or resolved to invalid value.",
RunAuctionAndWait(JsReplace(R"({
seller: $1,
decisionLogicURL: $2,
perBuyerCurrencies: {'https://invalid^&': 'USD'},
interestGroupBuyers: []
})",
test_origin, decision_url)));
EXPECT_TRUE(console_observer.Wait());
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionInvalidPerBuyerCurrenciesCurrency) {
GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin test_origin = url::Origin::Create(test_url);
GURL decision_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
console_observer.SetPattern(
"Uncaught (in promise) TypeError: Failed to execute 'runAdAuction' on "
"'NavigatorAuction': perBuyerCurrencies currency 'usd' for "
"AuctionAdConfig with seller 'https://a.test:*' must be a 3-letter "
"uppercase currency code.");
EXPECT_EQ(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': Promise "
"argument rejected or resolved to invalid value.",
RunAuctionAndWait(JsReplace(R"({
seller: $1,
decisionLogicURL: $2,
perBuyerCurrencies: {'*': 'usd'},
interestGroupBuyers: []
})",
test_origin, decision_url)));
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionInvalidSellerCurrency) {
ASSERT_TRUE(NavigateToURL(
shell(), embedded_https_test_server().GetURL("a.test", "/echo")));
AttachInterestGroupObserver();
EXPECT_EQ(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator':"
" sellerCurrency 'usd' for AuctionAdConfig with seller"
" 'https://test.com' must be a 3-letter uppercase currency code.",
RunAuctionAndWait(R"({
seller: 'https://test.com',
decisionLogicURL: 'https://test.com',
sellerCurrency: 'usd'
})"));
WaitForAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionInvalidPerBuyerGroupLimitsValue) {
ASSERT_TRUE(NavigateToURL(
shell(), embedded_https_test_server().GetURL("a.test", "/echo")));
AttachInterestGroupObserver();
EXPECT_EQ(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': "
"perBuyerGroupLimits value '0' for AuctionAdConfig with "
"seller 'https://test.com' must be greater than 0.",
RunAuctionAndWait(R"({
seller: 'https://test.com',
decisionLogicURL: 'https://test.com',
perBuyerGroupLimits: {'https://test.com': 0}
})"));
WaitForAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionInvalidPerBuyerGroupLimitsOrigin) {
ASSERT_TRUE(NavigateToURL(
shell(), embedded_https_test_server().GetURL("a.test", "/echo")));
AttachInterestGroupObserver();
EXPECT_EQ(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': "
"perBuyerGroupLimits buyer 'https://invalid^&' for AuctionAdConfig with "
"seller 'https://test.com' must be \"*\" (wildcard) or a valid https "
"origin.",
RunAuctionAndWait(R"({
seller: 'https://test.com',
decisionLogicURL: 'https://test.com',
perBuyerGroupLimits: {'https://invalid^&': 100}
})"));
WaitForAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionInvalidPerBuyerPrioritySignals) {
ASSERT_TRUE(NavigateToURL(
shell(), embedded_https_test_server().GetURL("a.test", "/echo")));
AttachInterestGroupObserver();
EXPECT_EQ(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': "
"perBuyerPrioritySignals key 'browserSignals.thisPrefixIsReserved' for "
"AuctionAdConfig with seller 'https://test.com' must not start with "
"reserved \"browserSignals.\" prefix.",
RunAuctionAndWait(R"({
seller: 'https://test.com',
decisionLogicURL: 'https://test.com',
perBuyerPrioritySignals: {
'https://foo.com/':{"browserSignals.thisPrefixIsReserved": 1}
}
})"));
WaitForAccessObserved({});
}
// Note -- this property is enforced by using the `double` WebIDL type.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, NonFiniteValuesRejected) {
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
std::string origin_string = url::Origin::Create(url).Serialize();
ASSERT_TRUE(NavigateToURL(shell(), url));
AttachInterestGroupObserver();
std::string test_cases[] = {
"NaN",
"Infinity",
"-Infinity",
"'Values must be numbers'",
};
for (const std::string& test_case : test_cases) {
SCOPED_TRACE(test_case);
EXPECT_EQ(
"TypeError: Failed to execute 'joinAdInterestGroup' on 'Navigator': "
"Failed to read the 'priority' property from 'AuctionAdInterestGroup': "
"The provided double value is non-finite.",
EvalJs(shell(),
base::StringPrintf(R"(
(async function() {
try {
await navigator.joinAdInterestGroup(
{
name: 'cars',
owner: '%s',
priority: %s,
},
/*joinDurationSec=*/1000);
} catch (e) {
return e.toString();
}
return 'done';
})())",
origin_string.c_str(), test_case.c_str())));
EXPECT_EQ(
"TypeError: Failed to execute 'joinAdInterestGroup' on 'Navigator': "
"Failed to read the 'priorityVector' property from "
"'AuctionAdInterestGroup': The provided double value is non-finite.",
EvalJs(shell(),
base::StringPrintf(R"(
(async function() {
try {
await navigator.joinAdInterestGroup(
{
name: 'cars',
owner: '%s',
priorityVector: {'foo': %s},
},
/*joinDurationSec=*/1000);
} catch (e) {
return e.toString();
}
return 'done';
})())",
origin_string.c_str(), test_case.c_str())));
EXPECT_EQ(
"TypeError: Failed to execute 'joinAdInterestGroup' on 'Navigator': "
"Failed to read the 'prioritySignalsOverrides' property from "
"'AuctionAdInterestGroup': The provided double value is non-finite.",
EvalJs(shell(),
base::StringPrintf(R"(
(async function() {
try {
await navigator.joinAdInterestGroup(
{
name: 'cars',
owner: '%s',
prioritySignalsOverrides: {'foo': %s},
},
/*joinDurationSec=*/1000);
} catch (e) {
return e.toString();
}
return 'done';
})())",
origin_string.c_str(), test_case.c_str())));
EXPECT_EQ(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': Failed to "
"read the 'perBuyerPrioritySignals' property from 'AuctionAdConfig': "
"The provided double value is non-finite.",
RunAuctionAndWait(base::StringPrintf(R"({
seller: 'https://test.com',
decisionLogicURL: 'https://test.com',
perBuyerPrioritySignals: {
'https://foo.com/':{'key': %s}
}
})",
test_case.c_str())));
EXPECT_EQ(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': Failed to "
"read the 'perBuyerPrioritySignals' property from 'AuctionAdConfig': "
"The provided double value is non-finite.",
RunAuctionAndWait(base::StringPrintf(R"({
seller: 'https://test.com',
decisionLogicURL: 'https://test.com',
perBuyerPrioritySignals: {
'*':{'key': %s}
}
})",
test_case.c_str())));
EXPECT_EQ(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': Failed to "
"read the 'auctionReportBuyers' property from 'AuctionAdConfig': "
"Failed to read the 'scale' property from 'AuctionReportBuyersConfig': "
"The provided double value is non-finite.",
RunAuctionAndWait(base::StringPrintf(R"({
seller: 'https://test.com',
decisionLogicURL: 'https://test.com',
auctionReportBuyers: {
'interestGroupCount':{'bucket': 0n, 'scale': %s}
}
})",
test_case.c_str())));
}
WaitForAccessObserved({});
}
// Test for invalid origin in perBuyerMultiBidLimits.
IN_PROC_BROWSER_TEST_F(InterestGroupMultiBidBrowserTest,
RunAdAuctionInvalidPerBuyerMultiBidLimits) {
ASSERT_TRUE(NavigateToURL(
shell(), embedded_https_test_server().GetURL("a.test", "/echo")));
AttachInterestGroupObserver();
EXPECT_EQ(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': "
"perBuyerMultiBidLimits buyer 'https://invalid^&' for AuctionAdConfig "
"with seller 'https://test.com' must be \"*\" (wildcard) or a valid "
"https origin.",
RunAuctionAndWait(R"({
seller: 'https://test.com',
decisionLogicURL: 'https://test.com',
perBuyerMultiBidLimits: {'https://invalid^&': 100}
})"));
WaitForAccessObserved({});
}
// It's invalid for an auction to have both component auctions and buyers.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionInvalidComponentAuctionsAndBuyers) {
ASSERT_TRUE(NavigateToURL(
shell(), embedded_https_test_server().GetURL("a.test", "/echo")));
EXPECT_EQ(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': Auctions "
"may only have one of 'interestGroupBuyers' or 'componentAuctions'.",
RunAuctionAndWait(R"({
seller: 'https://test.com',
decisionLogicURL: 'https://test.com',
interestGroupBuyers: ['https://test.com'],
componentAuctions: [{
seller: 'https://test.com',
decisionLogicURL: 'https://test.com',
interestGroupBuyers: ['https://test.com']
}]
})"));
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionInvalidComponentAuctionsArray) {
ASSERT_TRUE(NavigateToURL(
shell(), embedded_https_test_server().GetURL("a.test", "/echo")));
EXPECT_EQ(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': Failed to "
"read the 'componentAuctions' property from 'AuctionAdConfig': The "
"provided value cannot be converted to a sequence.",
RunAuctionAndWait(R"({
seller: 'https://test.com',
decisionLogicURL: 'https://test.com',
componentAuctions: ''
})"));
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionInvalidComponentAuctionsElementType) {
ASSERT_TRUE(NavigateToURL(
shell(), embedded_https_test_server().GetURL("a.test", "/echo")));
EXPECT_EQ(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': Failed to "
"read the 'componentAuctions' property from 'AuctionAdConfig': "
"The provided value is not of type 'AuctionAdConfig'.",
RunAuctionAndWait(R"({
seller: 'https://test.com',
decisionLogicURL: 'https://test.com',
componentAuctions: ['test']
})"));
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionInvalidComponentAuctionsAuctionConfig) {
ASSERT_TRUE(NavigateToURL(
shell(), embedded_https_test_server().GetURL("a.test", "/echo")));
EXPECT_EQ(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': seller "
"'http://test.com' for AuctionAdConfig must be a valid https origin.",
RunAuctionAndWait(R"({
seller: 'https://test.com',
decisionLogicURL: 'https://test.com',
componentAuctions: [{
seller: 'http://test.com',
decisionLogicURL: 'http://test.com'
}]
})"));
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionInvalidComponentAuctionDepth) {
ASSERT_TRUE(NavigateToURL(
shell(), embedded_https_test_server().GetURL("a.test", "/echo")));
EXPECT_EQ(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': Auctions "
"listed in componentAuctions may not have their own nested "
"componentAuctions.",
RunAuctionAndWait(R"({
seller: 'https://test.com',
decisionLogicURL: 'https://test.com',
componentAuctions: [{
seller: 'https://test2.com',
decisionLogicURL: 'https://test2.com',
componentAuctions: [{
seller: 'https://test3.com',
decisionLogicURL: 'https://test3.com',
}]
}]
})"));
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionInvalidPerBuyerSignals) {
GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin test_origin = url::Origin::Create(test_url);
GURL decision_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
console_observer.SetPattern(
"Uncaught (in promise) TypeError: Failed to execute 'runAdAuction' on "
"'NavigatorAuction': perBuyerSignals for AuctionAdConfig with seller "
"'https://a.test:*' must be a JSON-serializable object.");
EXPECT_EQ(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': Promise "
"argument rejected or resolved to invalid value.",
RunAuctionAndWait(JsReplace(R"({
seller: $1,
decisionLogicURL: $2,
perBuyerSignals: {'https://test.com': function() {}},
interestGroupBuyers: []
})",
test_origin, decision_url)));
EXPECT_TRUE(console_observer.Wait());
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionRejectPromiseDirectFromSellerSignals) {
GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin test_origin = url::Origin::Create(test_url);
ASSERT_TRUE(NavigateToURL(shell(), test_url));
GURL decision_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js");
const char kAuctionConfigTemplate[] = R"({
seller: $1,
decisionLogicURL: $2,
directFromSellerSignals: new Promise((resolve, reject) => { setTimeout(
() => { reject('boo'); }, 10) }),
interestGroupBuyers: []
})";
EXPECT_EQ(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': Promise "
"argument rejected or resolved to invalid value.",
RunAuctionAndWait(
JsReplace(kAuctionConfigTemplate, test_origin, decision_url)));
}
IN_PROC_BROWSER_TEST_F(
InterestGroupBrowserTest,
RunAdAuctionRejectPromiseDirectFromSellerSignalsHeaderAdSlot) {
GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin test_origin = url::Origin::Create(test_url);
ASSERT_TRUE(NavigateToURL(shell(), test_url));
GURL decision_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js");
const char kAuctionConfigTemplate[] = R"({
seller: $1,
decisionLogicURL: $2,
directFromSellerSignalsHeaderAdSlot: new Promise((resolve, reject) => {
setTimeout(() => { reject('boo'); }, 10) }),
interestGroupBuyers: []
})";
EXPECT_EQ(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': Promise "
"argument rejected or resolved to invalid value.",
RunAuctionAndWait(
JsReplace(kAuctionConfigTemplate, test_origin, decision_url)));
}
IN_PROC_BROWSER_TEST_F(
InterestGroupBrowserTest,
RunAdAuctionMissingDirectFromSellerSignalsHeaderAdSlotLogged) {
GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin test_origin = url::Origin::Create(test_url);
ASSERT_TRUE(NavigateToURL(shell(), test_url));
GURL decision_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js");
// Fetch a URL with adAuctionHeaders: true. The response has no
// Ad-Auction-Signals header, so signals aren't found. An error should be
// logged to devtools.
EXPECT_TRUE(ExecJs(
web_contents()->GetPrimaryMainFrame(),
content::JsReplace("fetch($1, {adAuctionHeaders: true})", test_url)));
const char kAuctionConfigTemplate[] = R"({
seller: $1,
decisionLogicURL: $2,
directFromSellerSignalsHeaderAdSlot: "notFound",
interestGroupBuyers: []
})";
WebContentsConsoleObserver console_observer(shell()->web_contents());
console_observer.SetPattern(
"Worklet error: When looking for directFromSellerSignalsHeaderAdSlot "
"notFound, failed to find a matching response.");
EXPECT_EQ(base::Value(),
RunAuctionAndWait(
JsReplace(kAuctionConfigTemplate, test_origin, decision_url)));
EXPECT_TRUE(console_observer.Wait());
}
// Parse errors for directFromSellerSignalsHeaderAdSlot are logged to devtools.
IN_PROC_BROWSER_TEST_F(
InterestGroupBrowserTest,
RunAdAuctionInvalidDirectFromSellerSignalsHeaderAdSlotLogged) {
constexpr char kBidderHost[] = "a.test";
constexpr char kTopFrameHost[] = "c.test";
constexpr char kSellerHost[] = "b.test";
url::Origin seller_origin = url::Origin::Create(
embedded_https_test_server().GetURL(kSellerHost, "/echo"));
const url::Origin top_frame_origin = url::Origin::Create(
embedded_https_test_server().GetURL(kTopFrameHost, "/echo"));
GURL bidder_url = embedded_https_test_server().GetURL(kBidderHost, "/echo");
ASSERT_TRUE(NavigateToURL(shell(), bidder_url));
url::Origin bidder_origin = url::Origin::Create(bidder_url);
ASSERT_EQ(kSuccess,
JoinInterestGroupAndVerify(
/*owner=*/bidder_origin, /*name=*/"cars", /*priority=*/0.0,
blink::InterestGroup::ExecutionMode::kCompatibilityMode,
/*bidding_url=*/
embedded_https_test_server().GetURL(
kBidderHost, "/interest_group/bidding_logic.js"),
/*ads=*/
{{{GURL("https://example.com/render"),
/*metadata=*/std::nullopt}}}));
GURL top_frame_url =
embedded_https_test_server().GetURL(kTopFrameHost, "/echo");
ASSERT_TRUE(NavigateToURL(shell(), top_frame_url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
console_observer.SetPattern(
"directFromSellerSignalsHeaderAdSlot: encountered dict without "
"\"adSlot\" key: Ad-Auction-Signals=[{ \"no\": \"adSlot\", "
"\"sellerSignals\": {\"json\": \"for\", \"the\": [\"seller\"]} }]");
const char kHeaderSignalsPath[] = "/header_direct_from_seller_signals.json";
// The actual body of the request is just an empty JSON dict for the test,
// but it could be any arbitrary payload that the server wants to deliver
// alongside the header signals.
const char kHeaderSignalsBodyResponse[] = "{}";
// The adSlot key is not present, so kHeaderSignalsResponse is invalid. The
// signals given to worklet functions should be null, and errors should be
// logged to devtools.
const char kHeaderSignalsResponse[] = R"([{
"no": "adSlot",
"sellerSignals": {"json": "for", "the": ["seller"]}
}])";
network_responder_->RegisterNetworkResponse(
kHeaderSignalsPath, kHeaderSignalsBodyResponse, "application/json",
/*extra_response_headers=*/
{{"Access-Control-Allow-Origin", top_frame_origin.Serialize()},
{"Ad-Auction-Signals", kHeaderSignalsResponse}});
EXPECT_TRUE(ExecJs(web_contents()->GetPrimaryMainFrame(),
content::JsReplace("fetch($1, {adAuctionHeaders: true})",
embedded_https_test_server().GetURL(
kSellerHost, kHeaderSignalsPath))));
EXPECT_EQ("https://example.com/render",
RunAuctionAndWaitForUrl(JsReplace(
R"(
{
seller: $1,
decisionLogicUrl: $2,
interestGroupBuyers: [$3],
directFromSellerSignalsHeaderAdSlot: "adSlot1"
})",
seller_origin,
embedded_https_test_server().GetURL(
kSellerHost,
"/interest_group/"
"decision_no_direct_from_seller_signals_validator.js"),
bidder_origin)));
EXPECT_TRUE(console_observer.Wait());
}
// If this test fails, check that you haven't added a required field that's
// alphabetically before directFromSellerSignalsHeaderAdSlot. See details at
// https://github.com/WICG/turtledove/issues/803
IN_PROC_BROWSER_TEST_F(
InterestGroupBrowserTest,
RunAdAuctionDirectFromSellerSignalsHeaderAdSlotFeatureDetection) {
GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
EXPECT_TRUE(EvalJs(shell(), R"(
(async function() {
let dfss = false;
navigator.runAdAuction({
get directFromSellerSignalsHeaderAdSlot() { dfss = true; }
}).catch((e) => {});
return dfss;
})())")
.ExtractBool());
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionPromiseInvalidDirectFromSellerSignals) {
GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin test_origin = url::Origin::Create(test_url);
ASSERT_TRUE(NavigateToURL(shell(), test_url));
GURL decision_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js");
const char kAuctionConfigTemplate[] = R"({
seller: $1,
decisionLogicURL: $2,
directFromSellerSignals: new Promise((resolve, reject) => { setTimeout(
() => { resolve('http://test.com/signals'); }, 10) }),
interestGroupBuyers: []
})";
WebContentsConsoleObserver console_observer(shell()->web_contents());
console_observer.SetPattern(
"Uncaught (in promise) TypeError: Failed to execute 'runAdAuction' on "
"'NavigatorAuction': directFromSellerSignals 'http://test.com/signals' "
"for AuctionAdConfig with seller 'https://a.test:*' must match seller "
"origin; only https scheme is supported.");
EXPECT_EQ(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': Promise "
"argument rejected or resolved to invalid value.",
RunAuctionAndWait(
JsReplace(kAuctionConfigTemplate, test_origin, decision_url)));
EXPECT_TRUE(console_observer.Wait());
}
IN_PROC_BROWSER_TEST_F(
InterestGroupBrowserTest,
RunAdAuctionPromiseToStringThrowDirectFromSellerSignals) {
GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin test_origin = url::Origin::Create(test_url);
ASSERT_TRUE(NavigateToURL(shell(), test_url));
GURL decision_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js");
const char kAuctionConfigTemplate[] = R"({
seller: $1,
decisionLogicURL: $2,
directFromSellerSignals: new Promise((resolve, reject) => {
let o = { toString: () => { throw "Don't stringify me!"; } }
resolve(o);
}),
interestGroupBuyers: []
})";
WebContentsConsoleObserver console_observer(shell()->web_contents());
console_observer.SetPattern("Uncaught (in promise) Don't stringify me!");
EXPECT_EQ(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': Promise "
"argument rejected or resolved to invalid value.",
RunAuctionAndWait(
JsReplace(kAuctionConfigTemplate, test_origin, decision_url)));
EXPECT_TRUE(console_observer.Wait());
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionInvalidDirectFromSellerSignalsInvalidURL) {
GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin test_origin = url::Origin::Create(test_url);
GURL decision_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
console_observer.SetPattern(
"Uncaught (in promise) TypeError: Failed to execute 'runAdAuction' on "
"'NavigatorAuction': directFromSellerSignals 'https://invalid^&' for "
"AuctionAdConfig with seller 'https://a.test:*' cannot be resolved to a "
"valid URL.");
EXPECT_EQ(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': Promise "
"argument rejected or resolved to invalid value.",
RunAuctionAndWait(JsReplace(R"({
seller: $1,
decisionLogicURL: $2,
directFromSellerSignals: 'https://invalid^&',
interestGroupBuyers: []
})",
test_origin, decision_url)));
EXPECT_TRUE(console_observer.Wait());
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionInvalidDirectFromSellerSignalsNotHttps) {
GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin test_origin = url::Origin::Create(test_url);
GURL decision_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
console_observer.SetPattern(
"Uncaught (in promise) TypeError: Failed to execute 'runAdAuction' on "
"'NavigatorAuction': directFromSellerSignals 'http://test.com/signals' "
"for AuctionAdConfig with seller 'https://a.test:*' must match seller "
"origin; only https scheme is supported.");
EXPECT_EQ(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': Promise "
"argument rejected or resolved to invalid value.",
RunAuctionAndWait(JsReplace(R"({
seller: $1,
decisionLogicURL: $2,
directFromSellerSignals: 'http://test.com/signals',
interestGroupBuyers: []
})",
test_origin, decision_url)));
EXPECT_TRUE(console_observer.Wait());
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionInvalidDirectFromSellerSignalsWrongOrigin) {
GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin test_origin = url::Origin::Create(test_url);
GURL decision_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
console_observer.SetPattern(
"Uncaught (in promise) TypeError: Failed to execute 'runAdAuction' on "
"'NavigatorAuction': directFromSellerSignals 'https://test2.com/signals' "
"for AuctionAdConfig with seller 'https://a.test:*' must match seller "
"origin; only https scheme is supported.");
EXPECT_EQ(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': Promise "
"argument rejected or resolved to invalid value.",
RunAuctionAndWait(JsReplace(R"({
seller: $1,
decisionLogicURL: $2,
directFromSellerSignals: 'https://test2.com/signals',
interestGroupBuyers: [$1]
})",
test_origin, decision_url)));
EXPECT_TRUE(console_observer.Wait());
}
IN_PROC_BROWSER_TEST_F(
InterestGroupBrowserTest,
RunAdAuctionInvalidDirectFromSellerSignalsHasQueryString) {
GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin test_origin = url::Origin::Create(test_url);
GURL decision_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
console_observer.SetPattern(
"Uncaught (in promise) TypeError: Failed to execute 'runAdAuction' on "
"'NavigatorAuction': directFromSellerSignals "
"'https://a.test:*/signals?shouldntBeHere' for AuctionAdConfig with "
"seller 'https://a.test:*' URL prefix must not have a query string.");
EXPECT_EQ(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': Promise "
"argument rejected or resolved to invalid value.",
RunAuctionAndWait(JsReplace(R"({
seller: $1,
decisionLogicURL: $2,
directFromSellerSignals: $1 + '/signals?shouldntBeHere',
interestGroupBuyers: [$1]
})",
test_origin, decision_url)));
EXPECT_TRUE(console_observer.Wait());
}
// `bidder_origin` is used in per-buyer signals, but the bundle only has
// per-buyer signals for `non_bidder_origin`.
//
// No signals should be delivered.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
DirectFromSellerSignalsNotInBuyers) {
constexpr char kBidderHost[] = "a.test";
constexpr char kTopFrameHost[] = "c.test";
constexpr char kSellerHost[] = "b.test";
constexpr char kNonBidderHost[] = "d.test";
url::Origin seller_origin = url::Origin::Create(
embedded_https_test_server().GetURL(kSellerHost, "/echo"));
url::Origin top_frame_origin = url::Origin::Create(
embedded_https_test_server().GetURL(kTopFrameHost, "/echo"));
GURL bidder_url = embedded_https_test_server().GetURL(kBidderHost, "/echo");
ASSERT_TRUE(NavigateToURL(shell(), bidder_url));
url::Origin bidder_origin = url::Origin::Create(bidder_url);
GURL non_bidder_url =
embedded_https_test_server().GetURL(kNonBidderHost, "/echo");
url::Origin non_bidder_origin = url::Origin::Create(non_bidder_url);
ASSERT_EQ(kSuccess,
JoinInterestGroupAndVerify(
/*owner=*/bidder_origin, /*name=*/"cars", /*priority=*/0.0,
blink::InterestGroup::ExecutionMode::kCompatibilityMode,
/*bidding_url=*/
embedded_https_test_server().GetURL(
kBidderHost,
"/interest_group/"
"bidding_no_direct_from_seller_signals_validator.js"),
/*ads=*/
{{{GURL("https://example.com/render"),
/*metadata=*/std::nullopt}}}));
std::vector<NetworkResponder::SubresourceResponse> subresource_responses = {
NetworkResponder::DirectFromSellerPerBuyerSignals(
non_bidder_origin, /*payload=*/
R"({"json": "for", "buyer": [1]})")};
std::vector<NetworkResponder::SubresourceBundle> bundles = {
NetworkResponder::SubresourceBundle(
/*bundle_url=*/embedded_https_test_server().GetURL(
kSellerHost, "/generated_bundle.wbn"),
/*subresources=*/subresource_responses)};
network_responder_->RegisterDirectFromSellerSignalsResponse(
/*bundles=*/bundles,
/*allow_origin=*/top_frame_origin.Serialize());
constexpr char kPagePath[] = "/page-with-bundles.html";
network_responder_->RegisterHtmlWithSubresourceBundles(
/*bundles=*/bundles,
/*page_url=*/kPagePath);
ASSERT_TRUE(NavigateToURL(
shell(), embedded_https_test_server().GetURL(kTopFrameHost, kPagePath)));
EXPECT_EQ("https://example.com/render",
RunAuctionAndWaitForUrl(JsReplace(
R"(
{
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$3],
directFromSellerSignals: $4
})",
seller_origin,
embedded_https_test_server().GetURL(
kSellerHost,
"/interest_group/"
"decision_no_direct_from_seller_signals_validator.js"),
bidder_origin,
embedded_https_test_server().GetURL(
kSellerHost, "/direct_from_seller_signals"))));
}
// The bundle is served from `seller_origin`, but the subresource is from
// `bidder_origin` -- subresource bundles don't allow subresources to be
// cross-origin with their bundle's origin.
//
// No signals should be delivered.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
DirectFromSellerSignalsBundleSubresourceOriginMismatch) {
constexpr char kBidderHost[] = "a.test";
constexpr char kTopFrameHost[] = "c.test";
constexpr char kSellerHost[] = "b.test";
url::Origin seller_origin = url::Origin::Create(
embedded_https_test_server().GetURL(kSellerHost, "/echo"));
url::Origin top_frame_origin = url::Origin::Create(
embedded_https_test_server().GetURL(kTopFrameHost, "/echo"));
GURL bidder_url = embedded_https_test_server().GetURL(kBidderHost, "/echo");
ASSERT_TRUE(NavigateToURL(shell(), bidder_url));
url::Origin bidder_origin = url::Origin::Create(bidder_url);
ASSERT_EQ(kSuccess,
JoinInterestGroupAndVerify(
/*owner=*/bidder_origin, /*name=*/"cars", /*priority=*/0.0,
blink::InterestGroup::ExecutionMode::kCompatibilityMode,
/*bidding_url=*/
embedded_https_test_server().GetURL(
kBidderHost,
"/interest_group/"
"bidding_no_direct_from_seller_signals_validator.js"),
/*ads=*/
{{{GURL("https://example.com/render"),
/*metadata=*/std::nullopt}}}));
std::vector<NetworkResponder::SubresourceResponse> subresource_responses = {
NetworkResponder::DirectFromSellerPerBuyerSignals(
bidder_origin, /*payload=*/
R"({"json": "for", "buyer": [1]})", /*prefix=*/
base::StringPrintf("%s/direct_from_seller_signals",
bidder_origin.Serialize().c_str()))};
std::vector<NetworkResponder::SubresourceBundle> bundles = {
NetworkResponder::SubresourceBundle(
/*bundle_url=*/embedded_https_test_server().GetURL(
kSellerHost, "/generated_bundle.wbn"),
/*subresources=*/subresource_responses)};
network_responder_->RegisterDirectFromSellerSignalsResponse(
/*bundles=*/bundles,
/*allow_origin=*/top_frame_origin.Serialize());
constexpr char kPagePath[] = "/page-with-bundles.html";
network_responder_->RegisterHtmlWithSubresourceBundles(
/*bundles=*/bundles,
/*page_url=*/kPagePath);
ASSERT_TRUE(NavigateToURL(
shell(), embedded_https_test_server().GetURL(kTopFrameHost, kPagePath)));
TestFencedFrameURLMappingResultObserver observer;
EXPECT_EQ("https://example.com/render",
RunAuctionAndWaitForUrl(JsReplace(
R"(
{
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$3],
directFromSellerSignals: $4
})",
seller_origin,
embedded_https_test_server().GetURL(
kSellerHost,
"/interest_group/"
"decision_no_direct_from_seller_signals_validator.js"),
bidder_origin,
embedded_https_test_server().GetURL(
kSellerHost, "/direct_from_seller_signals"))));
}
// Use "bundle_doesnt_exist.wbn" as the bundle filename -- the fetch for the
// bundle will fail, and null will be passed to the worklet. Note that no
// exception is thrown; the auction does run, since the
// <script type="webbundle"> tag does declare a subresource
// "/direct_from_seller_signals?auctionSignals", but the bundle itself (which
// loads asynchronously) fails to load, causing the auction-time
// DirectFromSellerSignals load to fail.
//
// No signals should be delivered.
IN_PROC_BROWSER_TEST_F(
InterestGroupBrowserTest,
RunAdAuctionInvalidDirectFromSellerSignalsBundleDoesntExist) {
constexpr char kBidderHost[] = "a.test";
constexpr char kTopFrameHost[] = "c.test";
constexpr char kSellerHost[] = "b.test";
url::Origin seller_origin = url::Origin::Create(
embedded_https_test_server().GetURL(kSellerHost, "/echo"));
url::Origin top_frame_origin = url::Origin::Create(
embedded_https_test_server().GetURL(kTopFrameHost, "/echo"));
GURL bidder_url = embedded_https_test_server().GetURL(kBidderHost, "/echo");
ASSERT_TRUE(NavigateToURL(shell(), bidder_url));
url::Origin bidder_origin = url::Origin::Create(bidder_url);
ASSERT_EQ(kSuccess,
JoinInterestGroupAndVerify(
/*owner=*/bidder_origin, /*name=*/"cars", /*priority=*/0.0,
blink::InterestGroup::ExecutionMode::kCompatibilityMode,
/*bidding_url=*/
embedded_https_test_server().GetURL(
kBidderHost,
"/interest_group/"
"bidding_no_direct_from_seller_signals_validator.js"),
/*ads=*/
{{{GURL("https://example.com/render"),
/*metadata=*/std::nullopt}}}));
std::vector<NetworkResponder::SubresourceResponse> subresource_responses = {
NetworkResponder::SubresourceResponse(
/*subresource_url=*/"/direct_from_seller_signals?auctionSignals",
/*payload=*/
// NOTE: This doesn't really matter -- it's never sent.
R"({"json": "for", "all": ["parties"]})")};
// Tell the page about this bundle, but don't actually serve it over the
// network.
std::vector<NetworkResponder::SubresourceBundle> bundles = {
NetworkResponder::SubresourceBundle(
/*bundle_url=*/embedded_https_test_server().GetURL(
kSellerHost, "/bundle_doesnt_exist.wbn"),
/*subresources=*/subresource_responses)};
constexpr char kPagePath[] = "/page-with-bundles.html";
network_responder_->RegisterHtmlWithSubresourceBundles(
/*bundles=*/bundles,
/*page_url=*/kPagePath);
ASSERT_TRUE(NavigateToURL(
shell(), embedded_https_test_server().GetURL(kTopFrameHost, kPagePath)));
EXPECT_EQ("https://example.com/render",
RunAuctionAndWaitForUrl(JsReplace(
R"(
{
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$3],
directFromSellerSignals: $4
})",
seller_origin,
embedded_https_test_server().GetURL(
kSellerHost,
"/interest_group/"
"decision_no_direct_from_seller_signals_validator.js"),
bidder_origin,
embedded_https_test_server().GetURL(
kSellerHost, "/direct_from_seller_signals"))));
}
// Create a cross-origin iframe, and run an auction in that iframe using
// DirectFromSellerSignals.
//
// The signals should be correctly loaded.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
DirectFromSellerSignalsInCrossOriginIframe) {
constexpr char kBidderHost[] = "a.test";
constexpr char kTopFrameHost[] = "c.test";
constexpr char kSellerHost[] = "b.test";
constexpr char kIframeHost[] = "d.test";
url::Origin seller_origin = url::Origin::Create(
embedded_https_test_server().GetURL(kSellerHost, "/echo"));
url::Origin iframe_origin = url::Origin::Create(
embedded_https_test_server().GetURL(kIframeHost, "/echo"));
GURL bidder_url = embedded_https_test_server().GetURL(kBidderHost, "/echo");
ASSERT_TRUE(NavigateToURL(shell(), bidder_url));
url::Origin bidder_origin = url::Origin::Create(bidder_url);
ASSERT_EQ(kSuccess,
JoinInterestGroupAndVerify(
/*owner=*/bidder_origin, /*name=*/"cars", /*priority=*/0.0,
blink::InterestGroup::ExecutionMode::kCompatibilityMode,
/*bidding_url=*/
embedded_https_test_server().GetURL(
kBidderHost, "/interest_group/bidding_logic.js"),
/*ads=*/
{{{GURL("https://example.com/render"),
/*metadata=*/std::nullopt}}}));
std::vector<NetworkResponder::SubresourceResponse> subresource_responses = {
NetworkResponder::SubresourceResponse(
/*subresource_url=*/"/direct_from_seller_signals?sellerSignals",
/*payload=*/
R"({"json": "for", "the": ["seller"]})")};
std::vector<NetworkResponder::SubresourceBundle> bundles = {
NetworkResponder::SubresourceBundle(
/*bundle_url=*/embedded_https_test_server().GetURL(
kSellerHost, "/generated_bundle.wbn"),
/*subresources=*/subresource_responses)};
network_responder_->RegisterDirectFromSellerSignalsResponse(
/*bundles=*/bundles,
/*allow_origin=*/iframe_origin.Serialize());
constexpr char kIframePagePath[] = "/page-with-bundles.html";
network_responder_->RegisterHtmlWithSubresourceBundles(
/*bundles=*/bundles,
/*page_url=*/kIframePagePath);
GURL top_frame_url = embedded_https_test_server().GetURL(
kTopFrameHost,
base::StringPrintf(
"/cross_site_iframe_factory.html?%s(%s{run-ad-auction})",
kTopFrameHost,
embedded_https_test_server()
.GetURL(kIframeHost, kIframePagePath)
.spec()
.c_str()));
for (bool use_promise : {false, true}) {
SCOPED_TRACE(use_promise);
ASSERT_TRUE(NavigateToURL(shell(), top_frame_url));
RenderFrameHost* const iframe_host =
ChildFrameAt(web_contents()->GetPrimaryMainFrame(), /*index=*/0);
ASSERT_TRUE(ExecJs(iframe_host, MaybePromiseFunction(use_promise)));
EXPECT_EQ("https://example.com/render",
RunAuctionAndWaitForUrl(
JsReplace(R"(
{
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$3],
directFromSellerSignals: maybePromise($4)
})",
seller_origin,
embedded_https_test_server().GetURL(
kSellerHost,
"/interest_group/"
"decision_simple_direct_from_"
"seller_signals_validator.js"),
bidder_origin,
embedded_https_test_server().GetURL(
kSellerHost, "/direct_from_seller_signals")),
iframe_host));
}
}
// Create a cross origin iframe, and load header-based directFromSellerSignals
// in that frame. Then, in the main frame, run an auction using the loaded
// signals.
IN_PROC_BROWSER_TEST_F(
InterestGroupBrowserTest,
DirectFromSellerSignalsHeaderAdSlotFromCrossOriginIframe) {
constexpr char kBidderHost[] = "a.test";
constexpr char kTopFrameHost[] = "c.test";
constexpr char kSellerHost[] = "b.test";
constexpr char kIframeHost[] = "d.test";
url::Origin seller_origin = url::Origin::Create(
embedded_https_test_server().GetURL(kSellerHost, "/echo"));
url::Origin iframe_origin = url::Origin::Create(
embedded_https_test_server().GetURL(kIframeHost, "/echo"));
GURL bidder_url = embedded_https_test_server().GetURL(kBidderHost, "/echo");
ASSERT_TRUE(NavigateToURL(shell(), bidder_url));
url::Origin bidder_origin = url::Origin::Create(bidder_url);
ASSERT_EQ(kSuccess,
JoinInterestGroupAndVerify(
/*owner=*/bidder_origin, /*name=*/"cars", /*priority=*/0.0,
blink::InterestGroup::ExecutionMode::kCompatibilityMode,
/*bidding_url=*/
embedded_https_test_server().GetURL(
kBidderHost, "/interest_group/bidding_logic.js"),
/*ads=*/
{{{GURL("https://example.com/render"),
/*metadata=*/std::nullopt}}}));
GURL top_frame_url = embedded_https_test_server().GetURL(
kTopFrameHost,
base::StringPrintf("/cross_site_iframe_factory.html?%s(%s)",
kTopFrameHost,
embedded_https_test_server()
.GetURL(kIframeHost, "/echo")
.spec()
.c_str()));
ASSERT_TRUE(NavigateToURL(shell(), top_frame_url));
RenderFrameHost* const iframe_host =
ChildFrameAt(web_contents()->GetPrimaryMainFrame(), /*index=*/0);
const char kHeaderSignalsPath[] = "/header_direct_from_seller_signals.json";
// The actual body of the request is just an empty JSON dict for the test,
// but it could be any arbitrary payload that the server wants to deliver
// alongside the header signals.
const char kHeaderSignalsBodyResponse[] = "{}";
const char kHeaderSignalsResponse[] = R"([{
"adSlot": "adSlot1",
"sellerSignals": {"json": "for", "the": ["seller"]}
}])";
network_responder_->RegisterNetworkResponse(
kHeaderSignalsPath, kHeaderSignalsBodyResponse, "application/json",
/*extra_response_headers=*/
{{"Access-Control-Allow-Origin", iframe_origin.Serialize()},
{"Ad-Auction-Signals", kHeaderSignalsResponse}});
EXPECT_TRUE(ExecJs(iframe_host,
content::JsReplace("fetch($1, {adAuctionHeaders: true})",
embedded_https_test_server().GetURL(
kSellerHost, kHeaderSignalsPath))));
EXPECT_EQ(
"https://example.com/render",
RunAuctionAndWaitForUrl(JsReplace(
R"(
{
seller: $1,
decisionLogicUrl: $2,
interestGroupBuyers: [$3],
directFromSellerSignalsHeaderAdSlot: "adSlot1"
})",
seller_origin,
embedded_https_test_server().GetURL(kSellerHost,
"/interest_group/"
"decision_simple_direct_from_"
"seller_signals_validator.js"),
bidder_origin)));
}
// Start an auction using directFromSellerSignalsHeaderAdSlot, but navigate away
// immediately after starting the auction.
IN_PROC_BROWSER_TEST_F(
InterestGroupBrowserTest,
DirectFromSellerSignalsHeaderAdSlotNavigateAwayDuringAuction) {
constexpr char kBidderHost[] = "a.test";
constexpr char kTopFrameHost[] = "c.test";
constexpr char kSellerHost[] = "b.test";
constexpr char kNewTopFrameHost[] = "d.test";
url::Origin seller_origin = url::Origin::Create(
embedded_https_test_server().GetURL(kSellerHost, "/echo"));
const url::Origin top_frame_origin = url::Origin::Create(
embedded_https_test_server().GetURL(kTopFrameHost, "/echo"));
GURL bidder_url = embedded_https_test_server().GetURL(kBidderHost, "/echo");
ASSERT_TRUE(NavigateToURL(shell(), bidder_url));
url::Origin bidder_origin = url::Origin::Create(bidder_url);
ASSERT_EQ(kSuccess,
JoinInterestGroupAndVerify(
/*owner=*/bidder_origin, /*name=*/"cars", /*priority=*/0.0,
blink::InterestGroup::ExecutionMode::kCompatibilityMode,
/*bidding_url=*/
embedded_https_test_server().GetURL(
kBidderHost, "/interest_group/bidding_logic.js"),
/*ads=*/
{{{GURL("https://example.com/render"),
/*metadata=*/std::nullopt}}}));
GURL top_frame_url =
embedded_https_test_server().GetURL(kTopFrameHost, "/echo");
ASSERT_TRUE(NavigateToURL(shell(), top_frame_url));
const char kHeaderSignalsPath[] = "/header_direct_from_seller_signals.json";
// The actual body of the request is just an empty JSON dict for the test,
// but it could be any arbitrary payload that the server wants to deliver
// alongside the header signals.
const char kHeaderSignalsBodyResponse[] = "{}";
const char kHeaderSignalsResponse[] = R"([{
"adSlot": "adSlot1",
"sellerSignals": {"json": "for", "the": ["seller"]}
}])";
network_responder_->RegisterNetworkResponse(
kHeaderSignalsPath, kHeaderSignalsBodyResponse, "application/json",
/*extra_response_headers=*/
{{"Access-Control-Allow-Origin", top_frame_origin.Serialize()},
{"Ad-Auction-Signals", kHeaderSignalsResponse}});
EXPECT_TRUE(ExecJs(web_contents()->GetPrimaryMainFrame(),
content::JsReplace("fetch($1, {adAuctionHeaders: true})",
embedded_https_test_server().GetURL(
kSellerHost, kHeaderSignalsPath))));
ExecuteScriptAsync(
web_contents()->GetPrimaryMainFrame(),
JsReplace(
R"(
(async function() {
return await navigator.runAdAuction({
seller: $1,
decisionLogicUrl: $2,
interestGroupBuyers: [$3],
directFromSellerSignalsHeaderAdSlot: "adSlot1"
});
})())",
seller_origin,
embedded_https_test_server().GetURL(kSellerHost,
"/interest_group/"
"decision_simple_direct_from_"
"seller_signals_validator.js"),
bidder_origin));
// Navigate away without waiting for the auction to complete. Nothing should
// crash.
ASSERT_TRUE(NavigateToURL(
shell(), embedded_https_test_server().GetURL(kNewTopFrameHost, "/echo")));
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
InvalidDirectFromSellerSignalsHeaderAdSlot) {
GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin test_origin = url::Origin::Create(test_url);
GURL decision_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
console_observer.SetPattern("Uncaught (in promise) Error!");
EXPECT_EQ(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': Promise "
"argument rejected or resolved to invalid value.",
RunAuctionAndWait(JsReplace(R"({
seller: $1,
decisionLogicURL: $2,
directFromSellerSignalsHeaderAdSlot: Promise.resolve((() => {
function CantConvertToString() {}
CantConvertToString.prototype.toString = function () {
throw 'Error!';
};
return new CantConvertToString();
})()),
interestGroupBuyers: [$1]
})",
test_origin, decision_url)));
EXPECT_TRUE(console_observer.Wait());
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
InvalidDirectFromSellerSignalsHeaderAdSlotAndBundles) {
GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_cars");
GURL decision_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
EXPECT_EQ(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': The auction "
"config fields directFromSellerSignals and "
"directFromSellerSignalsHeaderAdSlot must not both be specified for a "
"given component auction, top-level auction, or non-component auction.",
RunAuctionAndWait(JsReplace(R"({
seller: $1,
decisionLogicURL: $2,
directFromSellerSignals: Promise.resolve($3),
directFromSellerSignalsHeaderAdSlot: Promise.resolve("adSlot1"),
interestGroupBuyers: [$1]
})",
test_origin, decision_url, test_origin)));
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionInvalidAdditionalBidsNoNonce) {
GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin test_origin = url::Origin::Create(test_url);
GURL decision_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
const char kAuctionConfigTemplate[] = R"({
seller: $1,
decisionLogicURL: $2,
additionalBids: Promise.resolve([1, 2, 3]),
interestGroupBuyers: [$1]
})";
EXPECT_EQ(base::StringPrintf(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': "
"additionalBids specified for AuctionAdConfig with seller '%s' "
"which does not have an auctionNonce.",
test_origin.Serialize().c_str()),
RunAuctionAndWait(
JsReplace(kAuctionConfigTemplate, test_origin, decision_url)));
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionInvalidAdditionalBidsMalformattedNonce) {
GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin test_origin = url::Origin::Create(test_url);
GURL decision_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
const char kAuctionConfigTemplate[] = R"({
seller: $1,
decisionLogicURL: $2,
additionalBids: Promise.resolve([1, 2, 3]),
auctionNonce: "invalid",
interestGroupBuyers: [$1]
})";
EXPECT_EQ(base::StringPrintf(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': "
"auctionNonce for AuctionAdConfig with seller '%s' must be a "
"valid UUIDv4, but got, 'invalid'.",
test_origin.Serialize().c_str()),
RunAuctionAndWait(
JsReplace(kAuctionConfigTemplate, test_origin, decision_url)));
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionInvalidAdditionalBidsNoInterestGroupBuyers) {
GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin test_origin = url::Origin::Create(test_url);
GURL decision_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
std::string auction_nonce = CreateAuctionNonceAndWait();
const char kAuctionConfigTemplate[] = R"({
seller: $1,
decisionLogicURL: $2,
additionalBids: Promise.resolve([1, 2, 3]),
auctionNonce: $3
})";
EXPECT_EQ(base::StringPrintf(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': "
"additionalBids specified for AuctionAdConfig with seller '%s' "
"which has no interestGroupBuyers. All additionalBid buyers "
"must be in interestGroupBuyers.",
test_origin.Serialize().c_str()),
RunAuctionAndWait(JsReplace(kAuctionConfigTemplate, test_origin,
decision_url, auction_nonce)));
}
IN_PROC_BROWSER_TEST_F(
InterestGroupBrowserTest,
RunAdAuctionInvalidAdditionalBidsEmptyInterestGroupBuyers) {
GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin test_origin = url::Origin::Create(test_url);
GURL decision_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
std::string auction_nonce = CreateAuctionNonceAndWait();
const char kAuctionConfigTemplate[] = R"({
seller: $1,
decisionLogicURL: $2,
additionalBids: Promise.resolve([1, 2, 3]),
interestGroupBuyers: [],
auctionNonce: $3
})";
EXPECT_EQ(base::StringPrintf(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': "
"additionalBids specified for AuctionAdConfig with seller '%s' "
"which has no interestGroupBuyers. All additionalBid buyers "
"must be in interestGroupBuyers.",
test_origin.Serialize().c_str()),
RunAuctionAndWait(JsReplace(kAuctionConfigTemplate, test_origin,
decision_url, auction_nonce)));
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionInvalidSignedAdditionalBidsBase64) {
GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin test_origin = url::Origin::Create(test_url);
GURL decision_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
std::string auction_nonce = CreateAuctionNonceAndWait();
WebContentsConsoleObserver console_observer(shell()->web_contents());
console_observer.SetPattern(
"*Worklet error: Unable to base64-decode a signed additional bid.*");
const char kAuctionConfigTemplate[] = R"({
seller: $1,
decisionLogicURL: $2,
additionalBids: provideAdditionalBids($1, $3, ["{}"],
"invalid-signed-base-64"),
interestGroupBuyers: [$1],
auctionNonce: $3
})";
EXPECT_EQ(base::Value(),
RunAuctionAndWait(JsReplace(kAuctionConfigTemplate, test_origin,
decision_url, auction_nonce)));
EXPECT_TRUE(console_observer.Wait());
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionInvalidSignedAdditionalBidsJson) {
GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin test_origin = url::Origin::Create(test_url);
GURL decision_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
std::string auction_nonce = CreateAuctionNonceAndWait();
WebContentsConsoleObserver console_observer(shell()->web_contents());
console_observer.SetPattern(
"*Worklet error: Unable to parse signed additional bid as JSON*");
const char kAuctionConfigTemplate[] = R"({
seller: $1,
decisionLogicURL: $2,
additionalBids: provideAdditionalBids($1, $3, ["{}"],
"invalid-signed-json"),
interestGroupBuyers: [$1],
auctionNonce: $3
})";
EXPECT_EQ(base::Value(),
RunAuctionAndWait(JsReplace(kAuctionConfigTemplate, test_origin,
decision_url, auction_nonce)));
EXPECT_TRUE(console_observer.Wait());
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionInvalidSignedAdditionalBidsStructure) {
GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin test_origin = url::Origin::Create(test_url);
GURL decision_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
std::string auction_nonce = CreateAuctionNonceAndWait();
WebContentsConsoleObserver console_observer(shell()->web_contents());
console_observer.SetPattern(
"*Worklet error: Unable to decode signed additional bid: Signed "
"additional bid missing string 'bid' field.*");
const char kAuctionConfigTemplate[] = R"({
seller: $1,
decisionLogicURL: $2,
additionalBids: provideAdditionalBids($1, $3, ["{}"],
"invalid-signed-struct"),
interestGroupBuyers: [$1],
auctionNonce: $3
})";
EXPECT_EQ(base::Value(),
RunAuctionAndWait(JsReplace(kAuctionConfigTemplate, test_origin,
decision_url, auction_nonce)));
EXPECT_TRUE(console_observer.Wait());
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, Clickiness_CaptureView) {
constexpr char kRecordViewClickPath[] =
"/interest_group/record_view_click_event.html";
GURL test_url_a = embedded_https_test_server().GetURL(
"a.test", "/attribution_reporting/page_with_impression_creator.html");
url::Origin test_origin_a = url::Origin::Create(test_url_a);
ASSERT_TRUE(test_url_a.SchemeIs(url::kHttpsScheme));
ASSERT_TRUE(NavigateToURL(shell(), test_url_a));
const std::string record_event_response = base::StringPrintf(
"type=\"view\", eligible-origins=(\"%s\")", test_origin_a.Serialize());
network_responder_->RegisterNetworkResponse(
kRecordViewClickPath, "Throwaway response", "image/jpeg",
/*extra_response_headers=*/
{{"Ad-Auction-Record-Event", record_event_response}});
GURL record_event_url =
embedded_https_test_server().GetURL("c.test", kRecordViewClickPath);
EXPECT_TRUE(ExecJs(web_contents(), JsReplace("createAttributionSrcImg($1);",
record_event_url)));
// This join should succeed. Register a no-op update URL to use
// WaitForInterestGroupsSatisfyingInvalidatingCacheByUpdating().
RegisterNoOpUpdate();
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(test_origin_a, "cars")
.SetViewAndClickCountsProviders(
{{url::Origin::Create(record_event_url)}})
.SetUpdateUrl(embedded_https_test_server().GetURL(
"a.test", kNoOpUpdatePath))
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", kValidateViewClickBiddingLogicPath))
.SetAds({{{GURL(kAdURL), /*metadata=*/std::nullopt}}})
.Build()));
WaitForInterestGroupsSatisfyingInvalidatingCacheByUpdating(
test_origin_a,
base::BindLambdaForTesting(
[](scoped_refptr<StorageInterestGroups> groups) {
EXPECT_EQ(groups->size(), 1u);
const StorageInterestGroup& group = *groups->GetInterestGroups()[0];
const blink::mojom::ViewAndClickCountsPtr& view_and_click_counts =
group.bidding_browser_signals->view_and_click_counts;
EXPECT_EQ(group.interest_group.name, "cars");
return view_and_click_counts->view_counts->past_hour == 1 &&
view_and_click_counts->click_counts->past_hour == 0;
}));
ValidateViewClickCountsInGenerateBid(
/*expected_view_counts=*/{.past_hour = 1,
.past_day = 1,
.past_week = 1,
.past_30_days = 1,
.past_90_days = 1},
/*expected_click_counts=*/{.past_hour = 0,
.past_day = 0,
.past_week = 0,
.past_30_days = 0,
.past_90_days = 0});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, Clickiness_CaptureClick) {
constexpr char kRecordViewClickPath[] =
"/interest_group/record_view_click_event.html";
GURL test_url_a = embedded_https_test_server().GetURL(
"a.test", "/attribution_reporting/page_with_impression_creator.html");
url::Origin test_origin_a = url::Origin::Create(test_url_a);
ASSERT_TRUE(test_url_a.SchemeIs(url::kHttpsScheme));
ASSERT_TRUE(NavigateToURL(shell(), test_url_a));
const std::string record_event_response = base::StringPrintf(
"type=\"click\", eligible-origins=(\"%s\")", test_origin_a.Serialize());
network_responder_->RegisterNetworkResponse(
kRecordViewClickPath, "Throwaway response", "image/jpeg",
/*extra_response_headers=*/
{{"Ad-Auction-Record-Event", record_event_response}});
GURL record_event_url =
embedded_https_test_server().GetURL("c.test", kRecordViewClickPath);
EXPECT_TRUE(
ExecJs(web_contents(),
JsReplace(R"(
createAttributionSrcAnchor({id: 'link',
url: $1,
attributionsrc: $2,
target: $3});)",
embedded_https_test_server().GetURL("a.test", "/echo"),
record_event_url, "_top")));
TestNavigationObserver observer(web_contents());
EXPECT_TRUE(ExecJs(web_contents(), "simulateClick('link');"));
observer.Wait();
// This join should succeed. Register a no-op update URL to use
// WaitForInterestGroupsSatisfyingInvalidatingCacheByUpdating().
RegisterNoOpUpdate();
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(test_origin_a, "cars")
.SetViewAndClickCountsProviders(
{{url::Origin::Create(record_event_url)}})
.SetUpdateUrl(embedded_https_test_server().GetURL(
"a.test", kNoOpUpdatePath))
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", kValidateViewClickBiddingLogicPath))
.SetAds({{{GURL(kAdURL), /*metadata=*/std::nullopt}}})
.Build()));
WaitForInterestGroupsSatisfyingInvalidatingCacheByUpdating(
test_origin_a,
base::BindLambdaForTesting(
[](scoped_refptr<StorageInterestGroups> groups) {
EXPECT_EQ(groups->size(), 1u);
const StorageInterestGroup& group = *groups->GetInterestGroups()[0];
const blink::mojom::ViewAndClickCountsPtr& view_and_click_counts =
group.bidding_browser_signals->view_and_click_counts;
EXPECT_EQ(group.interest_group.name, "cars");
return view_and_click_counts->view_counts->past_hour == 0 &&
view_and_click_counts->click_counts->past_hour == 1;
}));
ValidateViewClickCountsInGenerateBid(
/*expected_view_counts=*/{.past_hour = 0,
.past_day = 0,
.past_week = 0,
.past_30_days = 0,
.past_90_days = 0},
/*expected_click_counts=*/{.past_hour = 1,
.past_day = 1,
.past_week = 1,
.past_30_days = 1,
.past_90_days = 1});
}
// Test where permissions policy blocks click reporting. This test uses a
// page with permission policy that allows reports from c.test.
// It then triggers an anchor with an attribution source URL that follows a
// redirect chain that consists of:
//
// - An a.test URL that tries to report a click, which should get blocked by
// permissions policy.
//
// - A b.test URL that tries to report a click, which should get blocked by
// permissions policy.
//
// - A c.test URL that tries to report a click, which should get allowed by
// permissions policy.
//
// The test then waits for reporting info from the second request to appear in
// the Click Info Db, and finally checks that the reporting info from the first
// request does not appear in the database. Waiting for the second request's
// info to appear before checking for the first's should avoid any data races,
// where the first request's info may not have made it to the database yet,
// since the requests should use the same pipe to pass reporting info to the
// browser process.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
Clickiness_CaptureClickNoPermission) {
GURL test_url_a = embedded_https_test_server().GetURL(
"a.test",
"/attribution_reporting/"
"page_with_record_ad_auction_events_policy.html");
url::Origin test_origin_a = url::Origin::Create(test_url_a);
const std::string record_event_response = base::StringPrintf(
"type=\"click\", eligible-origins=(\"%s\")", test_origin_a.Serialize());
constexpr char kRecordViewClickPath[] =
"/interest_group/record_view_click_event.html";
network_responder_->RegisterNetworkResponse(
kRecordViewClickPath, "Throwaway response", "image/jpeg",
/*extra_response_headers=*/
{{"Ad-Auction-Record-Event", record_event_response}});
GURL record_event_url =
embedded_https_test_server().GetURL("c.test", kRecordViewClickPath);
constexpr char kRecordEventAndRedirectPath[] =
"/interest_group/record_event_and_redirect_url.html";
GURL record_event_and_redirect_url = embedded_https_test_server().GetURL(
"b.test", kRecordEventAndRedirectPath);
network_responder_->RegisterNetworkResponse(
kRecordEventAndRedirectPath, "Throwaway response", "image/jpeg",
/*extra_response_headers=*/
{{"Ad-Auction-Record-Event", record_event_response},
{"Location", record_event_url.spec()}},
/*code=*/net::HTTP_MOVED_PERMANENTLY);
constexpr char kRecordEventAndRedirect2xPath[] =
"/interest_group/record_event_and_redirect_2x_url.html";
GURL record_event_and_redirect_2x_url = embedded_https_test_server().GetURL(
"a.test", kRecordEventAndRedirect2xPath);
network_responder_->RegisterNetworkResponse(
kRecordEventAndRedirect2xPath, "Throwaway response", "image/jpeg",
/*extra_response_headers=*/
{{"Ad-Auction-Record-Event", record_event_response},
{"Location", record_event_and_redirect_url.spec()}},
/*code=*/net::HTTP_MOVED_PERMANENTLY);
ASSERT_TRUE(NavigateToURL(shell(), test_url_a));
const char kCreateAttributingLink[] = R"(
createAttributionSrcAnchor({
id: 'link',
url: $1,
attributionsrc: $2,
target: $3
});
)";
EXPECT_TRUE(ExecJs(
shell(), JsReplace(kCreateAttributingLink,
embedded_https_test_server().GetURL("a.test", "/echo"),
record_event_and_redirect_2x_url, "_top")));
TestNavigationObserver observer(web_contents());
EXPECT_TRUE(ExecJs(shell(), "simulateClick('link');"));
observer.Wait();
// Wait for the write for the destination to show up in the data.
while (CheckViewClickInfoInDb(
/*provider_origin=*/url::Origin::Create(record_event_url),
/*eligible_origin=*/test_origin_a) != true) {
}
// The previous hops should not have anything record, they're blocked by
// permissions policy.
EXPECT_EQ(false, CheckViewClickInfoInDb(
/*provider_origin=*/url::Origin::Create(
record_event_and_redirect_2x_url),
/*eligible_origin=*/test_origin_a));
EXPECT_EQ(false, CheckViewClickInfoInDb(
/*provider_origin=*/url::Origin::Create(
record_event_and_redirect_url),
/*eligible_origin=*/test_origin_a));
}
// Both the redirect and the redirect destination serve event record headers.
// Make sure both get recorded.
//
// Note that this is somewhat contrived, as in real-world usage only one of the
// redirect or the redirect destination would typically record the click;
// however, by recording clicks on both, this test effectively ensures that a
// click can be recorded from either.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
Clickiness_CaptureRedirectViewClick) {
constexpr char kRedirectAndRecordViewClickPath[] =
"/interest_group/redirect_and_record_view_click_event.html";
constexpr char kRecordViewClickPath[] =
"/interest_group/record_view_click_event.html";
GURL test_url_a = embedded_https_test_server().GetURL(
"a.test", "/attribution_reporting/page_with_impression_creator.html");
url::Origin test_origin_a = url::Origin::Create(test_url_a);
ASSERT_TRUE(test_url_a.SchemeIs(url::kHttpsScheme));
ASSERT_TRUE(NavigateToURL(shell(), test_url_a));
std::string record_event_response = base::StringPrintf(
"type=\"click\", eligible-origins=(\"%s\")", test_origin_a.Serialize());
GURL redirect_target =
embedded_https_test_server().GetURL("c.test", kRecordViewClickPath);
// The redirect records an event, and so does the resource it redirects to.
network_responder_->RegisterNetworkResponse(
kRedirectAndRecordViewClickPath, "Throwaway response", "image/jpeg",
/*extra_response_headers=*/
{{"Ad-Auction-Record-Event", record_event_response},
{"Location", redirect_target.spec()}},
/*code=*/net::HTTP_MOVED_PERMANENTLY);
network_responder_->RegisterNetworkResponse(
kRecordViewClickPath, "Throwaway response", "image/jpeg",
/*extra_response_headers=*/
{{"Ad-Auction-Record-Event", record_event_response}});
GURL record_event_url = embedded_https_test_server().GetURL(
"c.test", kRedirectAndRecordViewClickPath);
EXPECT_TRUE(
ExecJs(web_contents(),
JsReplace(R"(
createAttributionSrcAnchor({id: 'link',
url: $1,
attributionsrc: $2,
target: $3});)",
embedded_https_test_server().GetURL("a.test", "/echo"),
record_event_url, "_top")));
TestNavigationObserver observer(web_contents());
EXPECT_TRUE(ExecJs(web_contents(), "simulateClick('link');"));
observer.Wait();
// This join should succeed. Register a no-op update URL to use
// WaitForInterestGroupsSatisfyingInvalidatingCacheByUpdating().
RegisterNoOpUpdate();
EXPECT_EQ(kSuccess, JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(test_origin_a, "cars")
.SetViewAndClickCountsProviders(
{{url::Origin::Create(record_event_url)}})
.SetUpdateUrl(embedded_https_test_server().GetURL(
"a.test", kNoOpUpdatePath))
.Build()));
WaitForInterestGroupsSatisfyingInvalidatingCacheByUpdating(
test_origin_a,
base::BindLambdaForTesting(
[](scoped_refptr<StorageInterestGroups> groups) {
EXPECT_EQ(groups->size(), 1u);
const StorageInterestGroup& group = *groups->GetInterestGroups()[0];
const blink::mojom::ViewAndClickCountsPtr& view_and_click_counts =
group.bidding_browser_signals->view_and_click_counts;
EXPECT_EQ(group.interest_group.name, "cars");
return view_and_click_counts->view_counts->past_hour == 0 &&
view_and_click_counts->click_counts->past_hour == 2;
}));
// TODO(crbug.com/394108643): Also check generateBid() once the plumbing is
// hooked up.
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, Clickiness_NotStructuredDict) {
constexpr char kRecordViewClickPath[] =
"/interest_group/record_view_click_event.html";
GURL test_url_a = embedded_https_test_server().GetURL(
"a.test", "/attribution_reporting/page_with_impression_creator.html");
url::Origin test_origin_a = url::Origin::Create(test_url_a);
ASSERT_TRUE(test_url_a.SchemeIs(url::kHttpsScheme));
ASSERT_TRUE(NavigateToURL(shell(), test_url_a));
// Double equals isn't valid in structured headers -- this message should be
// rejected.
const std::string record_event_response = base::StringPrintf(
"type==\"view\", eligible-origins=(\"%s\")", test_origin_a.Serialize());
network_responder_->RegisterNetworkResponse(
kRecordViewClickPath, "Throwaway response", "image/jpeg",
/*extra_response_headers=*/
{{"Ad-Auction-Record-Event", record_event_response}});
GURL record_event_url =
embedded_https_test_server().GetURL("c.test", kRecordViewClickPath);
EXPECT_TRUE(ExecJs(web_contents(), JsReplace("createAttributionSrcImg($1);",
record_event_url)));
// This join should succeed.
base::RunLoop().RunUntilIdle();
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(test_origin_a, "cars")
.SetViewAndClickCountsProviders(
{{url::Origin::Create(record_event_url)}})
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", kValidateViewClickBiddingLogicPath))
.SetAds({{{GURL(kAdURL), /*metadata=*/std::nullopt}}})
.Build()));
// No change to view or click counts. Note that this is somewhat racy, as if
// the product code erroneously records a view or a click, it could happen
// after the join -- in that case, failures may be flaky. However, correct
// product code shouldn't result in flakes.
scoped_refptr<StorageInterestGroups> groups =
GetInterestGroupsForOwner(test_origin_a);
ASSERT_EQ(groups->size(), 1u);
const StorageInterestGroup& group = *groups->GetInterestGroups()[0];
const blink::mojom::ViewAndClickCountsPtr& view_and_click_counts =
group.bidding_browser_signals->view_and_click_counts;
EXPECT_EQ(group.interest_group.name, "cars");
EXPECT_EQ(view_and_click_counts->view_counts->past_hour, 0);
EXPECT_EQ(view_and_click_counts->click_counts->past_hour, 0);
ValidateViewClickCountsInGenerateBid(
/*expected_view_counts=*/{.past_hour = 0,
.past_day = 0,
.past_week = 0,
.past_30_days = 0,
.past_90_days = 0},
/*expected_click_counts=*/{.past_hour = 0,
.past_day = 0,
.past_week = 0,
.past_30_days = 0,
.past_90_days = 0});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, Clickiness_InvalidType) {
constexpr char kRecordViewClickPath[] =
"/interest_group/record_view_click_event.html";
GURL test_url_a = embedded_https_test_server().GetURL(
"a.test", "/attribution_reporting/page_with_impression_creator.html");
url::Origin test_origin_a = url::Origin::Create(test_url_a);
ASSERT_TRUE(test_url_a.SchemeIs(url::kHttpsScheme));
ASSERT_TRUE(NavigateToURL(shell(), test_url_a));
// A valid structured dictionary, but not-view-or-click isn't a valid type --
// this message should be rejected.
const std::string record_event_response = base::StringPrintf(
"type=\"not-view-or-click\", eligible-origins=(\"%s\")",
test_origin_a.Serialize());
network_responder_->RegisterNetworkResponse(
kRecordViewClickPath, "Throwaway response", "image/jpeg",
/*extra_response_headers=*/
{{"Ad-Auction-Record-Event", record_event_response}});
GURL record_event_url =
embedded_https_test_server().GetURL("c.test", kRecordViewClickPath);
EXPECT_TRUE(ExecJs(web_contents(), JsReplace("createAttributionSrcImg($1);",
record_event_url)));
// This join should succeed.
base::RunLoop().RunUntilIdle();
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(test_origin_a, "cars")
.SetViewAndClickCountsProviders(
{{url::Origin::Create(record_event_url)}})
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", kValidateViewClickBiddingLogicPath))
.SetAds({{{GURL(kAdURL), /*metadata=*/std::nullopt}}})
.Build()));
// No change to view or click counts. Note that this is somewhat racy, as if
// the product code erroneously records a view or a click, it could happen
// after the join -- in that case, failures may be flaky. However, correct
// product code shouldn't result in flakes.
scoped_refptr<StorageInterestGroups> groups =
GetInterestGroupsForOwner(test_origin_a);
ASSERT_EQ(groups->size(), 1u);
const StorageInterestGroup& group = *groups->GetInterestGroups()[0];
const blink::mojom::ViewAndClickCountsPtr& view_and_click_counts =
group.bidding_browser_signals->view_and_click_counts;
EXPECT_EQ(group.interest_group.name, "cars");
EXPECT_EQ(view_and_click_counts->view_counts->past_hour, 0);
EXPECT_EQ(view_and_click_counts->click_counts->past_hour, 0);
ValidateViewClickCountsInGenerateBid(
/*expected_view_counts=*/{.past_hour = 0,
.past_day = 0,
.past_week = 0,
.past_30_days = 0,
.past_90_days = 0},
/*expected_click_counts=*/{.past_hour = 0,
.past_day = 0,
.past_week = 0,
.past_30_days = 0,
.past_90_days = 0});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
Clickiness_EligibleOriginNotAttested) {
// Only allow b.test and c.test for the Protected Audience API. One of the
// eligible origins, a.test is not attested. Click should only be recorded
// for b.test origin.
content_browser_client_->SetAllowList(
{url::Origin::Create(embedded_https_test_server().GetURL("b.test", "/")),
url::Origin::Create(
embedded_https_test_server().GetURL("c.test", "/"))});
constexpr char kRecordViewClickPath[] =
"/interest_group/record_view_click_event.html";
url::Origin test_origin_a = embedded_https_test_server().GetOrigin("a.test");
url::Origin test_origin_b = embedded_https_test_server().GetOrigin("b.test");
GURL test_url_c = embedded_https_test_server().GetURL(
"c.test", "/attribution_reporting/page_with_impression_creator.html");
ASSERT_TRUE(test_url_c.SchemeIs(url::kHttpsScheme));
ASSERT_TRUE(NavigateToURL(shell(), test_url_c));
const std::string record_event_response =
base::StringPrintf("type=\"click\", eligible-origins=(\"%s\" \"%s\")",
test_origin_a.Serialize(), test_origin_b.Serialize());
network_responder_->RegisterNetworkResponse(
kRecordViewClickPath, "Throwaway response", "image/jpeg",
/*extra_response_headers=*/
{{"Ad-Auction-Record-Event", record_event_response}});
GURL record_event_url =
embedded_https_test_server().GetURL("c.test", kRecordViewClickPath);
EXPECT_TRUE(
ExecJs(web_contents(),
JsReplace(R"(
createAttributionSrcAnchor({id: 'link',
url: $1,
attributionsrc: $2,
target: $3});)",
embedded_https_test_server().GetURL("a.test", "/echo"),
record_event_url, "_top")));
TestNavigationObserver observer(web_contents());
EXPECT_TRUE(ExecJs(web_contents(), "simulateClick('link');"));
observer.Wait();
base::RunLoop().RunUntilIdle();
// Directly check that view / click didn't get recorder, instead of getting
// them via a join. This avoids the awkwardness of needing to create an
// interest group for an origin that's not attested, or the raciness of trying
// to change attestation.
//
// This test shouldn't flake -- but if the product code isn't correctly
// blocking non-attested origins, there may be a chance that the below code
// reads from the database before the attempt to record the event completes.
EXPECT_EQ(false,
CheckViewClickInfoInDb(
/*provider_origin=*/url::Origin::Create(record_event_url),
/*eligible_origin=*/test_origin_a));
EXPECT_EQ(true, CheckViewClickInfoInDb(
/*provider_origin=*/url::Origin::Create(record_event_url),
/*eligible_origin=*/test_origin_b));
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
Clickiness_CrossSiteTopLevelNav) {
// Make sure we use the right top-level origin when checking permissions.
constexpr char kRecordViewClickPath[] =
"/interest_group/record_view_click_event.html";
url::Origin test_origin_a = embedded_https_test_server().GetOrigin("a.test");
GURL test_url_c = embedded_https_test_server().GetURL(
"c.test", "/attribution_reporting/page_with_impression_creator.html");
ASSERT_TRUE(test_url_c.SchemeIs(url::kHttpsScheme));
ASSERT_TRUE(NavigateToURL(shell(), test_url_c));
const std::string record_event_response = base::StringPrintf(
"type=\"click\", eligible-origins=(\"%s\")", test_origin_a.Serialize());
network_responder_->RegisterNetworkResponse(
kRecordViewClickPath, "Throwaway response", "image/jpeg",
/*extra_response_headers=*/
{{"Ad-Auction-Record-Event", record_event_response}});
GURL record_event_url =
embedded_https_test_server().GetURL("a.test", kRecordViewClickPath);
EXPECT_TRUE(ExecJs(web_contents(), JsReplace(R"(
createAttributionSrcAnchor({id: 'link',
url: $1,
attributionsrc: '',
target: $2});)",
record_event_url, "_top")));
TestNavigationObserver observer(web_contents());
EXPECT_TRUE(ExecJs(web_contents(), "simulateClick('link');"));
observer.Wait();
while (CheckViewClickInfoInDb(/*provider_origin=*/test_origin_a,
/*eligible_origin=*/test_origin_a) != true) {
base::RunLoop().RunUntilIdle();
}
// The top-level origin we expect for checks is the destination, as that's
// what other such checks use.
EXPECT_THAT(content_browser_client_->checked_top_frame_origins(),
testing::ElementsAre(test_origin_a));
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
Clickiness_ProviderNotAttested) {
// Only allow a.test for the Protected Audience API. The provider origin,
// c.test is not attested. No click should be recorded, even though the
// eligible origin and top level frame origin are both a.test.
content_browser_client_->SetAllowList({
url::Origin::Create(embedded_https_test_server().GetURL("a.test", "/")),
});
constexpr char kRecordViewClickPath[] =
"/interest_group/record_view_click_event.html";
GURL test_url_a = embedded_https_test_server().GetURL(
"a.test", "/attribution_reporting/page_with_impression_creator.html");
url::Origin test_origin_a = url::Origin::Create(test_url_a);
ASSERT_TRUE(test_url_a.SchemeIs(url::kHttpsScheme));
ASSERT_TRUE(NavigateToURL(shell(), test_url_a));
const std::string record_event_response = base::StringPrintf(
"type=\"click\", eligible-origins=(\"%s\")", test_origin_a.Serialize());
network_responder_->RegisterNetworkResponse(
kRecordViewClickPath, "Throwaway response", "image/jpeg",
/*extra_response_headers=*/
{{"Ad-Auction-Record-Event", record_event_response}});
GURL record_event_url =
embedded_https_test_server().GetURL("c.test", kRecordViewClickPath);
EXPECT_TRUE(
ExecJs(web_contents(),
JsReplace(R"(
createAttributionSrcAnchor({id: 'link',
url: $1,
attributionsrc: $2,
target: $3});)",
embedded_https_test_server().GetURL("a.test", "/echo"),
record_event_url, "_top")));
TestNavigationObserver observer(web_contents());
EXPECT_TRUE(ExecJs(web_contents(), "simulateClick('link');"));
observer.Wait();
// This join should succeed.
base::RunLoop().RunUntilIdle();
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(test_origin_a, "cars")
.SetViewAndClickCountsProviders(
{{url::Origin::Create(record_event_url)}})
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", kValidateViewClickBiddingLogicPath))
.SetAds({{{GURL(kAdURL), /*metadata=*/std::nullopt}}})
.Build()));
// No change to view or click counts. Note that this is somewhat racy, as if
// the product code erroneously records a view or a click, it could happen
// after the join -- in that case, failures may be flaky. However, correct
// product code shouldn't result in flakes.
scoped_refptr<StorageInterestGroups> groups =
GetInterestGroupsForOwner(test_origin_a);
ASSERT_EQ(groups->size(), 1u);
const StorageInterestGroup& group = *groups->GetInterestGroups()[0];
const blink::mojom::ViewAndClickCountsPtr& view_and_click_counts =
group.bidding_browser_signals->view_and_click_counts;
EXPECT_EQ(group.interest_group.name, "cars");
EXPECT_EQ(view_and_click_counts->view_counts->past_hour, 0);
EXPECT_EQ(view_and_click_counts->click_counts->past_hour, 0);
ValidateViewClickCountsInGenerateBid(
/*expected_view_counts=*/{.past_hour = 0,
.past_day = 0,
.past_week = 0,
.past_30_days = 0,
.past_90_days = 0},
/*expected_click_counts=*/{.past_hour = 0,
.past_day = 0,
.past_week = 0,
.past_30_days = 0,
.past_90_days = 0});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
Clickiness_TopLevelFrameNotAttested) {
constexpr char kRecordViewClickPath[] =
"/interest_group/record_view_click_event.html";
// Use a domain name that's not attested.
GURL test_url_not_attested = embedded_https_test_server().GetURL(
"d.test", "/attribution_reporting/page_with_impression_creator.html");
ASSERT_TRUE(test_url_not_attested.SchemeIs(url::kHttpsScheme));
ASSERT_TRUE(NavigateToURL(shell(), test_url_not_attested));
url::Origin test_origin_a = embedded_https_test_server().GetOrigin("a.test");
const std::string record_event_response = base::StringPrintf(
"type=\"view\", eligible-origins=(\"%s\")", test_origin_a.Serialize());
network_responder_->RegisterNetworkResponse(
kRecordViewClickPath, "Throwaway response", "image/jpeg",
/*extra_response_headers=*/
{{"Ad-Auction-Record-Event", record_event_response}});
GURL record_event_url =
embedded_https_test_server().GetURL("c.test", kRecordViewClickPath);
EXPECT_TRUE(ExecJs(web_contents(), JsReplace("createAttributionSrcImg($1);",
record_event_url)));
// This join should succeed.
base::RunLoop().RunUntilIdle();
ASSERT_TRUE(NavigateToURL(
shell(), embedded_https_test_server().GetURL("a.test", "/echo")));
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(test_origin_a, "cars")
.SetViewAndClickCountsProviders(
{{url::Origin::Create(record_event_url)}})
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", kValidateViewClickBiddingLogicPath))
.SetAds({{{GURL(kAdURL), /*metadata=*/std::nullopt}}})
.Build()));
// No change to view or click counts. Note that this is somewhat racy, as if
// the product code erroneously records a view or a click, it could happen
// after the join -- in that case, failures may be flaky. However, correct
// product code shouldn't result in flakes.
scoped_refptr<StorageInterestGroups> groups =
GetInterestGroupsForOwner(test_origin_a);
ASSERT_EQ(groups->size(), 1u);
const StorageInterestGroup& group = *groups->GetInterestGroups()[0];
const blink::mojom::ViewAndClickCountsPtr& view_and_click_counts =
group.bidding_browser_signals->view_and_click_counts;
EXPECT_EQ(group.interest_group.name, "cars");
EXPECT_EQ(view_and_click_counts->view_counts->past_hour, 0);
EXPECT_EQ(view_and_click_counts->click_counts->past_hour, 0);
ValidateViewClickCountsInGenerateBid(
/*expected_view_counts=*/{.past_hour = 0,
.past_day = 0,
.past_week = 0,
.past_30_days = 0,
.past_90_days = 0},
/*expected_click_counts=*/{.past_hour = 0,
.past_day = 0,
.past_week = 0,
.past_30_days = 0,
.past_90_days = 0});
}
class InterestGroupDisableClickinessBrowserTest
: public InterestGroupBrowserTest {
public:
InterestGroupDisableClickinessBrowserTest() {
feature_list_.InitWithFeatures(
{},
/*disabled_features=*/{network::features::kAdAuctionEventRegistration,
blink::features::kFledgeClickiness});
}
base::test::ScopedFeatureList feature_list_;
};
IN_PROC_BROWSER_TEST_F(InterestGroupDisableClickinessBrowserTest,
Clickiness_ViewClickNotInGenerateBid) {
constexpr char kValidateNoViewClickBiddingLogicPath[] =
"/interest_group/bidding_no_view_click_validator.js";
GURL test_url_a = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin test_origin_a = url::Origin::Create(test_url_a);
ASSERT_TRUE(test_url_a.SchemeIs(url::kHttpsScheme));
ASSERT_TRUE(NavigateToURL(shell(), test_url_a));
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(test_origin_a, "cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", kValidateNoViewClickBiddingLogicPath))
.SetAds({{{GURL(kAdURL), /*metadata=*/std::nullopt}}})
.Build()));
constexpr char kBiddingLogicScript[] = R"(
function generateBid(
interestGroup, unusedAuctionSignals, unusedPerBuyerSignals,
unusedTrustedBiddingSignals, browserSignals) {
if ('viewCounts' in browserSignals) {
throw 'viewCounts unexpectedly in browserSignals';
} else if ('clickCounts' in browserSignals) {
throw 'clickCounts unexpectedly in browserSignals';
}
const ad = interestGroup.ads[0];
return {'ad': ad, 'bid': 1, 'render': ad.renderURL};
})";
network_responder_->RegisterNetworkResponse(
kValidateNoViewClickBiddingLogicPath, kBiddingLogicScript,
"application/javascript");
// For simplicity, both the buyer and seller are the current top-frame
// origin.
const url::Origin buyer_and_seller_origin = url::Origin::Create(test_url_a);
EXPECT_EQ(GURL(kAdURL), RunAuctionAndWaitForUrl(JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1]
})",
buyer_and_seller_origin,
embedded_https_test_server().GetURL(
buyer_and_seller_origin.host(),
"/interest_group/decision_logic.js"))));
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionBuyersNoInterestGroup) {
GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
AttachInterestGroupObserver();
EXPECT_EQ(base::Value(),
RunAuctionAndWait(JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
})",
url::Origin::Create(test_url),
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js"))));
WaitForAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionAuctionReportBuyerKeysNotBigInt) {
GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
AttachInterestGroupObserver();
EXPECT_EQ(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': Failed to "
"read the 'auctionReportBuyerKeys' property from 'AuctionAdConfig': "
"Cannot convert 3 to a BigInt",
RunAuctionAndWait(JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
auctionReportBuyerKeys: [3],
})",
url::Origin::Create(test_url),
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js"))));
WaitForAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionAuctionReportBuyerKeysTooLargeBigInt) {
GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
AttachInterestGroupObserver();
EXPECT_EQ(base::StringPrintf(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': "
"auctionReportBuyerKeys for AuctionAdConfig with seller '%s': "
"Too large BigInt; Must fit in 128 bits",
url::Origin::Create(test_url).Serialize().c_str()),
RunAuctionAndWait(JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
auctionReportBuyerKeys: [1n << 129n],
})",
url::Origin::Create(test_url),
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js"))));
WaitForAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionAuctionReportBuyerKeysNegativeBigInt) {
GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
AttachInterestGroupObserver();
EXPECT_EQ(base::StringPrintf(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': "
"auctionReportBuyerKeys for AuctionAdConfig with seller '%s': "
"Negative BigInt cannot be converted to uint128",
url::Origin::Create(test_url).Serialize().c_str()),
RunAuctionAndWait(JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
auctionReportBuyerKeys: [-1n],
})",
url::Origin::Create(test_url),
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js"))));
WaitForAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(
InterestGroupBrowserTest,
RunAdAuctionAuctionReportBuyersUnknownReportTypeIgnored) {
GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
AttachInterestGroupObserver();
EXPECT_EQ(base::Value(),
RunAuctionAndWait(JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
auctionReportBuyerKeys: [1n],
auctionReportBuyers: {
unknownReportType: { bucket: 0n, scale: 1 },
}
})",
url::Origin::Create(test_url),
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js"))));
WaitForAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionAuctionReportBuyersIncompleteDictionary) {
GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
EXPECT_EQ(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': Failed to "
"read the 'auctionReportBuyers' property from 'AuctionAdConfig': Failed "
"to read the 'scale' property from 'AuctionReportBuyersConfig': Required "
"member is undefined.",
RunAuctionAndWait(JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
auctionReportBuyerKeys: [1n],
auctionReportBuyers: {
bidCount: { bucket: 0n },
}
})",
url::Origin::Create(test_url),
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js"))));
EXPECT_EQ(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': Failed to "
"read the 'auctionReportBuyers' property from 'AuctionAdConfig': Failed "
"to read the 'bucket' property from 'AuctionReportBuyersConfig': "
"Required member is undefined.",
RunAuctionAndWait(JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
auctionReportBuyerKeys: [1n],
auctionReportBuyers: {
bidCount: { scale: 1 },
}
})",
url::Origin::Create(test_url),
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js"))));
}
IN_PROC_BROWSER_TEST_F(
InterestGroupBrowserTest,
RunAdAuctionAuctionInvalidRequiredSellerCapabilitiesIgnored) {
GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
AttachInterestGroupObserver();
EXPECT_EQ(base::Value(),
RunAuctionAndWait(JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
requiredSellerCapabilities: ['non-valid-capability'],
})",
url::Origin::Create(test_url),
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js"))));
WaitForAccessObserved({});
}
class InterestGroupAuctionReportBuyersEnableDebugModeTest
: public InterestGroupBrowserTest {
public:
void SetUpOnMainThread() override {
InterestGroupBrowserTest::SetUpOnMainThread();
class TestPrivateAggregationManagerImpl
: public PrivateAggregationManagerImpl {
public:
TestPrivateAggregationManagerImpl(
std::unique_ptr<PrivateAggregationBudgeter> budgeter,
std::unique_ptr<PrivateAggregationHost> host)
: PrivateAggregationManagerImpl(std::move(budgeter),
std::move(host),
/*storage_partition=*/nullptr) {}
};
auto* storage_partition_impl =
static_cast<StoragePartitionImpl*>(shell()
->web_contents()
->GetBrowserContext()
->GetDefaultStoragePartition());
storage_partition_impl->OverridePrivateAggregationManagerForTesting(
std::make_unique<TestPrivateAggregationManagerImpl>(
std::make_unique<MockPrivateAggregationBudgeter>(),
std::make_unique<PrivateAggregationHost>(
/*on_report_request_received=*/mock_private_aggregation_cb_
.Get(),
/*browser_context=*/storage_partition_impl
->browser_context())));
}
void TearDownOnMainThread() override {
if (run_loop_) {
run_loop_->Run();
}
InterestGroupBrowserTest::TearDownOnMainThread();
}
void SetUpTestWithOneInterestGroup() {
test_url_ =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url_));
test_origin_ = url::Origin::Create(test_url_);
ad_url_ = embedded_https_test_server().GetURL(
"c.test", "/set-header?Supports-Loading-Mode: fenced-frame");
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin_,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"))
.SetTrustedBiddingSignalsUrl(
embedded_https_test_server().GetURL(
"a.test",
"/interest_group/trusted_bidding_signals.json"))
.SetTrustedBiddingSignalsKeys({{"key1"}})
.SetAds({{{ad_url_, R"({"ad":"metadata","here":[1,2]})"}}})
.SetAllSellersCapabilities(
{blink::SellerCapabilities::kInterestGroupCounts,
blink::SellerCapabilities::kLatencyStats})
.Build()));
}
void ExpectPrivateAggregationCall(
int bucket,
int value,
AggregatableReportSharedInfo::DebugMode debug_mode,
std::optional<uint64_t> debug_key = std::nullopt) {
ASSERT_FALSE(run_loop_);
run_loop_ = std::make_unique<base::RunLoop>();
// We only need to test that a request was made in PrivateAggregationHost,
// so we mock out the callback and check that it was called. The callback
// will only run *after* the ad auction finishes and the winner is rendered,
// but we register it beforehand so that it is guaranteed to detect when the
// private aggregation event is sent.
EXPECT_CALL(mock_private_aggregation_cb_, Run)
.WillRepeatedly(
[=, this](
PrivateAggregationHost::ReportRequestGenerator generator,
PrivateAggregationPendingContributions::Wrapper contributions,
PrivateAggregationBudgetKey budget_key,
PrivateAggregationHost::NullReportBehavior
null_report_behavior) {
AggregatableReportRequest request = GenerateReportRequest(
std::move(generator), std::move(contributions));
ASSERT_EQ(request.payload_contents().contributions.size(), 1u);
EXPECT_EQ(request.payload_contents().contributions[0].bucket,
bucket);
EXPECT_EQ(request.payload_contents().contributions[0].value,
value);
EXPECT_EQ(request.shared_info().reporting_origin, test_origin_);
EXPECT_EQ(request.shared_info().debug_mode, debug_mode);
EXPECT_EQ(request.debug_key(), debug_key);
EXPECT_EQ(budget_key.caller_api(),
PrivateAggregationCallerApi::kProtectedAudience);
EXPECT_EQ(budget_key.origin(), test_origin_);
EXPECT_EQ(
null_report_behavior,
PrivateAggregationHost::NullReportBehavior::kDontSendReport);
run_loop_->Quit();
});
}
void ExpectNoPrivateAggregationCall() {
EXPECT_CALL(mock_private_aggregation_cb_, Run).Times(0);
}
protected:
GURL test_url_;
url::Origin test_origin_;
GURL ad_url_;
base::test::ScopedFeatureList scoped_feature_list_;
base::MockRepeatingCallback<void(
PrivateAggregationHost::ReportRequestGenerator,
PrivateAggregationPendingContributions::Wrapper,
PrivateAggregationBudgetKey,
PrivateAggregationHost::NullReportBehavior)>
mock_private_aggregation_cb_;
private:
std::unique_ptr<base::RunLoop> run_loop_;
};
IN_PROC_BROWSER_TEST_F(InterestGroupAuctionReportBuyersEnableDebugModeTest,
NegativeDebugKey_Error) {
SetUpTestWithOneInterestGroup();
ExpectNoPrivateAggregationCall();
EXPECT_EQ(base::StringPrintf(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': "
"auctionReportBuyerDebugModeConfig for AuctionAdConfig with "
"seller '%s': Negative BigInt cannot be converted to uint64",
test_origin_.Serialize().c_str()),
RunAuctionAndWait(JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
auctionReportBuyerKeys: [1n],
auctionReportBuyers: {
bidCount: { bucket: 0n, scale: 1 },
},
auctionReportBuyerDebugModeConfig: { enabled: true, debugKey: -1n }
})",
test_origin_,
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js"))));
}
IN_PROC_BROWSER_TEST_F(InterestGroupAuctionReportBuyersEnableDebugModeTest,
TooLargeBigInt_Error) {
SetUpTestWithOneInterestGroup();
ExpectNoPrivateAggregationCall();
EXPECT_EQ(base::StringPrintf(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': "
"auctionReportBuyerDebugModeConfig for AuctionAdConfig with "
"seller '%s': Too large BigInt; Must fit in 64 bits",
test_origin_.Serialize().c_str()),
RunAuctionAndWait(JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
auctionReportBuyerKeys: [1n],
auctionReportBuyers: {
bidCount: { bucket: 0n, scale: 1 },
},
auctionReportBuyerDebugModeConfig: { enabled: true, debugKey: 1n << 64n }
})",
test_origin_,
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js"))));
}
IN_PROC_BROWSER_TEST_F(InterestGroupAuctionReportBuyersEnableDebugModeTest,
DebugKeySetWithEnabledFalse_Error) {
SetUpTestWithOneInterestGroup();
ExpectNoPrivateAggregationCall();
EXPECT_EQ(
base::StringPrintf(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': "
"auctionReportBuyerDebugModeConfig for AuctionAdConfig with seller "
"'%s': debugKey can only be specified when debug mode is enabled.",
test_origin_.Serialize().c_str()),
RunAuctionAndWait(JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
auctionReportBuyerKeys: [1n],
auctionReportBuyers: {
bidCount: { bucket: 0n, scale: 1 },
},
auctionReportBuyerDebugModeConfig: { enabled: false, debugKey: 1234n }
})",
test_origin_,
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js"))));
}
IN_PROC_BROWSER_TEST_F(InterestGroupAuctionReportBuyersEnableDebugModeTest,
DebugModeConfigNotADictionary_Error) {
SetUpTestWithOneInterestGroup();
ExpectNoPrivateAggregationCall();
EXPECT_EQ(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': Failed to "
"read the 'auctionReportBuyerDebugModeConfig' property from "
"'AuctionAdConfig': The provided value is not of type "
"'AuctionReportBuyerDebugModeConfig'.",
RunAuctionAndWait(JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
auctionReportBuyerKeys: [1n],
auctionReportBuyers: {
bidCount: { bucket: 0n, scale: 1 },
},
auctionReportBuyerDebugModeConfig: 1
})",
test_origin_,
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js"))));
ExpectNoPrivateAggregationCall();
}
IN_PROC_BROWSER_TEST_F(InterestGroupAuctionReportBuyersEnableDebugModeTest,
DebugModeWithoutDebugKey_Success) {
SetUpTestWithOneInterestGroup();
ExpectPrivateAggregationCall(
/*bucket=*/101, /*value=*/10,
AggregatableReportSharedInfo::DebugMode::kEnabled);
RunAuctionAndWaitForURLAndNavigateIframe(
JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
auctionReportBuyerKeys: [1n],
auctionReportBuyers: {
bidCount: { bucket: 100n, scale: 10 },
},
auctionReportBuyerDebugModeConfig: { enabled: true }
})",
test_origin_,
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js")),
/*expected_url=*/ad_url_);
}
IN_PROC_BROWSER_TEST_F(InterestGroupAuctionReportBuyersEnableDebugModeTest,
DebugModeWithDebugKey_Success) {
SetUpTestWithOneInterestGroup();
ExpectPrivateAggregationCall(
/*bucket=*/101, /*value=*/10,
AggregatableReportSharedInfo::DebugMode::kEnabled, /*debug_key=*/1234);
RunAuctionAndWaitForURLAndNavigateIframe(
JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
auctionReportBuyerKeys: [1n],
auctionReportBuyers: {
bidCount: { bucket: 100n, scale: 10 },
},
auctionReportBuyerDebugModeConfig: { enabled: true, debugKey: 1234n }
})",
test_origin_,
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js")),
/*expected_url=*/ad_url_);
}
IN_PROC_BROWSER_TEST_F(InterestGroupAuctionReportBuyersEnableDebugModeTest,
DebugModeDisabled_Success) {
SetUpTestWithOneInterestGroup();
ExpectPrivateAggregationCall(
/*bucket=*/101, /*value=*/10,
AggregatableReportSharedInfo::DebugMode::kDisabled);
RunAuctionAndWaitForURLAndNavigateIframe(
JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
auctionReportBuyerKeys: [1n],
auctionReportBuyers: {
bidCount: { bucket: 100n, scale: 10 },
},
auctionReportBuyerDebugModeConfig: { enabled: false }
})",
test_origin_,
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js")),
/*expected_url=*/ad_url_);
}
IN_PROC_BROWSER_TEST_F(InterestGroupAuctionReportBuyersEnableDebugModeTest,
DebugModeDisabledImplicity_Success) {
SetUpTestWithOneInterestGroup();
ExpectPrivateAggregationCall(
/*bucket=*/101, /*value=*/10,
AggregatableReportSharedInfo::DebugMode::kDisabled);
RunAuctionAndWaitForURLAndNavigateIframe(
JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
auctionReportBuyerKeys: [1n],
auctionReportBuyers: {
bidCount: { bucket: 100n, scale: 10 },
},
auctionReportBuyerDebugModeConfig: {}
})",
test_origin_,
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js")),
/*expected_url=*/ad_url_);
}
IN_PROC_BROWSER_TEST_F(InterestGroupAuctionReportBuyersEnableDebugModeTest,
DebugConfigMissing_Disabled) {
SetUpTestWithOneInterestGroup();
ExpectPrivateAggregationCall(
/*bucket=*/101, /*value=*/10,
AggregatableReportSharedInfo::DebugMode::kDisabled);
RunAuctionAndWaitForURLAndNavigateIframe(
JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
auctionReportBuyerKeys: [1n],
auctionReportBuyers: {
bidCount: { bucket: 100n, scale: 10 },
},
})",
test_origin_,
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js")),
/*expected_url=*/ad_url_);
}
IN_PROC_BROWSER_TEST_F(InterestGroupAuctionReportBuyersEnableDebugModeTest,
MultipleBuyersDebugConfig) {
SetUpTestWithOneInterestGroup();
// Navigate to a new advertiser page and join another interest group, which
// will win the auction.
GURL test_url_2 =
embedded_https_test_server().GetURL("b.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url_2));
url::Origin test_origin_2 = url::Origin::Create(test_url_2);
GURL ad_url_2 = embedded_https_test_server().GetURL(
"d.test", "/set-header?Supports-Loading-Mode: fenced-frame");
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin_2,
/*name=*/"winner")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"b.test",
"/interest_group/bidding_logic_with_debugging_report.js"))
.SetTrustedBiddingSignalsUrl(embedded_https_test_server().GetURL(
"b.test", "/interest_group/trusted_bidding_signals.json"))
.SetTrustedBiddingSignalsKeys({{"key1"}})
.SetAds({{{ad_url_2, /*metadata=*/std::nullopt}}})
.SetAllSellersCapabilities(
{blink::SellerCapabilities::kInterestGroupCounts,
blink::SellerCapabilities::kLatencyStats})
.Build()));
base::RunLoop run_loop;
std::optional<AggregatableReportRequest> request_returned;
EXPECT_CALL(mock_private_aggregation_cb_, Run)
.WillOnce(
[&](PrivateAggregationHost::ReportRequestGenerator generator,
PrivateAggregationPendingContributions::Wrapper contributions,
PrivateAggregationBudgetKey budget_key,
PrivateAggregationHost::NullReportBehavior null_report_behavior) {
request_returned = GenerateReportRequest(std::move(generator),
std::move(contributions));
EXPECT_EQ(budget_key.caller_api(),
PrivateAggregationCallerApi::kProtectedAudience);
EXPECT_EQ(budget_key.origin(), test_origin_);
EXPECT_EQ(
null_report_behavior,
PrivateAggregationHost::NullReportBehavior::kDontSendReport);
run_loop.Quit();
});
RunAuctionAndWaitForURLAndNavigateIframe(
JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1, $3],
auctionReportBuyerKeys: [1n, 2n],
auctionReportBuyers: {
bidCount: { bucket: 100n, scale: 10 },
},
auctionReportBuyerDebugModeConfig: { enabled: true, debugKey: 1234n }
})",
test_origin_,
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js"),
test_origin_2),
ad_url_2);
run_loop.Run();
ASSERT_TRUE(request_returned.has_value());
EXPECT_THAT(
request_returned->payload_contents().contributions,
testing::UnorderedElementsAre(
blink::mojom::AggregatableReportHistogramContribution(
/*bucket=*/101, /*value=*/10, /*filtering_id=*/std::nullopt),
blink::mojom::AggregatableReportHistogramContribution(
/*bucket=*/102, /*value=*/10, /*filtering_id=*/std::nullopt)));
EXPECT_EQ(request_returned->shared_info().reporting_origin, test_origin_);
EXPECT_EQ(request_returned->shared_info().debug_mode,
AggregatableReportSharedInfo::DebugMode::kEnabled);
EXPECT_EQ(request_returned->debug_key(), 1234);
}
// Run an auction with 2 interest groups. One of the interest groups will
// satisfy the `requiredSellerCapabilities` of the auction config, and one will
// not.
//
// The bid of the interest group satisfied the `requiredSellerCapabilities`
// wins the auction, even though normally the other interest group would win,
// because the other interest group was removed from the auction for failing to
// satisfy the `requiredSellerCapabilities`.
//
// Both interest groups set an update URL, so after the auction, a post auction
// interest group update occurs and succeeds for both groups. (This is so that
// bidders can choose to make the groups that don't meet the
// requiredSellerCapabilities update to then satisfy those conditions).
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RequiredSellerCapabilitiesWithPostAuctionUpdates) {
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad1_url = embedded_https_test_server().GetURL(
"c.test", "/echo?stop_bidding_after_win");
GURL ad2_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_bikes");
constexpr char kUpdatePath[] = "/interest_group/update.json";
constexpr char kUpdateResponse[] = R"(
{
"sellerCapabilities": {
"*": ["interest-group-counts", "latency-stats"]
}
})";
network_responder_->RegisterNetworkResponse(kUpdatePath, kUpdateResponse);
GURL update_url = embedded_https_test_server().GetURL("a.test", kUpdatePath);
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test",
"/interest_group/bidding_logic_stop_bidding_after_win.js"))
.SetAds({{{ad1_url, /*metadata=*/std::nullopt}}})
.SetUpdateUrl(update_url)
.Build()));
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"bikes")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"))
.SetAds({{{ad2_url, /*metadata=*/std::nullopt}}})
.SetUpdateUrl(update_url)
.SetAllSellersCapabilities(
{blink::SellerCapabilities::kInterestGroupCounts})
.Build()));
// `ad2_url` wins, because "cars" is removed for not satisfying
// requiredSellerCapabilities.
RunAuctionAndWaitForURLAndNavigateIframe(
JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
requiredSellerCapabilities: ['interest-group-counts'],
})",
test_origin,
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js")),
ad2_url);
// A post-auction update occurs.
WaitForInterestGroupsSatisfying(
test_origin,
base::BindLambdaForTesting(
[](scoped_refptr<StorageInterestGroups> groups) {
if (groups->size() != 2) {
return false;
}
for (const SingleStorageInterestGroup& group :
groups->GetInterestGroups()) {
if (group->interest_group.all_sellers_capabilities !=
blink::SellerCapabilitiesType(
{blink::SellerCapabilities::kInterestGroupCounts,
blink::SellerCapabilities::kLatencyStats})) {
return false;
}
}
return true;
}));
// `ad1_url` now wins, because "cars" satisfies requiredSellerCapabilities.
RunAuctionAndWaitForURLAndNavigateIframe(
JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
requiredSellerCapabilities: ['interest-group-counts'],
})",
test_origin,
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js")),
ad1_url);
}
// Like RequiredSellerCapabilitiesWithPostAuctionUpdates, but setting the
// sellerCapabilities per-buyer instead of for all buyers.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RequiredSellerCapabilitiesWithPerBuyerCapabilities) {
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad1_url = embedded_https_test_server().GetURL(
"c.test", "/echo?stop_bidding_after_win");
GURL ad2_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_bikes");
constexpr char kUpdatePath[] = "/interest_group/update.json";
std::string update_response =
base::StringPrintf(R"(
{
"sellerCapabilities": {
"%s": ["interest-group-counts", "latency-stats"]
}
})",
test_origin.Serialize().c_str());
network_responder_->RegisterNetworkResponse(kUpdatePath, update_response);
GURL update_url = embedded_https_test_server().GetURL("a.test", kUpdatePath);
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test",
"/interest_group/bidding_logic_stop_bidding_after_win.js"))
.SetAds({{{ad1_url, /*metadata=*/std::nullopt}}})
.SetUpdateUrl(update_url)
.Build()));
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"bikes")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"))
.SetAds({{{ad2_url, /*metadata=*/std::nullopt}}})
.SetUpdateUrl(update_url)
.SetSellerCapabilities(
{{{test_origin,
{blink::SellerCapabilities::kInterestGroupCounts}}}})
.Build()));
// `ad2_url` wins, because "cars" is removed for not satisfying
// requiredSellerCapabilities.
RunAuctionAndWaitForURLAndNavigateIframe(
JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
requiredSellerCapabilities: ['interest-group-counts'],
})",
test_origin,
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js")),
ad2_url);
// A post-auction update occurs.
WaitForInterestGroupsSatisfying(
test_origin,
base::BindLambdaForTesting(
[&test_origin](scoped_refptr<StorageInterestGroups> groups) {
if (groups->size() != 2) {
return false;
}
for (const SingleStorageInterestGroup& group :
groups->GetInterestGroups()) {
const auto& seller_capabilities =
group->interest_group.seller_capabilities;
if (!seller_capabilities) {
return false;
}
auto it = seller_capabilities->find(test_origin);
if (it == seller_capabilities->end()) {
return false;
}
if (it->second !=
blink::SellerCapabilitiesType(
{blink::SellerCapabilities::kInterestGroupCounts,
blink::SellerCapabilities::kLatencyStats})) {
return false;
}
}
return true;
}));
// `ad1_url` now wins, because "cars" satisfies requiredSellerCapabilities.
RunAuctionAndWaitForURLAndNavigateIframe(
JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
requiredSellerCapabilities: ['interest-group-counts'],
})",
test_origin,
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js")),
ad1_url);
}
// Like RequiredSellerCapabilitiesWithPerBuyerCapabilities, but the per-seller
// sellerCapabilities initially don't match the seller origin, so both interest
// groups are removed from the auction.
IN_PROC_BROWSER_TEST_F(
InterestGroupBrowserTest,
RequiredSellerCapabilitiesWithPerBuyerCapabilitiesNoMatch) {
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
url::Origin other_origin =
url::Origin::Create(embedded_https_test_server().GetURL("b.test", "/"));
GURL ad1_url = embedded_https_test_server().GetURL(
"c.test", "/echo?stop_bidding_after_win");
GURL ad2_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_bikes");
constexpr char kUpdatePath[] = "/interest_group/update.json";
std::string update_response =
base::StringPrintf(R"(
{
"sellerCapabilities": {
"%s": ["interest-group-counts", "latency-stats"]
}
})",
test_origin.Serialize().c_str());
network_responder_->RegisterNetworkResponse(kUpdatePath, update_response);
GURL update_url = embedded_https_test_server().GetURL("a.test", kUpdatePath);
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test",
"/interest_group/bidding_logic_stop_bidding_after_win.js"))
.SetAds({{{ad1_url, /*metadata=*/std::nullopt}}})
.SetUpdateUrl(update_url)
.Build()));
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"bikes")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"))
.SetAds({{{ad2_url, /*metadata=*/std::nullopt}}})
.SetUpdateUrl(update_url)
.SetSellerCapabilities(
{{{other_origin,
{blink::SellerCapabilities::kInterestGroupCounts}}}})
.Build()));
// There is no winner, because "cars" is removed for not satisfying
// requiredSellerCapabilities, and "bikes" is removed since it only grants
// "interest-group-counts" to `other_origin`, not seller `test_origin`.
EXPECT_EQ(base::Value(),
RunAuctionAndWait(JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
requiredSellerCapabilities: ['interest-group-counts'],
})",
test_origin,
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js"))));
// A post-auction update occurs.
WaitForInterestGroupsSatisfying(
test_origin,
base::BindLambdaForTesting(
[&test_origin](scoped_refptr<StorageInterestGroups> groups) {
if (groups->size() != 2) {
return false;
}
for (const SingleStorageInterestGroup& group :
groups->GetInterestGroups()) {
const auto& seller_capabilities =
group->interest_group.seller_capabilities;
if (!seller_capabilities) {
return false;
}
auto it = seller_capabilities->find(test_origin);
if (it == seller_capabilities->end()) {
return false;
}
if (it->second !=
blink::SellerCapabilitiesType(
{blink::SellerCapabilities::kInterestGroupCounts,
blink::SellerCapabilities::kLatencyStats})) {
return false;
}
}
return true;
}));
// `ad1_url` now wins, because "cars" satisfies requiredSellerCapabilities.
RunAuctionAndWaitForURLAndNavigateIframe(
JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
requiredSellerCapabilities: ['interest-group-counts'],
})",
test_origin,
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js")),
ad1_url);
}
// Only some of the requiredSellerCapabilities are present, so the interest
// group is still removed from the auction.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RequiredSellerCapabilitiesPartialMatch) {
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad1_url = embedded_https_test_server().GetURL(
"c.test", "/echo?stop_bidding_after_win");
GURL ad2_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_bikes");
constexpr char kUpdatePath[] = "/interest_group/update.json";
constexpr char kUpdateResponse[] = R"(
{
"sellerCapabilities": {
"*": ["interest-group-counts", "latency-stats"]
}
})";
network_responder_->RegisterNetworkResponse(kUpdatePath, kUpdateResponse);
GURL update_url = embedded_https_test_server().GetURL("a.test", kUpdatePath);
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test",
"/interest_group/bidding_logic_stop_bidding_after_win.js"))
.SetAds({{{ad1_url, /*metadata=*/std::nullopt}}})
.SetUpdateUrl(update_url)
.SetAllSellersCapabilities(
{blink::SellerCapabilities::kInterestGroupCounts})
.Build()));
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"bikes")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"))
.SetAds({{{ad2_url, /*metadata=*/std::nullopt}}})
.SetUpdateUrl(update_url)
.SetAllSellersCapabilities(
{blink::SellerCapabilities::kInterestGroupCounts,
blink::SellerCapabilities::kLatencyStats})
.Build()));
// `ad2_url` wins, because "cars" is removed for not satisfying
// requiredSellerCapabilities.
RunAuctionAndWaitForURLAndNavigateIframe(
JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
requiredSellerCapabilities: ['interest-group-counts', 'latency-stats'],
})",
test_origin,
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js")),
ad2_url);
// A post-auction update occurs.
WaitForInterestGroupsSatisfying(
test_origin,
base::BindLambdaForTesting(
[](scoped_refptr<StorageInterestGroups> groups) {
if (groups->size() != 2) {
return false;
}
for (const SingleStorageInterestGroup& group :
groups->GetInterestGroups()) {
if (group->interest_group.all_sellers_capabilities !=
blink::SellerCapabilitiesType(
{blink::SellerCapabilities::kInterestGroupCounts,
blink::SellerCapabilities::kLatencyStats})) {
return false;
}
}
return true;
}));
// `ad1_url` now wins, because "cars" satisfies requiredSellerCapabilities.
RunAuctionAndWaitForURLAndNavigateIframe(
JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
requiredSellerCapabilities: ['interest-group-counts'],
})",
test_origin,
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js")),
ad1_url);
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionPrivacySandboxDisabled) {
// Successful join at a.test
GURL test_url_a = embedded_https_test_server().GetURL("a.test", "/echo");
ASSERT_TRUE(NavigateToURL(shell(), test_url_a));
url::Origin test_origin_a = url::Origin::Create(test_url_a);
AttachInterestGroupObserver();
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin_a,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"))
.SetTrustedBiddingSignalsUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/trusted_bidding_signals.json"))
.SetTrustedBiddingSignalsKeys({{"key1"}})
.SetAds({{{GURL("https://example.com/render"),
R"({"ad":"metadata","here":[1,2]})"}}})
.Build()));
GURL test_url_d = embedded_https_test_server().GetURL("d.test", "/echo");
ASSERT_TRUE(NavigateToURL(shell(), test_url_d));
// Auction should not be run since d.test has the API disabled.
EXPECT_EQ(base::Value(),
RunAuctionAndWait(JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
auctionSignals: {x: 1},
sellerSignals: {yet: 'more', info: 1},
perBuyerSignals: {$3: {even: 'more', x: 4.5}}
})",
url::Origin::Create(test_url_d),
embedded_https_test_server().GetURL(
"d.test", "/interest_group/decision_logic.js"),
test_origin_a)));
// No requests should have been made for the interest group or auction URLs.
base::AutoLock auto_lock(requests_lock_);
EXPECT_FALSE(base::Contains(
received_https_test_server_requests_,
embedded_https_test_server().GetURL("/interest_group/bidding_logic.js")));
EXPECT_FALSE(
base::Contains(received_https_test_server_requests_,
embedded_https_test_server().GetURL(
"/interest_group/trusted_bidding_signals.json")));
EXPECT_FALSE(base::Contains(received_https_test_server_requests_,
embedded_https_test_server().GetURL(
"/interest_group/decision_logic.js")));
WaitForAccessObserved({
{"global", TestInterestGroupObserver::kJoin, test_origin_a, "cars"},
});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionDisabledInterestGroup) {
// Inject an interest group into the DB for a disabled site so we can
// try to remove it.
GURL disabled_domain = embedded_https_test_server().GetURL("d.test", "/");
url::Origin disabled_origin = url::Origin::Create(disabled_domain);
AttachInterestGroupObserver();
blink::InterestGroup disabled_group;
disabled_group.expiry = base::Time::Now() + base::Seconds(300);
disabled_group.owner = disabled_origin;
disabled_group.name = "candy";
disabled_group.bidding_url = embedded_https_test_server().GetURL(
disabled_domain.host(),
"/interest_group/bidding_logic_stop_bidding_after_win.js");
disabled_group.ads.emplace();
disabled_group.ads->emplace_back(
GURL("https://stop_bidding_after_win.com/render"), std::nullopt);
manager_->JoinInterestGroup(std::move(disabled_group), disabled_domain);
ASSERT_EQ(1, GetJoinCount(disabled_origin, "candy"));
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_cars");
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
test_url.host(), "/interest_group/bidding_logic.js"))
.SetTrustedBiddingSignalsUrl(embedded_https_test_server().GetURL(
test_url.host(),
"/interest_group/trusted_bidding_signals.json"))
.SetTrustedBiddingSignalsKeys({{"key1"}})
.SetAds(
/*ads=*/{{{ad_url, R"({"ad":"metadata","here":[1,2]})"}}})
.Build()));
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1, $3],
auctionSignals: {x: 1},
sellerSignals: {yet: 'more', info: 1},
perBuyerSignals: {$1: {even: 'more', x: 4.5}}
})",
test_origin,
embedded_https_test_server().GetURL(test_url.host(),
"/interest_group/decision_logic.js"),
disabled_origin);
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad_url);
// No requests should have been made for the disabled interest group's URLs.
base::AutoLock auto_lock(requests_lock_);
EXPECT_FALSE(base::Contains(
received_https_test_server_requests_,
embedded_https_test_server().GetURL(
"/interest_group/bidding_logic_stop_bidding_after_win.js")));
WaitForAccessObserved({
{"global", TestInterestGroupObserver::kJoin, disabled_origin, "candy"},
{"global", TestInterestGroupObserver::kJoin, test_origin, "cars"},
{"1", TestInterestGroupObserver::kLoaded, test_origin, "cars"},
{"1", TestInterestGroupObserver::kBid, test_origin, "cars", 1.0},
{"1", TestInterestGroupObserver::kWin, test_origin, "cars"},
});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, RunAdAuctionWithWinner) {
URLLoaderMonitor url_loader_monitor;
base::HistogramTester histogram_tester;
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_cars");
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"))
.SetTrustedBiddingSignalsUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/trusted_bidding_signals.json"))
.SetTrustedBiddingSignalsKeys({{"key1"}})
.SetAds(/*ads=*/{{{ad_url, R"({"ad":"metadata","here":[1,2]})"}}})
.Build()));
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
auctionSignals: {x: 1},
sellerSignals: {yet: 'more', info: 1},
sellerTimeout: 20000,
perBuyerSignals: {$1: {even: 'more', x: 4.5}},
perBuyerTimeouts: {$1: 10000, '*': 15000}
})",
test_origin,
embedded_https_test_server().GetURL("a.test",
"/interest_group/decision_logic.js"));
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad_url);
// Check ResourceRequest structs of requests issued by the worklet process.
const struct ExpectedRequest {
GURL url;
const char* accept_header;
bool expect_trusted_params;
network::mojom::RequestMode expected_request_mode;
} kExpectedRequests[] = {
{embedded_https_test_server().GetURL("a.test",
"/interest_group/bidding_logic.js"),
"application/javascript", /*expect_trusted_params=*/true,
network::mojom::RequestMode::kNoCors},
{embedded_https_test_server().GetURL(
"a.test",
"/interest_group/trusted_bidding_signals.json?"
"hostname=a.test&keys=key1&interestGroupNames=cars"),
"application/json", /*expect_trusted_params=*/true,
network::mojom::RequestMode::kCors},
{embedded_https_test_server().GetURL("a.test",
"/interest_group/decision_logic.js"),
"application/javascript", /*expect_trusted_params=*/false,
network::mojom::RequestMode::kNoCors},
};
for (const auto& expected_request : kExpectedRequests) {
SCOPED_TRACE(expected_request.url);
std::optional<network::ResourceRequest> request =
url_loader_monitor.GetRequestInfo(expected_request.url);
ASSERT_TRUE(request);
EXPECT_EQ(network::mojom::CredentialsMode::kOmit,
request->credentials_mode);
EXPECT_EQ(network::mojom::RedirectMode::kError, request->redirect_mode);
EXPECT_EQ(test_origin, request->request_initiator);
EXPECT_EQ(1u, request->headers.GetHeaderVector().size());
EXPECT_THAT(request->headers.GetHeader(net::HttpRequestHeaders::kAccept),
testing::Optional(expected_request.accept_header));
EXPECT_EQ(expected_request.expect_trusted_params,
request->trusted_params.has_value());
EXPECT_EQ(expected_request.expected_request_mode, request->mode);
if (request->trusted_params) {
// Requests for interest-group provided URLs are cross-origin to the
// publisher page, and set trusted params to use the right cache shard,
// using a trusted URLLoaderFactory.
const net::IsolationInfo& isolation_info =
request->trusted_params->isolation_info;
EXPECT_EQ(net::IsolationInfo::RequestType::kOther,
isolation_info.request_type());
url::Origin expected_origin = url::Origin::Create(expected_request.url);
EXPECT_EQ(expected_origin, isolation_info.top_frame_origin());
EXPECT_EQ(expected_origin, isolation_info.frame_origin());
EXPECT_TRUE(isolation_info.site_for_cookies().IsNull());
}
}
// Check ResourceRequest structs of report requests.
const auto kExpectedReportUrls = std::to_array<GURL>({
embedded_https_test_server().GetURL("a.test", "/echoall?report_seller"),
embedded_https_test_server().GetURL("a.test", "/echoall?report_bidder"),
});
for (const auto& expected_report_url : kExpectedReportUrls) {
SCOPED_TRACE(expected_report_url);
// Make sure the report URL was actually fetched over the network.
WaitForUrl(expected_report_url);
std::optional<network::ResourceRequest> request =
url_loader_monitor.WaitForUrl(expected_report_url);
ASSERT_TRUE(request);
EXPECT_EQ(network::mojom::CredentialsMode::kOmit,
request->credentials_mode);
EXPECT_EQ(network::mojom::RedirectMode::kError, request->redirect_mode);
EXPECT_EQ(test_origin, request->request_initiator);
EXPECT_TRUE(request->headers.IsEmpty());
ASSERT_TRUE(request->trusted_params);
const net::IsolationInfo& isolation_info =
request->trusted_params->isolation_info;
EXPECT_EQ(net::IsolationInfo::RequestType::kOther,
isolation_info.request_type());
EXPECT_TRUE(isolation_info.network_isolation_key().IsTransient());
EXPECT_TRUE(isolation_info.site_for_cookies().IsNull());
}
// The two reporting requests should use different NIKs to prevent the
// requests from being correlated.
EXPECT_NE(url_loader_monitor.GetRequestInfo(kExpectedReportUrls[0])
->trusted_params->isolation_info.network_isolation_key(),
url_loader_monitor.GetRequestInfo(kExpectedReportUrls[1])
->trusted_params->isolation_info.network_isolation_key());
// Two reporting requests should result in two AdNavigationStarted.
histogram_tester.ExpectTotalCount(
"Ads.InterestGroup.Auction.AdNavigationStarted", 2);
}
// Runs auction just like test InterestGroupBrowserTest.RunAdAuctionWithWinner,
// but runs with the ads specified with sizes info.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionWithSizeWithWinner) {
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_cars");
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic_with_size.js"))
.SetAds(
/*ads=*/{{{ad_url, /*metadata=*/std::nullopt,
/*size_group=*/"group_1"}}})
.SetAdSizes(
{{{"size_1",
blink::AdSize(
100, blink::AdSize::LengthUnit::kScreenWidth, 50,
blink::AdSize::LengthUnit::kScreenHeight)}}})
.SetSizeGroups({{{"group_1", {"size_1"}}}})
.Build()));
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1]
})",
test_origin,
embedded_https_test_server().GetURL("a.test",
"/interest_group/decision_logic.js"));
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad_url);
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, ForceReload) {
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_cars");
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"))
.SetTrustedBiddingSignalsUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/trusted_bidding_signals.json"))
.SetTrustedBiddingSignalsKeys({{"key1"}})
.SetAds(/*ads=*/{{{ad_url, R"({"ad":"metadata","here":[1,2]})"}}})
.Build()));
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1]})",
test_origin,
embedded_https_test_server().GetURL("a.test",
"/interest_group/decision_logic.js"));
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad_url);
const std::vector<GURL> kExpectedRequests = {
embedded_https_test_server().GetURL("a.test",
"/interest_group/bidding_logic.js"),
embedded_https_test_server().GetURL(
"a.test",
"/interest_group/trusted_bidding_signals.json?"
"hostname=a.test&keys=key1&interestGroupNames=cars"),
embedded_https_test_server().GetURL("a.test",
"/interest_group/decision_logic.js"),
};
EXPECT_TRUE(HasServerSeenUrls(kExpectedRequests));
// Do a regular reload, and re-run the auction. Things should be cached.
ClearReceivedRequests();
ReloadBlockUntilNavigationsComplete(shell(), 1);
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad_url);
for (const auto& expected_url : kExpectedRequests) {
EXPECT_FALSE(HasServerSeenUrl(expected_url));
}
// Now do a bypass-cache reload. Things should get requested again.
ClearReceivedRequests();
ReloadBypassingCacheBlockUntilNavigationsComplete(shell(), 1);
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad_url);
EXPECT_TRUE(HasServerSeenUrls(kExpectedRequests));
}
// Runs an auction just like InterestGroupBrowserTest.RunAdAuctionWithWinner,
// but where the render size is specified only in the interest group, not the
// bid. When size is only specified in one place, the ads are considered
// matching (as long as their urls are the same), but the auction proceeds as if
// no size information was specified.
IN_PROC_BROWSER_TEST_F(
InterestGroupBrowserTest,
RunAdAuctionWithSizeForInterestGroupWithWinnerNoMacroSubstitution) {
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url = embedded_https_test_server().GetURL(
"c.test", "/echo?render_cars&size={%AD_WIDTH%}x{%AD_HEIGHT%}");
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"))
.SetAds(
/*ads=*/{{{ad_url, /*metadata=*/std::nullopt,
/*size_group=*/"group_1"}}})
.SetAdSizes(
{{{"size_1",
blink::AdSize(
100, blink::AdSize::LengthUnit::kScreenWidth, 50,
blink::AdSize::LengthUnit::kScreenHeight)}}})
.SetSizeGroups({{{"group_1", {"size_1"}}}})
.Build()));
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1]
})",
test_origin,
embedded_https_test_server().GetURL("a.test",
"/interest_group/decision_logic.js"));
// Size is only specified in the interest group, ad size macro substitution
// should not happen.
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad_url);
}
// Runs an auction just like InterestGroupBrowserTest.RunAdAuctionWithWinner,
// but where the render size is specified only in the bid, not the interest
// group. When size is only specified in one place, the ads are considered
// matching (as long as their urls are the same), but the auction proceeds as if
// no size information was specified.
IN_PROC_BROWSER_TEST_F(
InterestGroupBrowserTest,
RunAdAuctionWithSizeForBidWithWinnerNoMacroSubstitution) {
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url = embedded_https_test_server().GetURL(
"c.test", "/echo?render_cars&size={%AD_WIDTH%}x{%AD_HEIGHT%}");
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic_with_size.js"))
.SetAds(/*ads=*/{{{ad_url, /*metadata=*/std::nullopt}}})
.Build()));
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1]
})",
test_origin,
embedded_https_test_server().GetURL("a.test",
"/interest_group/decision_logic.js"));
// Size is only specified in the bid, ad size macro substitution should not
// happen.
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad_url);
}
// Runs auction just like test InterestGroupBrowserTest.RunAdAuctionWithWinner,
// but runs with the ads specified with sizes info. The ad url contains size
// macros, which should be substituted with the size from the winning bid.
// The size macros use the {%...%} format.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionWithSizeWithWinnerMacroSubstitution) {
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url = embedded_https_test_server().GetURL(
"c.test", "/echo?render_cars&size={%AD_WIDTH%}x{%AD_HEIGHT%}");
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic_with_size.js"))
.SetAds(/*ads=*/{{{ad_url, /*metadata=*/std::nullopt,
/*size_group=*/"group_1"}}})
.SetAdSizes(
{{{"size_1",
blink::AdSize(
100, blink::AdSize::LengthUnit::kScreenWidth, 50,
blink::AdSize::LengthUnit::kScreenHeight)}}})
.SetSizeGroups({{{"group_1", {"size_1"}}}})
.Build()));
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1]
})",
test_origin,
embedded_https_test_server().GetURL("a.test",
"/interest_group/decision_logic.js"));
int screen_width = static_cast<int>(
display::Screen::Get()->GetPrimaryDisplay().GetSizeInPixel().width());
int screen_height = static_cast<int>(
0.5 *
display::Screen::Get()->GetPrimaryDisplay().GetSizeInPixel().height());
GURL expected_url = embedded_https_test_server().GetURL(
"c.test", base::StringPrintf("/echo?render_cars&size=%ix%i", screen_width,
screen_height));
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, expected_url);
}
// Same as RunAdAuctionWithSizeWithWinnerMacroSubstitution, except the size
// macros use the ${...} format.
IN_PROC_BROWSER_TEST_F(
InterestGroupBrowserTest,
RunAdAuctionWithSizeWithWinnerMacroSubstitutionAlternateFormat) {
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url = embedded_https_test_server().GetURL(
"c.test", "/echo?render_cars&size=${AD_WIDTH}x${AD_HEIGHT}");
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic_with_size.js"))
.SetAds(/*ads=*/{{{ad_url, /*metadata=*/std::nullopt,
/*size_group=*/"group_1"}}})
.SetAdSizes(
{{{"size_1",
blink::AdSize(
100, blink::AdSize::LengthUnit::kScreenWidth, 50,
blink::AdSize::LengthUnit::kScreenHeight)}}})
.SetSizeGroups({{{"group_1", {"size_1"}}}})
.Build()));
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1]
})",
test_origin,
embedded_https_test_server().GetURL("a.test",
"/interest_group/decision_logic.js"));
int screen_width = static_cast<int>(
display::Screen::Get()->GetPrimaryDisplay().GetSizeInPixel().width());
int screen_height = static_cast<int>(
0.5 *
display::Screen::Get()->GetPrimaryDisplay().GetSizeInPixel().height());
GURL expected_url = embedded_https_test_server().GetURL(
"c.test", base::StringPrintf("/echo?render_cars&size=%ix%i", screen_width,
screen_height));
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, expected_url);
}
class DeprecatedRenderURLReplacementsDisabledTest
: public InterestGroupBrowserTest {
public:
DeprecatedRenderURLReplacementsDisabledTest() {
feature_list_.InitAndDisableFeature(
{blink::features::kFledgeDeprecatedRenderURLReplacements});
}
private:
base::test::ScopedFeatureList feature_list_;
};
IN_PROC_BROWSER_TEST_F(DeprecatedRenderURLReplacementsDisabledTest,
FeatureDetection) {
const char kTestExpression[] = R"(
navigator.protectedAudience.queryFeatureSupport(
'deprecatedRenderURLReplacements');
)";
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/simple_page.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
EXPECT_EQ(false, EvalJs(shell(), kTestExpression));
}
// Run a single ad auction and pass deprecatedRenderURLReplacements, and make
// sure they do not have an effect since it is disabled.
IN_PROC_BROWSER_TEST_F(DeprecatedRenderURLReplacementsDisabledTest,
RunAdAuctionWithRenderURLReplacementsHasNoEffect) {
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url = GURL(embedded_https_test_server()
.GetURL("c.test", "/%%echo%%?${INTEREST_GROUP_NAME}")
.spec());
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
/*owner=*/test_origin,
/*name=*/"cars",
/*priority=*/0.0, /*execution_mode=*/
blink::InterestGroup::ExecutionMode::kCompatibilityMode,
/*bidding_url=*/
embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"),
/*ads=*/{{{ad_url, R"({"ad":"metadata","here":[1,2]})"}}}));
for (bool use_promise : {false, true}) {
SCOPED_TRACE(use_promise);
ASSERT_TRUE(ExecJs(shell(), MaybePromiseFunction(use_promise)));
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
// Signal for verifying that what goes into scoreAd matches
// the deprecatedRenderURLReplacements.
sellerSignals: {deprecatedRenderURLReplacementsExpected: undefined},
deprecatedRenderURLReplacements:
maybePromise({$3: "render_cars", "%%echo%%": "echo"})
})",
test_origin,
embedded_https_test_server().GetURL(
"a.test",
"/interest_group/"
"decision_logic_deprecated_render_url_replacements_validator.js"),
"${INTEREST_GROUP_NAME}");
auto result = RunAuctionAndWait(auction_config,
/*execution_target=*/std::nullopt);
GURL urn_url = GURL(result.ExtractString());
EXPECT_TRUE(urn_url.is_valid());
EXPECT_EQ(url::kUrnScheme, urn_url.scheme_piece());
NavigateIframeAndCheckURL(web_contents(), urn_url, ad_url);
}
}
class DeprecatedRenderURLReplacementsEnabledTest
: public InterestGroupBrowserTest {
public:
DeprecatedRenderURLReplacementsEnabledTest() {
feature_list_.InitAndEnableFeature(
{blink::features::kFledgeDeprecatedRenderURLReplacements});
}
private:
base::test::ScopedFeatureList feature_list_;
};
IN_PROC_BROWSER_TEST_F(DeprecatedRenderURLReplacementsEnabledTest,
FeatureDetection) {
const char kTestExpression[] = R"(
navigator.protectedAudience.queryFeatureSupport(
'deprecatedRenderURLReplacements');
)";
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/simple_page.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
EXPECT_EQ(true, EvalJs(shell(), kTestExpression));
}
// Run a single ad auction with a winner and ensure the render url replacements
// occur even if only one of the specified replacements is a match.
IN_PROC_BROWSER_TEST_F(DeprecatedRenderURLReplacementsEnabledTest,
RunAdAuctionWithWinnerWithPartialRenderURLReplacements) {
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url = GURL(embedded_https_test_server()
.GetURL("c.test", "/%%echo%%?${INTEREST_GROUP_NAME}")
.spec());
GURL expected_ad_url = embedded_https_test_server().GetURL(
"c.test", "/echo?${INTEREST_GROUP_NAME}");
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
/*owner=*/test_origin,
/*name=*/"cars",
/*priority=*/0.0, /*execution_mode=*/
blink::InterestGroup::ExecutionMode::kCompatibilityMode,
/*bidding_url=*/
embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"),
/*ads=*/{{{ad_url, R"({"ad":"metadata","here":[1,2]})"}}}));
for (bool use_promise : {false, true}) {
SCOPED_TRACE(use_promise);
ASSERT_TRUE(ExecJs(shell(), MaybePromiseFunction(use_promise)));
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
deprecatedRenderURLReplacements:
maybePromise({$3: "render_cars", "%%echo%%": "echo"})
})",
test_origin,
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js"),
"${DIFFERENT_INTEREST_GROUP_NAME}");
auto result = RunAuctionAndWait(auction_config);
GURL urn_url = GURL(result.ExtractString());
EXPECT_TRUE(urn_url.is_valid());
EXPECT_EQ(url::kUrnScheme, urn_url.scheme_piece());
NavigateIframeAndCheckURL(web_contents(), urn_url, expected_ad_url);
}
}
// Run a single ad auction with a winner and ensure the render url replacements
// occur.
IN_PROC_BROWSER_TEST_F(DeprecatedRenderURLReplacementsEnabledTest,
RunAdAuctionWithWinnerWithRenderURLReplacements) {
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url = GURL(embedded_https_test_server()
.GetURL("c.test", "/%%echo%%?${INTEREST_GROUP_NAME}")
.spec());
GURL expected_ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_cars");
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
/*owner=*/test_origin,
/*name=*/"cars",
/*priority=*/0.0, /*execution_mode=*/
blink::InterestGroup::ExecutionMode::kCompatibilityMode,
/*bidding_url=*/
embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"),
/*ads=*/{{{ad_url, R"({"ad":"metadata","here":[1,2]})"}}}));
for (bool use_promise : {false, true}) {
SCOPED_TRACE(use_promise);
ASSERT_TRUE(ExecJs(shell(), MaybePromiseFunction(use_promise)));
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
// Signal for verifying that what goes into scoreAd matches
// the deprecatedRenderURLReplacements.
sellerSignals: {deprecatedRenderURLReplacementsExpected:
{$3: "render_cars", "%%echo%%": "echo"}},
deprecatedRenderURLReplacements:
maybePromise({$3: "render_cars", "%%echo%%": "echo"})
})",
test_origin,
embedded_https_test_server().GetURL(
"a.test",
"/interest_group/"
"decision_logic_deprecated_render_url_replacements_validator.js"),
"${INTEREST_GROUP_NAME}");
auto result = RunAuctionAndWait(auction_config);
GURL urn_url = GURL(result.ExtractString());
EXPECT_TRUE(urn_url.is_valid());
EXPECT_EQ(url::kUrnScheme, urn_url.scheme_piece());
NavigateIframeAndCheckURL(web_contents(), urn_url, expected_ad_url);
}
}
// Run a single ad auction and ensure it fails when passed badly formatted
// replacements.
IN_PROC_BROWSER_TEST_F(DeprecatedRenderURLReplacementsEnabledTest,
RunAdAuctionFailsWithBadRenderURLReplacements) {
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url = GURL(embedded_https_test_server()
.GetURL("c.test", "/%%echo%%?${INTEREST_GROUP_NAME}")
.spec());
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
/*owner=*/test_origin,
/*name=*/"cars",
/*priority=*/0.0, /*execution_mode=*/
blink::InterestGroup::ExecutionMode::kCompatibilityMode,
/*bidding_url=*/
embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"),
/*ads=*/{{{ad_url, R"({"ad":"metadata","here":[1,2]})"}}}));
for (bool use_promise : {false, true}) {
SCOPED_TRACE(use_promise);
ASSERT_TRUE(ExecJs(shell(), MaybePromiseFunction(use_promise)));
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
deprecatedRenderURLReplacements:
maybePromise({$3: "render_cars", "%%echo%%": "echo"})
})",
test_origin,
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js"),
"${BAD_REPLACEMENT_NO_END_BRACKET");
auto result = RunAuctionAndWait(auction_config);
std::string expected_error_message =
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': Promise "
"argument rejected or resolved to invalid value.";
EXPECT_EQ(result, expected_error_message);
}
}
// Run a multi-seller auction with a single component seller with a winner and
// ensure it fails when passed badly formatted replacements.
IN_PROC_BROWSER_TEST_F(
DeprecatedRenderURLReplacementsEnabledTest,
SingleComponentAuctionFailsWithBadRenderURLReplacements) {
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url = GURL(embedded_https_test_server()
.GetURL("c.test", "/%%echo%%?${INTEREST_GROUP_NAME}")
.spec());
GURL expected_ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_cars");
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
/*owner=*/test_origin,
/*name=*/"cars",
/*priority=*/0.0,
/*execution_mode=*/
blink::InterestGroup::ExecutionMode::kCompatibilityMode,
/*bidding_url=*/
embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"),
/*ads=*/{{{ad_url, /*metadata=*/std::nullopt}}}));
for (bool use_promise : {false, true}) {
SCOPED_TRACE(use_promise);
ASSERT_TRUE(ExecJs(shell(), MaybePromiseFunction(use_promise)));
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
// Signal to the top-level seller to allow participation in a
// component auction.
auctionSignals: "sellerAllowsComponentAuction",
componentAuctions: [{
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
// Signal to the bidder and component seller to allow participation
// in a component auction.
auctionSignals: "bidderAllowsComponentAuction,"+
"sellerAllowsComponentAuction",
deprecatedRenderURLReplacements: maybePromise(
{$3: "render_cars", "NO_STARTING_PERCENTS%%": "echo"})
}]
})",
test_origin,
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js"),
"${INTEREST_GROUP_NAME}");
auto result = RunAuctionAndWait(auction_config);
std::string expected_error_message =
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': Promise "
"argument rejected or resolved to invalid value.";
EXPECT_EQ(result, expected_error_message);
}
}
// Run a multi-seller auction with a single component seller with a winner and
// ensure the render url replacements occur.
IN_PROC_BROWSER_TEST_F(
DeprecatedRenderURLReplacementsEnabledTest,
SingleComponentAuctionWithWinnerWithRenderURLReplacements) {
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url = GURL(embedded_https_test_server()
.GetURL("c.test", "/%%echo%%?${INTEREST_GROUP_NAME}")
.spec());
GURL expected_ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_cars");
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
/*owner=*/test_origin,
/*name=*/"cars",
/*priority=*/0.0,
/*execution_mode=*/
blink::InterestGroup::ExecutionMode::kCompatibilityMode,
/*bidding_url=*/
embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"),
/*ads=*/{{{ad_url, /*metadata=*/std::nullopt}}}));
for (bool use_promise : {false, true}) {
SCOPED_TRACE(use_promise);
ASSERT_TRUE(ExecJs(shell(), MaybePromiseFunction(use_promise)));
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
// Signal to the top-level seller to allow participation in a
// component auction.
auctionSignals: "sellerAllowsComponentAuction",
// Signal for verifying that what goes into scoreAd matches
// the deprecatedRenderURLReplacements.
sellerSignals: {deprecatedRenderURLReplacementsExpected: undefined},
componentAuctions: [{
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
// Signal to the bidder and component seller to allow participation
// in a component auction.
auctionSignals: "bidderAllowsComponentAuction,"+
"sellerAllowsComponentAuction",
// Signal for verifying that what goes into scoreAd matches
// the deprecatedRenderURLReplacements.
sellerSignals: {deprecatedRenderURLReplacementsExpected:
{$3: "render_cars", "%%echo%%": "echo"}},
deprecatedRenderURLReplacements:
maybePromise({$3: "render_cars", "%%echo%%": "echo"})
}]
})",
test_origin,
embedded_https_test_server().GetURL(
"a.test",
"/interest_group/"
"decision_logic_deprecated_render_url_replacements_validator.js"),
"${INTEREST_GROUP_NAME}");
auto result = RunAuctionAndWait(auction_config);
GURL urn_url = GURL(result.ExtractString());
EXPECT_TRUE(urn_url.is_valid());
EXPECT_EQ(url::kUrnScheme, urn_url.scheme_piece());
NavigateIframeAndCheckURL(web_contents(), urn_url, expected_ad_url);
}
}
// Run a multi-seller auction with a single component seller and ensure
// non-matching render url replacements do not affect the winner.
IN_PROC_BROWSER_TEST_F(
DeprecatedRenderURLReplacementsEnabledTest,
SingleComponentAdAuctionWithWinnerNotAffectedByRenderURLReplacements) {
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url = GURL(embedded_https_test_server()
.GetURL("c.test", "/%%echo%%?${INTEREST_GROUP_NAME}")
.spec());
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
/*owner=*/test_origin,
/*name=*/"cars",
/*priority=*/0.0,
/*execution_mode=*/
blink::InterestGroup::ExecutionMode::kCompatibilityMode,
/*bidding_url=*/
embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"),
/*ads=*/{{{ad_url, /*metadata=*/std::nullopt}}}));
for (bool use_promise : {false, true}) {
SCOPED_TRACE(use_promise);
ASSERT_TRUE(ExecJs(shell(), MaybePromiseFunction(use_promise)));
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
// Signal to the top-level seller to allow participation in a
// component auction.
auctionSignals: "sellerAllowsComponentAuction",
componentAuctions: [{
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
// Signal to the bidder and component seller to allow participation
// in a component auction.
auctionSignals: "bidderAllowsComponentAuction,"+
"sellerAllowsComponentAuction",
deprecatedRenderURLReplacements:
maybePromise({$3: "render_cars", "%%echo2%%": "echo"})
}]
})",
test_origin,
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js"),
"${INTEREST_GROUP2_NAME}");
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad_url);
}
}
// Run a multi-seller auction with a single component seller and ensure the
// render url replacements in the loser do not affect the winner.
IN_PROC_BROWSER_TEST_F(
DeprecatedRenderURLReplacementsEnabledTest,
SingleComponentAdAuctionWithWinnerWithRenderURLReplacements) {
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url = GURL(embedded_https_test_server()
.GetURL("c.test", "/%%echo%%?${INTEREST_GROUP_NAME}")
.spec());
GURL ad_url2 =
GURL(embedded_https_test_server()
.GetURL("c.test", "/%%echo2%%?${INTEREST_GROUP2_NAME}")
.spec());
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
/*owner=*/test_origin,
/*name=*/"cars",
/*priority=*/0.0,
/*execution_mode=*/
blink::InterestGroup::ExecutionMode::kCompatibilityMode,
/*bidding_url=*/
embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"),
/*ads=*/{{{ad_url, /*metadata=*/std::nullopt}}}));
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
/*owner=*/test_origin,
/*name=*/"boats",
/*priority=*/0.0,
/*execution_mode=*/
blink::InterestGroup::ExecutionMode::kCompatibilityMode,
/*bidding_url=*/
embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic_throws.js"),
/*ads=*/{{{ad_url2, /*metadata=*/std::nullopt}}}));
for (bool use_promise : {false, true}) {
SCOPED_TRACE(use_promise);
ASSERT_TRUE(ExecJs(shell(), MaybePromiseFunction(use_promise)));
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
// Signal to the top-level seller to allow participation in a
// component auction.
auctionSignals: "sellerAllowsComponentAuction",
componentAuctions: [{
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
// Signal to the bidder and component seller to allow participation
// in a component auction.
auctionSignals: "bidderAllowsComponentAuction,"+
"sellerAllowsComponentAuction",
deprecatedRenderURLReplacements:
maybePromise({$3: "render_cars", "%%echo2%%": "echo"})
}]
})",
test_origin,
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js"),
"${INTEREST_GROUP2_NAME}");
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad_url);
}
}
// Run a multi-seller auction with multiple component auctions and ensure the
// render url replacements in the loser does not affect the winner.
IN_PROC_BROWSER_TEST_F(
DeprecatedRenderURLReplacementsEnabledTest,
MultipleComponentAuctionsWithWinnerNotAffectedByOtherComponentAuctionsURLReplacements) {
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
GURL test_url2 =
embedded_https_test_server().GetURL("b.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
url::Origin test_origin2 = url::Origin::Create(test_url2);
GURL ad_url = GURL(embedded_https_test_server()
.GetURL("c.test", "/%%echo%%?${INTEREST_GROUP_NAME}")
.spec());
GURL expected_ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_cars");
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
/*owner=*/test_origin,
/*name=*/"cars",
/*priority=*/0.0,
/*execution_mode=*/
blink::InterestGroup::ExecutionMode::kCompatibilityMode,
/*bidding_url=*/
embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"),
/*ads=*/{{{ad_url, /*metadata=*/std::nullopt}}}));
ASSERT_TRUE(NavigateToURL(shell(), test_url2));
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
/*owner=*/test_origin2,
/*name=*/"boats",
/*priority=*/0.0,
/*execution_mode=*/
blink::InterestGroup::ExecutionMode::kCompatibilityMode,
/*bidding_url=*/
embedded_https_test_server().GetURL(
"b.test", "/interest_group/bidding_logic_throws.js"),
/*ads=*/{{{ad_url, /*metadata=*/std::nullopt}}}));
for (bool use_promise : {false, true}) {
SCOPED_TRACE(use_promise);
ASSERT_TRUE(ExecJs(shell(), MaybePromiseFunction(use_promise)));
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
// Signal to the top-level seller to allow participation in a
// component auction.
auctionSignals: "sellerAllowsComponentAuction",
componentAuctions: [{
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
// Signal to the bidder and component seller to allow participation
// in a component auction.
auctionSignals: "bidderAllowsComponentAuction,"+
"sellerAllowsComponentAuction",
},
{
// This seller loses because the bidding logic used for it, throws
// an error. So the replacements will have no effect.
seller: $3,
decisionLogicURL: $4,
interestGroupBuyers: [$3],
// Signal to the bidder and component seller to allow participation
// in a component auction.
auctionSignals: "bidderAllowsComponentAuction,"+
"sellerAllowsComponentAuction",
deprecatedRenderURLReplacements:
maybePromise({$5: "render_boats", "%%echo%%": "echo"})
}]
})",
test_origin,
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js"),
test_origin2,
embedded_https_test_server().GetURL(
"b.test", "/interest_group/decision_logic.js"),
"${INTEREST_GROUP_NAME}");
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad_url);
}
}
// Run a multi-seller auction with multiple component auctions and ensure the
// render url replacements do not occur if they create an invalid URL.
IN_PROC_BROWSER_TEST_F(
DeprecatedRenderURLReplacementsEnabledTest,
MultipleComponentAuctionsWithWinnerNotReplacedWithBadURLReplacementsWithinHostname) {
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
GURL test_url2 =
embedded_https_test_server().GetURL("b.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
url::Origin test_origin2 = url::Origin::Create(test_url2);
GURL ad_url = GURL("https://${video_rendering_url}");
GURL expected_ad_url = ad_url;
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
/*owner=*/test_origin,
/*name=*/"cars",
/*priority=*/0.0,
/*execution_mode=*/
blink::InterestGroup::ExecutionMode::kCompatibilityMode,
/*bidding_url=*/
embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"),
/*ads=*/{{{ad_url, /*metadata=*/std::nullopt}}}));
ASSERT_TRUE(NavigateToURL(shell(), test_url2));
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
/*owner=*/test_origin2,
/*name=*/"boats",
/*priority=*/0.0,
/*execution_mode=*/
blink::InterestGroup::ExecutionMode::kCompatibilityMode,
/*bidding_url=*/
embedded_https_test_server().GetURL(
"b.test", "/interest_group/bidding_logic_throws.js"),
/*ads=*/{{{ad_url, /*metadata=*/std::nullopt}}}));
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
// Signal to the top-level seller to allow participation in a
// component auction.
auctionSignals: "sellerAllowsComponentAuction",
componentAuctions: [{
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
// Signal to the bidder and component seller to allow participation
// in a component auction.
auctionSignals: "bidderAllowsComponentAuction,"+
"sellerAllowsComponentAuction",
deprecatedRenderURLReplacements: {$5: $6, "%%echo%%": "echo"}
},
{
// This seller loses because the bidding logic used for it, throws
// an error. So the replacements will have no effect.
seller: $3,
decisionLogicURL: $4,
interestGroupBuyers: [$3],
// Signal to the bidder and component seller to allow participation
// in a component auction.
auctionSignals: "bidderAllowsComponentAuction,"+
"sellerAllowsComponentAuction",
}]
})",
test_origin,
embedded_https_test_server().GetURL("a.test",
"/interest_group/decision_logic.js"),
test_origin2,
embedded_https_test_server().GetURL("b.test",
"/interest_group/decision_logic.js"),
"${video_rendering_url}",
"${video_rendering_url}https%3A%2F%2Fcomponent_seller_url_2${post_"
"component_url_string}");
auto result = RunAuctionAndWait(auction_config);
GURL urn_url = GURL(result.ExtractString());
EXPECT_TRUE(urn_url.is_valid());
EXPECT_EQ(url::kUrnScheme, urn_url.scheme_piece());
TestFencedFrameURLMappingResultObserver observer;
ConvertFencedFrameURNToURL(urn_url, &observer);
EXPECT_TRUE(observer.mapped_url()) << urn_url;
EXPECT_EQ(expected_ad_url, observer.mapped_url());
}
// Run a multi-seller auction with multiple component auctions and ensure the
// render url replacements occur within the hostname.
IN_PROC_BROWSER_TEST_F(
DeprecatedRenderURLReplacementsEnabledTest,
MultipleComponentAuctionsWithWinnerWithURLReplacementsWithinHostname) {
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
GURL test_url2 =
embedded_https_test_server().GetURL("b.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
url::Origin test_origin2 = url::Origin::Create(test_url2);
GURL ad_url = GURL("https://${video_rendering_url}");
GURL expected_ad_url = GURL("https://example.com");
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
/*owner=*/test_origin,
/*name=*/"cars",
/*priority=*/0.0,
/*execution_mode=*/
blink::InterestGroup::ExecutionMode::kCompatibilityMode,
/*bidding_url=*/
embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"),
/*ads=*/{{{ad_url, /*metadata=*/std::nullopt}}}));
ASSERT_TRUE(NavigateToURL(shell(), test_url2));
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
/*owner=*/test_origin2,
/*name=*/"boats",
/*priority=*/0.0,
/*execution_mode=*/
blink::InterestGroup::ExecutionMode::kCompatibilityMode,
/*bidding_url=*/
embedded_https_test_server().GetURL(
"b.test", "/interest_group/bidding_logic_throws.js"),
/*ads=*/{{{ad_url, /*metadata=*/std::nullopt}}}));
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
// Signal to the top-level seller to allow participation in a
// component auction.
auctionSignals: "sellerAllowsComponentAuction",
componentAuctions: [{
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
// Signal to the bidder and component seller to allow participation
// in a component auction.
auctionSignals: "bidderAllowsComponentAuction,"+
"sellerAllowsComponentAuction",
deprecatedRenderURLReplacements:{$5: $6, "%%echo%%": "echo"}
},
{
// This seller loses because the bidding logic used for it, throws
// an error. So the replacements will have no effect.
seller: $3,
decisionLogicURL: $4,
interestGroupBuyers: [$3],
// Signal to the bidder and component seller to allow participation
// in a component auction.
auctionSignals: "bidderAllowsComponentAuction,"+
"sellerAllowsComponentAuction",
}]
})",
test_origin,
embedded_https_test_server().GetURL("a.test",
"/interest_group/decision_logic.js"),
test_origin2,
embedded_https_test_server().GetURL("b.test",
"/interest_group/decision_logic.js"),
"${video_rendering_url}", "example.com");
auto result = RunAuctionAndWait(auction_config);
GURL urn_url = GURL(result.ExtractString());
EXPECT_TRUE(urn_url.is_valid());
EXPECT_EQ(url::kUrnScheme, urn_url.scheme_piece());
TestFencedFrameURLMappingResultObserver observer;
ConvertFencedFrameURNToURL(urn_url, &observer);
EXPECT_TRUE(observer.mapped_url()) << urn_url;
EXPECT_EQ(expected_ad_url, observer.mapped_url());
}
// Run a multi-seller auction with multiple component auctions and ensure that
// we can chain together replacements from deprecatedRenderURLReplacements with
// replacements in deprecatedReplaceInURN.
IN_PROC_BROWSER_TEST_F(DeprecatedRenderURLReplacementsEnabledTest,
MultipleComponentAuctionsWithChainingReplacements) {
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
GURL test_url2 =
embedded_https_test_server().GetURL("b.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
url::Origin test_origin2 = url::Origin::Create(test_url2);
GURL ad_url = GURL("https://${video_rendering_url}");
GURL expected_ad_url = GURL(
"https://${video_rendering_url}/"
"https%3A%2F%2Fcomponent_seller_url_2%%post_component_url_string%%/");
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
/*owner=*/test_origin,
/*name=*/"cars",
/*priority=*/0.0,
/*execution_mode=*/
blink::InterestGroup::ExecutionMode::kCompatibilityMode,
/*bidding_url=*/
embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"),
/*ads=*/{{{ad_url, /*metadata=*/std::nullopt}}}));
ASSERT_TRUE(NavigateToURL(shell(), test_url2));
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
/*owner=*/test_origin2,
/*name=*/"boats",
/*priority=*/0.0,
/*execution_mode=*/
blink::InterestGroup::ExecutionMode::kCompatibilityMode,
/*bidding_url=*/
embedded_https_test_server().GetURL(
"b.test", "/interest_group/bidding_logic_throws.js"),
/*ads=*/{{{ad_url, /*metadata=*/std::nullopt}}}));
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
// Signal to the top-level seller to allow participation in a
// component auction.
auctionSignals: "sellerAllowsComponentAuction",
componentAuctions: [{
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
// Signal to the bidder and component seller to allow participation
// in a component auction.
auctionSignals: "bidderAllowsComponentAuction,"+
"sellerAllowsComponentAuction",
deprecatedRenderURLReplacements:{$5: $6, "%%echo%%": "echo"}
},
{
// This seller loses because the bidding logic used for it, throws
// an error. So the replacements will have no effect.
seller: $3,
decisionLogicURL: $4,
interestGroupBuyers: [$3],
// Signal to the bidder and component seller to allow participation
// in a component auction.
auctionSignals: "bidderAllowsComponentAuction,"+
"sellerAllowsComponentAuction",
}]
})",
test_origin,
embedded_https_test_server().GetURL("a.test",
"/interest_group/decision_logic.js"),
test_origin2,
embedded_https_test_server().GetURL("b.test",
"/interest_group/decision_logic.js"),
"${video_rendering_url}",
"${video_rendering_url}/"
"https%3A%2F%2Fcomponent_seller_url_2%%post_component_url_string%%");
auto result = RunAuctionAndWait(auction_config);
GURL urn_url = GURL(result.ExtractString());
EXPECT_TRUE(urn_url.is_valid());
EXPECT_EQ(url::kUrnScheme, urn_url.scheme_piece());
{
TestFencedFrameURLMappingResultObserver observer;
ConvertFencedFrameURNToURL(urn_url, &observer);
EXPECT_TRUE(observer.mapped_url()) << urn_url;
EXPECT_EQ(expected_ad_url, observer.mapped_url());
}
EXPECT_TRUE(ReplaceInURNInJS(
urn_url, {{"${video_rendering_url}", "example.com/%%MORE_MACROS%%"},
{"%%post_component_url_string%%", "POST_COMPONENT_STUFF"}}));
GURL new_expected_url = GURL(
"https://example.com/%%MORE_MACROS%%/"
"https%3A%2F%2Fcomponent_seller_url_2POST_COMPONENT_STUFF/");
{
TestFencedFrameURLMappingResultObserver observer;
ConvertFencedFrameURNToURL(urn_url, &observer);
EXPECT_EQ(new_expected_url, observer.mapped_url());
}
}
// Run a multi-seller auction with multiple component auctions, and ensure an
// error is thrown when any of the component auctions pass badly formatted
// replacements.
IN_PROC_BROWSER_TEST_F(
DeprecatedRenderURLReplacementsEnabledTest,
MultipleComponentAuctionsFailsWhenAnyComponentAuctionHasBadURLReplacements) {
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
GURL test_url2 =
embedded_https_test_server().GetURL("b.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
url::Origin test_origin2 = url::Origin::Create(test_url2);
GURL ad_url = GURL(embedded_https_test_server()
.GetURL("c.test", "/%%echo%%?${INTEREST_GROUP_NAME}")
.spec());
GURL expected_ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_cars");
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
/*owner=*/test_origin,
/*name=*/"cars",
/*priority=*/0.0,
/*execution_mode=*/
blink::InterestGroup::ExecutionMode::kCompatibilityMode,
/*bidding_url=*/
embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"),
/*ads=*/{{{ad_url, /*metadata=*/std::nullopt}}}));
ASSERT_TRUE(NavigateToURL(shell(), test_url2));
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
/*owner=*/test_origin2,
/*name=*/"boats",
/*priority=*/0.0,
/*execution_mode=*/
blink::InterestGroup::ExecutionMode::kCompatibilityMode,
/*bidding_url=*/
embedded_https_test_server().GetURL(
"b.test", "/interest_group/bidding_logic_throws.js"),
/*ads=*/{{{ad_url, /*metadata=*/std::nullopt}}}));
for (bool use_promise : {false, true}) {
SCOPED_TRACE(use_promise);
ASSERT_TRUE(ExecJs(shell(), MaybePromiseFunction(use_promise)));
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
// Signal to the top-level seller to allow participation in a
// component auction.
auctionSignals: "sellerAllowsComponentAuction",
componentAuctions: [{
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
// Signal to the bidder and component seller to allow participation
// in a component auction.
auctionSignals: "bidderAllowsComponentAuction,"+
"sellerAllowsComponentAuction",
deprecatedRenderURLReplacements:
maybePromise({$5: "render_boats", "%%echo%%": "echo"})
},
{
seller: $3,
decisionLogicURL: $4,
interestGroupBuyers: [$3],
// Signal to the bidder and component seller to allow participation
// in a component auction.
auctionSignals: "bidderAllowsComponentAuction,"+
"sellerAllowsComponentAuction",
deprecatedRenderURLReplacements: maybePromise(
{$5: "render_boats", "%NO_STARTING_PERCENT%%": "echo"})
}]
})",
test_origin,
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js"),
test_origin2,
embedded_https_test_server().GetURL(
"b.test", "/interest_group/decision_logic.js"),
"${INTEREST_GROUP_NAME}");
auto result = RunAuctionAndWait(auction_config);
std::string expected_error_message =
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': Promise "
"argument rejected or resolved to invalid value.";
EXPECT_EQ(result, expected_error_message);
}
}
// Run a multi-seller auction and ensure it fails, if we pass
// 'deprecatedRenderURLReplacements' within the top level config, when there is
// a component auction.
IN_PROC_BROWSER_TEST_F(
DeprecatedRenderURLReplacementsEnabledTest,
RunComponentAdAuctionFailsWithRenderURLReplacementsInTopLevelAuctionConfig) {
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url = GURL(embedded_https_test_server()
.GetURL("c.test", "/%%echo%%?${INTEREST_GROUP_NAME}")
.spec());
GURL expected_ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_cars");
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
/*owner=*/test_origin,
/*name=*/"cars",
/*priority=*/0.0,
/*execution_mode=*/
blink::InterestGroup::ExecutionMode::kCompatibilityMode,
/*bidding_url=*/
embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"),
/*ads=*/{{{ad_url, /*metadata=*/std::nullopt}}}));
for (bool use_promise : {false, true}) {
SCOPED_TRACE(use_promise);
ASSERT_TRUE(ExecJs(shell(), MaybePromiseFunction(use_promise)));
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
// Signal to the top-level seller to allow participation in a
// component auction.
auctionSignals: "sellerAllowsComponentAuction",
deprecatedRenderURLReplacements:
maybePromise({$3: "render_cars", "%%echo%%": "echo"}),
componentAuctions: [{
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
// Signal to the bidder and component seller to allow participation
// in a component auction.
auctionSignals: "bidderAllowsComponentAuction,"+
"sellerAllowsComponentAuction",
deprecatedRenderURLReplacements:
maybePromise({$3: "render_cars", "%%echo%%": "echo"})
}]
})",
test_origin,
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js"),
"${INTEREST_GROUP_NAME}");
auto result = RunAuctionAndWait(auction_config);
std::string expected_error_message =
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': Auctions "
"may only specify 'deprecatedRenderURLReplacements' if they do not "
"have 'componentAuctions'.";
EXPECT_EQ(result, expected_error_message);
}
}
// This tests for when a replacement is done within
// deprecatedRenderURLReplacements, that the URL created after is a valid one,
// because it would crash otherwise.
IN_PROC_BROWSER_TEST_F(DeprecatedRenderURLReplacementsEnabledTest,
ReplacementWithInvalidURLReplacement) {
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url = GURL("https://${video_rendering_url}");
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
/*owner=*/test_origin,
/*name=*/"cars",
/*priority=*/0.0, /*execution_mode=*/
blink::InterestGroup::ExecutionMode::kCompatibilityMode,
/*bidding_url=*/
embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"),
/*ads=*/{{{ad_url, R"({"ad":"metadata","here":[1,2]})"}}}));
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
deprecatedRenderURLReplacements: {$3: $4, "%%echo%%": "echo"}
})",
test_origin,
embedded_https_test_server().GetURL("a.test",
"/interest_group/decision_logic.js"),
"${video_rendering_url}",
"${video_rendering_url}https%3A%2F%2Fcomponent_seller_url_2${post_"
"component_url_string}");
auto result = RunAuctionAndWait(auction_config);
GURL urn_url = GURL(result.ExtractString());
EXPECT_TRUE(urn_url.is_valid());
EXPECT_EQ(url::kUrnScheme, urn_url.scheme_piece());
TestFencedFrameURLMappingResultObserver observer;
ConvertFencedFrameURNToURL(urn_url, &observer);
EXPECT_TRUE(observer.mapped_url()) << urn_url;
EXPECT_EQ(ad_url, observer.mapped_url());
}
// This tests that replacements can be made within the hostname and in the path
// together.
IN_PROC_BROWSER_TEST_F(DeprecatedRenderURLReplacementsEnabledTest,
ReplacementWithinHostnameAndPath) {
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url = GURL("https://${host}/%%path%%https%3A%2F%2Fexample.com");
GURL expected_ad_url =
GURL("https://host/path?query=https%3A%2F%2Fexample.com");
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
/*owner=*/test_origin,
/*name=*/"cars",
/*priority=*/0.0, /*execution_mode=*/
blink::InterestGroup::ExecutionMode::kCompatibilityMode,
/*bidding_url=*/
embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"),
/*ads=*/{{{ad_url, R"({"ad":"metadata","here":[1,2]})"}}}));
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
deprecatedRenderURLReplacements: {$3: "host", $4: $5}
})",
test_origin,
embedded_https_test_server().GetURL("a.test",
"/interest_group/decision_logic.js"),
"${host}", "%%path%%", "path?query=");
auto result = RunAuctionAndWait(auction_config);
GURL urn_url = GURL(result.ExtractString());
EXPECT_TRUE(urn_url.is_valid());
EXPECT_EQ(url::kUrnScheme, urn_url.scheme_piece());
TestFencedFrameURLMappingResultObserver observer;
ConvertFencedFrameURNToURL(urn_url, &observer);
EXPECT_TRUE(observer.mapped_url()) << urn_url;
EXPECT_EQ(expected_ad_url, observer.mapped_url());
}
// This tests that replacements can be made within the hostname and the path,
// specifically that by adding a '/' within the replacements will still keep it
// valid.
IN_PROC_BROWSER_TEST_F(DeprecatedRenderURLReplacementsEnabledTest,
ReplacementWithinHostnameAndPathWithoutSlashSeperator) {
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url = GURL("https://${host}${path}example.com");
GURL expected_ad_url =
GURL("https://host/file.html?query=https%3A%2F%2Fexample.com/");
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
/*owner=*/test_origin,
/*name=*/"cars",
/*priority=*/0.0, /*execution_mode=*/
blink::InterestGroup::ExecutionMode::kCompatibilityMode,
/*bidding_url=*/
embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"),
/*ads=*/{{{ad_url, R"({"ad":"metadata","here":[1,2]})"}}}));
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
deprecatedRenderURLReplacements:{$3: $4, $5: $6}
})",
test_origin,
embedded_https_test_server().GetURL("a.test",
"/interest_group/decision_logic.js"),
"${host}", "host/file.html", "${path}", "?query=https%3A%2F%2F");
auto result = RunAuctionAndWait(auction_config);
GURL urn_url = GURL(result.ExtractString());
EXPECT_TRUE(urn_url.is_valid());
EXPECT_EQ(url::kUrnScheme, urn_url.scheme_piece());
TestFencedFrameURLMappingResultObserver observer;
ConvertFencedFrameURNToURL(urn_url, &observer);
EXPECT_TRUE(observer.mapped_url()) << urn_url;
EXPECT_EQ(expected_ad_url, observer.mapped_url());
}
// This tests for when a replacement is done within
// deprecatedRenderURLReplacements, that another can be done
// properly with deprecatedReplaceInURN.
IN_PROC_BROWSER_TEST_F(DeprecatedRenderURLReplacementsEnabledTest,
ChainingReplacements) {
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url = GURL("https://${video_rendering_url}");
GURL expected_ad_url = GURL(
"https://${video_rendering_url}/"
"https%3A%2F%2Fcomponent_seller_url_2%%post_component_url_string%%/");
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
/*owner=*/test_origin,
/*name=*/"cars",
/*priority=*/0.0, /*execution_mode=*/
blink::InterestGroup::ExecutionMode::kCompatibilityMode,
/*bidding_url=*/
embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"),
/*ads=*/{{{ad_url, R"({"ad":"metadata","here":[1,2]})"}}}));
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
deprecatedRenderURLReplacements: {$3: $4, "%%echo%%": "echo"}
})",
test_origin,
embedded_https_test_server().GetURL("a.test",
"/interest_group/decision_logic.js"),
"${video_rendering_url}",
"${video_rendering_url}/https%3A%2F%2Fcomponent_seller_url_2%%post_"
"component_url_string%%");
auto result = RunAuctionAndWait(auction_config);
GURL urn_url = GURL(result.ExtractString());
EXPECT_TRUE(urn_url.is_valid());
EXPECT_EQ(url::kUrnScheme, urn_url.scheme_piece());
{
TestFencedFrameURLMappingResultObserver observer;
ConvertFencedFrameURNToURL(urn_url, &observer);
EXPECT_TRUE(observer.mapped_url()) << urn_url;
EXPECT_EQ(expected_ad_url, observer.mapped_url());
}
EXPECT_TRUE(ReplaceInURNInJS(
urn_url, {{"${video_rendering_url}", "example.com/%%MORE_MACROS%%"},
{"%%post_component_url_string%%", "POST_COMPONENT_STUFF"}}));
GURL new_expected_url = GURL(
"https://example.com/%%MORE_MACROS%%/"
"https%3A%2F%2Fcomponent_seller_url_2POST_COMPONENT_STUFF/");
{
TestFencedFrameURLMappingResultObserver observer;
ConvertFencedFrameURNToURL(urn_url, &observer);
EXPECT_EQ(new_expected_url, observer.mapped_url());
}
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
PercentsInRenderURLHostnameThrowsTypeError) {
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
std::string origin_string = url::Origin::Create(url).Serialize();
ASSERT_TRUE(NavigateToURL(shell(), url));
AttachInterestGroupObserver();
const char kScriptTemplate[] = R"(
(async function() {
try {
await navigator.joinAdInterestGroup(
{
name: 'cars',
owner: $1,
ads: [{name:"foo",renderURL:$2}],
},
/*joinDurationSec=*/1000);
} catch (e) {
return e.toString();
}
return 'done';
})())";
EXPECT_EQ(base::StringPrintf(
"TypeError: Failed to execute 'joinAdInterestGroup' on "
"'Navigator': ad "
"renderURL 'https://%%%%host%%%%example.com' for "
"AuctionAdInterestGroup with "
"owner '%s' and name 'cars' cannot be resolved to a valid URL.",
origin_string.c_str()),
EvalJs(shell(), JsReplace(kScriptTemplate, origin_string.c_str(),
"https://%%host%%example.com")));
WaitForAccessObserved({});
}
// When trying to replace a render URL with deprecatedReplaceInURN, if the new
// URL is invalid, the URL will remain unchanged.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionWithWinnerInvalidReplacedURNDoesNotChange) {
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url = GURL("https://${video_rendering_url}");
GURL expected_ad_url = ad_url;
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
/*owner=*/test_origin,
/*name=*/"cars",
/*priority=*/0.0, /*execution_mode=*/
blink::InterestGroup::ExecutionMode::kCompatibilityMode,
/*bidding_url=*/
embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"),
/*ads=*/{{{ad_url, R"({"ad":"metadata","here":[1,2]})"}}}));
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
})",
test_origin,
embedded_https_test_server().GetURL("a.test",
"/interest_group/decision_logic.js"));
auto result = RunAuctionAndWait(auction_config,
/*execution_target=*/std::nullopt);
GURL urn_url = GURL(result.ExtractString());
EXPECT_TRUE(urn_url.is_valid());
EXPECT_EQ(url::kUrnScheme, urn_url.scheme_piece());
{
TestFencedFrameURLMappingResultObserver observer;
ConvertFencedFrameURNToURL(urn_url, &observer);
EXPECT_TRUE(observer.mapped_url()) << urn_url;
EXPECT_EQ(ad_url, observer.mapped_url());
}
EXPECT_TRUE(ReplaceInURNInJS(
urn_url, {{"${video_rendering_url}",
"${video_rendering_url}https%3A%2F%2Fcomponent_seller_url_2${"
"post_component_url_string}"}}));
{
TestFencedFrameURLMappingResultObserver observer;
ConvertFencedFrameURNToURL(urn_url, &observer);
EXPECT_EQ(expected_ad_url, observer.mapped_url());
}
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionWithWinnerReplacedURN) {
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url = GURL(embedded_https_test_server()
.GetURL("c.test", "/%%echo%%?${INTEREST_GROUP_NAME}")
.spec());
GURL expected_ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_cars");
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
/*owner=*/test_origin,
/*name=*/"cars",
/*priority=*/0.0, /*execution_mode=*/
blink::InterestGroup::ExecutionMode::kCompatibilityMode,
/*bidding_url=*/
embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"),
/*ads=*/{{{ad_url, R"({"ad":"metadata","here":[1,2]})"}}}));
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
})",
test_origin,
embedded_https_test_server().GetURL("a.test",
"/interest_group/decision_logic.js"));
auto result = RunAuctionAndWait(auction_config,
/*execution_target=*/std::nullopt);
GURL urn_url = GURL(result.ExtractString());
EXPECT_TRUE(urn_url.is_valid());
EXPECT_EQ(url::kUrnScheme, urn_url.scheme_piece());
{
TestFencedFrameURLMappingResultObserver observer;
ConvertFencedFrameURNToURL(urn_url, &observer);
EXPECT_TRUE(observer.mapped_url()) << urn_url;
EXPECT_EQ(ad_url, observer.mapped_url());
}
EXPECT_TRUE(ReplaceInURNInJS(
urn_url,
{{"${INTEREST_GROUP_NAME}", "render_cars"}, {"%%echo%%", "echo"}}));
{
TestFencedFrameURLMappingResultObserver observer;
ConvertFencedFrameURNToURL(urn_url, &observer);
EXPECT_EQ(expected_ad_url, observer.mapped_url());
}
NavigateIframeAndCheckURL(web_contents(), urn_url, expected_ad_url);
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
ReplaceURLFailsOnBadReplacementInput) {
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
GURL urn_url = GURL("urn:uuid:84a8bf15-8539-432d-bb9f-4eb20eaf400b");
std::string error;
EXPECT_FALSE(ReplaceInURNInJS(
urn_url, {{"${INTEREST_GROUP_NAME}", "render_cars"}, {"%echo%%", "echo"}},
&error));
EXPECT_THAT(error, HasSubstr("Replacements must be of the form "));
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
ReplaceURLFailsOnMalformedURN) {
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
GURL urn_url = GURL("http://test.com");
std::string error;
EXPECT_FALSE(ReplaceInURNInJS(
urn_url,
{{"${INTEREST_GROUP_NAME}", "render_cars"}, {"%%echo%%", "echo"}},
&error));
EXPECT_THAT(error, HasSubstr("Passed URL must be a valid URN URL."));
}
IN_PROC_BROWSER_TEST_F(
InterestGroupBrowserTest,
RunAdAuctionPerBuyerSignalsAndPerBuyerTimeoutsOriginNotInBuyers) {
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
AttachInterestGroupObserver();
GURL ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_cars");
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"))
.SetAds({{{ad_url, /*metadata=*/std::nullopt}}})
.Build()));
RunAuctionAndWaitForURLAndNavigateIframe(
JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
perBuyerSignals: {$1: {a:1}, 'https://not_in_buyers.com': {a:1}},
perBuyerTimeouts: {'https://not_in_buyers.com': 10000}
})",
test_origin,
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js")),
ad_url);
WaitForAccessObserved(
{{"global", TestInterestGroupObserver::kJoin, test_origin, "cars"},
{"1", TestInterestGroupObserver::kLoaded, test_origin, "cars"},
{"1", TestInterestGroupObserver::kBid, test_origin, "cars", 1.0},
{"1", TestInterestGroupObserver::kWin, test_origin, "cars"}});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, RunAdAuctionCancel) {
// Test cancelling an auction while it's still pending.
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_cars");
// This uses /hung as the script URL to avoid race with cancellation.
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"cars")
.SetBiddingUrl(
embedded_https_test_server().GetURL("a.test", "/hung"))
.SetAds({{{ad_url, R"({"ad":"metadata","here":[1,2]})"}}})
.Build()));
std::string auction_script = JsReplace(
R"(
let controller = new AbortController();
const config = {
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
signal: controller.signal
};
(async function() {
try {
let result = navigator.runAdAuction(config);
controller.abort('a reason');
return await result;
} catch (e) {
return e.toString();
}
})())",
test_origin,
embedded_https_test_server().GetURL("a.test",
"/interest_group/decision_logic.js"));
EXPECT_EQ("a reason", EvalJs(shell(), auction_script));
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, RunAdAuctionCancelLate) {
// Test cancelling an auction after it finished (which is a no-op).
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_cars");
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"))
.SetAds({{{ad_url, R"({"ad":"metadata","here":[1,2]})"}}})
.Build()));
std::string auction_script = JsReplace(
R"(
let controller = new AbortController();
const config = {
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
signal: controller.signal
};
(async function() {
try {
let result = await navigator.runAdAuction(config);
controller.abort();
return result;
} catch (e) {
return e.toString();
}
})())",
test_origin,
embedded_https_test_server().GetURL("a.test",
"/interest_group/decision_logic.js"));
auto result = EvalJs(shell(), auction_script);
GURL urn_url = GURL(result.ExtractString());
EXPECT_TRUE(urn_url.is_valid());
EXPECT_EQ(url::kUrnScheme, urn_url.scheme_piece());
TestFencedFrameURLMappingResultObserver observer;
ConvertFencedFrameURNToURL(urn_url, &observer);
EXPECT_EQ(ad_url, observer.mapped_url()->spec());
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, RunAdAuctionCancelBefore) {
// Test cancelling an auction before runAdAuction is even called.
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_cars");
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"))
.SetAds({{{ad_url, R"({"ad":"metadata","here":[1,2]})"}}})
.Build()));
std::string auction_script = JsReplace(
R"(
let controller = new AbortController();
const config = {
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
signal: controller.signal
};
(async function() {
try {
controller.abort();
return await navigator.runAdAuction(config);
} catch (e) {
return e.toString();
}
})())",
test_origin,
embedded_https_test_server().GetURL("a.test",
"/interest_group/decision_logic.js"));
EXPECT_EQ("AbortError: signal is aborted without reason",
EvalJs(shell(), auction_script));
}
// Runs an auction where the bidding function uses a WASM helper.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, RunAdAuctionWithBidderWasm) {
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_cars");
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic_use_wasm.js"))
.SetBiddingWasmHelperUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/multiply.wasm"))
.SetAds({{{ad_url, R"({"ad":"metadata","here":[1,2]})"}}})
.Build()));
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
})",
test_origin,
embedded_https_test_server().GetURL("a.test",
"/interest_group/decision_logic.js"));
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad_url);
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionWithDebugReporting) {
URLLoaderMonitor url_loader_monitor;
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad1_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_winner");
GURL ad2_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_bikes");
GURL ad3_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_shoes");
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"winner")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test",
"/interest_group/bidding_logic_with_debugging_report.js"))
.SetAds({{{ad1_url, /*metadata=*/std::nullopt}}})
.Build()));
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"bikes")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test",
"/interest_group/bidding_logic_with_debugging_report.js"))
.SetAds({{{ad2_url, /*metadata=*/std::nullopt}}})
.Build()));
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"shoes")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test",
"/interest_group/bidding_logic_with_debugging_report.js"))
.SetAds({{{ad3_url, /*metadata=*/std::nullopt}}})
.Build()));
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
auctionSignals: {x: 1},
sellerSignals: {yet: 'more', info: 1},
perBuyerSignals: {$1: {even: 'more', x: 4.5}}
})",
test_origin,
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic_with_debugging_report.js"));
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad1_url);
// Check ResourceRequest structs of report requests.
const auto kExpectedReportUrls = std::to_array<GURL>({
// Return value from seller's ReportResult() method.
embedded_https_test_server().GetURL("a.test", "/echoall?report_seller"),
// Return value from winning bidder's ReportWin() method.
embedded_https_test_server().GetURL("a.test",
"/echoall?report_bidder/winner"),
// Debugging report URL from seller for win report.
embedded_https_test_server().GetURL(
"a.test", "/echo?seller_debug_report_win/winner"),
// Debugging report URL from winning bidder for win report.
embedded_https_test_server().GetURL(
"a.test", "/echo?bidder_debug_report_win/winner"),
// Debugging report URL from seller for loss report.
embedded_https_test_server().GetURL(
"a.test", "/echo?seller_debug_report_loss/bikes"),
embedded_https_test_server().GetURL(
"a.test", "/echo?seller_debug_report_loss/shoes"),
// Debugging report URL from losing bidders for loss report.
embedded_https_test_server().GetURL(
"a.test", "/echo?bidder_debug_report_loss/bikes"),
embedded_https_test_server().GetURL(
"a.test", "/echo?bidder_debug_report_loss/shoes"),
});
for (const auto& expected_report_url : kExpectedReportUrls) {
SCOPED_TRACE(expected_report_url);
// Make sure the report URL was actually fetched over the network.
WaitForUrl(expected_report_url);
std::optional<network::ResourceRequest> request =
url_loader_monitor.WaitForUrl(expected_report_url);
ASSERT_TRUE(request);
EXPECT_EQ(network::mojom::CredentialsMode::kOmit,
request->credentials_mode);
EXPECT_EQ(network::mojom::RedirectMode::kError, request->redirect_mode);
EXPECT_EQ(test_origin, request->request_initiator);
EXPECT_TRUE(request->headers.IsEmpty());
ASSERT_TRUE(request->trusted_params);
const net::IsolationInfo& isolation_info =
request->trusted_params->isolation_info;
EXPECT_EQ(net::IsolationInfo::RequestType::kOther,
isolation_info.request_type());
EXPECT_TRUE(isolation_info.network_isolation_key().IsTransient());
EXPECT_TRUE(isolation_info.site_for_cookies().IsNull());
}
// The reporting requests should use different NIKs to prevent the requests
// from being correlated.
EXPECT_NE(url_loader_monitor.GetRequestInfo(kExpectedReportUrls[0])
->trusted_params->isolation_info.network_isolation_key(),
url_loader_monitor.GetRequestInfo(kExpectedReportUrls[2])
->trusted_params->isolation_info.network_isolation_key());
EXPECT_NE(url_loader_monitor.GetRequestInfo(kExpectedReportUrls[2])
->trusted_params->isolation_info.network_isolation_key(),
url_loader_monitor.GetRequestInfo(kExpectedReportUrls[3])
->trusted_params->isolation_info.network_isolation_key());
EXPECT_NE(url_loader_monitor.GetRequestInfo(kExpectedReportUrls[2])
->trusted_params->isolation_info.network_isolation_key(),
url_loader_monitor.GetRequestInfo(kExpectedReportUrls[4])
->trusted_params->isolation_info.network_isolation_key());
}
// All bidders' genereteBid() failed so no bid was made, thus no render url.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionWithDebugReportingNoBid) {
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad1_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_shoes");
GURL ad2_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_bikes");
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"shoes")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic_loop_forever.js"))
.SetAds({{{ad1_url, /*metadata=*/std::nullopt}}})
.Build()));
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"bikes")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic_throws.js"))
.SetAds({{{ad2_url, /*metadata=*/std::nullopt}}})
.Build()));
EXPECT_EQ(
base::Value(),
RunAuctionAndWait(JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
auctionSignals: {x: 1},
sellerSignals: {yet: 'more', info: 1},
})",
test_origin,
embedded_https_test_server().GetURL(
"a.test",
"/interest_group/decision_logic_with_debugging_report.js"))));
// Debugging loss reports which are made before generateBid()'s timeout and
// error-throwing statements should be sent. Those made after that should not
// be sent.
const GURL kExpectedReportUrls[] = {
// Debugging loss report URL (before the timeout) from bidder whose
// generateBid() timed out.
embedded_https_test_server().GetURL(
"a.test", "/echo?bidder_debug_report_loss/shoes/before_timeout"),
// Debugging loss report URL (before the error) from bidder whose
// generateBid() throws an error.
embedded_https_test_server().GetURL(
"a.test", "/echo?bidder_debug_report_loss/bikes/before_error")};
for (const auto& expected_report_url : kExpectedReportUrls) {
SCOPED_TRACE(expected_report_url);
WaitForUrl(expected_report_url);
}
}
// This test reproduces the crash reported in crbug.com/1451572.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, RunAdAuctionRepro1451572) {
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url =
embedded_https_test_server().GetURL("a.test", "/echo?render_cars");
const std::string repro_script = JsReplace(
R"(
(async function() {
// The crash occurs only if the second bid is filtered out before the first bid
// completes -- since the order this happens is arbitrary, the scenario is run
// several times to increase the chances of reproing the crash scenario.
//
// Two interest groups *are* required to repro the crash, and it's also required
// that only one of them has prioritization enabled and trusted signals set.
const ig_1= {
owner: $1,
name: "name_1",
biddingLogicURL: $3,
ads: [{renderURL: $1}],
};
const ig_2 = {
owner: $1,
name: "name_2",
// Intentionally use invalid bidding logic URL -- this results in the bid
// being filtered.
biddingLogicURL: $1,
ads: [{renderURL: $1}],
enableBiddingSignalsPrioritization: true,
trustedBiddingSignalsURL: $1
};
const config = {
seller: $1,
// Must have FLEDGE header and JS mime type.
decisionLogicURL: $2,
interestGroupBuyers: [$1]
};
for(let i = 0; i < 20; i++) {
await navigator.joinAdInterestGroup(ig_1, 200);
await navigator.joinAdInterestGroup(ig_2, 200);
await navigator.runAdAuction(config);
}
})())",
test_origin,
embedded_https_test_server().GetURL("a.test",
"/interest_group/decision_logic.js"),
embedded_https_test_server().GetURL("a.test",
"/interest_group/bidding_logic.js"));
EXPECT_EQ(base::Value(), EvalJs(shell(), repro_script));
}
// Test that the FLEDGE properly handles detached documents.
IN_PROC_BROWSER_TEST_F(InterestGroupFencedFrameBrowserTest,
DetachedDocumentDoesNotCrash) {
const char* kTestCases[] = {
R"(runAdAuction({
seller: "foo", // required
decisionLogicURL: "foo", // required
}, 0.0)
)",
R"(joinAdInterestGroup({
owner: "foo", // required
name: "foo", // required
})
)",
"leaveAdInterestGroup()",
"updateAdInterestGroups()",
"adAuctionComponents(1)",
R"(deprecatedURNToURL("foo")
)",
R"(deprecatedReplaceInURN("foo", {}))",
"canLoadAdAuctionFencedFrame()"};
GURL main_url =
embedded_https_test_server().GetURL("b.test", "/page_with_iframe.html");
for (const auto* test_case : kTestCases) {
ASSERT_TRUE(NavigateToURL(shell(), main_url));
EvalJsResult result = EvalJs(shell(), base::StringPrintf(R"(
try {
let child = document.getElementById("test_iframe");
const detachedNavigator = child.contentWindow.navigator;
child.remove();
detachedNavigator.%s;
} catch(e) {
}
"Did not crash"
)",
test_case));
EXPECT_EQ("Did not crash", result) << test_case;
}
}
// Can't do zero-argument leave of interest groups from http://localhost, even
// though it's a "secure context" (since it's potentially trustworthy).
IN_PROC_BROWSER_TEST_F(InterestGroupFencedFrameBrowserTest,
CantLeaveZeroArgHttpLocalhost) {
GURL test_url = embedded_test_server()->GetURL("/fenced_frames/basic.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
GURL resource_url = embedded_test_server()->GetURL(
"/set-header?Supports-Loading-Mode: fenced-frame");
// This is to get the raw exception string rather than the nicer
// (but more complicated) error message.
const char kLeaveScript[] = R"(
(async function() {
try {
await navigator.leaveAdInterestGroup();
return "success";
} catch (e) {
return e.toString();
}
})()
)";
NavigateFencedFrameAndWait(resource_url, resource_url, shell());
EXPECT_EQ(
"SecurityError: Failed to execute 'leaveAdInterestGroup' on 'Navigator': "
"May only leaveAdInterestGroup from an https origin.",
EvalJs(GetFencedFrameRenderFrameHost(shell()), kLeaveScript));
}
// Runs auction just like test InterestGroupBrowserTest.RunAdAuctionWithWinner,
// but runs with fenced frames enabled and expects to receive a URN URL to be
// used. After the auction, loads the URL in a fenced frame, and expects the
// correct URL is loaded.
IN_PROC_BROWSER_TEST_F(InterestGroupFencedFrameBrowserTest,
RunAdAuctionWithWinner) {
URLLoaderMonitor url_loader_monitor;
GURL test_url = embedded_https_test_server().GetURL(
"a.test", "/fenced_frames/basic.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url = embedded_https_test_server().GetURL(
"c.test", "/set-header?Supports-Loading-Mode: fenced-frame");
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"))
.SetTrustedBiddingSignalsUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/trusted_bidding_signals.json"))
.SetTrustedBiddingSignalsKeys({{"key1"}})
.SetAds({{{ad_url, R"({"ad":"metadata","here":[1,2]})"}}})
.Build()));
ASSERT_NO_FATAL_FAILURE(RunAuctionAndNavigateFencedFrame(
ad_url, JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
auctionSignals: {x: 1},
sellerSignals: {yet: 'more', info: 1},
perBuyerSignals: {$1: {even: 'more', x: 4.5}}
})",
test_origin,
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js"))));
// Check ResourceRequest structs of requests issued by the worklet process.
const struct ExpectedRequest {
GURL url;
const char* accept_header;
bool expect_trusted_params;
network::mojom::RequestMode expected_request_mode;
} kExpectedRequests[] = {
{embedded_https_test_server().GetURL("a.test",
"/interest_group/bidding_logic.js"),
"application/javascript", /*expect_trusted_params=*/true,
network::mojom::RequestMode::kNoCors},
{embedded_https_test_server().GetURL(
"a.test",
"/interest_group/trusted_bidding_signals.json"
"?hostname=a.test&keys=key1&interestGroupNames=cars"),
"application/json", /*expect_trusted_params=*/true,
network::mojom::RequestMode::kCors},
{embedded_https_test_server().GetURL("a.test",
"/interest_group/decision_logic.js"),
"application/javascript", /*expect_trusted_params=*/false,
network::mojom::RequestMode::kNoCors},
};
for (const auto& expected_request : kExpectedRequests) {
SCOPED_TRACE(expected_request.url);
std::optional<network::ResourceRequest> request =
url_loader_monitor.GetRequestInfo(expected_request.url);
ASSERT_TRUE(request);
EXPECT_EQ(network::mojom::CredentialsMode::kOmit,
request->credentials_mode);
EXPECT_EQ(network::mojom::RedirectMode::kError, request->redirect_mode);
EXPECT_EQ(test_origin, request->request_initiator);
EXPECT_EQ(1u, request->headers.GetHeaderVector().size());
EXPECT_THAT(request->headers.GetHeader(net::HttpRequestHeaders::kAccept),
testing::Optional(expected_request.accept_header));
EXPECT_EQ(expected_request.expect_trusted_params,
request->trusted_params.has_value());
EXPECT_EQ(expected_request.expected_request_mode, request->mode);
if (request->trusted_params) {
// Requests for interest-group provided URLs are cross-origin to the
// publisher page, and set trusted params to use the right cache shard,
// using a trusted URLLoaderFactory.
const net::IsolationInfo& isolation_info =
request->trusted_params->isolation_info;
EXPECT_EQ(net::IsolationInfo::RequestType::kOther,
isolation_info.request_type());
url::Origin expected_origin = url::Origin::Create(expected_request.url);
EXPECT_EQ(expected_origin, isolation_info.top_frame_origin());
EXPECT_TRUE(isolation_info.site_for_cookies().IsNull());
EXPECT_EQ(expected_origin, isolation_info.frame_origin());
}
}
// Check ResourceRequest structs of report requests.
const auto kExpectedReportUrls = std::to_array<GURL>({
embedded_https_test_server().GetURL("a.test", "/echoall?report_seller"),
embedded_https_test_server().GetURL("a.test", "/echoall?report_bidder"),
});
for (const auto& expected_report_url : kExpectedReportUrls) {
SCOPED_TRACE(expected_report_url);
// Make sure the report URL was actually fetched over the network.
WaitForUrl(expected_report_url);
std::optional<network::ResourceRequest> request =
url_loader_monitor.WaitForUrl(expected_report_url);
ASSERT_TRUE(request);
EXPECT_EQ(network::mojom::CredentialsMode::kOmit,
request->credentials_mode);
EXPECT_EQ(network::mojom::RedirectMode::kError, request->redirect_mode);
EXPECT_EQ(test_origin, request->request_initiator);
EXPECT_TRUE(request->headers.IsEmpty());
ASSERT_TRUE(request->trusted_params);
const net::IsolationInfo& isolation_info =
request->trusted_params->isolation_info;
EXPECT_EQ(net::IsolationInfo::RequestType::kOther,
isolation_info.request_type());
EXPECT_TRUE(isolation_info.network_isolation_key().IsTransient());
EXPECT_TRUE(isolation_info.site_for_cookies().IsNull());
}
// The two reporting requests should use different NIKs to prevent the
// requests from being correlated.
EXPECT_NE(url_loader_monitor.GetRequestInfo(kExpectedReportUrls[0])
->trusted_params->isolation_info.network_isolation_key(),
url_loader_monitor.GetRequestInfo(kExpectedReportUrls[1])
->trusted_params->isolation_info.network_isolation_key());
}
// Runs auction just like test
// InterestGroupBrowserTest.RunAdAuctionWithSizeWithWinner, but load the winning
// ad in a fenced frame and verify the ad size.
IN_PROC_BROWSER_TEST_F(InterestGroupFencedFrameBrowserTest,
RunAdAuctionWithSizeWithWinner) {
GURL test_url = embedded_https_test_server().GetURL(
"a.test", "/fenced_frames/basic.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url = embedded_https_test_server().GetURL(
"c.test", "/set-header?Supports-Loading-Mode: fenced-frame");
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic_with_size.js"))
.SetAds(/*ads=*/{{{ad_url, /*metadata=*/std::nullopt,
/*size_group=*/"group_1"}}})
.SetAdSizes(
{{{"size_1",
blink::AdSize(
100, blink::AdSize::LengthUnit::kScreenWidth, 50,
blink::AdSize::LengthUnit::kScreenHeight)}}})
.SetSizeGroups({{{"group_1", {"size_1"}}}})
.Build()));
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1]
})",
test_origin,
embedded_https_test_server().GetURL("a.test",
"/interest_group/decision_logic.js"));
ASSERT_NO_FATAL_FAILURE(
RunAuctionAndNavigateFencedFrame(ad_url, auction_config));
// Verify the ad is loaded with the size specified in the winning bid.
int screen_width = static_cast<int>(
display::Screen::Get()->GetPrimaryDisplay().GetSizeInPixel().width());
int screen_height = static_cast<int>(
0.5 *
display::Screen::Get()->GetPrimaryDisplay().GetSizeInPixel().height());
RenderFrameHost* ad_frame = GetFencedFrameRenderFrameHost(shell());
EXPECT_TRUE(WaitForLoadStop(web_contents()));
// Force layout.
EXPECT_TRUE(
ExecJs(ad_frame, "getComputedStyle(document.documentElement).width;"));
EXPECT_TRUE(
PollUntilEvalToTrue(JsReplace("innerWidth == $1 && innerHeight == $2",
screen_width, screen_height),
ad_frame));
}
IN_PROC_BROWSER_TEST_F(InterestGroupFencedFrameBrowserTest,
RunAdAuctionWithAdComponentWithSize) {
GURL test_url = embedded_https_test_server().GetURL(
"a.test", "/fenced_frames/basic.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
GURL ad_component_url = embedded_https_test_server().GetURL(
"d.test", "/set-header?Supports-Loading-Mode: fenced-frame");
GURL ad_url = embedded_https_test_server().GetURL(
"c.test", "/fenced_frames/basic.html");
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/url::Origin::Create(test_url),
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic_with_size.js"))
.SetAds(/*ads=*/{{{ad_url, /*metadata=*/std::nullopt,
/*size_group=*/"group_1"}}})
.SetAdComponents({{{ad_component_url, /*metadata=*/std::nullopt,
/*size_group=*/"group_2"}}})
.SetAdSizes(
{{{"size_1",
blink::AdSize(100, blink::AdSize::LengthUnit::kScreenWidth,
50,
blink::AdSize::LengthUnit::kScreenHeight)},
{"size_2",
blink::AdSize(50, blink::AdSize::LengthUnit::kPixels, 25,
blink::AdSize::LengthUnit::kPixels)}}})
.SetSizeGroups(
{{{"group_1", {"size_1"}}, {"group_2", {"size_2"}}}})
.Build()));
ASSERT_NO_FATAL_FAILURE(RunAuctionAndNavigateFencedFrame(
ad_url, JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1]
})",
url::Origin::Create(test_url),
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js"))));
RenderFrameHost* ad_frame = GetFencedFrameRenderFrameHost(shell());
// Verify the ad is loaded with the size specified in the winning bid.
int screen_width = static_cast<int>(
display::Screen::Get()->GetPrimaryDisplay().GetSizeInPixel().width());
int screen_height = static_cast<int>(
0.5 *
display::Screen::Get()->GetPrimaryDisplay().GetSizeInPixel().height());
EXPECT_TRUE(WaitForLoadStop(web_contents()));
// Force layout.
EXPECT_TRUE(
ExecJs(ad_frame, "getComputedStyle(document.documentElement).width;"));
EXPECT_TRUE(
PollUntilEvalToTrue(JsReplace("innerWidth == $1 && innerHeight == $2",
screen_width, screen_height),
ad_frame));
// Get the first component config from the fenced frame. Load it in the
// nested fenced frame. The load should succeed.
TestFrameNavigationObserver observer(GetFencedFrameRenderFrameHost(ad_frame));
EXPECT_TRUE(ExecJs(ad_frame, R"(
const configs = window.fence.getNestedConfigs();
document.querySelector('fencedframe').config = configs[0];
)"));
WaitForFencedFrameNavigation(ad_component_url, ad_frame, observer);
// Verify the ad component is loaded with the size specified in the winning
// bid.
RenderFrameHost* ad_component_frame = GetFencedFrameRenderFrameHost(ad_frame);
EXPECT_TRUE(WaitForLoadStop(web_contents()));
// Force layout.
EXPECT_TRUE(ExecJs(ad_component_frame,
"getComputedStyle(document.documentElement).width;"));
EXPECT_TRUE(PollUntilEvalToTrue(
JsReplace("innerWidth == $1 && innerHeight == $2", 50, 25),
ad_component_frame));
}
IN_PROC_BROWSER_TEST_F(InterestGroupFencedFrameBrowserTest,
RunAdAuctionWithWinnerReplacedURN) {
URLLoaderMonitor url_loader_monitor;
GURL test_url = embedded_https_test_server().GetURL(
"a.test", "/fenced_frames/basic.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url = embedded_https_test_server().GetURL(
"c.test", "/set-header?Supports-Loading-Mode: %%LOADING_MODE%%");
GURL expected_ad_url = embedded_https_test_server().GetURL(
"c.test", "/set-header?Supports-Loading-Mode: fenced-frame");
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"))
.SetTrustedBiddingSignalsUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/trusted_bidding_signals.json"))
.SetTrustedBiddingSignalsKeys({{"key1"}})
.SetAds({{{ad_url, R"({"ad":"metadata","here":[1,2]})"}}})
.Build()));
content::EvalJsResult urn_url_string =
RunAuctionAndWait(JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
auctionSignals: {x: 1},
sellerSignals: {yet: 'more', info: 1},
perBuyerSignals: {$1: {even: 'more', x: 4.5}}
})",
test_origin,
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js")),
shell());
GURL urn_url(urn_url_string.ExtractString());
ASSERT_TRUE(urn_url.is_valid())
<< "URL is not valid: " << urn_url_string.ExtractString();
EXPECT_EQ(url::kUrnScheme, urn_url.scheme_piece());
EXPECT_TRUE(
ReplaceInURNInJS(urn_url, {{"%%LOADING_MODE%%", "fenced-frame"}}));
NavigateFencedFrameAndWait(urn_url, expected_ad_url, shell());
}
// Test that `LeaveAdInterestGroup()` cannot be invoked without arguments in
// regular iframes. An error message should be shown.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
ArgumentsRequiredForLeaveGroupInRegularIframe) {
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
RenderFrameHost* iframe =
ChildFrameAt(web_contents()->GetPrimaryMainFrame(), 0);
WebContentsConsoleObserver console_observer(shell()->web_contents());
auto filter =
[](const content::WebContentsConsoleObserver::Message& message) {
return message.log_level == blink::mojom::ConsoleMessageLevel::kError;
};
console_observer.SetFilter(base::BindRepeating(filter));
EXPECT_TRUE(ExecJs(iframe, "navigator.leaveAdInterestGroup()"));
ASSERT_TRUE(console_observer.Wait());
ASSERT_FALSE(console_observer.messages().empty());
EXPECT_EQ(console_observer.GetMessageAt(0),
"Owner and name are required to call LeaveAdInterestGroup outside "
"of a fenced frame or an opaque origin iframe.");
}
// Test that `LeaveAdInterestGroup()` works in urn iframe that is same origin to
// the interset group owner.
IN_PROC_BROWSER_TEST_F(InterestGroupFencedFrameBrowserTest,
SameOriginUrnIframeLeaveGroupSucceed) {
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
// The ad url is same origin to the interest group owner.
GURL ad_url =
embedded_https_test_server().GetURL("a.test", "/echo?render_cars");
ASSERT_TRUE(test_origin.IsSameOriginWith(ad_url));
AttachInterestGroupObserver();
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"))
.SetAds({{{ad_url, /*metadata=*/std::nullopt}}})
.Build()));
ASSERT_NO_FATAL_FAILURE(RunAuctionAndWaitForURLAndNavigateIframe(
JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1]
})",
test_origin,
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js")),
ad_url));
// InterestGroupAccessObserver should see the join and auction.
WaitForAccessObserved(
{{"global", TestInterestGroupObserver::kJoin, test_origin, "cars"},
{"1", TestInterestGroupObserver::kLoaded, test_origin, "cars"},
{"1", TestInterestGroupObserver::kBid, test_origin, "cars", 1.0},
{"1", TestInterestGroupObserver::kWin, test_origin, "cars"}});
// Leaving the winning interest group should succeed. Do it by calling
// Javascript directly instead of loading a page that does this
// to avoid races with logging kBin or kWin.
RenderFrameHost* iframe =
ChildFrameAt(web_contents()->GetPrimaryMainFrame(), 0);
EXPECT_TRUE(ExecJs(iframe, "navigator.leaveAdInterestGroup()"));
// The leave should be observed.
WaitForAccessObserved(
{{"global", TestInterestGroupObserver::kLeave, test_origin, "cars"}});
EXPECT_TRUE(GetAllInterestGroups().empty());
}
// Test that `LeaveAdInterestGroup()` fails in urn iframe that is cross origin
// to the interset group owner.
IN_PROC_BROWSER_TEST_F(InterestGroupFencedFrameBrowserTest,
CrossOriginUrnIframeLeaveGroupFail) {
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
// The ad url is cross origin to the interest group owner.
GURL ad_url =
embedded_https_test_server().GetURL("b.test", "/echo?render_cars");
ASSERT_FALSE(test_origin.IsSameOriginWith(ad_url));
AttachInterestGroupObserver();
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"))
.SetAds({{{ad_url, /*metadata=*/std::nullopt}}})
.Build()));
ASSERT_NO_FATAL_FAILURE(RunAuctionAndWaitForURLAndNavigateIframe(
JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1]
})",
test_origin,
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js")),
ad_url));
// InterestGroupAccessObserver should see the join and auction.
WaitForAccessObserved(
{{"global", TestInterestGroupObserver::kJoin, test_origin, "cars"},
{"1", TestInterestGroupObserver::kLoaded, test_origin, "cars"},
{"1", TestInterestGroupObserver::kBid, test_origin, "cars", 1.0},
{"1", TestInterestGroupObserver::kWin, test_origin, "cars"}});
// Leaving the winning interest group should fail. Do it by calling Javascript
// directly instead of loading a page that does this to avoid races with
// logging kBin or kWin.
RenderFrameHost* iframe =
ChildFrameAt(web_contents()->GetPrimaryMainFrame(), 0);
EXPECT_TRUE(ExecJs(iframe, "navigator.leaveAdInterestGroup()"));
// No leave should be observed.
WaitForAccessObserved({});
EXPECT_EQ(1u, GetAllInterestGroups().size());
}
// Load the ad in an urn iframe. Test that `LeaveAdInterestGroup()` works from
// a nested regular iframe that is same origin to the interset group owner.
IN_PROC_BROWSER_TEST_F(InterestGroupFencedFrameBrowserTest,
LeaveGroupFromSameOriginNestedIframeSucceed) {
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
// The inner url is same origin to the interest group owner.
GURL inner_url = embedded_https_test_server().GetURL(
"a.test", "/set-header?Supports-Loading-Mode: fenced-frame");
ASSERT_TRUE(test_origin.IsSameOriginWith(inner_url));
// The ad url is cross origin to the interest group owner.
GURL ad_url = embedded_https_test_server().GetURL(
"b.test", "/fenced_frames/outer_inner_frame_as_param.html");
GURL::Replacements rep;
std::string query = "innerFrame=" + base::EscapeUrlEncodedData(
inner_url.spec(), /*use_plus=*/false);
rep.SetQueryStr(query);
ad_url = ad_url.ReplaceComponents(rep);
ASSERT_FALSE(test_origin.IsSameOriginWith(ad_url));
AttachInterestGroupObserver();
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"))
.SetAds({{{ad_url, /*metadata=*/std::nullopt}}})
.Build()));
ASSERT_NO_FATAL_FAILURE(RunAuctionAndWaitForURLAndNavigateIframe(
JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1]
})",
test_origin,
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js")),
ad_url));
// InterestGroupAccessObserver should see the join and auction.
WaitForAccessObserved(
{{"global", TestInterestGroupObserver::kJoin, test_origin, "cars"},
{"1", TestInterestGroupObserver::kLoaded, test_origin, "cars"},
{"1", TestInterestGroupObserver::kBid, test_origin, "cars", 1.0},
{"1", TestInterestGroupObserver::kWin, test_origin, "cars"}});
// Leaving the winning interest group from the nested iframe should succeed.
// Do it by calling Javascript directly instead of loading a page that does
// this to avoid races with logging kBin or kWin.
RenderFrameHost* iframe =
ChildFrameAt(web_contents()->GetPrimaryMainFrame(), 0);
RenderFrameHost* nested_iframe = ChildFrameAt(iframe, 0);
EXPECT_TRUE(ExecJs(nested_iframe, "navigator.leaveAdInterestGroup()"));
// The leave should be observed.
WaitForAccessObserved(
{{"global", TestInterestGroupObserver::kLeave, test_origin, "cars"}});
EXPECT_TRUE(GetAllInterestGroups().empty());
}
// Load the ad in an urn iframe. Test that `LeaveAdInterestGroup()` fails from
// a nested regular iframe that is cross origin to the interset group owner.
IN_PROC_BROWSER_TEST_F(InterestGroupFencedFrameBrowserTest,
LeaveGroupFromCrossOriginNestedIframeFail) {
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
// The inner url is cross origin to the interest group owner.
GURL inner_url = embedded_https_test_server().GetURL(
"b.test", "/set-header?Supports-Loading-Mode: fenced-frame");
ASSERT_FALSE(test_origin.IsSameOriginWith(inner_url));
// The ad url is cross origin to the interest group owner.
GURL ad_url = embedded_https_test_server().GetURL(
"b.test", "/fenced_frames/outer_inner_frame_as_param.html");
GURL::Replacements rep;
std::string query = "innerFrame=" + base::EscapeUrlEncodedData(
inner_url.spec(), /*use_plus=*/false);
rep.SetQueryStr(query);
ad_url = ad_url.ReplaceComponents(rep);
ASSERT_FALSE(test_origin.IsSameOriginWith(ad_url));
AttachInterestGroupObserver();
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"))
.SetAds({{{ad_url, /*metadata=*/std::nullopt}}})
.Build()));
ASSERT_NO_FATAL_FAILURE(RunAuctionAndWaitForURLAndNavigateIframe(
JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1]
})",
test_origin,
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js")),
ad_url));
// InterestGroupAccessObserver should see the join and auction.
WaitForAccessObserved(
{{"global", TestInterestGroupObserver::kJoin, test_origin, "cars"},
{"1", TestInterestGroupObserver::kLoaded, test_origin, "cars"},
{"1", TestInterestGroupObserver::kBid, test_origin, "cars", 1.0},
{"1", TestInterestGroupObserver::kWin, test_origin, "cars"}});
// Leaving the winning interest group from the nested iframe should fail.
// Do it by calling Javascript directly instead of loading a page that does
// this to avoid races with logging kBin or kWin.
RenderFrameHost* iframe =
ChildFrameAt(web_contents()->GetPrimaryMainFrame(), 0);
RenderFrameHost* nested_iframe = ChildFrameAt(iframe, 0);
EXPECT_TRUE(ExecJs(nested_iframe, "navigator.leaveAdInterestGroup()"));
// No leave should be observed.
WaitForAccessObserved({});
EXPECT_EQ(1u, GetAllInterestGroups().size());
}
// Runs two ad auctions with fenced frames enabled. Both auctions should
// succeed and are then loaded in separate fenced frames. Both auctions try to
// leave the interest group, but only the one whose ad matches the joining
// origin should succeed.
IN_PROC_BROWSER_TEST_F(InterestGroupFencedFrameBrowserTest,
RunTwoAdAuctionWithWinnerLeaveGroup) {
URLLoaderMonitor url_loader_monitor;
GURL test_url = embedded_https_test_server().GetURL(
"a.test",
base::StringPrintf("/cross_site_iframe_factory.html?a.test(%s,%s)",
base::EscapeUrlEncodedData(
embedded_https_test_server()
.GetURL("a.test", "/fenced_frames/basic.html")
.spec(),
/*use_plus=*/false)
.c_str(),
base::EscapeUrlEncodedData(
embedded_https_test_server()
.GetURL("b.test", "/fenced_frames/basic.html")
.spec(),
/*use_plus=*/false)
.c_str()));
ASSERT_TRUE(NavigateToURL(shell(), test_url));
RenderFrameHost* rfh1 =
ChildFrameAt(web_contents()->GetPrimaryMainFrame(), 0);
ASSERT_TRUE(rfh1);
RenderFrameHost* rfh2 =
ChildFrameAt(web_contents()->GetPrimaryMainFrame(), 1);
ASSERT_TRUE(rfh2);
url::Origin test_origin = url::Origin::Create(test_url);
AttachInterestGroupObserver();
GURL ad_url = embedded_https_test_server().GetURL(
"b.test", "/set-header?Supports-Loading-Mode: fenced-frame");
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test",
// Using bidding_logic_stop_bidding_after_win.js ensures the
// "cars" interest group wins the first auction (whose
// leaveAdInterestGroup call succeeds).
"/interest_group/bidding_logic_stop_bidding_after_win.js"))
.SetTrustedBiddingSignalsUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/trusted_bidding_signals.json"))
.SetTrustedBiddingSignalsKeys({{"key1"}})
.SetAds({{{ad_url, R"({"ad":"metadata","here":[1,2]})"}}})
.Build(),
rfh1));
GURL ad_url2 = embedded_https_test_server().GetURL(
"a.test", "/set-header?Supports-Loading-Mode: fenced-frame");
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"trucks")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"))
.SetTrustedBiddingSignalsUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/trusted_bidding_signals.json"))
.SetTrustedBiddingSignalsKeys({{"key1"}})
.SetAds({{{ad_url2, R"({"ad":"metadata","here":[1,2]})"}}})
.Build(),
rfh1));
ASSERT_NO_FATAL_FAILURE(RunAuctionAndNavigateFencedFrame(
ad_url,
JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
auctionSignals: {x: 1},
sellerSignals: {yet: 'more', info: 1},
perBuyerSignals: {$1: {even: 'more', x: 4.5}}
})",
test_origin,
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js")),
rfh1));
// InterestGroupAccessObserver should see the join and auction.
WaitForAccessObserved({
{"global", TestInterestGroupObserver::kJoin, test_origin, "cars"},
{"global", TestInterestGroupObserver::kJoin, test_origin, "trucks"},
{"1", TestInterestGroupObserver::kLoaded, test_origin, "trucks"},
{"1", TestInterestGroupObserver::kLoaded, test_origin, "cars"},
{"1", TestInterestGroupObserver::kBid, test_origin, "cars", 2.0},
{"1", TestInterestGroupObserver::kBid, test_origin, "trucks", 1.0},
{"1", TestInterestGroupObserver::kWin, test_origin, "cars"},
});
// Try to leave the winning interest group, which should fail, since the ad is
// on b.test, but the IG owner is a.test. Do the failed leave case first so
// that subsequent WaitForAccessObserved() calls would likely catch an
// unexpected leave event.
EXPECT_EQ(base::Value(), EvalJs(GetFencedFrameRenderFrameHost(rfh1),
"navigator.leaveAdInterestGroup()"));
ASSERT_NO_FATAL_FAILURE(RunAuctionAndNavigateFencedFrame(
ad_url2,
JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
auctionSignals: {x: 1},
sellerSignals: {yet: 'more', info: 1},
perBuyerSignals: {$1: {even: 'more', x: 4.5}}
})",
test_origin,
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js")),
rfh2));
// For the second auction, InterestGroupAccessObserver should see the two
// groups loaded, but just the truck group win. Do this before the leave
// attempt, as updating the data is potentially racy with the navigation
// committing, so the leave event could appear out of order.
WaitForAccessObserved(
{{"2", TestInterestGroupObserver::kLoaded, test_origin, "trucks"},
{"2", TestInterestGroupObserver::kLoaded, test_origin, "cars"},
{"2", TestInterestGroupObserver::kBid, test_origin, "trucks", 1.0},
{"2", TestInterestGroupObserver::kWin, test_origin, "trucks"}});
// Try to leave the winning interest group, which should succeed this time. Do
// it by calling Javascript directly instead of loading a page that does this
// to avoid races with logging kBin or kWin.
EXPECT_EQ(base::Value(), EvalJs(GetFencedFrameRenderFrameHost(rfh2),
"navigator.leaveAdInterestGroup()"));
WaitForAccessObserved(
{{"global", TestInterestGroupObserver::kLeave, test_origin, "trucks"}});
// Only the "truck" interest group should have been left.
auto groups = GetAllInterestGroups();
ASSERT_EQ(1u, groups.size());
EXPECT_EQ("cars", groups[0].name);
}
// Runs ad auction with fenced frames enabled. The auction should succeed and
// be loaded in a fenced frame. The displayed ad leaves the interest group
// from a nested iframe.
IN_PROC_BROWSER_TEST_F(InterestGroupFencedFrameBrowserTest,
RunAdAuctionWithWinnerNestedLeaveGroup) {
URLLoaderMonitor url_loader_monitor;
GURL test_url = embedded_https_test_server().GetURL(
"a.test", "/fenced_frames/basic.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
AttachInterestGroupObserver();
GURL inner_url = embedded_https_test_server().GetURL(
"a.test", "/set-header?Supports-Loading-Mode: fenced-frame");
GURL ad_url = embedded_https_test_server().GetURL(
"b.test", "/fenced_frames/outer_inner_frame_as_param.html");
GURL::Replacements rep;
std::string query = "innerFrame=" + base::EscapeUrlEncodedData(
inner_url.spec(), /*use_plus=*/false);
rep.SetQueryStr(query);
ad_url = ad_url.ReplaceComponents(rep);
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"))
.SetTrustedBiddingSignalsUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/trusted_bidding_signals.json"))
.SetTrustedBiddingSignalsKeys({{"key1"}})
.SetAds({{{ad_url, R"({"ad":"metadata","here":[1,2]})"}}})
.Build()));
ASSERT_NO_FATAL_FAILURE(RunAuctionAndNavigateFencedFrame(
ad_url, JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
auctionSignals: {x: 1},
sellerSignals: {yet: 'more', info: 1},
perBuyerSignals: {$1: {even: 'more', x: 4.5}}
})",
test_origin,
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js"))));
// InterestGroupAccessObserver should see the join and auction.
WaitForAccessObserved(
{{"global", TestInterestGroupObserver::kJoin, test_origin, "cars"},
{"1", TestInterestGroupObserver::kLoaded, test_origin, "cars"},
{"1", TestInterestGroupObserver::kBid, test_origin, "cars", 1.0},
{"1", TestInterestGroupObserver::kWin, test_origin, "cars"}});
// Leave the interest group and wait to observe the event. Do this after the
// above WaitForAccessObserved() call, as leaving is racy with recording the
// result of an auction.
EXPECT_EQ(base::Value(), EvalJs(GetFencedFrameRenderFrameHost(web_contents())
->child_at(0)
->current_frame_host(),
"navigator.leaveAdInterestGroup()"));
WaitForAccessObserved(
{{"global", TestInterestGroupObserver::kLeave, test_origin, "cars"}});
// The ad should have left the interest group when the page was shown.
EXPECT_EQ(0u, GetAllInterestGroups().size());
}
// Runs ad auction with fenced frames enabled. The auction should succeed and
// be loaded in a fenced frame. Then the fenced frame content performs a
// cross-origin navigation on itself, and the new document loads an iframe that
// is same-origin to the interest group. We leave the interest group from the
// iframe, and it should succeed.
IN_PROC_BROWSER_TEST_F(
InterestGroupFencedFrameBrowserTest,
RunAdAuctionWithWinnerLeaveGroupAfterRendererInitiatedNavigation) {
URLLoaderMonitor url_loader_monitor;
GURL test_url = embedded_https_test_server().GetURL(
"a.test", "/fenced_frames/basic.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
AttachInterestGroupObserver();
// First, load an ad urn-mapped to `test_url`.
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"))
.SetTrustedBiddingSignalsUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/trusted_bidding_signals.json"))
.SetTrustedBiddingSignalsKeys({{"key1"}})
.SetAds({{{test_url, R"({"ad":"metadata","here":[1,2]})"}}})
.Build()));
ASSERT_NO_FATAL_FAILURE(RunAuctionAndNavigateFencedFrame(
test_url, JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
auctionSignals: {x: 1},
sellerSignals: {yet: 'more', info: 1},
perBuyerSignals: {$1: {even: 'more', x: 4.5}}
})",
test_origin,
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js"))));
// Now perform a fenced frame content-initiated navigation to a cross-origin
// document that will load a same-origin (to the mapped url) iframe that will
// leave the interest group.
GURL inner_url = embedded_https_test_server().GetURL(
"a.test", "/fenced_frames/ad_that_leaves_interest_group.html");
GURL ad_url = embedded_https_test_server().GetURL(
"b.test", "/fenced_frames/outer_inner_frame_as_param.html");
GURL::Replacements rep;
std::string query = "innerFrame=" + base::EscapeUrlEncodedData(
inner_url.spec(), /*use_plus=*/false);
rep.SetQueryStr(query);
ad_url = ad_url.ReplaceComponents(rep);
RenderFrameHostImpl* ad_frame = GetFencedFrameRenderFrameHost(shell());
TestFrameNavigationObserver observer(ad_frame);
EXPECT_TRUE(ExecJs(ad_frame, JsReplace("document.location = $1;", ad_url)));
observer.Wait();
// Wait for the interest group to disappear.
WaitForInterestGroupsSatisfying(
test_origin, base::BindLambdaForTesting(
[](scoped_refptr<StorageInterestGroups> groups) -> bool {
return groups->size() == 0;
}));
}
// Creates a Fenced Frame and then tries to use the leaveAdInterestGroup API.
// Leaving the interest group should silently fail.
IN_PROC_BROWSER_TEST_F(InterestGroupFencedFrameBrowserTest,
LeaveAdInterestGroupNoAuction) {
URLLoaderMonitor url_loader_monitor;
GURL test_url = embedded_https_test_server().GetURL(
"a.test", "/fenced_frames/basic.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url = embedded_https_test_server().GetURL(
"a.test", "/fenced_frames/ad_that_leaves_interest_group.html");
AttachInterestGroupObserver();
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"))
.SetTrustedBiddingSignalsUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/trusted_bidding_signals.json"))
.SetTrustedBiddingSignalsKeys({{"key1"}})
.SetAds({{{ad_url, R"({"ad":"metadata","here":[1,2]})"}}})
.Build()));
// Navigate fenced frame with no ad.
ASSERT_NO_FATAL_FAILURE(NavigateFencedFrameAndWait(ad_url, ad_url, shell()));
// InterestGroupAccessObserver should see the join.
WaitForAccessObserved(
{{"global", TestInterestGroupObserver::kJoin, test_origin, "cars"}});
// The ad should not have left the interest group when the page was shown.
EXPECT_EQ(1u, GetAllInterestGroups().size());
}
// Use different origins for publisher, bidder, and seller, and make sure
// everything works as expected.
IN_PROC_BROWSER_TEST_F(InterestGroupFencedFrameBrowserTest, CrossOrigin) {
const char kPublisher[] = "a.test";
const char kBidder[] = "b.test";
const char kSeller[] = "c.test";
const char kAd[] = "d.test";
AttachInterestGroupObserver();
GURL ad_url = embedded_https_test_server().GetURL(
kAd, "/set-header?Supports-Loading-Mode: fenced-frame");
// Navigate to bidder site, and add an interest group.
GURL bidder_url = embedded_https_test_server().GetURL(kBidder, "/echo");
ASSERT_TRUE(NavigateToURL(shell(), bidder_url));
url::Origin bidder_origin = url::Origin::Create(bidder_url);
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/bidder_origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
kBidder, "/interest_group/bidding_logic.js"))
.SetTrustedBiddingSignalsUrl(embedded_https_test_server().GetURL(
kBidder, "/interest_group/trusted_bidding_signals.json"))
.SetTrustedBiddingSignalsKeys({{"key1"}})
.SetAds({{{ad_url, R"({"ad":"metadata","here":[1,2]})"}}})
.Build()));
// Navigate to publisher.
ASSERT_TRUE(
NavigateToURL(shell(), embedded_https_test_server().GetURL(
kPublisher, "/fenced_frames/basic.html")));
GURL seller_logic_url = embedded_https_test_server().GetURL(
kSeller, "/interest_group/decision_logic_need_signals.js");
// Register a seller script that only bids if the `trustedScoringSignals` are
// successfully fetched.
network_responder_->RegisterNetworkResponse(seller_logic_url.path(), R"(
function scoreAd(
adMetadata, bid, auctionConfig, trustedScoringSignals, browserSignals) {
// Reject bids if trustedScoringSignals is not received.
if (trustedScoringSignals.renderURL[browserSignals.renderURL] === "foo")
return bid;
return 0;
}
function reportResult(
auctionConfig, browserSignals) {
sendReportTo(auctionConfig.seller + '/echoall?report_seller');
return {
'success': true,
'signalsForWinner': {'signalForWinner': 1},
'reportUrl': auctionConfig.seller + '/report_seller',
};
}
)",
"application/javascript");
// Register seller signals with a value for `ad_url`.
GURL seller_signals_url = embedded_https_test_server().GetURL(
kSeller, "/trusted_scoring_signals.json");
network_responder_->RegisterNetworkResponse(
seller_signals_url.path(),
base::StringPrintf(R"({"renderUrls": {"%s": "foo"}})",
ad_url.spec().c_str()));
// Run an auction with the scoring script. It should succeed.
RunAuctionAndNavigateFencedFrame(
ad_url, JsReplace(
R"(
{
seller: $1,
decisionLogicURL: $2,
trustedScoringSignalsURL: $3,
interestGroupBuyers: [$4],
}
)",
url::Origin::Create(seller_logic_url), seller_logic_url,
seller_signals_url, bidder_origin));
WaitForAccessObserved({
{"global", TestInterestGroupObserver::kJoin, bidder_origin, "cars"},
{"1", TestInterestGroupObserver::kLoaded, bidder_origin, "cars"},
{"1", TestInterestGroupObserver::kBid, bidder_origin, "cars", 1.0},
{"1", TestInterestGroupObserver::kWin, bidder_origin, "cars"},
});
// Reporting urls should be fetched after an auction succeeded.
WaitForUrl(embedded_https_test_server().GetURL("/echoall?report_seller"));
WaitForUrl(embedded_https_test_server().GetURL("/echoall?report_bidder"));
// Double-check that the trusted scoring signals URL was requested as well.
WaitForUrl(embedded_https_test_server().GetURL(base::StringPrintf(
"/trusted_scoring_signals.json"
"?hostname=a.test"
"&renderUrls=%s",
base::EscapeQueryParamValue(ad_url.spec(), /*use_plus=*/false).c_str())));
}
// Test that ad_components in an iframe ad are requested.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionWinnerWithComponents) {
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url = embedded_https_test_server().GetURL(
"c.test", "/fenced_frames/ad_with_components.html");
GURL component_url =
embedded_https_test_server().GetURL("c.test", "/echo?component");
AttachInterestGroupObserver();
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"))
.SetTrustedBiddingSignalsUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/trusted_bidding_signals.json"))
.SetTrustedBiddingSignalsKeys({{"key1"}})
.SetAds({{{ad_url, R"({"ad":"metadata","here":[1,2]})"}}})
.SetAdComponents(
{{{component_url, R"({"ad":"component metadata"})"}}})
.Build()));
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
auctionSignals: {x: 1},
sellerSignals: {yet: 'more', info: 1},
perBuyerSignals: {$1: {even: 'more', x: 4.5}}
})",
test_origin,
embedded_https_test_server().GetURL("a.test",
"/interest_group/decision_logic.js"));
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad_url);
WaitForAccessObserved({
{"global", TestInterestGroupObserver::kJoin, test_origin, "cars"},
{"1", TestInterestGroupObserver::kLoaded, test_origin, "cars"},
{"1", TestInterestGroupObserver::kBid, test_origin, "cars", 1.0},
{"1", TestInterestGroupObserver::kWin, test_origin, "cars"},
});
// Wait for the component to load.
WaitForUrl(component_url);
}
// Make sure correct topFrameHostname is passed in. Check auctions from top
// frames, and iframes of various depth.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, TopFrameHostname) {
// Buyer, seller, and iframe all use the same host.
const char kOtherHost[] = "b.test";
// Top frame host is unique.
const char kTopFrameHost[] = "a.test";
// Navigate to bidder site, and add an interest group.
GURL other_url = embedded_https_test_server().GetURL(kOtherHost, "/echo");
url::Origin other_origin = url::Origin::Create(other_url);
ASSERT_TRUE(NavigateToURL(shell(), other_url));
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/other_origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
kOtherHost,
"/interest_group/bidding_logic_expect_top_frame_a_test.js"))
.SetAds({{{GURL("https://example.com/render"),
/*metadata=*/std::nullopt}}})
.Build()));
const struct {
int depth;
std::string top_frame_path;
const char* seller_path;
} kTestCases[] = {
{0, "/echo", "/interest_group/decision_logic_expect_top_frame_a_test.js"},
{1,
base::StringPrintf("/cross_site_iframe_factory.html?a.test(%s)",
kOtherHost),
"/interest_group/decision_logic_expect_top_frame_a_test.js"},
{2,
base::StringPrintf("/cross_site_iframe_factory.html?a.test(%s(%s))",
kOtherHost, kOtherHost),
"/interest_group/decision_logic_expect_top_frame_a_test.js"},
};
for (const auto& test_case : kTestCases) {
SCOPED_TRACE(test_case.depth);
// Navigate to publisher, with the cross-site iframe..
ASSERT_TRUE(
NavigateToURL(shell(), embedded_https_test_server().GetURL(
kTopFrameHost, test_case.top_frame_path)));
RenderFrameHost* frame = web_contents()->GetPrimaryMainFrame();
EXPECT_EQ(embedded_https_test_server().GetOrigin(kTopFrameHost),
frame->GetLastCommittedOrigin());
for (int i = 0; i < test_case.depth; ++i) {
frame = ChildFrameAt(frame, 0);
ASSERT_TRUE(frame);
EXPECT_EQ(other_origin, frame->GetLastCommittedOrigin());
}
// Run auction with a seller script with an "Access-Control-Allow-Origin"
// header. The auction should succeed.
GURL seller_logic_url =
embedded_https_test_server().GetURL(kOtherHost, test_case.seller_path);
ASSERT_EQ("https://example.com/render",
RunAuctionAndWaitForUrl(JsReplace(
R"(
{
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$3],
auctionSignals: {x: 1},
sellerSignals: {yet: 'more', info: 1},
perBuyerSignals: {$3: {even: 'more', x: 4.5}}
}
)",
url::Origin::Create(seller_logic_url),
seller_logic_url, other_origin),
frame));
}
}
// Test running auctions in cross-site iframes, and loading the winner into a
// nested fenced frame.
IN_PROC_BROWSER_TEST_F(InterestGroupFencedFrameBrowserTest, Iframe) {
// Use different hostnames for each participant.
const char kTopFrameHost[] = "a.test";
const char kBidderHost[] = "b.test";
const char kSellerHost[] = "c.test";
const char kIframeHost[] = "d.test";
const char kAdHost[] = "ad.d.test";
content_browser_client_->AddToAllowList({url::Origin::Create(
embedded_https_test_server().GetURL(kIframeHost, "/"))});
// Navigate to bidder site, and add an interest group.
GURL bidder_url = embedded_https_test_server().GetURL(kBidderHost, "/echo");
url::Origin bidder_origin = url::Origin::Create(bidder_url);
ASSERT_TRUE(NavigateToURL(shell(), bidder_url));
GURL ad_url = embedded_https_test_server().GetURL(
kAdHost, "/set-header?Supports-Loading-Mode: fenced-frame");
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
/*owner=*/bidder_origin,
/*name=*/"cars",
/*priority=*/0.0,
/*execution_mode=*/
blink::InterestGroup::ExecutionMode::kCompatibilityMode,
/*bidding_url=*/
embedded_https_test_server().GetURL(
kBidderHost, "/interest_group/bidding_logic.js"),
/*ads=*/{{{ad_url, /*metadata=*/std::nullopt}}}));
GURL main_frame_url = embedded_https_test_server().GetURL(
kTopFrameHost,
base::StringPrintf("/cross_site_iframe_factory.html?%s(%s)",
kTopFrameHost,
embedded_https_test_server()
.GetURL(kIframeHost, "/fenced_frames/basic.html")
.spec()
.c_str()));
ASSERT_TRUE(NavigateToURL(shell(), main_frame_url));
RenderFrameHost* iframe =
ChildFrameAt(web_contents()->GetPrimaryMainFrame(), 0);
ASSERT_TRUE(iframe);
EXPECT_EQ(kIframeHost, iframe->GetLastCommittedOrigin().host());
GURL seller_logic_url = embedded_https_test_server().GetURL(
kSellerHost, "/interest_group/decision_logic.js");
ASSERT_NO_FATAL_FAILURE(RunAuctionAndNavigateFencedFrame(
ad_url,
JsReplace(
R"(
{
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$3]
}
)",
url::Origin::Create(seller_logic_url), seller_logic_url,
bidder_origin),
iframe));
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionWithWinnerManyInterestGroups) {
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad1_url = embedded_https_test_server().GetURL(
"c.test", "/echo?stop_bidding_after_win");
GURL ad2_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_bikes");
GURL ad3_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_shoes");
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test",
"/interest_group/bidding_logic_stop_bidding_after_win.js"))
.SetAds({{{ad1_url, /*metadata=*/std::nullopt}}})
.Build()));
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"bikes")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"))
.SetTrustedBiddingSignalsUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/trusted_bidding_signals.json"))
.SetTrustedBiddingSignalsKeys({{"key1"}})
.SetAds({{{ad2_url, /*metadata=*/std::nullopt}}})
.Build()));
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"shoes")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"))
.SetAds({{{ad3_url, /*metadata=*/std::nullopt}}})
.Build()));
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"jetskis")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"))
.Build()));
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1, $3],
})",
test_origin,
embedded_https_test_server().GetURL("a.test",
"/interest_group/decision_logic.js"));
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad1_url);
// Seller and winning bidder should get reports, and other bidders shouldn't
// get reports.
WaitForUrl(embedded_https_test_server().GetURL("/echoall?report_seller"));
WaitForUrl(embedded_https_test_server().GetURL(
"/echoall?report_bidder_stop_bidding_after_win&cars"));
base::AutoLock auto_lock(requests_lock_);
EXPECT_FALSE(base::Contains(
received_https_test_server_requests_,
embedded_https_test_server().GetURL("/echoall?report_bidder")));
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, RunAdAuctionAllGroupsLimited) {
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad1_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_cars");
GURL ad2_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_bikes");
GURL ad3_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_shoes");
AttachInterestGroupObserver();
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"cars")
.SetPriority(2.3)
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"))
.SetAds({{{ad1_url, /*metadata=*/std::nullopt}}})
.Build()));
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"bikes")
.SetPriority(2.2)
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"))
.SetTrustedBiddingSignalsUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/trusted_bidding_signals.json"))
.SetTrustedBiddingSignalsKeys({{"key1"}})
.SetAds({{{ad2_url, /*metadata=*/std::nullopt}}})
.Build()));
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"shoes")
.SetPriority(2.1)
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"))
.SetAds({{{ad3_url, /*metadata=*/std::nullopt}}})
.Build()));
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
perBuyerGroupLimits: {'*': 1},
})",
test_origin,
embedded_https_test_server().GetURL("a.test",
"/interest_group/decision_logic.js"));
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad1_url);
WaitForAccessObserved({
{"global", TestInterestGroupObserver::kJoin, test_origin, "cars"},
{"global", TestInterestGroupObserver::kJoin, test_origin, "bikes"},
{"global", TestInterestGroupObserver::kJoin, test_origin, "shoes"},
{"1", TestInterestGroupObserver::kLoaded, test_origin, "shoes"},
{"1", TestInterestGroupObserver::kLoaded, test_origin, "bikes"},
{"1", TestInterestGroupObserver::kLoaded, test_origin, "cars"},
{"1", TestInterestGroupObserver::kBid, test_origin, "cars", 1.0},
{"1", TestInterestGroupObserver::kWin, test_origin, "cars"},
});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, RunAdAuctionOneGroupLimited) {
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
GURL test_url2 =
embedded_https_test_server().GetURL("b.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
url::Origin test_origin2 = url::Origin::Create(test_url2);
GURL ad1_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_cars");
GURL ad2_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_bikes");
GURL ad3_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_shoes");
AttachInterestGroupObserver();
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"cars")
.SetPriority(3)
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test",
"/interest_group/bidding_logic_stop_bidding_after_win.js"))
.SetAds({{{ad1_url, /*metadata=*/std::nullopt}}})
.Build()));
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"bikes")
.SetPriority(2)
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"))
.SetTrustedBiddingSignalsUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/trusted_bidding_signals.json"))
.SetTrustedBiddingSignalsKeys({{"key1"}})
.SetAds({{{ad2_url, /*metadata=*/std::nullopt}}})
.Build()));
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"shoes")
.SetPriority(1)
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"))
.SetAds({{{ad3_url, /*metadata=*/std::nullopt}}})
.Build()));
ASSERT_TRUE(NavigateToURL(shell(), test_url2));
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin2,
/*name=*/"cars")
.SetPriority(3)
.SetBiddingUrl(embedded_https_test_server().GetURL(
"b.test", "/interest_group/bidding_logic.js"))
.SetAds({{{ad1_url, /*metadata=*/std::nullopt}}})
.Build()));
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin2,
/*name=*/"bikes")
.SetPriority(2)
.SetBiddingUrl(embedded_https_test_server().GetURL(
"b.test", "/interest_group/bidding_logic.js"))
.SetTrustedBiddingSignalsUrl(embedded_https_test_server().GetURL(
"b.test", "/interest_group/trusted_bidding_signals.json"))
.SetTrustedBiddingSignalsKeys({{"key1"}})
.SetAds({{{ad2_url, /*metadata=*/std::nullopt}}})
.Build()));
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin2,
/*name=*/"shoes")
.SetPriority(1)
.SetBiddingUrl(embedded_https_test_server().GetURL(
"b.test", "/interest_group/bidding_logic.js"))
.SetAds({{{ad3_url, /*metadata=*/std::nullopt}}})
.Build()));
std::string auction_config = JsReplace(
R"({
seller: $3,
decisionLogicURL: $2,
interestGroupBuyers: [$1, $3],
perBuyerGroupLimits: {$1: 1, '*': 2},
})",
test_origin,
embedded_https_test_server().GetURL("b.test",
"/interest_group/decision_logic.js"),
test_origin2);
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad1_url);
WaitForAccessObserved({
{"global", TestInterestGroupObserver::kJoin, test_origin, "cars"},
{"global", TestInterestGroupObserver::kJoin, test_origin, "bikes"},
{"global", TestInterestGroupObserver::kJoin, test_origin, "shoes"},
{"global", TestInterestGroupObserver::kJoin, test_origin2, "cars"},
{"global", TestInterestGroupObserver::kJoin, test_origin2, "bikes"},
{"global", TestInterestGroupObserver::kJoin, test_origin2, "shoes"},
{"1", TestInterestGroupObserver::kLoaded, test_origin, "shoes"},
{"1", TestInterestGroupObserver::kLoaded, test_origin, "bikes"},
{"1", TestInterestGroupObserver::kLoaded, test_origin, "cars"},
{"1", TestInterestGroupObserver::kLoaded, test_origin2, "shoes"},
{"1", TestInterestGroupObserver::kLoaded, test_origin2, "bikes"},
{"1", TestInterestGroupObserver::kLoaded, test_origin2, "cars"},
{"1", TestInterestGroupObserver::kBid, test_origin, "cars", 2.0},
{"1", TestInterestGroupObserver::kBid, test_origin2, "bikes", 1.0},
{"1", TestInterestGroupObserver::kBid, test_origin2, "cars", 1.0},
{"1", TestInterestGroupObserver::kWin, test_origin, "cars"},
});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionOneGroupHighLimit) {
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
GURL test_url2 =
embedded_https_test_server().GetURL("b.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
url::Origin test_origin2 = url::Origin::Create(test_url2);
GURL ad1_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_cars");
GURL ad2_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_bikes");
GURL ad3_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_shoes");
AttachInterestGroupObserver();
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"cars")
.SetPriority(3)
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test",
"/interest_group/bidding_logic_stop_bidding_after_win.js"))
.SetAds({{{ad1_url, /*metadata=*/std::nullopt}}})
.Build()));
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"bikes")
.SetPriority(2)
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"))
.SetTrustedBiddingSignalsUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/trusted_bidding_signals.json"))
.SetTrustedBiddingSignalsKeys({{"key1"}})
.SetAds({{{ad2_url, /*metadata=*/std::nullopt}}})
.Build()));
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"shoes")
.SetPriority(1)
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"))
.SetAds({{{ad3_url, /*metadata=*/std::nullopt}}})
.Build()));
ASSERT_TRUE(NavigateToURL(shell(), test_url2));
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin2,
/*name=*/"cars")
.SetPriority(3)
.SetBiddingUrl(embedded_https_test_server().GetURL(
"b.test", "/interest_group/bidding_logic.js"))
.SetAds({{{ad1_url, /*metadata=*/std::nullopt}}})
.Build()));
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin2,
/*name=*/"bikes")
.SetPriority(2)
.SetBiddingUrl(embedded_https_test_server().GetURL(
"b.test", "/interest_group/bidding_logic.js"))
.SetTrustedBiddingSignalsUrl(embedded_https_test_server().GetURL(
"b.test", "/interest_group/trusted_bidding_signals.json"))
.SetTrustedBiddingSignalsKeys({{"key1"}})
.SetAds({{{ad2_url, /*metadata=*/std::nullopt}}})
.Build()));
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin2,
/*name=*/"shoes")
.SetPriority(1)
.SetBiddingUrl(embedded_https_test_server().GetURL(
"b.test", "/interest_group/bidding_logic.js"))
.SetAds({{{ad3_url, /*metadata=*/std::nullopt}}})
.Build()));
std::string auction_config = JsReplace(
R"({
seller: $3,
decisionLogicURL: $2,
interestGroupBuyers: [$1, $3],
perBuyerGroupLimits: {$3: 3, '*': 1},
})",
test_origin,
embedded_https_test_server().GetURL("b.test",
"/interest_group/decision_logic.js"),
test_origin2);
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad1_url);
WaitForAccessObserved({
{"global", TestInterestGroupObserver::kJoin, test_origin, "cars"},
{"global", TestInterestGroupObserver::kJoin, test_origin, "bikes"},
{"global", TestInterestGroupObserver::kJoin, test_origin, "shoes"},
{"global", TestInterestGroupObserver::kJoin, test_origin2, "cars"},
{"global", TestInterestGroupObserver::kJoin, test_origin2, "bikes"},
{"global", TestInterestGroupObserver::kJoin, test_origin2, "shoes"},
{"1", TestInterestGroupObserver::kLoaded, test_origin, "shoes"},
{"1", TestInterestGroupObserver::kLoaded, test_origin, "bikes"},
{"1", TestInterestGroupObserver::kLoaded, test_origin, "cars"},
{"1", TestInterestGroupObserver::kLoaded, test_origin2, "shoes"},
{"1", TestInterestGroupObserver::kLoaded, test_origin2, "bikes"},
{"1", TestInterestGroupObserver::kLoaded, test_origin2, "cars"},
{"1", TestInterestGroupObserver::kBid, test_origin, "cars", 2.0},
{"1", TestInterestGroupObserver::kBid, test_origin2, "bikes", 1.0},
{"1", TestInterestGroupObserver::kBid, test_origin2, "cars", 1.0},
{"1", TestInterestGroupObserver::kBid, test_origin2, "shoes", 1.0},
{"1", TestInterestGroupObserver::kWin, test_origin, "cars"},
});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionGroupLimitRandomized) {
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_ad");
std::vector<std::pair<std::string, double>> interest_groups = {
{"cars", 3},
{"motorcycles", 2},
{"bikes", 2},
{"shoes", 1},
{"scooters", 2}};
for (const auto& g : interest_groups) {
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/g.first)
.SetPriority(g.second)
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test",
"/interest_group/bidding_logic_stop_bidding_after_win.js"))
.SetAds({{{ad_url, /*metadata=*/std::nullopt}}})
.Build()));
}
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
perBuyerGroupLimits: {'*': 3},
})",
test_origin,
embedded_https_test_server().GetURL("a.test",
"/interest_group/decision_logic.js"));
std::vector<GURL> expected_urls = {
embedded_https_test_server().GetURL(
"a.test", "/echoall?report_bidder_stop_bidding_after_win&cars"),
embedded_https_test_server().GetURL(
"a.test",
"/echoall?report_bidder_stop_bidding_after_win&motorcycles"),
embedded_https_test_server().GetURL(
"a.test", "/echoall?report_bidder_stop_bidding_after_win&bikes"),
embedded_https_test_server().GetURL(
"a.test", "/echoall?report_bidder_stop_bidding_after_win&scooters"),
};
while (!HasServerSeenUrls(expected_urls)) {
EvalJsResult result = RunAuctionAndWait(auction_config);
// Some auctions will have no winner, depending on which interest groups
// were chosen to participate. No need to do anything more for those.
if (!result.is_ok() || result == base::Value()) {
continue;
}
// For other auctions, navigate iframe to winning URN to trigger reports.
// This should happen exactly 4 times, so shouldn't slow the test down too
// much.
NavigateIframeAndCheckURL(web_contents(), GURL(result.ExtractString()),
ad_url);
}
EXPECT_FALSE(HasServerSeenUrl(embedded_https_test_server().GetURL(
"a.test", "/echoall?report_bidder_stop_bidding_after_win&shoes")));
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, RunAdAuctionMultipleAuctions) {
GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
const url::Origin origin = url::Origin::Create(test_url);
GURL ad1_url = embedded_https_test_server().GetURL(
"c.test", "/echo?stop_bidding_after_win");
GURL ad2_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_shoes");
// This group will win if it has never won an auction.
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test",
"/interest_group/bidding_logic_stop_bidding_after_win.js"))
.SetAds({{{ad1_url, R"({"ad":"metadata","here":[1,2]})"}}})
.Build()));
GURL test_url2 =
embedded_https_test_server().GetURL("b.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url2));
const url::Origin origin2 = url::Origin::Create(test_url2);
// This group will win if the other interest group has won an auction.
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/origin2,
/*name=*/"shoes")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"b.test", "/interest_group/bidding_logic.js"))
.SetAds({{{ad2_url, /*metadata=*/std::nullopt}}})
.Build()));
// Both owners have one interest group in storage, and both interest groups
// have no `prev_wins`.
auto storage_interest_groups = GetInterestGroupsForOwner(origin);
EXPECT_EQ(storage_interest_groups->size(), 1u);
EXPECT_EQ(storage_interest_groups->GetInterestGroups()[0]
->bidding_browser_signals->prev_wins.size(),
0u);
EXPECT_EQ(storage_interest_groups->GetInterestGroups()[0]
->bidding_browser_signals->bid_count,
0);
auto storage_interest_groups2 = GetInterestGroupsForOwner(origin2);
EXPECT_EQ(storage_interest_groups2->size(), 1u);
EXPECT_EQ(storage_interest_groups2->GetInterestGroups()[0]
->bidding_browser_signals->prev_wins.size(),
0u);
EXPECT_EQ(storage_interest_groups2->GetInterestGroups()[0]
->bidding_browser_signals->bid_count,
0);
// Start observer after joins.
AttachInterestGroupObserver();
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1, $3],
})",
origin2,
embedded_https_test_server().GetURL("b.test",
"/interest_group/decision_logic.js"),
origin);
// Run an ad auction. Interest group cars of owner `test_url` wins.
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad1_url);
// Wait for interest groups to be updated. Interest groups are updated
// during/after commit, so this test is potentially racy without this.
WaitForAccessObserved(
{{"1", TestInterestGroupObserver::kLoaded, origin2, "shoes"},
{"1", TestInterestGroupObserver::kLoaded, origin, "cars"},
{"1", TestInterestGroupObserver::kBid, origin, "cars", 2.0},
{"1", TestInterestGroupObserver::kBid, origin2, "shoes", 1.0},
{"1", TestInterestGroupObserver::kWin, origin, "cars"}});
// `prev_wins` of `test_url`'s interest group cars is updated in storage.
storage_interest_groups = GetInterestGroupsForOwner(origin);
storage_interest_groups2 = GetInterestGroupsForOwner(origin2);
// Remove the above two loads from the observer.
WaitForAccessObserved(
{{"global", TestInterestGroupObserver::kLoaded, origin, "cars"},
{"global", TestInterestGroupObserver::kLoaded, origin2, "shoes"}});
EXPECT_EQ(storage_interest_groups->GetInterestGroups()[0]
->bidding_browser_signals->prev_wins.size(),
1u);
EXPECT_EQ(storage_interest_groups2->GetInterestGroups()[0]
->bidding_browser_signals->prev_wins.size(),
0u);
EXPECT_EQ(storage_interest_groups->GetInterestGroups()[0]
->bidding_browser_signals->prev_wins.front()
->ad_json,
JsReplace(R"({"metadata":"{\"ad\":\"metadata\",\"here\":[1,2]}",)"
R"("renderURL":$1})",
ad1_url));
EXPECT_EQ(storage_interest_groups->GetInterestGroups()[0]
->bidding_browser_signals->bid_count,
1);
EXPECT_EQ(storage_interest_groups2->GetInterestGroups()[0]
->bidding_browser_signals->bid_count,
1);
// Run auction again. Interest group shoes of owner `test_url2` wins.
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad2_url);
// Need to wait again.
WaitForAccessObserved(
{{"2", TestInterestGroupObserver::kLoaded, origin2, "shoes"},
{"2", TestInterestGroupObserver::kLoaded, origin, "cars"},
{"2", TestInterestGroupObserver::kBid, origin2, "shoes", 1.0},
{"2", TestInterestGroupObserver::kWin, origin2, "shoes"}});
// `test_url2`'s interest group shoes has one `prev_wins` in storage.
storage_interest_groups = GetInterestGroupsForOwner(origin);
storage_interest_groups2 = GetInterestGroupsForOwner(origin2);
// Remove the above two loads from the observer.
WaitForAccessObserved(
{{"global", TestInterestGroupObserver::kLoaded, origin, "cars"},
{"global", TestInterestGroupObserver::kLoaded, origin2, "shoes"}});
EXPECT_EQ(storage_interest_groups->GetInterestGroups()[0]
->bidding_browser_signals->prev_wins.size(),
1u);
EXPECT_EQ(storage_interest_groups2->GetInterestGroups()[0]
->bidding_browser_signals->prev_wins.size(),
1u);
EXPECT_EQ(storage_interest_groups2->GetInterestGroups()[0]
->bidding_browser_signals->prev_wins.front()
->ad_json,
JsReplace(R"({"renderURL":$1})", ad2_url));
// First interest group didn't bid this time.
EXPECT_EQ(storage_interest_groups->GetInterestGroups()[0]
->bidding_browser_signals->bid_count,
1);
EXPECT_EQ(storage_interest_groups2->GetInterestGroups()[0]
->bidding_browser_signals->bid_count,
2);
// Run auction third time, and only interest group "shoes" bids this time.
auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
})",
origin2,
embedded_https_test_server().GetURL("b.test",
"/interest_group/decision_logic.js"));
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad2_url);
// Need to wait again.
WaitForAccessObserved({
{"3", TestInterestGroupObserver::kLoaded, origin2, "shoes"},
{"3", TestInterestGroupObserver::kBid, origin2, "shoes", 1.0},
{"3", TestInterestGroupObserver::kWin, origin2, "shoes"},
});
// `test_url2`'s interest group shoes has two `prev_wins` in storage.
storage_interest_groups = GetInterestGroupsForOwner(origin);
storage_interest_groups2 = GetInterestGroupsForOwner(origin2);
EXPECT_EQ(storage_interest_groups->GetInterestGroups()[0]
->bidding_browser_signals->prev_wins.size(),
1u);
EXPECT_EQ(storage_interest_groups2->GetInterestGroups()[0]
->bidding_browser_signals->prev_wins.size(),
2u);
EXPECT_EQ(storage_interest_groups2->GetInterestGroups()[0]
->bidding_browser_signals->prev_wins.back()
->ad_json,
JsReplace(R"({"renderURL":$1})", ad2_url));
// First interest group didn't bid this time.
EXPECT_EQ(storage_interest_groups->GetInterestGroups()[0]
->bidding_browser_signals->bid_count,
1);
EXPECT_EQ(storage_interest_groups2->GetInterestGroups()[0]
->bidding_browser_signals->bid_count,
3);
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, ReportingMultipleAuctions) {
URLLoaderMonitor url_loader_monitor;
GURL test_url_a = embedded_https_test_server().GetURL("a.test", "/echo");
ASSERT_TRUE(NavigateToURL(shell(), test_url_a));
const url::Origin origin_a = url::Origin::Create(test_url_a);
GURL ad1_url = embedded_https_test_server().GetURL(
"c.test", "/echo?stop_bidding_after_win");
GURL ad2_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_shoes");
// This group will win if it has never won an auction.
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/origin_a,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test",
"/interest_group/bidding_logic_stop_bidding_after_win.js"))
.SetAds({{{ad1_url, R"({"ad":"metadata","here":[1,2]})"}}})
.Build()));
GURL test_url_b =
embedded_https_test_server().GetURL("b.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url_b));
const url::Origin origin_b = url::Origin::Create(test_url_b);
// This group will win if the other interest group has won an auction.
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/origin_b,
/*name=*/"shoes")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"b.test",
"/interest_group/bidding_logic_with_debugging_report.js"))
.SetAds({{{ad2_url, /*metadata=*/std::nullopt}}})
.Build()));
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1, $3],
})",
origin_b,
embedded_https_test_server().GetURL("b.test",
"/interest_group/decision_logic.js"),
origin_a);
// Setting a small reporting interval to run the test faster.
manager_->set_reporting_interval_for_testing(base::Milliseconds(1));
// Run an ad auction. Interest group cars of owner `test_url_a` wins.
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad1_url);
// Wait for database to be updated with the win, which may happen after the
// auction completes.
WaitForInterestGroupsSatisfying(
origin_a, base::BindLambdaForTesting(
[](scoped_refptr<StorageInterestGroups> groups) -> bool {
EXPECT_EQ(1u, groups->size());
return groups->GetInterestGroups()[0]
->bidding_browser_signals->prev_wins.size() ==
1u;
}));
// Run auction again on the same page. Interest group shoes of owner
// `test_url2` wins.
auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1, $3],
})",
origin_b,
embedded_https_test_server().GetURL("b.test",
"/interest_group/decision_logic.js"),
origin_a);
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad2_url);
// Wait for database to be updated with the win, which may happen after the
// auction completes.
WaitForInterestGroupsSatisfying(
origin_b, base::BindLambdaForTesting(
[](scoped_refptr<StorageInterestGroups> groups) -> bool {
EXPECT_EQ(1u, groups->size());
return groups->GetInterestGroups()[0]
->bidding_browser_signals->prev_wins.size() ==
1u;
}));
// Run the third auction on another page c.test, and only interest group
// "shoes" of c.test bids this time.
GURL test_url_c =
embedded_https_test_server().GetURL("c.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url_c));
const url::Origin origin_c = url::Origin::Create(test_url_c);
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/origin_c,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"c.test",
"/interest_group/bidding_logic_with_debugging_report.js"))
.SetAds({{{ad2_url, /*metadata=*/std::nullopt}}})
.Build()));
auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
sellerSignals: {reportTo: $3},
})",
origin_c,
embedded_https_test_server().GetURL(
"c.test",
"/interest_group/decision_logic_report_to_seller_signals.js"),
embedded_https_test_server().GetURL("c.test",
"/echoall?report_seller/cars"));
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad2_url);
// Check ResourceRequest structs of report requests.
// The URLs must not have the same path with different hostnames, because
// WaitForUrl() always replaces hostnames with "127.0.0.1", thus only waits
// for the first URL among URLs with the same path.
const struct ExpectedReportRequest {
GURL url;
url::Origin request_initiator;
} kExpectedReportRequests[] = {
// First auction's seller's ReportResult() URL.
{embedded_https_test_server().GetURL("b.test", "/echoall?report_seller"),
origin_b},
// First auction's winning bidder's ReportWin() URL.
{embedded_https_test_server().GetURL(
"a.test", "/echoall?report_bidder_stop_bidding_after_win&cars"),
origin_b},
// First auction's debugging loss report URL from bidder.
{embedded_https_test_server().GetURL(
"b.test", "/echo?bidder_debug_report_loss/shoes"),
origin_b},
// Second auction's seller's ReportResult() URL. Although this URL is the
// second time requesting this URL, this test does not confirm that we
// requested the URL twice unfortunately.
// TODO(qingxinwu): Update the test fixture's use of RequestMonitor
// instead of URLLoaderMonitor to handle duplicate URLs.
{embedded_https_test_server().GetURL("b.test", "/echoall?report_seller"),
origin_b},
// Second auction's winning bidder's ReportWin() URL.
{embedded_https_test_server().GetURL("b.test",
"/echoall?report_bidder/shoes"),
origin_b},
// Second auction's debugging win report URL from bidder.
{embedded_https_test_server().GetURL(
"b.test", "/echo?bidder_debug_report_win/shoes"),
origin_b},
// Third auction's seller's ReportResult() URL.
{embedded_https_test_server().GetURL("c.test",
"/echoall?report_seller/cars"),
origin_c},
// Third auction's winning bidder's ReportWin() URL.
{embedded_https_test_server().GetURL("c.test",
"/echoall?report_bidder/cars"),
origin_c},
// Third auction's debugging win report URL from seller.
{embedded_https_test_server().GetURL(
"c.test", "/echoall?report_seller/cars_debug_win_report"),
origin_c},
// Third auction's debugging win report URL from bidder.
{embedded_https_test_server().GetURL(
"c.test", "/echo?bidder_debug_report_win/cars"),
origin_c}};
for (const auto& expected_report_request : kExpectedReportRequests) {
SCOPED_TRACE(expected_report_request.url);
// Make sure the report URL was actually fetched over the network.
WaitForUrl(expected_report_request.url);
std::optional<network::ResourceRequest> request =
url_loader_monitor.WaitForUrl(expected_report_request.url);
ASSERT_TRUE(request);
EXPECT_EQ(network::mojom::CredentialsMode::kOmit,
request->credentials_mode);
EXPECT_EQ(network::mojom::RedirectMode::kError, request->redirect_mode);
EXPECT_EQ(expected_report_request.request_initiator,
request->request_initiator);
EXPECT_TRUE(request->headers.IsEmpty());
ASSERT_TRUE(request->trusted_params);
const net::IsolationInfo& isolation_info =
request->trusted_params->isolation_info;
EXPECT_EQ(net::IsolationInfo::RequestType::kOther,
isolation_info.request_type());
EXPECT_TRUE(isolation_info.network_isolation_key().IsTransient());
EXPECT_TRUE(isolation_info.site_for_cookies().IsNull());
}
}
// Adding an interest group and then immediately running the ad auction, without
// waiting in between, should always work because although adding the interest
// group is async (and intentionally without completion notification), it should
// complete before the auction runs.
//
// On regression, this test will likely only fail with very low frequency.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
AddInterestGroupRunAuctionWithWinnerWithoutWaiting) {
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_cars");
const char kName[] = "cars";
// All joinAdInterestGroup wrapper calls wait for the returned promise to
// complete. Inline the call to avoid waiting.
EXPECT_EQ(
"done",
EvalJs(shell(),
JsReplace(
R"(
(function() {
navigator.joinAdInterestGroup(
{
name: $1,
owner: $2,
biddingLogicURL: $3,
ads: $4
},
/*joinDurationSec=*/ 300);
return 'done';
})())",
kName, test_origin,
embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"),
MakeAdsValue(
{{{ad_url, R"({"ad":"metadata","here":[1,2]})"}}}))));
// All leaveAdInterestGroup wrapper calls wait for the returned promise to
// complete. Inline the call to avoid waiting.
EXPECT_EQ("done", EvalJs(shell(), JsReplace(R"(
(function() {
navigator.leaveAdInterestGroup({name: $1, owner: $2});
return 'done';
})())",
kName, test_origin)));
EXPECT_EQ(base::Value(),
RunAuctionAndWait(JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
auctionSignals: {x: 1},
sellerSignals: {yet: 'more', info: 1},
perBuyerSignals: {$1: {even: 'more', x: 4.5}}
})",
test_origin,
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js"))));
}
// The winning ad's render url is invalid (invalid url or has http scheme).
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, RunAdAuctionWithInvalidAdUrl) {
GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic_invalid_ad_url.js"))
.SetAds({{{GURL("https://shoes.com/render"),
R"({"ad":"metadata","here":[1,2]})"}}})
.Build()));
EXPECT_EQ(base::Value(),
RunAuctionAndWait(JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
})",
test_origin,
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js"))));
}
// Test that when there are no ad components, an array of ad components is still
// available, and they're all mapped to about:blank.
IN_PROC_BROWSER_TEST_F(InterestGroupFencedFrameBrowserTest, NoAdComponents) {
GURL test_url = embedded_https_test_server().GetURL(
"a.test", "/fenced_frames/basic.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
// Trying to retrieve the adAuctionComponents of the main frame should throw
// an exception.
EXPECT_FALSE(GetAdAuctionComponentsInJS(shell(), 1));
GURL ad_url = embedded_https_test_server().GetURL(
"c.test", "/fenced_frames/basic.html");
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
/*owner=*/url::Origin::Create(test_url),
/*name=*/"cars",
/*priority=*/0.0,
/*execution_mode=*/
blink::InterestGroup::ExecutionMode::kCompatibilityMode,
/*bidding_url=*/
embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"),
/*ads=*/{{{ad_url, /*metadata=*/std::nullopt}}},
/*ad_components=*/std::nullopt));
ASSERT_NO_FATAL_FAILURE(RunAuctionAndNavigateFencedFrame(
ad_url, JsReplace(
R"(
{
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1]
}
)",
url::Origin::Create(test_url),
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js"))));
// Check that adAuctionComponents() returns an array of URNs that all map to
// about:blank.
RenderFrameHostImpl* ad_frame = GetFencedFrameRenderFrameHost(shell());
CheckAdComponents(/*expected_ad_component_urls=*/std::vector<GURL>{},
ad_frame);
std::optional<std::vector<GURL>> all_component_urls =
GetAdAuctionComponentsInJS(ad_frame, blink::MaxAdAuctionAdComponents());
ASSERT_TRUE(all_component_urls);
NavigateFencedFrameAndWait((*all_component_urls)[0],
GURL(url::kAboutBlankURL),
GetFencedFrameRenderFrameHost(shell()));
NavigateFencedFrameAndWait(
(*all_component_urls)[blink::MaxAdAuctionAdComponents() - 1],
GURL(url::kAboutBlankURL), GetFencedFrameRenderFrameHost(shell()));
}
// Test with an ad component. Run an auction with an ad component, load the ad
// in a fenced frame, and the ad component in a nested fenced frame. Fully
// exercise navigator.adAuctionComponents() on the main ad's fenced frame.
IN_PROC_BROWSER_TEST_F(InterestGroupFencedFrameBrowserTest, AdComponents) {
GURL ad_component_url = embedded_https_test_server().GetURL(
"d.test", "/set-header?Supports-Loading-Mode: fenced-frame");
ASSERT_NO_FATAL_FAILURE(RunBasicAuctionWithAdComponents(ad_component_url));
// Trying to retrieve the adAuctionComponents of the main frame should throw
// an exception.
EXPECT_FALSE(GetAdAuctionComponentsInJS(shell(), 1));
// Check that adAuctionComponents() returns an array of URNs, the first of
// which maps to `ad_component_url`, and the rest of which map to about:blank.
RenderFrameHostImpl* ad_frame = GetFencedFrameRenderFrameHost(shell());
CheckAdComponents(
/*expected_ad_component_urls=*/std::vector<GURL>{ad_component_url},
ad_frame);
std::optional<std::vector<GURL>> all_component_urls =
GetAdAuctionComponentsInJS(ad_frame, blink::MaxAdAuctionAdComponents());
ASSERT_TRUE(all_component_urls);
NavigateFencedFrameAndWait((*all_component_urls)[1],
GURL(url::kAboutBlankURL),
GetFencedFrameRenderFrameHost(shell()));
NavigateFencedFrameAndWait(
(*all_component_urls)[blink::MaxAdAuctionAdComponents() - 1],
GURL(url::kAboutBlankURL), GetFencedFrameRenderFrameHost(shell()));
}
// Checked that navigator.adAuctionComponents() from an ad auction with
// components aren't leaked to other frames. In particular, check that they
// aren't provided to:
// * The main frame. It will throw an exception.
// * The fenced frame the ad component is loaded in, though it will have a list
// of URNs that map to about:blank.
// * The ad fenced frame itself, after a renderer-initiated navigation.
IN_PROC_BROWSER_TEST_F(InterestGroupFencedFrameBrowserTest,
AdComponentsNotLeaked) {
GURL ad_component_url = embedded_https_test_server().GetURL(
"d.test", "/fenced_frames/basic.html");
ASSERT_NO_FATAL_FAILURE(RunBasicAuctionWithAdComponents(ad_component_url));
// The top frame should have no ad components.
EXPECT_FALSE(GetAdAuctionComponentsInJS(shell(), 1));
// Check that adAuctionComponents(), when invoked in the ad component's frame,
// returns an array of URNs that all map to about:blank.
RenderFrameHostImpl* ad_frame = GetFencedFrameRenderFrameHost(shell());
RenderFrameHostImpl* ad_component_frame =
GetFencedFrameRenderFrameHost(ad_frame);
CheckAdComponents(/*expected_ad_component_urls=*/std::vector<GURL>{},
ad_component_frame);
// Navigate the ad component's nested fenced frame (3 fenced frames deep) to
// some of the URNs, which should navigate it to about:blank.
std::optional<std::vector<GURL>> all_component_urls =
GetAdAuctionComponentsInJS(ad_component_frame,
blink::MaxAdAuctionAdComponents());
ASSERT_TRUE(all_component_urls);
NavigateFencedFrameAndWait((*all_component_urls)[0],
GURL(url::kAboutBlankURL), ad_component_frame);
NavigateFencedFrameAndWait(
(*all_component_urls)[blink::MaxAdAuctionAdComponents() - 1],
GURL(url::kAboutBlankURL), ad_component_frame);
// Load a new URL in the top-level fenced frame, which should cause future
// navigator.adComponents() calls to fail. Use a new URL, so can wait for the
// server to see it. Same origin navigation so that the RenderFrameHost will
// be reused.
GURL new_url = embedded_https_test_server().GetURL(
ad_frame->GetLastCommittedOrigin().host(), "/echoall");
// Used to wait for navigation completion in the ShadowDOM case only.
// Harmlessly created but not used in the MPArch case.
TestFrameNavigationObserver observer(ad_frame);
EXPECT_TRUE(ExecJs(ad_frame, JsReplace("document.location = $1;", new_url)));
// Wait for the URL to be requested, to make sure the fenced frame actually
// made the request and, in the MPArch case, to make sure the load actually
// started.
WaitForUrl(new_url);
// Wait for the load to complete.
observer.Wait();
// Navigating the ad fenced frame may result in it using a new
// RenderFrameHost, invalidating the old `ad_frame`.
ad_frame = GetFencedFrameRenderFrameHost(shell());
// Make sure the expected page has loaded in the ad frame.
EXPECT_EQ(new_url, ad_frame->GetLastCommittedURL());
// Calling navigator.adAuctionComponents on the new frame should fail.
EXPECT_FALSE(GetAdAuctionComponentsInJS(ad_frame, 1));
}
// Test navigating multiple fenced frames to the same render URL from a single
// auction, when the winning bid included ad components. All fenced frames
// navigated to the URL should get ad component URLs from the winning bid.
IN_PROC_BROWSER_TEST_F(InterestGroupFencedFrameBrowserTest,
AdComponentsMainAdLoadedInMultipleFrames) {
GURL ad_component_url = embedded_https_test_server().GetURL(
"d.test", "/set-header?Supports-Loading-Mode: fenced-frame");
GURL test_url = embedded_https_test_server().GetURL(
"a.test", "/fenced_frames/basic.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
GURL ad_url = embedded_https_test_server().GetURL(
"c.test", "/fenced_frames/basic.html");
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
/*owner=*/url::Origin::Create(test_url),
/*name=*/"cars",
/*priority=*/0.0,
/*execution_mode=*/
blink::InterestGroup::ExecutionMode::kCompatibilityMode,
/*bidding_url=*/
embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"),
/*ads=*/{{{ad_url, /*metadata=*/std::nullopt}}},
/*ad_components=*/
{{{ad_component_url, /*metadata=*/std::nullopt}}}));
content::EvalJsResult urn_url_string = RunAuctionAndWait(JsReplace(
R"(
{
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1]
}
)",
url::Origin::Create(test_url),
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js")));
GURL urn_url(urn_url_string.ExtractString());
ASSERT_TRUE(urn_url.is_valid())
<< "URL is not valid: " << urn_url_string.ExtractString();
EXPECT_EQ(url::kUrnScheme, urn_url.scheme_piece());
// Repeatedly load the URN in fenced frames. The first two iterations use the
// original fenced frame, the next two use a new one that replaces the first.
for (int i = 0; i < 4; ++i) {
if (i == 2) {
EXPECT_TRUE(ExecJs(shell(),
"document.querySelector('fencedframe').remove();"
"const ff = document.createElement('fencedframe');"
"document.body.appendChild(ff);"));
}
ClearReceivedRequests();
NavigateFencedFrameAndWait(urn_url, ad_url, shell());
RenderFrameHost* ad_frame = GetFencedFrameRenderFrameHost(shell());
std::optional<std::vector<GURL>> components =
GetAdAuctionComponentsInJS(ad_frame, 1);
ASSERT_TRUE(components);
ASSERT_EQ(1u, components->size());
EXPECT_EQ(url::kUrnScheme, (*components)[0].scheme_piece());
NavigateFencedFrameAndWait((*components)[0], ad_component_url, ad_frame);
}
}
// Test with multiple ad components. Also checks that ad component metadata is
// passed in correctly.
IN_PROC_BROWSER_TEST_F(InterestGroupFencedFrameBrowserTest,
MultipleAdComponents) {
// Note that the extra "&1" and the like are added to make the URLs unique.
// They have no impact on the returned result, since they aren't a
// header/value pair.
std::vector<blink::InterestGroup::Ad> ad_components{
{embedded_https_test_server().GetURL(
"d.test", "/set-header?Supports-Loading-Mode: fenced-frame&1"),
std::nullopt},
{embedded_https_test_server().GetURL(
"d.test", "/set-header?Supports-Loading-Mode: fenced-frame&2"),
"2"},
{embedded_https_test_server().GetURL(
"d.test", "/set-header?Supports-Loading-Mode: fenced-frame&3"),
R"(["3",{"4":"five"}])"},
};
GURL test_url = embedded_https_test_server().GetURL(
"a.test", "/fenced_frames/basic.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
// Register bidding script that validates interestGroup.adComponents and
// returns the first and third components in the offered bid.
std::string bidding_script = R"(
let adComponents = interestGroup.adComponents;
if (adComponents.length !== 3)
throw 'Incorrect length';
if (adComponents[0].metadata !== undefined)
throw 'adComponents[0] has incorrect metadata: ' + adComponents[0].metadata;
if (adComponents[1].metadata !== 2)
throw 'adComponents[1] has incorrect metadata: ' + adComponents[1].metadata;
if (JSON.stringify(adComponents[2].metadata) !== '["3",{"4":"five"}]') {
throw 'adComponents[2] has incorrect metadata: ' + adComponents[2].metadata;
}
return {
ad: 'ad',
bid: 1,
render: interestGroup.ads[0].renderURL,
adComponents: [interestGroup.adComponents[0].renderURL,
interestGroup.adComponents[2].renderURL]
};
)";
GURL bidding_url = embedded_https_test_server().GetURL(
"a.test", "/generated_bidding_logic.js");
network_responder_->RegisterBidderScript(bidding_url.path(), bidding_script);
GURL ad_url = embedded_https_test_server().GetURL(
"c.test", "/fenced_frames/basic.html");
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
/*owner=*/url::Origin::Create(test_url),
/*name=*/"cars", /*priority=*/0.0, /*execution_mode=*/
blink::InterestGroup::ExecutionMode::kCompatibilityMode, bidding_url,
/*ads=*/{{{ad_url, /*metadata=*/std::nullopt}}}, ad_components));
ASSERT_NO_FATAL_FAILURE(RunAuctionAndNavigateFencedFrame(
ad_url, JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1]
})",
url::Origin::Create(test_url),
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js"))));
// Validate ad components array. The bidder script should return only the
// first and last ad component URLs, skpping the second.
RenderFrameHostImpl* ad_frame = GetFencedFrameRenderFrameHost(shell());
CheckAdComponents(
/*expected_ad_component_urls=*/{GURL(ad_components[0].render_url()),
GURL(ad_components[2].render_url())},
ad_frame);
// Get first three URLs from the fenced frame.
std::optional<std::vector<GURL>> components =
GetAdAuctionComponentsInJS(ad_frame, 3);
ASSERT_TRUE(components);
ASSERT_EQ(3u, components->size());
// Load each of the ad components in the nested fenced frame, validating the
// URLs they're mapped to.
NavigateFencedFrameAndWait((*components)[0],
GURL(ad_components[0].render_url()), ad_frame);
NavigateFencedFrameAndWait((*components)[1],
GURL(ad_components[2].render_url()), ad_frame);
NavigateFencedFrameAndWait((*components)[2], GURL(url::kAboutBlankURL),
ad_frame);
}
class InterestGroupAuctionFledgeDealSupportDisabledTest
: public InterestGroupBrowserTest {
public:
explicit InterestGroupAuctionFledgeDealSupportDisabledTest() {
scoped_feature_list_.InitAndDisableFeature(
blink::features::kFledgeAuctionDealSupport);
}
protected:
base::test::ScopedFeatureList scoped_feature_list_;
};
IN_PROC_BROWSER_TEST_F(InterestGroupAuctionFledgeDealSupportDisabledTest,
FeatureDetection) {
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/simple_page.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
ASSERT_EQ(true, EvalJs(shell(), "'protectedAudience' in navigator"));
const char kQuerySelectableReportingIds[] = R"(
navigator.protectedAudience.queryFeatureSupport(
'selectableReportingIds');
)";
EXPECT_EQ(false, EvalJs(shell(), kQuerySelectableReportingIds));
}
IN_PROC_BROWSER_TEST_F(InterestGroupAuctionFledgeDealSupportDisabledTest,
JoinInterestGroupSelectableReportingIdsIgnored) {
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
auto origin = url::Origin::Create(url);
std::vector<std::string> selectable_buyer_and_seller_reporting_ids = {
"selectable_id1", "selectable_id2"};
ASSERT_TRUE(NavigateToURL(shell(), url));
EXPECT_EQ(
kSuccess,
JoinInterestGroup(
blink::TestInterestGroupBuilder(origin, "cars")
.SetAds(
{{{GURL("https://example.com/render"),
/*metadata=*/std::nullopt, /*size_group=*/std::nullopt,
/*buyer_reporting_id=*/"brid1",
/*buyer_and_seller_reporting_id=*/"sh1",
/*selectable_buyer_and_seller_reporting_ids=*/
std::vector<std::string>{"selectable_id1",
"selectable_id2"},
/*ad_render_id=*/std::nullopt},
{GURL("https://example.com/render2"),
/*metadata=*/std::nullopt, /*size_group=*/std::nullopt,
/*buyer_reporting_id=*/std::nullopt,
/*buyer_and_seller_reporting_id=*/"sh2",
/*selectable_buyer_and_seller_reporting_ids=*/std::nullopt,
/*ad_render_id=*/std::nullopt}}})
.Build()));
scoped_refptr<StorageInterestGroups> groups =
GetInterestGroupsForOwner(origin);
ASSERT_EQ(groups->size(), 1u);
const blink::InterestGroup& group =
groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(group.ads.has_value());
ASSERT_EQ(group.ads->size(), 2u);
EXPECT_EQ(group.ads.value()[0].render_url(),
GURL("https://example.com/render"));
EXPECT_EQ(group.ads.value()[0].buyer_reporting_id, "brid1");
EXPECT_EQ(group.ads.value()[0].buyer_and_seller_reporting_id, "sh1");
EXPECT_FALSE(group.ads.value()[0]
.selectable_buyer_and_seller_reporting_ids.has_value());
EXPECT_EQ(group.ads.value()[1].render_url(),
GURL("https://example.com/render2"));
EXPECT_FALSE(group.ads.value()[1].buyer_reporting_id.has_value());
EXPECT_EQ(group.ads.value()[1].buyer_and_seller_reporting_id, "sh2");
EXPECT_FALSE(group.ads.value()[1]
.selectable_buyer_and_seller_reporting_ids.has_value());
}
// When an interest group contains selectableBuyerAndSellerReportingIds,
// and the feature is disabled, we expect the selectable to never be set,
// and therefore should be treated as a regular bid as if it was never there.
//
// reportWin() receives:
// - buyerAndSellerReportingId (original behavior)
//
// scoreAd() receives:
// - none of the reporting ids (original behavior)
//
// reportResult() receives:
// - buyerAndSellerReportingId (original behavior)
//
IN_PROC_BROWSER_TEST_F(InterestGroupAuctionFledgeDealSupportDisabledTest,
RunAdAuctionWithWinnerWithSelectedAndAllReportingIds) {
const GURL url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
const auto origin = url::Origin::Create(url);
// These scripts are generated by this test.
constexpr char kBiddingLogicPath[] =
"/interest_group/test_generated_bidding_argument_validator.js";
constexpr char kDecisionLogicPath[] =
"/interest_group/test_generated_decision_argument_validator.js";
constexpr char kBiddingLogicScript[] = R"(
function generateBid(
interestGroup, unusedAuctionSignals, unusedPerBuyerSignals,
unusedTrustedBiddingSignals, unusedBrowserSignals) {
const ad = interestGroup.ads[0];
// Verify that none of the reporting IDs are present in generate bid.
if (('selectableBuyerAndSellerReportingIds' in ad) ||
('buyerReportingId' in ad) ||
('buyerAndSellerReportingId' in ad)) {
throw "generateBid should not get any reporting ids when feature is disabled.";
}
return {'ad': ad, 'bid': 1, 'render': ad.renderURL,
'selectedBuyerAndSellerReportingId': 'selectable_id1'};
}
function reportWin(unusedAuctionSignals, unusedPerBuyerSignals,
unusedSellerSignals, browserSignals) {
let reportUrl = browserSignals.interestGroupOwner +
'/report_bidder/checksPassed';
if (browserSignals.buyerReportingId !== undefined ||
browserSignals.buyerAndSellerReportingId !== "bsid" ||
browserSignals.selectedBuyerAndSellerReportingId !== undefined) {
reportUrl = browserSignals.interestGroupOwner +
'/report_bidder/checksFailed';
}
sendReportTo(reportUrl);
}
)";
constexpr char kDecisionLogicScript[] = R"(
function scoreAd(
adMetadata, bid, auctionConfig, unusedTrustedScoringSignals,
browserSignals) {
if (browserSignals.selectedBuyerAndSellerReportingId !== undefined ||
browserSignals.buyerAndSellerReportingId !== undefined) {
throw "scoreAd should not get reporting id";
}
return bid;
}
function reportResult(auctionConfig, browserSignals) {
let reportUrl = auctionConfig.seller + '/report_seller/checksPassed';
if (browserSignals.selectedBuyerAndSellerReportingId !== undefined ||
browserSignals.buyerAndSellerReportingId !== "bsid") {
reportUrl = auctionConfig.seller + '/report_seller/checksFailed';
}
sendReportTo(reportUrl);
}
)";
network_responder_->RegisterNetworkResponse(
kBiddingLogicPath, kBiddingLogicScript, "application/javascript");
network_responder_->RegisterNetworkResponse(
kDecisionLogicPath, kDecisionLogicScript, "application/javascript");
GURL ad_url_with_selectable =
embedded_https_test_server().GetURL("a.test", "/echo?Selectable");
ASSERT_TRUE(NavigateToURL(shell(), url));
EXPECT_EQ(
kSuccess,
JoinInterestGroup(
blink::TestInterestGroupBuilder(origin, "selectables")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", kBiddingLogicPath))
.SetAds({{{ad_url_with_selectable,
/*metadata=*/std::nullopt, /*size_group=*/std::nullopt,
/*buyer_reporting_id=*/"brid",
/*buyer_and_seller_reporting_id=*/"bsid",
/*selectable_buyer_and_seller_reporting_ids=*/
std::vector<std::string>{"selectable_id1",
"selectable_id2"},
/*ad_render_id=*/std::nullopt}}})
.Build()));
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
auctionSignals: {x: 1}
})",
origin,
embedded_https_test_server().GetURL("a.test", kDecisionLogicPath));
RunAuctionAndWaitForURLAndNavigateIframe(auction_config,
ad_url_with_selectable);
WaitForUrl(embedded_https_test_server().GetURL(
"a.test", "/report_seller/checksPassed"));
WaitForUrl(embedded_https_test_server().GetURL(
"a.test", "/report_bidder/checksPassed"));
}
// When an interest group contains selectableBuyerAndSellerReportingIds,
// and one is selected in generateBid, this verifies that:
//
// reportWin() receives:
// - selectedBuyerAndSellerReportingId
// - buyerAndSellerReportingId (if present)
// - buyerReportingId (if present)
//
// scoreAd() receives:
// - selectedBuyerAndSellerReportingId
// - buyerAndSellerReportingId (if present)
//
// reportResult() receives:
// - selectedBuyerAndSellerReportingId
// - buyerAndSellerReportingId (if present)
//
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionWithWinnerWithSelectedAndAllReportingIds) {
const GURL url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
const auto origin = url::Origin::Create(url);
// These scripts are generated by this test.
constexpr char kBiddingLogicPath[] =
"/interest_group/test_generated_bidding_argument_validator.js";
constexpr char kDecisionLogicPath[] =
"/interest_group/test_generated_decision_argument_validator.js";
constexpr char kBiddingLogicScript[] = R"(
function generateBid(
interestGroup, unusedAuctionSignals, unusedPerBuyerSignals,
unusedTrustedBiddingSignals, unusedBrowserSignals) {
const ad = interestGroup.ads[0];
// Verify all of these are present in generate bid,
// if selectableBuyerAndSellerReportingIds is present.
if (ad.selectableBuyerAndSellerReportingIds.length !== 2 ||
ad.selectableBuyerAndSellerReportingIds[0] !== "selectable_id1" ||
ad.selectableBuyerAndSellerReportingIds[1] !== "selectable_id2" ||
ad.buyerReportingId !== "brid" ||
ad.buyerAndSellerReportingId !== "bsid" ) {
throw "generateBid does not contain valid reporting ids";
}
selectedReportingId = ad.selectableBuyerAndSellerReportingIds[0];
return {'ad': ad, 'bid': 1, 'render': ad.renderURL,
'selectedBuyerAndSellerReportingId': selectedReportingId};
}
function reportWin(unusedAuctionSignals, unusedPerBuyerSignals,
unusedSellerSignals, browserSignals) {
let reportUrl = browserSignals.interestGroupOwner + '/report_bidder/checksPassed';
if (browserSignals.buyerReportingId !== "brid" ||
browserSignals.buyerAndSellerReportingId !== "bsid" ||
browserSignals.selectedBuyerAndSellerReportingId !== "selectable_id1") {
reportUrl = browserSignals.interestGroupOwner + '/report_bidder/checksFailed';
}
sendReportTo(browserSignals.interestGroupOwner +
'/report_bidder/checksPassed');
}
)";
constexpr char kDecisionLogicScript[] = R"(
function scoreAd(
adMetadata, bid, auctionConfig, unusedTrustedScoringSignals,
browserSignals) {
if (browserSignals.selectedBuyerAndSellerReportingId !== "selectable_id1" ||
browserSignals.buyerAndSellerReportingId !== "bsid") {
throw "scoreAd does not contain valid reporting ids";
}
return bid;
}
function reportResult(auctionConfig, browserSignals) {
let reportUrl = auctionConfig.seller + '/report_seller/checksPassed';
if (browserSignals.selectedBuyerAndSellerReportingId !== "selectable_id1" ||
browserSignals.buyerAndSellerReportingId !== "bsid") {
reportUrl = auctionConfig.seller + '/report_seller/checksFailed';
}
sendReportTo(reportUrl);
}
)";
network_responder_->RegisterNetworkResponse(
kBiddingLogicPath, kBiddingLogicScript, "application/javascript");
network_responder_->RegisterNetworkResponse(
kDecisionLogicPath, kDecisionLogicScript, "application/javascript");
GURL ad_url_with_selectable =
embedded_https_test_server().GetURL("a.test", "/echo?Selectable");
ASSERT_TRUE(NavigateToURL(shell(), url));
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(origin, "selectables")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", kBiddingLogicPath))
.SetAds({{{ad_url_with_selectable,
/*metadata=*/std::nullopt, /*size_group=*/std::nullopt,
/*buyer_reporting_id=*/"brid",
/*buyer_and_seller_reporting_id=*/"bsid",
/*selectable_buyer_and_seller_reporting_ids=*/
std::vector<std::string>{"selectable_id1",
"selectable_id2"},
/*ad_render_id=*/std::nullopt}}})
.Build()));
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
auctionSignals: {x: 1}
})",
origin,
embedded_https_test_server().GetURL("a.test", kDecisionLogicPath));
RunAuctionAndWaitForURLAndNavigateIframe(auction_config,
ad_url_with_selectable);
WaitForUrl(embedded_https_test_server().GetURL(
"a.test", "/report_seller/checksPassed"));
WaitForUrl(embedded_https_test_server().GetURL(
"a.test", "/report_bidder/checksPassed"));
}
// When an interest group contains selectableBuyerAndSellerReportingIds,
// and one isn't selected in generateBid, this verifies that:
//
// reportWin() receives:
// - buyerAndSellerReportingId (original behavior)
//
// scoreAd() receives:
// - none of the reporting ids (original behavior)
//
// reportResult() receives:
// - buyerAndSellerReportingId (original behavior)
//
IN_PROC_BROWSER_TEST_F(
InterestGroupBrowserTest,
RunAdAuctionWithWinnerWithNoSelectedValidateReportingIds) {
const GURL url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
const auto origin = url::Origin::Create(url);
const std::vector<std::string> kSelectableBuyerAndSellerReportingIds = {
"selectable_id1", "selectable_id2"};
// These scripts are generated by this test.
constexpr char kBiddingLogicPath[] =
"/interest_group/test_generated_bidding_argument_validator.js";
constexpr char kDecisionLogicPath[] =
"/interest_group/test_generated_decision_argument_validator.js";
constexpr char kBiddingLogicScript[] = R"(
function generateBid(
interestGroup, unusedAuctionSignals, unusedPerBuyerSignals,
unusedTrustedBiddingSignals, unusedBrowserSignals) {
const ad = interestGroup.ads[0];
if (ad.selectableBuyerAndSellerReportingIds !== undefined &&
ad.buyerReportingId === "brid" &&
ad.buyerAndSellerReportingId === "bsid" ) {
return {'ad': ad, 'bid': 1000, 'render': ad.renderURL};
}
throw "generateBid does not contain valid reporting ids";
}
function reportWin(unusedAuctionSignals, unusedPerBuyerSignals,
unusedSellerSignals, browserSignals) {
let reportUrl = browserSignals.interestGroupOwner + '/report_bidder/checksPassed';
if (browserSignals.buyerReportingId === "brid" ||
browserSignals.buyerAndSellerReportingId !== "bsid" ||
browserSignals.selectedBuyerAndSellerReportingId === "selectable_id1") {
reportUrl = browserSignals.interestGroupOwner + '/report_bidder/checksFailed';
}
sendReportTo(reportUrl);
}
)";
constexpr char kDecisionLogicScript[] = R"(
function scoreAd(
adMetadata, bid, auctionConfig, unusedTrustedScoringSignals,
browserSignals) {
// These ids are only present when selected is present, which it is not.
if (browserSignals.selectedBuyerAndSellerReportingId === undefined &&
browserSignals.buyerAndSellerReportingId === undefined) {
return bid;
}
throw "scoreAd does not contain valid reporting ids";
}
function reportResult(auctionConfig, browserSignals) {
let reportUrl = auctionConfig.seller + '/report_seller/checksPassed';
if (browserSignals.selectedBuyerAndSellerReportingId !== undefined ||
browserSignals.buyerAndSellerReportingId !== "bsid") {
reportUrl = auctionConfig.seller + '/report_seller/checksFailed';
}
sendReportTo(reportUrl);
}
)";
network_responder_->RegisterNetworkResponse(
kBiddingLogicPath, kBiddingLogicScript, "application/javascript");
network_responder_->RegisterNetworkResponse(
kDecisionLogicPath, kDecisionLogicScript, "application/javascript");
GURL ad_url = embedded_https_test_server().GetURL("a.test", "/echo?ad");
ASSERT_TRUE(NavigateToURL(shell(), url));
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(origin, "selectables")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", kBiddingLogicPath))
.SetAds({{{ad_url,
/*metadata=*/std::nullopt, /*size_group=*/std::nullopt,
/*buyer_reporting_id=*/"brid",
/*buyer_and_seller_reporting_id=*/"bsid",
kSelectableBuyerAndSellerReportingIds,
/*ad_render_id=*/std::nullopt}}})
.Build()));
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
auctionSignals: {x: 1}
})",
origin,
embedded_https_test_server().GetURL("a.test", kDecisionLogicPath));
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad_url);
WaitForUrl(embedded_https_test_server().GetURL(
"a.test", "/report_seller/checksPassed"));
WaitForUrl(embedded_https_test_server().GetURL(
"a.test", "/report_bidder/checksPassed"));
}
// When an interest group contains selectableBuyerAndSellerReportingIds,
// and the one selected in generateBid is not present within the original array,
// this verifies that a worklet error is thrown and the bid is discarded.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionWithInvalidSelectedReportingId) {
const GURL url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
const auto origin = url::Origin::Create(url);
const std::vector<std::string> kSelectableBuyerAndSellerReportingIds = {
"selectable_id1", "selectable_id2"};
// These scripts are generated by this test.
constexpr char kBiddingLogicPath[] =
"/interest_group/test_generated_bidding_argument_validator.js";
constexpr char kDecisionLogicPath[] =
"/interest_group/test_generated_decision_argument_validator.js";
constexpr char kBiddingLogicScript[] = R"(
function generateBid(
interestGroup, unusedAuctionSignals, unusedPerBuyerSignals,
unusedTrustedBiddingSignals, unusedBrowserSignals) {
const ad = interestGroup.ads[0];
if (ad.selectableBuyerAndSellerReportingIds !== undefined) {
return {'ad': ad, 'bid': 1, 'render': ad.renderURL,
'selectedBuyerAndSellerReportingId':"invalid-selectedReportingId"};
}
throw "generateBid does not contain valid reporting ids";
}
function reportWin(unusedAuctionSignals, unusedPerBuyerSignals,
unusedSellerSignals, browserSignals) {
throw "reportWin should not be called";
}
)";
constexpr char kDecisionLogicScript[] = R"(
function scoreAd(
adMetadata, bid, auctionConfig, unusedTrustedScoringSignals,
browserSignals) {
throw "scoreAd should not be called";
}
function reportResult(auctionConfig, browserSignals) {
throw "reportResult should not be called";
}
)";
// Expected worklet error
WebContentsConsoleObserver console_observer(shell()->web_contents());
console_observer.SetPattern(
"*Worklet error: *generateBid() Invalid selected buyer and seller "
"reporting id*");
network_responder_->RegisterNetworkResponse(
kBiddingLogicPath, kBiddingLogicScript, "application/javascript");
network_responder_->RegisterNetworkResponse(
kDecisionLogicPath, kDecisionLogicScript, "application/javascript");
GURL ad_url_with_selectable =
embedded_https_test_server().GetURL("a.test", "/echo?Selectable");
ASSERT_TRUE(NavigateToURL(shell(), url));
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(origin, "selectables")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", kBiddingLogicPath))
.SetAds({{{ad_url_with_selectable,
/*metadata=*/std::nullopt, /*size_group=*/std::nullopt,
/*buyer_reporting_id=*/std::nullopt,
/*buyer_and_seller_reporting_id=*/std::nullopt,
kSelectableBuyerAndSellerReportingIds,
/*ad_render_id=*/std::nullopt}}})
.Build()));
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
auctionSignals: {x: 1}
})",
origin,
embedded_https_test_server().GetURL("a.test", kDecisionLogicPath));
EXPECT_EQ(base::Value(), RunAuctionAndWait(auction_config));
EXPECT_TRUE(console_observer.Wait());
}
// These end-to-end tests validate that information from navigator-exposed APIs
// is correctly passed to worklets.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
BuyerWorkletThrowsFailsAuction) {
GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
ASSERT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic_throws.js"))
.SetTrustedBiddingSignalsUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/trusted_bidding_signals.json"))
.SetTrustedBiddingSignalsKeys({{"key1"}})
.SetAds({{{GURL("https://example.com/render"),
R"({"ad":"metadata","here":[1,2,3]})"}}})
.Build()));
EXPECT_EQ(base::Value(),
RunAuctionAndWait(JsReplace(
R"(
{
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
})",
test_origin,
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js"))));
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, ComponentAuction) {
AttachInterestGroupObserver();
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_cars");
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
/*owner=*/test_origin,
/*name=*/"cars",
/*priority=*/0.0,
/*execution_mode=*/
blink::InterestGroup::ExecutionMode::kCompatibilityMode,
/*bidding_url=*/
embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"),
/*ads=*/{{{ad_url, /*metadata=*/std::nullopt}}}));
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
// Signal to the top-level seller to allow participation in a component
// auction.
auctionSignals: "sellerAllowsComponentAuction",
componentAuctions: [{
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
// Signal to the bidder and component seller to allow participation in
// a component auction.
auctionSignals: "bidderAllowsComponentAuction,"+
"sellerAllowsComponentAuction"
}]
})",
test_origin,
embedded_https_test_server().GetURL("a.test",
"/interest_group/decision_logic.js"));
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad_url);
WaitForAccessObserved({
{"global", TestInterestGroupObserver::kJoin, test_origin, "cars"},
{"1", TestInterestGroupObserver::kLoaded, test_origin, "cars"},
{"1", TestInterestGroupObserver::kBid, test_origin, "cars", 1.0},
{"2", TestInterestGroupObserver::kTopLevelBid, test_origin, "cars", 1.0,
/*bid_currency=*/std::nullopt, test_origin},
{"2", TestInterestGroupObserver::kWin, test_origin, "cars",
/*bid=*/std::nullopt, /*bid_currency=*/std::nullopt, test_origin},
});
}
// Test the case of a component argument in the case a bidder refuses to
// participate in component auctions.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
ComponentAuctionBidderRefuses) {
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_cars");
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
/*owner=*/test_origin,
/*name=*/"cars",
/*priority=*/0.0,
/*execution_mode=*/
blink::InterestGroup::ExecutionMode::kCompatibilityMode,
/*bidding_url=*/
embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"),
/*ads=*/{{{ad_url, /*metadata=*/std::nullopt}}}));
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
// Signal to the top-level seller to allow participation in a component
// auction.
auctionSignals: "sellerAllowsComponentAuction",
componentAuctions: [{
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
// Signal to the component seller to allow participation in a
// component auction.
auctionSignals: "sellerAllowsComponentAuction"
}]
})",
test_origin,
embedded_https_test_server().GetURL("a.test",
"/interest_group/decision_logic.js"));
EXPECT_EQ(base::Value(), RunAuctionAndWait(auction_config));
}
// Test the case of a component argument in the case the top-level seller
// refuses to participate in component auctions.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
ComponentAuctionTopLevelSellerRefuses) {
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_cars");
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
/*owner=*/test_origin,
/*name=*/"cars",
/*priority=*/0.0,
/*execution_mode=*/
blink::InterestGroup::ExecutionMode::kCompatibilityMode,
/*bidding_url=*/
embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"),
/*ads=*/{{{ad_url, /*metadata=*/std::nullopt}}}));
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
componentAuctions: [{
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
// Signal to the bidder and component seller to allow participation in
// a component auction.
auctionSignals: "bidderAllowsComponentAuction,"+
"sellerAllowsComponentAuction"
}]
})",
test_origin,
embedded_https_test_server().GetURL("a.test",
"/interest_group/decision_logic.js"));
EXPECT_EQ(base::Value(), RunAuctionAndWait(auction_config));
}
// Test the case of a component argument in the case a component seller refuses
// to participate in component auctions.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
ComponentAuctionComponentSellerRefuses) {
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_cars");
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
/*owner=*/test_origin,
/*name=*/"cars",
/*priority=*/0.0,
/*execution_mode=*/
blink::InterestGroup::ExecutionMode::kCompatibilityMode,
/*bidding_url=*/
embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"),
/*ads=*/{{{ad_url, /*metadata=*/std::nullopt}}}));
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
// Signal to the top-level seller to allow participation in a component
// auction.
auctionSignals: "sellerAllowsComponentAuction",
componentAuctions: [{
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
// Signal to the bidder to allow participation in a component auction.
auctionSignals: "bidderAllowsComponentAuction"
}]
})",
test_origin,
embedded_https_test_server().GetURL("a.test",
"/interest_group/decision_logic.js"));
EXPECT_EQ(base::Value(), RunAuctionAndWait(auction_config));
}
class InterestGroupWorkletValidationBrowserTest
: public InterestGroupBrowserTest,
public testing::WithParamInterface<bool> {
public:
bool UseHeaderDirectFromSellerSignals() const { return GetParam(); }
};
// Use bidder and seller worklet files that validate their arguments all have
// the expected values.
IN_PROC_BROWSER_TEST_P(InterestGroupWorkletValidationBrowserTest,
ValidateWorkletParameters) {
AttachInterestGroupObserver();
// Use different hostnames for each participant, since
// `trusted_bidding_signals` only checks the hostname of certain parameters.
constexpr char kBidderHost[] = "a.test";
constexpr char kSellerHost[] = "b.test";
constexpr char kTopFrameHost[] = "c.test";
constexpr char kSecondBidderHost[] = "d.test";
content_browser_client_->AddToAllowList({url::Origin::Create(
embedded_https_test_server().GetURL(kSecondBidderHost, "/"))});
const url::Origin top_frame_origin = url::Origin::Create(
embedded_https_test_server().GetURL(kTopFrameHost, "/echo"));
// Start by adding a placeholder bidder in domain d.test, used for
// perBuyerSignals validation.
GURL second_bidder_url =
embedded_https_test_server().GetURL(kSecondBidderHost, "/echo");
ASSERT_TRUE(NavigateToURL(shell(), second_bidder_url));
url::Origin second_bidder_origin = url::Origin::Create(second_bidder_url);
ASSERT_EQ(kSuccess,
JoinInterestGroupAndVerify(
/*owner=*/second_bidder_origin,
/*name=*/"boats",
/*priority=*/0.0,
/*execution_mode=*/
blink::InterestGroup::ExecutionMode::kCompatibilityMode,
/*bidding_url=*/
embedded_https_test_server().GetURL(
kSecondBidderHost, "/interest_group/bidding_logic.js"),
/*ads=*/
{{{GURL("https://should-not-be-returned/"),
/*metadata=*/std::nullopt}}}));
GURL bidder_url = embedded_https_test_server().GetURL(kBidderHost, "/echo");
ASSERT_TRUE(NavigateToURL(shell(), bidder_url));
url::Origin bidder_origin = url::Origin::Create(bidder_url);
ASSERT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(/*owner=*/bidder_origin,
/*name=*/"cars")
.SetEnableBiddingSignalsPrioritization(true)
.SetPriorityVector({{{"foo", 2}, {"bar", -11}}})
.SetPrioritySignalsOverrides({{{"foo", 1}}})
.SetBiddingUrl(embedded_https_test_server().GetURL(
kBidderHost, "/interest_group/bidding_argument_validator.js"))
.SetUpdateUrl(embedded_https_test_server().GetURL(
kBidderHost, "/not_found_update_url.json"))
.SetTrustedBiddingSignalsUrl(embedded_https_test_server().GetURL(
kBidderHost, "/interest_group/trusted_bidding_signals.json"))
.SetTrustedBiddingSignalsKeys({{"key1"}})
.SetUserBiddingSignals(
R"({"some":"json","stuff":{"here":[1,2]}})")
.SetAds({{{GURL("https://example.com/render"),
R"({"ad":"metadata","here":[1,2,3]})"}}})
.SetAdComponents({{{GURL("https://example.com/render-component"),
/*metadata=*/std::nullopt}}})
.Build()));
GURL seller_script_url = embedded_https_test_server().GetURL(
kSellerHost, "/interest_group/decision_argument_validator.js");
url::Origin seller_origin = url::Origin::Create(seller_script_url);
if (UseHeaderDirectFromSellerSignals()) {
// For `directFromSellerSignalsHeaderAdSlot`, we need to make a network
// request to a resource that has signals in the Ad-Auction-Signals header
// value.
ASSERT_TRUE(NavigateToURL(
shell(), embedded_https_test_server().GetURL(kTopFrameHost, "/echo")));
const char kHeaderSignalsPath[] = "/header_direct_from_seller_signals.json";
// The actual body of the request is just an empty JSON dict for the test,
// but it could be any arbitrary payload that the server wants to deliver
// alongside the header signals.
const char kHeaderSignalsBodyResponse[] = "{}";
const std::string kHeaderSignalsResponse =
base::StringPrintf(R"([{
"adSlot": "adSlot1",
"sellerSignals": {"json": "for", "the": ["seller"]},
"auctionSignals": {"json": "for", "all": ["parties"]},
"perBuyerSignals": {
"%s": {"json": "for", "buyer": [1]},
"%s": {"json": "for", "buyer": [2]}
}
}])",
bidder_origin.Serialize().c_str(),
second_bidder_origin.Serialize().c_str());
network_responder_->RegisterNetworkResponse(
kHeaderSignalsPath, kHeaderSignalsBodyResponse, "application/json",
/*extra_response_headers=*/
{{"Access-Control-Allow-Origin", top_frame_origin.Serialize()},
{"Ad-Auction-Signals", kHeaderSignalsResponse}});
EXPECT_TRUE(
ExecJs(web_contents()->GetPrimaryMainFrame(),
content::JsReplace("fetch($1, {adAuctionHeaders: true})",
embedded_https_test_server().GetURL(
kSellerHost, kHeaderSignalsPath))));
} else {
// For subresource bundle `directFromSellerSignals` to work, we need to
// navigate to a page that declares the subresource bundle resources we pass
// to those fields.
std::vector<NetworkResponder::SubresourceResponse> subresource_responses = {
NetworkResponder::SubresourceResponse(
/*subresource_url=*/
"/direct_from_seller_signals?sellerSignals",
/*payload=*/
R"({"json": "for", "the": ["seller"]})"),
NetworkResponder::SubresourceResponse(
/*subresource_url=*/"/direct_from_seller_signals?auctionSignals",
/*payload=*/
R"({"json": "for", "all": ["parties"]})"),
NetworkResponder::DirectFromSellerPerBuyerSignals(
bidder_origin, /*payload=*/
R"({"json": "for", "buyer": [1]})"),
NetworkResponder::DirectFromSellerPerBuyerSignals(
second_bidder_origin, /*payload=*/
R"({"json": "for", "buyer": [2]})"),
};
std::vector<NetworkResponder::SubresourceBundle> bundles = {
NetworkResponder::SubresourceBundle(
/*bundle_url=*/embedded_https_test_server().GetURL(
kSellerHost, "/generated_bundle.wbn"),
/*subresources=*/subresource_responses)};
network_responder_->RegisterDirectFromSellerSignalsResponse(
/*bundles=*/bundles,
/*allow_origin=*/top_frame_origin.Serialize());
constexpr char kPagePath[] = "/page-with-bundles.html";
network_responder_->RegisterHtmlWithSubresourceBundles(
/*bundles=*/bundles,
/*page_url=*/kPagePath);
ASSERT_TRUE(NavigateToURL(shell(), embedded_https_test_server().GetURL(
kTopFrameHost, kPagePath)));
}
TestFencedFrameURLMappingResultObserver observer;
ConvertFencedFrameURNToURL(
GURL(RunAuctionAndWait(
JsReplace(
R"(
{
seller: $1,
decisionLogicURL: $2,
trustedScoringSignalsURL: $3,
interestGroupBuyers: [$4, $5],
auctionSignals: {so: 'I', hear: ['you', 'like', 'json']},
sellerSignals: {signals: 'from', the: ['seller']},
$6: $7,
sellerTimeout: 20000,
reportingTimeout: 2000,
perBuyerSignals: {$4: {signalsForBuyer: 1}, $5: {signalsForBuyer: 2}},
perBuyerTimeouts: {$4: 11000, $5: 12000, '*': 15000},
perBuyerCumulativeTimeouts: {$4: 13000, $5: 14000, '*': 16000},
perBuyerPrioritySignals: {$4: {foo: 1}, '*': {BaR: -2}},
perBuyerCurrencies: {$4: 'USD', $5: 'CAD', '*': 'EUR'},
sellerCurrency: 'EUR',
sendCreativeScanningMetadata: true,
})",
seller_origin, seller_script_url,
embedded_https_test_server().GetURL(
kSellerHost,
"/interest_group/trusted_scoring_signals.json"),
bidder_origin, second_bidder_origin,
UseHeaderDirectFromSellerSignals()
? "directFromSellerSignalsHeaderAdSlot"
: "directFromSellerSignals",
UseHeaderDirectFromSellerSignals()
? "adSlot1"
: embedded_https_test_server()
.GetURL(kSellerHost, "/direct_from_seller_signals")
.spec()))
.ExtractString()),
&observer);
EXPECT_EQ(GURL("https://example.com/render"), observer.mapped_url());
// Run URN to URL mapping callback manually to trigger sending reports, and
// validate the right URLs are requested. Do this instead of navigating to
// the URN because the validation logic checks a fixed URL for this test,
// and don't want to send a random request to port 80 on localhost, which is
// what example.com is mapped to.
observer.on_navigate_callback().Run();
WaitForUrl(
embedded_https_test_server().GetURL(kSellerHost, "/echo?report_seller"));
WaitForUrl(
embedded_https_test_server().GetURL(kSellerHost, "/echo?report_bidder"));
WaitForAccessObserved({
{"global", TestInterestGroupObserver::kJoin, second_bidder_origin,
"boats"},
{"global", TestInterestGroupObserver::kJoin, bidder_origin, "cars"},
{"1", TestInterestGroupObserver::kLoaded, second_bidder_origin, "boats"},
{"1", TestInterestGroupObserver::kLoaded, bidder_origin, "cars"},
{"1", TestInterestGroupObserver::kBid, second_bidder_origin, "boats",
1.0},
{"1", TestInterestGroupObserver::kBid, bidder_origin, "cars", 2.0, "USD"},
{"1", TestInterestGroupObserver::kWin, bidder_origin, "cars"},
});
}
INSTANTIATE_TEST_SUITE_P(
All,
InterestGroupWorkletValidationBrowserTest,
testing::Bool(),
[](const testing::TestParamInfo<bool>& info) {
return base::StringPrintf(
"%s", info.param ? "header_direct_from_seller_signals"
: "subresource_bundle_direct_from_seller_signals");
});
// Same as above test, but leaves out the extra bidder and uses the older
// version 1 bidding signals format.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
ValidateWorkletParametersWithBiddingSignalsV1) {
// Use different hostnames for each participant, since
// `trusted_bidding_signals` only checks the hostname of certain parameters.
constexpr char kBidderHost[] = "a.test";
constexpr char kSellerHost[] = "b.test";
constexpr char kTopFrameHost[] = "c.test";
const url::Origin top_frame_origin = url::Origin::Create(
embedded_https_test_server().GetURL(kTopFrameHost, "/echo"));
// This is the primary interest group that wins the auction because
// bidding_argument_validator.js bids 2, whereas bidding_logic.js bids 1, and
// decision_logic.js just returns the bid as the rank -- highest rank wins.
GURL bidder_url = embedded_https_test_server().GetURL(kBidderHost, "/echo");
ASSERT_TRUE(NavigateToURL(shell(), bidder_url));
url::Origin bidder_origin = url::Origin::Create(bidder_url);
ASSERT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/bidder_origin,
/*name=*/"cars")
.SetEnableBiddingSignalsPrioritization(true)
.SetPriorityVector({{{"foo", 2}, {"bar", -11}}})
.SetPrioritySignalsOverrides({{{"foo", 1}}})
.SetBiddingUrl(embedded_https_test_server().GetURL(
kBidderHost, "/interest_group/bidding_argument_validator.js"))
.SetUpdateUrl(embedded_https_test_server().GetURL(
kBidderHost, "/not_found_update_url.json"))
.SetTrustedBiddingSignalsUrl(embedded_https_test_server().GetURL(
kBidderHost,
"/interest_group/trusted_bidding_signals_v1.json"))
.SetTrustedBiddingSignalsKeys({{"key1"}})
.SetUserBiddingSignals(
R"({"some":"json","stuff":{"here":[1,2]}})")
.SetAds({{{GURL("https://example.com/render"),
R"({"ad":"metadata","here":[1,2,3]})"}}})
.SetAdComponents({{{GURL("https://example.com/render-component"),
/*metadata=*/std::nullopt}}})
.Build()));
// For `directFromSellerSignals` to work, we need to navigate to a page that
// declares the subresource bundle resources we pass to those fields.
GURL seller_script_url = embedded_https_test_server().GetURL(
kSellerHost, "/interest_group/decision_argument_validator.js");
url::Origin seller_origin = url::Origin::Create(seller_script_url);
std::vector<NetworkResponder::SubresourceResponse> subresource_responses = {
NetworkResponder::SubresourceResponse(
/*subresource_url=*/
"/direct_from_seller_signals?sellerSignals",
/*payload=*/
R"({"json": "for", "the": ["seller"]})"),
NetworkResponder::SubresourceResponse(
/*subresource_url=*/"/direct_from_seller_signals?auctionSignals",
/*payload=*/
R"({"json": "for", "all": ["parties"]})"),
NetworkResponder::DirectFromSellerPerBuyerSignals(
bidder_origin, /*payload=*/
R"({"json": "for", "buyer": [1]})"),
};
std::vector<NetworkResponder::SubresourceBundle> bundles = {
NetworkResponder::SubresourceBundle(
/*bundle_url=*/embedded_https_test_server().GetURL(
kSellerHost, "/generated_bundle.wbn"),
/*subresources=*/subresource_responses)};
network_responder_->RegisterDirectFromSellerSignalsResponse(
/*bundles=*/bundles,
/*allow_origin=*/top_frame_origin.Serialize());
constexpr char kPagePath[] = "/page-with-bundles.html";
network_responder_->RegisterHtmlWithSubresourceBundles(
/*bundles=*/bundles,
/*page_url=*/kPagePath);
ASSERT_TRUE(NavigateToURL(
shell(), embedded_https_test_server().GetURL(kTopFrameHost, kPagePath)));
TestFencedFrameURLMappingResultObserver observer;
ConvertFencedFrameURNToURL(
GURL(RunAuctionAndWait(
JsReplace(R"(
{
seller: $1,
decisionLogicURL: $2,
trustedScoringSignalsURL: $3,
interestGroupBuyers: [$4, $5],
auctionSignals: {so: 'I', hear: ['you', 'like', 'json']},
sellerSignals: {signals: 'from', the: ['seller']},
directFromSellerSignals: $6,
sellerTimeout: 20000,
reportingTimeout: 2000,
perBuyerSignals: {$4: {signalsForBuyer: 1}, $5: {signalsForBuyer: 2}},
perBuyerTimeouts: {$4: 11000, $5: 12000, '*': 15000},
perBuyerCumulativeTimeouts: {$4: 13000, $5: 14000, '*': 16000},
perBuyerPrioritySignals: {$4: {foo: 1}, '*': {BaR: -2}},
perBuyerCurrencies: {$4: 'USD', $5: 'CAD', '*': 'EUR'},
sellerCurrency: 'EUR',
sendCreativeScanningMetadata: true,
})",
seller_origin, seller_script_url,
embedded_https_test_server().GetURL(
kSellerHost,
"/interest_group/trusted_scoring_signals.json"),
bidder_origin,
// Validation scripts expect https://d.test to also be
// listed as a bidder.
embedded_https_test_server().GetOrigin("d.test"),
embedded_https_test_server().GetURL(
kSellerHost, "/direct_from_seller_signals")))
.ExtractString()),
&observer);
EXPECT_EQ(GURL("https://example.com/render"), observer.mapped_url());
// Run URN to URL mapping callback manually to trigger sending reports, and
// validate the right URLs are requested. Do this instead of navigating to
// the URN because the validation logic checks a fixed URL for this test,
// and don't want to send a random request to port 80 on localhost, which is
// what example.com is mapped to.
observer.on_navigate_callback().Run();
WaitForUrl(
embedded_https_test_server().GetURL(kSellerHost, "/echo?report_seller"));
WaitForUrl(
embedded_https_test_server().GetURL(kSellerHost, "/echo?report_bidder"));
}
class InterestGroupComponentWorkletValidationBrowserTest
: public InterestGroupBrowserTest,
public testing::WithParamInterface<std::tuple<bool, bool>> {
public:
bool TopLevelUseHeaderDirectFromSellerSignals() const {
return std::get<0>(GetParam());
}
bool ComponentUseHeaderDirectFromSellerSignals() const {
return std::get<1>(GetParam());
}
};
// Use bidder and seller worklet files that validate their arguments all have
// the expected values, in the case of an auction with one component auction.
IN_PROC_BROWSER_TEST_P(InterestGroupComponentWorkletValidationBrowserTest,
ComponentAuctionValidateWorkletParameters) {
// Use different hostnames for each participant.
//
// Match assignments in above test as closely as possible, to make scripts
// similar.
constexpr char kBidderHost[] = "a.test";
constexpr char kTopLevelSellerHost[] = "b.test";
constexpr char kTopFrameHost[] = "c.test";
constexpr char kComponentSellerHost[] = "d.test";
content_browser_client_->AddToAllowList({url::Origin::Create(
embedded_https_test_server().GetURL(kComponentSellerHost, "/"))});
for (bool use_promise : {false, true}) {
SCOPED_TRACE(use_promise);
base::HistogramTester histogram_tester;
const url::Origin top_frame_origin = url::Origin::Create(
embedded_https_test_server().GetURL(kTopFrameHost, "/echo"));
GURL bidder_url = embedded_https_test_server().GetURL(kBidderHost, "/echo");
ASSERT_TRUE(NavigateToURL(shell(), bidder_url));
url::Origin bidder_origin = url::Origin::Create(bidder_url);
if (use_promise) {
// Cleanup between runs, to reset the stats to expected values.
EXPECT_EQ(kSuccess, LeaveInterestGroupAndVerify(/*owner=*/bidder_origin,
/*name=*/"cars"));
}
AttachInterestGroupObserver();
ASSERT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(/*owner=*/bidder_origin,
/*name=*/"cars")
.SetPriorityVector({{{"FOO", 2}}})
.SetPrioritySignalsOverrides({{{"FOO", 1}}})
.SetBiddingUrl(embedded_https_test_server().GetURL(
kBidderHost,
"/interest_group/"
"component_auction_bidding_argument_validator.js"))
.SetUpdateUrl(embedded_https_test_server().GetURL(
kBidderHost, "/not_found_update_url.json"))
.SetTrustedBiddingSignalsUrl(
embedded_https_test_server().GetURL(
kBidderHost,
"/interest_group/trusted_bidding_signals.json"))
.SetTrustedBiddingSignalsKeys({{"key1"}})
.SetUserBiddingSignals(
R"({"some":"json","stuff":{"here":[1,2]}})")
.SetAds({{{GURL("https://example.com/render"),
R"({"ad":"metadata","here":[1,2,3]})"}}})
.SetAdComponents(
{{{GURL("https://example.com/render-component"),
/*metadata=*/std::nullopt}}})
.Build()));
GURL top_level_seller_script_url = embedded_https_test_server().GetURL(
kTopLevelSellerHost,
"/interest_group/"
"component_auction_top_level_decision_argument_validator.js");
GURL component_seller_script_url = embedded_https_test_server().GetURL(
kComponentSellerHost,
"/interest_group/"
"component_auction_component_decision_argument_validator.js");
const url::Origin top_level_seller_origin =
url::Origin::Create(top_level_seller_script_url);
const url::Origin component_seller_origin =
url::Origin::Create(component_seller_script_url);
// For subresource bundle `directFromSellerSignals` to work, we need to
// navigate to a page that declares the subresource bundle resources we pass
// to those fields.
std::vector<NetworkResponder::SubresourceResponse>
top_level_subresource_responses = {
NetworkResponder::SubresourceResponse(
/*subresource_url=*/
"/direct_from_seller_signals?sellerSignals",
/*payload=*/
R"({"json": "for", "the": ["seller"]})"),
NetworkResponder::SubresourceResponse(
/*subresource_url=*/"/direct_from_seller_signals?"
"auctionSignals",
/*payload=*/
R"({"json": "for", "all": ["parties"]})"),
};
std::vector<NetworkResponder::SubresourceResponse>
component_subresource_responses = {
NetworkResponder::SubresourceResponse(
/*subresource_url=*/
"/direct_from_seller_signals?sellerSignals",
/*payload=*/
R"({"from": "component", "json": "for", "the": ["seller"]})"),
NetworkResponder::SubresourceResponse(
/*subresource_url=*/"/direct_from_seller_signals?"
"auctionSignals",
/*payload=*/
R"({"from": "component", "json": "for", "all": ["parties"]})"),
NetworkResponder::DirectFromSellerPerBuyerSignals(
bidder_origin, /*payload=*/
R"({"from": "component", "json": "for", "buyer": [1]})"),
};
std::vector<NetworkResponder::SubresourceBundle> bundles = {
NetworkResponder::SubresourceBundle(
/*bundle_url=*/embedded_https_test_server().GetURL(
kTopLevelSellerHost, "/0generated_bundle.wbn"),
/*subresources=*/top_level_subresource_responses),
{NetworkResponder::SubresourceBundle(
/*bundle_url=*/embedded_https_test_server().GetURL(
kComponentSellerHost, "/1generated_bundle.wbn"),
/*subresources=*/component_subresource_responses)}};
network_responder_->RegisterDirectFromSellerSignalsResponse(
/*bundles=*/bundles,
/*allow_origin=*/top_frame_origin.Serialize());
constexpr char kPagePath[] = "/page-with-bundles.html";
network_responder_->RegisterHtmlWithSubresourceBundles(
/*bundles=*/bundles,
/*page_url=*/kPagePath);
ASSERT_TRUE(NavigateToURL(shell(), embedded_https_test_server().GetURL(
kTopFrameHost, kPagePath)));
// For `directFromSellerSignalsHeaderAdSlot`, we need to make a network
// request to a resource that has signals in the Ad-Auction-Signals header
// value. We do this for both the top-level seller and the component seller.
const char kTopLevelHeaderSignalsPath[] =
"/top_level_header_direct_from_seller_signals.json";
const char kComponentHeaderSignalsPath[] =
"/component_header_direct_from_seller_signals.json";
// The actual body of both requests is just an empty JSON dict for the test,
// but it could be any arbitrary payload that the server wants to deliver
// alongside the header signals.
const char kHeaderSignalsBodyResponse[] = "{}";
// Intentionally use the same adSlot name for both responses -- responses
// from different sellers shouldn't conflict.
const char kTopLevelHeaderSignalsResponse[] = R"([{
"adSlot": "adSlot1",
"sellerSignals": {"json": "for", "the": ["seller"]},
"auctionSignals": {"json": "for", "all": ["parties"]}
}])";
const std::string kComponentHeaderSignalsResponse =
base::StringPrintf(R"([{
"adSlot": "adSlot1",
"sellerSignals": {"from": "component", "json": "for", "the": ["seller"]},
"auctionSignals": {
"from": "component", "json": "for", "all": ["parties"]},
"perBuyerSignals": {
"%s": {"from": "component", "json": "for", "buyer": [1]}
}
}])",
bidder_origin.Serialize().c_str());
network_responder_->RegisterNetworkResponse(
kTopLevelHeaderSignalsPath, kHeaderSignalsBodyResponse,
"application/json",
/*extra_response_headers=*/
{{"Access-Control-Allow-Origin", top_frame_origin.Serialize()},
{"Ad-Auction-Signals", kTopLevelHeaderSignalsResponse}});
network_responder_->RegisterNetworkResponse(
kComponentHeaderSignalsPath, kHeaderSignalsBodyResponse,
"application/json",
/*extra_response_headers=*/
{{"Access-Control-Allow-Origin", top_frame_origin.Serialize()},
{"Ad-Auction-Signals", kComponentHeaderSignalsResponse}});
EXPECT_TRUE(ExecJs(web_contents()->GetPrimaryMainFrame(),
content::JsReplace("fetch($1, {adAuctionHeaders: true})",
embedded_https_test_server().GetURL(
kTopLevelSellerHost,
kTopLevelHeaderSignalsPath))));
EXPECT_TRUE(ExecJs(web_contents()->GetPrimaryMainFrame(),
content::JsReplace("fetch($1, {adAuctionHeaders: true})",
embedded_https_test_server().GetURL(
kComponentSellerHost,
kComponentHeaderSignalsPath))));
// In addition to defining maybePromise(), set `componentSeller` and
// `componentBuyer` to avoid hitting JsReplace's limit of 10 substitutions
// below.
ASSERT_TRUE(
ExecJs(shell(),
MaybePromiseFunction(use_promise) +
JsReplace("componentSeller = $1;",
url::Origin::Create(component_seller_script_url)) +
JsReplace("componentBuyer = $1;", bidder_origin)));
TestFencedFrameURLMappingResultObserver observer;
ConvertFencedFrameURNToURL(
GURL(RunAuctionAndWait(
JsReplace(R"(
{
seller: $1,
decisionLogicURL: $2,
trustedScoringSignalsURL: $3,
auctionSignals: maybePromise(["top-level auction signals"]),
sellerSignals: maybePromise(["top-level seller signals"]),
$4: maybePromise($5),
sellerTimeout: 30000,
reportingTimeout: 3000,
// Note the 100ms delay for this promise. It's just to make sure that the time
// from inputs resolving -> auction resolved winds up in a different histogram
// bucket than the time from auction start -> auction resolved.
perBuyerSignals: maybePromise(
{[componentBuyer]: ["top-level buyer signals"]}),
perBuyerTimeouts: maybePromise({[componentBuyer]: 11000, '*': 15000}),
perBuyerCumulativeTimeouts: maybePromise(
{[componentBuyer]: 11100, '*': 15100}),
perBuyerPrioritySignals: {'*': {foo: 3}},
perBuyerCurrencies: {'*': 'MXN', [componentSeller]: 'CAD'},
componentAuctions: [{
seller: componentSeller,
decisionLogicURL: $6,
trustedScoringSignalsURL: $7,
interestGroupBuyers: [componentBuyer],
auctionSignals: maybePromise(["component auction signals"]),
sellerSignals: maybePromise(["component seller signals"]),
$8: maybePromise($9),
sellerTimeout: 20000,
reportingTimeout: 2000,
perBuyerSignals: maybePromise(
{[componentBuyer]: ["component buyer signals"]}),
perBuyerTimeouts: maybePromise({[componentBuyer]: 20000}),
perBuyerCumulativeTimeouts: maybePromise({[componentBuyer]: 20100}),
perBuyerPrioritySignals: {[componentBuyer]: {bar: 1}, '*': {BaZ: -2}},
perBuyerCurrencies: maybePromise({[componentBuyer]: 'USD'}),
sellerCurrency: 'CAD',
executionMode: 'group-by-origin',
}],
})",
top_level_seller_origin, top_level_seller_script_url,
embedded_https_test_server().GetURL(
kTopLevelSellerHost,
"/interest_group/trusted_scoring_signals.json"),
TopLevelUseHeaderDirectFromSellerSignals()
? "directFromSellerSignalsHeaderAdSlot"
: "directFromSellerSignals",
TopLevelUseHeaderDirectFromSellerSignals()
? "adSlot1"
: embedded_https_test_server()
.GetURL(kTopLevelSellerHost,
"/direct_from_seller_signals")
.spec(),
component_seller_script_url,
embedded_https_test_server().GetURL(
kComponentSellerHost,
"/interest_group/trusted_scoring_signals2.json"),
ComponentUseHeaderDirectFromSellerSignals()
? "directFromSellerSignalsHeaderAdSlot"
: "directFromSellerSignals",
ComponentUseHeaderDirectFromSellerSignals()
? "adSlot1"
: embedded_https_test_server()
.GetURL(kComponentSellerHost,
"/direct_from_seller_signals")
.spec()))
.ExtractString()),
&observer);
EXPECT_EQ(GURL("https://example.com/render"), observer.mapped_url());
// Run URN to URL mapping callback manually to trigger sending reports, and
// validate the right URLs are requested. Do this instead of navigating to
// the URN because the validation logic checks a fixed URL for this test,
// and don't want to send a random request to port 80 on localhost, which is
// what example.com is mapped to.
observer.on_navigate_callback().Run();
WaitForUrl(embedded_https_test_server().GetURL(
kTopLevelSellerHost, "/echo?report_top_level_seller"));
WaitForUrl(embedded_https_test_server().GetURL(
kComponentSellerHost, "/echo?report_component_seller"));
WaitForUrl(embedded_https_test_server().GetURL(kBidderHost,
"/echo?report_bidder"));
WaitForAccessObserved({
{"global", TestInterestGroupObserver::kJoin, bidder_origin, "cars"},
{"1", TestInterestGroupObserver::kLoaded, bidder_origin, "cars"},
{"1", TestInterestGroupObserver::kBid, bidder_origin, "cars", 2.0,
"USD"},
{"2", TestInterestGroupObserver::kTopLevelBid, bidder_origin, "cars",
42.0, "CAD", component_seller_origin},
{"2", TestInterestGroupObserver::kWin, bidder_origin, "cars",
/*bid=*/std::nullopt, /*bid_currency=*/std::nullopt,
component_seller_origin},
});
// Ensure that TimeFromInputsResolvedToAuctionResolved was recorded, and is
// a value less than TimeToResolve since we had promise-based values to wait
// on.
content::FetchHistogramsFromChildProcesses();
histogram_tester.ExpectTotalCount(
"Ads.InterestGroup.Auction.TimeFromInputsResolvedToAuctionResolved", 1);
histogram_tester.ExpectTotalCount("Ads.InterestGroup.Auction.TimeToResolve",
1);
EXPECT_LT(0u, histogram_tester.GetTotalSum(
"Ads.InterestGroup.Auction.TimeToResolve"));
// If this fails, either the auction mechanics ran amazingly fast (flake),
// or someone added or changed an AuctionHandleFunction and forgot to call
// AuctionHandle::OnResolved (bug).
EXPECT_LT(0u, histogram_tester.GetTotalSum(
"Ads.InterestGroup.Auction."
"TimeFromInputsResolvedToAuctionResolved"));
}
}
INSTANTIATE_TEST_SUITE_P(
All,
InterestGroupComponentWorkletValidationBrowserTest,
testing::Combine(testing::Bool(), testing::Bool()),
[](const testing::TestParamInfo<std::tuple<bool, bool>>& info) {
return base::StringPrintf(
"%s_%s",
std::get<0>(info.param)
? "top_level_header_direct_from_seller_signals"
: "top_level_subresource_bundle_direct_from_seller_signals",
std::get<1>(info.param)
? "component_header_direct_from_seller_signals"
: "component_subresource_bundle_direct_from_seller_signals");
});
// TODO(crbug.com/40266734): Remove this test once old names are no longer
// supported.
// Same as ComponentAuctionValidateWorkletParameters, but using deprecated names
// like decisionLogicUrl.
IN_PROC_BROWSER_TEST_F(
InterestGroupBrowserTest,
ComponentAuctionValidateWorkletParametersOldRunAdAuctionNames) {
// Use different hostnames for each participant.
//
// Match assignments in above test as closely as possible, to make scripts
// similar.
constexpr char kBidderHost[] = "a.test";
constexpr char kTopLevelSellerHost[] = "b.test";
constexpr char kTopFrameHost[] = "c.test";
constexpr char kComponentSellerHost[] = "d.test";
content_browser_client_->AddToAllowList({url::Origin::Create(
embedded_https_test_server().GetURL(kComponentSellerHost, "/"))});
for (bool use_promise : {false, true}) {
SCOPED_TRACE(use_promise);
const url::Origin top_frame_origin = url::Origin::Create(
embedded_https_test_server().GetURL(kTopFrameHost, "/echo"));
GURL bidder_url = embedded_https_test_server().GetURL(kBidderHost, "/echo");
ASSERT_TRUE(NavigateToURL(shell(), bidder_url));
url::Origin bidder_origin = url::Origin::Create(bidder_url);
if (use_promise) {
// Cleanup between runs, to reset the stats to expected values.
EXPECT_EQ(kSuccess, LeaveInterestGroupAndVerify(/*owner=*/bidder_origin,
/*name=*/"cars"));
}
ASSERT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(/*owner=*/bidder_origin,
/*name=*/"cars")
.SetPriorityVector({{{"FOO", 2}}})
.SetPrioritySignalsOverrides({{{"FOO", 1}}})
.SetBiddingUrl(embedded_https_test_server().GetURL(
kBidderHost,
"/interest_group/"
"component_auction_bidding_argument_validator.js"))
.SetUpdateUrl(embedded_https_test_server().GetURL(
kBidderHost, "/not_found_update_url.json"))
.SetTrustedBiddingSignalsUrl(
embedded_https_test_server().GetURL(
kBidderHost,
"/interest_group/trusted_bidding_signals.json"))
.SetTrustedBiddingSignalsKeys({{"key1"}})
.SetUserBiddingSignals(
R"({"some":"json","stuff":{"here":[1,2]}})")
.SetAds({{{GURL("https://example.com/render"),
R"({"ad":"metadata","here":[1,2,3]})"}}})
.SetAdComponents(
{{{GURL("https://example.com/render-component"),
/*metadata=*/std::nullopt}}})
.Build()));
// For `directFromSellerSignals` to work, we need to navigate to a page that
// declares the subresource bundle resources we pass to those fields.
GURL top_level_seller_script_url = embedded_https_test_server().GetURL(
kTopLevelSellerHost,
"/interest_group/"
"component_auction_top_level_decision_argument_validator.js");
GURL component_seller_script_url = embedded_https_test_server().GetURL(
kComponentSellerHost,
"/interest_group/"
"component_auction_component_decision_argument_validator.js");
const url::Origin top_level_seller_origin =
url::Origin::Create(top_level_seller_script_url);
const url::Origin component_seller_origin =
url::Origin::Create(component_seller_script_url);
std::vector<NetworkResponder::SubresourceResponse>
top_level_subresource_responses = {
NetworkResponder::SubresourceResponse(
/*subresource_url=*/
"/direct_from_seller_signals?sellerSignals",
/*payload=*/
R"({"json": "for", "the": ["seller"]})"),
NetworkResponder::SubresourceResponse(
/*subresource_url=*/"/direct_from_seller_signals?"
"auctionSignals",
/*payload=*/
R"({"json": "for", "all": ["parties"]})"),
};
std::vector<NetworkResponder::SubresourceResponse>
component_subresource_responses = {
NetworkResponder::SubresourceResponse(
/*subresource_url=*/
"/direct_from_seller_signals?sellerSignals",
/*payload=*/
R"({"from": "component", "json": "for", "the": ["seller"]})"),
NetworkResponder::SubresourceResponse(
/*subresource_url=*/"/direct_from_seller_signals?"
"auctionSignals",
/*payload=*/
R"({"from": "component", "json": "for", "all": ["parties"]})"),
NetworkResponder::DirectFromSellerPerBuyerSignals(
bidder_origin, /*payload=*/
R"({"from": "component", "json": "for", "buyer": [1]})"),
};
std::vector<NetworkResponder::SubresourceBundle> bundles = {
NetworkResponder::SubresourceBundle(
/*bundle_url=*/embedded_https_test_server().GetURL(
kTopLevelSellerHost, "/0generated_bundle.wbn"),
/*subresources=*/top_level_subresource_responses),
{NetworkResponder::SubresourceBundle(
/*bundle_url=*/embedded_https_test_server().GetURL(
kComponentSellerHost, "/1generated_bundle.wbn"),
/*subresources=*/component_subresource_responses)}};
network_responder_->RegisterDirectFromSellerSignalsResponse(
/*bundles=*/bundles,
/*allow_origin=*/top_frame_origin.Serialize());
constexpr char kPagePath[] = "/page-with-bundles.html";
network_responder_->RegisterHtmlWithSubresourceBundles(
/*bundles=*/bundles,
/*page_url=*/kPagePath);
ASSERT_TRUE(NavigateToURL(shell(), embedded_https_test_server().GetURL(
kTopFrameHost, kPagePath)));
ASSERT_TRUE(ExecJs(shell(), MaybePromiseFunction(use_promise)));
TestFencedFrameURLMappingResultObserver observer;
ConvertFencedFrameURNToURL(
GURL(RunAuctionAndWait(
JsReplace(
R"(
{
seller: $1,
decisionLogicUrl: $2,
trustedScoringSignalsUrl: $3,
auctionSignals: maybePromise(["top-level auction signals"]),
sellerSignals: maybePromise(["top-level seller signals"]),
directFromSellerSignals: maybePromise($4),
sellerTimeout: 30000,
reportingTimeout: 3000,
perBuyerSignals: maybePromise({$8: ["top-level buyer signals"]}),
perBuyerTimeouts: maybePromise({$8: 11000, '*': 15000}),
perBuyerCumulativeTimeouts: maybePromise({$8: 11100, '*': 15100}),
perBuyerPrioritySignals: {'*': {foo: 3}},
perBuyerCurrencies: {'*': 'MXN', $5: 'CAD'},
componentAuctions: [{
seller: $5,
decisionLogicUrl: $6,
trustedScoringSignalsUrl: $7,
interestGroupBuyers: [$8],
auctionSignals: maybePromise(["component auction signals"]),
sellerSignals: maybePromise(["component seller signals"]),
directFromSellerSignals: maybePromise($9),
sellerTimeout: 20000,
reportingTimeout: 2000,
perBuyerSignals: maybePromise({$8: ["component buyer signals"]}),
perBuyerTimeouts: maybePromise({$8: 20000}),
perBuyerCumulativeTimeouts: maybePromise({$8: 20100}),
perBuyerPrioritySignals: {$8: {bar: 1}, '*': {BaZ: -2}},
perBuyerCurrencies: maybePromise({$8: 'USD'}),
sellerCurrency: 'CAD',
executionMode: 'group-by-origin',
}],
})",
top_level_seller_origin, top_level_seller_script_url,
embedded_https_test_server().GetURL(
kTopLevelSellerHost,
"/interest_group/trusted_scoring_signals.json"),
embedded_https_test_server().GetURL(
kTopLevelSellerHost, "/direct_from_seller_signals"),
url::Origin::Create(component_seller_script_url),
component_seller_script_url,
embedded_https_test_server().GetURL(
kComponentSellerHost,
"/interest_group/trusted_scoring_signals2.json"),
bidder_origin,
embedded_https_test_server().GetURL(
kComponentSellerHost, "/direct_from_seller_signals")))
.ExtractString()),
&observer);
EXPECT_EQ(GURL("https://example.com/render"), observer.mapped_url());
// Run URN to URL mapping callback manually to trigger sending reports, and
// validate the right URLs are requested. Do this instead of navigating to
// the URN because the validation logic checks a fixed URL for this test,
// and don't want to send a random request to port 80 on localhost, which is
// what example.com is mapped to.
observer.on_navigate_callback().Run();
WaitForUrl(embedded_https_test_server().GetURL(
kTopLevelSellerHost, "/echo?report_top_level_seller"));
WaitForUrl(embedded_https_test_server().GetURL(
kComponentSellerHost, "/echo?report_component_seller"));
WaitForUrl(embedded_https_test_server().GetURL(kBidderHost,
"/echo?report_bidder"));
}
}
// TODO(crbug.com/40266734): Remove this test once old names are no longer
// supported.
// Same as ComponentAuctionValidateWorkletParameters, but using deprecated names
// like decisionLogicUrl and new names like decisionLogicURL together, using the
// same values for each.
IN_PROC_BROWSER_TEST_F(
InterestGroupBrowserTest,
ComponentAuctionValidateWorkletParametersOldRunAdAuctionNamesAndNewNames) {
// Use different hostnames for each participant.
//
// Match assignments in above test as closely as possible, to make scripts
// similar.
constexpr char kBidderHost[] = "a.test";
constexpr char kTopLevelSellerHost[] = "b.test";
constexpr char kTopFrameHost[] = "c.test";
constexpr char kComponentSellerHost[] = "d.test";
content_browser_client_->AddToAllowList({url::Origin::Create(
embedded_https_test_server().GetURL(kComponentSellerHost, "/"))});
for (bool use_promise : {false, true}) {
SCOPED_TRACE(use_promise);
const url::Origin top_frame_origin = url::Origin::Create(
embedded_https_test_server().GetURL(kTopFrameHost, "/echo"));
GURL bidder_url = embedded_https_test_server().GetURL(kBidderHost, "/echo");
ASSERT_TRUE(NavigateToURL(shell(), bidder_url));
url::Origin bidder_origin = url::Origin::Create(bidder_url);
if (use_promise) {
// Cleanup between runs, to reset the stats to expected values.
EXPECT_EQ(kSuccess, LeaveInterestGroupAndVerify(/*owner=*/bidder_origin,
/*name=*/"cars"));
}
ASSERT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(/*owner=*/bidder_origin,
/*name=*/"cars")
.SetPriorityVector({{{"FOO", 2}}})
.SetPrioritySignalsOverrides({{{"FOO", 1}}})
.SetBiddingUrl(embedded_https_test_server().GetURL(
kBidderHost,
"/interest_group/"
"component_auction_bidding_argument_validator.js"))
.SetUpdateUrl(embedded_https_test_server().GetURL(
kBidderHost, "/not_found_update_url.json"))
.SetTrustedBiddingSignalsUrl(
embedded_https_test_server().GetURL(
kBidderHost,
"/interest_group/trusted_bidding_signals.json"))
.SetTrustedBiddingSignalsKeys({{"key1"}})
.SetUserBiddingSignals(
R"({"some":"json","stuff":{"here":[1,2]}})")
.SetAds({{{GURL("https://example.com/render"),
R"({"ad":"metadata","here":[1,2,3]})"}}})
.SetAdComponents(
{{{GURL("https://example.com/render-component"),
/*metadata=*/std::nullopt}}})
.Build()));
// For `directFromSellerSignals` to work, we need to navigate to a page that
// declares the subresource bundle resources we pass to those fields.
GURL top_level_seller_script_url = embedded_https_test_server().GetURL(
kTopLevelSellerHost,
"/interest_group/"
"component_auction_top_level_decision_argument_validator.js");
GURL component_seller_script_url = embedded_https_test_server().GetURL(
kComponentSellerHost,
"/interest_group/"
"component_auction_component_decision_argument_validator.js");
const url::Origin top_level_seller_origin =
url::Origin::Create(top_level_seller_script_url);
const url::Origin component_seller_origin =
url::Origin::Create(component_seller_script_url);
std::vector<NetworkResponder::SubresourceResponse>
top_level_subresource_responses = {
NetworkResponder::SubresourceResponse(
/*subresource_url=*/
"/direct_from_seller_signals?sellerSignals",
/*payload=*/
R"({"json": "for", "the": ["seller"]})"),
NetworkResponder::SubresourceResponse(
/*subresource_url=*/"/direct_from_seller_signals?"
"auctionSignals",
/*payload=*/
R"({"json": "for", "all": ["parties"]})"),
};
std::vector<NetworkResponder::SubresourceResponse>
component_subresource_responses = {
NetworkResponder::SubresourceResponse(
/*subresource_url=*/
"/direct_from_seller_signals?sellerSignals",
/*payload=*/
R"({"from": "component", "json": "for", "the": ["seller"]})"),
NetworkResponder::SubresourceResponse(
/*subresource_url=*/"/direct_from_seller_signals?"
"auctionSignals",
/*payload=*/
R"({"from": "component", "json": "for", "all": ["parties"]})"),
NetworkResponder::DirectFromSellerPerBuyerSignals(
bidder_origin, /*payload=*/
R"({"from": "component", "json": "for", "buyer": [1]})"),
};
std::vector<NetworkResponder::SubresourceBundle> bundles = {
NetworkResponder::SubresourceBundle(
/*bundle_url=*/embedded_https_test_server().GetURL(
kTopLevelSellerHost, "/0generated_bundle.wbn"),
/*subresources=*/top_level_subresource_responses),
{NetworkResponder::SubresourceBundle(
/*bundle_url=*/embedded_https_test_server().GetURL(
kComponentSellerHost, "/1generated_bundle.wbn"),
/*subresources=*/component_subresource_responses)}};
network_responder_->RegisterDirectFromSellerSignalsResponse(
/*bundles=*/bundles,
/*allow_origin=*/top_frame_origin.Serialize());
constexpr char kPagePath[] = "/page-with-bundles.html";
network_responder_->RegisterHtmlWithSubresourceBundles(
/*bundles=*/bundles,
/*page_url=*/kPagePath);
ASSERT_TRUE(NavigateToURL(shell(), embedded_https_test_server().GetURL(
kTopFrameHost, kPagePath)));
ASSERT_TRUE(ExecJs(shell(), MaybePromiseFunction(use_promise)));
TestFencedFrameURLMappingResultObserver observer;
ConvertFencedFrameURNToURL(
GURL(RunAuctionAndWait(
JsReplace(
R"(
{
seller: $1,
decisionLogicURL: $2,
decisionLogicUrl: $2,
trustedScoringSignalsURL: $3,
trustedScoringSignalsUrl: $3,
auctionSignals: maybePromise(["top-level auction signals"]),
sellerSignals: maybePromise(["top-level seller signals"]),
directFromSellerSignals: maybePromise($4),
sellerTimeout: 30000,
reportingTimeout: 3000,
perBuyerSignals: maybePromise({$8: ["top-level buyer signals"]}),
perBuyerTimeouts: maybePromise({$8: 11000, '*': 15000}),
perBuyerCumulativeTimeouts: maybePromise({$8: 11100, '*': 15100}),
perBuyerPrioritySignals: {'*': {foo: 3}},
perBuyerCurrencies: {'*': 'MXN', $5: 'CAD'},
componentAuctions: [{
seller: $5,
decisionLogicURL: $6,
decisionLogicUrl: $6,
trustedScoringSignalsURL: $7,
trustedScoringSignalsUrl: $7,
interestGroupBuyers: [$8],
auctionSignals: maybePromise(["component auction signals"]),
sellerSignals: maybePromise(["component seller signals"]),
directFromSellerSignals: maybePromise($9),
sellerTimeout: 20000,
reportingTimeout: 2000,
perBuyerSignals: maybePromise({$8: ["component buyer signals"]}),
perBuyerTimeouts: maybePromise({$8: 20000}),
perBuyerCumulativeTimeouts: maybePromise({$8: 20100}),
perBuyerPrioritySignals: {$8: {bar: 1}, '*': {BaZ: -2}},
perBuyerCurrencies: maybePromise({$8: 'USD'}),
sellerCurrency: 'CAD',
executionMode: 'group-by-origin',
}],
})",
top_level_seller_origin, top_level_seller_script_url,
embedded_https_test_server().GetURL(
kTopLevelSellerHost,
"/interest_group/trusted_scoring_signals.json"),
embedded_https_test_server().GetURL(
kTopLevelSellerHost, "/direct_from_seller_signals"),
url::Origin::Create(component_seller_script_url),
component_seller_script_url,
embedded_https_test_server().GetURL(
kComponentSellerHost,
"/interest_group/trusted_scoring_signals2.json"),
bidder_origin,
embedded_https_test_server().GetURL(
kComponentSellerHost, "/direct_from_seller_signals")))
.ExtractString()),
&observer);
EXPECT_EQ(GURL("https://example.com/render"), observer.mapped_url());
// Run URN to URL mapping callback manually to trigger sending reports, and
// validate the right URLs are requested. Do this instead of navigating to
// the URN because the validation logic checks a fixed URL for this test,
// and don't want to send a random request to port 80 on localhost, which is
// what example.com is mapped to.
observer.on_navigate_callback().Run();
WaitForUrl(embedded_https_test_server().GetURL(
kTopLevelSellerHost, "/echo?report_top_level_seller"));
WaitForUrl(embedded_https_test_server().GetURL(
kComponentSellerHost, "/echo?report_component_seller"));
WaitForUrl(embedded_https_test_server().GetURL(kBidderHost,
"/echo?report_bidder"));
}
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
SellerWorkletThrowsFailsAuction) {
GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
ASSERT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"))
.SetTrustedBiddingSignalsUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/trusted_bidding_signals.json"))
.SetTrustedBiddingSignalsKeys({{"key1"}})
.SetAds({{{GURL("https://example.com/render"),
R"({"ad":"metadata","here":[1,2,3]})"}}})
.Build()));
EXPECT_EQ(base::Value(),
RunAuctionAndWait(JsReplace(
R"(
{
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
})",
test_origin,
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic_throws.js"))));
}
// JSON fields of joinAdInterestGroup() and runAdAuction() should support
// non-object types, like numbers.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
JoinInterestGroupNonObjectJSONFields) {
// These scripts are generated by this test.
constexpr char kBiddingLogicPath[] =
"/interest_group/non_object_bidding_argument_validator.js";
constexpr char kDecisionLogicPath[] =
"/interest_group/non_object_decision_argument_validator.js";
constexpr char kTrustedBiddingSignalsPath[] =
"/interest_group/non_object_bidding_signals.json";
const GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
// In the below JavaScript, if fields are incorrectly passed in as a string
// ("2") instead of a number (2), JSON.stringify() will wrap it in another
// layer of quotes, causing the test to fail. The order of properties produced
// by stringify() isn't guaranteed by the ECMAScript standard, but some sites
// depend on the V8 behavior of serializing in declaration order.
constexpr char kBiddingLogicScript[] = R"(
function generateBid(
interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals,
unusedBrowserSignals) {
validateInterestGroup(interestGroup);
validateAuctionSignals(auctionSignals);
validatePerBuyerSignals(perBuyerSignals);
validateTrustedBiddingSignals(trustedBiddingSignals);
const ad = interestGroup.ads[0];
return {'ad': ad, 'bid': 1, 'render': ad.renderURL};
}
function validateInterestGroup(interestGroup) {
const userBiddingSignalsJSON =
JSON.stringify(interestGroup.userBiddingSignals);
if (userBiddingSignalsJSON !== '1')
throw 'Wrong userBiddingSignals ' + userBiddingSignalsJSON;
if (interestGroup.ads.length !== 1)
throw 'Wrong ads.length ' + ads.length;
const adMetadataJSON = JSON.stringify(interestGroup.ads[0].metadata);
if (adMetadataJSON !== '2')
throw 'Wrong ads[0].metadata ' + adMetadataJSON;
}
function validateAuctionSignals(auctionSignals) {
const auctionSignalsJSON = JSON.stringify(auctionSignals);
if (auctionSignalsJSON !== '3')
throw 'Wrong auctionSignals ' + auctionSignalsJSON;
}
function validatePerBuyerSignals(perBuyerSignals) {
const perBuyerSignalsJson = JSON.stringify(perBuyerSignals);
if (perBuyerSignalsJson !== '5')
throw 'Wrong perBuyerSignals ' + perBuyerSignalsJson;
}
function validateTrustedBiddingSignals(trustedBiddingSignals) {
const trustedBiddingSignalsJSON = JSON.stringify(trustedBiddingSignals);
if (trustedBiddingSignalsJSON !== '{"key1":0}')
throw 'Wrong trustedBiddingSignals ' + trustedBiddingSignalsJSON;
}
)";
constexpr char kDecisionLogicScript[] = R"(
function scoreAd(
adMetadata, bid, auctionConfig, unusedTrustedScoringSignals,
unusedBrowserSignals) {
validateAdMetadata(adMetadata);
validateAuctionConfig(auctionConfig);
return bid;
}
function validateAdMetadata(adMetadata) {
const adMetadataJSON = JSON.stringify(adMetadata);
if (adMetadataJSON !==
'{"renderURL":"https://example.com/render",' +
'"renderUrl":"https://example.com/render",' +
'"metadata":2}')
throw 'Wrong adMetadata ' + adMetadataJSON;
}
function validateAuctionConfig(auctionConfig) {
const auctionSignalsJSON = JSON.stringify(auctionConfig.auctionSignals);
if (auctionSignalsJSON !== '3')
throw 'Wrong auctionSignals ' + auctionSignalsJSON;
const sellerSignalsJSON = JSON.stringify(auctionConfig.sellerSignals);
if (sellerSignalsJSON !== '4')
throw 'Wrong sellerSignals ' + sellerSignalsJSON;
const perBuyerSignalsJson = JSON.stringify(auctionConfig.perBuyerSignals);
if (!perBuyerSignalsJson.includes('a.test') ||
!perBuyerSignalsJson.includes('5')) {
throw 'Wrong perBuyerSignals ' + perBuyerSignalsJson;
}
}
)";
network_responder_->RegisterNetworkResponse(
kBiddingLogicPath, kBiddingLogicScript, "application/javascript");
network_responder_->RegisterNetworkResponse(
kDecisionLogicPath, kDecisionLogicScript, "application/javascript");
network_responder_->RegisterNetworkResponse(
kTrustedBiddingSignalsPath, R"({"key1":0})", "application/json");
EXPECT_EQ("done",
EvalJs(shell(), JsReplace(
R"(
(async function() {
try {
await navigator.joinAdInterestGroup(
{
name: 'cars',
owner: $1,
trustedBiddingSignalsURL: $2,
trustedBiddingSignalsKeys: ['key1'],
biddingLogicURL: $3,
userBiddingSignals: 1,
ads: [{renderURL:"https://example.com/render", metadata:2}],
},
/*joinDurationSec=*/1000);
} catch (e) {
return e.toString();
}
return 'done';
})())",
test_origin,
embedded_https_test_server().GetURL(
"a.test", kTrustedBiddingSignalsPath),
embedded_https_test_server().GetURL(
"a.test", kBiddingLogicPath))));
EXPECT_EQ(
"https://example.com/render",
RunAuctionAndWaitForUrl(JsReplace(
R"(
{
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
auctionSignals: 3,
sellerSignals: 4,
perBuyerSignals: {$1: 5}
})",
test_origin,
embedded_https_test_server().GetURL("a.test", kDecisionLogicPath))));
}
// Test for auctionSignals, perBuyerSignals, and sellerSignals being passed to
// runAdAuction as promises.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, PromiseSignals) {
// These scripts are generated by this test.
constexpr char kBiddingLogicPath[] =
"/interest_group/test_generated_bidding_argument_validator.js";
constexpr char kDecisionLogicPath[] =
"/interest_group/test_generated_decision_argument_validator.js";
const GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
// In the below JavaScript, if fields are incorrectly passed in as a string
// ("2") instead of a number (2), JSON.stringify() will wrap it in another
// layer of quotes, causing the test to fail.
constexpr char kBiddingLogicScript[] = R"(
function generateBid(
interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals,
unusedBrowserSignals) {
validateAuctionSignals(auctionSignals);
if (perBuyerSignals !== 5)
throw 'Wrong perBuyerSignals ' + JSON.stringify(perBuyerSignals);
const ad = interestGroup.ads[0];
return {'ad': ad, 'bid': 1, 'render': ad.renderURL};
}
function validateAuctionSignals(auctionSignals) {
const auctionSignalsJSON = JSON.stringify(auctionSignals);
if (auctionSignalsJSON !== '3')
throw 'Wrong auctionSignals ' + auctionSignalsJSON;
}
)";
constexpr char kDecisionLogicScript[] = R"(
function scoreAd(
adMetadata, bid, auctionConfig, unusedTrustedScoringSignals,
unusedBrowserSignals) {
validateAuctionConfig(auctionConfig);
return bid;
}
function validateAuctionConfig(auctionConfig) {
const auctionSignalsJSON = JSON.stringify(auctionConfig.auctionSignals);
if (auctionSignalsJSON !== '3')
throw 'Wrong auctionSignals ' + auctionSignalsJSON;
const sellerSignalsJSON = JSON.stringify(auctionConfig.sellerSignals);
if (sellerSignalsJSON !== '4')
throw 'Wrong sellerSignals ' + sellerSignalsJSON;
let ok = false;
const perBuyerSignalsJson = JSON.stringify(auctionConfig.perBuyerSignals);
for (key in auctionConfig.perBuyerSignals) {
if (key.startsWith("https://a.test")) {
ok = (auctionConfig.perBuyerSignals[key] === 5);
} else {
throw 'Wrong key in perBuyerSignals ' + perBuyerSignalsJson;
}
}
if (!ok) {
throw 'Wrong perBuyerSignals ' + perBuyerSignalsJson;
}
}
)";
network_responder_->RegisterNetworkResponse(
kBiddingLogicPath, kBiddingLogicScript, "application/javascript");
network_responder_->RegisterNetworkResponse(
kDecisionLogicPath, kDecisionLogicScript, "application/javascript");
EXPECT_EQ("done", EvalJs(shell(), JsReplace(
R"(
(async function() {
try {
await navigator.joinAdInterestGroup(
{
name: 'cars',
owner: $1,
biddingLogicURL: $2,
ads: [{renderURL:"https://example.com/render", metadata:2}],
},
/*joinDurationSec=*/100);
} catch (e) {
return e.toString();
}
return 'done';
})())",
test_origin,
embedded_https_test_server().GetURL(
"a.test", kBiddingLogicPath))));
EXPECT_EQ(
"https://example.com/render",
RunAuctionAndWaitForUrl(JsReplace(
R"(
{
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
auctionSignals: new Promise((resolve, reject) => {
setTimeout(
() => { resolve(3); }, 1)
}),
sellerSignals: new Promise((resolve, reject) => {
setTimeout(
() => { resolve(4); }, 1)
}),
perBuyerSignals: new Promise((resolve, reject) => {
setTimeout(
() => { resolve({$1: 5}); }, 1)
})
})",
test_origin,
embedded_https_test_server().GetURL("a.test", kDecisionLogicPath))));
}
// Test for abort before promises resolved.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, PromiseAborted) {
// These scripts are generated by this test.
constexpr char kBiddingLogicPath[] =
"/interest_group/test_generated_bidding_argument_validator.js";
constexpr char kDecisionLogicPath[] =
"/interest_group/test_generated_decision_argument_validator.js";
const GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
constexpr char kBiddingLogicScript[] = R"(
function generateBid(
interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals,
unusedBrowserSignals) {
validateAuctionSignals(auctionSignals);
const ad = interestGroup.ads[0];
return {'ad': ad, 'bid': 1, 'render': ad.renderURL};
}
function validateAuctionSignals(auctionSignals) {
const auctionSignalsJSON = JSON.stringify(auctionSignals);
if (auctionSignalsJSON !== '3')
throw 'Wrong auctionSignals ' + auctionSignalsJSON;
}
)";
constexpr char kDecisionLogicScript[] = R"(
function scoreAd(
adMetadata, bid, auctionConfig, unusedTrustedScoringSignals,
unusedBrowserSignals) {
validateAuctionConfig(auctionConfig);
return bid;
}
function validateAuctionConfig(auctionConfig) {
const auctionSignalsJSON = JSON.stringify(auctionConfig.auctionSignals);
if (auctionSignalsJSON !== '3')
throw 'Wrong auctionSignals ' + auctionSignalsJSON;
const sellerSignalsJSON = JSON.stringify(auctionConfig.sellerSignals);
if (sellerSignalsJSON !== '4')
throw 'Wrong sellerSignals ' + sellerSignalsJSON;
}
)";
network_responder_->RegisterNetworkResponse(
kBiddingLogicPath, kBiddingLogicScript, "application/javascript");
network_responder_->RegisterNetworkResponse(
kDecisionLogicPath, kDecisionLogicScript, "application/javascript");
EXPECT_EQ("done", EvalJs(shell(), JsReplace(
R"(
(async function() {
try {
await navigator.joinAdInterestGroup(
{
name: 'cars',
owner: $1,
biddingLogicURL: $2,
ads: [{renderURL:"https://example.com/render", metadata:2}],
},
/*joinDurationSec=*/100);
} catch (e) {
return e.toString();
}
return 'done';
})())",
test_origin,
embedded_https_test_server().GetURL(
"a.test", kBiddingLogicPath))));
std::string script = JsReplace(
R"(
(async function() {
let controller = new AbortController();
let adPromise = navigator.runAdAuction({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
auctionSignals: new Promise((resolve, reject) => {
setTimeout(
() => { resolve(3); }, 1)
}),
sellerSignals: new Promise((resolve, reject) => {
setTimeout(
() => { resolve(4); }, 1)
}),
perBuyerSignals: {$1: 5},
signal: controller.signal
});
controller.abort('manual cancel');
return await adPromise;
})())",
test_origin,
embedded_https_test_server().GetURL("a.test", kDecisionLogicPath));
EXPECT_EQ("a JavaScript error: \"manual cancel\"\n",
EvalJs(shell(), script).ExtractError());
}
// Test for auctionSignals, perBuyerSignals, directFromSellerSignals, and
// sellerSignals being passed to runAdAuction as promises... which resolve to
// nothing.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, PromiseSignalsNothing) {
// These scripts are generated by this test.
constexpr char kBiddingLogicPath[] =
"/interest_group/test_generated_bidding_argument_validator.js";
constexpr char kDecisionLogicPath[] =
"/interest_group/test_generated_decision_argument_validator.js";
const GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
// Need to fetch a URL with adAuctionHeaders: true for the auction to succeed
// when using directFromSellerSignalsHeaderAdSlot -- it doesn't matter which
// resource is fetched.
EXPECT_TRUE(ExecJs(
web_contents()->GetPrimaryMainFrame(),
content::JsReplace("fetch($1, {adAuctionHeaders: true})", test_url)));
constexpr char kBiddingLogicScript[] = R"(
function generateBid(
interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals,
unusedBrowserSignals, directFromSellerSignals) {
validateAuctionSignals(auctionSignals);
validateDirectFromSellerSignals(directFromSellerSignals);
const ad = interestGroup.ads[0];
if (perBuyerSignals !== null)
throw 'perBuyerSignals in generateBid not null!';
return {'ad': ad, 'bid': 1, 'render': ad.renderURL};
}
function validateAuctionSignals(auctionSignals) {
if (auctionSignals !== null)
throw 'auctionSignals in generateBid not null!';
}
function validateDirectFromSellerSignals(directFromSellerSignals) {
const perBuyerSignalsJSON =
JSON.stringify(directFromSellerSignals.perBuyerSignals);
if (perBuyerSignalsJSON !== 'null') {
throw 'Wrong directFromSellerSignals.perBuyerSignals ' +
perBuyerSignalsJSON;
}
const auctionSignalsJSON =
JSON.stringify(directFromSellerSignals.auctionSignals);
if (auctionSignalsJSON !== 'null') {
throw 'Wrong directFromSellerSignals.auctionSignals ' +
auctionSignalsJSON;
}
}
)";
constexpr char kDecisionLogicScript[] = R"(
function scoreAd(
adMetadata, bid, auctionConfig, unusedTrustedScoringSignals,
unusedBrowserSignals, directFromSellerSignals) {
validateAuctionConfig(auctionConfig);
validateDirectFromSellerSignals(directFromSellerSignals);
return bid;
}
function validateAuctionConfig(auctionConfig) {
if ('auctionSignals' in auctionConfig)
throw 'Have auctionSignals in scoreAd auctionConfig!';
if ('sellerSignals' in auctionConfig)
throw 'Have sellerSignals in scoreAd auctionConfig!';
if ('perBuyerSignals' in auctionConfig)
throw 'Have perBuyerSignals in scoreAd auctionConfig!';
if ('directFromSellerSignals' in auctionConfig)
throw 'Have directFromSellerSignals in scoreAd auctionConfig!';
}
function validateDirectFromSellerSignals(directFromSellerSignals) {
const sellerSignalsJSON =
JSON.stringify(directFromSellerSignals.sellerSignals);
if (sellerSignalsJSON !== 'null') {
throw 'Wrong directFromSellerSignals.sellerSignals ' +
sellerSignalsJSON;
}
const auctionSignalsJSON =
JSON.stringify(directFromSellerSignals.auctionSignals);
if (auctionSignalsJSON !== 'null') {
throw 'Wrong directFromSellerSignals.auctionSignals ' +
auctionSignalsJSON;
}
}
)";
network_responder_->RegisterNetworkResponse(
kBiddingLogicPath, kBiddingLogicScript, "application/javascript");
network_responder_->RegisterNetworkResponse(
kDecisionLogicPath, kDecisionLogicScript, "application/javascript");
EXPECT_EQ("done", EvalJs(shell(), JsReplace(
R"(
(async function() {
try {
await navigator.joinAdInterestGroup(
{
name: 'cars',
owner: $1,
biddingLogicURL: $2,
ads: [{renderURL:"https://example.com/render", metadata:2}],
},
/*joinDurationSec=*/100);
} catch (e) {
return e.toString();
}
return 'done';
})())",
test_origin,
embedded_https_test_server().GetURL(
"a.test", kBiddingLogicPath))));
for (bool header_direct_from_seller_signals : {false, true}) {
SCOPED_TRACE(header_direct_from_seller_signals);
EXPECT_EQ(
"https://example.com/render",
RunAuctionAndWaitForUrl(JsReplace(
R"(
{
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
auctionSignals: new Promise((resolve, reject) => {
setTimeout(
() => { resolve(); }, 1)
}),
sellerSignals: new Promise((resolve, reject) => {
setTimeout(
() => { resolve(undefined); }, 1)
}),
perBuyerSignals: new Promise((resolve, reject) => {
setTimeout(
() => { resolve(undefined); }, 1)
}),
$3: new Promise((resolve, reject) => {
setTimeout(
() => { resolve(null); }, 1)
}),
})",
test_origin,
embedded_https_test_server().GetURL("a.test", kDecisionLogicPath),
header_direct_from_seller_signals
? "directFromSellerSignalsHeaderAdSlot"
: "directFromSellerSignals")));
}
}
// Test for perBuyerTimeouts and perBuyerCumulativeTimeouts being passed to
// runAdAuction as promises.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
PromiseBuyerTimeoutsAndCumulativeBuyerTimeouts) {
// These scripts are generated by this test.
constexpr char kBiddingLogicPath[] =
"/interest_group/test_generated_bidding_argument_validator.js";
constexpr char kDecisionLogicPath[] =
"/interest_group/test_generated_decision_argument_validator.js";
const GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
// In the below JavaScript, if fields are incorrectly passed in as a string
// ("2") instead of a number (2), JSON.stringify() will wrap it in another
// layer of quotes, causing the test to fail.
constexpr char kBiddingLogicScript[] = R"(
function generateBid(
interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals,
unusedBrowserSignals) {
const ad = interestGroup.ads[0];
return {'ad': ad, 'bid': 1, 'render': ad.renderURL};
}
)";
constexpr char kDecisionLogicScript[] = R"(
function scoreAd(
adMetadata, bid, auctionConfig, unusedTrustedScoringSignals,
unusedBrowserSignals) {
validatePerBuyerTimeouts(auctionConfig.perBuyerTimeouts);
validatePerBuyerCumulativeTimeouts(auctionConfig.perBuyerCumulativeTimeouts);
return bid;
}
function validatePerBuyerTimeouts(perBuyerTimeouts) {
const perBuyerTimeoutsJSON = JSON.stringify(perBuyerTimeouts);
let ok = 0;
for (key in perBuyerTimeouts) {
if (key.startsWith("https://a.test") && perBuyerTimeouts[key] === 5000) {
++ok;
} else if (key === "https://b.test" && perBuyerTimeouts[key] === 6000) {
++ok;
} else if (key === '*' &&
perBuyerTimeouts[key] === 5600) {
++ok;
} else {
throw 'Wrong key in perBuyerTimeouts ' + perBuyerTimeoutsJSON;
}
}
if (ok !== 3) {
throw 'Wrong perBuyerTimeouts ' + perBuyerTimeoutsJSON;
}
}
function validatePerBuyerCumulativeTimeouts(perBuyerCumulativeTimeouts) {
const perBuyerCumulativeTimeoutsJSON =
JSON.stringify(perBuyerCumulativeTimeouts);
let ok = 0;
for (key in perBuyerCumulativeTimeouts) {
if (key.startsWith("https://a.test") &&
perBuyerCumulativeTimeouts[key] === 7000) {
++ok;
} else if (key === "https://c.test" &&
perBuyerCumulativeTimeouts[key] === 8000) {
++ok;
} else if (key === '*' &&
perBuyerCumulativeTimeouts[key] === 7600) {
++ok;
} else {
throw 'Wrong key in perCumulativeBuyerTimeouts ' +
perBuyerCumulativeTimeoutsJSON;
}
}
if (ok !== 3) {
throw 'Wrong perCumulativeBuyerTimeouts ' + perBuyerCumulativeTimeoutsJSON;
}
}
)";
network_responder_->RegisterNetworkResponse(
kBiddingLogicPath, kBiddingLogicScript, "application/javascript");
network_responder_->RegisterNetworkResponse(
kDecisionLogicPath, kDecisionLogicScript, "application/javascript");
EXPECT_EQ("done", EvalJs(shell(), JsReplace(
R"(
(async function() {
try {
await navigator.joinAdInterestGroup(
{
name: 'cars',
owner: $1,
biddingLogicURL: $2,
ads: [{renderURL:"https://example.com/render", metadata:2}],
},
/*joinDurationSec=*/100);
} catch (e) {
return e.toString();
}
return 'done';
})())",
test_origin,
embedded_https_test_server().GetURL(
"a.test", kBiddingLogicPath))));
EXPECT_EQ(
"https://example.com/render",
RunAuctionAndWaitForUrl(JsReplace(
R"(
{
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
perBuyerTimeouts: new Promise((resolve, reject) => {
setTimeout(
() => { resolve({$1: 5000, 'https://b.test': 6000, '*': 5600}); }, 1)
}),
perBuyerCumulativeTimeouts: new Promise((resolve, reject) => {
setTimeout(
() => { resolve({$1: 7000, 'https://c.test': 8000, '*': 7600}); }, 1)
})
})",
test_origin,
embedded_https_test_server().GetURL("a.test", kDecisionLogicPath))));
}
// Make sure that qutting with a live auction doesn't crash.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, QuitWithRunningAuction) {
URLLoaderMonitor url_loader_monitor;
GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
GURL hanging_url = embedded_https_test_server().GetURL("a.test", "/hung");
url::Origin hanging_origin = url::Origin::Create(hanging_url);
EXPECT_EQ(kSuccess, JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/hanging_origin,
/*name=*/"cars")
.SetBiddingUrl(hanging_url)
.SetAds({{{GURL("https://example.com/render"),
R"({"ad":"metadata","here":[1,2]})"}}})
.Build()));
ExecuteScriptAsync(shell(), JsReplace(R"(
navigator.runAdAuction({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1]
});
)",
hanging_origin, hanging_url));
WaitForUrl(embedded_https_test_server().GetURL("/hung"));
}
// These tests validate the `updateURL` and navigator.updateAdInterestGroups()
// functionality.
// The server JSON updates a number of updatable fields.
//
// The join and update events should be reported to devtools.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, UpdateAndReportToDevtools) {
GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin test_origin = url::Origin::Create(test_url);
ASSERT_TRUE(NavigateToURL(shell(), test_url));
AttachInterestGroupObserver();
// The server JSON updates all fields that can be updated.
constexpr char kUpdateUrlPath[] = "/interest_group/update_partial.json";
network_responder_->RegisterNetworkResponse(
kUpdateUrlPath, base::StringPrintf(R"({
"biddingLogicURL": "%s/interest_group/new_bidding_logic.js",
"trustedBiddingSignalsURL":
"%s/interest_group/new_trusted_bidding_signals_url.json",
"trustedBiddingSignalsKeys": ["new_key"],
"executionMode": "group-by-origin",
"ads": [{"renderURL": "%s/new_ad_render_url",
"metadata": {"new_a": "b"}
}]
})",
test_origin.Serialize().c_str(),
test_origin.Serialize().c_str(),
test_origin.Serialize().c_str()));
ASSERT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(/*owner=*/test_origin,
/*name=*/"cars")
.SetPriorityVector({{{"one", 1}}})
.SetPrioritySignalsOverrides({{{"two", 2}}})
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"))
.SetUpdateUrl(
embedded_https_test_server().GetURL("a.test", kUpdateUrlPath))
.SetTrustedBiddingSignalsUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/trusted_bidding_signals.json"))
.SetTrustedBiddingSignalsKeys({{"key1"}})
.SetUserBiddingSignals(
R"({"some":"json","stuff":{"here":[1,2]}})")
.SetAds({{{GURL("https://example.com/render"),
R"({"ad":"metadata","here":[1,2,3]})"}}})
.Build()));
EXPECT_EQ("done", UpdateInterestGroupsInJS());
WaitForAccessObserved({
{"global", TestInterestGroupObserver::kJoin, test_origin, "cars"},
{"global", TestInterestGroupObserver::kUpdate, test_origin, "cars"},
});
WaitForInterestGroupsSatisfying(
test_origin,
base::BindLambdaForTesting([](scoped_refptr<StorageInterestGroups>
groups) {
if (groups->size() != 1) {
return false;
}
const auto& group = groups->GetInterestGroups()[0]->interest_group;
return group.name == "cars" && group.priority == 0.0 &&
group.execution_mode ==
blink::InterestGroup::ExecutionMode::kGroupedByOriginMode &&
group.bidding_url.has_value() &&
group.bidding_url->path() ==
"/interest_group/new_bidding_logic.js" &&
group.trusted_bidding_signals_url.has_value() &&
group.trusted_bidding_signals_url->path() ==
"/interest_group/new_trusted_bidding_signals_url.json" &&
group.trusted_bidding_signals_keys.has_value() &&
group.trusted_bidding_signals_keys->size() == 1 &&
group.trusted_bidding_signals_keys.value()[0] == "new_key" &&
group.ads.has_value() && group.ads->size() == 1 &&
GURL(group.ads.value()[0].render_url()).path() ==
"/new_ad_render_url" &&
group.ads.value()[0].metadata == "{\"new_a\":\"b\"}";
}));
}
// The server JSON updates a number of updatable fields, using the deprecated
// `dailyUpdateUrl` field.
//
// TODO(crbug.com/40258629): Remove once support for `dailyUpdateUrl` is
// removed.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, DeprecatedDailyUpdateUrl) {
set_daily_update_url_ = true;
set_update_url_ = false;
GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin test_origin = url::Origin::Create(test_url);
ASSERT_TRUE(NavigateToURL(shell(), test_url));
// The server JSON updates all fields that can be updated.
constexpr char kUpdateUrlPath[] = "/interest_group/update_partial.json";
network_responder_->RegisterNetworkResponse(
kUpdateUrlPath, base::StringPrintf(R"({
"biddingLogicURL": "%s/interest_group/new_bidding_logic.js",
"trustedBiddingSignalsURL":
"%s/interest_group/new_trusted_bidding_signals_url.json",
"trustedBiddingSignalsKeys": ["new_key"],
"executionMode": "groupByOrigin",
"ads": [{"renderURL": "%s/new_ad_render_url",
"metadata": {"new_a": "b"}
}]
})",
test_origin.Serialize().c_str(),
test_origin.Serialize().c_str(),
test_origin.Serialize().c_str()));
ASSERT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(/*owner=*/test_origin,
/*name=*/"cars")
.SetPriorityVector({{{"one", 1}}})
.SetPrioritySignalsOverrides({{{"two", 2}}})
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"))
.SetUpdateUrl(
embedded_https_test_server().GetURL("a.test", kUpdateUrlPath))
.SetTrustedBiddingSignalsUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/trusted_bidding_signals.json"))
.SetTrustedBiddingSignalsKeys({{"key1"}})
.SetUserBiddingSignals(
R"({"some":"json","stuff":{"here":[1,2]}})")
.SetAds({{{GURL("https://example.com/render"),
R"({"ad":"metadata","here":[1,2,3]})"}}})
.Build()));
EXPECT_EQ("done", UpdateInterestGroupsInJS());
WaitForInterestGroupsSatisfying(
test_origin,
base::BindLambdaForTesting([](scoped_refptr<StorageInterestGroups>
groups) {
if (groups->size() != 1) {
return false;
}
const auto& group = groups->GetInterestGroups()[0]->interest_group;
return group.name == "cars" && group.priority == 0.0 &&
group.execution_mode ==
blink::InterestGroup::ExecutionMode::kGroupedByOriginMode &&
group.bidding_url.has_value() &&
group.bidding_url->path() ==
"/interest_group/new_bidding_logic.js" &&
group.trusted_bidding_signals_url.has_value() &&
group.trusted_bidding_signals_url->path() ==
"/interest_group/new_trusted_bidding_signals_url.json" &&
group.trusted_bidding_signals_keys.has_value() &&
group.trusted_bidding_signals_keys->size() == 1 &&
group.trusted_bidding_signals_keys.value()[0] == "new_key" &&
group.ads.has_value() && group.ads->size() == 1 &&
GURL(group.ads.value()[0].render_url()).path() ==
"/new_ad_render_url" &&
group.ads.value()[0].metadata == "{\"new_a\":\"b\"}";
}));
}
// The server JSON updates a number of updatable fields, using the deprecated
// `dailyUpdateUrl` field, and the `updateURL` field. Both have the same value.
// This is what consumers are expected to due during migration from one name to
// the other, so best to make sure it works.
//
// TODO(crbug.com/40258629): Remove once support for `dailyUpdateUrl` is
// removed.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
UpdateUrlAndDeprecatedDailyUpdateUrl) {
set_daily_update_url_ = true;
set_update_url_ = true;
GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin test_origin = url::Origin::Create(test_url);
ASSERT_TRUE(NavigateToURL(shell(), test_url));
// The server JSON updates all fields that can be updated.
constexpr char kUpdateUrlPath[] = "/interest_group/update_partial.json";
network_responder_->RegisterNetworkResponse(
kUpdateUrlPath, base::StringPrintf(R"({
"biddingLogicURL": "%s/interest_group/new_bidding_logic.js",
"trustedBiddingSignalsURL":
"%s/interest_group/new_trusted_bidding_signals_url.json",
"trustedBiddingSignalsKeys": ["new_key"],
"executionMode": "groupByOrigin",
"ads": [{"renderURL": "%s/new_ad_render_url",
"metadata": {"new_a": "b"}
}]
})",
test_origin.Serialize().c_str(),
test_origin.Serialize().c_str(),
test_origin.Serialize().c_str()));
ASSERT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(/*owner=*/test_origin,
/*name=*/"cars")
.SetPriorityVector({{{"one", 1}}})
.SetPrioritySignalsOverrides({{{"two", 2}}})
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"))
.SetUpdateUrl(
embedded_https_test_server().GetURL("a.test", kUpdateUrlPath))
.SetTrustedBiddingSignalsUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/trusted_bidding_signals.json"))
.SetTrustedBiddingSignalsKeys({{"key1"}})
.SetUserBiddingSignals(
R"({"some":"json","stuff":{"here":[1,2]}})")
.SetAds({{{GURL("https://example.com/render"),
R"({"ad":"metadata","here":[1,2,3]})"}}})
.Build()));
EXPECT_EQ("done", UpdateInterestGroupsInJS());
WaitForInterestGroupsSatisfying(
test_origin,
base::BindLambdaForTesting([](scoped_refptr<StorageInterestGroups>
groups) {
if (groups->size() != 1) {
return false;
}
const auto& group = groups->GetInterestGroups()[0]->interest_group;
return group.name == "cars" && group.priority == 0.0 &&
group.execution_mode ==
blink::InterestGroup::ExecutionMode::kGroupedByOriginMode &&
group.bidding_url.has_value() &&
group.bidding_url->path() ==
"/interest_group/new_bidding_logic.js" &&
group.trusted_bidding_signals_url.has_value() &&
group.trusted_bidding_signals_url->path() ==
"/interest_group/new_trusted_bidding_signals_url.json" &&
group.trusted_bidding_signals_keys.has_value() &&
group.trusted_bidding_signals_keys->size() == 1 &&
group.trusted_bidding_signals_keys.value()[0] == "new_key" &&
group.ads.has_value() && group.ads->size() == 1 &&
GURL(group.ads.value()[0].render_url()).path() ==
"/new_ad_render_url" &&
group.ads.value()[0].metadata == "{\"new_a\":\"b\"}";
}));
}
// Updates can proceed even if the page that started the update isn't running
// anymore.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
UpdateAndNavigateAwayStillCompletes) {
GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin test_origin = url::Origin::Create(test_url);
ASSERT_TRUE(NavigateToURL(shell(), test_url));
// Start an update, then navigate to a different page. The update completes
// even though the page that started the update is gone.
constexpr char kUpdateUrlPath[] = "/interest_group/update_partial.json";
network_responder_->RegisterNetworkResponse(
kUpdateUrlPath, base::StringPrintf(R"({
"ads": [{"renderURL": "%s/new_ad_render_url",
"metadata": {"new_a": "b"}
}]
})",
test_origin.Serialize().c_str()));
ASSERT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(/*owner=*/test_origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"))
.SetUpdateUrl(
embedded_https_test_server().GetURL("a.test", kUpdateUrlPath))
.SetTrustedBiddingSignalsUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/trusted_bidding_signals.json"))
.SetTrustedBiddingSignalsKeys({{"key1"}})
.SetUserBiddingSignals(
R"({"some":"json","stuff":{"here":[1,2]}})")
.SetAds({{{GURL("https://example.com/render"),
R"({"ad":"metadata","here":[1,2,3]})"}}})
.Build()));
EXPECT_EQ("done", UpdateInterestGroupsInJS());
// Navigate away -- the update should still continue.
GURL test_url_b = embedded_https_test_server().GetURL("b.test", "/echo");
ASSERT_TRUE(NavigateToURL(shell(), test_url_b));
WaitForInterestGroupsSatisfying(
test_origin,
base::BindLambdaForTesting(
[](scoped_refptr<StorageInterestGroups> groups) {
if (groups->size() != 1) {
return false;
}
const auto& group = groups->GetInterestGroups()[0]->interest_group;
return group.name == "cars" && group.bidding_url.has_value() &&
group.bidding_url->path() ==
"/interest_group/bidding_logic.js" &&
group.update_url.has_value() &&
group.update_url->path() ==
"/interest_group/update_partial.json" &&
group.trusted_bidding_signals_url.has_value() &&
group.trusted_bidding_signals_url->path() ==
"/interest_group/trusted_bidding_signals.json" &&
group.trusted_bidding_signals_keys.has_value() &&
group.trusted_bidding_signals_keys->size() == 1 &&
group.trusted_bidding_signals_keys.value()[0] == "key1" &&
group.ads.has_value() && group.ads->size() == 1 &&
GURL(group.ads.value()[0].render_url()).path() ==
"/new_ad_render_url" &&
group.ads.value()[0].metadata == "{\"new_a\":\"b\"}";
}));
}
// Bidders' generateBid() scripts that run forever should timeout. They will not
// affect other bidders or fail the auction.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionWithCustomPerBuyerTimeouts) {
const char kHostA[] = "a.test";
const char kHostB[] = "b.test";
// Navigate to other bidder site, and add an interest group.
GURL bidder_b_url = embedded_https_test_server().GetURL(kHostB, "/echo");
url::Origin bidder_b_origin = url::Origin::Create(bidder_b_url);
ASSERT_TRUE(NavigateToURL(shell(), bidder_b_url));
GURL ad_url_b =
embedded_https_test_server().GetURL(kHostB, "/echo?render_shoes");
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
/*owner=*/bidder_b_origin,
/*name=*/"shoes",
/*priority=*/0.0,
/*execution_mode=*/
blink::InterestGroup::ExecutionMode::kCompatibilityMode,
/*bidding_url=*/
embedded_https_test_server().GetURL(
kHostB, "/interest_group/bidding_logic.js"),
/*ads=*/{{{ad_url_b, /*metadata=*/std::nullopt}}}));
GURL bidder_a_url =
embedded_https_test_server().GetURL(kHostA, "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), bidder_a_url));
url::Origin bidder_a_origin = url::Origin::Create(bidder_a_url);
GURL ad1_url_a =
embedded_https_test_server().GetURL(kHostA, "/echo?render_cars");
GURL ad2_url_a =
embedded_https_test_server().GetURL(kHostA, "/echo?render_bikes");
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
/*owner=*/bidder_a_origin,
/*name=*/"cars",
/*priority=*/0.0,
/*execution_mode=*/
blink::InterestGroup::ExecutionMode::kCompatibilityMode,
/*bidding_url=*/
embedded_https_test_server().GetURL(
kHostA, "/interest_group/bidding_logic_loop_forever.js"),
/*ads=*/{{{ad1_url_a, /*metadata=*/std::nullopt}}}));
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
/*owner=*/bidder_a_origin,
/*name=*/"bikes",
/*priority=*/0.0, /*execution_mode=*/
blink::InterestGroup::ExecutionMode::kCompatibilityMode,
/*bidding_url=*/
embedded_https_test_server().GetURL(
kHostA, "/interest_group/bidding_logic_loop_forever.js"),
/*ads=*/{{{ad2_url_a, /*metadata=*/std::nullopt}}}));
// Set per buyer timeout of bidder a to 1 ms, so that its generateBid()
// scripts which has an endless loop times out fast.
const std::string kTestPerBuyerTimeouts[] = {
JsReplace("{$1: 1}", bidder_a_origin),
JsReplace("{$1: 1, '*': 10000}", bidder_a_origin),
JsReplace("{$1: 10000, '*': 1}", bidder_b_origin),
};
for (const auto& test_per_buyer_timeouts : kTestPerBuyerTimeouts) {
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1, $3],
)",
bidder_a_origin,
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js"),
bidder_b_origin);
// Since test_per_buyer_timeout is JSON, it shouldn't be wrapped in quotes,
// so can't use JsReplace.
auction_config += base::StringPrintf("perBuyerTimeouts: %s}",
test_per_buyer_timeouts.c_str());
// Bidder b won the auction.
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad_url_b);
}
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionWithCustomSellerTimeout) {
const char kHostA[] = "a.test";
GURL test_url =
embedded_https_test_server().GetURL(kHostA, "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url =
embedded_https_test_server().GetURL(kHostA, "/echo?render_cars");
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
/*owner=*/test_origin,
/*name=*/"cars",
/*priority=*/0.0,
/*execution_mode=*/
blink::InterestGroup::ExecutionMode::kCompatibilityMode,
/*bidding_url=*/
embedded_https_test_server().GetURL(
kHostA, "/interest_group/bidding_logic.js"),
/*ads=*/{{{ad_url, /*metadata=*/std::nullopt}}}));
// The auction fails, since seller's scoreAd() script times out after 1 ms.
EXPECT_EQ(
base::Value(),
RunAuctionAndWait(JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
sellerTimeout: 1,
})",
test_origin,
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic_loop_forever.js"))));
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionWithExperimentGroupId) {
const char kPublisher[] = "a.test";
const char kBidder[] = "b.test";
const char kSeller[] = "c.test";
// Navigate to bidder site, and add an interest group.
GURL bidder_url = embedded_https_test_server().GetURL(kBidder, "/echo");
ASSERT_TRUE(NavigateToURL(shell(), bidder_url));
url::Origin bidder_origin = url::Origin::Create(bidder_url);
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/bidder_origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
kBidder, "/interest_group/bidding_logic.js"))
.SetTrustedBiddingSignalsUrl(embedded_https_test_server().GetURL(
kBidder, "/interest_group/trusted_bidding_signals.json"))
.SetTrustedBiddingSignalsKeys({{"key1"}})
.SetAds({{{GURL("https://example.com/render"),
R"({"ad":"metadata","here":[1,2]})"}}})
.Build()));
// Navigate to publisher.
ASSERT_TRUE(NavigateToURL(
shell(), embedded_https_test_server().GetURL(kPublisher, "/echo")));
GURL seller_logic_url = embedded_https_test_server().GetURL(
kSeller, "/interest_group/decision_logic.js");
const char kAuctionConfigTemplate[] = R"({
seller: $1,
decisionLogicURL: $2,
trustedScoringSignalsURL: $3,
interestGroupBuyers: [$4],
sellerExperimentGroupId: 8349,
perBuyerExperimentGroupIds: {'*': 3498},
})";
EXPECT_EQ("https://example.com/render",
RunAuctionAndWaitForUrl(JsReplace(
kAuctionConfigTemplate, url::Origin::Create(seller_logic_url),
seller_logic_url,
embedded_https_test_server().GetURL(
kSeller, "/interest_group/trusted_scoring_signals.json"),
bidder_origin)));
// Make sure that the right trusted signals URLs got fetched, incorporating
// the experiment group ID.
WaitForUrl(embedded_https_test_server().GetURL(
"/interest_group/trusted_bidding_signals.json?hostname=a.test&keys=key1"
"&interestGroupNames=cars&experimentGroupId=3498"));
WaitForUrl(embedded_https_test_server().GetURL(
"/interest_group/trusted_scoring_signals.json?hostname=a.test"
"&renderUrls=https%3A%2F%2Fexample.com%2Frender&experimentGroupId=8349"));
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionWithLargeExperimentGroupId) {
// https://crbug.com/1523625 --- make sure all 16 bits go through.
const char kPublisher[] = "a.test";
const char kBidder[] = "b.test";
const char kSeller[] = "c.test";
// Navigate to bidder site, and add an interest group.
GURL bidder_url = embedded_https_test_server().GetURL(kBidder, "/echo");
ASSERT_TRUE(NavigateToURL(shell(), bidder_url));
url::Origin bidder_origin = url::Origin::Create(bidder_url);
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/bidder_origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
kBidder, "/interest_group/bidding_logic.js"))
.SetTrustedBiddingSignalsUrl(embedded_https_test_server().GetURL(
kBidder, "/interest_group/trusted_bidding_signals.json"))
.SetTrustedBiddingSignalsKeys({{"key1"}})
.SetAds({{{GURL("https://example.com/render"),
R"({"ad":"metadata","here":[1,2]})"}}})
.Build()));
// Navigate to publisher.
ASSERT_TRUE(NavigateToURL(
shell(), embedded_https_test_server().GetURL(kPublisher, "/echo")));
GURL seller_logic_url = embedded_https_test_server().GetURL(
kSeller, "/interest_group/decision_logic.js");
const char kAuctionConfigTemplate[] = R"({
seller: $1,
decisionLogicURL: $2,
trustedScoringSignalsURL: $3,
interestGroupBuyers: [$4],
sellerExperimentGroupId: 50000,
perBuyerExperimentGroupIds: {'*': 40000},
})";
EXPECT_EQ("https://example.com/render",
RunAuctionAndWaitForUrl(JsReplace(
kAuctionConfigTemplate, url::Origin::Create(seller_logic_url),
seller_logic_url,
embedded_https_test_server().GetURL(
kSeller, "/interest_group/trusted_scoring_signals.json"),
bidder_origin)));
// Make sure that the right trusted signals URLs got fetched, incorporating
// the experiment group ID.
WaitForUrl(embedded_https_test_server().GetURL(
"/interest_group/trusted_bidding_signals.json?hostname=a.test&keys=key1"
"&interestGroupNames=cars&experimentGroupId=40000"));
WaitForUrl(embedded_https_test_server().GetURL(
"/interest_group/trusted_scoring_signals.json?hostname=a.test"
"&renderUrls=https%3A%2F%2Fexample.com%2Frender&experimentGroupId="
"50000"));
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionWithPerBuyerExperimentGroupId) {
const char kPublisher[] = "a.test";
const char kBidder[] = "b.test";
const char kBidder2[] = "d.test";
const char kSeller[] = "c.test";
// Navigate to bidder site, and add an interest group, then same for bidder 2.
GURL bidder_url = embedded_https_test_server().GetURL(kBidder, "/echo");
ASSERT_TRUE(NavigateToURL(shell(), bidder_url));
url::Origin bidder_origin = url::Origin::Create(bidder_url);
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/bidder_origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
kBidder, "/interest_group/bidding_logic.js"))
.SetTrustedBiddingSignalsUrl(embedded_https_test_server().GetURL(
kBidder, "/interest_group/trusted_bidding_signals.json"))
.SetTrustedBiddingSignalsKeys({{"key1"}})
.SetAds({{{GURL("https://example.com/render"),
R"({"ad":"metadata","here":[1,2]})"}}})
.Build()));
GURL bidder2_url = embedded_https_test_server().GetURL(kBidder2, "/echo");
ASSERT_TRUE(NavigateToURL(shell(), bidder2_url));
url::Origin bidder2_origin = url::Origin::Create(bidder2_url);
content_browser_client_->AddToAllowList({bidder2_origin});
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/bidder2_origin,
/*name=*/"cars_and_trucks")
.SetBiddingUrl(embedded_https_test_server().GetURL(
kBidder2, "/interest_group/bidding_logic.js"))
.SetTrustedBiddingSignalsUrl(embedded_https_test_server().GetURL(
kBidder2, "/interest_group/trusted_bidding_signals.json"))
.SetTrustedBiddingSignalsKeys({{"key2"}})
.SetAds({{{GURL("https://example.com/render"),
R"({"ad":"metadata","here":[1,2]})"}}})
.Build()));
// Navigate to publisher.
ASSERT_TRUE(NavigateToURL(
shell(), embedded_https_test_server().GetURL(kPublisher, "/echo")));
GURL seller_logic_url = embedded_https_test_server().GetURL(
kSeller, "/interest_group/decision_logic.js");
const char kAuctionConfigTemplate[] = R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$3, $4],
perBuyerExperimentGroupIds: {'*': 3498,
$4: 1203},
})";
EXPECT_EQ("https://example.com/render",
RunAuctionAndWaitForUrl(JsReplace(
kAuctionConfigTemplate, url::Origin::Create(seller_logic_url),
seller_logic_url, bidder_origin, bidder2_origin)));
// Make sure that the right trusted signals URLs got fetched, incorporating
// the experiment group IDs.
WaitForUrl(embedded_https_test_server().GetURL(
"/interest_group/trusted_bidding_signals.json?hostname=a.test&keys=key1"
"&interestGroupNames=cars&experimentGroupId=3498"));
WaitForUrl(embedded_https_test_server().GetURL(
"/interest_group/trusted_bidding_signals.json?hostname=a.test&keys=key2"
"&interestGroupNames=cars_and_trucks&experimentGroupId=1203"));
}
// Test that the active multi-bid limit is properly passed all the way through
// to the bidder worklet's browserSignals.
IN_PROC_BROWSER_TEST_F(InterestGroupMultiBidBrowserTest,
RunAdAuctionWithPerBuyerMultiBidLimit) {
const char kPublisher[] = "a.test";
const char kBidder[] = "b.test";
const char kBidder2[] = "d.test";
const char kSeller[] = "c.test";
GURL bidder_url = embedded_https_test_server().GetURL(kBidder, "/echo");
url::Origin bidder_origin = url::Origin::Create(bidder_url);
GURL bidder2_url = embedded_https_test_server().GetURL(kBidder2, "/echo");
url::Origin bidder2_origin = url::Origin::Create(bidder2_url);
GURL ad1_url = embedded_https_test_server().GetURL(kBidder, "/ad1");
GURL ad2_url = embedded_https_test_server().GetURL(kBidder2, "/ad2");
// Navigate to bidder site, and add an interest group.
ASSERT_TRUE(NavigateToURL(shell(), bidder_url));
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/bidder_origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
kBidder, "/interest_group/bidding_logic_multibid_feature.js"))
.SetAds({{{ad1_url, /*metadata=*/std::nullopt}}})
.Build()));
// Navigate to publisher and run an ad auction.
ASSERT_TRUE(
NavigateToURL(shell(), embedded_https_test_server().GetURL(
kPublisher, "/page_with_iframe.html")));
GURL seller_logic_url = embedded_https_test_server().GetURL(
kSeller, "/interest_group/decision_logic.js");
const char kAuctionConfigTemplate[] = R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$3, $4],
perBuyerMultiBidLimits: {'*': 2, $4: 10},
})";
RunAuctionAndWaitForURLAndNavigateIframe(
JsReplace(kAuctionConfigTemplate, url::Origin::Create(seller_logic_url),
seller_logic_url, bidder_origin, bidder2_origin),
ad1_url);
// Check that reporting routed the proper multiBidLimit --- kBidder should
// get the *, so 2.
WaitForUrl(
embedded_https_test_server().GetURL(kBidder, "/echoall?report_bidder2"));
// Now navigate to bidder2 and add its interest group.
ASSERT_TRUE(NavigateToURL(shell(), bidder2_url));
content_browser_client_->AddToAllowList({bidder2_origin});
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/bidder2_origin,
/*name=*/"cars_and_trucks")
.SetBiddingUrl(embedded_https_test_server().GetURL(
kBidder2,
"/interest_group/bidding_logic_multibid_feature.js"))
.SetAds({{{ad2_url, /*metadata=*/std::nullopt}}})
.Build()));
// Navigate to publisher and run auction again.
ASSERT_TRUE(
NavigateToURL(shell(), embedded_https_test_server().GetURL(
kPublisher, "/page_with_iframe.html")));
RunAuctionAndWaitForURLAndNavigateIframe(
JsReplace(kAuctionConfigTemplate, url::Origin::Create(seller_logic_url),
seller_logic_url, bidder_origin, bidder2_origin),
ad2_url);
// Expect kBidder2 to win and its limit is 10.
WaitForUrl(embedded_https_test_server().GetURL(kBidder2,
"/echoall?report_bidder10"));
}
// Validate that createAdRequest is available and be successfully called as part
// of PARAKEET.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, CreateAdRequestWorks) {
GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
EXPECT_EQ("NotSupportedError: createAdRequest API not yet implemented",
CreateAdRequestAndWait());
}
// Validate that finalizeAd is available and be successfully called as part of
// PARAKEET.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, FinalizeAdWorks) {
GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
// The finalize API relies on createAdRequest, until it is fully implemented
// we expect a createAdRequest failure initially.
EXPECT_EQ("NotSupportedError: createAdRequest API not yet implemented",
FinalizeAdAndWait());
}
// The bidder worklet is served from a private network, everything else from a
// public network. The auction should fail.
IN_PROC_BROWSER_TEST_F(InterestGroupPrivateNetworkBrowserTest,
BidderOnPrivateNetwork) {
URLLoaderMonitor url_loader_monitor;
// Learn the bidder IG, served from the loopback server.
GURL bidder_url = embedded_https_test_server().GetURL(
"b.test", "/interest_group/bidding_logic.js");
ASSERT_TRUE(NavigateToURL(
shell(), embedded_https_test_server().GetURL("b.test", "/echo")));
url::Origin bidder_origin = url::Origin::Create(bidder_url);
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
/*owner=*/bidder_origin,
/*name=*/"Cthulhu", /*priority=*/0.0, /*execution_mode=*/
blink::InterestGroup::ExecutionMode::kCompatibilityMode, bidder_url,
/*ads=*/
{{{GURL("https://example.com/render"),
/*metadata=*/std::nullopt}}}));
// Use `remote_test_server_` for all other URLs.
GURL test_url = remote_test_server_.GetURL("a.test", "/echo");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
EXPECT_EQ(base::Value(),
RunAuctionAndWait(JsReplace(
R"(
{
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$3]
}
)",
url::Origin::Create(test_url),
remote_test_server_.GetURL("a.test",
"/interest_group/decision_logic.js"),
bidder_origin)));
// The URLLoaderMonitor should have seen a request for the bidder URL, which
// should have been made from a public address space.
std::optional<network::ResourceRequest> bidder_request =
url_loader_monitor.GetRequestInfo(bidder_url);
ASSERT_TRUE(bidder_request);
EXPECT_EQ(
network::mojom::IPAddressSpace::kPublic,
bidder_request->trusted_params->client_security_state->ip_address_space);
const network::URLLoaderCompletionStatus& bidder_status =
url_loader_monitor.WaitForRequestCompletion(bidder_url);
EXPECT_EQ(net::ERR_BLOCKED_BY_PRIVATE_NETWORK_ACCESS_CHECKS,
bidder_status.error_code);
EXPECT_THAT(bidder_status.cors_error_status,
Optional(network::CorsErrorStatus(
network::mojom::CorsError::kInsecurePrivateNetwork,
network::mojom::IPAddressSpace::kUnknown,
network::mojom::IPAddressSpace::kLoopback)));
}
IN_PROC_BROWSER_TEST_F(InterestGroupPrivateNetworkBrowserTest,
SellerOnPrivateNetwork) {
GURL seller_url = embedded_https_test_server().GetURL(
"b.test", "/interest_group/decision_logic.js");
// Use `remote_test_server_` for all URLs except the seller worklet.
GURL test_url = remote_test_server_.GetURL("a.test", "/echo");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
// Need to set this up before the join, since joining instantiates the
// AdAuctionServiceImpl's URLLoaderFactory.
URLLoaderMonitor url_loader_monitor;
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
/*owner=*/test_origin,
/*name=*/"Cthulhu",
/*priority=*/0.0, /*execution_mode=*/
blink::InterestGroup::ExecutionMode::kCompatibilityMode,
/*bidding_url=*/
remote_test_server_.GetURL("a.test",
"/interest_group/bidding_logic.js"),
/*ads=*/
{{{GURL("https://example.com/render"),
/*metadata=*/std::nullopt}}}));
EXPECT_EQ(base::Value(),
RunAuctionAndWait(JsReplace(
R"(
{
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$3]
}
)",
url::Origin::Create(seller_url), seller_url, test_origin)));
// The URLLoaderMonitor should have seen a request for the seller URL. The
// request should have gone through the renderer's URLLoader, and inherited
// its IPAddressSpace, instead of passing its own.
std::optional<network::ResourceRequest> seller_request =
url_loader_monitor.GetRequestInfo(seller_url);
ASSERT_TRUE(seller_request);
EXPECT_FALSE(seller_request->trusted_params);
const network::URLLoaderCompletionStatus& seller_status =
url_loader_monitor.WaitForRequestCompletion(seller_url);
EXPECT_EQ(net::ERR_BLOCKED_BY_PRIVATE_NETWORK_ACCESS_CHECKS,
seller_status.error_code);
EXPECT_THAT(
seller_status.cors_error_status,
Optional(network::CorsErrorStatus(
network::mojom::CorsError::kLocalNetworkAccessPermissionDenied,
network::mojom::IPAddressSpace::kUnknown,
network::mojom::IPAddressSpace::kLoopback)));
}
// Have the auction and worklets server from public IPs, but send reports to a
// private network. The reports should be blocked.
IN_PROC_BROWSER_TEST_F(InterestGroupPrivateNetworkBrowserTest,
ReportToPrivateNetwork) {
// Use `remote_test_server_` exclusively with hostname "a.test" for root page
// and script URLs.
GURL test_url =
remote_test_server_.GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url = remote_test_server_.GetURL("c.test", "/echo");
// Use `embedded_https_test_server()` exclusively with hostname "b.test" for
// reports.
GURL bidder_report_to_url =
embedded_https_test_server().GetURL("b.test", "/bidder_report");
GURL seller_report_to_url =
embedded_https_test_server().GetURL("b.test", "/seller_report");
GURL bidder_debug_win_report_url = embedded_https_test_server().GetURL(
"b.test", "/bidder_report_debug_win_report");
GURL seller_debug_win_report_url = embedded_https_test_server().GetURL(
"b.test", "/seller_report_debug_win_report");
URLLoaderMonitor url_loader_monitor;
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
/*owner=*/test_origin,
/*name=*/bidder_report_to_url.spec(),
/*priority=*/0.0, /*execution_mode=*/
blink::InterestGroup::ExecutionMode::kCompatibilityMode,
/*bidding_url=*/
remote_test_server_.GetURL(
"a.test", "/interest_group/bidding_logic_report_to_name.js"),
/*ads=*/
{{{ad_url,
/*metadata=*/std::nullopt}}}));
RunAuctionAndWaitForURLAndNavigateIframe(
JsReplace(
R"(
{
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
sellerSignals: {reportTo: $3},
}
)",
test_origin,
remote_test_server_.GetURL(
"a.test",
"/interest_group/decision_logic_report_to_seller_signals.js"),
seller_report_to_url),
ad_url);
// Wait for both requests to be completed, and check their IPAddressSpace and
// make sure that they failed.
EXPECT_EQ(network::mojom::IPAddressSpace::kPublic,
url_loader_monitor.WaitForUrl(bidder_report_to_url)
.trusted_params->client_security_state->ip_address_space);
EXPECT_EQ(network::mojom::IPAddressSpace::kPublic,
url_loader_monitor.WaitForUrl(seller_report_to_url)
.trusted_params->client_security_state->ip_address_space);
for (const GURL& report_url :
{bidder_report_to_url, seller_report_to_url, bidder_debug_win_report_url,
seller_debug_win_report_url}) {
SCOPED_TRACE(report_url.spec());
const network::URLLoaderCompletionStatus& report_status =
url_loader_monitor.WaitForRequestCompletion(report_url);
EXPECT_EQ(net::ERR_BLOCKED_BY_PRIVATE_NETWORK_ACCESS_CHECKS,
report_status.error_code);
EXPECT_THAT(report_status.cors_error_status,
Optional(network::CorsErrorStatus(
network::mojom::CorsError::kInsecurePrivateNetwork,
network::mojom::IPAddressSpace::kUnknown,
network::mojom::IPAddressSpace::kLoopback)));
}
}
// Have all requests for an auction served from a public network, and all
// reports send there as well. The auction should succeed, and all reports
// should be sent.
IN_PROC_BROWSER_TEST_F(InterestGroupPrivateNetworkBrowserTest,
ReportToPublicNetwork) {
// Use `remote_test_server_` exclusively with hostname "a.test" for root page
// and script URLs.
GURL test_url =
remote_test_server_.GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL bidder_url = remote_test_server_.GetURL(
"a.test", "/interest_group/bidding_logic_report_to_name.js");
GURL trusted_bidding_signals_url = remote_test_server_.GetURL(
"a.test", "/interest_group/trusted_bidding_signals.json");
GURL seller_url = remote_test_server_.GetURL(
"a.test", "/interest_group/decision_logic_report_to_seller_signals.js");
GURL ad_url = remote_test_server_.GetURL("c.test", "/echo");
// While reports should be made to these URLs in this test, their results
// don't matter, so there's no need for a test server to respond to these URLs
// with anything other than errors.
GURL bidder_report_to_url =
remote_test_server_.GetURL("a.test", "/bidder_report");
GURL seller_report_to_url =
remote_test_server_.GetURL("a.test", "/seller_report");
GURL bidder_debug_win_report_url =
remote_test_server_.GetURL("a.test", "/bidder_report_debug_win_report");
GURL seller_debug_win_report_url =
remote_test_server_.GetURL("a.test", "/seller_report_debug_win_report");
URLLoaderMonitor url_loader_monitor;
GURL trusted_bidding_signals_url_with_query = remote_test_server_.GetURL(
"a.test",
base::StringPrintf("/interest_group/trusted_bidding_signals.json"
"?hostname=a.test&keys=key1&interestGroupNames=%s",
base::EscapeQueryParamValue(
bidder_report_to_url.spec(), /*use_plus=*/true)
.c_str()));
ASSERT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/bidder_report_to_url.spec())
.SetBiddingUrl(bidder_url)
.SetTrustedBiddingSignalsUrl(trusted_bidding_signals_url)
.SetTrustedBiddingSignalsKeys({{"key1"}})
.SetAds({{{ad_url, /*metadata=*/std::nullopt}}})
.Build()));
std::string auction_config = JsReplace(
R"(
{
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
sellerSignals: {reportTo: $3},
}
)",
test_origin,
remote_test_server_.GetURL(
"a.test",
"/interest_group/decision_logic_report_to_seller_signals.js"),
seller_report_to_url);
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad_url);
EXPECT_EQ(network::mojom::IPAddressSpace::kPublic,
url_loader_monitor.WaitForUrl(bidder_url)
.trusted_params->client_security_state->ip_address_space);
EXPECT_EQ(
network::mojom::IPAddressSpace::kPublic,
url_loader_monitor.WaitForUrl(trusted_bidding_signals_url_with_query)
.trusted_params->client_security_state->ip_address_space);
// Unlike the others, the request for the seller URL has an empty
// `trusted_params`, since it uses the renderer's untrusted URLLoader.
EXPECT_FALSE(url_loader_monitor.WaitForUrl(seller_url).trusted_params);
EXPECT_EQ(network::mojom::IPAddressSpace::kPublic,
url_loader_monitor.WaitForUrl(seller_report_to_url)
.trusted_params->client_security_state->ip_address_space);
EXPECT_EQ(network::mojom::IPAddressSpace::kPublic,
url_loader_monitor.WaitForUrl(seller_debug_win_report_url)
.trusted_params->client_security_state->ip_address_space);
for (const GURL& report_url :
{bidder_report_to_url, seller_report_to_url, bidder_debug_win_report_url,
seller_debug_win_report_url}) {
SCOPED_TRACE(report_url.spec());
EXPECT_EQ(network::mojom::IPAddressSpace::kPublic,
url_loader_monitor.WaitForUrl(report_url)
.trusted_params->client_security_state->ip_address_space);
}
// Check that all reports reached the server.
WaitForUrl(bidder_report_to_url);
WaitForUrl(seller_report_to_url);
WaitForUrl(bidder_debug_win_report_url);
WaitForUrl(seller_debug_win_report_url);
}
// Make sure that the IPAddressSpace of the frame that triggers the update is
// respected for the update request. Does this by adding an interest group,
// trying to update it from a public page. The update since updates are
// considered same-origin requests and the private networking logic exempts
// those from needing preflights. Checks the IPAddressSpace passed to the
// network layer, though, to make sure the correct one is passed in. Have to use
// two interest groups to avoid the delay between updates.
IN_PROC_BROWSER_TEST_F(InterestGroupPrivateNetworkBrowserTest,
UpdatePublicVsPrivateNetwork) {
const char kPubliclyUpdateGroupName[] = "Publicly updated group";
const char kLocallyUpdateGroupName[] = "Locally updated group";
GURL update_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/update_partial.json");
GURL initial_bidding_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/initial_bidding_logic.js");
GURL new_bidding_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/new_bidding_logic.js");
// The server JSON updates biddingLogicURL only.
network_responder_->RegisterNetworkResponse(update_url.path(),
JsReplace(R"(
{
"biddingLogicURL": $1
}
)",
new_bidding_url));
URLLoaderMonitor url_loader_monitor;
for (bool public_address_space : {true, false}) {
SCOPED_TRACE(public_address_space);
GURL test_url;
std::string group_name;
if (public_address_space) {
// This header treats a response from a server on a private IP as if the
// server were on public address space.
test_url = embedded_https_test_server().GetURL(
"a.test",
"/set-header?Content-Security-Policy: treat-as-public-address");
group_name = kPubliclyUpdateGroupName;
} else {
test_url = embedded_https_test_server().GetURL("a.test", "/echo");
group_name = kLocallyUpdateGroupName;
}
ASSERT_TRUE(NavigateToURL(shell(), test_url));
ASSERT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/url::Origin::Create(test_url), group_name)
.SetBiddingUrl(initial_bidding_url)
.SetUpdateUrl(update_url)
.SetAds({{{GURL("https://example.com/render"),
/*metadata=*/std::nullopt}}})
.Build()));
EXPECT_EQ("done", UpdateInterestGroupsInJS());
// Wait for the update request to be made, and check its IPAddressSpace.
url_loader_monitor.WaitForUrls();
const network::ResourceRequest& request =
url_loader_monitor.WaitForUrl(update_url);
ASSERT_TRUE(request.trusted_params->client_security_state);
if (public_address_space) {
EXPECT_EQ(
network::mojom::IPAddressSpace::kPublic,
request.trusted_params->client_security_state->ip_address_space);
} else {
EXPECT_EQ(
network::mojom::IPAddressSpace::kLoopback,
request.trusted_params->client_security_state->ip_address_space);
}
// Not the main purpose of this test, but it should be using a transient
// NetworkIsolationKey as well.
ASSERT_TRUE(request.trusted_params->isolation_info.network_isolation_key()
.IsTransient());
// The request should succeed in both cases, due to being same-origin.
EXPECT_EQ(
net::OK,
url_loader_monitor.WaitForRequestCompletion(update_url).error_code);
url_loader_monitor.ClearRequests();
}
// Wait for both groups to be updated. Have to wait because just because
// URLLoaderMonitor has seen the request completed successfully doesn't mean
// that the InterestGroup has been updated yet.
WaitForInterestGroupsSatisfying(
url::Origin::Create(initial_bidding_url),
base::BindLambdaForTesting(
[&](scoped_refptr<StorageInterestGroups> storage_groups) {
bool publicly_updated_group_updated = false;
bool locally_updated_group_updated = false;
for (const auto& storage_group :
storage_groups->GetInterestGroups()) {
const blink::InterestGroup& group = storage_group->interest_group;
if (group.name == kPubliclyUpdateGroupName) {
publicly_updated_group_updated =
(new_bidding_url == group.bidding_url);
} else {
EXPECT_EQ(group.name, kLocallyUpdateGroupName);
locally_updated_group_updated =
(new_bidding_url == group.bidding_url);
}
}
return publicly_updated_group_updated &&
locally_updated_group_updated;
}));
}
// Create three interest groups, each belonging to different origins. Update one
// on a private network, but delay its server response. Update the second on a
// public network. Update the final interest group on a private interest group
// -- it should be updated after the first two. After the server responds to the
// first update request, all updates should proceed, and succeed.
IN_PROC_BROWSER_TEST_F(InterestGroupPrivateNetworkBrowserTest,
PrivateNetProtectionsApplyToSubsequentUpdates) {
constexpr char kLocallyUpdateGroupName[] = "Locally updated group";
constexpr char kPubliclyUpdateGroupName[] = "Publicly updated group";
// The update for a.test happens locally and gets deferred, whereas the update
// for b.test and c.test are allowed to proceed immediately.
const GURL update_url_a = embedded_https_test_server().GetURL(
"a.test", kDeferredUpdateResponsePath);
const GURL update_url_b = embedded_https_test_server().GetURL(
"b.test", "/interest_group/update_partial_b.json");
const GURL update_url_c = embedded_https_test_server().GetURL(
"c.test", "/interest_group/update_partial_c.json");
constexpr char kInitialBiddingPath[] =
"/interest_group/initial_bidding_logic.js";
const GURL initial_bidding_url_a =
embedded_https_test_server().GetURL("a.test", kInitialBiddingPath);
const GURL initial_bidding_url_b =
embedded_https_test_server().GetURL("b.test", kInitialBiddingPath);
const GURL initial_bidding_url_c =
embedded_https_test_server().GetURL("c.test", kInitialBiddingPath);
constexpr char kNewBiddingPath[] = "/interest_group/new_bidding_logic.js";
const GURL new_bidding_url_a =
embedded_https_test_server().GetURL("a.test", kNewBiddingPath);
const GURL new_bidding_url_b =
embedded_https_test_server().GetURL("b.test", kNewBiddingPath);
const GURL new_bidding_url_c =
embedded_https_test_server().GetURL("c.test", kNewBiddingPath);
// The server JSON updates biddingLogicURL only.
constexpr char kUpdateContentTemplate[] = R"(
{
"biddingLogicURL": $1
}
)";
// a.test's response is delayed until later.
network_responder_->RegisterNetworkResponse(
update_url_b.path(),
JsReplace(kUpdateContentTemplate, new_bidding_url_b));
network_responder_->RegisterNetworkResponse(
update_url_c.path(),
JsReplace(kUpdateContentTemplate, new_bidding_url_c));
// First, create an interest group in a.test and start updating it from a
// private site. The update doesn't finish yet because the network response
// is delayed.
ASSERT_TRUE(NavigateToURL(
shell(), embedded_https_test_server().GetURL("a.test", "/echo")));
ASSERT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/url::Origin::Create(initial_bidding_url_a),
kLocallyUpdateGroupName)
.SetBiddingUrl(initial_bidding_url_a)
.SetUpdateUrl(update_url_a)
.SetAds({{{GURL("https://example.com/render"),
/*metadata=*/std::nullopt}}})
.Build()));
EXPECT_EQ("done", UpdateInterestGroupsInJS());
// Now, create an interest group in b.test and start updating it from a
// public site. The update will be delayed because the first interest group
// hasn't finished updating.
ASSERT_TRUE(NavigateToURL(
shell(),
embedded_https_test_server().GetURL(
"b.test",
"/set-header?Content-Security-Policy: treat-as-public-address")));
ASSERT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/url::Origin::Create(initial_bidding_url_b),
kPubliclyUpdateGroupName)
.SetBiddingUrl(initial_bidding_url_b)
.SetUpdateUrl(update_url_b)
.SetAds({{{GURL("https://example.com/render"),
/*metadata=*/std::nullopt}}})
.Build()));
EXPECT_EQ("done", UpdateInterestGroupsInJS());
// Finally, create and update the last interest group on a private network.
ASSERT_TRUE(NavigateToURL(
shell(), embedded_https_test_server().GetURL("c.test", "/echo")));
ASSERT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/url::Origin::Create(initial_bidding_url_c),
kLocallyUpdateGroupName)
.SetBiddingUrl(initial_bidding_url_c)
.SetUpdateUrl(update_url_c)
.SetAds({{{GURL("https://example.com/render"),
/*metadata=*/std::nullopt}}})
.Build()));
EXPECT_EQ("done", UpdateInterestGroupsInJS());
// Now, finish the first interest group update by responding to its update
// network request. All interest groups should be able to update now.
network_responder_->DoDeferredUpdateResponse(
JsReplace(kUpdateContentTemplate, new_bidding_url_a));
// Wait for all the interest groups to update.
WaitForInterestGroupsSatisfying(
url::Origin::Create(initial_bidding_url_a),
base::BindLambdaForTesting(
[&](scoped_refptr<StorageInterestGroups> storage_groups) {
return storage_groups->size() == 1 &&
storage_groups->GetInterestGroups()[0]
->interest_group.bidding_url == new_bidding_url_a;
}));
WaitForInterestGroupsSatisfying(
url::Origin::Create(initial_bidding_url_b),
base::BindLambdaForTesting(
[&](scoped_refptr<StorageInterestGroups> storage_groups) {
return storage_groups->size() == 1 &&
storage_groups->GetInterestGroups()[0]
->interest_group.bidding_url == new_bidding_url_b;
}));
WaitForInterestGroupsSatisfying(
url::Origin::Create(initial_bidding_url_c),
base::BindLambdaForTesting(
[&](scoped_refptr<StorageInterestGroups> storage_groups) {
return storage_groups->size() == 1 &&
storage_groups->GetInterestGroups()[0]
->interest_group.bidding_url == new_bidding_url_c;
}));
}
// Join interest groups with local (private) update URLs, and run auctions from
// both a a main frame loaded with public address space, and with a private
// address space. Check that the address space of the main frame is always used
// for the updated.
//
// Different interest groups (with different origins) are used for the public
// and private auction, to avoid running into update rate limits.
IN_PROC_BROWSER_TEST_F(InterestGroupPrivateNetworkBrowserTest,
PrivateNetProtectionsApplyToPostAuctionUpdates) {
// Fetches for the interest group-related scripts and updates are always
// local, it's where they're updated from that matters. Interest group A will
// be updated from an auction on a public origin, and B from a private one.
const url::Origin interest_group_a_origin =
embedded_https_test_server().GetOrigin("a.test");
const url::Origin interest_group_b_origin =
embedded_https_test_server().GetOrigin("b.test");
constexpr char kUpdatePath[] = "/interest_group/update_partial_a.json";
constexpr char kUpdateResponse[] = R"(
{
"ads": [{"renderURL": "https://example.com/render2"
}]
})";
// The server JSON updates the ads only. Both update URLs use the same path,
// so only need to add a response once
network_responder_->RegisterNetworkResponse(kUpdatePath, kUpdateResponse);
// The origin of the seller script URL doesn't matter for these tests. Use
// an origin other than the interest groups just to make clear there are no
// dependencies on a shared origin anywhere.
const GURL decision_logic_url = embedded_https_test_server().GetURL(
"c.test", "/interest_group/decision_logic.js");
const struct {
// All interest group URLs are derived from this.
url::Origin interest_group_origin;
bool run_auction_from_public_address_space;
GURL auction_url;
} kTestCases[] = {
{interest_group_a_origin,
/*run_auction_from_public_address_space=*/true,
// This header treats a response from a server on a private IP as if the
// server were on public address space.
embedded_https_test_server().GetURL(
"c.test",
"/set-header?Content-Security-Policy: treat-as-public-address")},
{interest_group_b_origin,
/*run_auction_from_public_address_space=*/false,
embedded_https_test_server().GetURL("c.test", "/echo")},
};
for (const auto& test_case : kTestCases) {
SCOPED_TRACE(test_case.run_auction_from_public_address_space);
URLLoaderMonitor url_loader_monitor;
std::string interest_group_host = test_case.interest_group_origin.host();
GURL join_url =
embedded_https_test_server().GetURL(interest_group_host, "/echo");
GURL update_url =
embedded_https_test_server().GetURL(interest_group_host, kUpdatePath);
GURL bidding_url = embedded_https_test_server().GetURL(
interest_group_host, "/interest_group/bidding_logic.js");
ASSERT_TRUE(NavigateToURL(shell(), join_url));
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_case.interest_group_origin, "name")
.SetBiddingUrl(bidding_url)
.SetUpdateUrl(update_url)
.SetAds({{{GURL("https://example.com/render"),
/*metadata=*/std::nullopt}}})
.Build()));
ASSERT_TRUE(NavigateToURL(shell(), test_case.auction_url));
EvalJsResult auction_result = RunAuctionAndWait(JsReplace(
R"(
{
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$3],
})",
url::Origin::Create(decision_logic_url), decision_logic_url,
test_case.interest_group_origin));
if (test_case.run_auction_from_public_address_space) {
// The auction fails because the scripts get blocked; the update request
// should still happen.
EXPECT_EQ(base::Value(), auction_result);
} else {
TestFencedFrameURLMappingResultObserver observer;
ConvertFencedFrameURNToURL(GURL(auction_result.ExtractString()),
&observer);
EXPECT_EQ(GURL("https://example.com/render"), observer.mapped_url());
}
// Wait for the update request to be made, and check its IPAddressSpace.
url_loader_monitor.WaitForUrls();
const network::ResourceRequest& request =
url_loader_monitor.WaitForUrl(update_url);
ASSERT_TRUE(request.trusted_params->client_security_state);
if (test_case.run_auction_from_public_address_space) {
EXPECT_EQ(
network::mojom::IPAddressSpace::kPublic,
request.trusted_params->client_security_state->ip_address_space);
} else {
EXPECT_EQ(
network::mojom::IPAddressSpace::kLoopback,
request.trusted_params->client_security_state->ip_address_space);
}
// The update should succeed in both cases.
EXPECT_EQ(
net::OK,
url_loader_monitor.WaitForRequestCompletion(update_url).error_code);
// Not the main purpose of this test, but it should be using a transient
// NetworkIsolationKey as well.
ASSERT_TRUE(request.trusted_params->isolation_info.network_isolation_key()
.IsTransient());
}
// Wait for interest group B's ad URL to be successfully updated.
const GURL initial_ad_url = GURL("https://example.com/render");
const GURL new_ad_url = GURL("https://example.com/render2");
auto check_for_new_ad_url = base::BindLambdaForTesting(
[&](scoped_refptr<StorageInterestGroups> storage_groups) {
EXPECT_EQ(storage_groups->size(), 1u);
const blink::InterestGroup& group =
storage_groups->GetInterestGroups()[0]->interest_group;
EXPECT_TRUE(group.ads.has_value());
EXPECT_EQ(group.ads->size(), 1u);
if (group.ads.value()[0].render_url() == new_ad_url) {
return true;
}
EXPECT_EQ(initial_ad_url, group.ads.value()[0].render_url());
return false;
});
WaitForInterestGroupsSatisfying(interest_group_b_origin,
check_for_new_ad_url);
// Check that interest group A's ad URL was also updated.
auto storage_groups = GetInterestGroupsForOwner(interest_group_a_origin);
ASSERT_EQ(storage_groups->size(), 1u);
const blink::InterestGroup& group =
storage_groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(group.ads.has_value());
ASSERT_EQ(group.ads->size(), 1u);
EXPECT_EQ(new_ad_url, group.ads.value()[0].render_url());
}
// Interest group APIs succeeded (i.e., feature join-ad-interest-group is
// enabled by Permissions Policy), and runAdAuction succeeded (i.e., feature
// run-ad-auction is enabled by Permissions Policy) in all contexts, because
// the kAdInterestGroupAPIRestrictedPolicyByDefault runtime flag is disabled by
// default and in that case the default value for those features are
// EnableForAll.
//
// This test both makes sure that's the case, and that a warning is displayed
// exactly when calls would fail if the default Permissions Policies were
// changed to EnableForSelf.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
FeaturesEnabledForAllByPermissionsPolicy) {
// clang-format off
GURL test_url = embedded_https_test_server().GetURL(
"a.test",
"/cross_site_iframe_factory.html?a.test("
"a.test,"
"b.test("
"c.test{allow-join-ad-interest-group;run-ad-auction},"
"a.test{allow-join-ad-interest-group;run-ad-auction}"
"),"
"b.test{allow-join-ad-interest-group;run-ad-auction}("
"b.test,"
"a.test{allow-join-ad-interest-group;run-ad-auction},"
"a.test"
"),"
"b.test{allow-join-ad-interest-group},"
"b.test{allow-run-ad-auction}"
")");
// clang-format on
ASSERT_TRUE(NavigateToURL(shell(), test_url));
RenderFrameHost* main_frame = web_contents()->GetPrimaryMainFrame();
RenderFrameHost* same_origin_iframe = ChildFrameAt(main_frame, 0);
RenderFrameHost* cross_origin_iframe = ChildFrameAt(main_frame, 1);
RenderFrameHost* inner_cross_origin_iframe =
ChildFrameAt(cross_origin_iframe, 0);
RenderFrameHost* same_origin_iframe_in_cross_origin_iframe =
ChildFrameAt(cross_origin_iframe, 1);
RenderFrameHost* cross_origin_iframe_with_permissions =
ChildFrameAt(main_frame, 2);
RenderFrameHost* nested_cross_origin_iframe_with_permissions =
ChildFrameAt(cross_origin_iframe_with_permissions, 0);
RenderFrameHost* same_origin_iframe_in_cross_origin_iframe_with_permissions =
ChildFrameAt(cross_origin_iframe_with_permissions, 1);
RenderFrameHost* same_origin_iframe_in_cross_origin_iframe2 =
ChildFrameAt(cross_origin_iframe_with_permissions, 2);
RenderFrameHost* cross_origin_join_only = ChildFrameAt(main_frame, 3);
RenderFrameHost* cross_origin_run_auction_only = ChildFrameAt(main_frame, 4);
// The server JSON updates all fields that can be updated.
constexpr char kUpdateUrlPath[] = "/interest_group/update_partial.json";
network_responder_->RegisterNetworkResponse(kUpdateUrlPath,
base::StringPrintf(
R"(
{
"trustedBiddingSignalsKeys": ["new_key"],
}
)"));
base::RunLoop().RunUntilIdle();
GURL url;
url::Origin origin;
std::string host;
const auto execution_targets = std::to_array<RenderFrameHost*>({
main_frame,
// Test the next two cases twice, to make sure the caching logic works, or
// at least doesn't prevent duplicate warnings.
same_origin_iframe,
same_origin_iframe,
cross_origin_iframe,
cross_origin_iframe,
inner_cross_origin_iframe,
same_origin_iframe_in_cross_origin_iframe,
cross_origin_iframe_with_permissions,
nested_cross_origin_iframe_with_permissions,
same_origin_iframe_in_cross_origin_iframe_with_permissions,
same_origin_iframe_in_cross_origin_iframe2,
cross_origin_join_only,
cross_origin_run_auction_only,
});
// The execution targets that are expected to have permissions warnings.
const auto execution_targets_with_all_warnings =
std::to_array<RenderFrameHost*>({
cross_origin_iframe,
inner_cross_origin_iframe,
same_origin_iframe_in_cross_origin_iframe,
same_origin_iframe_in_cross_origin_iframe2,
});
const auto execution_targets_with_join_warnings =
std::to_array<RenderFrameHost*>({
cross_origin_run_auction_only,
});
const auto execution_targets_with_run_auction_warnings =
std::to_array<RenderFrameHost*>({
cross_origin_join_only,
});
for (size_t i = 0; i < std::size(execution_targets); ++i) {
SCOPED_TRACE(i);
auto* execution_target = execution_targets[i];
url = execution_target->GetLastCommittedURL();
origin = url::Origin::Create(url);
host = url.host();
WebContentsConsoleObserver console_observer(shell()->web_contents());
console_observer.SetPattern(WarningPermissionsPolicy("*", "*"));
// Use a second observer to wait until the last message is received.
WebContentsConsoleObserver last_message_console_observer(
shell()->web_contents());
if (base::Contains(execution_targets_with_all_warnings, execution_target) ||
base::Contains(execution_targets_with_join_warnings,
execution_target)) {
last_message_console_observer.SetPattern(WarningPermissionsPolicy(
"join-ad-interest-group", "leaveAdInterestGroup"));
} else {
last_message_console_observer.SetPattern(
WarningPermissionsPolicy("run-ad-auction", "runAdAuction"));
}
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
host, "/interest_group/bidding_logic.js"))
.SetUpdateUrl(embedded_https_test_server().GetURL(
host, "/interest_group/update_partial.json"))
.SetAds({{{GURL("https://example.com/render"),
/*metadata=*/std::nullopt}}})
.Build(),
execution_target));
EXPECT_EQ("https://example.com/render",
RunAuctionAndWaitForUrl(
JsReplace(
R"(
{
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
}
)",
origin,
embedded_https_test_server().GetURL(
host, "/interest_group/decision_logic.js")),
execution_target));
EXPECT_EQ("done", UpdateInterestGroupsInJS(execution_target));
EXPECT_EQ(kSuccess, LeaveInterestGroup(origin, "cars", execution_target));
if (base::Contains(execution_targets_with_all_warnings, execution_target)) {
EXPECT_TRUE(last_message_console_observer.Wait());
ASSERT_EQ(4u, console_observer.messages().size());
EXPECT_EQ(WarningPermissionsPolicy("join-ad-interest-group",
"joinAdInterestGroup"),
console_observer.GetMessageAt(0));
EXPECT_EQ(WarningPermissionsPolicy("run-ad-auction", "runAdAuction"),
console_observer.GetMessageAt(1));
EXPECT_EQ(WarningPermissionsPolicy("join-ad-interest-group",
"updateAdInterestGroups"),
console_observer.GetMessageAt(2));
EXPECT_EQ(WarningPermissionsPolicy("join-ad-interest-group",
"leaveAdInterestGroup"),
console_observer.GetMessageAt(3));
} else if (base::Contains(execution_targets_with_join_warnings,
execution_target)) {
EXPECT_TRUE(last_message_console_observer.Wait());
ASSERT_EQ(3u, console_observer.messages().size());
EXPECT_EQ(WarningPermissionsPolicy("join-ad-interest-group",
"joinAdInterestGroup"),
console_observer.GetMessageAt(0));
EXPECT_EQ(WarningPermissionsPolicy("join-ad-interest-group",
"updateAdInterestGroups"),
console_observer.GetMessageAt(1));
EXPECT_EQ(WarningPermissionsPolicy("join-ad-interest-group",
"leaveAdInterestGroup"),
console_observer.GetMessageAt(2));
} else if (base::Contains(execution_targets_with_run_auction_warnings,
execution_target)) {
EXPECT_TRUE(last_message_console_observer.Wait());
ASSERT_EQ(1u, console_observer.messages().size());
EXPECT_EQ(WarningPermissionsPolicy("run-ad-auction", "runAdAuction"),
console_observer.GetMessageAt(0));
} else {
EXPECT_TRUE(console_observer.messages().empty());
}
}
}
// Features join-ad-interest-group and run-ad-auction can be disabled by HTTP
// headers, and they cannot be enabled again by container policy in that case.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, FeaturesDisabledByHttpHeader) {
GURL test_url = embedded_https_test_server().GetURL(
"a.test",
"/interest_group/page-with-fledge-permissions-policy-disabled.html");
url::Origin origin = url::Origin::Create(test_url);
ASSERT_TRUE(NavigateToURL(shell(), test_url));
RenderFrameHost* main_frame = web_contents()->GetPrimaryMainFrame();
RenderFrameHost* iframe = ChildFrameAt(main_frame, 0);
for (auto* execution_target : {main_frame, iframe}) {
ExpectNotAllowedToJoinOrUpdateInterestGroup(origin, execution_target);
ExpectNotAllowedToRunAdAuction(
origin,
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js"),
execution_target);
ExpectNotAllowedToLeaveInterestGroup(origin, "cars", execution_target);
ExpectNotAllowedToClearOriginJoinedInterestGroups(origin, execution_target);
}
}
// Features join-ad-interest-group and run-ad-auction can be disabled by
// container policy.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
FeaturesDisabledByContainerPolicy) {
GURL test_url = embedded_https_test_server().GetURL(
"a.test",
"/interest_group/"
"page-with-fledge-permissions-policy-disabled-in-iframe.html");
url::Origin origin = url::Origin::Create(test_url);
ASSERT_TRUE(NavigateToURL(shell(), test_url));
RenderFrameHost* same_origin_iframe =
ChildFrameAt(web_contents()->GetPrimaryMainFrame(), 0);
ExpectNotAllowedToJoinOrUpdateInterestGroup(origin, same_origin_iframe);
ExpectNotAllowedToRunAdAuction(
origin,
embedded_https_test_server().GetURL("a.test",
"/interest_group/decision_logic.js"),
same_origin_iframe);
ExpectNotAllowedToLeaveInterestGroup(origin, "cars", same_origin_iframe);
ExpectNotAllowedToClearOriginJoinedInterestGroups(origin, same_origin_iframe);
}
// Interest group APIs succeeded (i.e., feature join-ad-interest-group is
// enabled by Permissions Policy), and runAdAuction succeeded (i.e., feature
// run-ad-auction is enabled by Permissions Policy) in
// (1) same-origin frames by default,
// (2) cross-origin iframes that enable those features in container policy
// (iframe "allow" attribute).
// (3) cross-origin iframes that enables those features inside parent
// cross-origin iframes that also enables those features.
IN_PROC_BROWSER_TEST_F(InterestGroupRestrictedPermissionsPolicyBrowserTest,
EnabledByPermissionsPolicy) {
// clang-format off
GURL test_url = embedded_https_test_server().GetURL(
"a.test",
"/cross_site_iframe_factory.html?a.test("
"a.test,"
"b.test{allow-join-ad-interest-group;run-ad-auction}("
"c.test{allow-join-ad-interest-group;run-ad-auction}"
")"
")");
// clang-format on
ASSERT_TRUE(NavigateToURL(shell(), test_url));
RenderFrameHost* main_frame = web_contents()->GetPrimaryMainFrame();
RenderFrameHost* same_origin_iframe = ChildFrameAt(main_frame, 0);
RenderFrameHost* cross_origin_iframe = ChildFrameAt(main_frame, 1);
RenderFrameHost* inner_cross_origin_iframe =
ChildFrameAt(cross_origin_iframe, 0);
// The server JSON updates all fields that can be updated.
constexpr char kUpdateUrlPath[] = "/interest_group/update_partial.json";
network_responder_->RegisterNetworkResponse(kUpdateUrlPath,
base::StringPrintf(
R"(
{
"trustedBiddingSignalsKeys": ["new_key"],
}
)"));
GURL url;
url::Origin origin;
std::string host;
const auto execution_targets = std::to_array<RenderFrameHost*>({
main_frame,
same_origin_iframe,
cross_origin_iframe,
inner_cross_origin_iframe,
});
for (auto* execution_target : execution_targets) {
url = execution_target->GetLastCommittedURL();
origin = url::Origin::Create(url);
host = url.host();
WebContentsConsoleObserver console_observer(shell()->web_contents());
console_observer.SetPattern(WarningPermissionsPolicy("*", "*"));
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
host, "/interest_group/bidding_logic.js"))
.SetUpdateUrl(embedded_https_test_server().GetURL(
host, "/interest_group/update_partial.json"))
.SetAds({{{GURL("https://example.com/render"),
/*metadata=*/std::nullopt}}})
.Build(),
execution_target));
EXPECT_EQ("https://example.com/render",
RunAuctionAndWaitForUrl(
JsReplace(
R"(
{
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
}
)",
origin,
embedded_https_test_server().GetURL(
host, "/interest_group/decision_logic.js")),
execution_target));
EXPECT_EQ("done", UpdateInterestGroupsInJS(execution_target));
EXPECT_EQ(kSuccess,
LeaveInterestGroupAndVerify(origin, "cars", execution_target));
// Join another interest group and make sure
// clearOriginJoinedAdInterestGroups() works as well.
EXPECT_EQ(kSuccess, JoinInterestGroupAndVerify(
/*owner=*/origin,
/*name=*/"bacon", execution_target));
EXPECT_EQ(kSuccess, ClearOriginJoinedInterestGroupsAndVerify(
origin, std::nullopt, execution_target));
EXPECT_TRUE(console_observer.messages().empty());
}
}
class ExtraJoinPermissionsInterestGroupBrowserTest
: public InterestGroupBrowserTest {
public:
ExtraJoinPermissionsInterestGroupBrowserTest() {
feature_list_.InitWithFeatures(
{features::kFledgeModifyInterestGroupPolicyCheckOnOwner},
/*disabled_features=*/{});
}
protected:
base::test::ScopedFeatureList feature_list_;
};
// When features::kFledgeJoinAdInterestGroupExtraPolicyCheck is enabled, a
// declared policy of (self) should prevent a x-origin owner from joining in the
// top frame.
IN_PROC_BROWSER_TEST_F(
ExtraJoinPermissionsInterestGroupBrowserTest,
JoinAdInterestGroupFailsForCrossOriginWithSelfDeclaredPolicy) {
const char kGroup[] = "aardvarks";
GURL join_declared_self_url = embedded_https_test_server().GetURL(
"b.test", "/interest_group/join_declared_self.html");
network_responder_->RegisterNetworkResponse(
join_declared_self_url.path(), "", "text/html",
{{"Permissions-Policy", "join-ad-interest-group=(self)"}});
url::Origin allow_join_origin = url::Origin::Create(
GURL(embedded_https_test_server().GetURL("allow-join.a.test", "/")));
url::Origin allow_leave_origin =
embedded_https_test_server().GetOrigin("allow-leave.a.test");
// Navigate to a cross-origin URL that declares Permissions-Policy:
// join-ad-interest-group=(self)
ASSERT_TRUE(
NavigateToURL(shell(), embedded_https_test_server().GetURL(
"b.test", join_declared_self_url.path())));
// Joining a group cross-origin should fail.
EXPECT_EQ("NotAllowedError: Permission to join interest group denied.",
JoinInterestGroupAndVerify(allow_join_origin, kGroup));
// Leaving a group cross-origin should fail.
EXPECT_EQ("NotAllowedError: Permission to leave interest group denied.",
LeaveInterestGroupAndVerify(allow_leave_origin, kGroup));
// Clearing a cross-origin's igs should fail.
EXPECT_EQ("NotAllowedError: Permission to leave interest groups denied.",
ClearOriginJoinedInterestGroupsAndVerify(allow_leave_origin));
}
class ExtraJoinPermissionsAndDefaultSelfInterestGroupBrowserTest
: public InterestGroupBrowserTest {
public:
ExtraJoinPermissionsAndDefaultSelfInterestGroupBrowserTest() {
feature_list_.InitWithFeatures(
{features::kFledgeModifyInterestGroupPolicyCheckOnOwner,
network::features::kAdInterestGroupAPIRestrictedPolicyByDefault},
/*disabled_features=*/{});
}
protected:
base::test::ScopedFeatureList feature_list_;
};
// When default self and features::kFledgeJoinAdInterestGroupExtraPolicyCheck
// are enabled, a x-origin join should be possible from the top-frame.
IN_PROC_BROWSER_TEST_F(
ExtraJoinPermissionsAndDefaultSelfInterestGroupBrowserTest,
JoinAdInterestGroupFailsForCrossOriginWithSelfDeclaredPolicy) {
const char kGroup[] = "aardvarks";
GURL join_declared_self_url = embedded_https_test_server().GetURL(
"b.test", "/interest_group/empty.html");
network_responder_->RegisterNetworkResponse(join_declared_self_url.path(), "",
"text/html", {{}});
url::Origin allow_join_origin = url::Origin::Create(
GURL(embedded_https_test_server().GetURL("allow-join.a.test", "/")));
url::Origin allow_leave_origin =
embedded_https_test_server().GetOrigin("allow-leave.a.test");
// Navigate to a URL without a document policy.
ASSERT_TRUE(
NavigateToURL(shell(), embedded_https_test_server().GetURL(
"b.test", join_declared_self_url.path())));
// Joining a group cross-origin should succeed.
EXPECT_EQ(kSuccess, JoinInterestGroupAndVerify(allow_join_origin, kGroup));
// Leaving a group cross-origin should succeed.
EXPECT_EQ(kSuccess, LeaveInterestGroupAndVerify(allow_leave_origin, kGroup));
// Clearing a cross-origin's igs should succeed.
EXPECT_EQ(kSuccess,
ClearOriginJoinedInterestGroupsAndVerify(allow_leave_origin));
}
// Delete this class and the test using it once
// kFledgeJoinAdInterestGroupExtraPolicyCheck is enabled by default. It is
// simply verifying the existing broken behavior.
class NoExtraJoinPermissionsInterestGroupBrowserTest
: public InterestGroupBrowserTest {
public:
NoExtraJoinPermissionsInterestGroupBrowserTest() {
feature_list_.InitWithFeatures(
/*enabled_features=*/{},
/*disabled_features=*/{
features::kFledgeModifyInterestGroupPolicyCheckOnOwner});
}
protected:
base::test::ScopedFeatureList feature_list_;
};
// When features::kFledgeJoinAdInterestGroupExtraPolicyCheck is disabled, a
// declared policy of (self) won't prevent a x-origin owner from joining in the
// top frame. Delete this test and the associated class once
// kFledgeJoinAdInterestGroupExtraPolicyCheck is enabled by default.
IN_PROC_BROWSER_TEST_F(
NoExtraJoinPermissionsInterestGroupBrowserTest,
JoinAdInterestGroupFailsForCrossOriginWithSelfDeclaredPolicy) {
const char kGroup[] = "aardvarks";
GURL join_declared_self_url = embedded_https_test_server().GetURL(
"b.test", "/interest_group/join_declared_self.html");
network_responder_->RegisterNetworkResponse(
join_declared_self_url.path(), "", "text/html",
{{"Permissions-Policy", "join-ad-interest-group=(self)"}});
url::Origin allow_join_origin = url::Origin::Create(
GURL(embedded_https_test_server().GetURL("allow-join.a.test", "/")));
url::Origin allow_leave_origin =
embedded_https_test_server().GetOrigin("allow-leave.a.test");
// Navigate to a cross-origin URL that declares Permissions-Policy:
// join-ad-interest-group=(self)
ASSERT_TRUE(
NavigateToURL(shell(), embedded_https_test_server().GetURL(
"b.test", join_declared_self_url.path())));
// Joining a group cross-origin should work.
EXPECT_EQ(kSuccess, JoinInterestGroupAndVerify(allow_join_origin, kGroup));
// Leaving a group cross-origin should work.
EXPECT_EQ(kSuccess, LeaveInterestGroupAndVerify(allow_leave_origin, kGroup));
// Clearing a cross-origin's igs should work.
EXPECT_EQ(kSuccess,
ClearOriginJoinedInterestGroupsAndVerify(allow_leave_origin));
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
LotsOfInterestGroupsEpsilonTimeout) {
GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
url::Origin test_origin = url::Origin::Create(test_url);
ASSERT_TRUE(NavigateToURL(shell(), test_url));
GURL ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_cars");
GURL decision_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js");
// Need lots of groups to exercise them being handled in chunks.
// Use /hung as script since we don't actually want to finish.
for (int group = 0; group < 100; ++group) {
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
/*owner=*/test_origin,
/*name=*/base::StringPrintf("cars%d", group),
/*priority=*/0.0,
/*execution_mode=*/
blink::InterestGroup::ExecutionMode::kCompatibilityMode,
/*bidding_url=*/
embedded_https_test_server().GetURL("a.test", "/hung"),
/*ads=*/{{{ad_url, /*metadata=*/std::nullopt}}}));
}
// Also add an IG on a different host also with hung script so that we don't
// terminate immediately.
GURL other_url =
embedded_https_test_server().GetURL("allow-join.b.test", "/hung");
url::Origin other_origin = url::Origin::Create(other_url);
content_browser_client_->AddToAllowList({other_origin});
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
/*owner=*/other_origin,
/*name=*/"bicycles",
/*priority=*/0.0,
/*execution_mode=*/
blink::InterestGroup::ExecutionMode::kCompatibilityMode,
/*bidding_url=*/other_url,
/*ads=*/{{{ad_url, /*metadata=*/std::nullopt}}}));
const char kAuctionConfigTemplate[] = R"({
seller: $1,
decisionLogicURL: $2,
perBuyerCumulativeTimeouts: {$1: 1, $3: 50},
interestGroupBuyers: [$1, $3]
})";
WebContentsConsoleObserver console_observer(shell()->web_contents());
console_observer.SetPattern(
"Worklet error: https://a.test:*/hung perBuyerCumulativeTimeout "
"exceeded during bid generation.");
EXPECT_EQ(base::Value(),
RunAuctionAndWait(JsReplace(kAuctionConfigTemplate, test_origin,
decision_url, other_origin)));
EXPECT_TRUE(console_observer.Wait());
}
// Interest group APIs throw NotAllowedError (i.e., feature
// join-ad-interest-group is disabled by Permissions Policy), and runAdAuction
// throws NotAllowedError (i.e, feature run-ad-auction is disabled by
// Permissions Policy) in
// (1) same-origin iframes that disabled the features using allow attribute,
// (2) cross-origin iframes that don't enable those features in container policy
// (iframe "allow" attribute).
// (3) iframes that enables those features inside parent cross-origin iframes
// that don't enable those features.
IN_PROC_BROWSER_TEST_F(InterestGroupRestrictedPermissionsPolicyBrowserTest,
DisabledByContainerPolicy) {
GURL other_url = embedded_https_test_server().GetURL("b.test", "/echo");
url::Origin other_origin = url::Origin::Create(other_url);
// clang-format off
GURL test_url = embedded_https_test_server().GetURL(
"a.test",
"/cross_site_iframe_factory.html?a.test("
"b.test("
"b.test{allow-join-ad-interest-group;run-ad-auction}"
")"
")");
// clang-format on
ASSERT_TRUE(NavigateToURL(shell(), test_url));
RenderFrameHost* outter_iframe =
ChildFrameAt(web_contents()->GetPrimaryMainFrame(), 0);
RenderFrameHost* inner_iframe = ChildFrameAt(outter_iframe, 0);
for (auto* execution_target : {outter_iframe, inner_iframe}) {
ExpectNotAllowedToJoinOrUpdateInterestGroup(other_origin, execution_target);
ExpectNotAllowedToRunAdAuction(
other_origin,
embedded_https_test_server().GetURL(
"b.test", "/interest_group/decision_logic.js"),
execution_target);
ExpectNotAllowedToLeaveInterestGroup(other_origin, "cars",
execution_target);
}
test_url = embedded_https_test_server().GetURL(
"a.test",
"/interest_group/"
"page-with-fledge-permissions-policy-disabled-in-iframe.html");
url::Origin origin = url::Origin::Create(test_url);
ASSERT_TRUE(NavigateToURL(shell(), test_url));
RenderFrameHost* same_origin_iframe =
ChildFrameAt(web_contents()->GetPrimaryMainFrame(), 0);
ExpectNotAllowedToJoinOrUpdateInterestGroup(origin, same_origin_iframe);
ExpectNotAllowedToRunAdAuction(
origin,
embedded_https_test_server().GetURL("a.test",
"/interest_group/decision_logic.js"),
same_origin_iframe);
ExpectNotAllowedToLeaveInterestGroup(origin, "cars", same_origin_iframe);
}
// Features join-ad-interest-group and run-ad-auction can be enabled/disabled
// separately.
IN_PROC_BROWSER_TEST_F(InterestGroupRestrictedPermissionsPolicyBrowserTest,
EnableOneOfInterestGroupAPIsAndAuctionAPIForIframe) {
GURL other_url = embedded_https_test_server().GetURL("b.test", "/echo");
url::Origin other_origin = url::Origin::Create(other_url);
// clang-format off
GURL test_url = embedded_https_test_server().GetURL(
"a.test",
"/cross_site_iframe_factory.html?a.test("
"b.test{allow-join-ad-interest-group},"
"b.test{allow-run-ad-auction})");
// clang-format on
ASSERT_TRUE(NavigateToURL(shell(), test_url));
RenderFrameHost* iframe_interest_group =
ChildFrameAt(web_contents()->GetPrimaryMainFrame(), 0);
RenderFrameHost* iframe_ad_auction =
ChildFrameAt(web_contents()->GetPrimaryMainFrame(), 1);
// Interest group APIs succeed and run ad auction fails for
// iframe_interest_group.
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/other_origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"b.test", "/interest_group/bidding_logic.js"))
.SetUpdateUrl(embedded_https_test_server().GetURL(
"b.test", "/interest_group/update_partial.json"))
.SetAds({{{GURL("https://example.com/render"),
/*metadata=*/std::nullopt}}})
.Build(),
iframe_interest_group));
EXPECT_EQ("done", UpdateInterestGroupsInJS(iframe_interest_group));
ExpectNotAllowedToRunAdAuction(
other_origin,
embedded_https_test_server().GetURL("b.test",
"/interest_group/decision_logic.js"),
iframe_interest_group);
// Interest group APIs fail and run ad auction succeeds for iframe_ad_auction.
ExpectNotAllowedToJoinOrUpdateInterestGroup(other_origin, iframe_ad_auction);
EXPECT_EQ("https://example.com/render",
RunAuctionAndWaitForUrl(
JsReplace(
R"(
{
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
}
)",
other_origin,
embedded_https_test_server().GetURL(
"b.test", "/interest_group/decision_logic.js")),
iframe_ad_auction));
ExpectNotAllowedToLeaveInterestGroup(other_origin, "cars", iframe_ad_auction);
EXPECT_EQ(kSuccess,
LeaveInterestGroup(other_origin, "cars", iframe_interest_group));
}
// Features join-ad-interest-group and run-ad-auction can be disabled by HTTP
// headers, and they cannot be enabled again by container policy in that case.
IN_PROC_BROWSER_TEST_F(InterestGroupRestrictedPermissionsPolicyBrowserTest,
DisabledByHttpHeader) {
GURL test_url = embedded_https_test_server().GetURL(
"a.test",
"/interest_group/page-with-fledge-permissions-policy-disabled.html");
url::Origin origin = url::Origin::Create(test_url);
ASSERT_TRUE(NavigateToURL(shell(), test_url));
RenderFrameHost* main_frame = web_contents()->GetPrimaryMainFrame();
RenderFrameHost* iframe = ChildFrameAt(main_frame, 0);
for (auto* execution_target : {main_frame, iframe}) {
ExpectNotAllowedToJoinOrUpdateInterestGroup(origin, execution_target);
ExpectNotAllowedToRunAdAuction(
origin,
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js"),
execution_target);
ExpectNotAllowedToLeaveInterestGroup(origin, "cars", execution_target);
}
}
// navigator.deprecatedURNToURL returns null for an invalid URN.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, DeprecatedURNToURLInvalidURN) {
GURL invalid_urn("urn:uuid:c36973b5-e5d9-de59-e4c4-364f137b3c7a");
EXPECT_EQ(std::nullopt, ConvertFencedFrameURNToURLInJS(invalid_urn));
}
// Tests navigator.deprecatedURNToURL for a valid URN. Covers both the cases
// where sendReports is false and true. Both are done in the same test because
// there's no way to wait until reports aren't sent, so first run a case that
// doesn't send reports, then run a case that does, and finally make sure that
// reports were only sent for the first case.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, DeprecatedURNToURLValidURN) {
struct TestCases {
bool send_reports;
// Host for buyer, seller, and publisher. Use a different hostname for each
// loop iteration so they can use different interest groups.
const char* host;
// Path for reports. Have to be different so can make reports are only send
// when `send_reports` is true.
GURL report_url;
};
const auto kTestCases = std::to_array<TestCases>({
{
/*send_reports=*/false,
/*host=*/"a.test",
/*report_path=*/
embedded_https_test_server().GetURL("c.test", "/report_for_a"),
},
{
/*send_reports=*/true,
/*host=*/"b.test",
/*report_path=*/
embedded_https_test_server().GetURL("c.test", "/report_for_b"),
},
});
for (const auto& test_case : kTestCases) {
base::HistogramTester histogram_tester;
GURL test_url =
embedded_https_test_server().GetURL(test_case.host, "/echo");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_cars");
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
/*owner=*/test_origin,
// This test uses a script that sends reports to name of the
// interest group, so use the report URL as the name.
/*name=*/test_case.report_url.spec(),
/*priority=*/0.0, /*execution_mode=*/
blink::InterestGroup::ExecutionMode::kCompatibilityMode,
/*bidding_url=*/
embedded_https_test_server().GetURL(
test_case.host,
"/interest_group/bidding_logic_report_to_name.js"),
/*ads=*/{{{ad_url, /*metadata=*/std::nullopt}}}));
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1]
})",
test_origin,
embedded_https_test_server().GetURL(
test_case.host, "/interest_group/decision_logic.js"));
auto result = RunAuctionAndWait(auction_config);
GURL urn_url = GURL(result.ExtractString());
EXPECT_TRUE(urn_url.is_valid());
EXPECT_EQ(url::kUrnScheme, urn_url.scheme_piece());
EXPECT_EQ(ad_url,
ConvertFencedFrameURNToURLInJS(urn_url, test_case.send_reports));
histogram_tester.ExpectTotalCount(
"Ads.InterestGroup.Auction.AdNavigationStarted", 1);
}
// Only the `send_reports` == true case should have sent a report. Wait for
// it, and then check that the report URL for the first case was not seen.
WaitForUrl(kTestCases[1].report_url);
EXPECT_FALSE(HasServerSeenUrl(kTestCases[0].report_url));
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, ExecutionModeCompatibility) {
const char kScript[] = R"(
if (!('count' in globalThis))
globalThis.count = 0;
function generateBid() {
++count;
return {ad: ["ad"], bid:count, render:$1 + count};
}
function reportWin(auctionSignals, perBuyerSignals, sellerSignals,
browserSignals) {
}
)";
const int kNumGroups = 10; // as many ads in each group, too.
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
std::vector<GURL> ad_urls;
for (int i = 0; i < kNumGroups; ++i) {
ad_urls.push_back(embedded_https_test_server().GetURL(
"c.test", "/echo?" + base::NumberToString(i + 1)));
}
network_responder_->RegisterNetworkResponse(
"/interest_group/bidding_logic.js",
JsReplace(kScript,
embedded_https_test_server().GetURL("c.test", "/echo?")),
"application/javascript");
std::vector<blink::InterestGroup::Ad> ads;
for (const GURL& ad_url : ad_urls) {
ads.emplace_back(ad_url, /*metadata=*/std::nullopt);
}
for (int i = 0; i < kNumGroups; ++i) {
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"cars" + base::NumberToString(i))
.SetExecutionMode(
blink::InterestGroup::ExecutionMode::kCompatibilityMode)
.SetBiddingUrl(embedded_https_test_server().GetURL(
test_url.host(), "/interest_group/bidding_logic.js"))
.SetAds(ads)
.Build()));
}
EXPECT_EQ(embedded_https_test_server().GetURL("c.test", "/echo?1"),
RunAuctionAndWaitForUrl(JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
})",
test_origin,
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js"))));
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, ExecutionModeGroupByOrigin) {
const char kScript[] = R"(
if (!('count' in globalThis))
globalThis.count = 0;
function generateBid() {
++count;
return {ad: ["ad"], bid:count, render:$1 + count};
}
function reportWin(auctionSignals, perBuyerSignals, sellerSignals,
browserSignals) {
}
)";
// Keep this number relatively low so that the groups don't get divided
// among multiple threads.
const int kNumGroups = 3; // as many ads in each group, too.
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
std::vector<GURL> ad_urls;
for (int i = 0; i < kNumGroups; ++i) {
ad_urls.push_back(embedded_https_test_server().GetURL(
"c.test", "/echo?" + base::NumberToString(i + 1)));
}
network_responder_->RegisterNetworkResponse(
"/interest_group/bidding_logic.js",
JsReplace(kScript,
embedded_https_test_server().GetURL("c.test", "/echo?")),
"application/javascript");
std::vector<blink::InterestGroup::Ad> ads;
for (const GURL& ad_url : ad_urls) {
ads.emplace_back(ad_url, /*metadata=*/std::nullopt);
}
for (int i = 0; i < kNumGroups; ++i) {
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"cars" + base::NumberToString(i))
.SetExecutionMode(
blink::InterestGroup::ExecutionMode::kGroupedByOriginMode)
.SetBiddingUrl(embedded_https_test_server().GetURL(
test_url.host(), "/interest_group/bidding_logic.js"))
.SetAds(ads)
.Build()));
}
EXPECT_EQ(embedded_https_test_server().GetURL("c.test", "/echo?3"),
RunAuctionAndWaitForUrl(JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
})",
test_origin,
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js"))));
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
SharedStorageWriteFromGenerateBid) {
const char kScript[] = R"(
if (!('count' in globalThis))
globalThis.count = 0;
function generateBid() {
sharedStorage.set('key0', 'value0');
++count;
return {ad: ["ad"], bid:count, render:$1 + count};
}
function reportWin(auctionSignals, perBuyerSignals, sellerSignals,
browserSignals) {
}
)";
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
network_responder_->RegisterNetworkResponse(
"/interest_group/bidding_logic.js",
JsReplace(kScript,
embedded_https_test_server().GetURL("c.test", "/echo?")),
"application/javascript");
std::vector<blink::InterestGroup::Ad> ads;
ads.emplace_back(embedded_https_test_server().GetURL("c.test", "/echo?1"),
/*metadata=*/std::nullopt);
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"cars0")
.SetBiddingUrl(embedded_https_test_server().GetURL(
test_url.host(), "/interest_group/bidding_logic.js"))
.SetAds(ads)
.Build()));
EXPECT_CALL(
*content_browser_client_,
LogWebFeatureForCurrentPage(
shell()->web_contents()->GetPrimaryMainFrame(),
blink::mojom::WebFeature::kSharedStorageWriteFromBidderGenerateBid));
EXPECT_EQ(embedded_https_test_server().GetURL("c.test", "/echo?1"),
RunAuctionAndWaitForUrl(JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
})",
test_origin,
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js"))));
}
enum FetchMethod {
kFetch,
kIFrame,
kDynamicIFrame,
};
class InterestGroupBrowserAdAuctionHeadersTest
: public InterestGroupBrowserTest {
protected:
struct FetchURLParams {
std::string origin;
std::optional<std::string> path;
std::string ad_auction_headers;
std::optional<GURL> redirect_url;
};
GURL GetFetchURL(FetchURLParams params) {
CHECK(!params.origin.empty());
base::StringPairs replacements;
replacements.emplace_back(std::make_pair(
"{{STATUS}}",
params.redirect_url.has_value() ? "301 Moved Permanently" : "200 OK"));
replacements.emplace_back(
std::make_pair("{{AD_AUCTION_HEADERS}}", params.ad_auction_headers));
replacements.emplace_back(std::make_pair(
"{{REDIRECT_HEADER}}", params.redirect_url.has_value()
? "Location: " + params.redirect_url->spec()
: ""));
return embedded_https_test_server().GetURL(
params.origin,
net::test_server::GetFilePathWithReplacements(
"/interest_group/" +
params.path.value_or(
"page_with_custom_ad_auction_result_header.html"),
replacements));
}
void CreateIframe(const GURL& url, bool has_ad_auction_headers_attribute) {
content::TestNavigationObserver nav_observer(web_contents());
ExecuteScriptAsync(
web_contents(),
content::JsReplace(R"(
{
const iframe = document.createElement("iframe");
iframe.adAuctionHeaders = $1;
iframe.src = $2;
document.body.appendChild(iframe);
}
)",
has_ad_auction_headers_attribute, url));
nav_observer.WaitForNavigationFinished();
EXPECT_TRUE(nav_observer.last_navigation_succeeded());
}
GURL GetPageWithIFrameURL(GURL fetch_url,
bool has_ad_auction_headers_attribute) {
base::StringPairs replacement;
replacement.emplace_back(std::make_pair(
"{{MAYBE_AD_AUCTION_HEADERS_ATTRIBUTE}}",
has_ad_auction_headers_attribute ? "adAuctionHeaders" : ""));
replacement.emplace_back(std::make_pair("{{SRC_URL}}", fetch_url.spec()));
return embedded_https_test_server().GetURL(
"a.test",
net::test_server::GetFilePathWithReplacements(
"/interest_group/page_with_iframe_with_ad_auction_headers.html",
replacement));
}
std::optional<std::string> GetAdAuctionHeader(
std::optional<std::string> path = std::nullopt) {
return GetAdAuctionHeaderForRequestPath(
"/interest_group/" +
path.value_or("page_with_custom_ad_auction_result_header.html"));
}
};
class InterestGroupBrowserAdAuctionHeadersAllMethodsAndOriginsTest
: public InterestGroupBrowserAdAuctionHeadersTest,
public ::testing::WithParamInterface<std::tuple<FetchMethod, bool>> {
protected:
FetchMethod ad_auction_headers_test_type() { return std::get<0>(GetParam()); }
bool is_cross_origin() { return std::get<1>(GetParam()); }
};
IN_PROC_BROWSER_TEST_P(
InterestGroupBrowserAdAuctionHeadersAllMethodsAndOriginsTest,
AdAuctionHeadersEligible_HasNoResponseHeaders) {
GURL fetch_url = GetFetchURL(
(FetchURLParams){.origin = is_cross_origin() ? "b.test" : "a.test",
.ad_auction_headers = ""});
switch (ad_auction_headers_test_type()) {
case FetchMethod::kFetch:
ASSERT_TRUE(
NavigateToURL(shell(), embedded_https_test_server().GetURL(
"a.test", "/interest_group/empty.html")));
EXPECT_TRUE(
ExecJs(web_contents()->GetPrimaryMainFrame(),
content::JsReplace("fetch($1, {adAuctionHeaders: true})",
fetch_url)));
break;
case FetchMethod::kIFrame:
ASSERT_TRUE(NavigateToURL(
shell(), GetPageWithIFrameURL(
fetch_url, /*has_ad_auction_headers_attribute=*/true)));
break;
case FetchMethod::kDynamicIFrame:
ASSERT_TRUE(
NavigateToURL(shell(), embedded_https_test_server().GetURL(
"a.test", "/interest_group/empty.html")));
CreateIframe(fetch_url, /*has_ad_auction_headers_attribute=*/true);
break;
}
std::optional<std::string> ad_auction_header = GetAdAuctionHeader();
EXPECT_TRUE(ad_auction_header);
EXPECT_EQ(*ad_auction_header, "?1");
url::Origin request_origin = url::Origin::Create(fetch_url);
EXPECT_FALSE(WitnessedAuctionResultForOrigin(
request_origin, base64Decode(kLegitimateAdAuctionResponse)));
EXPECT_EQ(ParseAndFindAdAuctionSignals(request_origin, "slot1"), nullptr);
EXPECT_THAT(TakeAuctionAdditionalBidsForOriginAndNonce(
request_origin, "00000000-0000-0000-0000-000000000000"),
::testing::IsEmpty());
}
IN_PROC_BROWSER_TEST_P(
InterestGroupBrowserAdAuctionHeadersAllMethodsAndOriginsTest,
AdAuctionHeadersEligible_HasAdAuctionResultResponseHeader) {
GURL fetch_url = GetFetchURL((FetchURLParams){
.origin = is_cross_origin() ? "b.test" : "a.test",
.ad_auction_headers =
base::StrCat({"Ad-Auction-Result: ", kLegitimateAdAuctionResponse})});
switch (ad_auction_headers_test_type()) {
case FetchMethod::kFetch:
ASSERT_TRUE(
NavigateToURL(shell(), embedded_https_test_server().GetURL(
"a.test", "/interest_group/empty.html")));
EXPECT_TRUE(ExecJs(web_contents()->GetPrimaryMainFrame(),
content::JsReplace(R"(
fetch($1, {adAuctionHeaders: true}).then((response) => {
if (!response.headers.get('Ad-Auction-Result')) {
throw 'Did not receive `Ad-Auction-Result` header';
}
});
)",
fetch_url)));
break;
case FetchMethod::kIFrame:
ASSERT_TRUE(NavigateToURL(
shell(), GetPageWithIFrameURL(
fetch_url, /*has_ad_auction_headers_attribute=*/true)));
break;
case FetchMethod::kDynamicIFrame:
ASSERT_TRUE(
NavigateToURL(shell(), embedded_https_test_server().GetURL(
"a.test", "/interest_group/empty.html")));
CreateIframe(fetch_url, /*has_ad_auction_headers_attribute=*/true);
break;
}
std::optional<std::string> ad_auction_header = GetAdAuctionHeader();
ASSERT_TRUE(ad_auction_header);
EXPECT_EQ(*ad_auction_header, "?1");
url::Origin request_origin = url::Origin::Create(fetch_url);
EXPECT_TRUE(WitnessedAuctionResultForOrigin(
request_origin, base64Decode(kLegitimateAdAuctionResponse)));
EXPECT_EQ(ParseAndFindAdAuctionSignals(request_origin, "slot1"), nullptr);
EXPECT_THAT(TakeAuctionAdditionalBidsForOriginAndNonce(
request_origin, "00000000-0000-0000-0000-000000000000"),
::testing::IsEmpty());
}
IN_PROC_BROWSER_TEST_P(
InterestGroupBrowserAdAuctionHeadersAllMethodsAndOriginsTest,
AdAuctionHeadersEligible_HasAdAuctionSignalsResponseHeader) {
GURL fetch_url = GetFetchURL((FetchURLParams){
.origin = is_cross_origin() ? "b.test" : "a.test",
.ad_auction_headers =
base::StrCat({"Ad-Auction-Signals: ", kLegitimateAdAuctionSignals})});
switch (ad_auction_headers_test_type()) {
case FetchMethod::kFetch:
ASSERT_TRUE(
NavigateToURL(shell(), embedded_https_test_server().GetURL(
"a.test", "/interest_group/empty.html")));
// Verify that the JavaScript doesn't see the `Ad-Auction-Signals`
// response header.
EXPECT_TRUE(ExecJs(web_contents()->GetPrimaryMainFrame(),
content::JsReplace(R"(
fetch($1, {adAuctionHeaders: true}).then((response) => {
if (response.headers.get('Ad-Auction-Signals')) {
throw 'Unexpectedly received `Ad-Auction-Signals` header';
}
});
)",
fetch_url)));
break;
case FetchMethod::kIFrame:
ASSERT_TRUE(NavigateToURL(
shell(), GetPageWithIFrameURL(
fetch_url, /*has_ad_auction_headers_attribute=*/true)));
break;
case FetchMethod::kDynamicIFrame:
ASSERT_TRUE(
NavigateToURL(shell(), embedded_https_test_server().GetURL(
"a.test", "/interest_group/empty.html")));
CreateIframe(fetch_url, /*has_ad_auction_headers_attribute=*/true);
break;
}
std::optional<std::string> ad_auction_header = GetAdAuctionHeader();
EXPECT_TRUE(ad_auction_header);
EXPECT_EQ(*ad_auction_header, "?1");
url::Origin request_origin = url::Origin::Create(fetch_url);
EXPECT_FALSE(WitnessedAuctionResultForOrigin(
request_origin, base64Decode(kLegitimateAdAuctionResponse)));
const scoped_refptr<HeaderDirectFromSellerSignals::Result> signals =
ParseAndFindAdAuctionSignals(url::Origin::Create(fetch_url), "slot1");
EXPECT_EQ(*signals->seller_signals(), R"({"signal1":"value1"})");
EXPECT_THAT(TakeAuctionAdditionalBidsForOriginAndNonce(
request_origin, "00000000-0000-0000-0000-000000000000"),
::testing::IsEmpty());
}
IN_PROC_BROWSER_TEST_P(
InterestGroupBrowserAdAuctionHeadersAllMethodsAndOriginsTest,
AdAuctionHeadersEligible_HasAdAuctionAdditionalBidResponseHeader) {
GURL fetch_url = GetFetchURL((FetchURLParams){
.origin = is_cross_origin() ? "b.test" : "a.test",
.ad_auction_headers =
base::StrCat({"Ad-Auction-Additional-Bid: ",
"00000000-0000-0000-0000-000000000000:e30="})});
switch (ad_auction_headers_test_type()) {
case FetchMethod::kFetch:
ASSERT_TRUE(
NavigateToURL(shell(), embedded_https_test_server().GetURL(
"a.test", "/interest_group/empty.html")));
// Verify that the JavaScript doesn't see the `Ad-Auction-Additional-Bid`
// response header.
EXPECT_TRUE(ExecJs(web_contents()->GetPrimaryMainFrame(),
content::JsReplace(R"(
fetch($1, {adAuctionHeaders: true}).then((response) => {
if (response.headers.get('Ad-Auction-Additional-Bid')) {
throw 'Unexpectedly received `Ad-Auction-Additional-Bid` header';
}
});
)",
fetch_url)));
break;
case FetchMethod::kIFrame:
ASSERT_TRUE(NavigateToURL(
shell(), GetPageWithIFrameURL(
fetch_url, /*has_ad_auction_headers_attribute=*/true)));
break;
case FetchMethod::kDynamicIFrame:
ASSERT_TRUE(
NavigateToURL(shell(), embedded_https_test_server().GetURL(
"a.test", "/interest_group/empty.html")));
CreateIframe(fetch_url, /*has_ad_auction_headers_attribute=*/true);
break;
}
std::optional<std::string> ad_auction_header = GetAdAuctionHeader();
EXPECT_TRUE(ad_auction_header);
EXPECT_EQ(*ad_auction_header, "?1");
url::Origin request_origin = url::Origin::Create(fetch_url);
EXPECT_FALSE(WitnessedAuctionResultForOrigin(
request_origin, base64Decode(kLegitimateAdAuctionResponse)));
EXPECT_EQ(ParseAndFindAdAuctionSignals(request_origin, "slot1"), nullptr);
EXPECT_THAT(
TakeAuctionAdditionalBidsForOriginAndNonce(
request_origin, "00000000-0000-0000-0000-000000000000"),
::testing::ElementsAre(::testing::FieldsAre(
/*signed_additional_bid=*/"e30=", /*seller_nonce=*/std::nullopt)));
}
IN_PROC_BROWSER_TEST_P(
InterestGroupBrowserAdAuctionHeadersAllMethodsAndOriginsTest,
AdAuctionHeadersEligible_MalformedAdAuctionAdditionalBidResponseHeader) {
// The second block has 3 digits, but it should have 4.
constexpr char kTooShortUuidV4[] = "00000000-000-0000-0000-000000000000";
GURL fetch_url = GetFetchURL((FetchURLParams){
.origin = is_cross_origin() ? "b.test" : "a.test",
.ad_auction_headers = base::StrCat(
{"Ad-Auction-Additional-Bid: ", kTooShortUuidV4, ":e30="})});
WebContentsConsoleObserver console_observer(web_contents());
console_observer.SetFilter(base::BindRepeating(IsErrorMessage));
console_observer.SetPattern(base::StrCat(
{"Malformed Ad-Auction-Additional-Bid: The first colon-delimited part "
"(the auction nonce) is expected to be 36 characters in size "
"(representing the canonical representation of a UUIDv4), but was "
"instead 35 characters. Header received: ",
kTooShortUuidV4, ":e30="}));
switch (ad_auction_headers_test_type()) {
case FetchMethod::kFetch:
ASSERT_TRUE(
NavigateToURL(shell(), embedded_https_test_server().GetURL(
"a.test", "/interest_group/empty.html")));
// Verify that the JavaScript doesn't see the `Ad-Auction-Additional-Bid`
// response header.
EXPECT_TRUE(ExecJs(web_contents()->GetPrimaryMainFrame(),
content::JsReplace(R"(
fetch($1, {adAuctionHeaders: true}).then((response) => {
if (response.headers.get('Ad-Auction-Additional-Bid')) {
throw 'Unexpectedly received `Ad-Auction-Additional-Bid` header';
}
});
)",
fetch_url)));
break;
case FetchMethod::kIFrame:
ASSERT_TRUE(NavigateToURL(
shell(), GetPageWithIFrameURL(
fetch_url, /*has_ad_auction_headers_attribute=*/true)));
break;
case FetchMethod::kDynamicIFrame:
ASSERT_TRUE(
NavigateToURL(shell(), embedded_https_test_server().GetURL(
"a.test", "/interest_group/empty.html")));
CreateIframe(fetch_url, /*has_ad_auction_headers_attribute=*/true);
break;
}
std::optional<std::string> ad_auction_header = GetAdAuctionHeader();
EXPECT_TRUE(ad_auction_header);
EXPECT_EQ(*ad_auction_header, "?1");
url::Origin request_origin = url::Origin::Create(fetch_url);
EXPECT_FALSE(WitnessedAuctionResultForOrigin(
request_origin, base64Decode(kLegitimateAdAuctionResponse)));
EXPECT_EQ(ParseAndFindAdAuctionSignals(request_origin, "slot1"), nullptr);
EXPECT_THAT(TakeAuctionAdditionalBidsForOriginAndNonce(request_origin,
kTooShortUuidV4),
::testing::IsEmpty());
// Verify the expected error is logged to the console.
ASSERT_TRUE(console_observer.Wait());
EXPECT_EQ(console_observer.messages().size(), 1u);
}
IN_PROC_BROWSER_TEST_P(
InterestGroupBrowserAdAuctionHeadersAllMethodsAndOriginsTest,
AdAuctionHeadersEligible_HasAllResponseHeaders) {
GURL fetch_url = GetFetchURL((FetchURLParams){
.origin = is_cross_origin() ? "b.test" : "a.test",
.ad_auction_headers = base::StrCat(
{"Ad-Auction-Result: ", kLegitimateAdAuctionResponse,
"\nAd-Auction-Signals: ", "{}", "\nAd-Auction-Additional-Bid: ",
"00000000-0000-0000-0000-000000000000:e30="})});
switch (ad_auction_headers_test_type()) {
case FetchMethod::kFetch:
ASSERT_TRUE(
NavigateToURL(shell(), embedded_https_test_server().GetURL(
"a.test", "/interest_group/empty.html")));
// Verify that the JavaScript doesn't see the
// `Ad-Auction-Signals` or `Ad-Auction-Additional-Bid` response headers.
// In contrast, it should see `Ad-Auction-Result`.
EXPECT_TRUE(ExecJs(web_contents()->GetPrimaryMainFrame(),
content::JsReplace(R"(
fetch($1, {adAuctionHeaders: true}).then((response) => {
if (!response.headers.get('Ad-Auction-Result')) {
throw 'Did not receive `Ad-Auction-Result` header';
}
if (response.headers.get('Ad-Auction-Signals')) {
throw 'Unexpectedly received `Ad-Auction-Signals` header';
}
if (response.headers.get('Ad-Auction-Additional-Bid')) {
throw 'Unexpectedly received `Ad-Auction-Additional-Bid` header';
}
});
)",
fetch_url)));
break;
case FetchMethod::kIFrame:
ASSERT_TRUE(NavigateToURL(
shell(), GetPageWithIFrameURL(
fetch_url, /*has_ad_auction_headers_attribute=*/true)));
break;
case FetchMethod::kDynamicIFrame:
ASSERT_TRUE(
NavigateToURL(shell(), embedded_https_test_server().GetURL(
"a.test", "/interest_group/empty.html")));
CreateIframe(fetch_url, /*has_ad_auction_headers_attribute=*/true);
break;
}
std::optional<std::string> ad_auction_header = GetAdAuctionHeader();
EXPECT_TRUE(ad_auction_header);
EXPECT_EQ(*ad_auction_header, "?1");
url::Origin request_origin = url::Origin::Create(fetch_url);
EXPECT_TRUE(WitnessedAuctionResultForOrigin(
request_origin, base64Decode(kLegitimateAdAuctionResponse)));
EXPECT_EQ(ParseAndFindAdAuctionSignals(request_origin, "slot1"), nullptr);
EXPECT_THAT(
TakeAuctionAdditionalBidsForOriginAndNonce(
request_origin, "00000000-0000-0000-0000-000000000000"),
::testing::ElementsAre(::testing::FieldsAre(
/*signed_additional_bid=*/"e30=", /*seller_nonce=*/std::nullopt)));
}
// On site a.test, test fetch request that gets redirected. Only the initial
// request is expected to have the ad auction request header "?1"; its response
// headers are ignored. The redirect request doesn't retain the ad auction
// request header, and so its response headers are also ignored.
IN_PROC_BROWSER_TEST_P(
InterestGroupBrowserAdAuctionHeadersAllMethodsAndOriginsTest,
RedirectOnSameSite_AdAuctionHeadersNotEligible) {
GURL redirect_url = GetFetchURL((FetchURLParams){
.origin = is_cross_origin() ? "b.test" : "a.test",
.path = "page_with_custom_ad_auction_result_header2.html",
.ad_auction_headers = base::StrCat(
{"Ad-Auction-Result: ", kLegitimateAdAuctionResponse,
"\nAd-Auction-Signals: ", "{}", "\nAd-Auction-Additional-Bid: ",
"00000000-0000-0000-0000-000000000000:e30="})});
GURL fetch_url = GetFetchURL((FetchURLParams){
.origin = is_cross_origin() ? "b.test" : "a.test",
.ad_auction_headers = base::StrCat(
{"Ad-Auction-Result: ", kLegitimateAdAuctionResponse,
"\nAd-Auction-Signals: ", "{}", "\nAd-Auction-Additional-Bid: ",
"00000000-0000-0000-0000-000000000000:e30="}),
.redirect_url = redirect_url});
switch (ad_auction_headers_test_type()) {
case FetchMethod::kFetch:
ASSERT_TRUE(
NavigateToURL(shell(), embedded_https_test_server().GetURL(
"a.test", "/interest_group/empty.html")));
// When this request is first issued, it appears to be eligible for
// adAuctionHeaders. However, when it responds as a redirect, neither
// the initial response nor the redirected response are eligible, and
// so none of the `Ad-Auction-Signals`, `Ad-Auction-Additional-Bid`, and
// `Ad-Auction-Result` response headers are processed for later use in
// the auction. At the same time, the browser _does_ clear the
// `Ad-Auction-Signals` and `Ad-Auction-Additional-Bid` response headers.
EXPECT_TRUE(ExecJs(web_contents()->GetPrimaryMainFrame(),
content::JsReplace(R"(
fetch($1, {adAuctionHeaders: true}).then((response) => {
if (!response.headers.get('Ad-Auction-Result')) {
throw 'Did not receive `Ad-Auction-Result` header';
}
if (response.headers.get('Ad-Auction-Signals')) {
throw 'Unexpectedly received `Ad-Auction-Signals` header';
}
if (response.headers.get('Ad-Auction-Additional-Bid')) {
throw 'Unexpectedly received `Ad-Auction-Additional-Bid` header';
}
});
)",
fetch_url)));
break;
case FetchMethod::kIFrame:
ASSERT_TRUE(NavigateToURL(
shell(), GetPageWithIFrameURL(
fetch_url, /*has_ad_auction_headers_attribute=*/true)));
break;
case FetchMethod::kDynamicIFrame:
ASSERT_TRUE(
NavigateToURL(shell(), embedded_https_test_server().GetURL(
"a.test", "/interest_group/empty.html")));
CreateIframe(fetch_url, /*has_ad_auction_headers_attribute=*/true);
break;
}
{
// For the initial fetch
std::optional<std::string> ad_auction_header = GetAdAuctionHeader();
EXPECT_TRUE(ad_auction_header);
EXPECT_EQ(*ad_auction_header, "?1");
}
{
// For the redirected fetch
std::optional<std::string> ad_auction_header =
GetAdAuctionHeader("page_with_custom_ad_auction_result_header2.html");
EXPECT_FALSE(ad_auction_header);
}
for (url::Origin request_origin :
{url::Origin::Create(fetch_url), url::Origin::Create(redirect_url)}) {
EXPECT_FALSE(WitnessedAuctionResultForOrigin(
request_origin, base64Decode(kLegitimateAdAuctionResponse)));
EXPECT_EQ(ParseAndFindAdAuctionSignals(request_origin, "slot1"), nullptr);
EXPECT_THAT(TakeAuctionAdditionalBidsForOriginAndNonce(
request_origin, "00000000-0000-0000-0000-000000000000"),
::testing::IsEmpty());
}
}
// On site a.test, test fetch request that gets redirected. Only the initial
// request is expected to have the ad auction request header "?1"; its response
// headers are ignored. The redirect request doesn't retain the ad auction
// request header, and so its response headers are also ignored. In the
// cross-origin test, the main site a.test makes a request to b.test, which
// redirects to c.test.
IN_PROC_BROWSER_TEST_P(
InterestGroupBrowserAdAuctionHeadersAllMethodsAndOriginsTest,
RedirectOnCrossSite_AdAuctionHeadersNotEligible) {
GURL redirect_url = GetFetchURL((FetchURLParams){
.origin = is_cross_origin() ? "c.test" : "a.test",
.path = "page_with_custom_ad_auction_result_header2.html",
.ad_auction_headers = base::StrCat(
{"Ad-Auction-Result: ", kLegitimateAdAuctionResponse,
"\nAd-Auction-Signals: ", "{}", "\nAd-Auction-Additional-Bid: ",
"00000000-0000-0000-0000-000000000000:e30="})});
GURL fetch_url = GetFetchURL((FetchURLParams){
.origin = is_cross_origin() ? "b.test" : "a.test",
.ad_auction_headers = base::StrCat(
{"Ad-Auction-Result: ", kLegitimateAdAuctionResponse,
"\nAd-Auction-Signals: ", "{}", "\nAd-Auction-Additional-Bid: ",
"00000000-0000-0000-0000-000000000000:e30="}),
.redirect_url = redirect_url});
switch (ad_auction_headers_test_type()) {
case FetchMethod::kFetch:
ASSERT_TRUE(
NavigateToURL(shell(), embedded_https_test_server().GetURL(
"a.test", "/interest_group/empty.html")));
// When this request is first issued, it appears to be eligible for
// adAuctionHeaders. However, when it responds as a redirect, neither
// the initial response nor the redirected response are eligible, and
// so none of the `Ad-Auction-Signals`, `Ad-Auction-Additional-Bid`, and
// `Ad-Auction-Result` response headers are processed for later use in
// the auction. At the same time, the browser _does_ clear the
// `Ad-Auction-Signals` and `Ad-Auction-Additional-Bid` response headers.
EXPECT_TRUE(ExecJs(web_contents()->GetPrimaryMainFrame(),
content::JsReplace(R"(
fetch($1, {adAuctionHeaders: true}).then((response) => {
if (!response.headers.get('Ad-Auction-Result')) {
throw 'Did not receive `Ad-Auction-Result` header';
}
if (response.headers.get('Ad-Auction-Signals')) {
throw 'Unexpectedly received `Ad-Auction-Signals` header';
}
if (response.headers.get('Ad-Auction-Additional-Bid')) {
throw 'Unexpectedly received `Ad-Auction-Additional-Bid` header';
}
});
)",
fetch_url)));
break;
case FetchMethod::kIFrame:
ASSERT_TRUE(NavigateToURL(
shell(), GetPageWithIFrameURL(
fetch_url, /*has_ad_auction_headers_attribute=*/true)));
break;
case FetchMethod::kDynamicIFrame:
ASSERT_TRUE(
NavigateToURL(shell(), embedded_https_test_server().GetURL(
"a.test", "/interest_group/empty.html")));
CreateIframe(fetch_url, /*has_ad_auction_headers_attribute=*/true);
break;
}
{
// // For the initial fetch
std::optional<std::string> ad_auction_header = GetAdAuctionHeader();
EXPECT_TRUE(ad_auction_header);
EXPECT_EQ(*ad_auction_header, "?1");
}
{
// For the redirected fetch
std::optional<std::string> ad_auction_header =
GetAdAuctionHeader("page_with_custom_ad_auction_result_header2.html");
EXPECT_FALSE(ad_auction_header);
}
for (url::Origin request_origin :
{url::Origin::Create(fetch_url), url::Origin::Create(redirect_url)}) {
EXPECT_FALSE(WitnessedAuctionResultForOrigin(
request_origin, base64Decode(kLegitimateAdAuctionResponse)));
EXPECT_EQ(ParseAndFindAdAuctionSignals(request_origin, "slot1"), nullptr);
EXPECT_THAT(TakeAuctionAdditionalBidsForOriginAndNonce(
request_origin, "00000000-0000-0000-0000-000000000000"),
::testing::IsEmpty());
}
}
IN_PROC_BROWSER_TEST_P(
InterestGroupBrowserAdAuctionHeadersAllMethodsAndOriginsTest,
NoAdAuctionRequestHeader_AdAuctionHeadersNotEligible) {
GURL fetch_url = GetFetchURL((FetchURLParams){
.origin = is_cross_origin() ? "b.test" : "a.test",
.ad_auction_headers = base::StrCat(
{"Ad-Auction-Result: ", kLegitimateAdAuctionResponse,
"\nAd-Auction-Signals: ", "{}", "\nAd-Auction-Additional-Bid: ",
"00000000-0000-0000-0000-000000000000:e30="})});
switch (ad_auction_headers_test_type()) {
case FetchMethod::kFetch:
ASSERT_TRUE(
NavigateToURL(shell(), embedded_https_test_server().GetURL(
"a.test", "/interest_group/empty.html")));
// For requests that don't specify adAuctionHeaders, none of the
// `Ad-Auction-Signals`, `Ad-Auction-Additional-Bid`, and
// `Ad-Auction-Result` response headers are affected by the browser,
// and all appear as expected. These response headers are still here
// even in the cross-site execution of this test because all three
// are included in the Access-Control-Expose-Headers header of the
// fetch response.
EXPECT_TRUE(ExecJs(web_contents()->GetPrimaryMainFrame(),
content::JsReplace(R"(
fetch($1).then((response) => {
if (!response.headers.get('Ad-Auction-Result')) {
throw 'Did not receive `Ad-Auction-Result` header';
}
if (!response.headers.get('Ad-Auction-Signals')) {
throw 'Did not receive `Ad-Auction-Signals` header';
}
if (!response.headers.get('Ad-Auction-Additional-Bid')) {
throw 'Did not receive `Ad-Auction-Additional-Bid` header';
}
});
)",
fetch_url)));
break;
case FetchMethod::kIFrame:
ASSERT_TRUE(NavigateToURL(
shell(), GetPageWithIFrameURL(
fetch_url, /*has_ad_auction_headers_attribute=*/false)));
break;
case FetchMethod::kDynamicIFrame:
ASSERT_TRUE(
NavigateToURL(shell(), embedded_https_test_server().GetURL(
"a.test", "/interest_group/empty.html")));
CreateIframe(fetch_url, /*has_ad_auction_headers_attribute=*/false);
break;
}
std::optional<std::string> ad_auction_header = GetAdAuctionHeader();
EXPECT_FALSE(ad_auction_header);
url::Origin request_origin = url::Origin::Create(fetch_url);
EXPECT_FALSE(WitnessedAuctionResultForOrigin(
request_origin, base64Decode(kLegitimateAdAuctionResponse)));
EXPECT_EQ(ParseAndFindAdAuctionSignals(request_origin, "slot1"), nullptr);
EXPECT_THAT(TakeAuctionAdditionalBidsForOriginAndNonce(
request_origin, "00000000-0000-0000-0000-000000000000"),
::testing::IsEmpty());
}
INSTANTIATE_TEST_SUITE_P(
FetchMethodAndOrigin,
InterestGroupBrowserAdAuctionHeadersAllMethodsAndOriginsTest,
::testing::Combine(::testing::Values(FetchMethod::kFetch,
FetchMethod::kIFrame,
FetchMethod::kDynamicIFrame),
::testing::Bool()),
[](const testing::TestParamInfo<
InterestGroupBrowserAdAuctionHeadersAllMethodsAndOriginsTest::
ParamType>& info) {
std::string same_or_cross_origin =
std::get<1>(info.param) ? "CrossOrigin" : "SameOrigin";
switch (std::get<0>(info.param)) {
case kFetch:
return base::StrCat({"Fetch_", same_or_cross_origin});
case kIFrame:
return base::StrCat({"IFrame_", same_or_cross_origin});
case kDynamicIFrame:
return base::StrCat({"DynamicIFrame_", same_or_cross_origin});
}
});
class InterestGroupBrowserAdAuctionHeadersAllMethodsTest
: public InterestGroupBrowserAdAuctionHeadersTest,
public ::testing::WithParamInterface<FetchMethod> {
protected:
FetchMethod ad_auction_headers_test_type() { return GetParam(); }
};
IN_PROC_BROWSER_TEST_P(
InterestGroupBrowserAdAuctionHeadersAllMethodsTest,
FetchNonAllowlistedCrossOrigin_HasAllResponseHeaders_AdAuctionHeadersNotEligible) {
// "d.test" is not allowlisted for the API. Thus the request isn't eligible
// for ad auction headers.
GURL fetch_url = GetFetchURL((FetchURLParams){
.origin = "d.test",
.ad_auction_headers = base::StrCat(
{"Ad-Auction-Result: ", kLegitimateAdAuctionResponse,
"\nAd-Auction-Signals: ", "{}", "\nAd-Auction-Additional-Bid: ",
"00000000-0000-0000-0000-000000000000:e30="})});
switch (ad_auction_headers_test_type()) {
case FetchMethod::kFetch:
ASSERT_TRUE(
NavigateToURL(shell(), embedded_https_test_server().GetURL(
"a.test", "/interest_group/empty.html")));
// Because this initial request is ineligible for adAuctionHeaders, none
// of the `Ad-Auction-Signals`, `Ad-Auction-Additional-Bid`, and
// `Ad-Auction-Result` response headers are processed for later use in
// the auction, and furthermore, neither the `Ad-Auction-Signals` nor the
// `Ad-Auction-Additional-Bid` response headers are cleared.
EXPECT_TRUE(ExecJs(web_contents()->GetPrimaryMainFrame(),
content::JsReplace(R"(
fetch($1).then((response) => {
if (!response.headers.get('Ad-Auction-Result')) {
throw 'Did not receive `Ad-Auction-Result` header';
}
if (!response.headers.get('Ad-Auction-Signals')) {
throw 'Did not receive `Ad-Auction-Signals` header';
}
if (!response.headers.get('Ad-Auction-Additional-Bid')) {
throw 'Did not receive `Ad-Auction-Additional-Bid` header';
}
});
)",
fetch_url)));
break;
case FetchMethod::kIFrame:
ASSERT_TRUE(NavigateToURL(
shell(), GetPageWithIFrameURL(
fetch_url, /*has_ad_auction_headers_attribute=*/true)));
break;
case FetchMethod::kDynamicIFrame:
ASSERT_TRUE(
NavigateToURL(shell(), embedded_https_test_server().GetURL(
"a.test", "/interest_group/empty.html")));
CreateIframe(fetch_url, /*has_ad_auction_headers_attribute=*/true);
break;
}
std::optional<std::string> ad_auction_header = GetAdAuctionHeader();
EXPECT_FALSE(ad_auction_header);
url::Origin request_origin = url::Origin::Create(fetch_url);
EXPECT_FALSE(WitnessedAuctionResultForOrigin(
request_origin, base64Decode(kLegitimateAdAuctionResponse)));
EXPECT_EQ(ParseAndFindAdAuctionSignals(request_origin, "slot1"), nullptr);
EXPECT_THAT(TakeAuctionAdditionalBidsForOriginAndNonce(
request_origin, "00000000-0000-0000-0000-000000000000"),
::testing::IsEmpty());
}
INSTANTIATE_TEST_SUITE_P(
FetchMethod,
InterestGroupBrowserAdAuctionHeadersAllMethodsTest,
::testing::Values(FetchMethod::kFetch,
FetchMethod::kIFrame,
FetchMethod::kDynamicIFrame),
[](const testing::TestParamInfo<
InterestGroupBrowserAdAuctionHeadersAllMethodsTest::ParamType>& info) {
switch (info.param) {
case kFetch:
return "Fetch";
case kIFrame:
return "IFrame";
case kDynamicIFrame:
return "DynamicIFrame";
}
});
// Runs an ad auction similar to the one in
// InterestGroupFencedFrameBrowserTest.RunAdAuctionWithWinner but also registers
// an ad beacon that is sent by the render URL.
IN_PROC_BROWSER_TEST_F(InterestGroupFencedFrameBrowserTest,
RunAdAuctionWithWinnerRegisterAdBeaconBuyer) {
URLLoaderMonitor url_loader_monitor;
GURL test_url = embedded_https_test_server().GetURL(
"a.test", "/fenced_frames/basic.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url = embedded_https_test_server().GetURL(
"c.test", "/fenced_frames/ad_with_fenced_frame_reporting.html");
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"))
.SetTrustedBiddingSignalsUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/trusted_bidding_signals.json"))
.SetTrustedBiddingSignalsKeys({{"key1"}})
.SetAds({{{ad_url, R"({"ad":"metadata","here":[1,2]})"}}})
.Build()));
ASSERT_NO_FATAL_FAILURE(RunAuctionAndNavigateFencedFrame(
ad_url, JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
})",
test_origin,
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js"))));
std::optional<network::ResourceRequest> request =
url_loader_monitor.WaitForUrl(embedded_https_test_server().GetURL(
"b.test", "/echoall?report_win_beacon"));
ASSERT_TRUE(request);
EXPECT_EQ(net::HttpRequestHeaders::kPostMethod, request->method);
}
// Runs auction like Just like
// InterestGroupFencedFrameBrowserTest.RunAdAuctionWithWinner but also registers
// an ad beacon that is sent by the render URL. The ad beacon will not be sent
// out because the origin the beacon is being sent to (b.test) is not enrolled.
IN_PROC_BROWSER_TEST_F(InterestGroupFencedFrameBrowserTest,
RunAdAuctionWithWinnerRegisterUnenrolledAdBeaconBuyer) {
// Un-enroll 'b.test' from Privacy Sandbox. Keep the other origins to allow
// the ad auction to run properly.
content_browser_client_->SetAllowList({
url::Origin::Create(embedded_https_test_server().GetURL("a.test", "/")),
url::Origin::Create(embedded_https_test_server().GetURL("c.test", "/")),
});
URLLoaderMonitor url_loader_monitor;
GURL test_url = embedded_https_test_server().GetURL(
"a.test", "/fenced_frames/basic.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url = embedded_https_test_server().GetURL(
"c.test", "/fenced_frames/ad_with_fenced_frame_reporting.html");
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"))
.SetTrustedBiddingSignalsUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/trusted_bidding_signals.json"))
.SetTrustedBiddingSignalsKeys({{"key1"}})
.SetAds({{{ad_url, R"({"ad":"metadata","here":[1,2]})"}}})
.Build()));
ASSERT_NO_FATAL_FAILURE(RunAuctionAndNavigateFencedFrame(
ad_url, JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
})",
test_origin,
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js"))));
// Send a basic request to the reporting destination.
GURL reporting_url = embedded_https_test_server().GetURL(
"b.test", "/echoall?report_win_beacon");
SendBasicRequest(reporting_url);
// Verify the request received is the basic request, which implies the
// beacon was not sent, as expected.
EXPECT_EQ(network_responder_->GetRequest()->content, "Basic request data");
EXPECT_TRUE(network_responder_->HasReceivedRequest());
}
// Runs an ad auction similar to the one in
// InterestGroupFencedFrameBrowserTest.RunAdAuctionWithWinner but also registers
// ad macros. The destination URL's ${} template is substituted with the ad
// macro's value if there is one, otherwise unsubstituted. A report is sent to
// the new destination URL.
IN_PROC_BROWSER_TEST_F(InterestGroupFencedFrameBrowserTest,
RunAdAuctionWithWinnerRegisterAdMacro) {
URLLoaderMonitor url_loader_monitor;
GURL test_url = embedded_https_test_server().GetURL(
"a.test", "/fenced_frames/basic.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url = embedded_https_test_server().GetURL(
"c.test", "/fenced_frames/ad_with_3pat_macro_reporting.html");
url::Origin allowed_origin = url::Origin::Create(GURL("https://b.test"));
content_browser_client_->AddToAllowList({allowed_origin});
std::vector<url::Origin> allowed_reporting_origins = {allowed_origin};
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test",
"/interest_group/bidding_logic_register_ad_macro.js"))
.SetAds(
{{{ad_url,
/*metadata=*/std::nullopt,
/*size_group=*/std::nullopt,
/*buyer_reporting_id=*/std::nullopt,
/*buyer_and_seller_reporting_id=*/std::nullopt,
/*selectable_buyer_and_seller_reporting_ids=*/std::nullopt,
/*ad_render_id=*/std::nullopt,
/*allowed_reporting_origins=*/
std::move(allowed_reporting_origins)}}})
.Build()));
ASSERT_NO_FATAL_FAILURE(RunAuctionAndNavigateFencedFrame(
ad_url, JsReplace(
R"({
seller: $1,
decisionLogicUrl: $2,
interestGroupBuyers: [$1],
})",
test_origin,
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js"))));
std::optional<network::ResourceRequest> request =
url_loader_monitor.WaitForUrl(
GURL("https://b.test/echo?a=value_a&b=value_b&c=${NOT_REGISTERED}"));
ASSERT_TRUE(request);
EXPECT_EQ(net::HttpRequestHeaders::kGetMethod, request->method);
}
// Runs an auction similar to
// InterestGroupFencedFrameBrowserTest.RunAdAuctionWithWinner, but also triggers
// sending a *private aggregation* event using `window.fence.reportEvent`.
IN_PROC_BROWSER_TEST_F(InterestGroupFencedFrameBrowserTest,
RunAdAuctionWithWinnerRegisterPrivateAggregationBuyer) {
URLLoaderMonitor url_loader_monitor;
GURL test_url = embedded_https_test_server().GetURL(
"a.test", "/fenced_frames/basic.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
base::RunLoop run_loop;
class TestPrivateAggregationManagerImpl
: public PrivateAggregationManagerImpl {
public:
TestPrivateAggregationManagerImpl(
std::unique_ptr<PrivateAggregationBudgeter> budgeter,
std::unique_ptr<PrivateAggregationHost> host)
: PrivateAggregationManagerImpl(std::move(budgeter),
std::move(host),
/*storage_partition=*/nullptr) {}
};
base::MockRepeatingCallback<void(
PrivateAggregationHost::ReportRequestGenerator,
PrivateAggregationPendingContributions::Wrapper,
PrivateAggregationBudgetKey, PrivateAggregationHost::NullReportBehavior)>
mock_callback;
auto* storage_partition_impl =
static_cast<StoragePartitionImpl*>(shell()
->web_contents()
->GetBrowserContext()
->GetDefaultStoragePartition());
storage_partition_impl->OverridePrivateAggregationManagerForTesting(
std::make_unique<TestPrivateAggregationManagerImpl>(
std::make_unique<MockPrivateAggregationBudgeter>(),
std::make_unique<PrivateAggregationHost>(
/*on_report_request_received=*/mock_callback.Get(),
/*browser_context=*/storage_partition_impl->browser_context())));
// We only need to test that a request was made in PrivateAggregationHost, so
// we mock out the callback and check that it was called. The callback will
// only run *after* the ad auction finishes, but we register it beforehand
// so that it is guaranteed to detect when the private aggregation event is
// sent.
EXPECT_CALL(mock_callback, Run)
.WillRepeatedly(
[&](PrivateAggregationHost::ReportRequestGenerator generator,
PrivateAggregationPendingContributions::Wrapper contributions,
PrivateAggregationBudgetKey budget_key,
PrivateAggregationHost::NullReportBehavior null_report_behavior) {
AggregatableReportRequest request = GenerateReportRequest(
std::move(generator), std::move(contributions));
ASSERT_EQ(request.payload_contents().contributions.size(), 1u);
EXPECT_EQ(request.payload_contents().contributions[0].bucket, 3);
EXPECT_EQ(request.payload_contents().contributions[0].value, 5);
EXPECT_EQ(request.shared_info().reporting_origin, test_origin);
EXPECT_EQ(budget_key.caller_api(),
PrivateAggregationCallerApi::kProtectedAudience);
EXPECT_EQ(budget_key.origin(), test_origin);
EXPECT_EQ(
null_report_behavior,
PrivateAggregationHost::NullReportBehavior::kDontSendReport);
run_loop.Quit();
});
GURL ad_url = embedded_https_test_server().GetURL(
"c.test",
"/fenced_frames/ad_with_fenced_frame_private_aggregation_reporting.html");
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"))
.SetTrustedBiddingSignalsUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/trusted_bidding_signals.json"))
.SetTrustedBiddingSignalsKeys({{"key1"}})
.SetAds({{{ad_url, R"({"ad":"metadata","here":[1,2]})"}}})
.Build()));
ASSERT_NO_FATAL_FAILURE(RunAuctionAndNavigateFencedFrame(
ad_url, JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
})",
test_origin,
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js"))));
run_loop.Run();
}
// Before M120: Leaving the interest group an ad component is not supported.
// M120 and afterwards: Leaving the group from an ad component that is same
// origin to interest group owner should succeed.
IN_PROC_BROWSER_TEST_F(InterestGroupFencedFrameBrowserTest,
SameOriginAdComponentsLeaveSucceed) {
url::Origin test_origin =
url::Origin::Create(embedded_https_test_server().GetURL("a.test", "/"));
GURL ad_component_url = embedded_https_test_server().GetURL(
"a.test", "/fenced_frames/ad_that_leaves_interest_group.html");
AttachInterestGroupObserver();
// The main frame will have an origin of "a.test".
ASSERT_NO_FATAL_FAILURE(RunBasicAuctionWithAdComponents(ad_component_url));
// InterestGroupAccessObserver should see the join, auction and the leave.
WaitForAccessObserved(
{{"global", TestInterestGroupObserver::kJoin, test_origin, "cars"},
{"1", TestInterestGroupObserver::kLoaded, test_origin, "cars"},
{"1", TestInterestGroupObserver::kBid, test_origin, "cars", 1.0},
{"1", TestInterestGroupObserver::kWin, test_origin, "cars"},
{"global", TestInterestGroupObserver::kLeave, test_origin, "cars"}});
EXPECT_TRUE(GetAllInterestGroups().empty());
}
// Leaving the group from a site that is cross origin to interest group owner
// should always fail, regardless of whether this is an ad component or not.
IN_PROC_BROWSER_TEST_F(InterestGroupFencedFrameBrowserTest,
CrossOriginAdComponentsLeaveFail) {
url::Origin test_origin =
url::Origin::Create(embedded_https_test_server().GetURL("a.test", "/"));
GURL ad_component_url = embedded_https_test_server().GetURL(
"b.test", "/fenced_frames/ad_that_leaves_interest_group.html");
AttachInterestGroupObserver();
// The main frame will have an origin of "a.test".
ASSERT_NO_FATAL_FAILURE(RunBasicAuctionWithAdComponents(ad_component_url));
// InterestGroupAccessObserver should see the join and auction, but not the
// implicit leave since it was blocked due to cross origin to the interest
// group owner.
WaitForAccessObserved(
{{"global", TestInterestGroupObserver::kJoin, test_origin, "cars"},
{"1", TestInterestGroupObserver::kLoaded, test_origin, "cars"},
{"1", TestInterestGroupObserver::kBid, test_origin, "cars", 1.0},
{"1", TestInterestGroupObserver::kWin, test_origin, "cars"}});
// The ad should not leave the interest group.
EXPECT_EQ(1u, GetAllInterestGroups().size());
}
class InterestGroupAuctionLimitBrowserTest : public InterestGroupBrowserTest {
public:
InterestGroupAuctionLimitBrowserTest() {
// Only 2 auctions are allowed per-page.
feature_list_.InitWithFeaturesAndParameters(
/*enabled_features=*/
{{features::kFledgeLimitNumAuctions, {{"max_auctions_per_page", "2"}}},
{network::features::kAdInterestGroupAPIRestrictedPolicyByDefault, {}}},
/*disabled_features=*/{});
// TODO(crbug.com/40172488): When
// kAdInterestGroupAPIRestrictedPolicyByDefault is the default, we won't
// need to set it here.
}
protected:
base::test::ScopedFeatureList feature_list_;
};
// TODO(crbug.com/40817393): Investigate why this is failing on
// android-pie-x86-rel.
#if BUILDFLAG(IS_ANDROID)
#define MAYBE_NavigatingWithBfcachePreservesAuctionLimits \
DISABLED_NavigatingWithBfcachePreservesAuctionLimits
#else
#define MAYBE_NavigatingWithBfcachePreservesAuctionLimits \
NavigatingWithBfcachePreservesAuctionLimits
#endif // BUILDFLAG(IS_ANDROID)
// Perform an auction, navigate the top-level frame, then navigate it back.
// Perform 2 more auctions. The second of those two should fail, because 2
// auctions have already been performed on the page -- one before the top level
// bfcached navigations, and one after.
//
// That is, the auction limit count is preserved due to bfcache.
IN_PROC_BROWSER_TEST_F(InterestGroupAuctionLimitBrowserTest,
MAYBE_NavigatingWithBfcachePreservesAuctionLimits) {
const GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
const url::Origin test_origin = url::Origin::Create(test_url);
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"))
.SetAds({{{GURL("https://example.com/render"),
R"({"ad":"metadata","here":[1,2]})"}}})
.Build()));
// 1st auction -- before navigations
EXPECT_EQ("https://example.com/render",
RunAuctionAndWaitForUrl(JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
})",
test_origin,
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js"))));
// Navigate, then navigate back. The auction limits shouldn't be reset since
// the original page goes into the bfcache.
const GURL test_url_b =
embedded_https_test_server().GetURL("b.test", "/echo");
ASSERT_TRUE(NavigateToURL(shell(), test_url_b));
TestNavigationObserver back_load_observer(shell()->web_contents());
shell()->web_contents()->GetController().GoBack();
back_load_observer.Wait();
// 2nd auction -- after navigations
EXPECT_EQ("https://example.com/render",
RunAuctionAndWaitForUrl(JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
})",
test_origin,
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js"))));
// 3rd auction -- after navigations; should fail due to hitting the auction
// limit.
EXPECT_EQ(base::Value(),
RunAuctionAndWait(JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
})",
test_origin,
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js"))));
}
// Create a page with a cross-origin iframe. Run an auction in the main frame,
// then run 2 auctions in the cross-origin iframe. The last auction should fail
// due to encontering the auction limit, since the limit is stored per-page (top
// level frame), not per frame.
IN_PROC_BROWSER_TEST_F(InterestGroupAuctionLimitBrowserTest,
AuctionLimitSharedWithCrossOriginFrameOnPage) {
// Give the cross-origin iframe permission to run auctions.
const GURL test_url = embedded_https_test_server().GetURL(
"a.test",
"/cross_site_iframe_factory.html?a.test(b.test{"
"allow-run-ad-auction})");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
const url::Origin test_origin = url::Origin::Create(test_url);
RenderFrameHost* const b_iframe =
ChildFrameAt(web_contents()->GetPrimaryMainFrame(), 0);
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"))
.SetAds({{{GURL("https://example.com/render"),
R"({"ad":"metadata","here":[1,2]})"}}})
.Build()));
// 1st auction -- in main frame
EXPECT_EQ("https://example.com/render",
RunAuctionAndWaitForUrl(JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
})",
test_origin,
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js"))));
// 2nd auction -- in cross-origin iframe
EXPECT_EQ("https://example.com/render",
RunAuctionAndWaitForUrl(
JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
})",
test_origin,
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js")),
b_iframe));
// 3rd auction -- in cross-origin iframe; should fail due to hitting the
// auction limit.
EXPECT_EQ(
base::Value(),
RunAuctionAndWait(JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
})",
test_origin,
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js")),
b_iframe));
}
// Test event-level reporting of ad component fenced frame. This test class is
// parameterized so that it tests the following scenarios, for an ad auction
// that results in a winning ad and a winning ad component.
// 1. Ad in fenced frame, ad component in a nested fenced frame.
// 2. Ad in fenced frame, ad component in a nested iframe.
// 3. Ad in iframe, ad component in a nested fenced frame.
// 4. Ad in iframe, ad component in a nested iframe.
// TODO(crbug.com/40060657): Once urn iframes are no longer supported, this test
// should only test scenario 1.
class InterestGroupAdComponentAutomaticBeaconBrowserTest
: public InterestGroupFencedFrameBrowserTest,
public testing::WithParamInterface<std::tuple<bool, bool>> {
public:
InterestGroupAdComponentAutomaticBeaconBrowserTest() = default;
std::unique_ptr<NetworkResponder> CreateNetworkResponder() override {
// Fenced frame window.fence.reportEvent API requires a responder that
// handles beacons sent to the reporting url.
return std::make_unique<NetworkResponder>(embedded_https_test_server(),
"/report_event.html");
}
// Return true if the ad from the auction is loaded in a fenced frame, false
// if loaded in an iframe.
bool IsAdLoadedInFencedFrame() const { return std::get<0>(GetParam()); }
// Return true if the ad component from the auction is loaded in a fenced
// frame, false if loaded in an iframe.
bool IsAdComponentLoadedInFencedFrame() const {
return std::get<1>(GetParam());
}
// Get the render frame host of the fenced frame or the iframe that renders
// the ad from the auction. This function assumes there is only one such frame
// within `execution_target`.
RenderFrameHost* GetAdRenderFrameHost(
const ToRenderFrameHost& execution_target,
bool is_fenced_frame) {
if (is_fenced_frame) {
return GetFencedFrameRenderFrameHost(execution_target);
}
FrameTreeNode* ad_frame_node =
FrameTreeNode::From(execution_target.render_frame_host());
EXPECT_EQ(ad_frame_node->child_count(), 1u);
FrameTreeNode* ad_component_frame_node = ad_frame_node->child_at(0);
return ad_component_frame_node->current_frame_host();
}
// Run an ad auction with an ad component. The ad is loaded in a fenced frame
// or an iframe according to the return value of `IsAdLoadedInFencedFrame()`.
// Similarly the ad component is loaded in a nested fenced frame or a nested
// iframe according to the return value of
// `IsAdComponentLoadedInFencedFrame()`.
void RunAdAuctionAndLoadAdComponent() {
std::string fenced_frame_url = "/fenced_frames/basic.html";
std::string iframe_url = "/fenced_frames/basic_iframe.html";
GURL test_url = embedded_https_test_server().GetURL(
"a.test", IsAdLoadedInFencedFrame() ? fenced_frame_url : iframe_url);
ASSERT_TRUE(NavigateToURL(shell(), test_url));
GURL ad_component_url = embedded_https_test_server().GetURL(
"a.test", "/set-header?Supports-Loading-Mode: fenced-frame");
// The ad url contains a fenced frame or an iframe, which is used to load
// the ad component.
GURL ad_url = embedded_https_test_server().GetURL(
"c.test",
IsAdComponentLoadedInFencedFrame() ? fenced_frame_url : iframe_url);
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
/*owner=*/url::Origin::Create(test_url),
/*name=*/"cars",
/*priority=*/0.0, /*execution_mode=*/
blink::InterestGroup::ExecutionMode::kCompatibilityMode,
/*bidding_url=*/
embedded_https_test_server().GetURL(
"a.test",
"/interest_group/bidding_logic_register_ad_beacon.js"),
/*ads=*/{{{ad_url, /*metadata=*/std::nullopt}}},
/*ad_components=*/
{{{ad_component_url, /*metadata=*/std::nullopt}}}));
if (IsAdLoadedInFencedFrame()) {
ASSERT_NO_FATAL_FAILURE(RunAuctionAndNavigateFencedFrame(
ad_url,
JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1]
})",
url::Origin::Create(test_url),
embedded_https_test_server().GetURL(
"a.test",
"/interest_group/decision_logic_register_ad_beacon.js")),
/*execution_target=*/std::nullopt));
} else {
ASSERT_NO_FATAL_FAILURE(RunAuctionAndWaitForURLAndNavigateIframe(
JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1]
})",
url::Origin::Create(test_url),
embedded_https_test_server().GetURL(
"a.test",
"/interest_group/decision_logic_register_ad_beacon.js")),
ad_url));
}
// Get the render frame host of the frame that renders the ad.
RenderFrameHost* ad_frame =
GetAdRenderFrameHost(shell()->web_contents()->GetPrimaryMainFrame(),
IsAdLoadedInFencedFrame());
// Validate the ad components from the ad frame.
std::optional<std::vector<GURL>> components =
GetAdAuctionComponentsInJS(ad_frame, 1);
ASSERT_TRUE(components);
ASSERT_EQ(1u, components->size());
EXPECT_EQ(url::kUrnScheme, (*components)[0].scheme_piece());
// Load the ad component.
if (IsAdComponentLoadedInFencedFrame()) {
NavigateFencedFrameAndWait((*components)[0], ad_component_url, ad_frame);
} else {
RenderFrameHost* ad_component_frame =
GetAdRenderFrameHost(ad_frame, IsAdComponentLoadedInFencedFrame());
TestFrameNavigationObserver observer(ad_component_frame);
EXPECT_TRUE(ExecJs(ad_frame,
JsReplace("document.querySelector('iframe').src = $1",
(*components)[0])));
observer.Wait();
EXPECT_EQ(ad_component_url, observer.last_committed_url());
EXPECT_TRUE(observer.last_navigation_succeeded());
}
}
private:
base::test::ScopedFeatureList feature_list_;
};
// Test window.fence.reportEvent from an ad component frame is disallowed:
// 1. Run an auction with an ad component.
// 2. Load the ad in a top-level frame.
// 3. Load the ad component in the nested frame.
// 4. Invoke window.fence.reportEvent from the nested ad component frame.
// 5. Expect reportEvent to fail because it is not allowed from an ad component.
// For an ad component, only reserved.top_navigation beacon is allowed.
IN_PROC_BROWSER_TEST_P(InterestGroupAdComponentAutomaticBeaconBrowserTest,
AdComponentReportEventNotAllowed) {
GURL ad_component_url = embedded_https_test_server().GetURL(
"a.test", "/set-header?Supports-Loading-Mode: fenced-frame");
ASSERT_NO_FATAL_FAILURE(RunAdAuctionAndLoadAdComponent());
RenderFrameHost* ad_frame =
GetAdRenderFrameHost(shell()->web_contents()->GetPrimaryMainFrame(),
IsAdLoadedInFencedFrame());
RenderFrameHost* ad_component_frame =
GetAdRenderFrameHost(ad_frame, IsAdComponentLoadedInFencedFrame());
// Monitor the console errors.
WebContentsConsoleObserver console_observer(web_contents());
console_observer.SetFilter(base::BindRepeating(IsErrorMessage));
console_observer.SetPattern(
"This frame is an ad component. It is not allowed to call "
"fence.reportEvent.");
// Invoke window.fence.reportEvent from the ad component frame. This should
// fail because only reserved.top_navigation event beacon is allowed from an
// ad component.
EXPECT_TRUE(ExecJs(ad_component_frame, R"(
window.fence.reportEvent(
{
eventType: 'click',
eventData: 'some data',
destination: ['seller']
}
);
)"));
// Verify the expected error is logged to the console.
ASSERT_TRUE(console_observer.Wait());
EXPECT_EQ(console_observer.messages().size(), 1u);
// Send a basic request to the reporting destination.
GURL reporting_url =
embedded_https_test_server().GetURL("a.test", "/report_event.html");
SendBasicRequest(reporting_url);
// Verify the request received is the basic request, which implies the
// reportEvent beacon was not sent, as expected.
EXPECT_EQ(network_responder_->GetRequest()->content, "Basic request data");
EXPECT_TRUE(network_responder_->HasReceivedRequest());
}
// Test window.fence.reportEvent from an iframe nested under an ad component
// frame is also disallowed:
// 1. Run an auction with an ad component.
// 2. Load the ad in a top-level frame.
// 3. Load the ad component in the nested frame.
// 4. Navigate the iframe nested under the ad component frame.
// 5. Invoke window.fence.reportEvent from the nested iframe under the ad
// component frame.
// 6. Expect reportEvent to fail because it is not allowed from an iframe nested
// under an ad component frame.
// For such a frame, only reserved.top_navigation beacon is allowed.
IN_PROC_BROWSER_TEST_P(InterestGroupAdComponentAutomaticBeaconBrowserTest,
ReportEventNotAllowedInNestedIframeUnderAdComponent) {
GURL ad_component_url = embedded_https_test_server().GetURL(
"a.test", "/set-header?Supports-Loading-Mode: fenced-frame");
ASSERT_NO_FATAL_FAILURE(RunAdAuctionAndLoadAdComponent());
RenderFrameHost* ad_frame =
GetAdRenderFrameHost(shell()->web_contents()->GetPrimaryMainFrame(),
IsAdLoadedInFencedFrame());
RenderFrameHost* ad_component_frame =
GetAdRenderFrameHost(ad_frame, IsAdComponentLoadedInFencedFrame());
// Add a nested iframe under the ad component frame.
EXPECT_TRUE(ExecJs(ad_component_frame,
"var nested_iframe = document.createElement('iframe');"
"document.body.appendChild(nested_iframe);"));
EXPECT_EQ(
1U, static_cast<RenderFrameHostImpl*>(ad_component_frame)->child_count());
auto* nested_iframe =
static_cast<RenderFrameHostImpl*>(ad_component_frame)->child_at(0);
// Navigate the added nested iframe.
GURL nested_iframe_url = embedded_https_test_server().GetURL(
"a.test", "/fenced_frames/title0.html");
TestFrameNavigationObserver observer(nested_iframe->current_frame_host());
EXPECT_TRUE(ExecJs(ad_component_frame,
JsReplace("nested_iframe.src = $1", nested_iframe_url)));
observer.Wait();
EXPECT_EQ(nested_iframe_url,
nested_iframe->current_frame_host()->GetLastCommittedURL());
// Monitor the console errors.
WebContentsConsoleObserver console_observer(web_contents());
console_observer.SetFilter(base::BindRepeating(IsErrorMessage));
console_observer.SetPattern(
"This frame is an ad component. It is not allowed to call "
"fence.reportEvent.");
// Invoke window.fence.reportEvent from the nested iframe. This should fail
// because only reserved.top_navigation event beacon is allowed from an
// ad component.
EXPECT_TRUE(ExecJs(nested_iframe->current_frame_host(), R"(
window.fence.reportEvent(
{
eventType: 'click',
eventData: 'some data',
destination: ['seller']
}
);
)"));
// Verify the expected error is logged to the console.
ASSERT_TRUE(console_observer.Wait());
EXPECT_EQ(console_observer.messages().size(), 1u);
// Send a basic request to the reporting destination.
GURL reporting_url =
embedded_https_test_server().GetURL("a.test", "/report_event.html");
SendBasicRequest(reporting_url);
// Verify the request received is the basic request, which implies the
// reportEvent beacon was not sent, as expected.
EXPECT_EQ(network_responder_->GetRequest()->content, "Basic request data");
EXPECT_TRUE(network_responder_->HasReceivedRequest());
}
// Test automatic beacons from an ad component frames nested in the main ad
// frame:
// 1. Run an auction with an ad component.
// 2. Load the ad in a top-level frame.
// 3. Load the ad component in the nested frame.
// 4. Register the automatic beacon data.
// 5. Navigate to a same-origin url.
// 6. The automatic beacon from ad component is sent successfully.
IN_PROC_BROWSER_TEST_P(InterestGroupAdComponentAutomaticBeaconBrowserTest,
AdComponentSameOriginNavigation) {
GURL ad_component_url = embedded_https_test_server().GetURL(
"a.test", "/set-header?Supports-Loading-Mode: fenced-frame");
ASSERT_NO_FATAL_FAILURE(RunAdAuctionAndLoadAdComponent());
RenderFrameHost* ad_frame =
GetAdRenderFrameHost(shell()->web_contents()->GetPrimaryMainFrame(),
IsAdLoadedInFencedFrame());
RenderFrameHost* ad_component_frame =
GetAdRenderFrameHost(ad_frame, IsAdComponentLoadedInFencedFrame());
// Set automatic beacon data for ad component.
EXPECT_TRUE(ExecJs(ad_component_frame, (R"(
window.fence.setReportEventDataForAutomaticBeacons(
{
eventType: 'reserved.top_navigation_commit',
eventData: 'should be igonred',
destination: ['seller']
}
);
)")));
// Perform a same-origin `_unfencedTop` navigation.
GURL navigation_url = embedded_https_test_server().GetURL(
"a.test", "/set-header?Supports-Loading-Mode: fenced-frame");
EXPECT_TRUE(
ExecJs(ad_component_frame,
JsReplace("window.open($1, '_unfencedTop');", navigation_url)));
// Expect automatic beacon being sent successfully with empty event data.
EXPECT_TRUE(network_responder_->GetRequest()->content.empty());
EXPECT_TRUE(network_responder_->HasReceivedRequest());
}
// Test automatic beacons from an ad component frames nested in the main ad
// frame:
// 1. Run an auction with an ad component.
// 2. Load the ad in a top-level frame.
// 3. Load the ad component in the nested frame.
// 4. Register the automatic beacon data.
// 5. Navigate to a cross-origin url.
// 6. The automatic beacon from ad component is sent successfully.
IN_PROC_BROWSER_TEST_P(InterestGroupAdComponentAutomaticBeaconBrowserTest,
AdComponentCrossOriginNavigation) {
GURL ad_component_url = embedded_https_test_server().GetURL(
"a.test", "/set-header?Supports-Loading-Mode: fenced-frame");
ASSERT_NO_FATAL_FAILURE(RunAdAuctionAndLoadAdComponent());
RenderFrameHost* ad_frame =
GetAdRenderFrameHost(shell()->web_contents()->GetPrimaryMainFrame(),
IsAdLoadedInFencedFrame());
RenderFrameHost* ad_component_frame =
GetAdRenderFrameHost(ad_frame, IsAdComponentLoadedInFencedFrame());
// Set automatic beacon data for ad component.
EXPECT_TRUE(ExecJs(ad_component_frame, (R"(
window.fence.setReportEventDataForAutomaticBeacons(
{
eventType: 'reserved.top_navigation_commit',
eventData: 'should be igonred',
destination: ['seller']
}
);
)")));
// Perform a cross-origin `_unfencedTop` navigation.
GURL navigation_url = embedded_https_test_server().GetURL(
"b.test", "/set-header?Supports-Loading-Mode: fenced-frame");
EXPECT_TRUE(
ExecJs(ad_component_frame,
JsReplace("window.open($1, '_unfencedTop');", navigation_url)));
// Expect automatic beacon being sent successfully with empty event data.
EXPECT_TRUE(network_responder_->GetRequest()->content.empty());
EXPECT_TRUE(network_responder_->HasReceivedRequest());
}
// Just like `AdComponentCrossOriginNavigation`, but with BackForwardCache
// disabled.
IN_PROC_BROWSER_TEST_P(InterestGroupAdComponentAutomaticBeaconBrowserTest,
AdComponentBFCacheDisabled) {
DisableBackForwardCacheForTesting(shell()->web_contents(),
BackForwardCache::TEST_REQUIRES_NO_CACHING);
GURL ad_component_url = embedded_https_test_server().GetURL(
"a.test", "/set-header?Supports-Loading-Mode: fenced-frame");
ASSERT_NO_FATAL_FAILURE(RunAdAuctionAndLoadAdComponent());
RenderFrameHost* ad_frame =
GetAdRenderFrameHost(shell()->web_contents()->GetPrimaryMainFrame(),
IsAdLoadedInFencedFrame());
RenderFrameHost* ad_component_frame =
GetAdRenderFrameHost(ad_frame, IsAdComponentLoadedInFencedFrame());
// Set automatic beacon data for ad component.
EXPECT_TRUE(ExecJs(ad_component_frame, (R"(
window.fence.setReportEventDataForAutomaticBeacons(
{
eventType: 'reserved.top_navigation_commit',
eventData: 'should be igonred',
destination: ['seller']
}
);
)")));
// Perform a cross-origin `_unfencedTop` navigation.
GURL navigation_url = embedded_https_test_server().GetURL(
"b.test", "/set-header?Supports-Loading-Mode: fenced-frame");
EXPECT_TRUE(
ExecJs(ad_component_frame,
JsReplace("window.open($1, '_unfencedTop');", navigation_url)));
// Expect automatic beacon being sent successfully with empty event data.
EXPECT_TRUE(network_responder_->GetRequest()->content.empty());
EXPECT_TRUE(network_responder_->HasReceivedRequest());
}
// No beacon is sent if `setReportEventDataForAutomaticBeacons` is not invoked
// to register automatic beacon data in ad component.
IN_PROC_BROWSER_TEST_P(InterestGroupAdComponentAutomaticBeaconBrowserTest,
AdComponentNoBeaconDataRegistered) {
GURL ad_component_url = embedded_https_test_server().GetURL(
"a.test", "/set-header?Supports-Loading-Mode: fenced-frame");
ASSERT_NO_FATAL_FAILURE(RunAdAuctionAndLoadAdComponent());
RenderFrameHost* ad_frame =
GetAdRenderFrameHost(shell()->web_contents()->GetPrimaryMainFrame(),
IsAdLoadedInFencedFrame());
RenderFrameHost* ad_component_frame =
GetAdRenderFrameHost(ad_frame, IsAdComponentLoadedInFencedFrame());
// Perform a cross-origin `_unfencedTop` navigation, without registering any
// automatic beacon data.
GURL navigation_url = embedded_https_test_server().GetURL(
"b.test", "/set-header?Supports-Loading-Mode: fenced-frame");
EXPECT_TRUE(
ExecJs(ad_component_frame,
JsReplace("window.open($1, '_unfencedTop');", navigation_url)));
// Send a basic request to the reporting destination.
GURL reporting_url =
embedded_https_test_server().GetURL("a.test", "/report_event.html");
SendBasicRequest(reporting_url);
// Verify the request received is the basic request, which implies the
// automatic beacon was not sent, as expected.
EXPECT_EQ(network_responder_->GetRequest()->content, "Basic request data");
EXPECT_TRUE(network_responder_->HasReceivedRequest());
}
// Set the event data to a string. The beacon should be sent, but without the
// data.
IN_PROC_BROWSER_TEST_P(InterestGroupAdComponentAutomaticBeaconBrowserTest,
AdComponentEventData) {
GURL ad_component_url = embedded_https_test_server().GetURL(
"a.test", "/set-header?Supports-Loading-Mode: fenced-frame");
ASSERT_NO_FATAL_FAILURE(RunAdAuctionAndLoadAdComponent());
RenderFrameHost* ad_frame =
GetAdRenderFrameHost(shell()->web_contents()->GetPrimaryMainFrame(),
IsAdLoadedInFencedFrame());
RenderFrameHost* ad_component_frame =
GetAdRenderFrameHost(ad_frame, IsAdComponentLoadedInFencedFrame());
// Set automatic beacon data for ad component, with the eventData field being
// an empty string.
EXPECT_TRUE(ExecJs(ad_component_frame, (R"(
window.fence.setReportEventDataForAutomaticBeacons(
{
eventType: 'reserved.top_navigation_commit',
eventData: 'this is the data',
destination: ['seller']
}
);
)")));
FencedDocumentData* fenced_document_data =
FencedDocumentData::GetForCurrentDocument(ad_component_frame);
std::string saved_beacon_data =
fenced_document_data
->GetAutomaticBeaconInfo(
blink::mojom::AutomaticBeaconType::kTopNavigationCommit)
->data;
EXPECT_EQ(saved_beacon_data, "");
// Perform a cross-origin `_unfencedTop` navigation.
GURL navigation_url = embedded_https_test_server().GetURL(
"b.test", "/set-header?Supports-Loading-Mode: fenced-frame");
EXPECT_TRUE(
ExecJs(ad_component_frame,
JsReplace("window.open($1, '_unfencedTop');", navigation_url)));
// Expect automatic beacon being sent successfully with empty event data.
EXPECT_TRUE(network_responder_->GetRequest()->content.empty());
EXPECT_TRUE(network_responder_->HasReceivedRequest());
}
// No beacon is sent if the navigation is without user activation.
IN_PROC_BROWSER_TEST_P(InterestGroupAdComponentAutomaticBeaconBrowserTest,
AdComponentNoUserActivation) {
GURL ad_component_url = embedded_https_test_server().GetURL(
"a.test", "/set-header?Supports-Loading-Mode: fenced-frame");
ASSERT_NO_FATAL_FAILURE(RunAdAuctionAndLoadAdComponent());
RenderFrameHost* ad_frame =
GetAdRenderFrameHost(shell()->web_contents()->GetPrimaryMainFrame(),
IsAdLoadedInFencedFrame());
RenderFrameHost* ad_component_frame =
GetAdRenderFrameHost(ad_frame, IsAdComponentLoadedInFencedFrame());
// Set automatic beacon data for ad component, this step must be done without
// user gesture. Otherwise later the '_unfencedTop' navigation will find there
// exists an user gesture.
EXPECT_TRUE(ExecJs(ad_component_frame, (R"(
window.fence.setReportEventDataForAutomaticBeacons(
{
eventType: 'reserved.top_navigation_commit',
eventData: 'should be igonred',
destination: ['seller']
}
);
)"),
EXECUTE_SCRIPT_NO_USER_GESTURE));
// Perform a cross-origin `_unfencedTop` navigation without user activation.
GURL navigation_url = embedded_https_test_server().GetURL(
"b.test", "/set-header?Supports-Loading-Mode: fenced-frame");
EXPECT_TRUE(
ExecJs(ad_component_frame,
JsReplace("window.open($1, '_unfencedTop');", navigation_url),
EXECUTE_SCRIPT_NO_USER_GESTURE));
// Send a basic request to the reporting destination.
GURL reporting_url =
embedded_https_test_server().GetURL("a.test", "/report_event.html");
SendBasicRequest(reporting_url);
// Verify the request received is the basic request, which implies the
// automatic beacon was not sent, as expected.
EXPECT_EQ(network_responder_->GetRequest()->content, "Basic request data");
EXPECT_TRUE(network_responder_->HasReceivedRequest());
}
// Call `setReportEventDataForAutomaticBeacons` without `eventData` field. The
// beacon should be sent with empty event data.
IN_PROC_BROWSER_TEST_P(InterestGroupAdComponentAutomaticBeaconBrowserTest,
AdComponentNoEventData) {
GURL ad_component_url = embedded_https_test_server().GetURL(
"a.test", "/set-header?Supports-Loading-Mode: fenced-frame");
ASSERT_NO_FATAL_FAILURE(RunAdAuctionAndLoadAdComponent());
RenderFrameHost* ad_frame =
GetAdRenderFrameHost(shell()->web_contents()->GetPrimaryMainFrame(),
IsAdLoadedInFencedFrame());
RenderFrameHost* ad_component_frame =
GetAdRenderFrameHost(ad_frame, IsAdComponentLoadedInFencedFrame());
// Set automatic beacon data for ad component without the eventData field.
EXPECT_TRUE(ExecJs(ad_component_frame, (R"(
window.fence.setReportEventDataForAutomaticBeacons(
{
eventType: 'reserved.top_navigation_commit',
destination: ['seller']
}
);
)")));
// Perform a cross-origin `_unfencedTop` navigation.
GURL navigation_url = embedded_https_test_server().GetURL(
"b.test", "/set-header?Supports-Loading-Mode: fenced-frame");
EXPECT_TRUE(
ExecJs(ad_component_frame,
JsReplace("window.open($1, '_unfencedTop');", navigation_url)));
// Expect automatic beacon being sent successfully with empty event data.
EXPECT_TRUE(network_responder_->GetRequest()->content.empty());
EXPECT_TRUE(network_responder_->HasReceivedRequest());
}
INSTANTIATE_TEST_SUITE_P(
All,
InterestGroupAdComponentAutomaticBeaconBrowserTest,
testing::Combine(testing::Bool(), testing::Bool()),
[](const testing::TestParamInfo<std::tuple<bool, bool>>& info) {
return base::StringPrintf(
"%s_%s", std::get<0>(info.param) ? "Ad_fenced_frame" : "Ad_iframe",
std::get<1>(info.param) ? "ad_component_fenced_frame"
: "ad_component_iframe");
});
class InterestGroupBiddingAndAuctionServerDisabledBrowserTest
: public InterestGroupBrowserTest {
public:
InterestGroupBiddingAndAuctionServerDisabledBrowserTest() {
feature_list_.InitAndDisableFeature(
blink::features::kFledgeBiddingAndAuctionServer);
}
protected:
base::test::ScopedFeatureList feature_list_;
};
IN_PROC_BROWSER_TEST_F(
InterestGroupBiddingAndAuctionServerDisabledBrowserTest,
FetchSameOrigin_AdAuctionHeadersEligible_HasBothAdAuctionResultAndSignalsResponseHeader) {
GURL main_frame_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/empty.html");
ASSERT_TRUE(NavigateToURL(shell(), main_frame_url));
base::StringPairs replacement;
replacement.emplace_back(std::make_pair("{{STATUS}}", "200 OK"));
replacement.emplace_back(std::make_pair(
"{{AD_AUCTION_HEADERS}}",
base::StrCat({"Ad-Auction-Result: ", kLegitimateAdAuctionResponse,
"\nAd-Auction-Signals: ", R"([{"adSlot":"slot1"}])"})));
replacement.emplace_back(std::make_pair("{{REDIRECT_HEADER}}", ""));
GURL fetch_url = embedded_https_test_server().GetURL(
"a.test", net::test_server::GetFilePathWithReplacements(
"/interest_group/"
"page_with_custom_ad_auction_result_header.html",
replacement));
// Verify that the JavaScript doesn't see the response header
// `Ad-Auction-Signals`. In contrast, it should see `Ad-Auction-Result`.
EXPECT_TRUE(ExecJs(web_contents()->GetPrimaryMainFrame(),
content::JsReplace(R"(
fetch($1, {adAuctionHeaders: true}).then((response) => {
if (!response.headers.get('Ad-Auction-Result')) {
throw 'Did not receive `Ad-Auction-Result` header';
}
if (response.headers.get('Ad-Auction-Signals')) {
throw 'Unexpectedly received `Ad-Auction-Signals` header';
}
});
)",
fetch_url)));
std::optional<std::string> ad_auction_header_value =
GetAdAuctionHeaderForRequestPath(
"/interest_group/page_with_custom_ad_auction_result_header.html");
EXPECT_TRUE(ad_auction_header_value);
EXPECT_EQ(*ad_auction_header_value, "?1");
// No witness is stored as `kFledgeBiddingAndAuctionServer` is disabled.
EXPECT_FALSE(WitnessedAuctionResultForOrigin(
url::Origin::Create(fetch_url),
base64Decode(kLegitimateAdAuctionResponse)));
const scoped_refptr<HeaderDirectFromSellerSignals::Result> signals =
ParseAndFindAdAuctionSignals(url::Origin::Create(fetch_url), "slot1");
EXPECT_NE(signals, nullptr);
}
class BiddingAndAuctionServerAPIsOriginTrialBrowserTest
: public ContentBrowserTest {
public:
BiddingAndAuctionServerAPIsOriginTrialBrowserTest() {
feature_list_.InitWithFeatures(
/*enabled_features=*/{blink::features::kFledge,
network::features::kInterestGroupStorage,
blink::features::kFledgeBiddingAndAuctionServer},
{});
}
void SetUpOnMainThread() override {
ContentBrowserTest::SetUpOnMainThread();
constexpr char kBaseDataDir[] = "content/test/data/interest_group/";
// We use a URLLoaderInterceptor, rather than the EmbeddedTestServer, since
// the origin trial token in the response is associated with a fixed
// origin, whereas EmbeddedTestServer serves content on a random port.
url_loader_interceptor_ =
std::make_unique<URLLoaderInterceptor>(base::BindLambdaForTesting(
[&](URLLoaderInterceptor::RequestParams* params) -> bool {
URLLoaderInterceptor::WriteResponse(
base::StrCat(
{kBaseDataDir, params->url_request.url.path_piece()}),
params->client.get());
return true;
}));
}
void TearDownOnMainThread() override { url_loader_interceptor_.reset(); }
WebContents* web_contents() { return shell()->web_contents(); }
FrameTreeNode* root() {
return static_cast<WebContentsImpl*>(web_contents())
->GetPrimaryFrameTree()
.root();
}
private:
std::unique_ptr<URLLoaderInterceptor> url_loader_interceptor_;
base::test::ScopedFeatureList feature_list_;
};
IN_PROC_BROWSER_TEST_F(BiddingAndAuctionServerAPIsOriginTrialBrowserTest,
RightOTFeatureEnabled) {
EXPECT_TRUE(NavigateToURL(
shell(), GURL("https://example.test/page_with_ba_server_ot.html")));
EXPECT_EQ(true, EvalJs(shell()->web_contents(),
"'getInterestGroupAdAuctionData' in navigator"));
}
class InterestGroupBiddingAndAuctionServerBrowserTest
: public InterestGroupBrowserTest {
public:
InterestGroupBiddingAndAuctionServerBrowserTest() {
feature_list_.InitWithFeaturesAndParameters(
{{blink::features::kFledgeBiddingAndAuctionServer,
{{"FledgeBiddingAndAuctionKeyURL", kKeyUrl.spec()}}},
{blink::features::kFledgeBiddingAndAuctionServerAPIMultiSeller, {}}},
{});
}
void TearDownOnMainThread() override {
InterestGroupBrowserTest::TearDownOnMainThread();
url_loader_interceptor_.reset();
}
// Attempts to get the auction blob for seller. Returns kSuccess if the
// operation claims to have succeeded, and the exception message on failure.
//
// If `execution_target` is non-null, uses it as the target. Otherwise, uses
// shell().
[[nodiscard]] std::string GetInterestGroupAdAuctionData(
std::optional<url::Origin> seller,
std::optional<std::string> coordinator,
std::optional<std::vector<std::pair<url::Origin, std::string>>> sellers =
std::nullopt,
blink::mojom::AuctionDataConfig* config = nullptr,
std::optional<ToRenderFrameHost> execution_target = std::nullopt) {
std::string fill_sellers;
if (sellers.has_value()) {
fill_sellers += "config.sellers = [";
for (const auto& [one_seller, one_coordinator] : *sellers) {
fill_sellers += JsReplace("{seller: $1, coordinatorOrigin: $2},",
one_seller, one_coordinator);
}
fill_sellers += "];";
}
std::string fill_config;
if (config) {
if (config->request_size) {
fill_config +=
JsReplace("config.requestSize = $1;",
static_cast<int>(config->request_size.value()));
}
if (config->per_buyer_configs.size() > 0) {
fill_config += "config.perBuyerConfig = {";
for (const auto& [buyer, per_buyer_config] :
config->per_buyer_configs) {
fill_config += JsReplace("$1: {", buyer);
if (per_buyer_config->target_size) {
fill_config += JsReplace(
"targetSize: $1",
static_cast<int>(per_buyer_config->target_size.value()));
}
fill_config += "},";
}
fill_config += "};";
}
}
return EvalJs(
execution_target ? *execution_target : shell(),
JsReplace(R"(
let config = {};
if ($1) {
config.seller = $2;
}
if ($3) {
config.coordinatorOrigin = $3;
}
if ($4) {
eval($4);
}
if ($5) {
eval($5);
}
(async function() {
try {
let data = await navigator.getInterestGroupAdAuctionData(config);
return btoa(String.fromCharCode.apply(null, data.request)) + '|' +
data.requestId;
} catch (e) {
return e.toString();
}
})())",
seller.has_value(), seller.value_or(url::Origin()),
coordinator.value_or(""), fill_sellers, fill_config))
.ExtractString();
}
void ProvideKeys() {
// We use a URLLoaderInterceptor instead of the EmbeddedTestServer, since we
// need to set the URL for it in the constructor, before the
// EmbeddedTestServer starts. At that point we don't know the port the
// EmbeddedTestServer will be using.
url_loader_interceptor_ =
std::make_unique<URLLoaderInterceptor>(base::BindLambdaForTesting(
[&](URLLoaderInterceptor::RequestParams* params) -> bool {
if (params->url_request.url != kKeyUrl) {
return false;
}
std::string headers =
"HTTP/1.1 200 OK\nContent-Type: application/json\n\n";
const uint8_t kTestPublicKey[] = {
0xa1, 0x5f, 0x40, 0x65, 0x86, 0xfa, 0xc4, 0x7b,
0x99, 0x59, 0x70, 0xf1, 0x85, 0xd9, 0xd8, 0x91,
0xc7, 0x4d, 0xcf, 0x1e, 0xb9, 0x1a, 0x7d, 0x50,
0xa5, 0x8b, 0x01, 0x68, 0x3e, 0x60, 0x05, 0x2d,
};
base::Value::Dict key;
key.Set("key", base::Base64Encode(kTestPublicKey));
key.Set("id", "12345678-9abc-def0-1234-56789abcdef0");
base::Value::List keys;
keys.Append(std::move(key));
base::Value::Dict outer;
outer.Set("keys", std::move(keys));
std::string json_output =
base::WriteJson(outer).value_or(std::string());
URLLoaderInterceptor::WriteResponse(headers, json_output,
params->client.get());
return true;
}));
}
protected:
const GURL kKeyUrl =
GURL("https://example.test/interest_group/b_and_a_keys.json");
std::unique_ptr<URLLoaderInterceptor> url_loader_interceptor_;
base::test::ScopedFeatureList feature_list_;
};
IN_PROC_BROWSER_TEST_F(InterestGroupBiddingAndAuctionServerBrowserTest,
TestEmpty) {
GURL test_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/empty.html");
url::Origin test_origin = url::Origin::Create(test_url);
ProvideKeys();
ASSERT_TRUE(NavigateToURL(shell(), test_url));
base::HistogramTester histogram_tester;
EXPECT_EQ("|", GetInterestGroupAdAuctionData(test_origin, std::nullopt));
content::FetchHistogramsFromChildProcesses();
histogram_tester.ExpectTotalCount(
"Ads.InterestGroup.GetInterestGroupAdAuctionData.TimeToResolve", 1);
std::vector<std::pair<url::Origin, std::string>> sellers = {
{test_origin, kDefaultBiddingAndAuctionGCPCoordinatorOrigin}};
EXPECT_EQ("|", GetInterestGroupAdAuctionData(
/*seller=*/std::nullopt, /*coordinator=*/std::nullopt,
std::move(sellers)));
content::FetchHistogramsFromChildProcesses();
histogram_tester.ExpectTotalCount(
"Ads.InterestGroup.GetInterestGroupAdAuctionData.TimeToResolve", 2);
}
IN_PROC_BROWSER_TEST_F(InterestGroupBiddingAndAuctionServerBrowserTest,
TestInvalidSeller) {
GURL test_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/empty.html");
url::Origin test_origin = url::Origin::Create(test_url);
ASSERT_TRUE(NavigateToURL(shell(), test_url));
base::HistogramTester histogram_tester;
EXPECT_EQ(
"TypeError: Failed to execute 'getInterestGroupAdAuctionData' on "
"'Navigator': seller 'null' for AdAuctionDataConfig must be a valid "
"https origin.",
GetInterestGroupAdAuctionData(
url::Origin(), kDefaultBiddingAndAuctionGCPCoordinatorOrigin));
content::FetchHistogramsFromChildProcesses();
histogram_tester.ExpectTotalCount(
"Ads.InterestGroup.GetInterestGroupAdAuctionData.TimeToResolve", 0);
}
IN_PROC_BROWSER_TEST_F(InterestGroupBiddingAndAuctionServerBrowserTest,
TestInvalidSellers) {
GURL test_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/empty.html");
url::Origin test_origin = url::Origin::Create(test_url);
ASSERT_TRUE(NavigateToURL(shell(), test_url));
base::HistogramTester histogram_tester;
std::vector<std::pair<url::Origin, std::string>> sellers = {
{url::Origin(), kDefaultBiddingAndAuctionGCPCoordinatorOrigin}};
EXPECT_EQ(
"TypeError: Failed to execute 'getInterestGroupAdAuctionData' on "
"'Navigator': seller 'null' in sellers for AdAuctionDataConfig must be a "
"valid https origin.",
GetInterestGroupAdAuctionData(
/*seller=*/std::nullopt, /*coordinator=*/std::nullopt,
std::move(sellers)));
content::FetchHistogramsFromChildProcesses();
histogram_tester.ExpectTotalCount(
"Ads.InterestGroup.GetInterestGroupAdAuctionData.TimeToResolve", 0);
}
IN_PROC_BROWSER_TEST_F(InterestGroupBiddingAndAuctionServerBrowserTest,
DuplicateSellers) {
GURL test_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/empty.html");
url::Origin test_origin = url::Origin::Create(test_url);
ASSERT_TRUE(NavigateToURL(shell(), test_url));
base::HistogramTester histogram_tester;
std::vector<std::pair<url::Origin, std::string>> sellers = {
{test_origin, kDefaultBiddingAndAuctionGCPCoordinatorOrigin},
{test_origin, kDefaultBiddingAndAuctionGCPCoordinatorOrigin}};
EXPECT_EQ(
"TypeError: Failed to execute 'getInterestGroupAdAuctionData' on "
"'Navigator': Each seller in sellers for AdAuctionDataConfig must be "
"unique.",
GetInterestGroupAdAuctionData(
/*seller=*/std::nullopt, /*coordinator=*/std::nullopt,
std::move(sellers)));
content::FetchHistogramsFromChildProcesses();
histogram_tester.ExpectTotalCount(
"Ads.InterestGroup.GetInterestGroupAdAuctionData.TimeToResolve", 0);
}
IN_PROC_BROWSER_TEST_F(InterestGroupBiddingAndAuctionServerBrowserTest,
BothSellerAndSellersInConfig) {
GURL test_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/empty.html");
url::Origin test_origin = url::Origin::Create(test_url);
ASSERT_TRUE(NavigateToURL(shell(), test_url));
base::HistogramTester histogram_tester;
std::vector<std::pair<url::Origin, std::string>> sellers = {
{test_origin, kDefaultBiddingAndAuctionGCPCoordinatorOrigin}};
EXPECT_EQ(
"TypeError: Failed to execute 'getInterestGroupAdAuctionData' on "
"'Navigator': Cannot provide both seller and sellers fields for "
"AdAuctionDataConfig.",
GetInterestGroupAdAuctionData(
/*seller=*/test_origin,
/*coordinator=*/kDefaultBiddingAndAuctionGCPCoordinatorOrigin,
std::move(sellers)));
content::FetchHistogramsFromChildProcesses();
histogram_tester.ExpectTotalCount(
"Ads.InterestGroup.GetInterestGroupAdAuctionData.TimeToResolve", 0);
}
IN_PROC_BROWSER_TEST_F(InterestGroupBiddingAndAuctionServerBrowserTest,
NeitherSellerNorSellersInConfig) {
GURL test_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/empty.html");
url::Origin test_origin = url::Origin::Create(test_url);
ASSERT_TRUE(NavigateToURL(shell(), test_url));
base::HistogramTester histogram_tester;
EXPECT_EQ(
"TypeError: Failed to execute 'getInterestGroupAdAuctionData' on "
"'Navigator': One of seller or sellers for AdAuctionDataConfig must be "
"provided.",
GetInterestGroupAdAuctionData(
/*seller=*/std::nullopt,
/*coordinator=*/std::nullopt,
/*sellers=*/std::nullopt));
content::FetchHistogramsFromChildProcesses();
histogram_tester.ExpectTotalCount(
"Ads.InterestGroup.GetInterestGroupAdAuctionData.TimeToResolve", 0);
}
IN_PROC_BROWSER_TEST_F(InterestGroupBiddingAndAuctionServerBrowserTest,
TestInvalidBuyer) {
GURL test_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/empty.html");
url::Origin test_origin = url::Origin::Create(test_url);
const unsigned int kRequestSize = 1024;
ASSERT_TRUE(NavigateToURL(shell(), test_url));
base::HistogramTester histogram_tester;
blink::mojom::AuctionDataConfig config;
config.request_size = kRequestSize;
config.per_buyer_configs.emplace(
url::Origin::Create(GURL("http://not.secure.test/")),
blink::mojom::AuctionDataBuyerConfig::New());
EXPECT_EQ(
"TypeError: Failed to execute 'getInterestGroupAdAuctionData' on "
"'Navigator': buyer origin 'http://not.secure.test' for "
"AdAuctionDataConfig must be a valid https origin.",
GetInterestGroupAdAuctionData(
test_origin, kDefaultBiddingAndAuctionGCPCoordinatorOrigin,
/*sellers=*/std::nullopt, &config));
content::FetchHistogramsFromChildProcesses();
histogram_tester.ExpectTotalCount(
"Ads.InterestGroup.GetInterestGroupAdAuctionData.TimeToResolve", 0);
std::vector<std::pair<url::Origin, std::string>> sellers = {
{test_origin, kDefaultBiddingAndAuctionGCPCoordinatorOrigin}};
EXPECT_EQ(
"TypeError: Failed to execute 'getInterestGroupAdAuctionData' on "
"'Navigator': buyer origin 'http://not.secure.test' for "
"AdAuctionDataConfig must be a valid https origin.",
GetInterestGroupAdAuctionData(
/*seller=*/std::nullopt, /*coordinator=*/std::nullopt,
std::move(sellers), &config));
content::FetchHistogramsFromChildProcesses();
histogram_tester.ExpectTotalCount(
"Ads.InterestGroup.GetInterestGroupAdAuctionData.TimeToResolve", 0);
}
IN_PROC_BROWSER_TEST_F(InterestGroupBiddingAndAuctionServerBrowserTest,
TestMissingBuyerSize) {
GURL test_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/empty.html");
url::Origin test_origin = url::Origin::Create(test_url);
ASSERT_TRUE(NavigateToURL(shell(), test_url));
base::HistogramTester histogram_tester;
blink::mojom::AuctionDataConfig config;
config.per_buyer_configs.emplace(test_origin,
blink::mojom::AuctionDataBuyerConfig::New());
EXPECT_EQ(
"TypeError: Failed to execute 'getInterestGroupAdAuctionData' on "
"'Navigator': All per-buyer configs must have a target size when request "
"size is not specified.",
GetInterestGroupAdAuctionData(
test_origin, kDefaultBiddingAndAuctionGCPCoordinatorOrigin,
/*sellers=*/std::nullopt, &config));
content::FetchHistogramsFromChildProcesses();
histogram_tester.ExpectTotalCount(
"Ads.InterestGroup.GetInterestGroupAdAuctionData.TimeToResolve", 0);
std::vector<std::pair<url::Origin, std::string>> sellers = {
{test_origin, kDefaultBiddingAndAuctionGCPCoordinatorOrigin}};
EXPECT_EQ(
"TypeError: Failed to execute 'getInterestGroupAdAuctionData' on "
"'Navigator': All per-buyer configs must have a target size when request "
"size is not specified.",
GetInterestGroupAdAuctionData(
/*seller=*/std::nullopt, /*coordinator=*/std::nullopt,
std::move(sellers), &config));
content::FetchHistogramsFromChildProcesses();
histogram_tester.ExpectTotalCount(
"Ads.InterestGroup.GetInterestGroupAdAuctionData.TimeToResolve", 0);
}
IN_PROC_BROWSER_TEST_F(InterestGroupBiddingAndAuctionServerBrowserTest,
TestInvalidComputedSize) {
GURL test_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/empty.html");
url::Origin test_origin = url::Origin::Create(test_url);
url::Origin test_origin2 = url::Origin::Create(GURL("https://b.test/"));
ASSERT_TRUE(NavigateToURL(shell(), test_url));
base::HistogramTester histogram_tester;
blink::mojom::AuctionDataConfig config;
config.per_buyer_configs.emplace(test_origin,
blink::mojom::AuctionDataBuyerConfig::New(
std::numeric_limits<uint32_t>::max()));
config.per_buyer_configs.emplace(test_origin2,
blink::mojom::AuctionDataBuyerConfig::New(
std::numeric_limits<uint32_t>::max()));
EXPECT_EQ(
"TypeError: Failed to execute 'getInterestGroupAdAuctionData' on "
"'Navigator': Computed request size is invalid.",
GetInterestGroupAdAuctionData(
test_origin, kDefaultBiddingAndAuctionGCPCoordinatorOrigin,
/*sellers=*/std::nullopt, &config));
content::FetchHistogramsFromChildProcesses();
histogram_tester.ExpectTotalCount(
"Ads.InterestGroup.GetInterestGroupAdAuctionData.TimeToResolve", 0);
std::vector<std::pair<url::Origin, std::string>> sellers = {
{test_origin, kDefaultBiddingAndAuctionGCPCoordinatorOrigin}};
EXPECT_EQ(
"TypeError: Failed to execute 'getInterestGroupAdAuctionData' on "
"'Navigator': Computed request size is invalid.",
GetInterestGroupAdAuctionData(
/*seller=*/std::nullopt, /*coordinator=*/std::nullopt,
std::move(sellers), &config));
content::FetchHistogramsFromChildProcesses();
histogram_tester.ExpectTotalCount(
"Ads.InterestGroup.GetInterestGroupAdAuctionData.TimeToResolve", 0);
}
IN_PROC_BROWSER_TEST_F(InterestGroupBiddingAndAuctionServerBrowserTest,
Preconnects) {
GURL test_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/empty.html");
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_cars");
ProvideKeys();
ASSERT_TRUE(NavigateToURL(shell(), test_url));
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(/*owner=*/test_origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"))
.SetAds(
{{{ad_url, /*metadata=*/std::nullopt,
/*size_group=*/std::nullopt,
/*buyer_reporting_id=*/std::nullopt,
/*buyer_and_seller_reporting_id=*/std::nullopt,
/*selectable_buyer_and_seller_reporting_ids=*/std::nullopt,
/*ad_render_id=*/"buyCars"}}})
.Build()));
class PreconnectCheckingNetworkContext : public network::TestNetworkContext {
public:
explicit PreconnectCheckingNetworkContext(GURL expected_url)
: expected_url_(std::move(expected_url)) {}
~PreconnectCheckingNetworkContext() override = default;
void PreconnectSockets(
uint32_t num_streams,
const GURL& url,
network::mojom::CredentialsMode credentials_mode,
const net::NetworkAnonymizationKey& network_anonymization_key,
const net::MutableNetworkTrafficAnnotationTag& traffic_annotation,
const std::optional<net::ConnectionKeepAliveConfig>& keepalive_config,
mojo::PendingRemote<network::mojom::ConnectionChangeObserverClient>
observer_client) override {
EXPECT_EQ(1u, num_streams);
EXPECT_EQ(expected_url_, url);
EXPECT_EQ(credentials_mode, network::mojom::CredentialsMode::kInclude);
run_loop_.Quit();
}
base::RunLoop& run_loop() { return run_loop_; }
private:
base::RunLoop run_loop_;
const GURL expected_url_;
};
mojo::PendingRemote<network::mojom::NetworkContext> pending_remote;
auto preconnect_check = mojo::MakeSelfOwnedReceiver(
std::make_unique<PreconnectCheckingNetworkContext>(test_origin.GetURL()),
pending_remote.InitWithNewPipeAndPassReceiver());
shell()
->web_contents()
->GetBrowserContext()
->GetDefaultStoragePartition()
->SetNetworkContextForTesting(std::move(pending_remote));
std::ignore = GetInterestGroupAdAuctionData(test_origin, std::nullopt);
static_cast<PreconnectCheckingNetworkContext*>(preconnect_check->impl())
->run_loop()
.Run();
}
IN_PROC_BROWSER_TEST_F(InterestGroupBiddingAndAuctionServerBrowserTest,
UsesRequestSize) {
GURL test_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/empty.html");
url::Origin test_origin = url::Origin::Create(test_url);
url::Origin test_origin2 = embedded_https_test_server().GetOrigin("b.test");
const unsigned int kRequestSize = 1024;
GURL ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_cars");
ProvideKeys();
ASSERT_TRUE(NavigateToURL(shell(), test_url));
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(/*owner=*/test_origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"))
.SetAds(
{{{ad_url, /*metadata=*/std::nullopt,
/*size_group=*/std::nullopt,
/*buyer_reporting_id=*/std::nullopt,
/*buyer_and_seller_reporting_id=*/std::nullopt,
/*selectable_buyer_and_seller_reporting_ids=*/std::nullopt,
/*ad_render_id=*/"buyCars"}}})
.Build()));
blink::mojom::AuctionDataConfig config;
config.request_size = kRequestSize;
config.per_buyer_configs.emplace(test_origin,
blink::mojom::AuctionDataBuyerConfig::New());
config.per_buyer_configs.emplace(
test_origin2, blink::mojom::AuctionDataBuyerConfig::New(kRequestSize));
std::string result = GetInterestGroupAdAuctionData(
test_origin, std::nullopt, /*sellers=*/std::nullopt, &config);
const unsigned int kCharsInUUID = 36;
const unsigned int kCharsInSeparator = 1;
const unsigned int kRequestBase64SizeChars =
std::ceil(kRequestSize / 3.0) * 4;
EXPECT_EQ(kRequestBase64SizeChars + kCharsInSeparator + kCharsInUUID,
result.size());
}
IN_PROC_BROWSER_TEST_F(InterestGroupBiddingAndAuctionServerBrowserTest,
UsesImplicitRequestSize) {
GURL test_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/empty.html");
url::Origin test_origin = url::Origin::Create(test_url);
url::Origin test_origin2 = embedded_https_test_server().GetOrigin("b.test");
const unsigned int kRequestSize = 1024;
GURL ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_cars");
ProvideKeys();
ASSERT_TRUE(NavigateToURL(shell(), test_url));
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(/*owner=*/test_origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"))
.SetAds(
{{{ad_url, /*metadata=*/std::nullopt,
/*size_group=*/std::nullopt,
/*buyer_reporting_id=*/std::nullopt,
/*buyer_and_seller_reporting_id=*/std::nullopt,
/*selectable_buyer_and_seller_reporting_ids=*/std::nullopt,
/*ad_render_id=*/"buyCars"}}})
.Build()));
blink::mojom::AuctionDataConfig config;
config.per_buyer_configs.emplace(
test_origin,
blink::mojom::AuctionDataBuyerConfig::New(/*target_size=*/kRequestSize));
std::string result = GetInterestGroupAdAuctionData(
test_origin, std::nullopt, /*sellers=*/std::nullopt, &config);
const unsigned int kCharsInUUID = 36;
const unsigned int kCharsInSeparator = 1;
const unsigned int kRequestBase64SizeChars =
std::ceil(kRequestSize / 3.0) * 4;
EXPECT_EQ(kRequestBase64SizeChars + kCharsInSeparator + kCharsInUUID,
result.size());
}
IN_PROC_BROWSER_TEST_F(InterestGroupBiddingAndAuctionServerBrowserTest,
TestInvalidCoordinator) {
GURL test_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/empty.html");
url::Origin test_origin = url::Origin::Create(test_url);
ASSERT_TRUE(NavigateToURL(shell(), test_url));
base::HistogramTester histogram_tester;
EXPECT_EQ(
"TypeError: Failed to execute 'getInterestGroupAdAuctionData' on "
"'Navigator': coordinatorOrigin 'foo' for AdAuctionDataConfig must be "
"a valid https origin.",
GetInterestGroupAdAuctionData(test_origin, "foo"));
content::FetchHistogramsFromChildProcesses();
histogram_tester.ExpectTotalCount(
"Ads.InterestGroup.GetInterestGroupAdAuctionData.TimeToResolve", 0);
}
IN_PROC_BROWSER_TEST_F(InterestGroupBiddingAndAuctionServerBrowserTest,
TestInvalidCoordinatorInSellers) {
GURL test_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/empty.html");
url::Origin test_origin = url::Origin::Create(test_url);
ASSERT_TRUE(NavigateToURL(shell(), test_url));
base::HistogramTester histogram_tester;
std::vector<std::pair<url::Origin, std::string>> sellers = {
{test_origin, "foo"}};
EXPECT_EQ(
"TypeError: Failed to execute 'getInterestGroupAdAuctionData' on "
"'Navigator': coordinatorOrigin 'foo' in sellers for AdAuctionDataConfig "
"must be a valid https origin.",
GetInterestGroupAdAuctionData(/*seller=*/std::nullopt,
/*coordinator=*/std::nullopt,
std::move(sellers)));
content::FetchHistogramsFromChildProcesses();
histogram_tester.ExpectTotalCount(
"Ads.InterestGroup.GetInterestGroupAdAuctionData.TimeToResolve", 0);
}
IN_PROC_BROWSER_TEST_F(InterestGroupBiddingAndAuctionServerBrowserTest,
ChecksPermissionPolicyWarning) {
// TODO(behamilton): Merge with
// InterestGroupBrowserTest.FeaturesEnabledForAllByPermissionsPolicy once this
// feature has been released.
// clang-format off
GURL test_url = embedded_https_test_server().GetURL(
"a.test",
"/cross_site_iframe_factory.html?a.test("
"a.test,"
"b.test("
"c.test{allow-join-ad-interest-group;run-ad-auction},"
"a.test{allow-join-ad-interest-group;run-ad-auction}"
")"
")");
// clang-format on
url::Origin test_origin = url::Origin::Create(test_url);
ProvideKeys();
ASSERT_TRUE(NavigateToURL(shell(), test_url));
RenderFrameHost* main_frame = web_contents()->GetPrimaryMainFrame();
RenderFrameHost* same_origin_iframe = ChildFrameAt(main_frame, 0);
RenderFrameHost* cross_origin_iframe = ChildFrameAt(main_frame, 1);
RenderFrameHost* inner_cross_origin_iframe =
ChildFrameAt(cross_origin_iframe, 0);
RenderFrameHost* same_origin_iframe_in_cross_origin_iframe =
ChildFrameAt(cross_origin_iframe, 1);
RenderFrameHost* execution_targets[] = {
main_frame, same_origin_iframe, cross_origin_iframe,
inner_cross_origin_iframe, same_origin_iframe_in_cross_origin_iframe};
base::HistogramTester histogram_tester;
for (auto* execution_target : execution_targets) {
SCOPED_TRACE(execution_target->GetLastCommittedURL().spec());
WebContentsConsoleObserver console_observer(shell()->web_contents());
console_observer.SetPattern(WarningPermissionsPolicy("*", "*"));
EXPECT_EQ("|",
GetInterestGroupAdAuctionData(
test_origin, kDefaultBiddingAndAuctionGCPCoordinatorOrigin,
/*sellers=*/std::nullopt,
/*config=*/nullptr, execution_target));
RenderFrameHost* execution_targets_with_message[] = {
cross_origin_iframe, inner_cross_origin_iframe,
same_origin_iframe_in_cross_origin_iframe};
if (base::Contains(execution_targets_with_message, execution_target)) {
EXPECT_TRUE(console_observer.Wait());
EXPECT_EQ(WarningPermissionsPolicy("run-ad-auction",
"getInterestGroupAdAuctionData"),
console_observer.GetMessageAt(0));
} else {
EXPECT_TRUE(console_observer.messages().empty());
}
}
content::FetchHistogramsFromChildProcesses();
histogram_tester.ExpectTotalCount(
"Ads.InterestGroup.GetInterestGroupAdAuctionData.TimeToResolve", 5);
}
// Check that the renderer doesn't crash if we don't have a decision logic URL.
IN_PROC_BROWSER_TEST_F(InterestGroupBiddingAndAuctionServerBrowserTest,
DecisionLogicURLNotRequired) {
GURL test_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/empty.html");
std::string auction_config = R"({
seller: "https://seller.example.com",
interestGroupBuyers: ["https://buyer.example.com"],
serverResponse: new Uint8Array(20),
requestId: "00000000-0000-0000-0000-000000000000",
})";
ASSERT_TRUE(NavigateToURL(shell(), test_url));
base::HistogramTester histogram_tester;
EXPECT_EQ(base::Value(), RunAuctionAndWait(auction_config));
content::FetchHistogramsFromChildProcesses();
// Make sure the right histogram was logged (not the on-device histogram).
histogram_tester.ExpectTotalCount(
"Ads.InterestGroup.ServerAuction.TimeToResolve", 1);
histogram_tester.ExpectTotalCount("Ads.InterestGroup.Auction.TimeToResolve",
0);
}
IN_PROC_BROWSER_TEST_F(InterestGroupBiddingAndAuctionServerBrowserTest,
DecisionLogicURLRequiredForComponent) {
GURL test_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/empty.html");
url::Origin test_origin = url::Origin::Create(test_url);
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
// Signal to the top-level seller to allow participation in a component
// auction.
auctionSignals: "sellerAllowsComponentAuction",
componentAuctions: [{
seller: $1,
// No decisionLogicURL
interestGroupBuyers: [$1],
}]
})",
test_origin,
embedded_https_test_server().GetURL("a.test",
"/interest_group/decision_logic.js"));
ASSERT_TRUE(NavigateToURL(shell(), test_url));
base::HistogramTester histogram_tester;
EXPECT_EQ(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': Missing "
"required field ad auction config decisionLogicURL or serverResponse",
RunAuctionAndWait(auction_config));
content::FetchHistogramsFromChildProcesses();
histogram_tester.ExpectTotalCount(
"Ads.InterestGroup.ServerAuction.TimeToResolve", 0);
histogram_tester.ExpectTotalCount("Ads.InterestGroup.Auction.TimeToResolve",
0);
}
IN_PROC_BROWSER_TEST_F(InterestGroupBiddingAndAuctionServerBrowserTest,
DecisionLogicURLNotRequiredForServerComponent) {
GURL test_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/empty.html");
url::Origin test_origin = url::Origin::Create(test_url);
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
// Signal to the top-level seller to allow participation in a component
// auction.
auctionSignals: "sellerAllowsComponentAuction",
componentAuctions: [{
seller: $1,
interestGroupBuyers: [$1],
serverResponse: new Uint8Array(20),
requestId: "00000000-0000-0000-0000-000000000000",
}]
})",
test_origin,
embedded_https_test_server().GetURL("a.test",
"/interest_group/decision_logic.js"));
ASSERT_TRUE(NavigateToURL(shell(), test_url));
EXPECT_EQ(base::Value(), RunAuctionAndWait(auction_config));
}
class InterestGroupBiddingAndAuctionMultiSellerServerDisabledBrowserTest
: public InterestGroupBiddingAndAuctionServerBrowserTest {
public:
InterestGroupBiddingAndAuctionMultiSellerServerDisabledBrowserTest() {
feature_list_.InitWithFeaturesAndParameters(
{{blink::features::kFledgeBiddingAndAuctionServer,
{{"FledgeBiddingAndAuctionKeyURL", kKeyUrl.spec()}}}},
{blink::features::kFledgeBiddingAndAuctionServerAPIMultiSeller});
}
protected:
const GURL kKeyUrl =
GURL("https://example.test/interest_group/b_and_a_keys.json");
base::test::ScopedFeatureList feature_list_;
};
IN_PROC_BROWSER_TEST_F(
InterestGroupBiddingAndAuctionMultiSellerServerDisabledBrowserTest,
MissingRequiredSeller) {
GURL test_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/empty.html");
url::Origin test_origin = url::Origin::Create(test_url);
ASSERT_TRUE(NavigateToURL(shell(), test_url));
base::HistogramTester histogram_tester;
std::vector<std::pair<url::Origin, std::string>> sellers = {
{test_origin, kDefaultBiddingAndAuctionGCPCoordinatorOrigin}};
EXPECT_EQ(
"TypeError: Failed to execute 'getInterestGroupAdAuctionData' on "
"'Navigator': Failed to read the 'seller' property from "
"'AdAuctionDataConfig': Required member is undefined.",
GetInterestGroupAdAuctionData(
/*seller=*/std::nullopt,
/*coordinator=*/std::nullopt, std::move(sellers)));
content::FetchHistogramsFromChildProcesses();
histogram_tester.ExpectTotalCount(
"Ads.InterestGroup.GetInterestGroupAdAuctionData.TimeToResolve", 0);
}
// TODO(crbug.com/40927353): Re-enable this test
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, AuctionNonceIsValid) {
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_cars");
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"))
.SetAds({{{ad_url, /*metadata=*/std::nullopt}}})
.Build()));
std::string auction_nonce = CreateAuctionNonceAndWait();
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
auctionNonce: $3,
})",
test_origin,
embedded_https_test_server().GetURL("a.test",
"/interest_group/decision_logic.js"),
auction_nonce);
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad_url);
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
AuctionNonceOnComponentIsValid) {
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_cars");
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"))
.SetAds({{{ad_url, /*metadata=*/std::nullopt}}})
.Build()));
std::string auction_nonce = CreateAuctionNonceAndWait();
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
// Signal to the top-level seller to allow participation in a component
// auction.
auctionSignals: "sellerAllowsComponentAuction",
componentAuctions: [{
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
// Signal to the bidder and component seller to allow participation in
// a component auction.
auctionSignals: "bidderAllowsComponentAuction,"+
"sellerAllowsComponentAuction",
auctionNonce: $3,
}]
})",
test_origin,
embedded_https_test_server().GetURL("a.test",
"/interest_group/decision_logic.js"),
auction_nonce);
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad_url);
}
// TODO(crbug.com/40275797): When negative targeting is fully
// implemented, we can directly observe that the auction or component auction
// that specified the invalid nonce isn't eligible to participate in the
// auction. Until then, we use the error emitted by the AuctionNonceManager to
// infer that it caught the invalid nonce.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
AuctionNonceNotFromCreateAuctionNonce) {
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_cars");
std::string auction_nonce = CreateAuctionNonceAndWait();
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicUrl: $2,
interestGroupBuyers: [$1],
auctionNonce: $3,
})",
test_origin,
embedded_https_test_server().GetURL("a.test",
"/interest_group/decision_logic.js"),
ConvertUuidWithOnlyZeros(auction_nonce));
// Monitor the console errors.
WebContentsConsoleObserver console_observer(web_contents());
console_observer.SetFilter(base::BindRepeating(IsErrorMessage));
console_observer.SetPattern(
"Invalid AuctionConfig. The config provided an auctionNonce value "
"that was _not_ created by a previous call to createAuctionNonce.");
EXPECT_EQ(base::Value(), RunAuctionAndWait(auction_config));
// Verify the expected error is logged to the console.
ASSERT_TRUE(console_observer.Wait());
EXPECT_EQ(console_observer.messages().size(), 1u);
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
AuctionNonceOnComponentNotFromCreateAuctionNonce) {
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_cars");
std::string auction_nonce = CreateAuctionNonceAndWait();
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
// Signal to the top-level seller to allow participation in a component
// auction.
auctionSignals: "sellerAllowsComponentAuction",
componentAuctions: [{
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
// Signal to the bidder and component seller to allow participation in
// a component auction.
auctionSignals: "bidderAllowsComponentAuction,"+
"sellerAllowsComponentAuction",
auctionNonce: $3,
}]
})",
test_origin,
embedded_https_test_server().GetURL("a.test",
"/interest_group/decision_logic.js"),
ConvertUuidWithOnlyZeros(auction_nonce));
// Monitor the console errors.
WebContentsConsoleObserver console_observer(web_contents());
console_observer.SetFilter(base::BindRepeating(IsErrorMessage));
console_observer.SetPattern(
"Invalid AuctionConfig. The config provided an auctionNonce value "
"that was _not_ created by a previous call to createAuctionNonce.");
EXPECT_EQ(base::Value(), RunAuctionAndWait(auction_config));
// Verify the expected error is logged to the console.
ASSERT_TRUE(console_observer.Wait());
EXPECT_EQ(console_observer.messages().size(), 1u);
}
class InterestGroupSellerNonceDisabledTest : public InterestGroupBrowserTest {
protected:
InterestGroupSellerNonceDisabledTest() {
scoped_feature_list_.InitAndDisableFeature(
blink::features::kFledgeSellerNonce);
}
base::test::ScopedFeatureList scoped_feature_list_;
};
IN_PROC_BROWSER_TEST_F(InterestGroupSellerNonceDisabledTest, FeatureDetection) {
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/simple_page.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
ASSERT_EQ(true, EvalJs(shell(), "'protectedAudience' in navigator"));
const char kQuerySellerNonce[] = R"(
navigator.protectedAudience.queryFeatureSupport(
'sellerNonce');
)";
EXPECT_EQ(false, EvalJs(shell(), kQuerySellerNonce));
}
class InterestGroupMaxLifetimeMsDisabledBrowserTest
: public InterestGroupBrowserTest {
public:
InterestGroupMaxLifetimeMsDisabledBrowserTest() {
scoped_feature_list_.InitAndDisableFeature(
blink::features::kFledgeMaxGroupLifetimeFeature);
}
base::test::ScopedFeatureList scoped_feature_list_;
};
IN_PROC_BROWSER_TEST_F(InterestGroupMaxLifetimeMsDisabledBrowserTest,
FeatureDetection) {
constexpr double kDefaultMaxGroupLifetimeMs = base::Days(30).InMilliseconds();
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/simple_page.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
ASSERT_EQ(true, EvalJs(shell(), "'protectedAudience' in navigator"));
const char kQueryMaxGroupLifetimeMs[] = R"(
navigator.protectedAudience.queryFeatureSupport(
'maxGroupLifetimeMs');
)";
EXPECT_EQ(kDefaultMaxGroupLifetimeMs,
EvalJs(shell(), kQueryMaxGroupLifetimeMs));
}
class InterestGroupChangeMaxLifetimeMsBrowserTest
: public InterestGroupBrowserTest {
public:
static constexpr double kDefaultMaxGroupLifetimeMs =
base::Days(90).InMilliseconds();
InterestGroupChangeMaxLifetimeMsBrowserTest() {
scoped_feature_list_.InitAndEnableFeatureWithParameters(
blink::features::kFledgeMaxGroupLifetimeFeature,
{{"fledge_max_group_lifetime",
base::StringPrintf("%fms", kDefaultMaxGroupLifetimeMs)}});
}
base::test::ScopedFeatureList scoped_feature_list_;
};
IN_PROC_BROWSER_TEST_F(InterestGroupChangeMaxLifetimeMsBrowserTest,
Clickiness_CaptureView) {
constexpr char kRecordViewClickPath[] =
"/interest_group/record_view_click_event.html";
GURL test_url_a = embedded_https_test_server().GetURL(
"a.test", "/attribution_reporting/page_with_impression_creator.html");
url::Origin test_origin_a = url::Origin::Create(test_url_a);
ASSERT_TRUE(test_url_a.SchemeIs(url::kHttpsScheme));
ASSERT_TRUE(NavigateToURL(shell(), test_url_a));
const std::string record_event_response = base::StringPrintf(
"type=\"view\", eligible-origins=(\"%s\")", test_origin_a.Serialize());
network_responder_->RegisterNetworkResponse(
kRecordViewClickPath, "Throwaway response", "image/jpeg",
/*extra_response_headers=*/
{{"Ad-Auction-Record-Event", record_event_response}});
GURL record_event_url =
embedded_https_test_server().GetURL("c.test", kRecordViewClickPath);
EXPECT_TRUE(ExecJs(web_contents(), JsReplace("createAttributionSrcImg($1);",
record_event_url)));
// This join should succeed. Register a no-op update URL to use
// WaitForInterestGroupsSatisfyingInvalidatingCacheByUpdating().
RegisterNoOpUpdate();
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(test_origin_a, "cars")
.SetViewAndClickCountsProviders(
{{url::Origin::Create(record_event_url)}})
.SetUpdateUrl(embedded_https_test_server().GetURL(
"a.test", kNoOpUpdatePath))
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", kValidateViewClickBiddingLogicPath))
.SetAds({{{GURL(kAdURL), /*metadata=*/std::nullopt}}})
.Build()));
WaitForInterestGroupsSatisfyingInvalidatingCacheByUpdating(
test_origin_a,
base::BindLambdaForTesting(
[](scoped_refptr<StorageInterestGroups> groups) {
EXPECT_EQ(groups->size(), 1u);
const StorageInterestGroup& group = *groups->GetInterestGroups()[0];
const blink::mojom::ViewAndClickCountsPtr& view_and_click_counts =
group.bidding_browser_signals->view_and_click_counts;
EXPECT_EQ(group.interest_group.name, "cars");
return view_and_click_counts->view_counts->past_hour == 1 &&
view_and_click_counts->click_counts->past_hour == 0;
}));
ValidateViewClickCountsInGenerateBid(
/*expected_view_counts=*/{.past_hour = 1,
.past_day = 1,
.past_week = 1,
.past_30_days = 1,
.past_90_days = 1},
/*expected_click_counts=*/{.past_hour = 0,
.past_day = 0,
.past_week = 0,
.past_30_days = 0,
.past_90_days = 0});
}
IN_PROC_BROWSER_TEST_F(InterestGroupChangeMaxLifetimeMsBrowserTest,
Clickiness_CaptureClick) {
constexpr char kRecordViewClickPath[] =
"/interest_group/record_view_click_event.html";
GURL test_url_a = embedded_https_test_server().GetURL(
"a.test", "/attribution_reporting/page_with_impression_creator.html");
url::Origin test_origin_a = url::Origin::Create(test_url_a);
ASSERT_TRUE(test_url_a.SchemeIs(url::kHttpsScheme));
ASSERT_TRUE(NavigateToURL(shell(), test_url_a));
const std::string record_event_response = base::StringPrintf(
"type=\"click\", eligible-origins=(\"%s\")", test_origin_a.Serialize());
network_responder_->RegisterNetworkResponse(
kRecordViewClickPath, "Throwaway response", "image/jpeg",
/*extra_response_headers=*/
{{"Ad-Auction-Record-Event", record_event_response}});
GURL record_event_url =
embedded_https_test_server().GetURL("c.test", kRecordViewClickPath);
EXPECT_TRUE(
ExecJs(web_contents(),
JsReplace(R"(
createAttributionSrcAnchor({id: 'link',
url: $1,
attributionsrc: $2,
target: $3});)",
embedded_https_test_server().GetURL("a.test", "/echo"),
record_event_url, "_top")));
TestNavigationObserver observer(web_contents());
EXPECT_TRUE(ExecJs(web_contents(), "simulateClick('link');"));
observer.Wait();
// This join should succeed. Register a no-op update URL to use
// WaitForInterestGroupsSatisfyingInvalidatingCacheByUpdating().
RegisterNoOpUpdate();
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(test_origin_a, "cars")
.SetViewAndClickCountsProviders(
{{url::Origin::Create(record_event_url)}})
.SetUpdateUrl(embedded_https_test_server().GetURL(
"a.test", kNoOpUpdatePath))
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", kValidateViewClickBiddingLogicPath))
.SetAds({{{GURL(kAdURL), /*metadata=*/std::nullopt}}})
.Build()));
WaitForInterestGroupsSatisfyingInvalidatingCacheByUpdating(
test_origin_a,
base::BindLambdaForTesting(
[](scoped_refptr<StorageInterestGroups> groups) {
EXPECT_EQ(groups->size(), 1u);
const StorageInterestGroup& group = *groups->GetInterestGroups()[0];
const blink::mojom::ViewAndClickCountsPtr& view_and_click_counts =
group.bidding_browser_signals->view_and_click_counts;
EXPECT_EQ(group.interest_group.name, "cars");
return view_and_click_counts->view_counts->past_hour == 0 &&
view_and_click_counts->click_counts->past_hour == 1;
}));
ValidateViewClickCountsInGenerateBid(
/*expected_view_counts=*/{.past_hour = 0,
.past_day = 0,
.past_week = 0,
.past_30_days = 0,
.past_90_days = 0},
/*expected_click_counts=*/{.past_hour = 1,
.past_day = 1,
.past_week = 1,
.past_30_days = 1,
.past_90_days = 1});
}
IN_PROC_BROWSER_TEST_F(InterestGroupChangeMaxLifetimeMsBrowserTest,
FeatureDetection) {
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/simple_page.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
ASSERT_EQ(true, EvalJs(shell(), "'protectedAudience' in navigator"));
const char kQueryMaxGroupLifetimeMs[] = R"(
navigator.protectedAudience.queryFeatureSupport(
'maxGroupLifetimeMs');
)";
EXPECT_EQ(kDefaultMaxGroupLifetimeMs,
EvalJs(shell(), kQueryMaxGroupLifetimeMs));
}
class InterestGroupKAnonmityEnforcedBrowserTest
: public InterestGroupBrowserTest {
public:
InterestGroupKAnonmityEnforcedBrowserTest() {
feature_list_.InitWithFeatures({blink::features::kEnforceAnonymityExposure,
blink::features::kFledgeEnforceKAnonymity},
{});
}
protected:
base::test::ScopedFeatureList feature_list_;
};
IN_PROC_BROWSER_TEST_F(InterestGroupKAnonmityEnforcedBrowserTest,
DeprecatedKAnonEnforced) {
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
ASSERT_TRUE(NavigateToURL(shell(), url));
std::string script = R"(
(function() {
return navigator.deprecatedRunAdAuctionEnforcesKAnonymity;
})())";
EXPECT_EQ(true, EvalJs(shell(), script).ExtractBool());
}
class InterestGroupKAnonmityNotEnforcedBrowserTest
: public InterestGroupBrowserTest {
public:
InterestGroupKAnonmityNotEnforcedBrowserTest() {
feature_list_.InitWithFeatures({blink::features::kEnforceAnonymityExposure},
{blink::features::kFledgeEnforceKAnonymity});
}
protected:
base::test::ScopedFeatureList feature_list_;
};
IN_PROC_BROWSER_TEST_F(InterestGroupKAnonmityNotEnforcedBrowserTest,
DeprecatedKAnonNotEnforced) {
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
ASSERT_TRUE(NavigateToURL(shell(), url));
std::string script = R"(
(function() {
return navigator.deprecatedRunAdAuctionEnforcesKAnonymity;
})())";
EXPECT_EQ(false, EvalJs(shell(), script).ExtractBool());
}
class InterestGroupKAnonmityEnforceAnonymityExposureDisabledTest
: public InterestGroupBrowserTest {
public:
InterestGroupKAnonmityEnforceAnonymityExposureDisabledTest() {
feature_list_.InitWithFeatures(
{}, {blink::features::kEnforceAnonymityExposure});
}
protected:
base::test::ScopedFeatureList feature_list_;
};
IN_PROC_BROWSER_TEST_F(
InterestGroupKAnonmityEnforceAnonymityExposureDisabledTest,
EnforceAnonymityExposureDisabled) {
GURL url = embedded_https_test_server().GetURL("a.test", "/echo");
ASSERT_TRUE(NavigateToURL(shell(), url));
std::string script = R"(
(function() {
return navigator.deprecatedRunAdAuctionEnforcesKAnonymity;
})())";
EXPECT_EQ(base::Value(), EvalJs(shell(), script));
}
class InterestGroupBiddingAndAuctionServerRestrictedPermissionsPolicyBrowserTest
: public InterestGroupBiddingAndAuctionServerBrowserTest {
public:
InterestGroupBiddingAndAuctionServerRestrictedPermissionsPolicyBrowserTest() {
feature_list_.InitAndEnableFeature(
network::features::kAdInterestGroupAPIRestrictedPolicyByDefault);
}
protected:
base::test::ScopedFeatureList feature_list_;
};
IN_PROC_BROWSER_TEST_F(
InterestGroupBiddingAndAuctionServerRestrictedPermissionsPolicyBrowserTest,
ChecksPermissionPolicyEnforce) {
// TODO(behamilton): Merge with
// InterestGroupBrowserTest.EnabledByPermissionsPolicy once this feature has
// been released.
GURL test_url = embedded_https_test_server().GetURL(
"a.test",
"/cross_site_iframe_factory.html?a.test("
"a.test,"
"b.test("
"c.test{allow-join-ad-interest-group;run-ad-auction},"
"a.test{allow-join-ad-interest-group;run-ad-auction},"
"a.test{allow-join-ad-interest-group;run-ad-auction}"
")"
")");
url::Origin test_origin = url::Origin::Create(test_url);
ProvideKeys();
ASSERT_TRUE(NavigateToURL(shell(), test_url));
RenderFrameHost* main_frame = web_contents()->GetPrimaryMainFrame();
RenderFrameHost* same_origin_iframe = ChildFrameAt(main_frame, 0);
RenderFrameHost* cross_origin_iframe = ChildFrameAt(main_frame, 1);
RenderFrameHost* inner_cross_origin_iframe =
ChildFrameAt(cross_origin_iframe, 0);
RenderFrameHost* same_origin_iframe_in_cross_origin_iframe =
ChildFrameAt(cross_origin_iframe, 1);
RenderFrameHost* same_origin_iframe_in_cross_origin_iframe2 =
ChildFrameAt(cross_origin_iframe, 2);
RenderFrameHost* execution_targets[] = {
main_frame,
same_origin_iframe,
cross_origin_iframe,
inner_cross_origin_iframe,
same_origin_iframe_in_cross_origin_iframe,
same_origin_iframe_in_cross_origin_iframe2};
for (auto* execution_target : execution_targets) {
SCOPED_TRACE(execution_target->GetLastCommittedURL().spec());
RenderFrameHost* execution_targets_with_message[] = {
cross_origin_iframe, inner_cross_origin_iframe,
same_origin_iframe_in_cross_origin_iframe,
same_origin_iframe_in_cross_origin_iframe2};
if (base::Contains(execution_targets_with_message, execution_target)) {
EXPECT_EQ(
"NotAllowedError: Failed to execute 'getInterestGroupAdAuctionData' "
"on 'Navigator': "
"Feature run-ad-auction is not enabled by Permissions Policy",
GetInterestGroupAdAuctionData(
test_origin, kDefaultBiddingAndAuctionGCPCoordinatorOrigin,
/*sellers=*/std::nullopt,
/*config=*/nullptr, execution_target));
} else {
EXPECT_EQ("|",
GetInterestGroupAdAuctionData(
test_origin, kDefaultBiddingAndAuctionGCPCoordinatorOrigin,
/*sellers=*/std::nullopt,
/*config=*/nullptr, execution_target));
}
}
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionWithAdditionalBid) {
URLLoaderMonitor url_loader_monitor;
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_cars");
GURL additional_bid_ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_horses");
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"))
.SetAds(/*ads=*/{{{ad_url, /*metadata=*/std::nullopt}}})
.Build()));
for (bool should_win : {true, false}) {
SCOPED_TRACE(should_win);
AttachInterestGroupObserver();
ClearReceivedRequests();
ASSERT_TRUE(NavigateToURL(shell(), test_url));
std::string auction_nonce = CreateAuctionNonceAndWait();
GURL additional_bid_logic_url = embedded_https_test_server().GetURL(
"b.test", "/interest_group/bidding_logic_additional_bid.js");
url::Origin additional_bid_origin =
url::Origin::Create(additional_bid_logic_url);
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicUrl: $2,
interestGroupBuyers: [$1, $6],
auctionNonce: $3,
additionalBids: provideAdditionalBids($1, $3, [JSON.stringify({
interestGroup: {
name: 'campaign123',
biddingLogicURL: $5,
owner:$6
},
bid: {
ad: ['ad'],
bid: $7,
render: $4,
},
auctionNonce: $3,
seller: $1,
})])})",
test_origin,
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js"),
auction_nonce, additional_bid_ad_url, additional_bid_logic_url,
additional_bid_origin, should_win ? 1.99 : 0.1);
RunAuctionAndWaitForURLAndNavigateIframe(
auction_config, should_win ? additional_bid_ad_url : ad_url);
WaitForUrl(embedded_https_test_server().GetURL("a.test",
"/echoall?report_seller"));
if (should_win) {
WaitForUrl(embedded_https_test_server().GetURL(
"a.test", "/echoall?report_bidder_additional"));
EXPECT_FALSE(HasServerSeenUrl(embedded_https_test_server().GetURL(
"a.test", "/echoall?report_bidder")));
WaitForAccessObserved({
{"1", TestInterestGroupObserver::kLoaded, test_origin, "cars"},
{"1", TestInterestGroupObserver::kBid, test_origin, "cars", 1.0},
{"1", TestInterestGroupObserver::kAdditionalBid,
additional_bid_origin, "campaign123", 1.99},
{"1", TestInterestGroupObserver::kAdditionalBidWin,
additional_bid_origin, "campaign123"},
});
} else {
WaitForUrl(embedded_https_test_server().GetURL("a.test",
"/echoall?report_bidder"));
EXPECT_FALSE(HasServerSeenUrl(embedded_https_test_server().GetURL(
"a.test", "/echoall?report_bidder_additional")));
WaitForAccessObserved(
{{"1", TestInterestGroupObserver::kLoaded, test_origin, "cars"},
{"1", TestInterestGroupObserver::kBid, test_origin, "cars", 1.0},
{"1", TestInterestGroupObserver::kAdditionalBid,
additional_bid_origin, "campaign123", 0.1},
{"1", TestInterestGroupObserver::kWin, test_origin, "cars"}});
}
}
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionWithAdditionalBidNoRegularBids) {
URLLoaderMonitor url_loader_monitor;
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL additional_bid_ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_horses");
AttachInterestGroupObserver();
ClearReceivedRequests();
ASSERT_TRUE(NavigateToURL(shell(), test_url));
std::string auction_nonce = CreateAuctionNonceAndWait();
GURL additional_bid_logic_url = embedded_https_test_server().GetURL(
"b.test", "/interest_group/bidding_logic_additional_bid.js");
url::Origin additional_bid_origin =
url::Origin::Create(additional_bid_logic_url);
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicUrl: $2,
interestGroupBuyers: [$1, $6],
auctionNonce: $3,
additionalBids: provideAdditionalBids($1, $3, [JSON.stringify({
interestGroup: {
name: 'campaign123',
biddingLogicURL: $5,
owner:$6
},
bid: {
ad: ['ad'],
bid: 2,
render: $4,
},
auctionNonce: $3,
seller: $1,
})])})",
test_origin,
embedded_https_test_server().GetURL("a.test",
"/interest_group/decision_logic.js"),
auction_nonce, additional_bid_ad_url, additional_bid_logic_url,
additional_bid_origin);
RunAuctionAndWaitForURLAndNavigateIframe(auction_config,
additional_bid_ad_url);
WaitForUrl(
embedded_https_test_server().GetURL("a.test", "/echoall?report_seller"));
WaitForUrl(embedded_https_test_server().GetURL(
"a.test", "/echoall?report_bidder_additional"));
WaitForAccessObserved({
{"1", TestInterestGroupObserver::kAdditionalBid, additional_bid_origin,
"campaign123", 2.0},
{"1", TestInterestGroupObserver::kAdditionalBidWin, additional_bid_origin,
"campaign123"},
});
}
// Two additional bids, second one of which wins.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionWithWinningAdditionalBidFromTwo) {
URLLoaderMonitor url_loader_monitor;
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_cars");
GURL additional_bid_ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_horses");
GURL second_additional_bid_ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_zebras");
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"))
.SetAds(/*ads=*/{{{ad_url, /*metadata=*/std::nullopt}}})
.Build()));
std::string auction_nonce = CreateAuctionNonceAndWait();
GURL additional_bid_logic_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic_additional_bid.js");
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicUrl: $2,
interestGroupBuyers: [$1],
auctionNonce: $3,
additionalBids: provideAdditionalBids($1, $3, [JSON.stringify({
interestGroup: {
name: 'campaign123',
biddingLogicURL: $5,
owner:$6
},
bid: {
ad: ['ad'],
bid: 1.99,
render: $4,
},
auctionNonce: $3,
seller: $1,
}), JSON.stringify({
interestGroup: {
name: 'campaign234',
biddingLogicURL: $5,
owner:$6
},
bid: {
ad: ['ad'],
bid: 3.99,
render: $7,
},
auctionNonce: $3,
seller: $1,
})])})",
test_origin,
embedded_https_test_server().GetURL("a.test",
"/interest_group/decision_logic.js"),
auction_nonce, additional_bid_ad_url, additional_bid_logic_url,
url::Origin::Create(additional_bid_logic_url),
second_additional_bid_ad_url);
RunAuctionAndWaitForURLAndNavigateIframe(auction_config,
second_additional_bid_ad_url);
WaitForUrl(
embedded_https_test_server().GetURL("a.test", "/echoall?report_seller"));
WaitForUrl(embedded_https_test_server().GetURL(
"a.test", "/echoall?report_bidder_additional"));
EXPECT_FALSE(HasServerSeenUrl(
embedded_https_test_server().GetURL("a.test", "/echoall?report_bidder")));
}
// Two additional bids, the lower one of which wins since the upper one is
// disabled by negative targeting.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionWithWinningAdditionalBidFromTwoWithNT) {
URLLoaderMonitor url_loader_monitor;
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_cars");
GURL additional_bid_ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_horses");
GURL second_additional_bid_ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_zebras");
constexpr char kNegativeInterestGroupName[] = "current-zebra-owner";
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"))
.SetAds(/*ads=*/{{{ad_url, /*metadata=*/std::nullopt}}})
.Build()));
EXPECT_EQ(kSuccess, JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/url::Origin::Create(test_url),
/*name=*/kNegativeInterestGroupName)
.SetAdditionalBidKey(kPublicKey1)
.Build()));
std::string auction_nonce = CreateAuctionNonceAndWait();
GURL additional_bid_logic_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic_additional_bid.js");
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicUrl: $2,
interestGroupBuyers: [$1],
auctionNonce: $3,
additionalBids: provideAdditionalBids($1, $3, [JSON.stringify({
interestGroup: {
name: 'campaign123',
biddingLogicURL: $5,
owner:$6
},
bid: {
ad: ['ad'],
bid: 1.99,
render: $4,
},
auctionNonce: $3,
seller: $1,
}), JSON.stringify({
interestGroup: {
name: 'campaign234',
biddingLogicURL: $5,
owner:$6
},
bid: {
ad: ['ad'],
bid: 3.99,
render: $7,
},
auctionNonce: $3,
seller: $1,
negativeInterestGroup: $8
})])})",
test_origin,
embedded_https_test_server().GetURL("a.test",
"/interest_group/decision_logic.js"),
auction_nonce, additional_bid_ad_url, additional_bid_logic_url,
url::Origin::Create(additional_bid_logic_url),
second_additional_bid_ad_url, kNegativeInterestGroupName);
RunAuctionAndWaitForURLAndNavigateIframe(auction_config,
additional_bid_ad_url);
WaitForUrl(
embedded_https_test_server().GetURL("a.test", "/echoall?report_seller"));
WaitForUrl(embedded_https_test_server().GetURL(
"a.test", "/echoall?report_bidder_additional"));
EXPECT_FALSE(HasServerSeenUrl(
embedded_https_test_server().GetURL("a.test", "/echoall?report_bidder")));
}
// If additional bid's script doesn't have necessary CORS permissions, it can
// still win, but the reporting worklet won't happen.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionWithWinningAdditionalBidNoCors) {
URLLoaderMonitor url_loader_monitor;
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_cars");
GURL additional_bid_ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_horses");
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"))
.SetAds(/*ads=*/{{{ad_url, /*metadata=*/std::nullopt}}})
.Build()));
std::string auction_nonce = CreateAuctionNonceAndWait();
GURL additional_bid_logic_url = embedded_https_test_server().GetURL(
"b.test", "/interest_group/bidding_logic_additional_bid_no_cors.js");
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicUrl: $2,
interestGroupBuyers: [$1, $6],
auctionNonce: $3,
additionalBids: provideAdditionalBids($1, $3, [JSON.stringify({
interestGroup: {
name: 'campaign123',
biddingLogicURL: $5,
owner:$6
},
bid: {
ad: ['ad'],
bid: 1.99,
render: $4,
},
auctionNonce: $3,
seller: $1,
})])})",
test_origin,
embedded_https_test_server().GetURL("a.test",
"/interest_group/decision_logic.js"),
auction_nonce, additional_bid_ad_url, additional_bid_logic_url,
url::Origin::Create(additional_bid_logic_url));
RunAuctionAndWaitForURLAndNavigateIframe(auction_config,
additional_bid_ad_url);
WaitForUrl(
embedded_https_test_server().GetURL("a.test", "/echoall?report_seller"));
EXPECT_FALSE(HasServerSeenUrl(embedded_https_test_server().GetURL(
"a.test", "/echoall?report_bidder_additional")));
EXPECT_FALSE(HasServerSeenUrl(
embedded_https_test_server().GetURL("a.test", "/echoall?report_bidder")));
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionWithWrongCurrencyAdditionalBid) {
URLLoaderMonitor url_loader_monitor;
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_cars");
GURL additional_bid_ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_horses");
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"))
.SetAds(/*ads=*/{{{ad_url, /*metadata=*/std::nullopt}}})
.Build()));
std::string auction_nonce = CreateAuctionNonceAndWait();
GURL additional_bid_logic_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic_additional_bid.js");
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicUrl: $2,
interestGroupBuyers: [$1],
auctionNonce: $3,
perBuyerCurrencies: {
$6: 'CAD'
},
additionalBids: provideAdditionalBids($1, $3, [
JSON.stringify({
interestGroup: {
name: 'campaign123',
biddingLogicURL: $5,
owner:$6
},
bid: {
ad: ['ad'],
bid: 1.99,
bidCurrency: 'USD',
render: $4,
},
auctionNonce: $3,
seller: $1,
}),
])})",
test_origin,
embedded_https_test_server().GetURL("a.test",
"/interest_group/decision_logic.js"),
auction_nonce, additional_bid_ad_url, additional_bid_logic_url,
url::Origin::Create(additional_bid_logic_url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
console_observer.SetPattern(
"Worklet error: Rejecting an additionalBid due to currency mismatch.");
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad_url);
EXPECT_TRUE(console_observer.Wait());
WaitForUrl(
embedded_https_test_server().GetURL("a.test", "/echoall?report_seller"));
WaitForUrl(
embedded_https_test_server().GetURL("a.test", "/echoall?report_bidder"));
EXPECT_FALSE(HasServerSeenUrl(embedded_https_test_server().GetURL(
"a.test", "/echoall?report_bidder_additional")));
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionWithBrokenAdditionalBid) {
URLLoaderMonitor url_loader_monitor;
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_cars");
GURL additional_bid_ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_horses");
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"))
.SetAds(/*ads=*/{{{ad_url, /*metadata=*/std::nullopt}}})
.Build()));
std::string auction_nonce = CreateAuctionNonceAndWait();
GURL additional_bid_logic_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic_additional_bid.js");
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicUrl: $2,
interestGroupBuyers: [$1],
auctionNonce: $3,
additionalBids: provideAdditionalBids($1, $3, ['"boo'])
})",
test_origin,
embedded_https_test_server().GetURL("a.test",
"/interest_group/decision_logic.js"),
auction_nonce, additional_bid_ad_url, additional_bid_logic_url,
url::Origin::Create(additional_bid_logic_url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
console_observer.SetPattern("*Unable to parse additional bid as JSON*");
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad_url);
EXPECT_TRUE(console_observer.Wait());
WaitForUrl(
embedded_https_test_server().GetURL("a.test", "/echoall?report_seller"));
WaitForUrl(
embedded_https_test_server().GetURL("a.test", "/echoall?report_bidder"));
EXPECT_FALSE(HasServerSeenUrl(embedded_https_test_server().GetURL(
"a.test", "/echoall?report_bidder_additional")));
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionWithNoIGsAndEmptyAdditionalBids) {
URLLoaderMonitor url_loader_monitor;
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_cars");
GURL additional_bid_ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_horses");
std::string auction_nonce = CreateAuctionNonceAndWait();
GURL additional_bid_logic_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic_additional_bid.js");
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicUrl: $2,
interestGroupBuyers: [$1],
auctionNonce: $3,
additionalBids: provideAdditionalBids($1, $3, [])})",
test_origin,
embedded_https_test_server().GetURL("a.test",
"/interest_group/decision_logic.js"),
auction_nonce, additional_bid_ad_url, additional_bid_logic_url,
url::Origin::Create(additional_bid_logic_url));
EXPECT_EQ(base::Value(), RunAuctionAndWait(auction_config));
}
// Basic negative IG test: one negative IG specified in the additional bid,
// and it's present on the user's device, so it suppresses the additional bid.
IN_PROC_BROWSER_TEST_F(
InterestGroupBrowserTest,
AdditionalBidWithOneValidAndPresentNegativeIGDoesNotBid) {
URLLoaderMonitor url_loader_monitor;
constexpr char kTestOrigin[] = "a.test";
GURL test_url = embedded_https_test_server().GetURL(kTestOrigin,
"/page_with_iframe.html");
GURL ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_cars");
GURL additional_bid_ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_horses");
constexpr char kNegativeInterestGroupName[] = "current-horse-owner";
ASSERT_TRUE(NavigateToURL(shell(), test_url));
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/url::Origin::Create(test_url),
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
kTestOrigin, "/interest_group/bidding_logic.js"))
.SetAds(/*ads=*/{{{ad_url, /*metadata=*/std::nullopt}}})
.Build()));
EXPECT_EQ(kSuccess, JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/url::Origin::Create(test_url),
/*name=*/kNegativeInterestGroupName)
.SetAdditionalBidKey(kPublicKey1)
.Build()));
std::string auction_nonce = CreateAuctionNonceAndWait();
GURL additional_bid_logic_url = embedded_https_test_server().GetURL(
kTestOrigin, "/interest_group/bidding_logic_additional_bid.js");
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicUrl: $2,
interestGroupBuyers: [$1],
auctionNonce: $3,
additionalBids: provideAdditionalBids($1, $3, [JSON.stringify({
interestGroup: {
name: 'campaign123',
biddingLogicURL: $5,
owner:$6
},
bid: {
ad: ['ad'],
bid: 1.99,
render: $4,
},
auctionNonce: $3,
seller: $1,
negativeInterestGroup: $7
})])})",
url::Origin::Create(test_url),
embedded_https_test_server().GetURL(kTestOrigin,
"/interest_group/decision_logic.js"),
auction_nonce, additional_bid_ad_url, additional_bid_logic_url,
url::Origin::Create(additional_bid_logic_url),
kNegativeInterestGroupName);
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad_url);
WaitForUrl(embedded_https_test_server().GetURL(kTestOrigin,
"/echoall?report_seller"));
WaitForUrl(embedded_https_test_server().GetURL(kTestOrigin,
"/echoall?report_bidder"));
EXPECT_FALSE(HasServerSeenUrl(embedded_https_test_server().GetURL(
kTestOrigin, "/echoall?report_bidder_additional")));
}
// Same as AdditionalBidWithOneValidAndPresentNegativeIGDoesNotBid,
// but with IG bid and additional bid from different buyers.
IN_PROC_BROWSER_TEST_F(
InterestGroupBrowserTest,
AdditionalBidFromDifferentBuyerAndOnePresentAndValidNegativeIGDoesNotBid) {
URLLoaderMonitor url_loader_monitor;
constexpr char kTestOrigin[] = "allow-join.a.test";
GURL test_url = embedded_https_test_server().GetURL(kTestOrigin,
"/page_with_iframe.html");
constexpr char kTestAdditionalBidOrigin[] = "allow-join.b.test";
GURL additional_bid_logic_url = embedded_https_test_server().GetURL(
kTestAdditionalBidOrigin,
"/interest_group/bidding_logic_additional_bid.js");
GURL ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_cars");
GURL additional_bid_ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_horses");
constexpr char kNegativeInterestGroupName[] = "current-horse-owner";
ASSERT_TRUE(NavigateToURL(shell(), test_url));
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/url::Origin::Create(test_url),
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
kTestOrigin, "/interest_group/bidding_logic.js"))
.SetAds(/*ads=*/{{{ad_url, /*metadata=*/std::nullopt}}})
.Build()));
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/url::Origin::Create(additional_bid_logic_url),
/*name=*/kNegativeInterestGroupName)
.SetAdditionalBidKey(kPublicKey1)
.Build()));
std::string auction_nonce = CreateAuctionNonceAndWait();
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicUrl: $2,
interestGroupBuyers: [$1, $6],
auctionNonce: $3,
additionalBids: provideAdditionalBids($1, $3, [JSON.stringify({
interestGroup: {
name: 'campaign123',
biddingLogicURL: $5,
owner:$6
},
bid: {
ad: ['ad'],
bid: 1.99,
render: $4,
},
auctionNonce: $3,
seller: $1,
negativeInterestGroup: $7
})])})",
url::Origin::Create(test_url),
embedded_https_test_server().GetURL(kTestOrigin,
"/interest_group/decision_logic.js"),
auction_nonce, additional_bid_ad_url, additional_bid_logic_url,
url::Origin::Create(additional_bid_logic_url),
kNegativeInterestGroupName);
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad_url);
WaitForUrl(embedded_https_test_server().GetURL(kTestOrigin,
"/echoall?report_seller"));
WaitForUrl(embedded_https_test_server().GetURL(kTestOrigin,
"/echoall?report_bidder"));
EXPECT_FALSE(HasServerSeenUrl(embedded_https_test_server().GetURL(
kTestOrigin, "/echoall?report_bidder_additional")));
}
// Additional bid buyers must be specified in interestGroupBuyers for the bid
// to participate in the auction.
IN_PROC_BROWSER_TEST_F(
InterestGroupBrowserTest,
AdditionalBidWithBuyerNotIncludedInInterestGroupBuyersDoesNotBid) {
URLLoaderMonitor url_loader_monitor;
constexpr char kTestOrigin[] = "allow-join.a.test";
GURL test_url = embedded_https_test_server().GetURL(kTestOrigin,
"/page_with_iframe.html");
constexpr char kTestAdditionalBidOrigin[] = "allow-join.b.test";
GURL additional_bid_logic_url = embedded_https_test_server().GetURL(
kTestAdditionalBidOrigin,
"/interest_group/bidding_logic_additional_bid.js");
GURL ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_cars");
GURL additional_bid_ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_horses");
constexpr char kNegativeInterestGroupName[] = "current-horse-owner";
ASSERT_TRUE(NavigateToURL(shell(), test_url));
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/url::Origin::Create(test_url),
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
kTestOrigin, "/interest_group/bidding_logic.js"))
.SetAds(/*ads=*/{{{ad_url, /*metadata=*/std::nullopt}}})
.Build()));
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/url::Origin::Create(additional_bid_logic_url),
/*name=*/kNegativeInterestGroupName)
.SetAdditionalBidKey(kPublicKey1)
.Build()));
std::string auction_nonce = CreateAuctionNonceAndWait();
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicUrl: $2,
interestGroupBuyers: [$1], // Here, we intentionally omit b.test
auctionNonce: $3,
additionalBids: provideAdditionalBids($1, $3, [JSON.stringify({
interestGroup: {
name: 'campaign123',
biddingLogicURL: $5,
owner:$6
},
bid: {
ad: ['ad'],
bid: 1.99,
render: $4,
},
auctionNonce: $3,
seller: $1,
negativeInterestGroup: $7
})])})",
url::Origin::Create(test_url),
embedded_https_test_server().GetURL(kTestOrigin,
"/interest_group/decision_logic.js"),
auction_nonce, additional_bid_ad_url, additional_bid_logic_url,
url::Origin::Create(additional_bid_logic_url),
kNegativeInterestGroupName);
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad_url);
WaitForUrl(embedded_https_test_server().GetURL(kTestOrigin,
"/echoall?report_seller"));
WaitForUrl(embedded_https_test_server().GetURL(kTestOrigin,
"/echoall?report_bidder"));
EXPECT_FALSE(HasServerSeenUrl(embedded_https_test_server().GetURL(
kTestOrigin, "/echoall?report_bidder_additional")));
}
// One negative IG specified in the additional bid, but it's not present on the
// user's device, so the additional bid participates in the auction and wins.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
AdditionalBidWithOneNotPresentNegativeIGSoDoesBid) {
URLLoaderMonitor url_loader_monitor;
constexpr char kTestOrigin[] = "a.test";
GURL test_url = embedded_https_test_server().GetURL(kTestOrigin,
"/page_with_iframe.html");
GURL ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_cars");
GURL additional_bid_ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_horses");
constexpr char kNegativeInterestGroupName[] = "current-horse-owner";
ASSERT_TRUE(NavigateToURL(shell(), test_url));
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/url::Origin::Create(test_url),
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
kTestOrigin, "/interest_group/bidding_logic.js"))
.SetAds(/*ads=*/{{{ad_url, /*metadata=*/std::nullopt}}})
.Build()));
std::string auction_nonce = CreateAuctionNonceAndWait();
GURL additional_bid_logic_url = embedded_https_test_server().GetURL(
kTestOrigin, "/interest_group/bidding_logic_additional_bid.js");
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicUrl: $2,
interestGroupBuyers: [$1],
auctionNonce: $3,
additionalBids: provideAdditionalBids($1, $3, [JSON.stringify({
interestGroup: {
name: 'campaign123',
biddingLogicURL: $5,
owner:$6
},
bid: {
ad: ['ad'],
bid: 1.99,
render: $4,
},
auctionNonce: $3,
seller: $1,
negativeInterestGroup: $7
})])})",
url::Origin::Create(test_url),
embedded_https_test_server().GetURL(kTestOrigin,
"/interest_group/decision_logic.js"),
auction_nonce, additional_bid_ad_url, additional_bid_logic_url,
url::Origin::Create(additional_bid_logic_url),
kNegativeInterestGroupName);
RunAuctionAndWaitForURLAndNavigateIframe(auction_config,
additional_bid_ad_url);
WaitForUrl(embedded_https_test_server().GetURL(kTestOrigin,
"/echoall?report_seller"));
WaitForUrl(embedded_https_test_server().GetURL(
kTestOrigin, "/echoall?report_bidder_additional"));
EXPECT_FALSE(HasServerSeenUrl(embedded_https_test_server().GetURL(
kTestOrigin, "/echoall?report_bidder")));
}
// Two negative IGs are specified in the additional bid, one is present
// on the user's device, the other is not, but because multiple negative IGs
// are interpreted as "suppress if any of these are present", the additional bid
// is suppressed.
IN_PROC_BROWSER_TEST_F(
InterestGroupBrowserTest,
AdditionalBidWithMultipleNegativeIGsAndEvenOnePresentSoDoesNotBid) {
URLLoaderMonitor url_loader_monitor;
constexpr char kTestOrigin[] = "a.test";
GURL test_url = embedded_https_test_server().GetURL(kTestOrigin,
"/page_with_iframe.html");
GURL ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_cars");
GURL additional_bid_ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_horses");
constexpr char kNegativeInterestGroupName[] = "current-horse-owner";
ASSERT_TRUE(NavigateToURL(shell(), test_url));
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/url::Origin::Create(test_url),
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
kTestOrigin, "/interest_group/bidding_logic.js"))
.SetAds(/*ads=*/{{{ad_url, /*metadata=*/std::nullopt}}})
.Build()));
EXPECT_EQ(kSuccess, JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/url::Origin::Create(test_url),
/*name=*/kNegativeInterestGroupName)
.SetAdditionalBidKey(kPublicKey1)
.Build()));
std::string auction_nonce = CreateAuctionNonceAndWait();
GURL additional_bid_logic_url = embedded_https_test_server().GetURL(
kTestOrigin, "/interest_group/bidding_logic_additional_bid.js");
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicUrl: $2,
interestGroupBuyers: [$1],
auctionNonce: $3,
additionalBids: provideAdditionalBids($1, $3, [JSON.stringify({
interestGroup: {
name: 'campaign123',
biddingLogicURL: $5,
owner:$6
},
bid: {
ad: ['ad'],
bid: 1.99,
render: $4,
},
auctionNonce: $3,
seller: $1,
negativeInterestGroups: {
joiningOrigin: $7,
interestGroupNames: [
$8,
"some-negative-ig-this-user-is-not-a-member-of"
]
}
})])})",
url::Origin::Create(test_url),
embedded_https_test_server().GetURL(kTestOrigin,
"/interest_group/decision_logic.js"),
auction_nonce, additional_bid_ad_url, additional_bid_logic_url,
url::Origin::Create(additional_bid_logic_url),
url::Origin::Create(test_url), kNegativeInterestGroupName);
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad_url);
WaitForUrl(embedded_https_test_server().GetURL(kTestOrigin,
"/echoall?report_seller"));
WaitForUrl(embedded_https_test_server().GetURL(kTestOrigin,
"/echoall?report_bidder"));
EXPECT_FALSE(HasServerSeenUrl(embedded_https_test_server().GetURL(
kTestOrigin, "/echoall?report_bidder_additional")));
}
// Like above, two negative IGs are specified in the additional bid, but this
// time both are present, giving it two reasons to suppress the additional bid.
IN_PROC_BROWSER_TEST_F(
InterestGroupBrowserTest,
AdditionalBidWithMultipleNegativeIGsAndMultiplePresentSoStillDoesNotBid) {
URLLoaderMonitor url_loader_monitor;
constexpr char kTestOrigin[] = "a.test";
GURL test_url = embedded_https_test_server().GetURL(kTestOrigin,
"/page_with_iframe.html");
GURL ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_cars");
GURL additional_bid_ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_horses");
constexpr char kNegativeInterestGroupName1[] = "current-horse-owner";
constexpr char kNegativeInterestGroupName2[] = "current-buggy-owner";
ASSERT_TRUE(NavigateToURL(shell(), test_url));
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/url::Origin::Create(test_url),
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
kTestOrigin, "/interest_group/bidding_logic.js"))
.SetAds(/*ads=*/{{{ad_url, /*metadata=*/std::nullopt}}})
.Build()));
EXPECT_EQ(kSuccess, JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/url::Origin::Create(test_url),
/*name=*/kNegativeInterestGroupName1)
.SetAdditionalBidKey(kPublicKey1)
.Build()));
EXPECT_EQ(kSuccess, JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/url::Origin::Create(test_url),
/*name=*/kNegativeInterestGroupName2)
.SetAdditionalBidKey(kPublicKey2)
.Build()));
std::string auction_nonce = CreateAuctionNonceAndWait();
GURL additional_bid_logic_url = embedded_https_test_server().GetURL(
kTestOrigin, "/interest_group/bidding_logic_additional_bid.js");
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicUrl: $2,
interestGroupBuyers: [$1],
auctionNonce: $3,
additionalBids: provideAdditionalBids($1, $3, [JSON.stringify({
interestGroup: {
name: 'campaign123',
biddingLogicURL: $5,
owner:$6
},
bid: {
ad: ['ad'],
bid: 1.99,
render: $4,
},
auctionNonce: $3,
seller: $1,
negativeInterestGroups: {
joiningOrigin: $7,
interestGroupNames: [
$8,
$9
]
}
})])})",
url::Origin::Create(test_url),
embedded_https_test_server().GetURL(kTestOrigin,
"/interest_group/decision_logic.js"),
auction_nonce, additional_bid_ad_url, additional_bid_logic_url,
url::Origin::Create(additional_bid_logic_url),
url::Origin::Create(test_url), kNegativeInterestGroupName1,
kNegativeInterestGroupName2);
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad_url);
WaitForUrl(embedded_https_test_server().GetURL(kTestOrigin,
"/echoall?report_seller"));
WaitForUrl(embedded_https_test_server().GetURL(kTestOrigin,
"/echoall?report_bidder"));
EXPECT_FALSE(HasServerSeenUrl(embedded_https_test_server().GetURL(
kTestOrigin, "/echoall?report_bidder_additional")));
}
// Again, two negative IGs are specified in the additional bid, but this time,
// neither is present on the user's device, and so the additional bid
// participates in the auction and wins.
IN_PROC_BROWSER_TEST_F(
InterestGroupBrowserTest,
AdditionalBidWithMultipleNegativeIGsButNeitherPresentSoDoesBid) {
URLLoaderMonitor url_loader_monitor;
constexpr char kTestOrigin[] = "a.test";
GURL test_url = embedded_https_test_server().GetURL(kTestOrigin,
"/page_with_iframe.html");
GURL ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_cars");
GURL additional_bid_ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_horses");
constexpr char kNegativeInterestGroupName1[] = "current-horse-owner";
constexpr char kNegativeInterestGroupName2[] = "current-buggy-owner";
ASSERT_TRUE(NavigateToURL(shell(), test_url));
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/url::Origin::Create(test_url),
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
kTestOrigin, "/interest_group/bidding_logic.js"))
.SetAds(/*ads=*/{{{ad_url, /*metadata=*/std::nullopt}}})
.Build()));
std::string auction_nonce = CreateAuctionNonceAndWait();
GURL additional_bid_logic_url = embedded_https_test_server().GetURL(
kTestOrigin, "/interest_group/bidding_logic_additional_bid.js");
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicUrl: $2,
interestGroupBuyers: [$1],
auctionNonce: $3,
additionalBids: provideAdditionalBids($1, $3, [JSON.stringify({
interestGroup: {
name: 'campaign123',
biddingLogicURL: $5,
owner:$6
},
bid: {
ad: ['ad'],
bid: 1.99,
render: $4,
},
auctionNonce: $3,
seller: $1,
negativeInterestGroups: {
joiningOrigin: $7,
interestGroupNames: [
$8,
$9
]
}
})])})",
url::Origin::Create(test_url),
embedded_https_test_server().GetURL(kTestOrigin,
"/interest_group/decision_logic.js"),
auction_nonce, additional_bid_ad_url, additional_bid_logic_url,
url::Origin::Create(additional_bid_logic_url),
url::Origin::Create(test_url), kNegativeInterestGroupName1,
kNegativeInterestGroupName2);
RunAuctionAndWaitForURLAndNavigateIframe(auction_config,
additional_bid_ad_url);
WaitForUrl(embedded_https_test_server().GetURL(kTestOrigin,
"/echoall?report_seller"));
WaitForUrl(embedded_https_test_server().GetURL(
kTestOrigin, "/echoall?report_bidder_additional"));
EXPECT_FALSE(HasServerSeenUrl(embedded_https_test_server().GetURL(
kTestOrigin, "/echoall?report_bidder")));
}
// An error scenario: One negative IG specified in the additional bid, but the
// signature associated with the key of that InterestGroup is invalid. (The
// signature is present, but can't be validated against the key specified.)
// As such, the negative IG is ignored, and so the bid participates in the
// auction and wins.
IN_PROC_BROWSER_TEST_F(
InterestGroupBrowserTest,
AdditionalBidWithAPresentNegativeIGWithABadSignatureSoDoesBid) {
URLLoaderMonitor url_loader_monitor;
constexpr char kTestOrigin[] = "a.test";
GURL test_url = embedded_https_test_server().GetURL(kTestOrigin,
"/page_with_iframe.html");
GURL ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_cars");
GURL additional_bid_ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_horses");
constexpr char kNegativeInterestGroupName[] = "current-horse-owner";
ASSERT_TRUE(NavigateToURL(shell(), test_url));
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/url::Origin::Create(test_url),
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
kTestOrigin, "/interest_group/bidding_logic.js"))
.SetAds(/*ads=*/{{{ad_url, /*metadata=*/std::nullopt}}})
.Build()));
EXPECT_EQ(kSuccess, JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/url::Origin::Create(test_url),
/*name=*/kNegativeInterestGroupName)
.SetAdditionalBidKey(kPublicKey1)
.Build()));
std::string auction_nonce = CreateAuctionNonceAndWait();
GURL additional_bid_logic_url = embedded_https_test_server().GetURL(
kTestOrigin, "/interest_group/bidding_logic_additional_bid.js");
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicUrl: $2,
interestGroupBuyers: [$1],
auctionNonce: $3,
additionalBids: provideAdditionalBids($1, $3, [JSON.stringify({
interestGroup: {
name: 'campaign123',
biddingLogicURL: $5,
owner:$6
},
bid: {
ad: ['ad'],
bid: 1.99,
render: $4,
},
auctionNonce: $3,
seller: $1,
negativeInterestGroup: $7
})],
"invalid-signature")})",
url::Origin::Create(test_url),
embedded_https_test_server().GetURL(kTestOrigin,
"/interest_group/decision_logic.js"),
auction_nonce, additional_bid_ad_url, additional_bid_logic_url,
url::Origin::Create(additional_bid_logic_url),
kNegativeInterestGroupName);
WebContentsConsoleObserver console_observer(shell()->web_contents());
console_observer.SetPattern("*Warning:*additional bid*");
RunAuctionAndWaitForURLAndNavigateIframe(auction_config,
additional_bid_ad_url);
EXPECT_TRUE(console_observer.Wait());
ASSERT_EQ(console_observer.messages().size(), static_cast<size_t>(2));
// We get this error because some of the signatures on the signed additional
// bid were bad, i.e. the signature could not be verified against the given
// key for the given bid string.
EXPECT_TRUE(base::MatchPattern(
console_observer.GetMessageAt(0),
"*Some signatures on an additional bid*failed to verify."))
<< "Actual message: " << console_observer.GetMessageAt(0);
// But we also get this error because the negative InterestGroup has no
// corresponding valid signature, so we have to ignore it.
EXPECT_TRUE(base::MatchPattern(
console_observer.GetMessageAt(1),
"*Ignoring negative targeting group 'current-horse-owner'*"
"since its key does not correspond to a valid signature."))
<< "Actual message: " << console_observer.GetMessageAt(1);
WaitForUrl(embedded_https_test_server().GetURL(kTestOrigin,
"/echoall?report_seller"));
WaitForUrl(embedded_https_test_server().GetURL(
kTestOrigin, "/echoall?report_bidder_additional"));
EXPECT_FALSE(HasServerSeenUrl(embedded_https_test_server().GetURL(
kTestOrigin, "/echoall?report_bidder")));
}
// An error scenario: One negative IG specified in the additional bid, but the
// IG specifies a key for which there is no signature present on the signed
// additional bid. (A valid signature is present, but not one that can be used
// to validate that particular negative IG.) As such, the negative IG is
// ignored, and so the bid participates in the auction and wins.
IN_PROC_BROWSER_TEST_F(
InterestGroupBrowserTest,
AdditionalBidWithAPresentNegativeIGWithNoMatchingSignagtureSoDoesBid) {
URLLoaderMonitor url_loader_monitor;
constexpr char kTestOrigin[] = "a.test";
GURL test_url = embedded_https_test_server().GetURL(kTestOrigin,
"/page_with_iframe.html");
GURL ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_cars");
GURL additional_bid_ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_horses");
constexpr char kNegativeInterestGroupName[] = "current-horse-owner";
ASSERT_TRUE(NavigateToURL(shell(), test_url));
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/url::Origin::Create(test_url),
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
kTestOrigin, "/interest_group/bidding_logic.js"))
.SetAds(/*ads=*/{{{ad_url, /*metadata=*/std::nullopt}}})
.Build()));
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/url::Origin::Create(test_url),
/*name=*/kNegativeInterestGroupName)
.SetAdditionalBidKey(kPublicKeyWithNoMatchingSignature)
.Build()));
std::string auction_nonce = CreateAuctionNonceAndWait();
GURL additional_bid_logic_url = embedded_https_test_server().GetURL(
kTestOrigin, "/interest_group/bidding_logic_additional_bid.js");
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicUrl: $2,
interestGroupBuyers: [$1],
auctionNonce: $3,
additionalBids: provideAdditionalBids($1, $3, [JSON.stringify({
interestGroup: {
name: 'campaign123',
biddingLogicURL: $5,
owner:$6
},
bid: {
ad: ['ad'],
bid: 1.99,
render: $4,
},
auctionNonce: $3,
seller: $1,
negativeInterestGroup: $7
})])})",
url::Origin::Create(test_url),
embedded_https_test_server().GetURL(kTestOrigin,
"/interest_group/decision_logic.js"),
auction_nonce, additional_bid_ad_url, additional_bid_logic_url,
url::Origin::Create(additional_bid_logic_url),
kNegativeInterestGroupName);
WebContentsConsoleObserver console_observer(shell()->web_contents());
// Here, there are valid signatures, but none matching for this IG.
console_observer.SetPattern(
"*Warning: Ignoring negative targeting group 'current-horse-owner'*"
"since its key does not correspond to a valid signature.");
RunAuctionAndWaitForURLAndNavigateIframe(auction_config,
additional_bid_ad_url);
EXPECT_TRUE(console_observer.Wait());
WaitForUrl(embedded_https_test_server().GetURL(kTestOrigin,
"/echoall?report_seller"));
WaitForUrl(embedded_https_test_server().GetURL(
kTestOrigin, "/echoall?report_bidder_additional"));
EXPECT_FALSE(HasServerSeenUrl(embedded_https_test_server().GetURL(
kTestOrigin, "/echoall?report_bidder")));
}
// An error scenario: the additional bid specifies multiple negative IGs, both
// present. One has a bad signature, but the other is valid. Since only one
// valid negative IG is needed to suppress an additional bid, it's suppressed.
IN_PROC_BROWSER_TEST_F(
InterestGroupBrowserTest,
AdditionalBidWithMultipleNegativeIGsOneValidAndOneBadSignatureSoDoesNotBid) {
URLLoaderMonitor url_loader_monitor;
constexpr char kTestOrigin[] = "a.test";
GURL test_url = embedded_https_test_server().GetURL(kTestOrigin,
"/page_with_iframe.html");
GURL ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_cars");
GURL additional_bid_ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_horses");
constexpr char kNegativeInterestGroupName1[] = "current-horse-owner";
constexpr char kNegativeInterestGroupName2[] = "current-buggy-owner";
ASSERT_TRUE(NavigateToURL(shell(), test_url));
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/url::Origin::Create(test_url),
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
kTestOrigin, "/interest_group/bidding_logic.js"))
.SetAds(/*ads=*/{{{ad_url, /*metadata=*/std::nullopt}}})
.Build()));
EXPECT_EQ(kSuccess, JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/url::Origin::Create(test_url),
/*name=*/kNegativeInterestGroupName1)
.SetAdditionalBidKey(kPublicKey1)
.Build()));
EXPECT_EQ(kSuccess, JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/url::Origin::Create(test_url),
/*name=*/kNegativeInterestGroupName2)
.SetAdditionalBidKey(kPublicKey2)
.Build()));
std::string auction_nonce = CreateAuctionNonceAndWait();
GURL additional_bid_logic_url = embedded_https_test_server().GetURL(
kTestOrigin, "/interest_group/bidding_logic_additional_bid.js");
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicUrl: $2,
interestGroupBuyers: [$1],
auctionNonce: $3,
additionalBids: provideAdditionalBids($1, $3, [JSON.stringify({
interestGroup: {
name: 'campaign123',
biddingLogicURL: $5,
owner:$6
},
bid: {
ad: ['ad'],
bid: 1.99,
render: $4,
},
auctionNonce: $3,
seller: $1,
negativeInterestGroups: {
joiningOrigin: $7,
interestGroupNames: [
$8,
$9
]
}
})],
"one-invalid-signature")})",
url::Origin::Create(test_url),
embedded_https_test_server().GetURL(kTestOrigin,
"/interest_group/decision_logic.js"),
auction_nonce, additional_bid_ad_url, additional_bid_logic_url,
url::Origin::Create(additional_bid_logic_url),
url::Origin::Create(test_url), kNegativeInterestGroupName1,
kNegativeInterestGroupName2);
WebContentsConsoleObserver console_observer(shell()->web_contents());
console_observer.SetPattern("*Warning:*additional bid*");
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad_url);
EXPECT_TRUE(console_observer.Wait());
ASSERT_EQ(console_observer.messages().size(), static_cast<size_t>(2));
// We get this error because some of the signatures on the signed additional
// bid were bad, i.e. the signature could not be verified against the given
// key for the given bid string.
EXPECT_TRUE(base::MatchPattern(
console_observer.GetMessageAt(0),
"*Some signatures on an additional bid*failed to verify."))
<< "Actual message: " << console_observer.GetMessageAt(0);
// But we also get this error because the negative InterestGroup has no
// corresponding valid signature, so we have to ignore it.
EXPECT_TRUE(base::MatchPattern(
console_observer.GetMessageAt(1),
"*Ignoring negative targeting group 'current-horse-owner'*"
"since its key does not correspond to a valid signature."))
<< "Actual message: " << console_observer.GetMessageAt(1);
WaitForUrl(embedded_https_test_server().GetURL(kTestOrigin,
"/echoall?report_seller"));
WaitForUrl(embedded_https_test_server().GetURL(kTestOrigin,
"/echoall?report_bidder"));
EXPECT_FALSE(HasServerSeenUrl(embedded_https_test_server().GetURL(
kTestOrigin, "/echoall?report_bidder_additional")));
}
// An error scenario: the additional bid specifies multiple negative IGs, both
// present. This time, both have bad signatures. With all negative IGs ignored,
// the additional bid participates in the auction and wins.
IN_PROC_BROWSER_TEST_F(
InterestGroupBrowserTest,
AdditionalBidWithMultipleNegativeIGsWithBadSignatureOnAllDoesBid) {
URLLoaderMonitor url_loader_monitor;
constexpr char kTestOrigin[] = "a.test";
GURL test_url = embedded_https_test_server().GetURL(kTestOrigin,
"/page_with_iframe.html");
GURL ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_cars");
GURL additional_bid_ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_horses");
constexpr char kNegativeInterestGroupName1[] = "current-horse-owner";
constexpr char kNegativeInterestGroupName2[] = "current-buggy-owner";
ASSERT_TRUE(NavigateToURL(shell(), test_url));
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/url::Origin::Create(test_url),
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
kTestOrigin, "/interest_group/bidding_logic.js"))
.SetAds(/*ads=*/{{{ad_url, /*metadata=*/std::nullopt}}})
.Build()));
EXPECT_EQ(kSuccess, JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/url::Origin::Create(test_url),
/*name=*/kNegativeInterestGroupName1)
.SetAdditionalBidKey(kPublicKey1)
.Build()));
EXPECT_EQ(kSuccess, JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/url::Origin::Create(test_url),
/*name=*/kNegativeInterestGroupName2)
.SetAdditionalBidKey(kPublicKey2)
.Build()));
std::string auction_nonce = CreateAuctionNonceAndWait();
GURL additional_bid_logic_url = embedded_https_test_server().GetURL(
kTestOrigin, "/interest_group/bidding_logic_additional_bid.js");
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicUrl: $2,
interestGroupBuyers: [$1],
auctionNonce: $3,
additionalBids: provideAdditionalBids($1, $3, [JSON.stringify({
interestGroup: {
name: 'campaign123',
biddingLogicURL: $5,
owner:$6
},
bid: {
ad: ['ad'],
bid: 1.99,
render: $4,
},
auctionNonce: $3,
seller: $1,
negativeInterestGroups: {
joiningOrigin: $7,
interestGroupNames: [
$8,
$9
]
}
})],
"invalid-signature")})",
url::Origin::Create(test_url),
embedded_https_test_server().GetURL(kTestOrigin,
"/interest_group/decision_logic.js"),
auction_nonce, additional_bid_ad_url, additional_bid_logic_url,
url::Origin::Create(additional_bid_logic_url),
url::Origin::Create(test_url), kNegativeInterestGroupName1,
kNegativeInterestGroupName2);
WebContentsConsoleObserver console_observer(shell()->web_contents());
console_observer.SetPattern("*Warning:*additional bid*");
RunAuctionAndWaitForURLAndNavigateIframe(auction_config,
additional_bid_ad_url);
EXPECT_TRUE(console_observer.Wait());
ASSERT_EQ(console_observer.messages().size(), static_cast<size_t>(3));
// We get this error because some of the signatures on the signed additional
// bid were bad, i.e. the signature could not be verified against the given
// key for the given bid string.
EXPECT_TRUE(base::MatchPattern(
console_observer.GetMessageAt(0),
"*Some signatures on an additional bid*failed to verify."))
<< "Actual message: " << console_observer.GetMessageAt(0);
// But we also get these errors because the negative InterestGroups have no
// corresponding valid signature, so we have to ignore them.
EXPECT_TRUE(base::MatchPattern(
console_observer.GetMessageAt(1),
"*Ignoring negative targeting group 'current-horse-owner'*"
"since its key does not correspond to a valid signature."))
<< "Actual message: " << console_observer.GetMessageAt(1);
EXPECT_TRUE(base::MatchPattern(
console_observer.GetMessageAt(2),
"*Ignoring negative targeting group 'current-buggy-owner'*"
"since its key does not correspond to a valid signature."))
<< "Actual message: " << console_observer.GetMessageAt(2);
WaitForUrl(embedded_https_test_server().GetURL(kTestOrigin,
"/echoall?report_seller"));
WaitForUrl(embedded_https_test_server().GetURL(
kTestOrigin, "/echoall?report_bidder_additional"));
EXPECT_FALSE(HasServerSeenUrl(embedded_https_test_server().GetURL(
kTestOrigin, "/echoall?report_bidder")));
}
// Two negative IGs, one with a joining origin that does match that identified
// in the additional bid, and one that does not. The negative targeting code
// ignores the negative IG with the bad joining origin, but still suppresses
// the additional bid because of the presence and validity of the other negative
// IG.
IN_PROC_BROWSER_TEST_F(
InterestGroupBrowserTest,
AdditionalBidWithMultipleNegativeIGsOneWithWrongJoiningOriginDoesNotBid) {
URLLoaderMonitor url_loader_monitor;
constexpr char kTestOrigin[] = "allow-join.a.test";
GURL test_url = embedded_https_test_server().GetURL(kTestOrigin,
"/page_with_iframe.html");
GURL ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_cars");
GURL additional_bid_ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_horses");
constexpr char kNegativeInterestGroupName1[] = "current-horse-owner";
constexpr char kNegativeInterestGroupName2[] = "current-buggy-owner";
GURL first_ig_joining_origin_url = embedded_https_test_server().GetURL(
"allow-join.b.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), first_ig_joining_origin_url));
EXPECT_EQ(kSuccess, JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/url::Origin::Create(test_url),
/*name=*/kNegativeInterestGroupName1)
.SetAdditionalBidKey(kPublicKey1)
.Build()));
ASSERT_TRUE(NavigateToURL(shell(), test_url));
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/url::Origin::Create(test_url),
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
kTestOrigin, "/interest_group/bidding_logic.js"))
.SetAds(/*ads=*/{{{ad_url, /*metadata=*/std::nullopt}}})
.Build()));
EXPECT_EQ(kSuccess, JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/url::Origin::Create(test_url),
/*name=*/kNegativeInterestGroupName2)
.SetAdditionalBidKey(kPublicKey2)
.Build()));
std::string auction_nonce = CreateAuctionNonceAndWait();
GURL additional_bid_logic_url = embedded_https_test_server().GetURL(
kTestOrigin, "/interest_group/bidding_logic_additional_bid.js");
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicUrl: $2,
interestGroupBuyers: [$1],
auctionNonce: $3,
additionalBids: provideAdditionalBids($1, $3, [JSON.stringify({
interestGroup: {
name: 'campaign123',
biddingLogicURL: $5,
owner:$6
},
bid: {
ad: ['ad'],
bid: 1.99,
render: $4,
},
auctionNonce: $3,
seller: $1,
negativeInterestGroups: {
joiningOrigin: $7,
interestGroupNames: [
$8,
$9
]
}
})])})",
url::Origin::Create(test_url),
embedded_https_test_server().GetURL(kTestOrigin,
"/interest_group/decision_logic.js"),
auction_nonce, additional_bid_ad_url, additional_bid_logic_url,
url::Origin::Create(additional_bid_logic_url),
url::Origin::Create(test_url), kNegativeInterestGroupName1,
kNegativeInterestGroupName2);
WebContentsConsoleObserver console_observer(shell()->web_contents());
// Here, there are valid signatures, but none matching for this IG.
console_observer.SetPattern(
"*Warning: Ignoring negative targeting group 'current-horse-owner'*"
"since it does not have the expected joining origin.");
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad_url);
EXPECT_TRUE(console_observer.Wait());
WaitForUrl(embedded_https_test_server().GetURL(kTestOrigin,
"/echoall?report_seller"));
WaitForUrl(embedded_https_test_server().GetURL(kTestOrigin,
"/echoall?report_bidder"));
EXPECT_FALSE(HasServerSeenUrl(embedded_https_test_server().GetURL(
kTestOrigin, "/echoall?report_bidder_additional")));
}
// Two negative IGs, both with a joining origins that do match that identified
// in the additional bid. The negative targeting code thus ignores both negative
// IGs, and the additional bid participates in the auction and wins.
IN_PROC_BROWSER_TEST_F(
InterestGroupBrowserTest,
AdditionalBidWithMultipleNegativeIGsAllWithWrongJoiningOriginDoesBid) {
URLLoaderMonitor url_loader_monitor;
constexpr char kTestOrigin[] = "allow-join.a.test";
GURL test_url = embedded_https_test_server().GetURL(kTestOrigin,
"/page_with_iframe.html");
GURL ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_cars");
GURL additional_bid_ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_horses");
constexpr char kNegativeInterestGroupName1[] = "current-horse-owner";
constexpr char kNegativeInterestGroupName2[] = "current-buggy-owner";
GURL joining_origin_url = embedded_https_test_server().GetURL(
"allow-join.b.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), joining_origin_url));
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/url::Origin::Create(test_url),
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
kTestOrigin, "/interest_group/bidding_logic.js"))
.SetAds(/*ads=*/{{{ad_url, /*metadata=*/std::nullopt}}})
.Build()));
EXPECT_EQ(kSuccess, JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/url::Origin::Create(test_url),
/*name=*/kNegativeInterestGroupName1)
.SetAdditionalBidKey(kPublicKey1)
.Build()));
EXPECT_EQ(kSuccess, JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/url::Origin::Create(test_url),
/*name=*/kNegativeInterestGroupName2)
.SetAdditionalBidKey(kPublicKey2)
.Build()));
ASSERT_TRUE(NavigateToURL(shell(), test_url));
std::string auction_nonce = CreateAuctionNonceAndWait();
GURL additional_bid_logic_url = embedded_https_test_server().GetURL(
kTestOrigin, "/interest_group/bidding_logic_additional_bid.js");
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicUrl: $2,
interestGroupBuyers: [$1],
auctionNonce: $3,
additionalBids: provideAdditionalBids($1, $3, [JSON.stringify({
interestGroup: {
name: 'campaign123',
biddingLogicURL: $5,
owner:$6
},
bid: {
ad: ['ad'],
bid: 1.99,
render: $4,
},
auctionNonce: $3,
seller: $1,
negativeInterestGroups: {
joiningOrigin: $7,
interestGroupNames: [
$8,
$9
]
}
})])})",
url::Origin::Create(test_url),
embedded_https_test_server().GetURL(kTestOrigin,
"/interest_group/decision_logic.js"),
auction_nonce, additional_bid_ad_url, additional_bid_logic_url,
url::Origin::Create(additional_bid_logic_url),
url::Origin::Create(test_url), kNegativeInterestGroupName1,
kNegativeInterestGroupName2);
WebContentsConsoleObserver console_observer(shell()->web_contents());
console_observer.SetPattern("*Warning:*additional bid*");
RunAuctionAndWaitForURLAndNavigateIframe(auction_config,
additional_bid_ad_url);
EXPECT_TRUE(console_observer.Wait());
ASSERT_EQ(console_observer.messages().size(), static_cast<size_t>(2));
EXPECT_TRUE(base::MatchPattern(
console_observer.GetMessageAt(0),
"*Ignoring negative targeting group 'current-horse-owner'*"
"since it does not have the expected joining origin."))
<< "Actual message: " << console_observer.GetMessageAt(0);
EXPECT_TRUE(base::MatchPattern(
console_observer.GetMessageAt(1),
"*Ignoring negative targeting group 'current-buggy-owner'*"
"since it does not have the expected joining origin."))
<< "Actual message: " << console_observer.GetMessageAt(1);
WaitForUrl(embedded_https_test_server().GetURL(kTestOrigin,
"/echoall?report_seller"));
WaitForUrl(embedded_https_test_server().GetURL(
kTestOrigin, "/echoall?report_bidder_additional"));
EXPECT_FALSE(HasServerSeenUrl(embedded_https_test_server().GetURL(
kTestOrigin, "/echoall?report_bidder")));
}
// No additional bids is also fine. The additional bid always participates.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
AdditionalBidWithNoNegativeIGSAlwaysBids) {
URLLoaderMonitor url_loader_monitor;
constexpr char kTestOrigin[] = "a.test";
GURL test_url = embedded_https_test_server().GetURL(kTestOrigin,
"/page_with_iframe.html");
GURL ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_cars");
GURL additional_bid_ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_horses");
constexpr char kNegativeInterestGroupName[] = "current-horse-owner";
ASSERT_TRUE(NavigateToURL(shell(), test_url));
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/url::Origin::Create(test_url),
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
kTestOrigin, "/interest_group/bidding_logic.js"))
.SetAds(/*ads=*/{{{ad_url, /*metadata=*/std::nullopt}}})
.Build()));
std::string auction_nonce = CreateAuctionNonceAndWait();
GURL additional_bid_logic_url = embedded_https_test_server().GetURL(
kTestOrigin, "/interest_group/bidding_logic_additional_bid.js");
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicUrl: $2,
interestGroupBuyers: [$1],
auctionNonce: $3,
additionalBids: provideAdditionalBids($1, $3, [JSON.stringify({
interestGroup: {
name: 'campaign123',
biddingLogicURL: $5,
owner:$6
},
bid: {
ad: ['ad'],
bid: 1.99,
render: $4,
},
auctionNonce: $3,
seller: $1,
})])
})",
url::Origin::Create(test_url),
embedded_https_test_server().GetURL(kTestOrigin,
"/interest_group/decision_logic.js"),
auction_nonce, additional_bid_ad_url, additional_bid_logic_url,
url::Origin::Create(additional_bid_logic_url),
kNegativeInterestGroupName);
RunAuctionAndWaitForURLAndNavigateIframe(auction_config,
additional_bid_ad_url);
WaitForUrl(embedded_https_test_server().GetURL(kTestOrigin,
"/echoall?report_seller"));
WaitForUrl(embedded_https_test_server().GetURL(
kTestOrigin, "/echoall?report_bidder_additional"));
EXPECT_FALSE(HasServerSeenUrl(embedded_https_test_server().GetURL(
kTestOrigin, "/echoall?report_bidder")));
}
// If the additional bid is missing a topLevelSeller in a component auction, it
// does not bid.
IN_PROC_BROWSER_TEST_F(
InterestGroupBrowserTest,
AdditionalBidOnComponentAuctionMissingTopLevelSellerDoesNotBid) {
URLLoaderMonitor url_loader_monitor;
constexpr char kTestOrigin[] = "a.test";
GURL test_url = embedded_https_test_server().GetURL(kTestOrigin,
"/page_with_iframe.html");
GURL ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_cars");
GURL additional_bid_ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_horses");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/url::Origin::Create(test_url),
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
kTestOrigin, "/interest_group/bidding_logic.js"))
.SetAds(/*ads=*/{{{ad_url, /*metadata=*/std::nullopt}}})
.Build()));
std::string auction_nonce = CreateAuctionNonceAndWait();
GURL additional_bid_logic_url = embedded_https_test_server().GetURL(
kTestOrigin, "/interest_group/bidding_logic_additional_bid.js");
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
// Signal to the top-level seller to allow participation in a component
// auction.
auctionSignals: "sellerAllowsComponentAuction",
componentAuctions: [{
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
// Signal to the bidder and component seller to allow participation in
// a component auction.
auctionSignals: "bidderAllowsComponentAuction,"+
"sellerAllowsComponentAuction",
auctionNonce: $3,
additionalBids: provideAdditionalBids($1, $3, [JSON.stringify({
interestGroup: {
name: 'campaign123',
biddingLogicURL: $5,
owner:$6
},
bid: {
ad: ['ad'],
bid: 1.99,
render: $4,
},
auctionNonce: $3,
seller: $1,
})])
}]
})",
url::Origin::Create(test_url),
embedded_https_test_server().GetURL(kTestOrigin,
"/interest_group/decision_logic.js"),
auction_nonce, additional_bid_ad_url, additional_bid_logic_url,
url::Origin::Create(additional_bid_logic_url));
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad_url);
WaitForUrl(embedded_https_test_server().GetURL(kTestOrigin,
"/echoall?report_seller"));
WaitForUrl(embedded_https_test_server().GetURL(kTestOrigin,
"/echoall?report_bidder"));
EXPECT_FALSE(HasServerSeenUrl(embedded_https_test_server().GetURL(
kTestOrigin, "/echoall?report_bidder_additional")));
}
// Here, the additional bid is on a component auction, and has no negative IG,
// so it does bid and win.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
AdditionalBidOnComponentAuctionWithNoNegativeIGDoesBid) {
AttachInterestGroupObserver();
URLLoaderMonitor url_loader_monitor;
constexpr char kTestOrigin[] = "a.test";
GURL test_url = embedded_https_test_server().GetURL(kTestOrigin,
"/page_with_iframe.html");
GURL ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_cars");
GURL additional_bid_ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_horses");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/url::Origin::Create(test_url),
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
kTestOrigin, "/interest_group/bidding_logic.js"))
.SetAds(/*ads=*/{{{ad_url, /*metadata=*/std::nullopt}}})
.Build()));
std::string auction_nonce = CreateAuctionNonceAndWait();
GURL additional_bid_logic_url = embedded_https_test_server().GetURL(
kTestOrigin, "/interest_group/bidding_logic_additional_bid.js");
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
// Signal to the top-level seller to allow participation in a component
// auction.
auctionSignals: "sellerAllowsComponentAuction",
componentAuctions: [{
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
// Signal to the bidder and component seller to allow participation in
// a component auction.
auctionSignals: "bidderAllowsComponentAuction,"+
"sellerAllowsComponentAuction",
auctionNonce: $3,
additionalBids: provideAdditionalBids($1, $3, [JSON.stringify({
interestGroup: {
name: 'campaign123',
biddingLogicURL: $5,
owner:$6
},
bid: {
ad: ['ad'],
bid: 1.99,
render: $4,
},
auctionNonce: $3,
seller: $1,
topLevelSeller: $1,
})])
}]
})",
url::Origin::Create(test_url),
embedded_https_test_server().GetURL(kTestOrigin,
"/interest_group/decision_logic.js"),
auction_nonce, additional_bid_ad_url, additional_bid_logic_url,
url::Origin::Create(additional_bid_logic_url));
RunAuctionAndWaitForURLAndNavigateIframe(auction_config,
additional_bid_ad_url);
WaitForUrl(embedded_https_test_server().GetURL(kTestOrigin,
"/echoall?report_seller"));
WaitForUrl(embedded_https_test_server().GetURL(
kTestOrigin, "/echoall?report_bidder_additional"));
EXPECT_FALSE(HasServerSeenUrl(embedded_https_test_server().GetURL(
kTestOrigin, "/echoall?report_bidder")));
url::Origin bid_origin = url::Origin::Create(test_url);
WaitForAccessObserved(
{{"global", TestInterestGroupObserver::kJoin, bid_origin, "cars"},
{"1", TestInterestGroupObserver::kLoaded, bid_origin, "cars"},
{"1", TestInterestGroupObserver::kAdditionalBid, bid_origin,
"campaign123", 1.99},
{"1", TestInterestGroupObserver::kBid, bid_origin, "cars", 1.0},
{"2", TestInterestGroupObserver::kTopLevelAdditionalBid, bid_origin,
"campaign123", 1.99, /*bid_currency=*/std::nullopt,
/*component_seller_origin=*/bid_origin},
{"2", TestInterestGroupObserver::kAdditionalBidWin, bid_origin,
"campaign123", /*bid=*/std::nullopt, /*bid_currency=*/std::nullopt,
/*component_seller_origin=*/bid_origin}});
}
// Test to make sure that Promises configuration fields that are checked early
// on don't actually end up running their handlers if a later config check
// fails.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionPromiseSideEffects) {
ASSERT_TRUE(NavigateToURL(
shell(), embedded_https_test_server().GetURL("a.test", "/echo")));
WebContentsConsoleObserver console_observer(shell()->web_contents());
console_observer.SetPattern("*I am a*");
EXPECT_EQ(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': "
"sellerCurrency 'currency is checked late' for AuctionAdConfig with "
"seller 'https://test.com' must be a 3-letter uppercase currency code.",
RunAuctionAndWait(R"({
seller: 'https://test.com',
decisionLogicURL: 'https://test.com',
sellerCurrency: 'currency is checked late',
ignored: setTimeout(() => {console.log('I am a timer'); }, 1),
perBuyerTimeouts: { 'https://test.com': {
valueOf: () => { console.log('I am a side effect!') }
}
}
})"));
EXPECT_TRUE(console_observer.Wait());
EXPECT_EQ("I am a timer", console_observer.GetMessageAt(0));
}
// Tests the order of events observed by the InterestGroupObserver. Those events
// are ultimately displayed in devtools, so want to provide them in a consistent
// order.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, NotificationOrder) {
GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
AttachInterestGroupObserver();
EXPECT_EQ(kSuccess, EvalJs(shell(), R"(
(async function() {
try {
// Join a pair of same-origin interest groups with no bidding script.
// Unlike other tests, don't wait for the join to complete
// before making more FLEDGE calls. The calls should still be
// executed in order, since they're all same-origin.
navigator.joinAdInterestGroup(
{name: 'cars', owner: document.location.origin},
/*joinDurationSec=*/300);
navigator.joinAdInterestGroup(
{name: 'golf carts', owner: document.location.origin},
/*joinDurationSec=*/300);
// This should clear the interest group joined second only.
// If this left multiple groups, there'd be no guarantee about
// the relative order of those two groups, so it deliberately
// leaves only one group.
navigator.clearOriginJoinedAdInterestGroups(
document.location.origin, ['cars'],
/*joinDurationSec=*/300);
// Run an auction. Don't bother using a real decision logic URL.
// Since the only interest group has no bidding script, it won't
// need to run.
navigator.runAdAuction({seller: document.location.origin,
decisionLogicURL: document.location.href,
interestGroupBuyers: [window.location.origin]});
navigator.leaveAdInterestGroup(
{name: 'cars', owner: document.location.origin});
// No need to wait for the promises to resolve - the test will
// wait for the right events to be observed instead.
return 'success';
} catch (e) {
return e.toString();
}
})())"));
// Expect events to be in order.
WaitForAccessObservedInOrder(
{{"global", TestInterestGroupObserver::kJoin, test_origin, "cars"},
{"global", TestInterestGroupObserver::kJoin, test_origin, "golf carts"},
{"global", TestInterestGroupObserver::kClear, test_origin, "golf carts"},
{"1", TestInterestGroupObserver::kLoaded, test_origin, "cars"},
{"global", TestInterestGroupObserver::kLeave, test_origin, "cars"}});
}
// Make sure we don't crash when cross-frame promise resolution tries to notify
// an auction in a detached frame.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, DetachedFramePromiseResolve) {
constexpr char kTestOrigin[] = "a.test";
GURL test_url = embedded_https_test_server().GetURL(kTestOrigin,
"/page_with_iframe.html");
GURL ad_url =
embedded_https_test_server().GetURL(kTestOrigin, "/echo?render_cars");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/url::Origin::Create(test_url),
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
kTestOrigin, "/interest_group/bidding_logic.js"))
.SetAds(/*ads=*/{{{ad_url, /*metadata=*/std::nullopt}}})
.Build()));
FrameTreeNode* parent =
FrameTreeNode::From(web_contents()->GetPrimaryMainFrame());
ASSERT_GT(parent->child_count(), 0u);
RenderFrameHost* iframe = parent->child_at(0)->current_frame_host();
const char kFrameScriptTemplate[] = R"(
let auctionSignalsPromise = new Promise((resolve, reject) => {
window.resolveFunc = () => {
resolve({a: "foo"});
}
});
const auctionConfig = {
seller: $1,
decisionLogicUrl: $2,
interestGroupBuyers: [$1],
auctionSignals: auctionSignalsPromise
}
navigator.runAdAuction(auctionConfig);
"ok";
)";
const char kTopLevelScript[] = R"(
let frame = document.getElementById("test_iframe");
let resolveFn = frame.contentWindow.resolveFunc;
frame.remove();
resolveFn();
)";
EXPECT_EQ(
"ok",
EvalJs(iframe,
JsReplace(kFrameScriptTemplate, url::Origin::Create(test_url),
embedded_https_test_server().GetURL(
kTestOrigin, "/interest_group/decision_logic.js"))));
EXPECT_EQ(base::Value(), EvalJs(shell(), kTopLevelScript));
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, FeatureDetection) {
constexpr double kDefaultMaxGroupLifetimeMs = base::Days(30).InMilliseconds();
// Since kFledgeCustomMaxAuctionAdComponents is rolling out, feature
// detection helper should be visible.
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/simple_page.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
ASSERT_EQ(true, EvalJs(shell(), "'protectedAudience' in navigator"));
// Current state of various features for the fixture's flags.
const char kQueryComponentLimit[] = R"(
navigator.protectedAudience.queryFeatureSupport(
'adComponentsLimit');
)";
const char kQueryUrlReplacements[] = R"(
navigator.protectedAudience.queryFeatureSupport(
'deprecatedRenderURLReplacements');
)";
const char kQueryReportingTimeout[] = R"(
navigator.protectedAudience.queryFeatureSupport(
'reportingTimeout');
)";
const char kQueryCrossOriginTrustedSignals[] = R"(
navigator.protectedAudience.queryFeatureSupport(
'permitCrossOriginTrustedSignals');
)";
const char kQueryRealTimeReporting[] = R"(
navigator.protectedAudience.queryFeatureSupport(
'realTimeReporting');
)";
const char kQuerySelectableReportingIds[] = R"(
navigator.protectedAudience.queryFeatureSupport(
'selectableReportingIds');
)";
const char kQuerySellerNonce[] = R"(
navigator.protectedAudience.queryFeatureSupport(
'sellerNonce');
)";
const char kQueryTrustedSignalsKVv2Support[] = R"(
navigator.protectedAudience.queryFeatureSupport(
'trustedSignalsKVv2');
)";
const char kQueryMaxGroupLifetimeMs[] = R"(
navigator.protectedAudience.queryFeatureSupport(
'maxGroupLifetimeMs');
)";
const char kQueryAll[] = R"(
navigator.protectedAudience.queryFeatureSupport('*');
)";
EXPECT_EQ(40, EvalJs(shell(), kQueryComponentLimit));
EXPECT_EQ(true, EvalJs(shell(), kQueryUrlReplacements));
EXPECT_EQ(true, EvalJs(shell(), kQueryReportingTimeout));
EXPECT_EQ(true, EvalJs(shell(), kQuerySelectableReportingIds));
EXPECT_EQ(true, EvalJs(shell(), kQueryTrustedSignalsKVv2Support));
EXPECT_EQ(true, EvalJs(shell(), kQueryCrossOriginTrustedSignals));
EXPECT_EQ(false, EvalJs(shell(), kQueryRealTimeReporting));
EXPECT_EQ(true, EvalJs(shell(), kQuerySellerNonce));
EXPECT_EQ(kDefaultMaxGroupLifetimeMs,
EvalJs(shell(), kQueryMaxGroupLifetimeMs));
auto all_result = EvalJs(shell(), kQueryAll);
EXPECT_THAT(all_result,
EvalJsResult::IsOkAndHolds(base::test::IsJson(base::StringPrintf(
R"({
"adComponentsLimit": 40,
"deprecatedRenderURLReplacements": true,
"permitCrossOriginTrustedSignals": true,
"realTimeReporting": false,
"reportingTimeout": true,
"selectableReportingIds": true,
"sellerNonce": true,
"trustedSignalsKVv2": true,
"maxGroupLifetimeMs": %f
})",
kDefaultMaxGroupLifetimeMs))))
<< all_result;
}
// Worklet handling of zero seller timeout.
// See https://crbug.com/325302199
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, ZeroSellerTimeout) {
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_cars");
WebContentsConsoleObserver console_observer(shell()->web_contents());
console_observer.SetPattern("*Worklet error*");
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"))
.SetAds(/*ads=*/{{{ad_url, "null"}}})
.Build()));
const char kConfigTemplate[] = R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
sellerTimeout: 0,
})";
std::string auction_config =
JsReplace(kConfigTemplate, test_origin,
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js"));
EXPECT_EQ(base::Value(), RunAuctionAndWait(auction_config));
ASSERT_TRUE(console_observer.Wait());
// We should get a nice error, not a worklet crash.
EXPECT_EQ("Worklet error: scoreAd() aborted due to zero timeout.",
console_observer.GetMessageAt(0));
}
// Worklet handling of zero buyer timeout. This actually seems to have worked
// correctly for a very long time.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, ZeroBuyerTimeout) {
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_cars");
WebContentsConsoleObserver console_observer(shell()->web_contents());
console_observer.SetPattern("*Worklet error*");
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"))
.SetAds(/*ads=*/{{{ad_url, "null"}}})
.Build()));
const char kConfigTemplate[] = R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
perBuyerTimeouts: {'*': 0},
})";
std::string auction_config =
JsReplace(kConfigTemplate, test_origin,
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js"));
EXPECT_EQ(base::Value(), RunAuctionAndWait(auction_config));
ASSERT_TRUE(console_observer.Wait());
// We should get a nice warning, not a worklet crash.
EXPECT_EQ("Worklet error: generateBid() aborted due to zero timeout.",
console_observer.GetMessageAt(0));
}
// Cumulative timeout with lots of IGs while tracing is on,
// see https://crbug.com/336342803
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, RunAdAuctionTraceTimeout) {
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_cars");
// Add a lot of interest groups, so that "worklet available" notifications get
// split up.
for (int i = 0; i < 100; ++i) {
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/base::NumberToString(i))
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"))
.SetAds(/*ads=*/{{{ad_url, /*metadata=*/std::nullopt}}})
.Build()));
}
const char kAuctionConfigTemplate[] = R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
perBuyerCumulativeTimeouts: {'*': 1}
})";
// Start tracing "fledge" category.
base::trace_event::TraceConfig trace_config(
"fledge", base::trace_event::RECORD_UNTIL_FULL);
base::RunLoop run_loop;
ASSERT_TRUE(TracingController::GetInstance()->StartTracing(
trace_config, run_loop.QuitClosure()));
run_loop.Run();
// Run the auction.
std::string auction_config =
JsReplace(kAuctionConfigTemplate, test_origin,
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js"));
// Whether there is a winner or not is actually not predictable, since
// something could finish in the 1ms window. That's OK since this we only care
// about whether this crashes or not.
content::EvalJsResult result = RunAuctionAndWait(auction_config);
// Stop tracing.
base::RunLoop stop_run_loop;
bool success = TracingController::GetInstance()->StopTracing(
TracingController::CreateStringEndpoint(base::BindLambdaForTesting(
[&](std::unique_ptr<std::string> trace_data_string) {
stop_run_loop.Quit();
})));
EXPECT_TRUE(success);
stop_run_loop.Run();
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
ReportingTimeoutPassedToWorklets) {
const char kHostA[] = "a.test";
const char kHostB[] = "b.test";
GURL test_url =
embedded_https_test_server().GetURL(kHostA, "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
url::Origin test_origin_b =
url::Origin::Create(embedded_https_test_server().GetURL(kHostB, "/echo"));
GURL ad_url =
embedded_https_test_server().GetURL(kHostA, "/echo?render_cars");
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
/*owner=*/test_origin,
/*name=*/"cars",
/*priority=*/0.0,
/*execution_mode=*/
blink::InterestGroup::ExecutionMode::kCompatibilityMode,
/*bidding_url=*/
embedded_https_test_server().GetURL(
kHostA, "/interest_group/report_win_reporting_timeout_value.js"),
/*ads=*/{{{ad_url, /*metadata=*/std::nullopt}}}));
const char kConfigTemplate[] = R"({
seller: $1,
decisionLogicURL: $2,
reportingTimeout: 3000,
componentAuctions: [{
seller: $3,
decisionLogicURL: $4,
interestGroupBuyers: [$1],
reportingTimeout: 2000,
}]
})";
std::string auction_config = JsReplace(
kConfigTemplate, test_origin,
embedded_https_test_server().GetURL(
kHostA, "/interest_group/report_result_reporting_timeout_value.js"),
test_origin_b,
embedded_https_test_server().GetURL(
kHostB, "/interest_group/report_result_reporting_timeout_value.js"));
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad_url);
// All report functions succeeded as expected, and reportResult() worklets get
// expected auctionConfig.reportingTimeout.
WaitForUrl(embedded_https_test_server().GetURL(
kHostA, "/echoall?report_bidder,reportingTimeout=2000"));
WaitForUrl(embedded_https_test_server().GetURL(
kHostA, "/echoall?report_seller,reportingTimeout=3000"));
WaitForUrl(embedded_https_test_server().GetURL(
kHostB, "/echoall?report_seller,reportingTimeout=2000"));
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
ReportResultTimedOutWithCustomReportingTimeout) {
const char kHostA[] = "a.test";
GURL test_url =
embedded_https_test_server().GetURL(kHostA, "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url =
embedded_https_test_server().GetURL(kHostA, "/echo?render_cars");
WebContentsConsoleObserver console_observer(shell()->web_contents());
console_observer.SetPattern(
"Worklet error: https://a.test:* execution of `reportResult` timed out.");
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
/*owner=*/test_origin,
/*name=*/"cars",
/*priority=*/0.0,
/*execution_mode=*/
blink::InterestGroup::ExecutionMode::kCompatibilityMode,
/*bidding_url=*/
embedded_https_test_server().GetURL(
kHostA, "/interest_group/bidding_logic.js"),
/*ads=*/{{{ad_url, /*metadata=*/std::nullopt}}}));
const char kConfigTemplate[] = R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
reportingTimeout: 500,
})";
std::string auction_config =
JsReplace(kConfigTemplate, test_origin,
embedded_https_test_server().GetURL(
kHostA, "/interest_group/report_result_loop_forever.js"));
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad_url);
// reportWin()'s report should be sent still since it didn't timeout.
WaitForUrl(
embedded_https_test_server().GetURL(kHostA, "/echoall?report_bidder"));
// reportResult() timed out.
EXPECT_TRUE(console_observer.Wait());
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
ReportingTimeoutTopLevelNotAffectComponentAuction) {
const char kHostA[] = "a.test";
const char kHostB[] = "b.test";
const char decisionLogicJs[] =
"/interest_group/report_result_reporting_timeout_value.js";
GURL test_url =
embedded_https_test_server().GetURL(kHostA, "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
url::Origin test_origin_b =
url::Origin::Create(embedded_https_test_server().GetURL(kHostB, "/echo"));
GURL ad_url =
embedded_https_test_server().GetURL(kHostA, "/echo?render_cars");
WebContentsConsoleObserver console_observer(shell()->web_contents());
console_observer.SetPattern("*Worklet error*");
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
/*owner=*/test_origin,
/*name=*/"cars",
/*priority=*/0.0,
/*execution_mode=*/
blink::InterestGroup::ExecutionMode::kCompatibilityMode,
/*bidding_url=*/
embedded_https_test_server().GetURL(
kHostA, "/interest_group/report_win_reporting_timeout_value.js"),
/*ads=*/{{{ad_url, /*metadata=*/std::nullopt}}}));
const char kConfigTemplate[] = R"({
seller: $1,
decisionLogicURL: $2,
reportingTimeout: 0,
componentAuctions: [{
seller: $3,
decisionLogicURL: $4,
interestGroupBuyers: [$1],
reportingTimeout: 2000,
}]
})";
std::string auction_config =
JsReplace(kConfigTemplate, test_origin,
embedded_https_test_server().GetURL(kHostA, decisionLogicJs),
test_origin_b,
embedded_https_test_server().GetURL(kHostB, decisionLogicJs));
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad_url);
ASSERT_TRUE(console_observer.Wait());
// Top level reportResult() aborted due to 0 timeout.
EXPECT_EQ("Worklet error: reportResult() aborted due to zero timeout.",
console_observer.GetMessageAt(0));
// Component auction's reportResult() and reportWin() both succeeded, and get
// reporting timeouts from component auction.
WaitForUrl(embedded_https_test_server().GetURL(
kHostB, "/echoall?report_seller,reportingTimeout=2000"));
WaitForUrl(embedded_https_test_server().GetURL(
kHostA, "/echoall?report_bidder,reportingTimeout=2000"));
}
// Worklet handling of zero reporting timeout.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, ZeroReportingTimeout) {
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_cars");
WebContentsConsoleObserver console_observer(shell()->web_contents());
console_observer.SetPattern("*Worklet error*");
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/report_win_loop_forever.js"))
.SetAds(/*ads=*/{{{ad_url, "null"}}})
.Build()));
const char kConfigTemplate[] = R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
reportingTimeout: 0,
})";
std::string auction_config =
JsReplace(kConfigTemplate, test_origin,
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js"));
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad_url);
ASSERT_TRUE(console_observer.Wait());
// We should get nice errors, not worklet crashes.
EXPECT_EQ(2u, console_observer.messages().size());
EXPECT_EQ("Worklet error: reportResult() aborted due to zero timeout.",
console_observer.GetMessageAt(0));
EXPECT_EQ("Worklet error: reportWin() aborted due to zero timeout.",
console_observer.GetMessageAt(1));
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
ZeroComponentReportingTimeout) {
const char kHostA[] = "a.test";
const char kHostB[] = "b.test";
GURL test_url =
embedded_https_test_server().GetURL(kHostA, "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
url::Origin test_origin_b =
url::Origin::Create(embedded_https_test_server().GetURL(kHostB, "/echo"));
GURL ad_url =
embedded_https_test_server().GetURL(kHostA, "/echo?render_cars");
WebContentsConsoleObserver console_observer(shell()->web_contents());
console_observer.SetPattern("*Worklet error*");
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
/*owner=*/test_origin,
/*name=*/"cars",
/*priority=*/0.0,
/*execution_mode=*/
blink::InterestGroup::ExecutionMode::kCompatibilityMode,
/*bidding_url=*/
embedded_https_test_server().GetURL(
kHostA, "/interest_group/report_win_loop_forever.js"),
/*ads=*/{{{ad_url, /*metadata=*/std::nullopt}}}));
const char kConfigTemplate[] = R"({
seller: $1,
decisionLogicURL: $2,
auctionSignals: "sellerAllowsComponentAuction",
reportingTimeout: 500,
componentAuctions: [{
seller: $3,
decisionLogicURL: $4,
interestGroupBuyers: [$1],
reportingTimeout: 0,
}]
})";
std::string auction_config =
JsReplace(kConfigTemplate, test_origin,
embedded_https_test_server().GetURL(
kHostA, "/interest_group/decision_logic.js"),
test_origin_b,
embedded_https_test_server().GetURL(
kHostB, "/interest_group/report_result_loop_forever.js"));
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad_url);
ASSERT_TRUE(console_observer.Wait());
// Component auction's reportResult() and reportWin() aborted due to 0
// timeout.
EXPECT_EQ("Worklet error: reportResult() aborted due to zero timeout.",
console_observer.GetMessageAt(0));
EXPECT_EQ("Worklet error: reportWin() aborted due to zero timeout.",
console_observer.GetMessageAt(1));
// Top level's reportResult() does not timeout.
WaitForUrl(
embedded_https_test_server().GetURL(kHostA, "/echoall?report_seller"));
}
class UsesAnticipatoryProcessesTest : public InterestGroupBrowserTest {
public:
UsesAnticipatoryProcessesTest() {
feature_list_.InitAndEnableFeatureWithParameters(
features::kFledgeStartAnticipatoryProcesses,
{{"AnticipatoryProcessHoldTime", "10s"}});
// Set up the conditions so that Android will have desktop-like behavior for
// process creation.
scoped_command_line_.GetProcessCommandLine()->AppendSwitch(
switches::kSitePerProcess);
}
private:
base::test::ScopedFeatureList feature_list_;
base::test::ScopedCommandLine scoped_command_line_;
};
IN_PROC_BROWSER_TEST_F(UsesAnticipatoryProcessesTest,
UsesAnticipatoryProcesses) {
GURL joining_url = embedded_https_test_server().GetURL("c.test", "/echo");
url::Origin joining_origin = url::Origin::Create(joining_url);
ASSERT_TRUE(NavigateToURL(shell(), joining_url));
GURL ad_url =
embedded_https_test_server().GetURL("a.test", "/echo?render_winner");
// Use a bidding script that does not bid to prevent the auction from having a
// winner. If the auction has a winner, there will be a race condition where a
// a worklet will be created for reporting.
GURL script_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic_do_not_bid.js");
blink::InterestGroup interest_group =
blink::TestInterestGroupBuilder(
/*owner=*/url::Origin::Create(script_url),
/*name=*/"interest_group")
.SetBiddingUrl(script_url)
.SetAds({{{ad_url, std::nullopt}}})
.Build();
AttachInterestGroupObserver();
manager_->JoinInterestGroup(interest_group, joining_url);
WaitForAccessObserved({{"global", TestInterestGroupObserver::kJoin,
interest_group.owner, "interest_group"}});
std::optional<url::Origin> cached_signals_origin;
const char kConfigTemplate[] = R"({
seller: $1,
decisionLogicURL: $2,
auctionSignals: "bidderAllowsComponentAuction,"+
"sellerAllowsComponentAuction",
componentAuctions:
[{
seller: $3,
decisionLogicURL: $4,
interestGroupBuyers: [$5, $6],
auctionSignals: "bidderAllowsComponentAuction,"+
"sellerAllowsComponentAuction"
}]
})";
GURL top_level_seller_script = embedded_https_test_server().GetURL(
"c.test", "/interest_group/decision_logic.js");
GURL component_seller_script = embedded_https_test_server().GetURL(
"d.test", "/interest_group/decision_logic.js");
content_browser_client_->AddToAllowList(
{url::Origin::Create(component_seller_script)});
std::string auction_config = JsReplace(
kConfigTemplate, url::Origin::Create(top_level_seller_script),
top_level_seller_script, url::Origin::Create(component_seller_script),
component_seller_script, interest_group.owner,
url::Origin::Create(embedded_https_test_server().GetURL("b.test", "/")));
// Run an auction so that a.test will be cached.
{
base::HistogramTester histogram_tester;
auto result = RunAuctionAndWait(auction_config);
histogram_tester.ExpectUniqueSample("Ads.InterestGroup.Auction.Result2",
AuctionResult::kNoBids, 1);
histogram_tester.ExpectUniqueSample(
"Ads.InterestGroup.Auction.Seller.RequestWorkletServiceOutcome",
AuctionProcessManager::RequestWorkletServiceOutcome::
kCreatedNewDedicatedProcess,
2);
histogram_tester.ExpectUniqueSample(
"Ads.InterestGroup.Auction.Buyer.RequestWorkletServiceOutcome",
AuctionProcessManager::RequestWorkletServiceOutcome::
kCreatedNewDedicatedProcess,
1);
EXPECT_TRUE(manager_->GetCachedOwnerAndSignalsOrigins(
interest_group.owner, cached_signals_origin));
}
// Run the auction.
//
// 1. a.test, the first buyer, is cached. That means we should start an
// anticipatory process for it.
// 2. b.test, the second buyer, doesn't have any IGs. We won't request an
// anticipatory process or create a worklet for it.
// 3. c.test and d.test, will not have any anticipatory process
// started them it, because we only start anticipatory processes for buyers.
// Worklets will still be requested for them.
//
// Overall there will be 1 anticipatory process started and 3 worklets
// requested.
{
base::HistogramTester histogram_tester;
auto result = RunAuctionAndWait(auction_config);
histogram_tester.ExpectUniqueSample("Ads.InterestGroup.Auction.Result2",
AuctionResult::kNoBids, 1);
histogram_tester.ExpectUniqueSample(
"Ads.InterestGroup.Auction.Seller.RequestWorkletServiceOutcome",
AuctionProcessManager::RequestWorkletServiceOutcome::
kCreatedNewDedicatedProcess,
2);
histogram_tester.ExpectUniqueSample(
"Ads.InterestGroup.Auction.Buyer.RequestWorkletServiceOutcome",
AuctionProcessManager::RequestWorkletServiceOutcome::kUsedIdleProcess,
1);
}
}
class InterestGroupBFCacheBrowserTest : public InterestGroupBrowserTest {
public:
InterestGroupBFCacheBrowserTest() {
feature_list_.InitAndEnableFeature(features::kBackForwardCache);
}
private:
base::test::ScopedFeatureList feature_list_;
};
// When there is a cross-document navigation and the original page goes into the
// bfcache, the weak pointer to the auction initiator page should not be reset.
IN_PROC_BROWSER_TEST_F(
InterestGroupBFCacheBrowserTest,
CrossDocumentNavigationShouldNotResetAuctionInitiatorPageInBFCache) {
const GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
const url::Origin test_origin = url::Origin::Create(test_url);
RenderFrameHostImplWrapper main_frame(web_contents()->GetPrimaryMainFrame());
// Before first Protected Audience API call, the auction initiator page should
// not be set.
ASSERT_EQ(main_frame->auction_initiator_page(), nullptr);
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"))
.SetAds({{{GURL("https://example.com/render"),
R"({"ad":"metadata","here":[1,2]})"}}})
.Build()));
base::WeakPtr<PageImpl> auction_initiator_page =
main_frame->auction_initiator_page();
ASSERT_NE(auction_initiator_page, nullptr);
EXPECT_EQ(auction_initiator_page.get(), &(main_frame->GetPage()));
// Navigate, the main frame is put into bfcache.
const GURL test_url_b =
embedded_https_test_server().GetURL("b.test", "/echo");
ASSERT_TRUE(NavigateToURL(shell(), test_url_b));
ASSERT_TRUE(main_frame->IsInBackForwardCache());
// Navigate back.
TestNavigationObserver back_load_observer(shell()->web_contents());
shell()->web_contents()->GetController().GoBack();
back_load_observer.Wait();
// The auction initiator page should not be reset since the original page goes
// into the bfcache.
ASSERT_NE(main_frame->auction_initiator_page(), nullptr);
EXPECT_EQ(main_frame->auction_initiator_page().get(),
&(main_frame->GetPage()));
EXPECT_EQ(main_frame->auction_initiator_page().get(),
auction_initiator_page.get());
}
class InterestGroupAdComponentLimitBrowserTest
: public InterestGroupBrowserTest {
public:
InterestGroupAdComponentLimitBrowserTest() {
const std::map<std::string, std::string> params = {
{"FledgeAdComponentLimit", "45"}};
feature_list_.InitAndEnableFeatureWithParameters(
blink::features::kFledgeCustomMaxAuctionAdComponents, params);
}
~InterestGroupAdComponentLimitBrowserTest() override = default;
protected:
base::test::ScopedFeatureList feature_list_;
};
IN_PROC_BROWSER_TEST_F(InterestGroupAdComponentLimitBrowserTest,
FeatureDetection) {
const char kTestExpression[] = R"(
navigator.protectedAudience.queryFeatureSupport(
'adComponentsLimit');
)";
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/simple_page.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
EXPECT_EQ(45, EvalJs(shell(), kTestExpression));
}
class InterestGroupOOPIFBrowserTest : public InterestGroupBrowserTest {
public:
void SetUpCommandLine(base::CommandLine* command_line) override {
IsolateAllSitesForTesting(command_line);
InterestGroupBrowserTest::SetUpCommandLine(command_line);
}
};
// Test to make sure we don't crash when Page changes with DFSS ad slot pending.
// https://crbug.com/326085515
IN_PROC_BROWSER_TEST_F(InterestGroupOOPIFBrowserTest,
PageImplChangeDirectFromSellerSignals) {
GURL initial_url(embedded_https_test_server().GetURL(
"a.test",
"/cross_site_iframe_factory.html?a.test(b.test{allow-join-ad-interest-"
"group;run-ad-auction})"));
GURL next_url(
embedded_https_test_server().GetURL("login.a.test", "/title1.html"));
url::Origin frame_origin =
url::Origin::Create(embedded_https_test_server().GetURL("b.test", "/"));
// 1) Navigate on a page with an OOPIF.
EXPECT_TRUE(NavigateToURL(shell(), initial_url));
FrameTreeNode* root_ftn = web_contents()->GetPrimaryFrameTree().root();
RenderFrameHostImpl* child_rfh = root_ftn->child_at(0)->current_frame_host();
// Join an interest group on the iframe.
blink::InterestGroup interest_group =
blink::TestInterestGroupBuilder(frame_origin, "cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
frame_origin.GetURL().host(), "/interest_group/bidding_logic.js"))
.SetAds({{{GURL("https://example.com/render"), "null"}}})
.Build();
EXPECT_EQ(kSuccess, JoinInterestGroupAndVerify(interest_group, child_rfh));
// 2) Act as if there was an infinite unload handler in the OOPIF.
child_rfh->DoNotDeleteForTesting();
// Set an arbitrarily long timeout to ensure the subframe unload timer doesn't
// fire before we call OnDetach().
child_rfh->SetSubframeUnloadTimeoutForTesting(base::Seconds(30));
// With BackForwardCache, old document doesn't fire unload handlers as the
// page is stored in BackForwardCache on navigation.
DisableBackForwardCacheForTesting(web_contents(),
BackForwardCache::TEST_USES_UNLOAD_EVENT);
// 3) Start an auction in the child frame. This is done by hand here so the
// test can easily produce an RPC with bad timing, rather than trying to get
// JS on something that's being shut down produce this effect.
blink::AuctionConfig auction_config;
GURL seller_url = embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js");
auction_config.seller = url::Origin::Create(seller_url);
auction_config.decision_logic_url = seller_url;
auction_config.non_shared_params.interest_group_buyers = {frame_origin};
auction_config.expects_direct_from_seller_signals_header_ad_slot = true;
mojo::Remote<blink::mojom::AdAuctionService> ad_auction_service;
mojo::Remote<blink::mojom::AbortableAdAuction> abortable_auction;
AdAuctionServiceImpl::CreateMojoService(
child_rfh, ad_auction_service.BindNewPipeAndPassReceiver());
base::RunLoop run_loop;
std::optional<blink::FencedFrame::RedactedFencedFrameConfig> maybe_config;
ad_auction_service->RunAdAuction(
auction_config, abortable_auction.BindNewPipeAndPassReceiver(),
base::BindLambdaForTesting(
[&run_loop, &maybe_config](
bool aborted_by_script,
const std::optional<
blink::FencedFrame::RedactedFencedFrameConfig>& config) {
EXPECT_FALSE(aborted_by_script);
maybe_config = config;
run_loop.Quit();
}));
// Progress to as far as it can get without the DFSS promise being resolved.
base::RunLoop().RunUntilIdle();
EXPECT_FALSE(run_loop.AnyQuitCalled());
// 4) Navigate the main frame to a same-site url. The unload handler of the
// OOPIF is running.
EXPECT_TRUE(NavigateToURL(shell(), next_url));
EXPECT_TRUE(child_rfh->IsPendingDeletion());
// 5) Complete the auction, which should fail cleanly.
abortable_auction->ResolvedDirectFromSellerSignalsHeaderAdSlotPromise(
blink::mojom::AuctionAdConfigAuctionId::NewMainAuction(0), "adSlot1");
run_loop.Run();
if (SiteIsolationPolicy::AreOriginKeyedProcessesEnabledByDefault()) {
// With Origin Isolation enabled, the cross-origin, same-site navigation
// ends up being cross-process, so a config value is expected.
EXPECT_TRUE(maybe_config.has_value());
} else {
// Without Origin Isolation enabled, whether on not there is a config value
// depends on the RenderDocument status.
EXPECT_EQ(maybe_config.has_value(),
root_ftn->current_frame_host()
->ShouldChangeRenderFrameHostOnSameSiteNavigation());
}
}
class InterestGroupCrossOriginTrustedSignalsBrowserTest
: public InterestGroupBrowserTest {
public:
InterestGroupCrossOriginTrustedSignalsBrowserTest() = default;
~InterestGroupCrossOriginTrustedSignalsBrowserTest() override = default;
void TestTrustedSellerSignals(bool expect_success,
bool add_cors_header,
bool add_script_header,
bool attest_signals_origin);
// Tries to run an auction which uses cross-site trusted bidding signals.
// `add_cors_header` controls whether the signals JSON has an appropriate
// Access-Control-Allow-Origin.
//
// `attest_signals_origin` controls whether content_browser_client
// permits the origin trusted signals or not.
//
// `expect_success` controls whether the test expects the auction to succeed
// or not.
void TestTrustedBidderSignals(bool expect_success,
bool add_cors_header,
bool attest_signals_origin);
protected:
base::test::ScopedFeatureList feature_list_;
};
void InterestGroupCrossOriginTrustedSignalsBrowserTest::
TestTrustedSellerSignals(bool expect_success,
bool add_cors_header,
bool add_script_header,
bool attest_signals_origin) {
// This test helper cares about kSellerSignals and kSeller being cross-origin.
// Whether other origins are the same or different does not matter.
const char kPublisher[] = "a.test";
const char kBidder[] = "b.test";
const char kSeller[] = "c.test";
const char kSellerSignals[] = "trusted-signals.d.test";
GURL seller_signals_url = embedded_https_test_server().GetURL(
kSellerSignals, "/trusted_scoring_signals.json");
if (attest_signals_origin) {
content_browser_client_->AddToAllowList(
{url::Origin::Create(seller_signals_url)});
}
GURL ad_url = embedded_https_test_server().GetURL(
kBidder, "/set-header?Supports-Loading-Mode: fenced-frame");
// Navigate to bidder site, and add an interest group.
GURL bidder_url = embedded_https_test_server().GetURL(kBidder, "/echo");
ASSERT_TRUE(NavigateToURL(shell(), bidder_url));
url::Origin bidder_origin = url::Origin::Create(bidder_url);
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/bidder_origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
kBidder, "/interest_group/bidding_logic.js"))
.SetAds({{{ad_url, /*metadata=*/std::nullopt}}})
.Build()));
// Navigate to publisher.
ASSERT_TRUE(
NavigateToURL(shell(), embedded_https_test_server().GetURL(
kPublisher, "/page_with_iframe.html")));
WebContentsConsoleObserver console_observer(shell()->web_contents());
if (!attest_signals_origin) {
console_observer.SetPattern(
"Worklet error: runAdAuction() auction with seller 'https://c.test:*' "
"failed because it lacks attestation of cross-origin trusted signals "
"origin 'https://trusted-signals.d.test:*' or that origin is "
"disallowed by user preferences");
}
GURL seller_logic_url = embedded_https_test_server().GetURL(
kSeller, "/interest_group/decision_logic_need_signals.js");
// Register a seller script that only bids if
// `crossOriginTrustedScoringSignals` are successfully fetched.
NetworkResponder::ResponseHeaders extra_js_headers;
if (add_script_header) {
extra_js_headers.emplace_back(
std::string("Ad-Auction-Allow-Trusted-Scoring-Signals-From"),
base::StringPrintf(
"\"%s\"",
url::Origin::Create(seller_signals_url).Serialize().c_str()));
}
const char kSellerScriptTemplate[] = R"(
function scoreAd(
adMetadata, bid, auctionConfig, trustedScoringSignals, browserSignals,
directFromSellerSignals, crossOriginTrustedScoringSignals) {
if (trustedScoringSignals !== null) {
return 0;
}
if (browserSignals.crossOriginDataVersion !== 25) {
return 0;
}
if ('dataVersion' in browserSignals) {
return 0;
}
if (crossOriginTrustedScoringSignals[$1].renderURL[
browserSignals.renderURL] === "foo") {
return bid;
}
return 0;
}
)";
network_responder_->RegisterNetworkResponse(
seller_logic_url.path(),
JsReplace(kSellerScriptTemplate, url::Origin::Create(seller_signals_url)),
"application/javascript", std::move(extra_js_headers));
// Register seller signals with a value for `ad_url`, and a CORS header
// permitting script to access it.
NetworkResponder::ResponseHeaders extra_json_headers;
extra_json_headers.emplace_back("Data-Version", "25");
if (add_cors_header) {
extra_json_headers.emplace_back(
std::string("Access-Control-Allow-Origin"),
url::Origin::Create(seller_logic_url).Serialize());
}
network_responder_->RegisterNetworkResponse(
seller_signals_url.path(),
base::StringPrintf(R"({"renderUrls": {"%s": "foo"}})",
ad_url.spec().c_str()),
"application/json", std::move(extra_json_headers));
// Run an auction with the scoring script. It should succeed.
const char kConfigTemplate[] = R"({
seller: $1,
decisionLogicURL: $2,
trustedScoringSignalsURL: $3,
interestGroupBuyers: [$4],
})";
std::string config =
JsReplace(kConfigTemplate, url::Origin::Create(seller_logic_url),
seller_logic_url, seller_signals_url, bidder_origin);
// Should succeed with CORS header, fail without.
if (expect_success) {
EXPECT_EQ(ad_url, RunAuctionAndWaitForUrl(config));
} else {
EXPECT_EQ(base::Value(), RunAuctionAndWait(config));
}
if (!attest_signals_origin) {
EXPECT_TRUE(console_observer.Wait());
}
}
void InterestGroupCrossOriginTrustedSignalsBrowserTest::
TestTrustedBidderSignals(bool expect_success,
bool add_cors_header,
bool attest_signals_origin) {
// This test helper cares about kBidder and kBidderSignals being cross-origin.
// Whether other origins are the same or different does not matter.
const char kPublisher[] = "a.test";
const char kBidder[] = "b.test";
const char kSeller[] = "c.test";
const char kBidderSignals[] = "trusted-signals.d.test";
GURL bidder_script_url = embedded_https_test_server().GetURL(
kBidder, "/interest_group/cotbs_bidding_logic.js");
GURL bidder_signals_url = embedded_https_test_server().GetURL(
kBidderSignals, "/trusted_bidding_signals.json");
if (attest_signals_origin) {
content_browser_client_->AddToAllowList(
{url::Origin::Create(bidder_signals_url)});
}
GURL ad_url = embedded_https_test_server().GetURL(
kBidder, "/set-header?Supports-Loading-Mode: fenced-frame");
// Navigate to bidder site, and add an interest group.
GURL bidder_url = embedded_https_test_server().GetURL(kBidder, "/echo");
ASSERT_TRUE(NavigateToURL(shell(), bidder_url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
if (!attest_signals_origin) {
console_observer.SetPattern(
"joinAdInterestGroup of interest group with owner 'https://b.test:*' "
"blocked because it lacks attestation of cross-origin trusted signals "
"origin 'https://trusted-signals.d.test:*' or that origin is "
"disallowed by user preferences");
}
url::Origin bidder_origin = url::Origin::Create(bidder_url);
auto ig = blink::TestInterestGroupBuilder(
/*owner=*/bidder_origin,
/*name=*/"cars")
.SetBiddingUrl(bidder_script_url)
.SetAds({{{ad_url, /*metadata=*/std::nullopt}}})
.SetTrustedBiddingSignalsUrl(bidder_signals_url)
.SetTrustedBiddingSignalsKeys({{"key1"}})
.Build();
if (attest_signals_origin) {
EXPECT_EQ(kSuccess, JoinInterestGroupAndVerify(ig));
} else {
// Can't actually tell the join failed, but it won't verify.
EXPECT_EQ(kSuccess, JoinInterestGroup(ig));
EXPECT_TRUE(console_observer.Wait());
// Auction will fail w/o an IG available.
}
// Navigate to publisher.
ASSERT_TRUE(
NavigateToURL(shell(), embedded_https_test_server().GetURL(
kPublisher, "/page_with_iframe.html")));
// Register a bidder script that only bids if
// `crossOriginTrustedBiddingSignals` are successfully fetched.
const char kBidScriptTemplate[] = R"(
function generateBid(
interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals,
browserSignals, directFromSellerSignals,
crossOriginTrustedBiddingSignals) {
if (trustedBiddingSignals !== null) {
return {};
}
if (browserSignals.crossOriginDataVersion !== 23) {
return {};
}
if ('dataVersion' in browserSignals) {
return {};
}
if (crossOriginTrustedBiddingSignals[$1].key1 != '1') {
return {};
}
return {
bid: 1,
render: interestGroup.ads[0].renderURL,
};
})";
network_responder_->RegisterNetworkResponse(
bidder_script_url.path(),
JsReplace(kBidScriptTemplate, url::Origin::Create(bidder_signals_url)),
"application/javascript");
// Register bidder signals with a value for `key1`, and, if configured,
// a CORS header permitting script to access it.
NetworkResponder::ResponseHeaders extra_json_headers;
extra_json_headers.emplace_back("Data-Version", "23");
extra_json_headers.emplace_back("Ad-Auction-Bidding-Signals-Format-Version",
"2");
const char kJson[] = R"(
{ "keys": { "key1": "1" } }
)";
if (add_cors_header) {
extra_json_headers.emplace_back(
std::string("Access-Control-Allow-Origin"),
url::Origin::Create(bidder_script_url).Serialize());
}
network_responder_->RegisterNetworkResponse(bidder_signals_url.path(), kJson,
"application/json",
std::move(extra_json_headers));
GURL seller_logic_url = embedded_https_test_server().GetURL(
kSeller, "/interest_group/decision_logic.js");
const char kConfigTemplate[] = R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$3],
})";
std::string config =
JsReplace(kConfigTemplate, url::Origin::Create(seller_logic_url),
seller_logic_url, bidder_origin);
if (expect_success) {
EXPECT_EQ(ad_url, RunAuctionAndWaitForUrl(config));
} else {
EXPECT_EQ(base::Value(), RunAuctionAndWait(config));
}
}
IN_PROC_BROWSER_TEST_F(InterestGroupCrossOriginTrustedSignalsBrowserTest,
SellerSignalsPermitted) {
TestTrustedSellerSignals(/*expect_success=*/true, /*add_cors_header=*/true,
/*add_script_header=*/true,
/*attest_signals_origin=*/true);
}
IN_PROC_BROWSER_TEST_F(InterestGroupCrossOriginTrustedSignalsBrowserTest,
SellerSignalsNoCors) {
TestTrustedSellerSignals(/*expect_success=*/false, /*add_cors_header=*/false,
/*add_script_header=*/true,
/*attest_signals_origin=*/true);
}
IN_PROC_BROWSER_TEST_F(InterestGroupCrossOriginTrustedSignalsBrowserTest,
SellerSignalsNoScriptHeader) {
TestTrustedSellerSignals(/*expect_success=*/false, /*add_cors_header=*/true,
/*add_script_header=*/false,
/*attest_signals_origin=*/true);
}
IN_PROC_BROWSER_TEST_F(InterestGroupCrossOriginTrustedSignalsBrowserTest,
SellerSignalsNoAttestation) {
TestTrustedSellerSignals(/*expect_success=*/false, /*add_cors_header=*/true,
/*add_script_header=*/true,
/*attest_signals_origin=*/false);
}
IN_PROC_BROWSER_TEST_F(InterestGroupCrossOriginTrustedSignalsBrowserTest,
FeatureDetection) {
constexpr double kDefaultMaxGroupLifetimeMs = base::Days(30).InMilliseconds();
const char kTestExpression[] = R"(
navigator.protectedAudience.queryFeatureSupport(
'permitCrossOriginTrustedSignals');
)";
const char kQueryAll[] = R"(
navigator.protectedAudience.queryFeatureSupport('*');
)";
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/simple_page.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
EXPECT_EQ(true, EvalJs(shell(), kTestExpression));
auto all_result = EvalJs(shell(), kQueryAll);
EXPECT_THAT(all_result,
EvalJsResult::IsOkAndHolds(base::test::IsJson(base::StringPrintf(
R"({
"adComponentsLimit": 40,
"deprecatedRenderURLReplacements": true,
"permitCrossOriginTrustedSignals": true,
"realTimeReporting": false,
"reportingTimeout": true,
"selectableReportingIds": true,
"sellerNonce": true,
"trustedSignalsKVv2": true,
"maxGroupLifetimeMs": %f
})",
kDefaultMaxGroupLifetimeMs))))
<< all_result;
}
IN_PROC_BROWSER_TEST_F(InterestGroupCrossOriginTrustedSignalsBrowserTest,
BidderSignalsCors) {
TestTrustedBidderSignals(/*expect_success=*/true, /*add_cors_header=*/true,
/*attest_signals_origin=*/true);
}
IN_PROC_BROWSER_TEST_F(InterestGroupCrossOriginTrustedSignalsBrowserTest,
BidderSignalsNoCors) {
TestTrustedBidderSignals(/*expect_success=*/false, /*add_cors_header=*/false,
/*attest_signals_origin=*/true);
}
IN_PROC_BROWSER_TEST_F(InterestGroupCrossOriginTrustedSignalsBrowserTest,
BidderSignalsNoAttestation) {
TestTrustedBidderSignals(/*expect_success=*/false, /*add_cors_header=*/true,
/*attest_signals_origin=*/false);
}
class FledgeEnableUserAgentOverrideBrowserTest
: public InterestGroupBrowserTest {
public:
FledgeEnableUserAgentOverrideBrowserTest() {
feature_list_.InitWithFeatures(
/*enabled_features=*/{features::kFledgeEnableUserAgentOverrides},
/*disabled_features=*/{});
}
protected:
base::test::ScopedFeatureList feature_list_;
};
IN_PROC_BROWSER_TEST_F(FledgeEnableUserAgentOverrideBrowserTest,
ReportingMultipleAuctionsWithUserAgentOverridden) {
URLLoaderMonitor url_loader_monitor;
web_contents()->SetUserAgentOverride(
blink::UserAgentOverride::UserAgentOnly("overridden-user-agent"),
/*override_in_new_tabs=*/false);
web_contents()
->GetController()
.GetLastCommittedEntry()
->SetIsOverridingUserAgent(true);
GURL test_url_a = embedded_https_test_server().GetURL("a.test", "/echo");
ASSERT_TRUE(NavigateToURL(shell(), test_url_a));
const url::Origin origin_a = url::Origin::Create(test_url_a);
GURL ad1_url = embedded_https_test_server().GetURL(
"c.test", "/echo?stop_bidding_after_win");
GURL ad2_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_shoes");
// This group will win if it has never won an auction.
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/origin_a,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test",
"/interest_group/bidding_logic_stop_bidding_after_win.js"))
.SetAds({{{ad1_url, R"({"ad":"metadata","here":[1,2]})"}}})
.Build()));
GURL test_url_b =
embedded_https_test_server().GetURL("b.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url_b));
const url::Origin origin_b = url::Origin::Create(test_url_b);
// This group will win if the other interest group has won an auction.
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/origin_b,
/*name=*/"shoes")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"b.test",
"/interest_group/bidding_logic_with_debugging_report.js"))
.SetAds({{{ad2_url, /*metadata=*/std::nullopt}}})
.Build()));
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1, $3],
})",
origin_b,
embedded_https_test_server().GetURL("b.test",
"/interest_group/decision_logic.js"),
origin_a);
// Setting a small reporting interval to run the test faster.
manager_->set_reporting_interval_for_testing(base::Milliseconds(1));
// Run an ad auction. Interest group cars of owner `test_url_a` wins.
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad1_url);
// Wait for database to be updated with the win, which may happen after the
// auction completes.
WaitForInterestGroupsSatisfying(
origin_a, base::BindLambdaForTesting(
[](scoped_refptr<StorageInterestGroups> groups) -> bool {
EXPECT_EQ(1u, groups->size());
return groups->GetInterestGroups()[0]
->bidding_browser_signals->prev_wins.size() ==
1u;
}));
// Run auction again on the same page. Interest group shoes of owner
// `test_url2` wins.
auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1, $3],
})",
origin_b,
embedded_https_test_server().GetURL("b.test",
"/interest_group/decision_logic.js"),
origin_a);
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad2_url);
// Wait for database to be updated with the win, which may happen after the
// auction completes.
WaitForInterestGroupsSatisfying(
origin_b, base::BindLambdaForTesting(
[](scoped_refptr<StorageInterestGroups> groups) -> bool {
EXPECT_EQ(1u, groups->size());
return groups->GetInterestGroups()[0]
->bidding_browser_signals->prev_wins.size() ==
1u;
}));
// Check ResourceRequest structs of report requests.
// The URLs must not have the same path with different hostnames, because
// WaitForUrl() always replaces hostnames with "127.0.0.1", thus only waits
// for the first URL among URLs with the same path.
const struct ExpectedReportRequest {
GURL url;
url::Origin request_initiator;
} kExpectedReportRequests[] = {
// First auction's seller's ReportResult() URL.
{embedded_https_test_server().GetURL("b.test", "/echoall?report_seller"),
origin_b},
// First auction's winning bidder's ReportWin() URL.
{embedded_https_test_server().GetURL(
"a.test", "/echoall?report_bidder_stop_bidding_after_win&cars"),
origin_b},
// First auction's debugging loss report URL from bidder.
{embedded_https_test_server().GetURL(
"b.test", "/echo?bidder_debug_report_loss/shoes"),
origin_b},
// Second auction's seller's ReportResult() URL.
{embedded_https_test_server().GetURL("b.test", "/echoall?report_seller"),
origin_b},
// Second auction's winning bidder's ReportWin() URL.
{embedded_https_test_server().GetURL("b.test",
"/echoall?report_bidder/shoes"),
origin_b},
// Second auction's debugging win report URL from bidder.
{embedded_https_test_server().GetURL(
"b.test", "/echo?bidder_debug_report_win/shoes"),
origin_b},
};
for (const auto& expected_report_request : kExpectedReportRequests) {
SCOPED_TRACE(expected_report_request.url);
// Make sure the report URL was actually fetched over the network.
WaitForUrl(expected_report_request.url);
std::optional<network::ResourceRequest> request =
url_loader_monitor.WaitForUrl(expected_report_request.url);
ASSERT_TRUE(request);
EXPECT_EQ(expected_report_request.request_initiator,
request->request_initiator);
EXPECT_FALSE(request->headers.IsEmpty());
EXPECT_THAT(request->headers.GetHeader(net::HttpRequestHeaders::kUserAgent),
"overridden-user-agent");
}
}
IN_PROC_BROWSER_TEST_F(FledgeEnableUserAgentOverrideBrowserTest,
RunAdAuctionWithWinnerWithUserAgentOverridden) {
URLLoaderMonitor url_loader_monitor;
web_contents()->SetUserAgentOverride(
blink::UserAgentOverride::UserAgentOnly("overridden-user-agent"),
/*override_in_new_tabs=*/false);
web_contents()
->GetController()
.GetLastCommittedEntry()
->SetIsOverridingUserAgent(true);
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_cars");
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"))
.SetTrustedBiddingSignalsUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/trusted_bidding_signals.json"))
.SetTrustedBiddingSignalsKeys({{"key1"}})
.SetAds(/*ads=*/{{{ad_url, R"({"ad":"metadata","here":[1,2]})"}}})
.Build()));
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
})",
test_origin,
embedded_https_test_server().GetURL("a.test",
"/interest_group/decision_logic.js"));
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad_url);
// Check ResourceRequest structs of requests issued by the worklet process.
const struct ExpectedRequest {
GURL url;
const char* accept_header;
} kExpectedRequests[] = {
{embedded_https_test_server().GetURL("a.test",
"/interest_group/bidding_logic.js"),
"application/javascript"},
{embedded_https_test_server().GetURL(
"a.test",
"/interest_group/trusted_bidding_signals.json?"
"hostname=a.test&keys=key1&interestGroupNames=cars"),
"application/json"},
{embedded_https_test_server().GetURL("a.test",
"/interest_group/decision_logic.js"),
"application/javascript"},
};
for (const auto& expected_request : kExpectedRequests) {
SCOPED_TRACE(expected_request.url);
std::optional<network::ResourceRequest> request =
url_loader_monitor.GetRequestInfo(expected_request.url);
ASSERT_TRUE(request);
EXPECT_EQ(2u, request->headers.GetHeaderVector().size());
EXPECT_THAT(request->headers.GetHeader(net::HttpRequestHeaders::kAccept),
testing::Optional(expected_request.accept_header));
EXPECT_THAT(request->headers.GetHeader(net::HttpRequestHeaders::kUserAgent),
"overridden-user-agent");
}
}
IN_PROC_BROWSER_TEST_F(FledgeEnableUserAgentOverrideBrowserTest,
UpdateURLHasOverridenUserAgent) {
URLLoaderMonitor url_loader_monitor;
web_contents()->SetUserAgentOverride(
blink::UserAgentOverride::UserAgentOnly("overridden-user-agent"),
/*override_in_new_tabs=*/false);
web_contents()
->GetController()
.GetLastCommittedEntry()
->SetIsOverridingUserAgent(true);
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url =
embedded_https_test_server().GetURL("a.test", "/echo?render_cars");
constexpr char kUpdatePath[] = "/interest_group/update.json";
GURL update_url = embedded_https_test_server().GetURL("a.test", kUpdatePath);
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"))
.SetUpdateUrl(update_url)
.SetAds(/*ads=*/{{{ad_url, R"({"ad":"metadata","here":[1,2]})"}}})
.Build()));
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
})",
test_origin,
embedded_https_test_server().GetURL("a.test",
"/interest_group/decision_logic.js"));
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad_url);
EXPECT_EQ("done", UpdateInterestGroupsInJS());
url_loader_monitor.WaitForUrls();
const network::ResourceRequest& request =
url_loader_monitor.WaitForUrl(update_url);
EXPECT_EQ(1u, request.headers.GetHeaderVector().size());
EXPECT_THAT(request.headers.GetHeader(net::HttpRequestHeaders::kUserAgent),
"overridden-user-agent");
}
IN_PROC_BROWSER_TEST_F(FledgeEnableUserAgentOverrideBrowserTest,
RunAdAuctionWithDebugReporting) {
URLLoaderMonitor url_loader_monitor;
web_contents()->SetUserAgentOverride(
blink::UserAgentOverride::UserAgentOnly("overridden-user-agent"),
/*override_in_new_tabs=*/false);
web_contents()
->GetController()
.GetLastCommittedEntry()
->SetIsOverridingUserAgent(true);
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad1_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_winner");
GURL ad2_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_bikes");
GURL ad3_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_shoes");
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"winner")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test",
"/interest_group/bidding_logic_with_debugging_report.js"))
.SetAds({{{ad1_url, /*metadata=*/std::nullopt}}})
.Build()));
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"bikes")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test",
"/interest_group/bidding_logic_with_debugging_report.js"))
.SetAds({{{ad2_url, /*metadata=*/std::nullopt}}})
.Build()));
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"shoes")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test",
"/interest_group/bidding_logic_with_debugging_report.js"))
.SetAds({{{ad3_url, /*metadata=*/std::nullopt}}})
.Build()));
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
})",
test_origin,
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic_with_debugging_report.js"));
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad1_url);
// Check ResourceRequest structs of report requests.
const auto kExpectedReportUrls = std::to_array<GURL>({
// Return value from seller's ReportResult() method.
embedded_https_test_server().GetURL("a.test", "/echoall?report_seller"),
// Return value from winning bidder's ReportWin() method.
embedded_https_test_server().GetURL("a.test",
"/echoall?report_bidder/winner"),
// Debugging report URL from seller for win report.
embedded_https_test_server().GetURL(
"a.test", "/echo?seller_debug_report_win/winner"),
// Debugging report URL from winning bidder for win report.
embedded_https_test_server().GetURL(
"a.test", "/echo?bidder_debug_report_win/winner"),
// Debugging report URL from seller for loss report.
embedded_https_test_server().GetURL(
"a.test", "/echo?seller_debug_report_loss/bikes"),
embedded_https_test_server().GetURL(
"a.test", "/echo?seller_debug_report_loss/shoes"),
// Debugging report URL from losing bidders for loss report.
embedded_https_test_server().GetURL(
"a.test", "/echo?bidder_debug_report_loss/bikes"),
embedded_https_test_server().GetURL(
"a.test", "/echo?bidder_debug_report_loss/shoes"),
});
for (const auto& expected_report_url : kExpectedReportUrls) {
SCOPED_TRACE(expected_report_url);
// Make sure the report URL was actually fetched over the network.
WaitForUrl(expected_report_url);
std::optional<network::ResourceRequest> request =
url_loader_monitor.WaitForUrl(expected_report_url);
ASSERT_TRUE(request);
EXPECT_EQ(test_origin, request->request_initiator);
EXPECT_FALSE(request->headers.IsEmpty());
EXPECT_THAT(request->headers.GetHeader(net::HttpRequestHeaders::kUserAgent),
"overridden-user-agent");
}
}
class FledgeEnableUserAgentOverrideDisabledBrowserTest
: public InterestGroupBrowserTest {
public:
FledgeEnableUserAgentOverrideDisabledBrowserTest() {
feature_list_.InitWithFeatures(
/*enabled_features=*/{},
/*disabled_features=*/{features::kFledgeEnableUserAgentOverrides});
}
protected:
base::test::ScopedFeatureList feature_list_;
};
IN_PROC_BROWSER_TEST_F(FledgeEnableUserAgentOverrideDisabledBrowserTest,
RunAdAuctionWithWinnerWithUserAgentOverridden) {
URLLoaderMonitor url_loader_monitor;
web_contents()->SetUserAgentOverride(
blink::UserAgentOverride::UserAgentOnly("overridden-user-agent"), false);
web_contents()
->GetController()
.GetLastCommittedEntry()
->SetIsOverridingUserAgent(true);
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_cars");
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"))
.SetTrustedBiddingSignalsUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/trusted_bidding_signals.json"))
.SetTrustedBiddingSignalsKeys({{"key1"}})
.SetAds(/*ads=*/{{{ad_url, R"({"ad":"metadata","here":[1,2]})"}}})
.Build()));
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
})",
test_origin,
embedded_https_test_server().GetURL("a.test",
"/interest_group/decision_logic.js"));
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad_url);
// Check ResourceRequest structs of requests issued by the worklet process.
const struct ExpectedRequest {
GURL url;
const char* accept_header;
} kExpectedRequests[] = {
{embedded_https_test_server().GetURL("a.test",
"/interest_group/bidding_logic.js"),
"application/javascript"},
{embedded_https_test_server().GetURL(
"a.test",
"/interest_group/trusted_bidding_signals.json?"
"hostname=a.test&keys=key1&interestGroupNames=cars"),
"application/json"},
{embedded_https_test_server().GetURL("a.test",
"/interest_group/decision_logic.js"),
"application/javascript"},
};
for (const auto& expected_request : kExpectedRequests) {
SCOPED_TRACE(expected_request.url);
std::optional<network::ResourceRequest> request =
url_loader_monitor.GetRequestInfo(expected_request.url);
ASSERT_TRUE(request);
EXPECT_EQ(1u, request->headers.GetHeaderVector().size());
EXPECT_THAT(request->headers.GetHeader(net::HttpRequestHeaders::kAccept),
testing::Optional(expected_request.accept_header));
EXPECT_THAT(request->headers.GetHeader(net::HttpRequestHeaders::kUserAgent),
std::nullopt);
}
}
IN_PROC_BROWSER_TEST_F(FledgeEnableUserAgentOverrideDisabledBrowserTest,
ReportingMultipleAuctionsWithUserAgentOverridden) {
URLLoaderMonitor url_loader_monitor;
web_contents()->SetUserAgentOverride(
blink::UserAgentOverride::UserAgentOnly("overridden-user-agent"),
/*override_in_new_tabs=*/false);
web_contents()
->GetController()
.GetLastCommittedEntry()
->SetIsOverridingUserAgent(true);
GURL test_url_a = embedded_https_test_server().GetURL("a.test", "/echo");
ASSERT_TRUE(NavigateToURL(shell(), test_url_a));
const url::Origin origin_a = url::Origin::Create(test_url_a);
GURL ad1_url = embedded_https_test_server().GetURL(
"c.test", "/echo?stop_bidding_after_win");
GURL ad2_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_shoes");
// This group will win if it has never won an auction.
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/origin_a,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test",
"/interest_group/bidding_logic_stop_bidding_after_win.js"))
.SetAds({{{ad1_url, R"({"ad":"metadata","here":[1,2]})"}}})
.Build()));
GURL test_url_b =
embedded_https_test_server().GetURL("b.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url_b));
const url::Origin origin_b = url::Origin::Create(test_url_b);
// This group will win if the other interest group has won an auction.
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/origin_b,
/*name=*/"shoes")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"b.test",
"/interest_group/bidding_logic_with_debugging_report.js"))
.SetAds({{{ad2_url, /*metadata=*/std::nullopt}}})
.Build()));
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1, $3],
})",
origin_b,
embedded_https_test_server().GetURL("b.test",
"/interest_group/decision_logic.js"),
origin_a);
// Setting a small reporting interval to run the test faster.
manager_->set_reporting_interval_for_testing(base::Milliseconds(1));
// Run an ad auction. Interest group cars of owner `test_url_a` wins.
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad1_url);
// Wait for database to be updated with the win, which may happen after the
// auction completes.
WaitForInterestGroupsSatisfying(
origin_a, base::BindLambdaForTesting(
[](scoped_refptr<StorageInterestGroups> groups) -> bool {
EXPECT_EQ(1u, groups->size());
return groups->GetInterestGroups()[0]
->bidding_browser_signals->prev_wins.size() ==
1u;
}));
// Run auction again on the same page. Interest group shoes of owner
// `test_url2` wins.
auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1, $3],
})",
origin_b,
embedded_https_test_server().GetURL("b.test",
"/interest_group/decision_logic.js"),
origin_a);
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad2_url);
// Wait for database to be updated with the win, which may happen after the
// auction completes.
WaitForInterestGroupsSatisfying(
origin_b, base::BindLambdaForTesting(
[](scoped_refptr<StorageInterestGroups> groups) -> bool {
EXPECT_EQ(1u, groups->size());
return groups->GetInterestGroups()[0]
->bidding_browser_signals->prev_wins.size() ==
1u;
}));
// Check ResourceRequest structs of report requests.
// The URLs must not have the same path with different hostnames, because
// WaitForUrl() always replaces hostnames with "127.0.0.1", thus only waits
// for the first URL among URLs with the same path.
const struct ExpectedReportRequest {
GURL url;
url::Origin request_initiator;
} kExpectedReportRequests[] = {
// First auction's seller's ReportResult() URL.
{embedded_https_test_server().GetURL("b.test", "/echoall?report_seller"),
origin_b},
// First auction's winning bidder's ReportWin() URL.
{embedded_https_test_server().GetURL(
"a.test", "/echoall?report_bidder_stop_bidding_after_win&cars"),
origin_b},
// First auction's debugging loss report URL from bidder.
{embedded_https_test_server().GetURL(
"b.test", "/echo?bidder_debug_report_loss/shoes"),
origin_b},
// Second auction's seller's ReportResult() URL.
{embedded_https_test_server().GetURL("b.test", "/echoall?report_seller"),
origin_b},
// Second auction's winning bidder's ReportWin() URL.
{embedded_https_test_server().GetURL("b.test",
"/echoall?report_bidder/shoes"),
origin_b},
// Second auction's debugging win report URL from bidder.
{embedded_https_test_server().GetURL(
"b.test", "/echo?bidder_debug_report_win/shoes"),
origin_b},
};
for (const auto& expected_report_request : kExpectedReportRequests) {
SCOPED_TRACE(expected_report_request.url);
// Make sure the report URL was actually fetched over the network.
WaitForUrl(expected_report_request.url);
std::optional<network::ResourceRequest> request =
url_loader_monitor.WaitForUrl(expected_report_request.url);
ASSERT_TRUE(request);
EXPECT_EQ(expected_report_request.request_initiator,
request->request_initiator);
EXPECT_TRUE(request->headers.IsEmpty());
}
}
IN_PROC_BROWSER_TEST_F(FledgeEnableUserAgentOverrideDisabledBrowserTest,
UpdateURLDoesNotOverrideUserAgent) {
URLLoaderMonitor url_loader_monitor;
web_contents()->SetUserAgentOverride(
blink::UserAgentOverride::UserAgentOnly("overridden-user-agent"),
/*override_in_new_tabs=*/false);
web_contents()
->GetController()
.GetLastCommittedEntry()
->SetIsOverridingUserAgent(true);
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url =
embedded_https_test_server().GetURL("a.test", "/echo?render_cars");
constexpr char kUpdatePath[] = "/interest_group/update.json";
GURL update_url = embedded_https_test_server().GetURL("a.test", kUpdatePath);
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"))
.SetUpdateUrl(update_url)
.SetAds(/*ads=*/{{{ad_url, R"({"ad":"metadata","here":[1,2]})"}}})
.Build()));
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
})",
test_origin,
embedded_https_test_server().GetURL("a.test",
"/interest_group/decision_logic.js"));
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad_url);
EXPECT_EQ("done", UpdateInterestGroupsInJS());
url_loader_monitor.WaitForUrls();
const network::ResourceRequest& request =
url_loader_monitor.WaitForUrl(update_url);
EXPECT_EQ(0u, request.headers.GetHeaderVector().size());
}
IN_PROC_BROWSER_TEST_F(FledgeEnableUserAgentOverrideDisabledBrowserTest,
RunAdAuctionWithDebugReporting) {
URLLoaderMonitor url_loader_monitor;
web_contents()->SetUserAgentOverride(
blink::UserAgentOverride::UserAgentOnly("overridden-user-agent"),
/*override_in_new_tabs=*/false);
web_contents()
->GetController()
.GetLastCommittedEntry()
->SetIsOverridingUserAgent(true);
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad1_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_winner");
GURL ad2_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_bikes");
GURL ad3_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_shoes");
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"winner")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test",
"/interest_group/bidding_logic_with_debugging_report.js"))
.SetAds({{{ad1_url, /*metadata=*/std::nullopt}}})
.Build()));
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"bikes")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test",
"/interest_group/bidding_logic_with_debugging_report.js"))
.SetAds({{{ad2_url, /*metadata=*/std::nullopt}}})
.Build()));
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"shoes")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test",
"/interest_group/bidding_logic_with_debugging_report.js"))
.SetAds({{{ad3_url, /*metadata=*/std::nullopt}}})
.Build()));
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
})",
test_origin,
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic_with_debugging_report.js"));
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad1_url);
// Check ResourceRequest structs of report requests.
const auto kExpectedReportUrls = std::to_array<GURL>({
// Return value from seller's ReportResult() method.
embedded_https_test_server().GetURL("a.test", "/echoall?report_seller"),
// Return value from winning bidder's ReportWin() method.
embedded_https_test_server().GetURL("a.test",
"/echoall?report_bidder/winner"),
// Debugging report URL from seller for win report.
embedded_https_test_server().GetURL(
"a.test", "/echo?seller_debug_report_win/winner"),
// Debugging report URL from winning bidder for win report.
embedded_https_test_server().GetURL(
"a.test", "/echo?bidder_debug_report_win/winner"),
// Debugging report URL from seller for loss report.
embedded_https_test_server().GetURL(
"a.test", "/echo?seller_debug_report_loss/bikes"),
embedded_https_test_server().GetURL(
"a.test", "/echo?seller_debug_report_loss/shoes"),
// Debugging report URL from losing bidders for loss report.
embedded_https_test_server().GetURL(
"a.test", "/echo?bidder_debug_report_loss/bikes"),
embedded_https_test_server().GetURL(
"a.test", "/echo?bidder_debug_report_loss/shoes"),
});
for (const auto& expected_report_url : kExpectedReportUrls) {
SCOPED_TRACE(expected_report_url);
// Make sure the report URL was actually fetched over the network.
WaitForUrl(expected_report_url);
std::optional<network::ResourceRequest> request =
url_loader_monitor.WaitForUrl(expected_report_url);
ASSERT_TRUE(request);
EXPECT_EQ(test_origin, request->request_initiator);
EXPECT_TRUE(request->headers.IsEmpty());
}
}
class RealTimeReportingAndUserAgentOverrideEnabledTest
: public InterestGroupBrowserTest {
public:
RealTimeReportingAndUserAgentOverrideEnabledTest() {
feature_list_.InitWithFeatures(
/*enabled_features=*/{blink::features::kFledgeRealTimeReporting,
features::kFledgeEnableUserAgentOverrides},
/*disabled_features=*/{});
}
private:
base::test::ScopedFeatureList feature_list_;
};
IN_PROC_BROWSER_TEST_F(RealTimeReportingAndUserAgentOverrideEnabledTest,
RealTimeReportingHasUAOverride) {
const char kHostA[] = "a.test";
const char kHostB[] = "b.test";
// Setting a small reporting interval to run the test faster.
manager_->set_reporting_interval_for_testing(base::Milliseconds(1));
manager_->set_max_report_queue_length_for_testing(50);
manager_->set_max_active_report_requests_for_testing(50);
URLLoaderMonitor url_loader_monitor;
web_contents()->SetUserAgentOverride(
blink::UserAgentOverride::UserAgentOnly("overridden-user-agent"),
/*override_in_new_tabs=*/false);
web_contents()
->GetController()
.GetLastCommittedEntry()
->SetIsOverridingUserAgent(true);
GURL test_url =
embedded_https_test_server().GetURL(kHostA, "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url =
embedded_https_test_server().GetURL(kHostA, "/echo?render_cars");
url::Origin test_origin_b =
url::Origin::Create(embedded_https_test_server().GetURL(kHostB, "/echo"));
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
kHostA,
"/interest_group/bidding_logic_with_real_time_reporting.js"))
.SetAds({{{ad_url, /*metadata=*/std::nullopt}}})
.Build()));
const char kConfigTemplate[] = R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$3],
sellerRealTimeReportingConfig: {type: 'default-local-reporting'},
perBuyerRealTimeReportingConfig: {
$3: {type: 'default-local-reporting'},
}
})";
std::string auction_config = JsReplace(
kConfigTemplate, test_origin_b,
embedded_https_test_server().GetURL(
kHostB, "/interest_group/decision_logic_with_real_time_reporting.js"),
test_origin);
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad_url);
const auto kExpectedReportUrls = std::to_array<GURL>({
embedded_https_test_server().GetURL(
"a.test", "/.well-known/interest-group/real-time-report"),
embedded_https_test_server().GetURL(
"b.test", "/.well-known/interest-group/real-time-report"),
});
for (const auto& expected_report_url : kExpectedReportUrls) {
SCOPED_TRACE(expected_report_url);
// Make sure the report URL was actually fetched over the network.
WaitForUrl(expected_report_url);
std::optional<network::ResourceRequest> request =
url_loader_monitor.WaitForUrl(expected_report_url);
ASSERT_TRUE(request);
EXPECT_FALSE(request->headers.IsEmpty());
EXPECT_THAT(request->headers.GetHeader(net::HttpRequestHeaders::kUserAgent),
"overridden-user-agent");
}
}
class RealTimeReportingEnabledAndUserAgentOverrideDisabledTest
: public InterestGroupBrowserTest {
public:
RealTimeReportingEnabledAndUserAgentOverrideDisabledTest() {
feature_list_.InitWithFeatures(
/*enabled_features=*/{blink::features::kFledgeRealTimeReporting},
/*disabled_features=*/{features::kFledgeEnableUserAgentOverrides});
}
private:
base::test::ScopedFeatureList feature_list_;
};
IN_PROC_BROWSER_TEST_F(RealTimeReportingEnabledAndUserAgentOverrideDisabledTest,
RealTimeReportingHasNoUAOverride) {
const char kHostA[] = "a.test";
const char kHostB[] = "b.test";
// Setting a small reporting interval to run the test faster.
manager_->set_reporting_interval_for_testing(base::Milliseconds(1));
manager_->set_max_report_queue_length_for_testing(50);
manager_->set_max_active_report_requests_for_testing(50);
URLLoaderMonitor url_loader_monitor;
web_contents()->SetUserAgentOverride(
blink::UserAgentOverride::UserAgentOnly("overridden-user-agent"),
/*override_in_new_tabs=*/false);
web_contents()
->GetController()
.GetLastCommittedEntry()
->SetIsOverridingUserAgent(true);
GURL test_url =
embedded_https_test_server().GetURL(kHostA, "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url =
embedded_https_test_server().GetURL(kHostA, "/echo?render_cars");
url::Origin test_origin_b =
url::Origin::Create(embedded_https_test_server().GetURL(kHostB, "/echo"));
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
kHostA,
"/interest_group/bidding_logic_with_real_time_reporting.js"))
.SetAds({{{ad_url, /*metadata=*/std::nullopt}}})
.Build()));
const char kConfigTemplate[] = R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$3],
sellerRealTimeReportingConfig: {type: 'default-local-reporting'},
perBuyerRealTimeReportingConfig: {
$3: {type: 'default-local-reporting'},
}
})";
std::string auction_config = JsReplace(
kConfigTemplate, test_origin_b,
embedded_https_test_server().GetURL(
kHostB, "/interest_group/decision_logic_with_real_time_reporting.js"),
test_origin);
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad_url);
const auto kExpectedReportUrls = std::to_array<GURL>({
embedded_https_test_server().GetURL(
"a.test", "/.well-known/interest-group/real-time-report"),
embedded_https_test_server().GetURL(
"b.test", "/.well-known/interest-group/real-time-report"),
});
for (const auto& expected_report_url : kExpectedReportUrls) {
SCOPED_TRACE(expected_report_url);
// Make sure the report URL was actually fetched over the network.
WaitForUrl(expected_report_url);
std::optional<network::ResourceRequest> request =
url_loader_monitor.WaitForUrl(expected_report_url);
ASSERT_TRUE(request);
EXPECT_FALSE(request->headers.IsEmpty());
EXPECT_NE(request->headers.GetHeader(net::HttpRequestHeaders::kUserAgent),
"overridden-user-agent");
}
}
class RealTimeReportingEnabledTest : public InterestGroupBrowserTest {
public:
RealTimeReportingEnabledTest() {
feature_list_.InitWithFeatures(
/*enabled_features=*/{blink::features::kFledgeRealTimeReporting},
/*disabled_features=*/{features::kFledgeEnableUnNoisedRealTimeReport});
}
private:
base::test::ScopedFeatureList feature_list_;
};
// Opted-in buyers will receive real time histograms. Since real time reporting
// URLs have same path and different reporting origins, but this browser test
// always replaces requests' hosts to "127.0.0.1" and only handles requests
// based on their paths, we cannot test different origin sellers' and buyers'
// real time reports at the same time (they'll be treated as the same request).
IN_PROC_BROWSER_TEST_F(RealTimeReportingEnabledTest, RealTimeReporting) {
const char kHostA[] = "a.test";
const char kHostB[] = "b.test";
// Setting a small reporting interval to run the test faster.
manager_->set_reporting_interval_for_testing(base::Milliseconds(1));
manager_->set_max_report_queue_length_for_testing(50);
manager_->set_max_active_report_requests_for_testing(50);
URLLoaderMonitor url_loader_monitor;
GURL test_url =
embedded_https_test_server().GetURL(kHostA, "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url =
embedded_https_test_server().GetURL(kHostA, "/echo?render_cars");
url::Origin test_origin_b =
url::Origin::Create(embedded_https_test_server().GetURL(kHostB, "/echo"));
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
/*owner=*/test_origin,
/*name=*/"cars",
/*priority=*/0.0,
/*execution_mode=*/
blink::InterestGroup::ExecutionMode::kCompatibilityMode,
/*bidding_url=*/
embedded_https_test_server().GetURL(
kHostA,
"/interest_group/bidding_logic_with_real_time_reporting.js"),
/*ads=*/{{{ad_url, /*metadata=*/std::nullopt}}}));
// Only opt in buyer, otherwise seller will send a real time report as well.
const char kConfigTemplate[] = R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$3],
perBuyerRealTimeReportingConfig: {
$3: {type: 'default-local-reporting'}
}
})";
std::string auction_config = JsReplace(
kConfigTemplate, test_origin_b,
embedded_https_test_server().GetURL(
kHostB, "/interest_group/decision_logic_with_real_time_reporting.js"),
test_origin);
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad_url);
const GURL expected_report_url = embedded_https_test_server().GetURL(
"a.test", "/.well-known/interest-group/real-time-report");
WaitForUrl(expected_report_url);
std::optional<network::ResourceRequest> request =
url_loader_monitor.WaitForUrl(expected_report_url);
ASSERT_TRUE(request);
EXPECT_EQ(net::HttpRequestHeaders::kPostMethod, request->method);
EXPECT_EQ(network::mojom::CredentialsMode::kOmit, request->credentials_mode);
EXPECT_EQ(network::mojom::RedirectMode::kError, request->redirect_mode);
EXPECT_EQ(test_origin, request->request_initiator);
EXPECT_THAT(request->headers.GetHeader(net::HttpRequestHeaders::kContentType),
testing::Optional(std::string("application/cbor")));
ASSERT_TRUE(request->trusted_params);
const net::IsolationInfo& isolation_info =
request->trusted_params->isolation_info;
EXPECT_EQ(net::IsolationInfo::RequestType::kOther,
isolation_info.request_type());
EXPECT_TRUE(isolation_info.network_isolation_key().IsTransient());
EXPECT_TRUE(isolation_info.site_for_cookies().IsNull());
// Check the request body, which is the real time report in cbor.
std::string body = network::GetUploadData(*request);
const auto maybe_map = cbor::Reader::Read(base::as_byte_span(body));
ASSERT_TRUE(maybe_map && maybe_map->is_map());
const auto& map = maybe_map->GetMap();
const auto version_it = map.find(cbor::Value("version"));
ASSERT_TRUE(version_it != map.end() && version_it->second.is_integer());
EXPECT_EQ(1, version_it->second.GetInteger());
for (const std::string& field : {"histogram", "platformHistogram"}) {
const auto histogram_it = map.find(cbor::Value(field));
ASSERT_TRUE(histogram_it != map.end() && histogram_it->second.is_map());
const auto& histogram_map = histogram_it->second.GetMap();
const auto buckets_it = histogram_map.find(cbor::Value("buckets"));
ASSERT_TRUE(buckets_it != histogram_map.end() &&
buckets_it->second.is_bytestring());
std::vector<uint8_t> buckets = buckets_it->second.GetBytestring();
size_t expected_buckets_size = field == "histogram" ? 128u : 1u;
CHECK_EQ(expected_buckets_size, buckets.size());
const auto length_it = histogram_map.find(cbor::Value("length"));
ASSERT_TRUE(length_it != histogram_map.end() &&
length_it->second.is_integer());
int expected_length = field == "histogram" ? 1024 : 4;
EXPECT_EQ(expected_length, length_it->second.GetInteger());
}
}
// Opted-in sellers will receive real time histograms, even if they don't call
// the API.
IN_PROC_BROWSER_TEST_F(RealTimeReportingEnabledTest,
RealTimeReportingSellerDoesNotCallAPI) {
const char kHostA[] = "a.test";
const char kHostB[] = "b.test";
// Setting a small reporting interval to run the test faster.
manager_->set_reporting_interval_for_testing(base::Milliseconds(1));
manager_->set_max_report_queue_length_for_testing(50);
manager_->set_max_active_report_requests_for_testing(50);
URLLoaderMonitor url_loader_monitor;
GURL test_url =
embedded_https_test_server().GetURL(kHostA, "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url =
embedded_https_test_server().GetURL(kHostA, "/echo?render_cars");
url::Origin test_origin_b =
url::Origin::Create(embedded_https_test_server().GetURL(kHostB, "/echo"));
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
/*owner=*/test_origin,
/*name=*/"cars",
/*priority=*/0.0,
/*execution_mode=*/
blink::InterestGroup::ExecutionMode::kCompatibilityMode,
/*bidding_url=*/
embedded_https_test_server().GetURL(
kHostA, "/interest_group/bidding_logic.js"),
/*ads=*/{{{ad_url, /*metadata=*/std::nullopt}}}));
const char kConfigTemplate[] = R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$3],
sellerRealTimeReportingConfig: {type: 'default-local-reporting'},
})";
std::string auction_config =
JsReplace(kConfigTemplate, test_origin_b,
embedded_https_test_server().GetURL(
kHostB, "/interest_group/decision_logic.js"),
test_origin);
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad_url);
const GURL expected_report_url = embedded_https_test_server().GetURL(
"b.test", "/.well-known/interest-group/real-time-report");
WaitForUrl(expected_report_url);
std::optional<network::ResourceRequest> request =
url_loader_monitor.WaitForUrl(expected_report_url);
ASSERT_TRUE(request);
EXPECT_EQ(net::HttpRequestHeaders::kPostMethod, request->method);
EXPECT_EQ(network::mojom::CredentialsMode::kOmit, request->credentials_mode);
EXPECT_EQ(network::mojom::RedirectMode::kError, request->redirect_mode);
EXPECT_EQ(test_origin, request->request_initiator);
EXPECT_THAT(request->headers.GetHeader(net::HttpRequestHeaders::kContentType),
testing::Optional(std::string("application/cbor")));
ASSERT_TRUE(request->trusted_params);
const net::IsolationInfo& isolation_info =
request->trusted_params->isolation_info;
EXPECT_EQ(net::IsolationInfo::RequestType::kOther,
isolation_info.request_type());
EXPECT_TRUE(isolation_info.network_isolation_key().IsTransient());
EXPECT_TRUE(isolation_info.site_for_cookies().IsNull());
}
// Opted-in buyers will receive real time histograms, even if they don't call
// the API.
IN_PROC_BROWSER_TEST_F(RealTimeReportingEnabledTest,
RealTimeReportingBuyerDoesNotCallAPI) {
const char kHostA[] = "a.test";
const char kHostB[] = "b.test";
// Setting a small reporting interval to run the test faster.
manager_->set_reporting_interval_for_testing(base::Milliseconds(1));
manager_->set_max_report_queue_length_for_testing(50);
manager_->set_max_active_report_requests_for_testing(50);
URLLoaderMonitor url_loader_monitor;
GURL test_url =
embedded_https_test_server().GetURL(kHostA, "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url =
embedded_https_test_server().GetURL(kHostA, "/echo?render_cars");
url::Origin test_origin_b =
url::Origin::Create(embedded_https_test_server().GetURL(kHostB, "/echo"));
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
/*owner=*/test_origin,
/*name=*/"cars",
/*priority=*/0.0,
/*execution_mode=*/
blink::InterestGroup::ExecutionMode::kCompatibilityMode,
/*bidding_url=*/
embedded_https_test_server().GetURL(
kHostA, "/interest_group/bidding_logic.js"),
/*ads=*/{{{ad_url, /*metadata=*/std::nullopt}}}));
const char kConfigTemplate[] = R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$3],
perBuyerRealTimeReportingConfig: {
$3: {type: 'default-local-reporting'}
}
})";
std::string auction_config =
JsReplace(kConfigTemplate, test_origin_b,
embedded_https_test_server().GetURL(
kHostB, "/interest_group/decision_logic.js"),
test_origin);
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad_url);
const GURL expected_report_url = embedded_https_test_server().GetURL(
"a.test", "/.well-known/interest-group/real-time-report");
WaitForUrl(expected_report_url);
std::optional<network::ResourceRequest> request =
url_loader_monitor.WaitForUrl(expected_report_url);
ASSERT_TRUE(request);
EXPECT_EQ(net::HttpRequestHeaders::kPostMethod, request->method);
}
IN_PROC_BROWSER_TEST_F(RealTimeReportingEnabledTest,
RealTimeReportingRateLimitIsPerPagePerReportOrigin) {
const GURL kRealTimeReportUrlA = embedded_https_test_server().GetURL(
"a.test", "/.well-known/interest-group/real-time-report");
URLLoaderMonitor url_loader_monitor;
// Setting a small reporting interval to run the test faster.
manager_->set_reporting_interval_for_testing(base::Milliseconds(1));
manager_->set_max_report_queue_length_for_testing(50);
manager_->set_max_active_report_requests_for_testing(50);
// Two real time reports allowed to be sent per reporting origin per page per
// day.
manager_->set_real_time_reporting_window_for_testing(base::Days(1));
manager_->set_max_real_time_reports_for_testing(1);
GURL test_url_a =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url_a));
const url::Origin origin_a = url::Origin::Create(test_url_a);
GURL ad1_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_winner");
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/origin_a,
/*name=*/"winner")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test",
"/interest_group/bidding_logic_with_real_time_reporting.js"))
.SetAds({{{ad1_url, R"({"ad":"metadata","here":[1,2]})"}}})
.Build()));
// Only opt-in origin_a as buyer.
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
perBuyerRealTimeReportingConfig: {
$1: {type: 'default-local-reporting'}
}
})",
origin_a,
embedded_https_test_server().GetURL(
"a.test",
"/interest_group/decision_logic_with_real_time_reporting.js"));
// Run an ad auction.
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad1_url);
// origin_a sent one as buyer. Since it does not opt-in as seller, so the
// report was from its `generateBid()`.
WaitForUrl(kRealTimeReportUrlA);
ClearReceivedRequests();
// Run auction again on the same page. origin_a opted in as seller.
auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
sellerRealTimeReportingConfig: {type: 'default-local-reporting'},
perBuyerRealTimeReportingConfig: {
$1: {type: 'default-local-reporting'}
}
})",
origin_a,
embedded_https_test_server().GetURL(
"a.test",
"/interest_group/decision_logic_with_real_time_reporting.js"));
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad1_url);
// No real time report is sent after this auction, since origin_a is rate
// limited now.
EXPECT_TRUE(!HasServerSeenUrl(kRealTimeReportUrlA));
ClearReceivedRequests();
// Navigate to the same URL to open a new page of it, to test that the rate
// limit is per page, not per URL or something.
ASSERT_TRUE(NavigateToURL(shell(), test_url_a));
auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
perBuyerRealTimeReportingConfig: {
$1: {type: 'default-local-reporting'}
}
})",
origin_a,
embedded_https_test_server().GetURL(
"a.test",
"/interest_group/decision_logic_with_real_time_reporting.js"));
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad1_url);
// origin_a sent real time reports after the auction, because rate limit was
// per reporting origin per page, and it's a new page after navigation,
// although it's the same URL.
WaitForUrl(kRealTimeReportUrlA);
ClearReceivedRequests();
// Run the third auction on another page c.test.
GURL test_url_c =
embedded_https_test_server().GetURL("c.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url_c));
auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
perBuyerRealTimeReportingConfig: {
$1: {type: 'default-local-reporting'}
}
})",
origin_a,
embedded_https_test_server().GetURL(
"a.test",
"/interest_group/decision_logic_with_real_time_reporting.js"));
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad1_url);
// origin_a sent real time reports after the auction, because rate limit was
// per reporting origin per page, and there was rate limit for page a.test,
// not for page c.test.
WaitForUrl(kRealTimeReportUrlA);
}
IN_PROC_BROWSER_TEST_F(RealTimeReportingEnabledTest, FeatureDetection) {
constexpr double kDefaultMaxGroupLifetimeMs = base::Days(30).InMilliseconds();
const char kQueryRealTimeReporting[] = R"(
navigator.protectedAudience.queryFeatureSupport(
'realTimeReporting');
)";
const char kQueryAll[] = R"(
navigator.protectedAudience.queryFeatureSupport('*');
)";
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/simple_page.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
EXPECT_EQ(true, EvalJs(shell(), kQueryRealTimeReporting));
auto all_result = EvalJs(shell(), kQueryAll);
EXPECT_THAT(all_result,
EvalJsResult::IsOkAndHolds(base::test::IsJson(base::StringPrintf(
R"({
"adComponentsLimit": 40,
"deprecatedRenderURLReplacements": true,
"permitCrossOriginTrustedSignals": true,
"realTimeReporting": true,
"reportingTimeout": true,
"selectableReportingIds": true,
"sellerNonce": true,
"trustedSignalsKVv2": true,
"maxGroupLifetimeMs": %f
})",
kDefaultMaxGroupLifetimeMs))))
<< all_result;
}
class FledgeUnNoisedRealTimeReportEnabledTest
: public InterestGroupBrowserTest {
public:
FledgeUnNoisedRealTimeReportEnabledTest() {
feature_list_.InitWithFeatures(
/*enabled_features=*/{blink::features::kFledgeRealTimeReporting,
features::kFledgeEnableUnNoisedRealTimeReport},
/*disabled_features=*/{});
}
private:
base::test::ScopedFeatureList feature_list_;
};
IN_PROC_BROWSER_TEST_F(FledgeUnNoisedRealTimeReportEnabledTest,
ReportVersion2) {
const char kHostA[] = "a.test";
const char kHostB[] = "b.test";
// Setting a small reporting interval to run the test faster.
manager_->set_reporting_interval_for_testing(base::Milliseconds(1));
manager_->set_max_report_queue_length_for_testing(50);
manager_->set_max_active_report_requests_for_testing(50);
URLLoaderMonitor url_loader_monitor;
GURL test_url =
embedded_https_test_server().GetURL(kHostA, "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url =
embedded_https_test_server().GetURL(kHostA, "/echo?render_cars");
url::Origin test_origin_b =
url::Origin::Create(embedded_https_test_server().GetURL(kHostB, "/echo"));
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
/*owner=*/test_origin,
/*name=*/"cars",
/*priority=*/0.0,
/*execution_mode=*/
blink::InterestGroup::ExecutionMode::kCompatibilityMode,
/*bidding_url=*/
embedded_https_test_server().GetURL(
kHostA,
"/interest_group/bidding_logic_with_real_time_reporting.js"),
/*ads=*/{{{ad_url, /*metadata=*/std::nullopt}}}));
// Only opt in buyer, otherwise seller will send a real time report as well.
const char kConfigTemplate[] = R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$3],
perBuyerRealTimeReportingConfig: {
$3: {type: 'default-local-reporting'}
}
})";
std::string auction_config = JsReplace(
kConfigTemplate, test_origin_b,
embedded_https_test_server().GetURL(
kHostB, "/interest_group/decision_logic_with_real_time_reporting.js"),
test_origin);
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad_url);
const GURL expected_report_url = embedded_https_test_server().GetURL(
"a.test", "/.well-known/interest-group/real-time-report");
WaitForUrl(expected_report_url);
std::optional<network::ResourceRequest> request =
url_loader_monitor.WaitForUrl(expected_report_url);
ASSERT_TRUE(request);
EXPECT_EQ(net::HttpRequestHeaders::kPostMethod, request->method);
EXPECT_EQ(network::mojom::CredentialsMode::kOmit, request->credentials_mode);
EXPECT_EQ(network::mojom::RedirectMode::kError, request->redirect_mode);
EXPECT_EQ(test_origin, request->request_initiator);
EXPECT_THAT(request->headers.GetHeader(net::HttpRequestHeaders::kContentType),
testing::Optional(std::string("application/cbor")));
ASSERT_TRUE(request->trusted_params);
const net::IsolationInfo& isolation_info =
request->trusted_params->isolation_info;
EXPECT_EQ(net::IsolationInfo::RequestType::kOther,
isolation_info.request_type());
EXPECT_TRUE(isolation_info.network_isolation_key().IsTransient());
EXPECT_TRUE(isolation_info.site_for_cookies().IsNull());
// Check the request body, which is the real time report in cbor.
std::string body = network::GetUploadData(*request);
cbor::Reader::Config config;
config.allow_floating_point = true;
const auto maybe_map = cbor::Reader::Read(base::as_byte_span(body), config);
ASSERT_TRUE(maybe_map && maybe_map->is_map());
const auto& map = maybe_map->GetMap();
const auto version_it = map.find(cbor::Value("version"));
ASSERT_TRUE(version_it != map.end() && version_it->second.is_integer());
EXPECT_EQ(2, version_it->second.GetInteger());
const auto flip_probability_it = map.find(cbor::Value("flipProbability"));
ASSERT_TRUE(flip_probability_it != map.end() &&
flip_probability_it->second.is_double());
EXPECT_GT(flip_probability_it->second.GetDouble(), 0.377);
EXPECT_LT(flip_probability_it->second.GetDouble(), 0.378);
for (const std::string& field : {"histogram", "platformHistogram"}) {
const auto histogram_it = map.find(cbor::Value(field));
ASSERT_TRUE(histogram_it != map.end() && histogram_it->second.is_map());
const auto& histogram_map = histogram_it->second.GetMap();
const auto buckets_it = histogram_map.find(cbor::Value("buckets"));
ASSERT_TRUE(buckets_it != histogram_map.end() &&
buckets_it->second.is_bytestring());
std::vector<uint8_t> buckets = buckets_it->second.GetBytestring();
size_t expected_buckets_size = field == "histogram" ? 128u : 1u;
CHECK_EQ(expected_buckets_size, buckets.size());
const auto length_it = histogram_map.find(cbor::Value("length"));
ASSERT_TRUE(length_it != histogram_map.end() &&
length_it->second.is_integer());
int expected_length = field == "histogram" ? 1024 : 4;
EXPECT_EQ(expected_length, length_it->second.GetInteger());
}
}
class RealTimeReportingDisabledTest : public InterestGroupBrowserTest {
public:
RealTimeReportingDisabledTest() {
feature_list_.InitAndDisableFeature(
{blink::features::kFledgeRealTimeReporting});
}
private:
base::test::ScopedFeatureList feature_list_;
};
IN_PROC_BROWSER_TEST_F(RealTimeReportingDisabledTest, FeatureDetection) {
const char kTestExpression[] = R"(
navigator.protectedAudience.queryFeatureSupport(
'realTimeReporting');
)";
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/simple_page.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
EXPECT_EQ(false, EvalJs(shell(), kTestExpression));
}
class InterestGroupPreconnectOwnerAndSignalsOriginsTest
: public InterestGroupBrowserTest,
public testing::WithParamInterface<bool> {
public:
InterestGroupPreconnectOwnerAndSignalsOriginsTest() {
feature_list_.InitAndEnableFeature(features::kFledgeUsePreconnectCache);
}
~InterestGroupPreconnectOwnerAndSignalsOriginsTest() override = default;
class PreconnectListener
: public net::test_server::EmbeddedTestServerConnectionListener {
public:
PreconnectListener() = default;
~PreconnectListener() override = default;
// net::test_server::EmbeddedTestServerConnectionListener implementation:
std::unique_ptr<net::StreamSocket> AcceptedSocket(
std::unique_ptr<net::StreamSocket> connection) override {
base::AutoLock auto_lock(socket_count_lock_);
num_accepted_sockets_++;
if (waiting_for_accepted_socket_loop_ &&
num_accepted_sockets_ == waiting_for_accepted_socket_count_) {
waiting_for_accepted_socket_loop_->Quit();
}
return connection;
}
void ReadFromSocket(const net::StreamSocket& connection, int rv) override {}
// Wait until at least `count` connections have occurred. Cause an EXPECT
// failure if more than `count` connections have been observed.
void WaitForAcceptedSockets(size_t count) {
base::RunLoop run_loop;
{
base::AutoLock auto_lock(socket_count_lock_);
DCHECK(!waiting_for_accepted_socket_loop_);
if (num_accepted_sockets_ >= count) {
EXPECT_EQ(num_accepted_sockets_, count);
return;
}
waiting_for_accepted_socket_count_ = count;
waiting_for_accepted_socket_loop_ = &run_loop;
}
// Can't do this while holding the lock, since we're waiting on a method
// that grabs the lock off thread.
run_loop.Run();
{
base::AutoLock autolock(socket_count_lock_);
EXPECT_EQ(num_accepted_sockets_, count);
waiting_for_accepted_socket_loop_ = nullptr;
}
}
private:
base::Lock socket_count_lock_;
size_t num_accepted_sockets_ GUARDED_BY(socket_count_lock_) = 0;
size_t waiting_for_accepted_socket_count_ GUARDED_BY(socket_count_lock_) =
0;
raw_ptr<base::RunLoop> waiting_for_accepted_socket_loop_
GUARDED_BY(socket_count_lock_);
};
std::unique_ptr<net::test_server::EmbeddedTestServer> SetUpEmbeddedServer(
PreconnectListener& preconnect_listener) {
std::unique_ptr<net::test_server::EmbeddedTestServer> server =
std::make_unique<net::EmbeddedTestServer>(
net::EmbeddedTestServer::Type::TYPE_HTTPS);
server->SetConnectionListener(&preconnect_listener);
server->SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
server->AddDefaultHandlers(GetTestDataFilePath());
return server;
}
std::unique_ptr<net::test_server::HttpResponse> HandleSignalsRequest(
const url::Origin owner_origin,
const net::test_server::HttpRequest& request) {
base::AutoLock auto_lock(requests_lock_);
if (!base::StartsWith(request.relative_url,
"/interest_group/trusted_bidding_signals.json")) {
return nullptr;
}
auto response = std::make_unique<net::test_server::BasicHttpResponse>();
response->set_content_type("application/json");
const char kJson[] = R"({ "keys": { "key1": "1" } })";
response->set_content(kJson);
response->AddCustomHeader("Access-Control-Allow-Origin",
owner_origin.Serialize());
response->AddCustomHeader(kFledgeHeader, "true");
response->AddCustomHeader("Ad-Auction-Bidding-Signals-Format-Version", "2");
num_signals_requests_++;
return response;
}
void IncrementOwnerRequests(const net::test_server::HttpRequest& request) {
base::AutoLock auto_lock(requests_lock_);
num_owner_requests_++;
}
size_t GetNumOwnerRequests() {
base::AutoLock auto_lock(requests_lock_);
return num_owner_requests_;
}
size_t GetNumSignalsRequests() {
base::AutoLock auto_lock(requests_lock_);
return num_signals_requests_;
}
bool UseConfigWithComponentAuction() const { return GetParam(); }
protected:
base::test::ScopedFeatureList feature_list_;
size_t num_owner_requests_ GUARDED_BY(requests_lock_) = 0;
size_t num_signals_requests_ GUARDED_BY(requests_lock_) = 0;
};
IN_PROC_BROWSER_TEST_P(InterestGroupPreconnectOwnerAndSignalsOriginsTest,
PreconnectsToOwnerAndSignalsOrigins) {
GURL joining_url = embedded_https_test_server().GetURL("c.test", "/echo");
url::Origin joining_origin = url::Origin::Create(joining_url);
ASSERT_TRUE(NavigateToURL(shell(), joining_url));
// Set up owner and bidding signals servers with listeners for preconnections.
PreconnectListener owner_connection_listener;
std::unique_ptr<net::test_server::EmbeddedTestServer> owner_test_server =
SetUpEmbeddedServer(owner_connection_listener);
owner_test_server->RegisterRequestMonitor(
base::BindRepeating(&InterestGroupPreconnectOwnerAndSignalsOriginsTest::
IncrementOwnerRequests,
base::Unretained(this)));
EXPECT_TRUE(owner_test_server->Start());
// Use a bidding script that does not bid to prevent the auction from having a
// winner. If the auction has a winner, there will be a race condition where a
// new bidder worklet will be created for reporting, causing an extra
// preconnect.
GURL script_url = owner_test_server->GetURL(
"a.test", "/interest_group/bidding_logic_do_not_bid.js");
url::Origin owner_origin = url::Origin::Create(script_url);
PreconnectListener signals_connection_listener;
std::unique_ptr<net::test_server::EmbeddedTestServer> signals_test_server =
SetUpEmbeddedServer(signals_connection_listener);
signals_test_server->RegisterRequestHandler(base::BindRepeating(
&InterestGroupPreconnectOwnerAndSignalsOriginsTest::HandleSignalsRequest,
base::Unretained(this), owner_origin));
EXPECT_TRUE(signals_test_server->Start());
GURL trusted_bidding_signals_url = signals_test_server->GetURL(
"b.test", "/interest_group/trusted_bidding_signals.json");
url::Origin signals_origin = url::Origin::Create(trusted_bidding_signals_url);
content_browser_client_->AddToAllowList({signals_origin, owner_origin});
std::string config_template;
if (UseConfigWithComponentAuction()) {
config_template = R"({
seller: $1,
decisionLogicURL: $2,
auctionSignals: "bidderAllowsComponentAuction,"+
"sellerAllowsComponentAuction",
componentAuctions:
[{
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$3],
auctionSignals: "bidderAllowsComponentAuction,"+
"sellerAllowsComponentAuction"
}]
})";
} else {
config_template = R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$3],
sellerTimeout: 3000,
})";
}
std::string auction_config =
JsReplace(config_template, joining_origin,
embedded_https_test_server().GetURL(
"c.test", "/interest_group/decision_logic.js"),
owner_origin);
// We have no interest groups & nothing cached at this point.
base::HistogramTester histogram_tester;
auto result1 = RunAuctionAndWait(auction_config);
histogram_tester.ExpectUniqueSample("Ads.InterestGroup.Auction.Result2",
AuctionResult::kNoInterestGroups, 1);
histogram_tester.ExpectUniqueSample(
"Ads.InterestGroup.Auction.NumOwnerOriginsCachedForPreconnect", 0, 1);
// Create the interest group we'll be using throughout the test.
GURL ad_url = owner_test_server->GetURL("a.test", "/echo?render_winner");
blink::InterestGroup interest_group =
blink::TestInterestGroupBuilder(
/*owner=*/url::Origin::Create(script_url),
/*name=*/"interest_group")
.SetBiddingUrl(script_url)
.SetTrustedBiddingSignalsUrl(trusted_bidding_signals_url)
.SetTrustedBiddingSignalsKeys({{"key1"}})
.SetAds({{{ad_url, std::nullopt}}})
.Build();
// Join the interest group with no ads so that when we run an auction, it will
// get filtered after it's loaded -- but we'll still preconnect to the server.
// Loading the interest group and running UpdateCachedOriginsIfEnabled will
// cache the bidding signals and owner origins.
blink::InterestGroup interest_group_without_ads = interest_group;
interest_group_without_ads.ads = std::nullopt;
AttachInterestGroupObserver();
manager_->JoinInterestGroup(interest_group_without_ads, joining_url);
WaitForAccessObserved({{"global", TestInterestGroupObserver::kJoin,
interest_group_without_ads.owner, "interest_group"}});
GetInterestGroupsForOwner(interest_group.owner);
manager_->UpdateCachedOriginsIfEnabled(interest_group.owner);
std::optional<url::Origin> cached_signals_origin;
EXPECT_TRUE(manager_->GetCachedOwnerAndSignalsOrigins(
interest_group_without_ads.owner, cached_signals_origin));
EXPECT_EQ(cached_signals_origin, signals_origin);
auto result2 = RunAuctionAndWait(auction_config);
histogram_tester.ExpectUniqueSample("Ads.InterestGroup.Auction.Result2",
AuctionResult::kNoInterestGroups, 2);
histogram_tester.ExpectBucketCount(
"Ads.InterestGroup.Auction.NumOwnerOriginsCachedForPreconnect", 1, 1);
// We've preconnected to each server but received no requests.
owner_connection_listener.WaitForAcceptedSockets(1u);
signals_connection_listener.WaitForAcceptedSockets(1u);
EXPECT_EQ(GetNumOwnerRequests(), 0u);
EXPECT_EQ(GetNumSignalsRequests(), 0u);
// Now update our IG to have ads & rerun the auction. It will no longer be
// filtered in the auction. We will expect no new connections. We will use the
// existing connections to fetch trusted signals and code.
AttachInterestGroupObserver();
manager_->JoinInterestGroup(interest_group, joining_url);
WaitForAccessObserved({{"global", TestInterestGroupObserver::kJoin,
interest_group.owner, "interest_group"}});
auto result3 = RunAuctionAndWait(auction_config);
histogram_tester.ExpectTotalCount("Ads.InterestGroup.Auction.Result2", 3);
histogram_tester.ExpectBucketCount("Ads.InterestGroup.Auction.Result2",
AuctionResult::kNoInterestGroups, 2);
histogram_tester.ExpectBucketCount(
"Ads.InterestGroup.Auction.NumOwnerOriginsCachedForPreconnect", 1, 2);
// We *did not* open any new connections. However, we've requested code and
// signals, implying we've used the existing connections.
owner_connection_listener.WaitForAcceptedSockets(1u);
signals_connection_listener.WaitForAcceptedSockets(1u);
EXPECT_EQ(GetNumOwnerRequests(), 1u);
EXPECT_EQ(GetNumSignalsRequests(), 1u);
}
INSTANTIATE_TEST_SUITE_P(All,
InterestGroupPreconnectOwnerAndSignalsOriginsTest,
testing::Bool());
class DedicatedAuctionProcessManagerTest : public InterestGroupBrowserTest {
public:
DedicatedAuctionProcessManagerTest() = default;
~DedicatedAuctionProcessManagerTest() override = default;
};
IN_PROC_BROWSER_TEST_F(
DedicatedAuctionProcessManagerTest,
PidGetsAssignedEvenIfOriginalProcessHandleDeletedShortlyAfterRequestWorkletService) {
// Make sure that a pid still gets assigned to a worklet process even if its
// original ProcessHandle no longer exists.
DedicatedAuctionProcessManager auction_manager(
manager_->trusted_signals_cache());
AuctionProcessManager::ProcessHandle second_process_handle;
{
AuctionProcessManager::ProcessHandle original_process_handle;
ASSERT_TRUE(auction_manager.RequestWorkletService(
AuctionProcessManager::WorkletType::kSeller,
url::Origin::Create(GURL("https://a.test")), nullptr,
&original_process_handle, base::DoNothing()));
ASSERT_TRUE(auction_manager.RequestWorkletService(
AuctionProcessManager::WorkletType::kSeller,
url::Origin::Create(GURL("https://a.test")), nullptr,
&second_process_handle, base::DoNothing()));
ASSERT_EQ(original_process_handle.worklet_process_for_testing(),
second_process_handle.worklet_process_for_testing());
}
base::RunLoop run_loop;
std::optional<base::ProcessId> pid =
second_process_handle.worklet_process_for_testing()->GetPid(
base::BindLambdaForTesting(
[&run_loop](base::ProcessId) { run_loop.Quit(); }));
if (!pid.has_value()) {
run_loop.Run();
}
ASSERT_TRUE(second_process_handle.worklet_process_for_testing()
->GetPid(base::DoNothing())
.has_value());
}
#if BUILDFLAG(IS_ANDROID)
class InterestGroupUseMainThreadInRendererTest
: public InterestGroupBrowserTest {
public:
InterestGroupUseMainThreadInRendererTest() {
feature_list_.InitAndDisableFeature(
features::kFledgeAndroidWorkletOffMainThread);
}
protected:
base::test::ScopedFeatureList feature_list_;
};
// This just makes sure that turning off kFledgeAndroidWorkletOffMainThread
// (which is on in field trial config) doesn't break things.
IN_PROC_BROWSER_TEST_F(InterestGroupUseMainThreadInRendererTest,
BasicOperation) {
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
GURL ad_url =
embedded_https_test_server().GetURL("c.test", "/echo?render_cars");
EXPECT_EQ(kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"))
.SetAds(/*ads=*/{{{ad_url, /*metadata=*/std::nullopt}}})
.Build()));
const char kConfigTemplate[] = R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
})";
RunAuctionAndWaitForURLAndNavigateIframe(
JsReplace(kConfigTemplate, test_origin,
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js")),
/*expected_url=*/ad_url);
}
#endif
class InterestGroupTrustedSignalsKVv2DisabledTest
: public InterestGroupBrowserTest {
public:
explicit InterestGroupTrustedSignalsKVv2DisabledTest() {
scoped_feature_list_.InitAndDisableFeature(
blink::features::kFledgeTrustedSignalsKVv2Support);
}
protected:
base::test::ScopedFeatureList scoped_feature_list_;
};
IN_PROC_BROWSER_TEST_F(InterestGroupTrustedSignalsKVv2DisabledTest,
FeatureDetection) {
GURL test_url =
embedded_https_test_server().GetURL("a.test", "/simple_page.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
ASSERT_EQ(true, EvalJs(shell(), "'protectedAudience' in navigator"));
const char kQueryTrustedSignalsKVv2Support[] = R"(
navigator.protectedAudience.queryFeatureSupport(
'trustedSignalsKVv2');
)";
EXPECT_EQ(false, EvalJs(shell(), kQueryTrustedSignalsKVv2Support));
}
// The test parameter indicates whether the browser process's
// TrustedSignalsCache should be enabled. Inherit from
// InterestGroupPrivateNetworkBrowserTest to enable testing the KVv2 paths
// correctly implement local network protections.
class InterestGroupTrustedSignalsKVv2BrowserTest
: public InterestGroupPrivateNetworkBrowserTest,
public testing::WithParamInterface<bool> {
public:
InterestGroupTrustedSignalsKVv2BrowserTest() {
std::vector<base::test::FeatureRef> enabled_features{
blink::features::kFledgeBiddingAndAuctionServer,
blink::features::kFledgeTrustedSignalsKVv2Support,
features::kFledgeStoreBandAKeysInDB};
std::vector<base::test::FeatureRef> disabled_features;
if (EnableSignalsCache()) {
enabled_features.emplace_back(features::kFledgeUseKVv2SignalsCache);
} else {
disabled_features.emplace_back(features::kFledgeUseKVv2SignalsCache);
}
feature_list_.InitWithFeatures(enabled_features, disabled_features);
}
bool EnableSignalsCache() const { return GetParam(); }
void SetUpOnMainThread() override {
embedded_https_test_server().RegisterRequestHandler(base::BindRepeating(
&InterestGroupTrustedSignalsKVv2BrowserTest::HandleTrustedKVv2Signals,
base::Unretained(this)));
remote_test_server_.RegisterRequestHandler(base::BindRepeating(
&InterestGroupTrustedSignalsKVv2BrowserTest::HandleTrustedKVv2Signals,
base::Unretained(this)));
InterestGroupPrivateNetworkBrowserTest::SetUpOnMainThread();
}
// Sets up default public keys for `signals_origins` using
// `kCoordinatorOrigin`. May only be called once per test.
void ConfigureSignalsServerKeys(
base::span<const url::Origin> signals_origins) {
ConfigureTestPrivacySandboxCoordinatorKeys(
manager_, InterestGroupManager::TrustedServerAPIType::kTrustedKeyValue,
kCoordinatorOrigin, signals_origins);
}
// These test helps set up an auction on simulated public servers with signals
// URLs that are cross-origin to the bidder / seller that uses them.
//
// `expect_success` indicates whether the signals URL is expected to be
// successfully fetched.
//
// `add_access_control_allow_origin_header` controls whether the corresponding
// Access-Control-Allow-Origin header is included in responses.
//
// `signals_on_private_origin` controls whether the signals are servers from a
// private origin or on another public one.
//
// `attest_signals_origin` controls whether the signals URL is considered
// attested.
//
// `add_script_header` sets whether the URL response for the JS file includes
// an "Ad-Auction-Allow-Trusted-Scoring-Signals-From header", and is only an
// option for the seller, since the bidder already implicity provided
// permissions to request data from the signals URL when its interest group
// was joined.
void TestTrustedKVv2BiddingSignalsCrossOrigin(
bool expect_success,
bool add_access_control_allow_origin_header,
bool signals_on_private_origin,
bool attest_signals_origin);
void TestTrustedKVv2ScoringSignalsCrossOrigin(
bool expect_success,
bool add_access_control_allow_origin_header,
bool add_script_header,
bool signals_on_private_origin,
bool attest_signals_origin);
protected:
std::unique_ptr<net::test_server::HttpResponse> HandleTrustedKVv2Signals(
const net::test_server::HttpRequest& request) {
if (!base::StartsWith(request.relative_url,
"/trusted_kvv2_bidding_signals") &&
!base::StartsWith(request.relative_url,
"/trusted_kvv2_scoring_signals")) {
return nullptr;
}
// Only posts should be sent to the KVv2 serer - no GETs or OPTIONs.
EXPECT_EQ(request.method, net::test_server::METHOD_POST);
const char kBiddingBase[] =
R"([
{
"id": 0,
"dataVersion": 100,
"keyGroupOutputs": [
{
"tags": [
"interestGroupNames"
],
"keyValues": {
"group": {
"value": "{\"priorityVector\":{\"foo\":1}}"
}
}
},
{
"tags": [
"keys"
],
"keyValues": {
"key1": {
"value": "1"
},
"key2": {
"value": "\"2\""
}
}
}
]
}
])";
const char kScoringBase[] =
R"([
{
"id": 0,
"dataVersion": 100,
"keyGroupOutputs": [
{
"tags": [
"renderURLs"
],
"keyValues": {
"https://bar.test/": {
"value": "1"
}
}
},
{
"tags": [
"adComponentRenderURLs"
],
"keyValues": {
"https://barsub.test/": {
"value": "2"
},
"https://foosub.test/": {
"value": "\"3\""
}
}
}
]
}
])";
base::AutoLock auto_lock(lock_);
// Decrypt the request.
auto response_key_config = quiche::ObliviousHttpHeaderKeyConfig::Create(
kTestPrivacySandboxCoordinatorId, EVP_HPKE_DHKEM_X25519_HKDF_SHA256,
EVP_HPKE_HKDF_SHA256, EVP_HPKE_AES_256_GCM);
CHECK(response_key_config.ok()) << response_key_config.status();
auto ohttp_gateway = quiche::ObliviousHttpGateway::Create(
GetTestPrivacySandboxCoordinatorPrivateKey(),
response_key_config.value())
.value();
auto received_request = ohttp_gateway.DecryptObliviousHttpRequest(
request.content, "message/ad-auction-trusted-signals-request");
CHECK(received_request.ok()) << received_request.status();
cbor::Value::MapValue compression_group;
compression_group.try_emplace(cbor::Value("compressionGroupId"),
cbor::Value(0));
compression_group.try_emplace(cbor::Value("ttlMs"), cbor::Value(100));
if (base::StartsWith(request.relative_url,
"/trusted_kvv2_bidding_signals")) {
compression_group.try_emplace(
cbor::Value("content"),
cbor::Value(auction_worklet::test::ToCborVector(kBiddingBase)));
} else {
compression_group.try_emplace(
cbor::Value("content"),
cbor::Value(auction_worklet::test::ToCborVector(kScoringBase)));
}
cbor::Value::ArrayValue compression_groups;
compression_groups.emplace_back(std::move(compression_group));
cbor::Value::MapValue body_map;
body_map.try_emplace(cbor::Value("compressionGroups"),
cbor::Value(std::move(compression_groups)));
cbor::Value body_value(std::move(body_map));
std::optional<std::vector<uint8_t>> maybe_body_bytes =
cbor::Writer::Write(body_value);
std::string response_body = auction_worklet::test::CreateKVv2ResponseBody(
base::as_string_view(maybe_body_bytes.value()));
auto response_context =
std::move(received_request).value().ReleaseContext();
// Encrypt the response body.
auto maybe_response = ohttp_gateway.CreateObliviousHttpResponse(
response_body, response_context,
"message/ad-auction-trusted-signals-response");
EXPECT_TRUE(maybe_response.ok()) << maybe_response.status();
auto response = std::make_unique<net::test_server::BasicHttpResponse>();
response->set_content_type("message/ad-auction-trusted-signals-response");
response->set_content(maybe_response->EncapsulateAndSerialize());
response->AddCustomHeader("Ad-Auction-Allowed", "true");
if (!access_control_allow_origin_header_.empty()) {
response->AddCustomHeader("Access-Control-Allow-Origin",
access_control_allow_origin_header_);
}
return response;
}
void SetAccessControlAllowOriginHeader(std::string header) {
base::AutoLock auto_lock(lock_);
access_control_allow_origin_header_ = header;
}
base::test::ScopedFeatureList feature_list_;
const url::Origin kCoordinatorOrigin =
url::Origin::Create(GURL("https://coordinator.test"));
base::Lock lock_;
std::string access_control_allow_origin_header_ GUARDED_BY(lock_);
};
void InterestGroupTrustedSignalsKVv2BrowserTest::
TestTrustedKVv2BiddingSignalsCrossOrigin(
bool expect_success,
bool add_access_control_allow_origin_header,
bool signals_on_private_origin,
bool attest_signals_origin) {
const char kPublisher[] = "a.test";
const char kBidder[] = "b.test";
const char kSeller[] = "c.test";
const char kBidderSignals[] = "d.test";
GURL test_url =
remote_test_server_.GetURL(kPublisher, "/page_with_iframe.html");
GURL ad_url = remote_test_server_.GetURL(kBidder, "/echo?render_cars");
GURL bidder_url = remote_test_server_.GetURL(kBidder, "/echo");
GURL bidder_script_url = remote_test_server_.GetURL(
kBidder, "/interest_group/bidding_logic_trusted_kvv2_bidding_signals.js");
// If `signals_on_private_origin` use embedded_https_test_server(), which is
// considered to be on a private network, instead of `remote_test_server_`,
// which is considered to be on a public one.
GURL bidder_signals_url =
(signals_on_private_origin ? embedded_https_test_server()
: remote_test_server_)
.GetURL(kBidderSignals, "/trusted_kvv2_bidding_signals");
GURL seller_script_url =
remote_test_server_.GetURL(kSeller, "/interest_group/decision_logic.js");
ConfigureSignalsServerKeys({url::Origin::Create(bidder_signals_url)});
url::Origin bidder_origin = url::Origin::Create(bidder_script_url);
url::Origin seller_origin = url::Origin::Create(seller_script_url);
if (add_access_control_allow_origin_header) {
SetAccessControlAllowOriginHeader(
url::Origin::Create(bidder_script_url).Serialize());
}
if (attest_signals_origin) {
content_browser_client_->AddToAllowList(
{url::Origin::Create(bidder_signals_url)});
}
// Navigate to bidder site, and add an interest group.
ASSERT_TRUE(NavigateToURL(shell(), bidder_url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
if (!attest_signals_origin) {
console_observer.SetPattern(
"joinAdInterestGroup of interest group with owner 'https://b.test:*' "
"blocked because it lacks attestation of cross-origin trusted signals "
"origin 'https://d.test:*' or that origin is disallowed by user "
"preferences");
}
auto ig =
blink::TestInterestGroupBuilder(
/*owner=*/bidder_origin,
/*name=*/"group")
.SetBiddingUrl(bidder_script_url)
.SetTrustedBiddingSignalsUrl(bidder_signals_url)
.SetTrustedBiddingSignalsKeys({{"key1", "key2"}})
.SetAds(/*ads=*/{{{ad_url, R"({"ad":"metadata","here":[1,2]})"}}})
.SetTrustedBiddingSignalsCoordinator(
url::Origin::Create(GURL("https://coordinator.test")))
.SetExecutionMode(
blink::InterestGroup::ExecutionMode::kGroupedByOriginMode)
.Build();
if (attest_signals_origin) {
EXPECT_EQ(kSuccess, JoinInterestGroupAndVerify(ig));
} else {
// Can't actually tell the join failed, but it won't verify.
EXPECT_EQ(kSuccess, JoinInterestGroup(ig));
EXPECT_TRUE(console_observer.Wait());
// Auction will fail w/o an IG available.
}
// Register a bidder script that only bids if
// `crossOriginTrustedBiddingSignals` is successfully fetched.
const char kBidScriptTemplate[] = R"(
function generateBid(
interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals,
browserSignals, directFromSellerSignals,
crossOriginTrustedBiddingSignals) {
if ('dataVersion' in browserSignals) {
throw 'Unexpected dataVersion in browserSignals.';
}
if (browserSignals.crossOriginDataVersion !== 100) {
throw 'Unexpected crossOriginDataVersion: ' +
browserSignals.crossOriginDataVersion;
}
if (trustedBiddingSignals !== null) {
throw 'Unexpected trustedBiddingSignals found.';
}
if (crossOriginTrustedBiddingSignals[$1].key1 !== 1 ||
crossOriginTrustedBiddingSignals[$1].key2 !== '2') {
throw 'Unexpected crossOriginTrustedBiddingSignals: ' +
JSON.stringify(crossOriginTrustedBiddingSignals);
}
return {
bid: 1,
render: interestGroup.ads[0].renderURL,
};
})";
network_responder_->RegisterNetworkResponse(
bidder_script_url.path(),
JsReplace(kBidScriptTemplate, url::Origin::Create(bidder_signals_url)),
"application/javascript");
// Navigate to publisher.
ASSERT_TRUE(NavigateToURL(
shell(),
remote_test_server_.GetURL(kPublisher, "/page_with_iframe.html")));
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$3],
})",
seller_origin, seller_script_url, bidder_origin);
if (expect_success) {
EXPECT_EQ(ad_url, RunAuctionAndWaitForUrl(auction_config));
} else {
EXPECT_EQ(base::Value(), RunAuctionAndWait(auction_config));
}
}
void InterestGroupTrustedSignalsKVv2BrowserTest::
TestTrustedKVv2ScoringSignalsCrossOrigin(
bool expect_success,
bool add_access_control_allow_origin_header,
bool add_script_header,
bool signals_on_private_origin,
bool attest_signals_origin) {
const char kPublisher[] = "a.test";
const char kBidder[] = "b.test";
const char kSeller[] = "c.test";
const char kSellerSignals[] = "d.test";
GURL test_url =
remote_test_server_.GetURL(kPublisher, "/page_with_iframe.html");
GURL ad_url = GURL("https://bar.test/");
GURL bidder_url = remote_test_server_.GetURL(kBidder, "/echo");
GURL bidder_script_url =
remote_test_server_.GetURL(kBidder, "/interest_group/bidding_logic.js");
GURL seller_script_url = remote_test_server_.GetURL(
kSeller,
"/interest_group/decision_logic_trusted_kvv2_scoring_signals.js");
// If `signals_on_private_origin` use embedded_https_test_server(), which is
// considered to be on a private network, instead of `remote_test_server_`,
// which is considered to be on a public one.
GURL seller_signals_url =
(signals_on_private_origin ? embedded_https_test_server()
: remote_test_server_)
.GetURL(kSellerSignals, "/trusted_kvv2_scoring_signals");
ConfigureSignalsServerKeys({url::Origin::Create(seller_signals_url)});
url::Origin bidder_origin = url::Origin::Create(bidder_script_url);
url::Origin seller_origin = url::Origin::Create(seller_script_url);
if (add_access_control_allow_origin_header) {
SetAccessControlAllowOriginHeader(
url::Origin::Create(seller_script_url).Serialize());
}
if (attest_signals_origin) {
content_browser_client_->AddToAllowList(
{url::Origin::Create(seller_signals_url)});
}
// Navigate to bidder site, and add an interest group.
ASSERT_TRUE(NavigateToURL(shell(), bidder_url));
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/bidder_origin,
/*name=*/"group")
.SetBiddingUrl(bidder_script_url)
.SetAds({{{ad_url, /*metadata=*/std::nullopt}}})
.SetAdComponents(
{{{GURL("https://barsub.test/"), /*metadata=*/std::nullopt},
{GURL("https://foosub.test/"), /*metadata=*/std::nullopt}}})
.Build()));
const char kBidderScript[] = R"(
function generateBid(interestGroup, auctionSignals, perBuyerSignals,
trustedBiddingSignals, browserSignals) {
const ad = interestGroup.ads[0];
let result = {'ad': ad, 'bid': 1, 'render': ad.renderURL};
if (interestGroup.adComponents && interestGroup.adComponents[0])
result.adComponents =
[interestGroup.adComponents[0].renderURL,
interestGroup.adComponents[1].renderURL];
return result;
})";
network_responder_->RegisterNetworkResponse(
bidder_script_url.path(), kBidderScript, "application/javascript");
// Navigate to publisher.
ASSERT_TRUE(NavigateToURL(shell(), test_url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
if (!attest_signals_origin) {
console_observer.SetPattern(
"Worklet error: runAdAuction() auction with seller 'https://c.test:*' "
"failed because it lacks attestation of cross-origin trusted signals "
"origin 'https://d.test:*' or that origin is disallowed by user "
"preferences");
}
// Register a seller script that only scores if `trustedScoringSignals` is
// successfully fetched.
const char kSellerScriptTemplate[] = R"(
function scoreAd(
adMetadata, bid, auctionConfig, trustedScoringSignals, browserSignals,
directFromSellerSignals, crossOriginTrustedScoringSignals) {
if ('dataVersion' in browserSignals) {
throw 'Unexpected dataVersion in browserSignals.';
}
if (browserSignals.crossOriginDataVersion !== 100) {
throw 'Unexpected crossOriginDataVersion: ' +
browserSignals.crossOriginDataVersion;
}
if (trustedScoringSignals !== null) {
throw 'Unexpected trustedScoringSignals found.';
}
if (crossOriginTrustedScoringSignals[$1].renderURL[browserSignals.renderURL] !== 1 ||
crossOriginTrustedScoringSignals[$1].adComponentRenderURLs[browserSignals.adComponents[0]] !== 2 ||
crossOriginTrustedScoringSignals[$1].adComponentRenderURLs[browserSignals.adComponents[1]] !== '3') {
throw 'Unexpected crossOriginTrustedScoringSignals: ' +
JSON.stringify(crossOriginTrustedScoringSignals);
}
return bid;
}
)";
NetworkResponder::ResponseHeaders extra_js_headers;
if (add_script_header) {
extra_js_headers.emplace_back(
std::string("Ad-Auction-Allow-Trusted-Scoring-Signals-From"),
base::StringPrintf(
"\"%s\"",
url::Origin::Create(seller_signals_url).Serialize().c_str()));
}
network_responder_->RegisterNetworkResponse(
seller_script_url.path(),
JsReplace(kSellerScriptTemplate, url::Origin::Create(seller_signals_url)),
"application/javascript", std::move(extra_js_headers));
std::string auction_config = JsReplace(R"({
seller: $1,
decisionLogicURL: $2,
trustedScoringSignalsURL: $3,
interestGroupBuyers: [$4],
trustedScoringSignalsCoordinator: "https://coordinator.test"
})",
seller_origin, seller_script_url,
seller_signals_url, bidder_origin);
if (expect_success) {
EXPECT_EQ(ad_url, RunAuctionAndWaitForUrl(auction_config));
} else {
EXPECT_EQ(base::Value(), RunAuctionAndWait(auction_config));
}
if (!attest_signals_origin) {
EXPECT_TRUE(console_observer.Wait());
}
}
INSTANTIATE_TEST_SUITE_P(All,
InterestGroupTrustedSignalsKVv2BrowserTest,
testing::Bool());
IN_PROC_BROWSER_TEST_P(InterestGroupTrustedSignalsKVv2BrowserTest,
TrustedKVv2BiddingSignals) {
const char kPublisher[] = "a.test";
const char kBidder[] = "b.test";
const char kSeller[] = "c.test";
const char kBidderSignals[] = "b.test";
GURL test_url =
embedded_https_test_server().GetURL(kPublisher, "/page_with_iframe.html");
GURL ad_url =
embedded_https_test_server().GetURL(kBidder, "/echo?render_cars");
GURL bidder_url = embedded_https_test_server().GetURL(kBidder, "/echo");
GURL bidder_script_url = embedded_https_test_server().GetURL(
kBidder, "/interest_group/bidding_logic_trusted_kvv2_bidding_signals.js");
GURL bidder_signals_url = embedded_https_test_server().GetURL(
kBidderSignals, "/trusted_kvv2_bidding_signals");
GURL seller_script_url = embedded_https_test_server().GetURL(
kSeller, "/interest_group/decision_logic.js");
ConfigureSignalsServerKeys({url::Origin::Create(bidder_signals_url)});
url::Origin bidder_origin = url::Origin::Create(bidder_script_url);
url::Origin seller_origin = url::Origin::Create(seller_script_url);
// Navigate to bidder site, and add an interest group.
ASSERT_TRUE(NavigateToURL(shell(), bidder_url));
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/bidder_origin,
/*name=*/"group")
.SetBiddingUrl(bidder_script_url)
.SetTrustedBiddingSignalsUrl(bidder_signals_url)
.SetTrustedBiddingSignalsKeys({{"key1", "key2"}})
.SetAds(/*ads=*/{{{ad_url, R"({"ad":"metadata","here":[1,2]})"}}})
.SetTrustedBiddingSignalsCoordinator(
url::Origin::Create(GURL("https://coordinator.test")))
.SetExecutionMode(
blink::InterestGroup::ExecutionMode::kGroupedByOriginMode)
.Build()));
// Register a bidder script that only bids if `trustedBiddingSignals` is
// successfully fetched.
const char kBidderScript[] = R"(
function generateBid(
interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals,
browserSignals, directFromSellerSignals,
crossOriginTrustedBiddingSignals) {
if ('crossOriginDataVersion' in browserSignals) {
throw 'Unexpected crossOriginDataVersion in browserSignals.';
}
if (browserSignals.dataVersion !== 100) {
throw 'Unexpected dataVersion: ' + browserSignals.dataVersion;
}
if (crossOriginTrustedBiddingSignals !== null) {
throw 'Unexpected crossOriginTrustedBiddingSignals found.';
}
if (trustedBiddingSignals.key1 !== 1 ||
trustedBiddingSignals.key2 !== '2') {
throw 'Unexpected trustedBiddingSignals: ' +
JSON.stringify(trustedBiddingSignals);
}
return {
bid: 1,
render: interestGroup.ads[0].renderURL,
};
})";
network_responder_->RegisterNetworkResponse(
bidder_script_url.path(), kBidderScript, "application/javascript");
// Navigate to publisher.
ASSERT_TRUE(
NavigateToURL(shell(), embedded_https_test_server().GetURL(
kPublisher, "/page_with_iframe.html")));
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$3],
})",
seller_origin, seller_script_url, bidder_origin);
EXPECT_EQ(ad_url, RunAuctionAndWaitForUrl(auction_config));
}
IN_PROC_BROWSER_TEST_P(InterestGroupTrustedSignalsKVv2BrowserTest,
TrustedKVv2ScoringSignals) {
const char kPublisher[] = "a.test";
const char kBidder[] = "b.test";
const char kSeller[] = "c.test";
const char kSellerSignals[] = "c.test";
GURL test_url =
embedded_https_test_server().GetURL(kPublisher, "/page_with_iframe.html");
GURL ad_url = GURL("https://bar.test/");
GURL bidder_url = embedded_https_test_server().GetURL(kBidder, "/echo");
GURL bidder_script_url = embedded_https_test_server().GetURL(
kBidder, "/interest_group/bidding_logic.js");
GURL seller_script_url = embedded_https_test_server().GetURL(
kSeller,
"/interest_group/decision_logic_trusted_kvv2_scoring_signals.js");
GURL seller_signals_url = embedded_https_test_server().GetURL(
kSellerSignals, "/trusted_kvv2_scoring_signals");
ConfigureSignalsServerKeys({url::Origin::Create(seller_signals_url)});
url::Origin bidder_origin = url::Origin::Create(bidder_script_url);
url::Origin seller_origin = url::Origin::Create(seller_script_url);
// Navigate to bidder site, and add an interest group.
ASSERT_TRUE(NavigateToURL(shell(), bidder_url));
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/bidder_origin,
/*name=*/"group")
.SetBiddingUrl(bidder_script_url)
.SetAds({{{ad_url, /*metadata=*/std::nullopt}}})
.SetAdComponents(
{{{GURL("https://barsub.test/"), /*metadata=*/std::nullopt},
{GURL("https://foosub.test/"), /*metadata=*/std::nullopt}}})
.Build()));
const char kBidderScript[] = R"(
function generateBid(interestGroup, auctionSignals, perBuyerSignals,
trustedBiddingSignals, browserSignals) {
const ad = interestGroup.ads[0];
let result = {'ad': ad, 'bid': 1, 'render': ad.renderURL};
if (interestGroup.adComponents && interestGroup.adComponents[0])
result.adComponents =
[interestGroup.adComponents[0].renderURL,
interestGroup.adComponents[1].renderURL];
return result;
})";
network_responder_->RegisterNetworkResponse(
bidder_script_url.path(), kBidderScript, "application/javascript");
// Navigate to publisher.
ASSERT_TRUE(NavigateToURL(shell(), test_url));
// Register a seller script that only scores if `trustedScoringSignals` is
// successfully fetched.
const char kSellerScript[] = R"(
function scoreAd(
adMetadata, bid, auctionConfig, trustedScoringSignals, browserSignals,
directFromSellerSignals, crossOriginTrustedScoringSignals) {
if ('crossOriginDataVersion' in browserSignals) {
throw 'Unexpected crossOriginDataVersion in browserSignals.';
}
if (browserSignals.dataVersion !== 100) {
throw 'Unexpected dataVersion: ' + browserSignals.dataVersion;
}
if (crossOriginTrustedScoringSignals !== null) {
throw 'Unexpected crossOriginTrustedScoringSignals found.';
}
if (trustedScoringSignals.renderURL[browserSignals.renderURL] !== 1 ||
trustedScoringSignals.adComponentRenderURLs[browserSignals.adComponents[0]] !== 2 ||
trustedScoringSignals.adComponentRenderURLs[browserSignals.adComponents[1]] !== '3') {
throw 'Unexpected trustedScoringSignals: ' + JSON.stringify(trustedScoringSignals);
}
return bid;
}
)";
network_responder_->RegisterNetworkResponse(
seller_script_url.path(), kSellerScript, "application/javascript");
std::string auction_config = JsReplace(R"({
seller: $1,
decisionLogicURL: $2,
trustedScoringSignalsURL: $3,
interestGroupBuyers: [$4],
trustedScoringSignalsCoordinator: "https://coordinator.test"
})",
seller_origin, seller_script_url,
seller_signals_url, bidder_origin);
EXPECT_EQ(ad_url, RunAuctionAndWaitForUrl(auction_config));
}
IN_PROC_BROWSER_TEST_P(InterestGroupTrustedSignalsKVv2BrowserTest,
TrustedKVv2BiddingSignalsCrossOriginSuccess) {
TestTrustedKVv2BiddingSignalsCrossOrigin(
/*expect_success=*/true,
/*add_access_control_allow_origin_header=*/true,
/*signals_on_private_origin=*/false,
/*attest_signals_origin=*/true);
}
IN_PROC_BROWSER_TEST_P(InterestGroupTrustedSignalsKVv2BrowserTest,
TrustedKVv2BiddingSignalsCrossOriginNoCors) {
TestTrustedKVv2BiddingSignalsCrossOrigin(
/*expect_success=*/false,
/*add_access_control_allow_origin_header=*/false,
/*signals_on_private_origin=*/false,
/*attest_signals_origin=*/true);
}
IN_PROC_BROWSER_TEST_P(InterestGroupTrustedSignalsKVv2BrowserTest,
TrustedKVv2BiddingSignalsCrossOriginNoAttestation) {
TestTrustedKVv2BiddingSignalsCrossOrigin(
/*expect_success=*/false,
/*add_access_control_allow_origin_header=*/true,
/*signals_on_private_origin=*/false,
/*attest_signals_origin=*/false);
}
IN_PROC_BROWSER_TEST_P(
InterestGroupTrustedSignalsKVv2BrowserTest,
TrustedKVv2BiddingSignalsCrossOriginPrivateNetworkPrivateNetworkFailure) {
TestTrustedKVv2BiddingSignalsCrossOrigin(
/*expect_success=*/false,
/*add_access_control_allow_origin_header=*/true,
/*signals_on_private_origin=*/true,
/*attest_signals_origin=*/true);
}
IN_PROC_BROWSER_TEST_P(InterestGroupTrustedSignalsKVv2BrowserTest,
TrustedKVv2ScoringSignalsCrossOriginSuccess) {
TestTrustedKVv2ScoringSignalsCrossOrigin(
/*expect_success=*/true,
/*add_access_control_allow_origin_header=*/true,
/*add_script_header=*/true,
/*signals_on_private_origin=*/false,
/*attest_signals_origin=*/true);
}
IN_PROC_BROWSER_TEST_P(InterestGroupTrustedSignalsKVv2BrowserTest,
TrustedKVv2ScoringSignalsCrossOriginNoCors) {
TestTrustedKVv2ScoringSignalsCrossOrigin(
/*expect_success=*/false,
/*add_access_control_allow_origin_header=*/false,
/*add_script_header=*/true,
/*signals_on_private_origin=*/false,
/*attest_signals_origin=*/true);
}
IN_PROC_BROWSER_TEST_P(InterestGroupTrustedSignalsKVv2BrowserTest,
TrustedKVv2ScoringSignalsCrossOriginNoscriptHeader) {
TestTrustedKVv2ScoringSignalsCrossOrigin(
/*expect_success=*/false,
/*add_access_control_allow_origin_header=*/true,
/*add_script_header=*/false,
/*signals_on_private_origin=*/false,
/*attest_signals_origin=*/true);
}
IN_PROC_BROWSER_TEST_P(InterestGroupTrustedSignalsKVv2BrowserTest,
TrustedKVv2ScoringSignalsCrossOriginNoAttestation) {
TestTrustedKVv2ScoringSignalsCrossOrigin(
/*expect_success=*/false,
/*add_access_control_allow_origin_header=*/true,
/*add_script_header=*/true,
/*signals_on_private_origin=*/false,
/*attest_signals_origin=*/false);
}
IN_PROC_BROWSER_TEST_P(
InterestGroupTrustedSignalsKVv2BrowserTest,
TrustedKVv2ScoringSignalsCrossOriginPrivateNetworkPrivateNetworkFailure) {
TestTrustedKVv2ScoringSignalsCrossOrigin(
/*expect_success=*/false,
/*add_access_control_allow_origin_header=*/true,
/*add_script_header=*/true,
/*signals_on_private_origin=*/true,
/*attest_signals_origin=*/true);
}
// Test trying to enable sending creative scanning metadata w/KVv2.
// This currently doesn't actually do anything w/it, since it's only supported
// for V1; the rest of signal handling should still work, however.
IN_PROC_BROWSER_TEST_P(InterestGroupTrustedSignalsKVv2BrowserTest,
TrustedKVv2ScoringSignalsCreativeScanningMetadata) {
const char kPublisher[] = "a.test";
const char kBidder[] = "b.test";
const char kSeller[] = "c.test";
const char kSellerSignals[] = "c.test";
GURL test_url =
embedded_https_test_server().GetURL(kPublisher, "/page_with_iframe.html");
GURL ad_url = GURL("https://bar.test/");
GURL bidder_url = embedded_https_test_server().GetURL(kBidder, "/echo");
GURL bidder_script_url = embedded_https_test_server().GetURL(
kBidder, "/interest_group/bidding_logic.js");
GURL seller_script_url = embedded_https_test_server().GetURL(
kSeller,
"/interest_group/decision_logic_trusted_kvv2_scoring_signals.js");
GURL seller_signals_url = embedded_https_test_server().GetURL(
kSellerSignals, "/trusted_kvv2_scoring_signals");
ConfigureSignalsServerKeys({url::Origin::Create(seller_signals_url)});
url::Origin bidder_origin = url::Origin::Create(bidder_script_url);
url::Origin seller_origin = url::Origin::Create(seller_script_url);
// Navigate to bidder site, and add an interest group.
ASSERT_TRUE(NavigateToURL(shell(), bidder_url));
blink::InterestGroup ig =
blink::TestInterestGroupBuilder(
/*owner=*/bidder_origin,
/*name=*/"group")
.SetBiddingUrl(bidder_script_url)
.SetAds({{{ad_url, /*metadata=*/std::nullopt}}})
.SetAdComponents(
{{{GURL("https://barsub.test/"), /*metadata=*/std::nullopt},
{GURL("https://foosub.test/"), /*metadata=*/std::nullopt}}})
.Build();
ig.ads.value()[0].creative_scanning_metadata = "ad0";
ig.ad_components.value()[0].creative_scanning_metadata = "adc0";
ig.ad_components.value()[1].creative_scanning_metadata = "adc1";
EXPECT_EQ(kSuccess, JoinInterestGroupAndVerify(ig));
const char kBidderScript[] = R"(
function generateBid(interestGroup, auctionSignals, perBuyerSignals,
trustedBiddingSignals, browserSignals) {
const ad = interestGroup.ads[0];
let result = {'ad': ad, 'bid': 1, 'render': ad.renderURL};
if (interestGroup.adComponents && interestGroup.adComponents[0])
result.adComponents =
[interestGroup.adComponents[0].renderURL,
interestGroup.adComponents[1].renderURL];
return result;
})";
network_responder_->RegisterNetworkResponse(
bidder_script_url.path(), kBidderScript, "application/javascript");
// Navigate to publisher.
ASSERT_TRUE(NavigateToURL(shell(), test_url));
// Register a seller script that only scores if `trustedScoringSignals` is
// successfully fetched.
const char kSellerScript[] = R"(
function scoreAd(
adMetadata, bid, auctionConfig, trustedScoringSignals, browserSignals,
directFromSellerSignals, crossOriginTrustedScoringSignals) {
if ('crossOriginDataVersion' in browserSignals) {
throw 'Unexpected crossOriginDataVersion in browserSignals.';
}
if (browserSignals.dataVersion !== 100) {
throw 'Unexpected dataVersion: ' + browserSignals.dataVersion;
}
if (crossOriginTrustedScoringSignals !== null) {
throw 'Unexpected crossOriginTrustedScoringSignals found.';
}
if (trustedScoringSignals.renderURL[browserSignals.renderURL] !== 1 ||
trustedScoringSignals.adComponentRenderURLs[
browserSignals.adComponents[0]] !== 2 ||
trustedScoringSignals.adComponentRenderURLs[
browserSignals.adComponents[1]] !== '3') {
throw 'Unexpected trustedScoringSignals: ' +
JSON.stringify(trustedScoringSignals);
}
return bid;
}
)";
network_responder_->RegisterNetworkResponse(
seller_script_url.path(), kSellerScript, "application/javascript");
std::string auction_config = JsReplace(R"({
seller: $1,
decisionLogicURL: $2,
trustedScoringSignalsURL: $3,
interestGroupBuyers: [$4],
sendCreativeScanningMetadata: true,
trustedScoringSignalsCoordinator: "https://coordinator.test"
})",
seller_origin, seller_script_url,
seller_signals_url, bidder_origin);
EXPECT_EQ(ad_url, RunAuctionAndWaitForUrl(auction_config));
}
class InterestGroupTrustedSignalsKVv2ContextualDataBrowserTest
: public InterestGroupTrustedSignalsKVv2BrowserTest {
public:
void SetUpOnMainThread() override {
embedded_https_test_server().RegisterRequestHandler(
base::BindRepeating(&HandleTrustedKVv2Signals));
InterestGroupPrivateNetworkBrowserTest::SetUpOnMainThread();
}
void TestPerBuyerTKVSignals(const std::string& expected_bidding_key,
const std::string& buyer_tkv_signals) {
const char kPublisher[] = "a.test";
const char kBidder[] = "b.test";
const char kSeller[] = "c.test";
GURL test_url = embedded_https_test_server().GetURL(
kPublisher, "/page_with_iframe.html");
GURL ad_url =
embedded_https_test_server().GetURL(kBidder, "/echo?render_cars");
GURL bidder_url = embedded_https_test_server().GetURL(kBidder, "/echo");
GURL bidder_script_url = embedded_https_test_server().GetURL(
kBidder,
"/interest_group/bidding_logic_trusted_kvv2_bidding_signals.js");
GURL bidder_signals_url = embedded_https_test_server().GetURL(
kBidder, "/trusted_kvv2_bidding_signals");
GURL seller_script_url = embedded_https_test_server().GetURL(
kSeller, "/interest_group/decision_logic.js");
ConfigureSignalsServerKeys({url::Origin::Create(bidder_signals_url)});
url::Origin bidder_origin = url::Origin::Create(bidder_script_url);
url::Origin seller_origin = url::Origin::Create(seller_script_url);
// Navigate to bidder site, and add an interest group.
ASSERT_TRUE(NavigateToURL(shell(), bidder_url));
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/bidder_origin,
/*name=*/"group")
.SetBiddingUrl(bidder_script_url)
.SetTrustedBiddingSignalsUrl(bidder_signals_url)
.SetTrustedBiddingSignalsKeys({{"bidderTKVSignals"}})
.SetAds(
/*ads=*/{{{ad_url, /*metadata=*/std::nullopt}}})
.SetTrustedBiddingSignalsCoordinator(
url::Origin::Create(GURL("https://coordinator.test")))
.SetExecutionMode(
blink::InterestGroup::ExecutionMode::kGroupedByOriginMode)
.Build()));
// Register a bidder script that only bids if `trustedBiddingSignals` is
// successfully fetched.
const char kBidderScript[] = R"(
function generateBid(
interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals,
browserSignals, directFromSellerSignals,
crossOriginTrustedBiddingSignals) {
if ('crossOriginDataVersion' in browserSignals) {
throw 'Unexpected crossOriginDataVersion in browserSignals.';
}
if (crossOriginTrustedBiddingSignals !== null) {
throw 'Unexpected crossOriginTrustedBiddingSignals found.';
}
if (trustedBiddingSignals.bidderTKVSignals !== %s) {
throw 'Unexpected trustedBiddingSignals: ' +
JSON.stringify(trustedBiddingSignals);
}
return {
bid: 1,
render: interestGroup.ads[0].renderURL,
};
})";
network_responder_->RegisterNetworkResponse(
bidder_script_url.path(),
base::StringPrintf(kBidderScript, expected_bidding_key.c_str()),
"application/javascript");
// Navigate to publisher.
ASSERT_TRUE(
NavigateToURL(shell(), embedded_https_test_server().GetURL(
kPublisher, "/page_with_iframe.html")));
const char kAuctionConfig[] =
R"({
seller: "%s",
decisionLogicURL: "%s",
interestGroupBuyers: ["%s"],
perBuyerTKVSignals: {"%s": %s},
})";
std::string auction_config = base::StringPrintf(
kAuctionConfig, seller_origin.Serialize().c_str(),
seller_script_url.spec().c_str(), bidder_origin.Serialize().c_str(),
bidder_origin.Serialize().c_str(), buyer_tkv_signals.c_str());
EXPECT_EQ(ad_url, RunAuctionAndWaitForUrl(auction_config));
}
void TestSellerTKVSignals(const std::string& expected_render_url_value,
const std::string& seller_tkv_signals) {
const char kPublisher[] = "a.test";
const char kBidder[] = "b.test";
const char kSeller[] = "c.test";
GURL test_url = embedded_https_test_server().GetURL(
kPublisher, "/page_with_iframe.html");
GURL ad_url = GURL("https://bar.test/");
GURL bidder_url = embedded_https_test_server().GetURL(kBidder, "/echo");
GURL bidder_script_url = embedded_https_test_server().GetURL(
kBidder, "/interest_group/bidding_logic.js");
GURL seller_script_url = embedded_https_test_server().GetURL(
kSeller,
"/interest_group/decision_logic_trusted_kvv2_scoring_signals.js");
GURL seller_signals_url = embedded_https_test_server().GetURL(
kSeller, "/trusted_kvv2_scoring_signals");
ConfigureSignalsServerKeys({url::Origin::Create(seller_signals_url)});
url::Origin bidder_origin = url::Origin::Create(bidder_script_url);
url::Origin seller_origin = url::Origin::Create(seller_script_url);
// Navigate to bidder site, and add an interest group.
ASSERT_TRUE(NavigateToURL(shell(), bidder_url));
EXPECT_EQ(kSuccess, JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/bidder_origin,
/*name=*/"group")
.SetBiddingUrl(bidder_script_url)
.SetAds({{{ad_url, /*metadata=*/std::nullopt}}})
.Build()));
// Navigate to publisher.
ASSERT_TRUE(NavigateToURL(shell(), test_url));
// Register a seller script that only scores if `trustedScoringSignals` is
// successfully fetched.
const char kSellerScript[] = R"(
function scoreAd(
adMetadata, bid, auctionConfig, trustedScoringSignals, browserSignals,
directFromSellerSignals, crossOriginTrustedScoringSignals) {
if ('crossOriginDataVersion' in browserSignals) {
throw 'Unexpected crossOriginDataVersion in browserSignals.';
}
if (crossOriginTrustedScoringSignals !== null) {
throw 'Unexpected crossOriginTrustedScoringSignals found.';
}
if (trustedScoringSignals.renderURL[browserSignals.renderURL] !== %s) {
throw 'Unexpected trustedScoringSignals: ' + JSON.stringify(trustedScoringSignals);
}
return bid;
})";
network_responder_->RegisterNetworkResponse(
seller_script_url.path(),
base::StringPrintf(kSellerScript, expected_render_url_value.c_str()),
"application/javascript");
const char kAuctionConfig[] =
R"({
seller: "%s",
decisionLogicURL: "%s",
trustedScoringSignalsURL: "%s",
interestGroupBuyers: ["%s"],
trustedScoringSignalsCoordinator: "https://coordinator.test",
sellerTKVSignals: %s
})";
std::string auction_config = base::StringPrintf(
kAuctionConfig, seller_origin.Serialize().c_str(),
seller_script_url.spec().c_str(), seller_signals_url.spec().c_str(),
bidder_origin.Serialize().c_str(), seller_tkv_signals.c_str());
EXPECT_EQ(ad_url, RunAuctionAndWaitForUrl(auction_config));
}
protected:
static std::unique_ptr<net::test_server::HttpResponse>
HandleTrustedKVv2Signals(const net::test_server::HttpRequest& request) {
if (!base::StartsWith(request.relative_url,
"/trusted_kvv2_bidding_signals") &&
!base::StartsWith(request.relative_url,
"/trusted_kvv2_scoring_signals")) {
return nullptr;
}
// Only posts should be sent to the KVv2 serer - no GETs or OPTIONs.
EXPECT_EQ(request.method, net::test_server::METHOD_POST);
constexpr char kBiddingBase[] =
R"([
{
"id": 0,
"keyGroupOutputs": [
{
"tags": [
"keys"
],
"keyValues": {
"bidderTKVSignals": {
"value": "%s"
}
}
}
]
}
])";
constexpr char kScoringBase[] =
R"([
{
"id": 0,
"keyGroupOutputs": [
{
"tags": [
"renderURLs"
],
"keyValues": {
"https://bar.test/": {
"value": "%s"
}
}
}
]
}
])";
// Decrypt the request.
auto response_key_config = quiche::ObliviousHttpHeaderKeyConfig::Create(
kTestPrivacySandboxCoordinatorId, EVP_HPKE_DHKEM_X25519_HKDF_SHA256,
EVP_HPKE_HKDF_SHA256, EVP_HPKE_AES_256_GCM);
CHECK(response_key_config.ok()) << response_key_config.status();
auto ohttp_gateway = quiche::ObliviousHttpGateway::Create(
GetTestPrivacySandboxCoordinatorPrivateKey(),
response_key_config.value())
.value();
auto received_request = ohttp_gateway.DecryptObliviousHttpRequest(
request.content, "message/ad-auction-trusted-signals-request");
CHECK(received_request.ok()) << received_request.status();
// Get the request body as a CBOR value.
base::span<const uint8_t> body_span =
base::as_byte_span(received_request->GetPlaintextData());
base::SpanReader reader(body_span);
// Skip the first 1 byte since the request is always uncompressed.
reader.Skip(1u);
uint32_t length;
reader.ReadU32BigEndian(length);
std::optional<base::span<const uint8_t>> cbor_bytes = reader.Read(length);
CHECK(cbor_bytes);
std::optional<cbor::Value> request_body =
cbor::Reader::Read(base::span(cbor_bytes.value()));
CHECK(request_body);
// Extract the `contextualData` from the request body.
std::string contextual_data = kNoContextualDataValue;
const cbor::Value::MapValue& request_map = request_body->GetMap();
auto per_partition_metadata_it =
request_map.find(cbor::Value("perPartitionMetadata"));
if (per_partition_metadata_it != request_map.end()) {
const cbor::Value::ArrayValue& contextual_data_array =
per_partition_metadata_it->second.GetMap()
.at(cbor::Value("contextualData"))
.GetArray();
// With current browser test framework setup, there should only be one
// contextual data entry.
CHECK(contextual_data_array.size() == 1u);
contextual_data = contextual_data_array[0]
.GetMap()
.at(cbor::Value("value"))
.GetString();
}
// Construct the response body.
std::string escaped_contextual_data;
CHECK(base::EscapeJSONString(contextual_data,
/*put_in_quotes=*/false,
&escaped_contextual_data));
std::string content;
if (base::StartsWith(request.relative_url,
"/trusted_kvv2_bidding_signals")) {
content =
base::StringPrintf(kBiddingBase, escaped_contextual_data.c_str());
} else {
content =
base::StringPrintf(kScoringBase, escaped_contextual_data.c_str());
}
cbor::Value::MapValue compression_group;
compression_group.try_emplace(cbor::Value("compressionGroupId"),
cbor::Value(0));
compression_group.try_emplace(
cbor::Value("content"),
cbor::Value(auction_worklet::test::ToCborVector(content)));
cbor::Value::ArrayValue compression_groups;
compression_groups.emplace_back(std::move(compression_group));
cbor::Value::MapValue body_map;
body_map.try_emplace(cbor::Value("compressionGroups"),
cbor::Value(std::move(compression_groups)));
cbor::Value body_value(std::move(body_map));
std::optional<std::vector<uint8_t>> maybe_body_bytes =
cbor::Writer::Write(body_value);
std::string response_body = auction_worklet::test::CreateKVv2ResponseBody(
base::as_string_view(maybe_body_bytes.value()));
auto response_context =
std::move(received_request).value().ReleaseContext();
// Encrypt the response body.
auto maybe_response = ohttp_gateway.CreateObliviousHttpResponse(
std::move(response_body), response_context,
"message/ad-auction-trusted-signals-response");
EXPECT_TRUE(maybe_response.ok()) << maybe_response.status();
auto response = std::make_unique<net::test_server::BasicHttpResponse>();
response->set_content_type("message/ad-auction-trusted-signals-response");
response->set_content(maybe_response->EncapsulateAndSerialize());
response->AddCustomHeader("Ad-Auction-Allowed", "true");
return response;
}
const url::Origin kCoordinatorOrigin =
url::Origin::Create(GURL("https://coordinator.test"));
};
INSTANTIATE_TEST_SUITE_P(
All,
InterestGroupTrustedSignalsKVv2ContextualDataBrowserTest,
testing::Values(true));
// Test that providing an invalid `buyer_tkv_signals` value for a buyer results
// in the auction still running, but providing no contextual data.
IN_PROC_BROWSER_TEST_P(InterestGroupTrustedSignalsKVv2ContextualDataBrowserTest,
BuyerTkvSignalsRunAdAuctionBadPerBuyerData) {
TestPerBuyerTKVSignals(/*expected_bidding_key=*/kNoContextualDataValue,
/*buyer_tkv_signals=*/"function(){}");
}
IN_PROC_BROWSER_TEST_P(InterestGroupTrustedSignalsKVv2ContextualDataBrowserTest,
PerBuyerTKVSignalsIsUndefined) {
TestPerBuyerTKVSignals(/*expected_bidding_key=*/kNoContextualDataValue,
/*buyer_tkv_signals=*/"undefined");
}
IN_PROC_BROWSER_TEST_P(InterestGroupTrustedSignalsKVv2ContextualDataBrowserTest,
PerBuyerTKVSignalsIsInteger) {
TestPerBuyerTKVSignals(/*expected_bidding_key=*/"100",
/*buyer_tkv_signals=*/"100");
}
IN_PROC_BROWSER_TEST_P(InterestGroupTrustedSignalsKVv2ContextualDataBrowserTest,
PerBuyerTKVSignalsIsString) {
TestPerBuyerTKVSignals(/*expected_bidding_key=*/"\"contextual data\"",
/*buyer_tkv_signals=*/"\"contextual data\"");
}
IN_PROC_BROWSER_TEST_P(InterestGroupTrustedSignalsKVv2ContextualDataBrowserTest,
PerBuyerTKVSignalsIsArray) {
TestPerBuyerTKVSignals(/*expected_bidding_key=*/"\"[1, 2, 3]\"",
/*buyer_tkv_signals=*/"\"[1, 2, 3]\"");
}
IN_PROC_BROWSER_TEST_P(InterestGroupTrustedSignalsKVv2ContextualDataBrowserTest,
PerBuyerTKVSignalsIsNull) {
TestPerBuyerTKVSignals(/*expected_bidding_key=*/"null",
/*buyer_tkv_signals=*/"null");
}
// Passing in a rejected promise as a `perBuyerTKVSignals` should result in no
// signals being sent, but the auction still being run to completion.
IN_PROC_BROWSER_TEST_P(InterestGroupTrustedSignalsKVv2ContextualDataBrowserTest,
PerBuyerTKVSignalsAlreadyRejectedPromise) {
TestPerBuyerTKVSignals(/*expected_bidding_key=*/kNoContextualDataValue,
/*buyer_tkv_signals=*/
R"(new Promise((resolve, reject) => { reject(); }))");
}
// Rejecting a promise passed in as a `perBuyerTKVSignals` should result in no
// signals being sent, but the auction still being run to completion.
IN_PROC_BROWSER_TEST_P(InterestGroupTrustedSignalsKVv2ContextualDataBrowserTest,
BuyerTkvSignalsRunAdAuctionAndThenRejectPromise) {
TestPerBuyerTKVSignals(/*expected_bidding_key=*/kNoContextualDataValue,
/*buyer_tkv_signals=*/
R"(new Promise((resolve, reject) => {
setTimeout(() => { reject(); }, 100);
}))");
}
// Test that providing an invalid `buyer_tkv_signals` value for a buyer via a
// promise that's resolved while an auction is ongoing results in the auction
// still running, but providing no contextual data.
IN_PROC_BROWSER_TEST_P(InterestGroupTrustedSignalsKVv2ContextualDataBrowserTest,
BuyerTkvSignalsRunAdAuctionWithPromiseWithBadData) {
TestPerBuyerTKVSignals(/*expected_bidding_key=*/kNoContextualDataValue,
/*buyer_tkv_signals=*/
R"(new Promise((resolve, reject) => {
setTimeout(() => { resolve(function(){}); },
100);
}))");
}
// Test the case of providing a valid `buyer_tkv_signals` value after the
// auction starts.
IN_PROC_BROWSER_TEST_P(InterestGroupTrustedSignalsKVv2ContextualDataBrowserTest,
BuyerTkvSignalsRunAdAuctionWithPromise) {
TestPerBuyerTKVSignals(/*expected_bidding_key=*/"\"contextual data\"",
/*buyer_tkv_signals=*/
R"(new Promise((resolve, reject) => {
setTimeout(
() => { resolve("contextual data"); },
100);
}))");
}
IN_PROC_BROWSER_TEST_P(InterestGroupTrustedSignalsKVv2ContextualDataBrowserTest,
SellerTKVSignalsIsInteger) {
TestSellerTKVSignals(/*expected_render_url_value=*/"100",
/*seller_tkv_signals=*/"100");
}
IN_PROC_BROWSER_TEST_P(InterestGroupTrustedSignalsKVv2ContextualDataBrowserTest,
SellerTKVSignalsIsString) {
TestSellerTKVSignals(/*expected_render_url_value=*/"\"contextual data\"",
/*seller_tkv_signals=*/"\"contextual data\"");
}
IN_PROC_BROWSER_TEST_P(InterestGroupTrustedSignalsKVv2ContextualDataBrowserTest,
SellerTKVSignalsIsArray) {
TestSellerTKVSignals(/*expected_render_url_value=*/"\"[1, 2, 3]\"",
/*seller_tkv_signals=*/"\"[1, 2, 3]\"");
}
// For "null" test case, `sellerTKVSignals` behaves differently than
// `perBuyerTKVSignals` in that it uses IDL to convert from renderer side config
// to mojom promise value. It seems there is a special handling for "null" in
// the IDL code, which will result an empty promise value in the end.
IN_PROC_BROWSER_TEST_P(InterestGroupTrustedSignalsKVv2ContextualDataBrowserTest,
SellerTKVSignalsIsNull) {
TestSellerTKVSignals(/*expected_render_url_value=*/kNoContextualDataValue,
/*seller_tkv_signals=*/"null");
}
// Same as "null" test case, "undefined" also results an empty promise value.
IN_PROC_BROWSER_TEST_P(InterestGroupTrustedSignalsKVv2ContextualDataBrowserTest,
SellerTKVSignalsIsUndefined) {
TestSellerTKVSignals(/*expected_render_url_value=*/kNoContextualDataValue,
/*seller_tkv_signals=*/"undefined");
}
IN_PROC_BROWSER_TEST_P(InterestGroupTrustedSignalsKVv2ContextualDataBrowserTest,
SellerTKVSignalsRunAdAuctionWithPromise) {
TestSellerTKVSignals(/*expected_render_url_value=*/"\"contextual data\"",
/*seller_tkv_signals=*/
R"(new Promise((resolve, reject) => {
setTimeout(
() => { resolve("contextual data"); },
100);
}))");
}
class DisableKVv2ContextualDataBrowserTest
: public InterestGroupTrustedSignalsKVv2ContextualDataBrowserTest {
public:
DisableKVv2ContextualDataBrowserTest() {
feature_list_.InitAndDisableFeature(
blink::features::kFledgeTrustedSignalsKVv2ContextualData);
}
protected:
base::test::ScopedFeatureList feature_list_;
};
INSTANTIATE_TEST_SUITE_P(All,
DisableKVv2ContextualDataBrowserTest,
testing::Values(true));
IN_PROC_BROWSER_TEST_P(DisableKVv2ContextualDataBrowserTest,
PerBuyerTKVSignalsWithDisabledFeature) {
TestPerBuyerTKVSignals(/*expected_bidding_key=*/kNoContextualDataValue,
/*buyer_tkv_signals=*/"\"contextual data\"");
}
IN_PROC_BROWSER_TEST_P(DisableKVv2ContextualDataBrowserTest,
SellerTKVSignalsWithDisabledFeature) {
TestSellerTKVSignals(/*expected_render_url_value=*/kNoContextualDataValue,
/*seller_tkv_signals=*/"\"contextual data\"");
}
class DisableLocalAuctionInterestGroupBrowserTest
: public InterestGroupBrowserTest {
public:
DisableLocalAuctionInterestGroupBrowserTest() {
feature_list_.InitWithFeatures(
/*enabled_features=*/
{blink::features::kFledgeDisableLocalAdsAuctions},
/*disabled_features=*/
{});
}
~DisableLocalAuctionInterestGroupBrowserTest() override {
content_browser_client_.reset();
}
void SetUpTestWithOneInterestGroup() {
test_url_ =
embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url_));
test_origin_ = url::Origin::Create(test_url_);
ad_url_ = embedded_https_test_server().GetURL(
"c.test", "/set-header?Supports-Loading-Mode: fenced-frame");
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(
blink::TestInterestGroupBuilder(
/*owner=*/test_origin_,
/*name=*/"cars")
.SetBiddingUrl(embedded_https_test_server().GetURL(
"a.test", "/interest_group/bidding_logic.js"))
.SetTrustedBiddingSignalsUrl(
embedded_https_test_server().GetURL(
"a.test",
"/interest_group/trusted_bidding_signals.json"))
.SetTrustedBiddingSignalsKeys({{"key1"}})
.SetAds({{{ad_url_, R"({"ad":"metadata","here":[1,2]})"}}})
.SetAllSellersCapabilities(
{blink::SellerCapabilities::kInterestGroupCounts,
blink::SellerCapabilities::kLatencyStats})
.Build()));
}
protected:
base::test::ScopedFeatureList feature_list_;
GURL test_url_;
url::Origin test_origin_;
GURL ad_url_;
};
IN_PROC_BROWSER_TEST_F(DisableLocalAuctionInterestGroupBrowserTest,
DisableLocalAuctions) {
SetUpTestWithOneInterestGroup();
// If local auctions are disable via kFledgeDisableLocalAdsAuctions and
// there's no "serverResponse" key in the auctionConfig, runAdAuction will
// not throw an error. It will return nullptr, which matches what happens
// when there is no auction winner.
EXPECT_EQ(base::Value(),
RunAuctionAndWait(JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
auctionReportBuyerKeys: [1n],
auctionReportBuyers: {
bidCount: { bucket: 100n, scale: 10 },
}})",
test_origin_,
embedded_https_test_server().GetURL(
"a.test", "/interest_group/decision_logic.js"))));
}
IN_PROC_BROWSER_TEST_F(DisableLocalAuctionInterestGroupBrowserTest,
DisableLocalComponentAuctions) {
SetUpTestWithOneInterestGroup();
// Even if the top-level auction has a server response, verify that the
// component auctions are also not locally hosted.
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicURL: $2,
serverResponse: "serverResponse",
auctionSignals: "bidderAllowsComponentAuction,"+
"sellerAllowsComponentAuction",
componentAuctions:
[{
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
auctionSignals: "bidderAllowsComponentAuction,"+
"sellerAllowsComponentAuction"
},
{
seller: $1,
decisionLogicURL: $2,
interestGroupBuyers: [$1],
auctionSignals: "bidderAllowsComponentAuction,"+
"sellerAllowsComponentAuction"
}]
})",
test_origin_,
embedded_https_test_server().GetURL(test_origin_.host(),
"/interest_group/decision_logic.js"));
EXPECT_EQ(base::Value(), RunAuctionAndWait(auction_config));
}
} // namespace
} // namespace content