blob: 2804a12203a63c921c20f8b294317f2685a56dad [file] [log] [blame]
// Copyright 2021 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <sstream>
#include <string>
#include <vector>
#include "base/callback.h"
#include "base/callback_forward.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/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/strings/stringprintf.h"
#include "base/synchronization/lock.h"
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_timeouts.h"
#include "base/thread_annotations.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "components/network_session_configurator/common/network_switches.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_service_impl.h"
#include "content/browser/interest_group/interest_group_manager_impl.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/public/browser/browser_context.h"
#include "content/public/common/content_client.h"
#include "content/public/common/content_features.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_utils.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/mojom/bidder_worklet.mojom.h"
#include "content/shell/browser/shell.h"
#include "content/test/test_content_browser_client.h"
#include "content/test/test_fenced_frame_url_mapping_result_observer.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/embedded_test_server.h"
#include "net/test/embedded_test_server/http_request.h"
#include "net/test/embedded_test_server/http_response.h"
#include "services/data_decoder/public/cpp/test_support/in_process_data_decoder.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 "testing/gmock/include/gmock/gmock-matchers.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/common/interest_group/ad_auction_constants.h"
#include "third_party/blink/public/common/interest_group/interest_group.h"
#include "third_party/blink/public/mojom/interest_group/ad_auction_service.mojom.h"
#include "third_party/blink/public/mojom/interest_group/interest_group_types.mojom.h"
#include "url/gurl.h"
#include "url/origin.h"
#include "url/url_constants.h"
namespace content {
namespace {
using ::testing::Eq;
using ::testing::Optional;
// Creates string representations of ads and adComponents arrays from the
// provided InterestGroup::Ads.
std::string MakeAdsArg(const std::vector<blink::InterestGroup::Ad>& ads) {
std::string out = "";
for (const auto& ad : ads) {
if (!out.empty())
out += ",";
if (ad.metadata) {
// Since ad.metadata is JSON, it shouldn't be wrapped in quotes, so can't
// use JsReplace.
out += base::StringPrintf("{renderUrl : '%s', metadata: %s}",
ad.render_url.spec().c_str(),
ad.metadata->c_str());
} else {
out += JsReplace("{renderUrl : $1}", ad.render_url);
}
}
return "[" + out + "]";
}
class AllowlistedOriginContentBrowserClient : public TestContentBrowserClient {
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::RenderFrameHost* render_frame_host,
ContentBrowserClient::InterestGroupApiOperation operation,
const url::Origin& top_frame_origin,
const url::Origin& api_origin) override {
return allow_list_.contains(top_frame_origin) &&
allow_list_.contains(api_origin);
}
private:
base::flat_set<url::Origin> allow_list_;
};
// Allows registering responses to network requests.
class NetworkResponder {
public:
explicit NetworkResponder(net::EmbeddedTestServer& server) {
server.RegisterRequestHandler(base::BindRepeating(
&NetworkResponder::RequestHandler, base::Unretained(this)));
}
NetworkResponder(const NetworkResponder&) = delete;
NetworkResponder& operator=(const NetworkResponder&) = delete;
void RegisterNetworkResponse(
const std::string& url_path,
const std::string& body,
const std::string& mime_type = "application/json") {
base::AutoLock auto_lock(response_map_lock_);
Response response;
response.body = body;
response.mime_type = mime_type;
response_map_[url_path] = response;
}
// 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");
}
private:
struct Response {
std::string body;
std::string mime_type;
};
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("X-Allow-FLEDGE", "true");
response->set_code(net::HTTP_OK);
response->set_content(it->second.body);
response->set_content_type(it->second.mime_type);
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_);
};
class InterestGroupTestObserver
: public InterestGroupManagerImpl::InterestGroupObserverInterface {
public:
using Entry = std::tuple<
InterestGroupManagerImpl::InterestGroupObserverInterface::AccessType,
std::string,
std::string>;
void OnInterestGroupAccessed(
const base::Time& access_time,
InterestGroupManagerImpl::InterestGroupObserverInterface::AccessType type,
const std::string& owner_origin,
const std::string& name) override {
accesses.emplace_back(Entry{type, owner_origin, name});
}
std::vector<Entry> accesses;
};
class InterestGroupBrowserTest : public ContentBrowserTest {
public:
InterestGroupBrowserTest() {
feature_list_.InitWithFeatures(
/*`enabled_features`=*/
{blink::features::kInterestGroupStorage,
blink::features::kAdInterestGroupAPI, blink::features::kParakeet,
blink::features::kFledge, blink::features::kAllowURNsInIframes},
/*disabled_features=*/
{blink::features::kFencedFrames});
}
~InterestGroupBrowserTest() override {
if (old_content_browser_client_)
SetBrowserClientForTesting(old_content_browser_client_);
}
void SetUpOnMainThread() override {
ContentBrowserTest::SetUpOnMainThread();
host_resolver()->AddRule("*", "127.0.0.1");
ASSERT_TRUE(embedded_test_server()->Start());
https_server_ = std::make_unique<net::EmbeddedTestServer>(
net::test_server::EmbeddedTestServer::TYPE_HTTPS);
https_server_->SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
https_server_->AddDefaultHandlers(GetTestDataFilePath());
https_server_->RegisterRequestMonitor(base::BindRepeating(
&InterestGroupBrowserTest::OnHttpsTestServerRequestMonitor,
base::Unretained(this)));
network_responder_ = std::make_unique<NetworkResponder>(*https_server_);
ASSERT_TRUE(https_server_->Start());
manager_ = static_cast<InterestGroupManagerImpl*>(
shell()
->web_contents()
->GetBrowserContext()
->GetDefaultStoragePartition()
->GetInterestGroupManager());
observer_ = std::make_unique<InterestGroupTestObserver>();
content_browser_client_.SetAllowList(
{url::Origin::Create(https_server_->GetURL("a.test", "/")),
url::Origin::Create(https_server_->GetURL("b.test", "/")),
url::Origin::Create(https_server_->GetURL("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.
url::Origin::Create(embedded_test_server()->GetURL("a.test", "/")),
url::Origin::Create(embedded_test_server()->GetURL("b.test", "/")),
url::Origin::Create(embedded_test_server()->GetURL("c.test", "/"))});
old_content_browser_client_ =
SetBrowserClientForTesting(&content_browser_client_);
}
[[nodiscard]] bool JoinInterestGroupInJS(url::Origin owner,
std::string name) {
return "done" ==
EvalJs(shell(),
base::StringPrintf(R"(
(function() {
navigator.joinAdInterestGroup(
{name: '%s', owner: '%s'}, /*joinDurationSec=*/ 300);
return 'done';
})())",
name.c_str(), owner.Serialize().c_str()));
}
// 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]] bool JoinInterestGroupInJS(
const blink::InterestGroup& group,
const absl::optional<ToRenderFrameHost> execution_target =
absl::nullopt) {
// TODO(qingxin): Use base::Value to replace ostringstream.
std::ostringstream buf;
buf << "{"
<< "name: '" << group.name << "', "
<< "owner: '" << group.owner << "'";
if (group.bidding_url) {
buf << ", biddingLogicUrl: '" << *group.bidding_url << "'";
}
if (group.bidding_wasm_helper_url) {
buf << ", biddingWasmHelperUrl: '" << *group.bidding_wasm_helper_url
<< "'";
}
if (group.update_url) {
buf << ", dailyUpdateUrl: '" << *group.update_url << "'";
}
if (group.trusted_bidding_signals_url) {
buf << ", trustedBiddingSignalsUrl: '"
<< *group.trusted_bidding_signals_url << "'";
}
if (group.user_bidding_signals) {
buf << ", userBiddingSignals: " << group.user_bidding_signals.value();
}
if (group.trusted_bidding_signals_keys) {
buf << ", trustedBiddingSignalsKeys: [";
for (size_t i = 0; i < group.trusted_bidding_signals_keys->size(); ++i) {
if (i > 0)
buf << ",";
buf << "'" << (*group.trusted_bidding_signals_keys)[i] << "'";
}
buf << "]";
}
if (group.ads) {
buf << ", ads: " << MakeAdsArg(*group.ads);
}
if (group.ad_components) {
buf << ", adComponents: " << MakeAdsArg(*group.ad_components);
}
buf << "}";
return "done" == EvalJs(execution_target ? *execution_target : shell(),
base::StringPrintf(R"(
(function() {
navigator.joinAdInterestGroup(
%s, /*join_duration_sec=*/ 300);
return 'done';
})())",
buf.str().c_str()));
}
// If `execution_target` is non-null, uses it as the target. Otherwise, uses
// shell().
EvalJsResult UpdateInterestGroupsInJS(
const absl::optional<ToRenderFrameHost> execution_target =
absl::nullopt) {
return EvalJs(execution_target ? *execution_target : shell(), R"(
(function() {
try {
navigator.updateAdInterestGroups();
} catch (e) {
return e.toString();
}
return 'done';
})())");
}
// If `execution_target` is non-null, uses it as the target. Otherwise, uses
// shell().
bool LeaveInterestGroupInJS(
url::Origin owner, std::string name,
const absl::optional<ToRenderFrameHost> execution_target =
absl::nullopt) {
return "done" ==
EvalJs(execution_target ? *execution_target : shell(),
base::StringPrintf(R"(
(function() {
navigator.leaveAdInterestGroup({name: '%s', owner: '%s'});
return 'done';
})())",
name.c_str(), owner.Serialize().c_str()));
}
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;
}
std::vector<StorageInterestGroup> GetInterestGroupsForOwner(
const url::Origin& owner) {
std::vector<StorageInterestGroup> interest_groups;
base::RunLoop run_loop;
manager_->GetInterestGroupsForOwner(
owner, base::BindLambdaForTesting(
[&run_loop, &interest_groups](
std::vector<StorageInterestGroup> groups) {
interest_groups = std::move(groups);
run_loop.Quit();
}));
run_loop.Run();
return interest_groups;
}
std::vector<std::pair<url::Origin, std::string>> GetAllInterestGroups() {
std::vector<std::pair<url::Origin, std::string>> interest_groups;
for (const auto& owner : GetAllInterestGroupsOwners()) {
for (const auto& storage_group : GetInterestGroupsForOwner(owner)) {
interest_groups.emplace_back(storage_group.interest_group.owner,
storage_group.interest_group.name);
}
}
return interest_groups;
}
int GetJoinCount(const url::Origin& owner, const std::string& name) {
for (const auto& storage_group : GetInterestGroupsForOwner(owner)) {
if (storage_group.interest_group.name == name) {
return storage_group.bidding_browser_signals->join_count;
}
}
return 0;
}
// If `execution_target` is non-null, uses it as the target. Otherwise, uses
// shell().
[[nodiscard]] bool JoinInterestGroupAndWaitInJs(
const blink::InterestGroup& group,
const absl::optional<ToRenderFrameHost> execution_target =
absl::nullopt) {
int initial_count = GetJoinCount(group.owner, group.name);
if (!JoinInterestGroupInJS(group, execution_target)) {
return false;
}
while (GetJoinCount(group.owner, group.name) != initial_count + 1) {
}
return true;
}
// Simplified method to join an interest group for tests that only care about
// a few fields.
bool JoinInterestGroupAndWaitInJs(
const url::Origin& owner,
const std::string& name,
absl::optional<GURL> bidding_url = absl::nullopt,
absl::optional<std::vector<blink::InterestGroup::Ad>> ads = absl::nullopt,
absl::optional<std::vector<blink::InterestGroup::Ad>> ad_components =
absl::nullopt) {
return JoinInterestGroupAndWaitInJs(blink::InterestGroup(
/*expiry=*/base::Time(), owner, name, std::move(bidding_url),
/*bidding_wasm_helper_url=*/absl::nullopt,
/*update_url=*/absl::nullopt,
/*trusted_bidding_signals_url=*/absl::nullopt,
/*trusted_bidding_signals_keys=*/absl::nullopt,
/*user_bidding_signals=*/absl::nullopt, std::move(ads),
std::move(ad_components)));
}
bool LeaveInterestGroupAndWait(const url::Origin& owner,
const std::string& name) {
if (!LeaveInterestGroupInJS(owner, name)) {
return false;
}
while (GetJoinCount(owner, name) != 0) {
}
return true;
}
// 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 absl::optional<ToRenderFrameHost> execution_target =
absl::nullopt) {
return EvalJs(execution_target ? *execution_target : shell(),
base::StringPrintf(
R"(
(async function() {
try {
return await navigator.runAdAuction(%s);
} catch (e) {
return e.toString();
}
})())",
auction_config_json.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 absl::optional<ToRenderFrameHost> execution_target =
absl::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->GetMainFrame());
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. Returns the mapped URL.
void RunAuctionAndWaitForURLAndNavigateIframe(
const std::string& auction_config_json,
GURL expected_url) {
auto result = RunAuctionAndWait(auction_config_json,
/*execution_target=*/absl::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;
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 absl::optional<ToRenderFrameHost> execution_target =
absl::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 absl::optional<ToRenderFrameHost> execution_target =
absl::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(const std::vector<StorageInterestGroup>&)>
condition) {
while (true) {
if (condition.Run(GetInterestGroupsForOwner(owner)))
break;
}
}
// Waits for `url` to be requested by `https_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_);
received_https_test_server_requests_.insert(request.GetURL());
if (wait_for_url_ == request.GetURL()) {
wait_for_url_ = GURL();
request_run_loop_->Quit();
}
}
void ClearReceivedRequests() {
base::AutoLock auto_lock(requests_lock_);
received_https_test_server_requests_.clear();
}
bool HasServerSeenUrl(const GURL& url) {
base::AutoLock auto_lock(requests_lock_);
return received_https_test_server_requests_.find(url) !=
received_https_test_server_requests_.end();
}
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"(
(function() {
try {
navigator.joinAdInterestGroup(
{
name: 'cars',
owner: $1,
},
/*joinDurationSec=*/1);
} 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"(
(function() {
try {
navigator.leaveAdInterestGroup({name: '%s', owner: '%s'});
} catch (e) {
return e.toString();
}
return 'done';
})())",
name.c_str(), origin.Serialize().c_str())));
}
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, feature %s will not be enabled by default by "
"Permissions Policy (thus calling %s will be rejected with "
"NotAllowedError) in cross-origin iframes or same-origin iframes nested"
" in cross-origin iframes",
feature.c_str(), api.c_str());
}
void ConvertFencedFrameURNToURL(
const GURL& urn_url,
TestFencedFrameURLMappingResultObserver* observer,
const absl::optional<ToRenderFrameHost> execution_target =
absl::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);
}
absl::optional<GURL> ConvertFencedFrameURNToURLInJS(
const GURL& urn_url,
const absl::optional<ToRenderFrameHost> execution_target =
absl::nullopt) {
ToRenderFrameHost adapter(execution_target ? *execution_target : shell());
EvalJsResult result = EvalJs(adapter, JsReplace(R"(
navigator.deprecatedURNToURL($1)
)",
urn_url));
if (!result.error.empty() || result.value.is_none())
return absl::nullopt;
return GURL(result.ExtractString());
}
void AttachInterestGroupObserver() {
manager_->AddInterestGroupObserver(observer_.get());
}
void ExpectAccessObserved(
const std::vector<InterestGroupTestObserver::Entry>& expected) {
EXPECT_EQ(expected, observer_->accesses);
}
WebContentsImpl* web_contents() const {
return static_cast<WebContentsImpl*>(shell()->web_contents());
}
protected:
std::unique_ptr<net::EmbeddedTestServer> https_server_;
base::test::ScopedFeatureList feature_list_;
AllowlistedOriginContentBrowserClient content_browser_client_;
raw_ptr<ContentBrowserClient> old_content_browser_client_;
std::unique_ptr<InterestGroupTestObserver> observer_;
raw_ptr<InterestGroupManagerImpl> manager_;
base::Lock requests_lock_;
std::set<GURL> received_https_test_server_requests_
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_;
};
// At the moment, InterestGroups use URN urls when fenced frames are enabled,
// and normal URLs when not. This means they require ads be loaded in fenced
// frames when Chrome is running with the option enabled.
class InterestGroupFencedFrameBrowserTest
: public InterestGroupBrowserTest,
public ::testing::WithParamInterface<
blink::features::FencedFramesImplementationType> {
public:
InterestGroupFencedFrameBrowserTest() {
// Tests are run with both the ShadowDOM and MPArch ("Multi-Page
// Architecture") fenced frames implementations.
feature_list_.InitAndEnableFeatureWithParameters(
blink::features::kFencedFrames,
{{"implementation_type", GetFencedFrameFeatureParam()}});
}
const char* GetFencedFrameFeatureParam() const {
switch (GetParam()) {
case blink::features::FencedFramesImplementationType::kShadowDOM:
return "shadow_dom";
case blink::features::FencedFramesImplementationType::kMPArch:
return "mparch";
}
}
~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,
absl::optional<ToRenderFrameHost> execution_target = absl::nullopt) {
if (!execution_target)
execution_target = shell();
content::EvalJsResult urn_url_string =
RunAuctionAndWait(auction_config_json, execution_target);
ASSERT_TRUE(urn_url_string.value.is_string())
<< "Expected string, but got " << urn_url_string.value;
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());
NavigateFencedFrameAndWait(urn_url, expected_ad_url, *execution_target);
}
// Navigates the only fenced frame in `execution_target` to `url` and waits
// for the navigation to complete, 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.
void NavigateFencedFrameAndWait(const GURL& url,
const GURL& expected_url,
const ToRenderFrameHost& execution_target) {
// Use to wait for navigation completion in the ShadowDOM case only.
// Harmlessly created but not used in the MPArch case.
TestFrameNavigationObserver observer(
GetFencedFrameRenderFrameHost(execution_target));
EXPECT_TRUE(ExecJs(
execution_target,
JsReplace("document.querySelector('fencedframe').src = $1;", url)));
// 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.
ASSERT_EQ(GURL(url::kAboutBlankURL), expected_url);
}
switch (GetParam()) {
case blink::features::FencedFramesImplementationType::kShadowDOM: {
observer.Wait();
break;
}
case blink::features::FencedFramesImplementationType::kMPArch: {
// Wait for the load to complete.
FencedFrame* fenced_frame = GetFencedFrame(execution_target);
fenced_frame->WaitForDidStopLoadingForTesting();
}
}
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) {
switch (GetParam()) {
case blink::features::FencedFramesImplementationType::kShadowDOM: {
// Make sure there's only one child frame.
CHECK(!ChildFrameAt(execution_target, 1));
CHECK(ChildFrameAt(execution_target, 0));
return static_cast<RenderFrameHostImpl*>(
ChildFrameAt(execution_target, 0));
}
case blink::features::FencedFramesImplementationType::kMPArch: {
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) {
CHECK_EQ(GetParam(),
blink::features::FencedFramesImplementationType::kMPArch);
std::vector<FencedFrame*> fenced_frames =
static_cast<RenderFrameHostImpl*>(execution_target.render_frame_host())
->GetFencedFrames();
CHECK_EQ(1u, fenced_frames.size());
return fenced_frames[0];
}
// 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) {
GURL test_url =
https_server_->GetURL("a.test", "/fenced_frames/basic.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
GURL ad_url = https_server_->GetURL("c.test", "/fenced_frames/nested.html");
EXPECT_TRUE(JoinInterestGroupAndWaitInJs(
/*owner=*/url::Origin::Create(test_url),
/*name=*/"cars",
/*bidding_url=*/
https_server_->GetURL("a.test", "/interest_group/bidding_logic.js"),
/*ads=*/{{{ad_url, /*metadata=*/absl::nullopt}}},
/*ad_components=*/{{{ad_component_url, /*metadata=*/absl::nullopt}}}));
ASSERT_NO_FATAL_FAILURE(RunAuctionAndNavigateFencedFrame(
ad_url, JsReplace(
R"({
seller: $1,
decisionLogicUrl: $2,
interestGroupBuyers: [$1]
})",
url::Origin::Create(test_url),
https_server_->GetURL(
"a.test", "/interest_group/decision_logic.js"))));
// Get first component URL from the fenced frame.
RenderFrameHost* ad_frame = GetFencedFrameRenderFrameHost(shell());
absl::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);
}
absl::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.error.empty())
return absl::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.value.is_list());
if (!result.value.is_list())
return absl::nullopt;
std::vector<GURL> out;
for (const auto& value : result.value.GetList()) {
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::kMaxAdAuctionAdComponents. 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) {
while (expected_ad_component_urls.size() <
blink::kMaxAdAuctionAdComponents) {
expected_ad_component_urls.emplace_back(GURL(url::kAboutBlankURL));
}
absl::optional<std::vector<GURL>> all_component_urls =
GetAdAuctionComponentsInJS(render_frame_host,
blink::kMaxAdAuctionAdComponents);
ASSERT_TRUE(all_component_urls);
ASSERT_EQ(blink::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 < blink::kMaxAdAuctionAdComponents; ++i) {
absl::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,
blink::kMaxAdAuctionAdComponents + 1));
EXPECT_EQ(all_component_urls,
GetAdAuctionComponentsInJS(render_frame_host,
blink::kMaxAdAuctionAdComponents + 2));
EXPECT_EQ(all_component_urls,
GetAdAuctionComponentsInJS(render_frame_host, 32768));
}
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) {
feature_list_.InitAndEnableFeature(
features::kPrivateNetworkAccessRespectPreflightResults);
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)));
EXPECT_TRUE(remote_test_server_.Start());
}
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();
// Extend allow list to include the remote server.
content_browser_client_.AddToAllowList(
{url::Origin::Create(remote_test_server_.GetURL("a.test", "/")),
url::Origin::Create(remote_test_server_.GetURL("b.test", "/")),
url::Origin::Create(remote_test_server_.GetURL("c.test", "/"))});
}
protected:
// Test server which is treated as remote, due to command line options. Can't
// use "Content-Security-Policy: treat-as-public-address", because that would
// block all local requests, including loading the seller script, even if the
// seller script had the same header.
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(
blink::features::kAdInterestGroupAPIRestrictedPolicyByDefault);
}
protected:
base::test::ScopedFeatureList feature_list_;
};
INSTANTIATE_TEST_SUITE_P(
All,
InterestGroupFencedFrameBrowserTest,
::testing::Values(
blink::features::FencedFramesImplementationType::kShadowDOM,
blink::features::FencedFramesImplementationType::kMPArch));
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, JoinLeaveInterestGroup) {
GURL test_url_a = https_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));
AttachInterestGroupObserver();
// This join should succeed and be added to the database.
EXPECT_TRUE(JoinInterestGroupAndWaitInJs(test_origin_a, "cars"));
// This join should fail and throw an exception since a.test is not the same
// origin as foo.a.test.
EXPECT_FALSE(JoinInterestGroupInJS(
url::Origin::Create(GURL("https://foo.a.test")), "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_FALSE(JoinInterestGroupInJS(blink::InterestGroup(
/*expiry=*/base::Time(),
/*owner=*/test_origin_a,
/*name=*/"bicycles",
/*bidding_url=*/GURL("https://bid.a.test"),
/*bidding_wasm_helper_url=*/absl::nullopt,
/*update_url=*/absl::nullopt,
/*trusted_bidding_signals_url=*/absl::nullopt,
/*trusted_bidding_signals_keys=*/absl::nullopt,
/*user_bidding_signals=*/absl::nullopt,
/*ads=*/absl::nullopt,
/*ad_components=*/absl::nullopt)));
// This join should fail and throw an exception since a.test is not the same
// origin as the update_url, update.a.test.
EXPECT_FALSE(JoinInterestGroupInJS(blink::InterestGroup(
/*expiry=*/base::Time(),
/*owner=*/test_origin_a,
/*name=*/"tricycles",
/*bidding_url=*/absl::nullopt,
/*bidding_wasm_helper_url=*/absl::nullopt,
/*update_url=*/GURL("https://update.a.test"),
/*trusted_bidding_signals_url=*/absl::nullopt,
/*trusted_bidding_signals_keys=*/absl::nullopt,
/*user_bidding_signals=*/absl::nullopt,
/*ads=*/absl::nullopt,
/*ad_components=*/absl::nullopt)));
// This join should fail and throw an exception since a.test is not the same
// origin as the trusted_bidding_signals_url, signals.a.test.
EXPECT_FALSE(JoinInterestGroupInJS(blink::InterestGroup(
/*expiry=*/base::Time(),
/*owner=*/test_origin_a,
/*name=*/"four-wheelers",
/*bidding_url=*/absl::nullopt,
/*bidding_wasm_helper_url=*/absl::nullopt,
/*update_url=*/absl::nullopt,
/*trusted_bidding_signals_url=*/GURL("https://signals.a.test"),
/*trusted_bidding_signals_keys=*/absl::nullopt,
/*user_bidding_signals=*/absl::nullopt,
/*ads=*/absl::nullopt,
/*ad_components=*/absl::nullopt)));
// 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.
GURL test_url_d = https_server_->GetURL("d.test", "/echo");
url::Origin test_origin_d = url::Origin::Create(test_url_d);
ASSERT_TRUE(NavigateToURL(shell(), test_url_d));
EXPECT_TRUE(JoinInterestGroupInJS(test_origin_d, "toys"));
// Another successful join.
GURL test_url_b = https_server_->GetURL("b.test", "/echo");
url::Origin test_origin_b = url::Origin::Create(test_url_b);
ASSERT_TRUE(NavigateToURL(shell(), test_url_b));
EXPECT_TRUE(JoinInterestGroupAndWaitInJs(test_origin_b, "trucks"));
// Check that only the a.test and b.test interest groups were added to
// the database.
std::vector<std::pair<url::Origin, std::string>> expected_groups = {
{test_origin_a, "cars"}, {test_origin_b, "trucks"}};
std::vector<std::pair<url::Origin, std::string>> 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 for that site so we can try
// to remove it.
manager_->JoinInterestGroup(
blink::InterestGroup(
/*expiry=*/base::Time::Now() + base::Seconds(300),
/*owner=*/test_origin_d,
/*name=*/"candy",
/*bidding_url=*/absl::nullopt,
/*bidding_wasm_helper_url=*/absl::nullopt,
/*update_url=*/absl::nullopt,
/*trusted_bidding_signals_url=*/absl::nullopt,
/*trusted_bidding_signals_keys=*/absl::nullopt,
/*user_bidding_signals=*/absl::nullopt,
/*ads=*/absl::nullopt,
/*ad_components=*/absl::nullopt),
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.
EXPECT_TRUE(LeaveInterestGroupInJS(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_TRUE(LeaveInterestGroupInJS(test_origin_b, "cars"));
// This leave should silently fail because it is cross-origin.
ASSERT_TRUE(NavigateToURL(shell(), test_url_a));
EXPECT_TRUE(LeaveInterestGroupInJS(test_origin_b, "trucks"));
// This leave should succeed.
EXPECT_TRUE(LeaveInterestGroupAndWait(test_origin_a, "cars"));
// We expect that test_origin_b and the (injected) test_origin_d's interest
// groups remain.
expected_groups = {{test_origin_b, "trucks"}, {test_origin_d, "candy"}};
received_groups = GetAllInterestGroups();
EXPECT_THAT(received_groups,
testing::UnorderedElementsAreArray(expected_groups));
ExpectAccessObserved(
{{InterestGroupTestObserver::kJoin, test_origin_a.Serialize(), "cars"},
{InterestGroupTestObserver::kJoin, test_origin_b.Serialize(), "trucks"},
{InterestGroupTestObserver::kJoin, test_origin_d.Serialize(), "candy"},
{InterestGroupTestObserver::kLeave, test_origin_b.Serialize(), "cars"},
{InterestGroupTestObserver::kLeave, test_origin_a.Serialize(), "cars"}});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
JoinInterestGroupInvalidOwner) {
ASSERT_TRUE(NavigateToURL(shell(), https_server_->GetURL("a.test", "/echo")));
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"(
(function() {
try {
navigator.joinAdInterestGroup(
{
name: 'cars',
owner: 'https://invalid^&',
},
/*joinDurationSec=*/1);
} catch (e) {
return e.toString();
}
return 'done';
})())"));
ExpectAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
JoinInterestGroupOwnerDoesntMatchFrame) {
const GURL page_url = https_server_->GetURL("a.test", "/echo");
ASSERT_TRUE(NavigateToURL(shell(), page_url));
EXPECT_EQ(
base::StringPrintf(
"TypeError: Failed to execute 'joinAdInterestGroup' on 'Navigator': "
"owner 'https://test.com' for AuctionAdInterestGroup with name "
"'cars' match frame origin '%s'.",
url::Origin::Create(page_url).Serialize().c_str()),
EvalJs(shell(), R"(
(function() {
try {
navigator.joinAdInterestGroup(
{
name: 'cars',
owner: 'https://test.com',
},
/*joinDurationSec=*/1);
} catch (e) {
return e.toString();
}
return 'done';
})())"));
ExpectAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
JoinInterestGroupInvalidBiddingLogicUrl) {
GURL url = https_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': "
"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"(
(function() {
try {
navigator.joinAdInterestGroup(
{
name: 'cars',
owner: $1,
biddingLogicUrl: 'https://invalid^&',
},
/*joinDurationSec=*/1);
} catch (e) {
return e.toString();
}
return 'done';
})())",
origin_string.c_str())));
ExpectAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
JoinInterestGroupInvalidBiddingWasmHelperUrl) {
const char kScriptTemplate[] = R"(
(function() {
try {
navigator.joinAdInterestGroup(
{
name: 'cars',
owner: $1,
biddingWasmHelperUrl: 'https://invalid^&',
},
/*joinDurationSec=*/1);
} catch (e) {
return e.toString();
}
return 'done';
})())";
GURL url = https_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,
JoinInterestGroupInvalidDailyUpdateUrl) {
GURL url = https_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': "
"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"(
(function() {
try {
navigator.joinAdInterestGroup(
{
name: 'cars',
owner: $1,
dailyUpdateUrl: 'https://invalid^&',
},
/*joinDurationSec=*/1);
} catch (e) {
return e.toString();
}
return 'done';
})())",
origin_string.c_str())));
ExpectAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
JoinInterestGroupInvalidTrustedBiddingSignalsUrl) {
GURL url = https_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': 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"(
(function() {
try {
navigator.joinAdInterestGroup(
{
name: 'cars',
owner: $1,
trustedBiddingSignalsUrl: 'https://invalid^&',
},
/*joinDurationSec=*/1);
} catch (e) {
return e.toString();
}
return 'done';
})())",
origin_string.c_str())));
ExpectAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
JoinInterestGroupInvalidUserBiddingSignals) {
GURL url = https_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': "
"userBiddingSignals for AuctionAdInterestGroup with owner '%s' and "
"name 'cars' must be a JSON-serializable object.",
origin_string.c_str()),
EvalJs(shell(), JsReplace(R"(
(function() {
try {
navigator.joinAdInterestGroup(
{
name: 'cars',
owner: $1,
userBiddingSignals: function() {},
},
/*joinDurationSec=*/1);
} catch (e) {
return e.toString();
}
return 'done';
})())",
origin_string.c_str())));
ExpectAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
JoinInterestGroupInvalidAdUrl) {
GURL url = https_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': "
"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"(
(function() {
try {
navigator.joinAdInterestGroup(
{
name: 'cars',
owner: $1,
ads: [{renderUrl:"https://invalid^&"}],
},
/*joinDurationSec=*/1);
} catch (e) {
return e.toString();
}
return 'done';
})())",
origin_string.c_str())));
ExpectAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
JoinInterestGroupInvalidAdMetadata) {
GURL url = https_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': ad metadata for AuctionAdInterestGroup with "
"owner '%s' and name 'cars' must be a JSON-serializable object.",
origin_string.c_str()),
EvalJs(shell(), JsReplace(R"(
(function() {
let x = {};
let y = {};
x.a = y;
y.a = x;
try {
navigator.joinAdInterestGroup(
{
name: 'cars',
owner: $1,
ads: [{renderUrl:"https://test.com", metadata:x}],
},
/*joinDurationSec=*/1);
} catch (e) {
return e.toString();
}
return 'done';
})())",
origin_string.c_str())));
ExpectAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
LeaveInterestGroupInvalidOwner) {
ASSERT_TRUE(NavigateToURL(shell(), https_server_->GetURL("a.test", "/echo")));
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"(
(function() {
try {
navigator.leaveAdInterestGroup(
{
name: 'cars',
owner: 'https://invalid^&',
},
/*joinDurationSec=*/1);
} catch (e) {
return e.toString();
}
return 'done';
})())"));
ExpectAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, RunAdAuctionInvalidSeller) {
ASSERT_TRUE(NavigateToURL(shell(), https_server_->GetURL("a.test", "/echo")));
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'
})"));
ExpectAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, RunAdAuctionHttpSeller) {
ASSERT_TRUE(NavigateToURL(shell(), https_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: 'http://test.com',
decisionLogicUrl: 'https://test.com/decision_logic'
})"));
ExpectAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionInvalidDecisionLogicUrl) {
ASSERT_TRUE(NavigateToURL(shell(), https_server_->GetURL("a.test", "/echo")));
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^&'
})"));
ExpectAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionInvalidTrustedScoringSignalsUrl) {
GURL url = https_server_->GetURL("a.test", "/echo");
url::Origin origin = url::Origin::Create(url);
ASSERT_TRUE(NavigateToURL(shell(), url));
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)));
ExpectAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionDecisionLogicUrlDifferentFromSeller) {
GURL test_url = https_server_->GetURL("a.test", "/echo");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
AttachInterestGroupObserver();
EXPECT_TRUE(JoinInterestGroupAndWaitInJs(blink::InterestGroup(
/*expiry=*/base::Time(),
/*owner=*/test_origin,
/*name=*/"cars",
/*bidding_url=*/
https_server_->GetURL("a.test", "/interest_group/bidding_logic.js"),
/*bidding_wasm_helper_url=*/absl::nullopt,
/*update_url=*/absl::nullopt,
/*trusted_bidding_signals_url=*/absl::nullopt,
/*trusted_bidding_signals_keys=*/absl::nullopt,
/*user_bidding_signals=*/"{some: 'json', data: {here: [1, 2]}}",
/*ads=*/
{{{GURL("https://example.com/render"), "{ad:'metadata', here:[1,2]}"}}},
/*ad_components=*/absl::nullopt)));
EXPECT_EQ(nullptr, RunAuctionAndWait(JsReplace(
R"({
seller: $1,
decisionLogicUrl: $2,
interestGroupBuyers: [$1],
})",
test_origin,
https_server_->GetURL(
"b.test", "/interest_group/decision_logic.js"))));
ExpectAccessObserved({
{InterestGroupTestObserver::kJoin, test_origin.Serialize(), "cars"},
});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionInvalidInterestGroupBuyers) {
ASSERT_TRUE(NavigateToURL(shell(), https_server_->GetURL("a.test", "/echo")));
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^&'],
})"));
ExpectAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionInvalidInterestGroupBuyersStr) {
ASSERT_TRUE(NavigateToURL(shell(), https_server_->GetURL("a.test", "/echo")));
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',
})"));
ExpectAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionNoInterestGroupBuyers) {
ASSERT_TRUE(NavigateToURL(shell(), https_server_->GetURL("a.test", "/echo")));
EXPECT_EQ(nullptr, RunAuctionAndWait(R"({
seller: 'https://test.com',
decisionLogicUrl: 'https://test.com',
})"));
ExpectAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionEmptyInterestGroupBuyers) {
ASSERT_TRUE(NavigateToURL(shell(), https_server_->GetURL("a.test", "/echo")));
EXPECT_EQ(nullptr, RunAuctionAndWait(R"({
seller: 'https://test.com',
decisionLogicUrl: 'https://test.com',
interestGroupBuyers: [],
})"));
ExpectAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionInvalidAuctionSignals) {
ASSERT_TRUE(NavigateToURL(shell(), https_server_->GetURL("a.test", "/echo")));
EXPECT_EQ(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': "
"auctionSignals for AuctionAdConfig with seller 'https://test.com' must "
"be a JSON-serializable object.",
RunAuctionAndWait(R"({
seller: 'https://test.com',
decisionLogicUrl: 'https://test.com',
auctionSignals: alert
})"));
ExpectAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionInvalidSellerSignals) {
ASSERT_TRUE(NavigateToURL(shell(), https_server_->GetURL("a.test", "/echo")));
EXPECT_EQ(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': "
"sellerSignals for AuctionAdConfig with seller 'https://test.com' must "
"be a JSON-serializable object.",
RunAuctionAndWait(R"({
seller: 'https://test.com',
decisionLogicUrl: 'https://test.com',
sellerSignals: function() {}
})"));
ExpectAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionInvalidPerBuyerSignalsOrigin) {
ASSERT_TRUE(NavigateToURL(shell(), https_server_->GetURL("a.test", "/echo")));
EXPECT_EQ(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': "
"perBuyerSignals 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',
perBuyerSignals: {'https://invalid^&': {a:1}}
})"));
ExpectAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionInvalidPerBuyerSignals) {
ASSERT_TRUE(NavigateToURL(shell(), https_server_->GetURL("a.test", "/echo")));
EXPECT_EQ(
"TypeError: Failed to execute 'runAdAuction' on 'Navigator': "
"perBuyerSignals for AuctionAdConfig with seller 'https://test.com' "
"must be a JSON-serializable object.",
RunAuctionAndWait(R"({
seller: 'https://test.com',
decisionLogicUrl: 'https://test.com',
perBuyerSignals: {'https://test.com': function() {}}
})"));
ExpectAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionBuyersNoInterestGroup) {
GURL test_url = https_server_->GetURL("a.test", "/echo");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
AttachInterestGroupObserver();
EXPECT_EQ(nullptr, RunAuctionAndWait(JsReplace(
R"({
seller: $1,
decisionLogicUrl: $2,
interestGroupBuyers: [$1],
})",
url::Origin::Create(test_url),
https_server_->GetURL(
"a.test", "/interest_group/decision_logic.js"))));
ExpectAccessObserved({});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionPrivacySandboxDisabled) {
// Successful join at a.test
GURL test_url_a = https_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_TRUE(JoinInterestGroupAndWaitInJs(blink::InterestGroup(
/*expiry=*/base::Time(),
/*owner=*/test_origin_a,
/*name=*/"cars",
/*bidding_url=*/
https_server_->GetURL("a.test", "/interest_group/bidding_logic.js"),
/*bidding_wasm_helper_url=*/absl::nullopt,
/*update_url=*/absl::nullopt,
/*trusted_bidding_signals_url=*/
https_server_->GetURL("a.test",
"/interest_group/trusted_bidding_signals.json"),
/*trusted_bidding_signals_keys=*/{{"key1"}},
/*user_bidding_signals=*/"{some: 'json', data: {here: [1, 2]}}",
/*ads=*/
{{{GURL("https://example.com/render"), "{ad:'metadata', here:[1,2]}"}}},
/*ad_components=*/absl::nullopt)));
GURL test_url_d = https_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(
nullptr,
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),
https_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_,
https_server_->GetURL("/interest_group/bidding_logic.js")));
EXPECT_FALSE(base::Contains(
received_https_test_server_requests_,
https_server_->GetURL("/interest_group/trusted_bidding_signals.json")));
EXPECT_FALSE(base::Contains(
received_https_test_server_requests_,
https_server_->GetURL("/interest_group/decision_logic.js")));
ExpectAccessObserved({
{InterestGroupTestObserver::kJoin, test_origin_a.Serialize(), "cars"},
});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionDisabledInterestGroup) {
// Inject an interest group into the DB for that for a disabled site so we can
// try to remove it.
GURL disabled_domain = https_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 = https_server_->GetURL(
disabled_domain.host(),
"/interest_group/bidding_logic_stop_bidding_after_win.js");
disabled_group.ads.emplace();
disabled_group.ads->emplace_back(blink::InterestGroup::Ad(
GURL("https://stop_bidding_after_win.com/render"), absl::nullopt));
manager_->JoinInterestGroup(std::move(disabled_group), disabled_domain);
ASSERT_EQ(1, GetJoinCount(disabled_origin, "candy"));
GURL test_url = https_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 = https_server_->GetURL("c.test", "/echo?render_cars");
EXPECT_TRUE(JoinInterestGroupAndWaitInJs(blink::InterestGroup(
/*expiry=*/base::Time(),
/*owner=*/test_origin,
/*name=*/"cars",
/*bidding_url=*/
https_server_->GetURL(test_url.host(),
"/interest_group/bidding_logic.js"),
/*bidding_wasm_helper_url=*/absl::nullopt,
/*update_url=*/absl::nullopt,
/*trusted_bidding_signals_url=*/
https_server_->GetURL(test_url.host(),
"/interest_group/trusted_bidding_signals.json"),
/*trusted_bidding_signals_keys=*/{{"key1"}},
/*user_bidding_signals=*/"{some: 'json', data: {here: [1, 2]}}",
/*ads=*/
{{{ad_url, "{ad:'metadata', here:[1,2]}"}}},
/*ad_components=*/absl::nullopt)));
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,
https_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_,
https_server_->GetURL(
"/interest_group/bidding_logic_stop_bidding_after_win.js")));
ExpectAccessObserved({
{InterestGroupTestObserver::kJoin, disabled_origin.Serialize(), "candy"},
{InterestGroupTestObserver::kJoin, test_origin.Serialize(), "cars"},
{InterestGroupTestObserver::kBid, test_origin.Serialize(), "cars"},
{InterestGroupTestObserver::kWin, test_origin.Serialize(), "cars"},
});
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, RunAdAuctionWithWinner) {
URLLoaderMonitor url_loader_monitor;
GURL test_url = https_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 = https_server_->GetURL("c.test", "/echo?render_cars");
EXPECT_TRUE(JoinInterestGroupAndWaitInJs(blink::InterestGroup(
/*expiry=*/base::Time(),
/*owner=*/test_origin,
/*name=*/"cars",
/*bidding_url=*/
https_server_->GetURL("a.test", "/interest_group/bidding_logic.js"),
/*bidding_wasm_helper_url=*/absl::nullopt,
/*update_url=*/absl::nullopt,
/*trusted_bidding_signals_url=*/
https_server_->GetURL("a.test",
"/interest_group/trusted_bidding_signals.json"),
/*trusted_bidding_signals_keys=*/{{"key1"}},
/*user_bidding_signals=*/"{some: 'json', data: {here: [1, 2]}}",
/*ads=*/
{{{ad_url, "{ad:'metadata', here:[1,2]}"}}},
/*ad_components=*/absl::nullopt)));
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,
https_server_->GetURL("a.test", "/interest_group/decision_logic.js"));
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad_url);
// InterestGroupAccessObserver never was activated, so nothing was observed.
ExpectAccessObserved({});
// Check ResourceRequest structs of requests issued by the worklet process.
const struct ExpectedRequest {
GURL url;
const char* accept_header;
bool expect_trusted_params;
} kExpectedRequests[] = {
{https_server_->GetURL("a.test", "/interest_group/bidding_logic.js"),
"application/javascript", /*expect_trusted_params=*/true},
{https_server_->GetURL(
"a.test",
"/interest_group/"
"trusted_bidding_signals.json?hostname=a.test&keys=key1"),
"application/json", /*expect_trusted_params=*/true},
{https_server_->GetURL("a.test", "/interest_group/decision_logic.js"),
"application/javascript", /*expect_trusted_params=*/false},
};
for (const auto& expected_request : kExpectedRequests) {
SCOPED_TRACE(expected_request.url);
absl::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());
std::string accept_value;
ASSERT_TRUE(request->headers.GetHeader(net::HttpRequestHeaders::kAccept,
&accept_value));
EXPECT_EQ(expected_request.accept_header, accept_value);
EXPECT_EQ(expected_request.expect_trusted_params,
request->trusted_params.has_value());
EXPECT_EQ(network::mojom::RequestMode::kNoCors, 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 GURL kExpectedReportUrls[] = {
https_server_->GetURL("a.test", "/echoall?report_seller"),
https_server_->GetURL("a.test", "/echoall?report_bidder"),
};
for (const auto& expected_report_url : kExpectedReportUrls) {
SCOPED_TRACE(expected_report_url);
// Wait for the report URL to be fetched, which only happens after the
// auction has completed.
WaitForURL(expected_report_url);
absl::optional<network::ResourceRequest> request =
url_loader_monitor.GetRequestInfo(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());
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionPerBuyerSignalsOriginNotInBuyers) {
GURL test_url = https_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 = https_server_->GetURL("c.test", "/echo?render_cars");
EXPECT_TRUE(JoinInterestGroupAndWaitInJs(blink::InterestGroup(
/*expiry=*/base::Time(),
/*owner=*/test_origin,
/*name=*/"cars",
/*bidding_url=*/
https_server_->GetURL("a.test", "/interest_group/bidding_logic.js"),
/*bidding_wasm_helper_url=*/absl::nullopt,
/*update_url=*/absl::nullopt,
/*trusted_bidding_signals_url=*/absl::nullopt,
/*trusted_bidding_signals_keys=*/absl::nullopt,
/*user_bidding_signals=*/absl::nullopt,
/*ads=*/
{{{ad_url, /*metadata=*/absl::nullopt}}},
/*ad_components=*/absl::nullopt)));
RunAuctionAndWaitForURLAndNavigateIframe(
JsReplace(
R"({
seller: $1,
decisionLogicUrl: $2,
interestGroupBuyers: [$1],
perBuyerSignals: {$1: {a:1}, 'https://not_in_buyers.com': {a:1}}
})",
test_origin,
https_server_->GetURL("a.test", "/interest_group/decision_logic.js")),
ad_url);
ExpectAccessObserved(
{{InterestGroupTestObserver::kJoin, test_origin.Serialize(), "cars"},
{InterestGroupTestObserver::kBid, test_origin.Serialize(), "cars"},
{InterestGroupTestObserver::kWin, test_origin.Serialize(), "cars"}});
}
// Runs an auction where the bidding function uses a WASM helper.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, RunAdAuctionWithBidderWasm) {
GURL test_url = https_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 = https_server_->GetURL("c.test", "/echo?render_cars");
EXPECT_TRUE(JoinInterestGroupAndWaitInJs(blink::InterestGroup(
/*expiry=*/base::Time(),
/*owner=*/test_origin,
/*name=*/"cars",
/*bidding_url=*/
https_server_->GetURL("a.test",
"/interest_group/bidding_logic_use_wasm.js"),
/*bidding_wasm_helper_url=*/
https_server_->GetURL("a.test", "/interest_group/multiply.wasm"),
/*update_url=*/absl::nullopt,
/*trusted_bidding_signals_url=*/absl::nullopt,
/*trusted_bidding_signals_keys=*/{},
/*user_bidding_signals=*/"{}",
/*ads=*/
{{{ad_url, "{ad:'metadata', here:[1,2]}"}}},
/*ad_components=*/absl::nullopt)));
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicUrl: $2,
interestGroupBuyers: [$1],
})",
test_origin,
https_server_->GetURL("a.test", "/interest_group/decision_logic.js"));
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad_url);
}
// Runs auction just like the above test, 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_P(InterestGroupFencedFrameBrowserTest,
RunAdAuctionWithWinner) {
URLLoaderMonitor url_loader_monitor;
GURL test_url = https_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 = https_server_->GetURL(
"c.test", "/set-header?Supports-Loading-Mode: fenced-frame");
EXPECT_TRUE(JoinInterestGroupAndWaitInJs(blink::InterestGroup(
/*expiry=*/base::Time(),
/*owner=*/test_origin,
/*name=*/"cars",
/*bidding_url=*/
https_server_->GetURL("a.test", "/interest_group/bidding_logic.js"),
/*bidding_wasm_helper_url=*/absl::nullopt,
/*update_url=*/absl::nullopt,
/*trusted_bidding_signals_url=*/
https_server_->GetURL("a.test",
"/interest_group/trusted_bidding_signals.json"),
/*trusted_bidding_signals_keys=*/{{"key1"}},
/*user_bidding_signals=*/"{some: 'json', data: {here: [1, 2]}}",
{{{ad_url, "{ad:'metadata', here:[1,2]}"}}},
/*ad_components=*/absl::nullopt)));
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,
https_server_->GetURL("a.test",
"/interest_group/decision_logic.js"))));
// InterestGroupAccessObserver never was activated, so nothing was observed.
ExpectAccessObserved({});
// Check ResourceRequest structs of requests issued by the worklet process.
const struct ExpectedRequest {
GURL url;
const char* accept_header;
bool expect_trusted_params;
} kExpectedRequests[] = {
{https_server_->GetURL("a.test", "/interest_group/bidding_logic.js"),
"application/javascript", /*expect_trusted_params=*/true},
{https_server_->GetURL(
"a.test",
"/interest_group/"
"trusted_bidding_signals.json?hostname=a.test&keys=key1"),
"application/json", /*expect_trusted_params=*/true},
{https_server_->GetURL("a.test", "/interest_group/decision_logic.js"),
"application/javascript", /*expect_trusted_params=*/false},
};
for (const auto& expected_request : kExpectedRequests) {
SCOPED_TRACE(expected_request.url);
absl::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());
std::string accept_value;
ASSERT_TRUE(request->headers.GetHeader(net::HttpRequestHeaders::kAccept,
&accept_value));
EXPECT_EQ(expected_request.accept_header, accept_value);
EXPECT_EQ(expected_request.expect_trusted_params,
request->trusted_params.has_value());
EXPECT_EQ(network::mojom::RequestMode::kNoCors, 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 GURL kExpectedReportUrls[] = {
https_server_->GetURL("a.test", "/echoall?report_seller"),
https_server_->GetURL("a.test", "/echoall?report_bidder"),
};
for (const auto& expected_report_url : kExpectedReportUrls) {
SCOPED_TRACE(expected_report_url);
// Wait for the report URL to be fetched, which only happens after the
// auction has completed.
WaitForURL(expected_report_url);
absl::optional<network::ResourceRequest> request =
url_loader_monitor.GetRequestInfo(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());
}
// Use different origins for publisher, bidder, and seller, and make sure
// everything works as expected.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, CrossOrigin) {
const char kPublisher[] = "a.test";
const char kBidder[] = "b.test";
const char kSeller[] = "c.test";
AttachInterestGroupObserver();
// Navigate to bidder site, and add an interest group.
GURL bidder_url = https_server_->GetURL(kBidder, "/echo");
ASSERT_TRUE(NavigateToURL(shell(), bidder_url));
url::Origin bidder_origin = url::Origin::Create(bidder_url);
EXPECT_TRUE(JoinInterestGroupAndWaitInJs(blink::InterestGroup(
/*expiry=*/base::Time(),
/*owner=*/bidder_origin,
/*name=*/"cars",
/*bidding_url=*/
https_server_->GetURL(kBidder, "/interest_group/bidding_logic.js"),
/*bidding_wasm_helper_url=*/absl::nullopt,
/*update_url=*/absl::nullopt,
/*trusted_bidding_signals_url=*/
https_server_->GetURL(kBidder,
"/interest_group/trusted_bidding_signals.json"),
/*trusted_bidding_signals_keys=*/{{"key1"}},
/*user_bidding_signals=*/"{some: 'json', data: {here: [1, 2]}}",
/*ads=*/
{{{GURL("https://example.com/render"), "{ad:'metadata', here:[1,2]}"}}},
/*ad_components=*/absl::nullopt)));
// Navigate to publisher.
ASSERT_TRUE(
NavigateToURL(shell(), https_server_->GetURL(kPublisher, "/echo")));
GURL seller_logic_url = https_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 bits if trustedScoringSignals is not received.
if (trustedScoringSignals.renderUrl["https://example.com/render"] === "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");
// Run an auction with the scoring script. It should succeed.
ASSERT_EQ("https://example.com/render",
RunAuctionAndWaitForURL(JsReplace(
R"(
{
seller: $1,
decisionLogicUrl: $2,
trustedScoringSignalsUrl: $3,
interestGroupBuyers: [$4],
}
)",
url::Origin::Create(seller_logic_url), seller_logic_url,
https_server_->GetURL(
kSeller, "/interest_group/trusted_scoring_signals.json"),
bidder_origin)));
ExpectAccessObserved({
{InterestGroupTestObserver::kJoin, bidder_origin.Serialize(), "cars"},
{InterestGroupTestObserver::kBid, bidder_origin.Serialize(), "cars"},
{InterestGroupTestObserver::kWin, bidder_origin.Serialize(), "cars"},
});
// Reporting urls should be fetched after an auction succeeded.
WaitForURL(https_server_->GetURL("/echoall?report_seller"));
WaitForURL(https_server_->GetURL("/echoall?report_bidder"));
// Double-check that the trusted scoring signals URL was requested as well.
WaitForURL(
https_server_->GetURL("/interest_group/trusted_scoring_signals.json"
"?hostname=a.test"
"&renderUrls=https%3A%2F%2Fexample.com%2Frender"));
}
// Test that ad_components in an iframe ad are requested.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionWinnerWithComponents) {
GURL test_url = https_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 =
https_server_->GetURL("c.test", "/fenced_frames/ad_with_components.html");
GURL component_url = https_server_->GetURL("c.test", "/echo?component");
AttachInterestGroupObserver();
EXPECT_TRUE(JoinInterestGroupAndWaitInJs(blink::InterestGroup(
/*expiry=*/base::Time(),
/*owner=*/test_origin,
/*name=*/"cars",
/*bidding_url=*/
https_server_->GetURL("a.test", "/interest_group/bidding_logic.js"),
/*bidding_wasm_helper_url=*/absl::nullopt,
/*update_url=*/absl::nullopt,
/*trusted_bidding_signals_url=*/
https_server_->GetURL("a.test",
"/interest_group/trusted_bidding_signals.json"),
/*trusted_bidding_signals_keys=*/{{"key1"}},
/*user_bidding_signals=*/"{some: 'json', data: {here: [1, 2]}}",
/*ads=*/
{{{ad_url, "{ad:'metadata', here:[1,2]}"}}},
/*ad_components=*/
{{{component_url, "{ad:'component metadata'}"}}})));
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,
https_server_->GetURL("a.test", "/interest_group/decision_logic.js"));
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad_url);
ExpectAccessObserved({
{InterestGroupTestObserver::kJoin, test_origin.Serialize(), "cars"},
{InterestGroupTestObserver::kBid, test_origin.Serialize(), "cars"},
{InterestGroupTestObserver::kWin, test_origin.Serialize(), "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 = https_server_->GetURL(kOtherHost, "/echo");
url::Origin other_origin = url::Origin::Create(other_url);
ASSERT_TRUE(NavigateToURL(shell(), other_url));
EXPECT_TRUE(JoinInterestGroupAndWaitInJs(blink::InterestGroup(
/*expiry=*/base::Time(),
/*owner=*/other_origin,
/*name=*/"cars",
/*bidding_url=*/
https_server_->GetURL(
kOtherHost,
"/interest_group/bidding_logic_expect_top_frame_a_test.js"),
/*bidding_wasm_helper_url=*/absl::nullopt,
/*update_url=*/absl::nullopt,
/*trusted_bidding_signals_url=*/absl::nullopt,
/*trusted_bidding_signals_keys=*/absl::nullopt,
/*user_bidding_signals=*/absl::nullopt,
/*ads=*/
{{{GURL("https://example.com/render"), /*metadata=*/absl::nullopt}}},
/*ad_components=*/absl::nullopt)));
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(),
https_server_->GetURL(kTopFrameHost, test_case.top_frame_path)));
RenderFrameHost* frame = web_contents()->GetMainFrame();
EXPECT_EQ(https_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 =
https_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));
// Reporting urls should be fetched after an auction succeeded.
WaitForURL(https_server_->GetURL("/echoall?report_seller"));
WaitForURL(https_server_->GetURL("/echoall?report_bidder"));
ClearReceivedRequests();
}
}
// Test running auctions in cross-site iframes, and loading the winner into a
// nested fenced frame.
IN_PROC_BROWSER_TEST_P(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(https_server_->GetURL(kIframeHost, "/"))});
// Navigate to bidder site, and add an interest group.
GURL bidder_url = https_server_->GetURL(kBidderHost, "/echo");
url::Origin bidder_origin = url::Origin::Create(bidder_url);
ASSERT_TRUE(NavigateToURL(shell(), bidder_url));
GURL ad_url = https_server_->GetURL(
kAdHost, "/set-header?Supports-Loading-Mode: fenced-frame");
EXPECT_TRUE(JoinInterestGroupAndWaitInJs(
/*owner=*/bidder_origin,
/*name=*/"cars",
/*bidding_url=*/
https_server_->GetURL(kBidderHost, "/interest_group/bidding_logic.js"),
/*ads=*/{{{ad_url, /*metadata=*/absl::nullopt}}}));
GURL main_frame_url = https_server_->GetURL(
kTopFrameHost,
base::StringPrintf(
"/cross_site_iframe_factory.html?%s(%s)", kTopFrameHost,
https_server_->GetURL(kIframeHost, "/fenced_frames/basic.html")
.spec()
.c_str()));
ASSERT_TRUE(NavigateToURL(shell(), main_frame_url));
RenderFrameHost* iframe = ChildFrameAt(web_contents()->GetMainFrame(), 0);
ASSERT_TRUE(iframe);
EXPECT_EQ(kIframeHost, iframe->GetLastCommittedOrigin().host());
GURL seller_logic_url =
https_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 = https_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 =
https_server_->GetURL("c.test", "/echo?stop_bidding_after_win");
GURL ad2_url = https_server_->GetURL("c.test", "/echo?render_bikes");
GURL ad3_url = https_server_->GetURL("c.test", "/echo?render_shoes");
EXPECT_TRUE(JoinInterestGroupAndWaitInJs(blink::InterestGroup(
/*expiry=*/base::Time(),
/*owner=*/test_origin,
/*name=*/"cars",
/*bidding_url=*/
https_server_->GetURL(
"a.test", "/interest_group/bidding_logic_stop_bidding_after_win.js"),
/*bidding_wasm_helper_url=*/absl::nullopt,
/*update_url=*/absl::nullopt,
/*trusted_bidding_signals_url=*/absl::nullopt,
/*trusted_bidding_signals_keys=*/absl::nullopt,
/*user_bidding_signals=*/"{some: 'json', data: {here: [1, 2]}}",
/*ads=*/
{{{ad1_url,
/*metadata=*/absl::nullopt}}},
/*ad_components=*/absl::nullopt)));
EXPECT_TRUE(JoinInterestGroupAndWaitInJs(blink::InterestGroup(
/*expiry=*/base::Time(),
/*owner=*/test_origin,
/*name=*/"bikes",
/*bidding_url=*/
https_server_->GetURL("a.test", "/interest_group/bidding_logic.js"),
/*bidding_wasm_helper_url=*/absl::nullopt,
/*update_url=*/absl::nullopt,
/*trusted_bidding_signals_url=*/
https_server_->GetURL("a.test",
"/interest_group/trusted_bidding_signals.json"),
/*trusted_bidding_signals_keys=*/{{"key1"}},
/*user_bidding_signals=*/"{some: 'json', data: {here: [1, 2]}}",
/*ads=*/
{{{ad2_url, /*metadata=*/absl::nullopt}}},
/*ad_components=*/absl::nullopt)));
EXPECT_TRUE(JoinInterestGroupAndWaitInJs(blink::InterestGroup(
/*expiry=*/base::Time(),
/*owner=*/test_origin,
/*name=*/"shoes",
/*bidding_url=*/
https_server_->GetURL("a.test", "/interest_group/bidding_logic.js"),
/*bidding_wasm_helper_url=*/absl::nullopt,
/*update_url=*/absl::nullopt,
/*trusted_bidding_signals_url=*/absl::nullopt,
/*trusted_bidding_signals_keys=*/absl::nullopt,
/*user_bidding_signals=*/"{some: 'json', data: {here: [1, 2]}}",
/*ads=*/
{{{ad3_url, /*metadata=*/absl::nullopt}}},
/*ad_components=*/absl::nullopt)));
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicUrl: $2,
interestGroupBuyers: [$1, $3],
})",
test_origin,
https_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(https_server_->GetURL("/echoall?report_seller"));
WaitForURL(
https_server_->GetURL("/echoall?report_bidder_stop_bidding_after_win"));
base::AutoLock auto_lock(requests_lock_);
EXPECT_FALSE(base::Contains(received_https_test_server_requests_,
https_server_->GetURL("/echoall?report_bidder")));
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, RunAdAuctionMultipleAuctions) {
GURL test_url = https_server_->GetURL("a.test", "/echo");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
const url::Origin origin = url::Origin::Create(test_url);
GURL ad1_url =
https_server_->GetURL("c.test", "/echo?stop_bidding_after_win");
GURL ad2_url = https_server_->GetURL("c.test", "/echo?render_shoes");
// This group will win if it has never won an auction.
EXPECT_TRUE(JoinInterestGroupAndWaitInJs(blink::InterestGroup(
/*expiry=*/base::Time(),
/*owner=*/origin,
/*name=*/"cars",
/*bidding_url=*/
https_server_->GetURL(
"a.test", "/interest_group/bidding_logic_stop_bidding_after_win.js"),
/*bidding_wasm_helper_url=*/absl::nullopt,
/*update_url=*/absl::nullopt,
/*trusted_bidding_signals_url=*/absl::nullopt,
/*trusted_bidding_signals_keys=*/absl::nullopt,
/*user_bidding_signals=*/"{some: 'json', data: {here: [1, 2]}}",
/*ads=*/
{{{ad1_url, "{ad:'metadata', here:[1,2]}"}}},
/*ad_components=*/absl::nullopt)));
GURL test_url2 = https_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_TRUE(JoinInterestGroupAndWaitInJs(blink::InterestGroup(
/*expiry=*/base::Time(),
/*owner=*/origin2,
/*name=*/"shoes",
/*bidding_url=*/
https_server_->GetURL("b.test", "/interest_group/bidding_logic.js"),
/*bidding_wasm_helper_url=*/absl::nullopt,
/*update_url=*/absl::nullopt,
/*trusted_bidding_signals_url=*/absl::nullopt,
/*trusted_bidding_signals_keys=*/absl::nullopt,
/*user_bidding_signals=*/"{some: 'json', data: {here: [1, 2]}}",
/*ads=*/
{{{ad2_url, /*metadata=*/absl::nullopt}}},
/*ad_components=*/absl::nullopt)));
// Both owners have one interest group in storage, and both interest groups
// have no `prev_wins`.
std::vector<StorageInterestGroup> storage_interest_groups =
GetInterestGroupsForOwner(origin);
EXPECT_EQ(storage_interest_groups.size(), 1u);
EXPECT_EQ(
storage_interest_groups.front().bidding_browser_signals->prev_wins.size(),
0u);
EXPECT_EQ(storage_interest_groups.front().bidding_browser_signals->bid_count,
0);
std::vector<StorageInterestGroup> storage_interest_groups2 =
GetInterestGroupsForOwner(origin2);
EXPECT_EQ(storage_interest_groups2.size(), 1u);
EXPECT_EQ(storage_interest_groups2.front()
.bidding_browser_signals->prev_wins.size(),
0u);
EXPECT_EQ(storage_interest_groups2.front().bidding_browser_signals->bid_count,
0);
std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicUrl: $2,
interestGroupBuyers: [$1, $3],
})",
origin2,
https_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);
// `prev_wins` of `test_url`'s interest group cars is updated in storage.
storage_interest_groups = GetInterestGroupsForOwner(origin);
storage_interest_groups2 = GetInterestGroupsForOwner(origin2);
EXPECT_EQ(
storage_interest_groups.front().bidding_browser_signals->prev_wins.size(),
1u);
EXPECT_EQ(storage_interest_groups2.front()
.bidding_browser_signals->prev_wins.size(),
0u);
EXPECT_EQ(
storage_interest_groups.front()
.bidding_browser_signals->prev_wins.front()
->ad_json,
JsReplace(
R"({"render_url":$1,"metadata":{"ad":"metadata","here":[1,2]}})",
ad1_url));
EXPECT_EQ(storage_interest_groups.front().bidding_browser_signals->bid_count,
1);
EXPECT_EQ(storage_interest_groups2.front().bidding_browser_signals->bid_count,
1);
// Start observer in the middle.
AttachInterestGroupObserver();
// Run auction again. Interest group shoes of owner `test_url2` wins.
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad2_url);
// `test_url2`'s interest group shoes has one `prev_wins` in storage.
storage_interest_groups = GetInterestGroupsForOwner(origin);
storage_interest_groups2 = GetInterestGroupsForOwner(origin2);
EXPECT_EQ(
storage_interest_groups.front().bidding_browser_signals->prev_wins.size(),
1u);
EXPECT_EQ(storage_interest_groups2.front()
.bidding_browser_signals->prev_wins.size(),
1u);
EXPECT_EQ(storage_interest_groups2.front()
.bidding_browser_signals->prev_wins.front()
->ad_json,
JsReplace(R"({"render_url":$1})", ad2_url));
// First interest group didn't bid this time.
EXPECT_EQ(storage_interest_groups.front().bidding_browser_signals->bid_count,
1);
EXPECT_EQ(storage_interest_groups2.front().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,
https_server_->GetURL("b.test", "/interest_group/decision_logic.js"));
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad2_url);
// `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.front().bidding_browser_signals->prev_wins.size(),
1u);
EXPECT_EQ(storage_interest_groups2.front()
.bidding_browser_signals->prev_wins.size(),
2u);
EXPECT_EQ(storage_interest_groups2.front()
.bidding_browser_signals->prev_wins.back()
->ad_json,
JsReplace(R"({"render_url":$1})", ad2_url));
// First interest group didn't bid this time.
EXPECT_EQ(storage_interest_groups.front().bidding_browser_signals->bid_count,
1);
EXPECT_EQ(storage_interest_groups2.front().bidding_browser_signals->bid_count,
3);
// Observer was not active for joins and first auction.
ExpectAccessObserved({
{InterestGroupTestObserver::kBid, origin2.Serialize(), "shoes"},
{InterestGroupTestObserver::kWin, origin2.Serialize(), "shoes"},
{InterestGroupTestObserver::kBid, origin2.Serialize(), "shoes"},
{InterestGroupTestObserver::kWin, origin2.Serialize(), "shoes"},
});
}
// Adding an interest group and then immediately running the ad acution, 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 = https_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 = https_server_->GetURL("c.test", "/echo?render_cars");
// Use JoinInterestGroupInJS() instead of JoinInterestGroupAndWaitInJs().
EXPECT_TRUE(JoinInterestGroupInJS(blink::InterestGroup(
/*expiry=*/base::Time(),
/*owner=*/test_origin,
/*name=*/"cars",
/*bidding_url=*/
https_server_->GetURL("a.test", "/interest_group/bidding_logic.js"),
/*bidding_wasm_helper_url=*/absl::nullopt,
/*update_url=*/absl::nullopt,
/*trusted_bidding_signals_url=*/
https_server_->GetURL("a.test",
"/interest_group/trusted_bidding_signals.json"),
/*trusted_bidding_signals_keys=*/{{"key1"}},
/*user_bidding_signals=*/"{some: 'json', data: {here: [1, 2]}}",
/*ads=*/
{{{ad_url, "{ad:'metadata', here : [1,2] }"}}},
/*ad_components=*/absl::nullopt)));
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,
https_server_->GetURL("a.test", "/interest_group/decision_logic.js"));
RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad_url);
// Leave the interest group, then re-run the auction. We shouldn't get a
// result.
LeaveInterestGroupInJS(/*owner=*/test_origin, /*name=*/"cars");
EXPECT_EQ(nullptr, 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,
https_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 = https_server_->GetURL("a.test", "/echo");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
EXPECT_TRUE(JoinInterestGroupAndWaitInJs(blink::InterestGroup(
/*expiry=*/base::Time(),
/*owner=*/test_origin,
/*name=*/"cars",
/*bidding_url=*/
https_server_->GetURL("a.test",
"/interest_group/bidding_logic_invalid_ad_url.js"),
/*bidding_wasm_helper_url=*/absl::nullopt,
/*update_url=*/absl::nullopt,
/*trusted_bidding_signals_url=*/absl::nullopt,
/*trusted_bidding_signals_keys=*/absl::nullopt,
/*user_bidding_signals=*/"{some: 'json', data: {here: [1, 2]}}",
/*ads=*/
{{{GURL("https://shoes.com/render"), "{ad:'metadata', here : [1,2] }"}}},
/*ad_components=*/absl::nullopt)));
EXPECT_EQ(nullptr, RunAuctionAndWait(JsReplace(
R"({
seller: $1,
decisionLogicUrl: $2,
interestGroupBuyers: [$1],
})",
test_origin,
https_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_P(InterestGroupFencedFrameBrowserTest, NoAdComponents) {
GURL test_url = https_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 = https_server_->GetURL("c.test", "/fenced_frames/nested.html");
EXPECT_TRUE(JoinInterestGroupAndWaitInJs(
/*owner=*/url::Origin::Create(test_url),
/*name=*/"cars",
/*bidding_url=*/
https_server_->GetURL("a.test", "/interest_group/bidding_logic.js"),
/*ads=*/{{{ad_url, /*metadata=*/absl::nullopt}}},
/*ad_components=*/absl::nullopt));
ASSERT_NO_FATAL_FAILURE(RunAuctionAndNavigateFencedFrame(
ad_url, JsReplace(
R"(
{
seller: $1,
decisionLogicUrl: $2,
interestGroupBuyers: [$1]
}
)",
url::Origin::Create(test_url),
https_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);
// Navigate the ad component fenced frame to some of the URNs, which
// should navigate it to about:blank. MPArch mode currently crashes on
// navigations to about:blank, so skip in that case.
//
// TODO(https://crbug.com/1268238): Always do this once MPArch can handle
// about:blank navigations.
if (GetParam() != blink::features::FencedFramesImplementationType::kMPArch) {
absl::optional<std::vector<GURL>> all_component_urls =
GetAdAuctionComponentsInJS(ad_frame, blink::kMaxAdAuctionAdComponents);
ASSERT_TRUE(all_component_urls);
NavigateFencedFrameAndWait((*all_component_urls)[0],
GURL(url::kAboutBlankURL),
GetFencedFrameRenderFrameHost(shell()));
NavigateFencedFrameAndWait(
(*all_component_urls)[blink::kMaxAdAuctionAdComponents - 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_P(InterestGroupFencedFrameBrowserTest, AdComponents) {
GURL ad_component_url = https_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);
// Navigate the ad component fenced frame to some of the about:blank URNs.
// MPArch mode currently crashes on navigations to about:blank, so skip in
// that case.
//
// TODO(https://crbug.com/1268238): Always do this once MPArch can handle
// about:blank navigations.
if (GetParam() != blink::features::FencedFramesImplementationType::kMPArch) {
absl::optional<std::vector<GURL>> all_component_urls =
GetAdAuctionComponentsInJS(ad_frame, blink::kMaxAdAuctionAdComponents);
ASSERT_TRUE(all_component_urls);
NavigateFencedFrameAndWait((*all_component_urls)[1],
GURL(url::kAboutBlankURL),
GetFencedFrameRenderFrameHost(shell()));
NavigateFencedFrameAndWait(
(*all_component_urls)[blink::kMaxAdAuctionAdComponents - 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_P(InterestGroupFencedFrameBrowserTest,
AdComponentsNotLeaked) {
GURL ad_component_url =
https_server_->GetURL("d.test", "/fenced_frames/nested.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. MPArch mode
// currently crashes on navigations to about:blank, so skip in that case.
//
// TODO(https://crbug.com/1268238): Always do this once MPArch can handle
// about:blank navigations.
if (GetParam() != blink::features::FencedFramesImplementationType::kMPArch) {
absl::optional<std::vector<GURL>> all_component_urls =
GetAdAuctionComponentsInJS(ad_component_frame,
blink::kMaxAdAuctionAdComponents);
ASSERT_TRUE(all_component_urls);
NavigateFencedFrameAndWait((*all_component_urls)[0],
GURL(url::kAboutBlankURL),
GetFencedFrameRenderFrameHost(shell()));
NavigateFencedFrameAndWait(
(*all_component_urls)[blink::kMaxAdAuctionAdComponents - 1],
GURL(url::kAboutBlankURL), GetFencedFrameRenderFrameHost(shell()));
}
// 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 = https_server_->GetURL(
ad_frame->GetLastCommittedOrigin().host(), "/echoall");
// Use 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.
switch (GetParam()) {
case blink::features::FencedFramesImplementationType::kShadowDOM: {
observer.Wait();
break;
}
case blink::features::FencedFramesImplementationType::kMPArch: {
// Wait for the load to complete.
FencedFrame* fenced_frame = GetFencedFrame(shell());
fenced_frame->WaitForDidStopLoadingForTesting();
}
}
// 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_P(InterestGroupFencedFrameBrowserTest,
AdComponentsMainAdLoadedInMultipleFrames) {
GURL ad_component_url = https_server_->GetURL(
"d.test", "/set-header?Supports-Loading-Mode: fenced-frame");
GURL test_url = https_server_->GetURL("a.test", "/fenced_frames/basic.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
GURL ad_url = https_server_->GetURL("c.test", "/fenced_frames/nested.html");
EXPECT_TRUE(JoinInterestGroupAndWaitInJs(
/*owner=*/url::Origin::Create(test_url),
/*name=*/"cars",
/*bidding_url=*/
https_server_->GetURL("a.test", "/interest_group/bidding_logic.js"),
/*ads=*/{{{ad_url, /*metadata=*/absl::nullopt}}},
/*ad_components=*/{{{ad_component_url, /*metadata=*/absl::nullopt}}}));
content::EvalJsResult urn_url_string = RunAuctionAndWait(JsReplace(
R"(
{
seller: $1,
decisionLogicUrl: $2,
interestGroupBuyers: [$1]
}
)",
url::Origin::Create(test_url),
https_server_->GetURL("a.test", "/interest_group/decision_logic.js")));
ASSERT_TRUE(urn_url_string.value.is_string())
<< "Expected string, but got " << urn_url_string.value;
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();"
"document.body.appendChild(document.createElement('fencedframe'));"));
}
ClearReceivedRequests();
NavigateFencedFrameAndWait(urn_url, ad_url, shell());
RenderFrameHost* ad_frame = GetFencedFrameRenderFrameHost(shell());
absl::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_P(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{
{https_server_->GetURL(
"d.test", "/set-header?Supports-Loading-Mode: fenced-frame&1"),
absl::nullopt},
{https_server_->GetURL(
"d.test", "/set-header?Supports-Loading-Mode: fenced-frame&2"),
"2"},
{https_server_->GetURL(
"d.test", "/set-header?Supports-Loading-Mode: fenced-frame&3"),
"[3, {'4': 'five'}]"},
};
GURL test_url = https_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 =
https_server_->GetURL("a.test", "/generated_bidding_logic.js");
network_responder_->RegisterBidderScript(bidding_url.path(), bidding_script);
GURL ad_url = https_server_->GetURL("c.test", "/fenced_frames/nested.html");
EXPECT_TRUE(JoinInterestGroupAndWaitInJs(
/*owner=*/url::Origin::Create(test_url),
/*name=*/"cars", bidding_url,
/*ads=*/{{{ad_url, /*metadata=*/absl::nullopt}}}, ad_components));
ASSERT_NO_FATAL_FAILURE(RunAuctionAndNavigateFencedFrame(
ad_url, JsReplace(
R"({
seller: $1,
decisionLogicUrl: $2,
interestGroupBuyers: [$1]
})",
url::Origin::Create(test_url),
https_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=*/{ad_components[0].render_url,
ad_components[2].render_url},
ad_frame);
// Get first three URLs from the fenced frame.
absl::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], ad_components[0].render_url,
ad_frame);
NavigateFencedFrameAndWait((*components)[1], ad_components[2].render_url,
ad_frame);
// MPArch currently crashes on navigations to about:blank.
//
// TODO(https://crbug.com/1268238): Always do this once MPArch can handle
// about:blank navigations.
if (GetParam() != blink::features::FencedFramesImplementationType::kMPArch) {
NavigateFencedFrameAndWait((*components)[2], GURL(url::kAboutBlankURL),
ad_frame);
}
}
// 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 = https_server_->GetURL("a.test", "/echo");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
ASSERT_TRUE(JoinInterestGroupAndWaitInJs(blink::InterestGroup(
/*expiry=*/base::Time(),
/*owner=*/test_origin,
/*name=*/"cars",
/*bidding_url=*/
https_server_->GetURL("a.test",
"/interest_group/bidding_logic_throws.js"),
/*bidding_wasm_helper_url=*/absl::nullopt,
/*update_url=*/absl::nullopt,
/*trusted_bidding_signals_url=*/
https_server_->GetURL("a.test",
"/interest_group/trusted_bidding_signals.json"),
/*trusted_bidding_signals_keys=*/{{"key1"}},
/*user_bidding_signals=*/"{some: 'json', data: {here: [1, 2, 3]}}",
/*ads=*/
{{{GURL("https://example.com/render"), "{ad:'metadata', here:[1,2,3]}"}}},
/*ad_components=*/absl::nullopt)));
EXPECT_EQ(
nullptr,
EvalJs(shell(), JsReplace(
R"(
(async function() {
return await navigator.runAdAuction({
seller: $1,
decisionLogicUrl: $2,
interestGroupBuyers: [$1],
});
})())",
test_origin,
https_server_->GetURL(
"a.test", "/interest_group/decision_logic.js"))));
}
// Use bidder and seller worklet files that validate their arguments all have
// the expected values.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, ValidateWorkletParameters) {
// 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(https_server_->GetURL(kSecondBidderHost, "/"))});
// Start by adding a placeholder bidder in domain d.test, used for
// perBuyerSignals validation.
GURL second_bidder_url = https_server_->GetURL(kSecondBidderHost, "/echo");
ASSERT_TRUE(NavigateToURL(shell(), second_bidder_url));
url::Origin second_bidder_origin = url::Origin::Create(second_bidder_url);
ASSERT_TRUE(JoinInterestGroupAndWaitInJs(
/*owner=*/second_bidder_origin,
/*name=*/"boats",
/*bidding_url=*/
https_server_->GetURL(kSecondBidderHost,
"/interest_group/bidding_logic.js"),
/*ads=*/
{{{GURL("https://should-not-be-returned/"),
/*metadata=*/absl::nullopt}}}));
// 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 = https_server_->GetURL(kBidderHost, "/echo");
ASSERT_TRUE(NavigateToURL(shell(), bidder_url));
url::Origin bidder_origin = url::Origin::Create(bidder_url);
ASSERT_TRUE(JoinInterestGroupAndWaitInJs(blink::InterestGroup(
/*expiry=*/base::Time(),
/*owner=*/bidder_origin,
/*name=*/"cars",
/*bidding_url=*/
https_server_->GetURL(kBidderHost,
"/interest_group/bidding_argument_validator.js"),
/*bidding_wasm_helper_url=*/absl::nullopt,
/*update_url=*/absl::nullopt,
/*trusted_bidding_signals_url=*/
https_server_->GetURL(kBidderHost,
"/interest_group/trusted_bidding_signals.json"),
/*trusted_bidding_signals_keys=*/{{"key1"}},
/*user_bidding_signals=*/"{some: 'json', data: {here: [1, 2, 3]}}",
/*ads=*/
{{{GURL("https://example.com/render"), "{ad:'metadata', here:[1,2,3]}"}}},
/*ad_components=*/
{{{GURL("https://example.com/render-component"),
/*metadata=*/absl::nullopt}}})));
ASSERT_TRUE(
NavigateToURL(shell(), https_server_->GetURL(kTopFrameHost, "/echo")));
GURL seller_script_url = https_server_->GetURL(
kSellerHost, "/interest_group/decision_argument_validator.js");
TestFencedFrameURLMappingResultObserver observer;
ConvertFencedFrameURNToURL(
GURL(EvalJs(shell(),
JsReplace(
R"(
(async function() {
return await navigator.runAdAuction({
seller: $1,
decisionLogicUrl: $2,
trustedScoringSignalsUrl: $3,
interestGroupBuyers: [$4, $5],
auctionSignals: {so: 'I', hear: ['you', 'like', 'json']},
sellerSignals: {signals: 'from', the: ['seller']},
perBuyerSignals: {$4: {signalsForBuyer: 1}, $5: {signalsForBuyer: 2}}
});
})())",
url::Origin::Create(seller_script_url), seller_script_url,
https_server_->GetURL(
kSellerHost,
"/interest_group/trusted_scoring_signals.json"),
bidder_origin, second_bidder_origin))
.ExtractString()),
&observer);
EXPECT_EQ(GURL("https://example.com/render"), observer.mapped_url());
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
SellerWorkletThrowsFailsAuction) {
GURL test_url = https_server_->GetURL("a.test", "/echo");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);
ASSERT_TRUE(JoinInterestGroupAndWaitInJs(blink::InterestGroup(
/*expiry=*/base::Time(),
/*owner=*/test_origin,
/*name=*/"cars",
/*bidding_url=*/
https_server_->GetURL("a.test", "/interest_group/bidding_logic.js"),
/*bidding_wasm_helper_url=*/absl::nullopt,
/*update_url=*/absl::nullopt,
/*trusted_bidding_signals_url=*/
https_server_->GetURL("a.test",
"/interest_group/trusted_bidding_signals.json"),
/*trusted_bidding_signals_keys=*/{{"key1"}},
/*user_bidding_signals=*/"{some: 'json', data: {here: [1, 2, 3]}}",
/*ads=*/
{{{GURL("https://example.com/render"), "{ad:'metadata', here:[1,2,3]}"}}},
/*ad_components=*/absl::nullopt)));
EXPECT_EQ(
nullptr,
EvalJs(shell(),
JsReplace(
R"(
(async function() {
return await navigator.runAdAuction({
seller: $1,
decisionLogicUrl: $2,
interestGroupBuyers: [$1],
});
})())",
test_origin,
https_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 = https_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 perBuyerSignas ' + 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","metadata":2}')
throw 'Wrong adMetadata ' + adMetadataJSON;
}
function validateAuctionConfig(auctionConfig) {
const auctionSignalsJSON = JSON.stringify(auctionConfig.auctionSignals);
if (auctionSignalsJSON !== '3')
throw 'Wrong auctionSignals ' + auctionConfig.auctionSignalsJSON;
const sellerSignalsJSON = JSON.stringify(auctionConfig.sellerSignals);
if (sellerSignalsJSON !== '4')
throw 'Wrong sellerSignals ' + auctionConfig.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"(
(function() {
try {
navigator.joinAdInterestGroup(
{
name: 'cars',
owner: $1,
trustedBiddingSignalsUrl: $2,
trustedBiddingSignalsKeys: ['key1'],
biddingLogicUrl: $3,
userBiddingSignals: 1,
ads: [{renderUrl:"https://example.com/render", metadata:2}],
},
/*joinDurationSec=*/1);
} catch (e) {
return e.toString();
}
return 'done';
})())",
test_origin,
https_server_->GetURL("a.test", kTrustedBiddingSignalsPath),
https_server_->GetURL("a.test", kBiddingLogicPath))));
TestFencedFrameURLMappingResultObserver observer;
ConvertFencedFrameURNToURL(
GURL(EvalJs(shell(),
JsReplace(
R"(
(async function() {
return await navigator.runAdAuction({
seller: $1,
decisionLogicUrl: $2,
interestGroupBuyers: [$1],
auctionSignals: 3,
sellerSignals: 4,
perBuyerSignals: {$1: 5}
});
})())",
test_origin,
https_server_->GetURL("a.test", kDecisionLogicPath)))
.ExtractString()),
&observer);
EXPECT_EQ(GURL("https://example.com/render"), observer.mapped_url());
}
// 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 = https_server_->GetURL("a.test", "/echo");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
GURL hanging_url = https_server_->GetURL("a.test", "/hung");
url::Origin hanging_origin = url::Origin::Create(hanging_url);
EXPECT_TRUE(JoinInterestGroupAndWaitInJs(blink::InterestGroup(
/*expiry=*/base::Time(),
/*owner=*/hanging_origin,
/*name=*/"cars",
/*bidding_url=*/hanging_url,
/*bidding_wasm_helper_url=*/absl::nullopt,
/*update_url=*/absl::nullopt,
/*trusted_bidding_signals_url=*/absl::nullopt,
/*trusted_bidding_signals_keys=*/absl::nullopt,
/*user_bidding_signals=*/"{some: 'json', data: {here: [1, 2]}}",
/*ads=*/
{{{GURL("https://example.com/render"), "{ad:'metadata', here:[1,2]}"}}},
/*ad_components=*/absl::nullopt)));
ExecuteScriptAsync(shell(), JsReplace(R"(
navigator.runAdAuction({
seller: $1,
decisionLogicUrl: $2,
interestGroupBuyers: [$1]
});
)",
hanging_origin, hanging_url));
WaitForURL(https_server_->GetURL("/hung"));
}
// These tests validate the `dailyUpdateUrl` and
// navigator.updateAdInterestGroups() functionality.
// The server JSON updates all fields that can be updated.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, UpdateAllUpdatableFields) {
GURL test_url = https_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 kDailyUpdateUrlPath[] =
"/interest_group/daily_update_partial.json";
network_responder_->RegisterNetworkResponse(
kDailyUpdateUrlPath, base::StringPrintf(R"({
"biddingLogicUrl": "%s/interest_group/new_bidding_logic.js",
"trustedBiddingSignalsUrl":
"%s/interest_group/new_trusted_bidding_signals_url.json",
"trustedBiddingSignalsKeys": ["new_key"],
"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_TRUE(JoinInterestGroupAndWaitInJs(blink::InterestGroup(
/*expiry=*/base::Time(),
/*owner=*/test_origin,
/*name=*/"cars",
/*bidding_url=*/
https_server_->GetURL("a.test", "/interest_group/bidding_logic.js"),
/*bidding_wasm_helper_url=*/absl::nullopt,
/*update_url=*/
https_server_->GetURL("a.test", kDailyUpdateUrlPath),
/*trusted_bidding_signals_url=*/
https_server_->GetURL("a.test",
"/interest_group/trusted_bidding_signals.json"),
/*trusted_bidding_signals_keys=*/{{"key1"}},
/*user_bidding_signals=*/"{some: 'json', data: {here: [1, 2, 3]}}",
/*ads=*/
{{{GURL("https://example.com/render"), "{ad:'metadata', here:[1,2,3]}"}}},
/*ad_components=*/absl::nullopt)));
EXPECT_EQ("done", UpdateInterestGroupsInJS());
WaitForInterestGroupsSatisfying(
test_origin,
base::BindLambdaForTesting(
[](const std::vector<StorageInterestGroup>& groups) {
if (groups.size() != 1)
return false;
const auto& group = groups[0].interest_group;
return group.name == "cars" && 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 &&
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 = https_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 kDailyUpdateUrlPath[] =
"/interest_group/daily_update_partial.json";
network_responder_->RegisterNetworkResponse(
kDailyUpdateUrlPath, base::StringPrintf(R"({
"ads": [{"renderUrl": "%s/new_ad_render_url",
"metadata": {"new_a": "b"}
}]
})",
test_origin.Serialize().c_str()));
ASSERT_TRUE(JoinInterestGroupAndWaitInJs(blink::InterestGroup(
/*expiry=*/base::Time(),
/*owner=*/test_origin,
/*name=*/"cars",
/*bidding_url=*/
https_server_->GetURL("a.test", "/interest_group/bidding_logic.js"),
/*bidding_wasm_helper_url=*/absl::nullopt,
/*update_url=*/
https_server_->GetURL("a.test", kDailyUpdateUrlPath),
/*trusted_bidding_signals_url=*/
https_server_->GetURL("a.test",
"/interest_group/trusted_bidding_signals.json"),
/*trusted_bidding_signals_keys=*/{{"key1"}},
/*user_bidding_signals=*/"{some: 'json', data: {here: [1, 2, 3]}}",
/*ads=*/
{{{GURL("https://example.com/render"), "{ad:'metadata', here:[1,2,3]}"}}},
/*ad_components=*/absl::nullopt)));
EXPECT_EQ("done", UpdateInterestGroupsInJS());
// Navigate away -- the update should still continue.
GURL test_url_b = https_server_->GetURL("b.test", "/echo");
ASSERT_TRUE(NavigateToURL(shell(), test_url_b));
WaitForInterestGroupsSatisfying(
test_origin,
base::BindLambdaForTesting(
[](const std::vector<StorageInterestGroup>& groups) {
if (groups.size() != 1)
return false;
const auto& group = groups[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/daily_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 &&
group.ads.value()[0].render_url.path() ==
"/new_ad_render_url" &&
group.ads.value()[0].metadata == "{\"new_a\":\"b\"}";
}));
}
// This test exercises the interest group and ad auction services directly,
// rather than via Blink, to ensure that those services running in the browser
// implement important security checks (Blink may also perform its own
// checking, but the render process is untrusted).
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, RunAdAuctionBasicBypassBlink) {
ASSERT_TRUE(NavigateToURL(shell(), https_server_->GetURL("a.test", "/echo")));
mojo::Remote<blink::mojom::AdAuctionService> auction_service;
AdAuctionServiceImpl::CreateMojoService(
web_contents()->GetMainFrame(),
auction_service.BindNewPipeAndPassReceiver());
base::RunLoop run_loop;
auto auction_config = blink::mojom::AuctionAdConfig::New();
auction_config->auction_ad_config_non_shared_params =
blink::mojom::AuctionAdConfigNonSharedParams::New();
auction_service->RunAdAuction(
std::move(auction_config),
base::BindLambdaForTesting([&run_loop](const absl::optional<GURL>& url) {
EXPECT_THAT(url, Eq(absl::nullopt));
run_loop.Quit();
}));
run_loop.Run();
}
// Fixture for Blink-bypassing auction tests that share the same interest group
// -- useful for checking auction service security validations.
class InterestGroupBrowserTestRunAdAuctionBypassBlink
: public InterestGroupBrowserTest {
protected:
void SetUpOnMainThread() override {
InterestGroupBrowserTest::SetUpOnMainThread();
ad_url_ = https_server_->GetURL("c.test", "/echo?render_ad");
GURL test_url_a = https_server_->GetURL("a.test", "/echo");
test_origin_a_ = url::Origin::Create(test_url_a);
ASSERT_TRUE(test_url_a.SchemeIs(url::kHttpsScheme));
ASSERT_TRUE(NavigateToURL(shell(), test_url_a));
mojo::Remote<blink::mojom::AdAuctionService> interest_service;
AdAuctionServiceImpl::CreateMojoService(
web_contents()->GetMainFrame(),
interest_service.BindNewPipeAndPassReceiver());
// Set up ad_url_ as the only interest group ad in the auction.
blink::InterestGroup interest_group;
interest_group.expiry = base::Time::Now() + base::Seconds(300);
constexpr char kGroupName[] = "cars";
interest_group.name = kGroupName;
interest_group.owner = test_origin_a_;
interest_group.bidding_url =
https_server_->GetURL("a.test", "/interest_group/bidding_logic.js");
interest_group.trusted_bidding_signals_url = https_server_->GetURL(
"a.test", "/interest_group/trusted_bidding_signals.json");
interest_group.trusted_bidding_signals_keys.emplace();
interest_group.trusted_bidding_signals_keys->push_back("key1");
interest_group.user_bidding_signals =
"{\"some\": \"json\", \"data\": {\"here\": [1, 2, 3]}}";
interest_group.ads.emplace();
interest_group.ads->push_back(blink::InterestGroup::Ad(
/* render_url = */ ad_url_,
/* metadata = */ "{\"ad\": \"metadata\", \"here\": [1, 2, 3]}"));
interest_service->JoinInterestGroup(std::move(interest_group));
interest_service.FlushForTesting();
EXPECT_EQ(1, GetJoinCount(test_origin_a_, kGroupName));
}
absl::optional<GURL> RunAuctionBypassBlink(
blink::mojom::AuctionAdConfigPtr config) {
absl::optional<GURL> maybe_url;
base::RunLoop run_loop;
mojo::Remote<blink::mojom::AdAuctionService> auction_service;
AdAuctionServiceImpl::CreateMojoService(
web_contents()->GetMainFrame(),
auction_service.BindNewPipeAndPassReceiver());
auction_service->RunAdAuction(
std::move(config),
base::BindLambdaForTesting(
[&run_loop, &maybe_url](const absl::optional<GURL>& url) {
maybe_url = url;
run_loop.Quit();
}));
run_loop.Run();
if (maybe_url) {
TestFencedFrameURLMappingResultObserver observer;
ConvertFencedFrameURNToURL(*maybe_url, &observer);
EXPECT_TRUE(observer.mapped_url());
absl::optional<GURL> decoded_URL = observer.mapped_url();
EXPECT_EQ(decoded_URL, ConvertFencedFrameURNToURLInJS(*maybe_url));
NavigateIframeAndCheckURL(web_contents(), *maybe_url,
decoded_URL.value_or(GURL()));
return *observer.mapped_url();
}
return absl::nullopt;
}
url::Origin test_origin_a_;
GURL ad_url_;
};
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTestRunAdAuctionBypassBlink,
BasicSuccess) {
GURL test_url_b = https_server_->GetURL("b.test", "/page_with_iframe.html");
ASSERT_TRUE(test_url_b.SchemeIs(url::kHttpsScheme));
url::Origin test_origin_b = url::Origin::Create(test_url_b);
ASSERT_TRUE(NavigateToURL(shell(), test_url_b));
auto config = blink::mojom::AuctionAdConfig::New();
config->seller = test_origin_b;
config->decision_logic_url =
https_server_->GetURL("b.test", "/interest_group/decision_logic.js");
config->auction_ad_config_non_shared_params =
blink::mojom::AuctionAdConfigNonSharedParams::New();
config->auction_ad_config_non_shared_params->interest_group_buyers = {
test_origin_a_};
EXPECT_THAT(RunAuctionBypassBlink(std::move(config)), Optional(Eq(ad_url_)));
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTestRunAdAuctionBypassBlink,
SellerNotHttps) {
GURL test_url_b = https_server_->GetURL("a.test", "/echo");
url::Origin test_origin_b = url::Origin::Create(test_url_b);
ASSERT_TRUE(NavigateToURL(shell(), test_url_b));
auto config = blink::mojom::AuctionAdConfig::New();
config->seller = test_origin_b;
config->decision_logic_url = embedded_test_server()->GetURL(
"b.test", "/interest_group/decision_logic.js");
ASSERT_TRUE(config->decision_logic_url.SchemeIs(url::kHttpScheme));
config->auction_ad_config_non_shared_params =
blink::mojom::AuctionAdConfigNonSharedParams::New();
config->auction_ad_config_non_shared_params->interest_group_buyers = {
test_origin_a_};
EXPECT_THAT(RunAuctionBypassBlink(std::move(config)), Eq(absl::nullopt));
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTestRunAdAuctionBypassBlink,
WrongDecisionUrlOrigin) {
// The `decision_logic_url` origin doesn't match `seller`s, which is invalid.
auto config = blink::mojom::AuctionAdConfig::New();
config->seller = test_origin_a_;
config->decision_logic_url =
https_server_->GetURL("b.test", "/interest_group/decision_logic.js");
config->auction_ad_config_non_shared_params =
blink::mojom::AuctionAdConfigNonSharedParams::New();
config->auction_ad_config_non_shared_params->interest_group_buyers = {
test_origin_a_};
EXPECT_THAT(RunAuctionBypassBlink(std::move(config)), Eq(absl::nullopt));
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTestRunAdAuctionBypassBlink,
InterestGroupBuyerOriginNotHttps) {
GURL test_url_b = https_server_->GetURL("b.test", "/page_with_iframe.html");
ASSERT_TRUE(test_url_b.SchemeIs(url::kHttpsScheme));
url::Origin test_origin_b = url::Origin::Create(test_url_b);
ASSERT_TRUE(NavigateToURL(shell(), test_url_b));
// Same hostname as `test_url_a_`, different scheme. This buyer is not valid
// because it is not https, so the auction fails.
GURL test_url_a_http = embedded_test_server()->GetURL("a.test", "/echo");
ASSERT_TRUE(test_url_a_http.SchemeIs(url::kHttpScheme));
url::Origin test_origin_a_http = url::Origin::Create(test_url_a_http);
auto config = blink::mojom::AuctionAdConfig::New();
config->seller = test_origin_b;
config->decision_logic_url =
https_server_->GetURL("b.test", "/interest_group/decision_logic.js");
config->auction_ad_config_non_shared_params =
blink::mojom::AuctionAdConfigNonSharedParams::New();
config->auction_ad_config_non_shared_params->interest_group_buyers = {
test_origin_a_http};
EXPECT_THAT(RunAuctionBypassBlink(std::move(config)), Eq(absl::nullopt));
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTestRunAdAuctionBypassBlink,
InterestGroupBuyerOriginNotHttpsMultipleBuyers) {
GURL test_url_b = https_server_->GetURL("b.test", "/page_with_iframe.html");
ASSERT_TRUE(test_url_b.SchemeIs(url::kHttpsScheme));
url::Origin test_origin_b = url::Origin::Create(test_url_b);
ASSERT_TRUE(NavigateToURL(shell(), test_url_b));
// Same hostname as `test_url_a_`, different scheme. This buyer is not valid
// because it is not https, so the auction fails, even though the other buyer
// is valid.
GURL test_url_a_http = embedded_test_server()->GetURL("a.test", "/echo");
ASSERT_TRUE(test_url_a_http.SchemeIs(url::kHttpScheme));
url::Origin test_origin_a_http = url::Origin::Create(test_url_a_http);
auto config = blink::mojom::AuctionAdConfig::New();
config->seller = test_origin_b;
config->decision_logic_url =
https_server_->GetURL("b.test", "/interest_group/decision_logic.js");
config->auction_ad_config_non_shared_params =
blink::mojom::AuctionAdConfigNonSharedParams::New();
config->auction_ad_config_non_shared_params->interest_group_buyers = {
test_origin_a_, test_origin_a_http};
EXPECT_THAT(RunAuctionBypassBlink(std::move(config)), Eq(absl::nullopt));
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTestRunAdAuctionBypassBlink,
BuyerWithNoRegisteredInterestGroupsIgnored) {
GURL test_url_b = https_server_->GetURL("b.test", "/page_with_iframe.html");
ASSERT_TRUE(test_url_b.SchemeIs(url::kHttpsScheme));
url::Origin test_origin_b = url::Origin::Create(test_url_b);
ASSERT_TRUE(NavigateToURL(shell(), test_url_b));
// New valid origin, not associated with any registered interest group. Its
// presence in the auctions `interest_group_buyers` shouldn't affect the
// auction outcome.
GURL test_url_c = https_server_->GetURL("c.test", "/echo");
ASSERT_TRUE(test_url_c.SchemeIs(url::kHttpsScheme));
url::Origin test_origin_c = url::Origin::Create(test_url_c);
auto config = blink::mojom::AuctionAdConfig::New();
config->seller = test_origin_b;
config->decision_logic_url =
https_server_->GetURL("b.test", "/interest_group/decision_logic.js");
config->auction_ad_config_non_shared_params =
blink::mojom::AuctionAdConfigNonSharedParams::New();
config->auction_ad_config_non_shared_params->interest_group_buyers = {
test_origin_a_, test_origin_c};
EXPECT_THAT(RunAuctionBypassBlink(std::move(config)), Optional(Eq(ad_url_)));
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTestRunAdAuctionBypassBlink,
PerBuyerSignalsValid) {
GURL test_url_b = https_server_->GetURL("b.test", "/page_with_iframe.html");
ASSERT_TRUE(test_url_b.SchemeIs(url::kHttpsScheme));
url::Origin test_origin_b = url::Origin::Create(test_url_b);
ASSERT_TRUE(NavigateToURL(shell(), test_url_b));
// Per-buyer signals are valid because `test_origin_a_` is in the set of
// buyers, so the auction succeeds.
auto config = blink::mojom::AuctionAdConfig::New();
config->seller = test_origin_b;
config->decision_logic_url =
https_server_->GetURL("b.test", "/interest_group/decision_logic.js");
config->auction_ad_config_non_shared_params =
blink::mojom::AuctionAdConfigNonSharedParams::New();
config->auction_ad_config_non_shared_params->interest_group_buyers = {
test_origin_a_};
config->auction_ad_config_non_shared_params->per_buyer_signals.emplace();
config->auction_ad_config_non_shared_params->per_buyer_signals
.value()[test_origin_a_] = "{\"even\": \"more\", \"x\": 4.5}";
EXPECT_THAT(RunAuctionBypassBlink(std::move(config)), Optional(Eq(ad_url_)));
}
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTestRunAdAuctionBypassBlink,
TrustedScoringSignalsUrlWrongOrigin) {
GURL test_url_b = https_server_->GetURL("b.test", "/page_with_iframe.html");
ASSERT_TRUE(test_url_b.SchemeIs(url::kHttpsScheme));
url::Origin test_origin_b = url::Origin::Create(test_url_b);
ASSERT_TRUE(NavigateToURL(shell(), test_url_b));
auto config = blink::mojom::AuctionAdConfig::New();
config->seller = test_origin_b;
config->decision_logic_url =
https_server_->GetURL("b.test", "/interest_group/decision_logic.js");
config->trusted_scoring_signals_url = https_server_->GetURL(
"not-b.test", "/interest_group/trusted_scoring_signals.json");
config->auction_ad_config_non_shared_params =
blink::mojom::AuctionAdConfigNonSharedParams::New();
config->auction_ad_config_non_shared_params->interest_group_buyers = {
test_origin_a_};
EXPECT_THAT(RunAuctionBypassBlink(std::move(config)), Eq(absl::nullopt));
}
// Validate that createAdRequest is available and be successfully called as part
// of PARAKEET.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, CreateAdRequestWorks) {
GURL test_url = https_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 = https_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) {
// Learn the bidder IG, served from the local server.
GURL bidder_url =
https_server_->GetURL("b.test", "/interest_group/bidding_logic.js");
ASSERT_TRUE(NavigateToURL(shell(), https_server_->GetURL("b.test", "/echo")));
url::Origin bidder_origin = url::Origin::Create(bidder_url);
EXPECT_TRUE(JoinInterestGroupAndWaitInJs(
/*owner=*/bidder_origin,
/*name=*/"Cthulhu", bidder_url,
/*ads=*/
{{{GURL("https://example.com/render"), /*metadata=*/absl::nullopt}}}));
URLLoaderMonitor url_loader_monitor;
// 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(nullptr, 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.
absl::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_FAILED, bidder_status.error_code);
EXPECT_THAT(bidder_status.cors_error_status,
Optional(network::CorsErrorStatus(
network::mojom::CorsError::kPreflightMissingAllowOriginHeader,
network::mojom::IPAddressSpace::kLocal,
network::mojom::IPAddressSpace::kUnknown)));
}
IN_PROC_BROWSER_TEST_F(InterestGroupPrivateNetworkBrowserTest,
SellerOnPrivateNetwork) {
GURL seller_url =
https_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);
EXPECT_TRUE(JoinInterestGroupAndWaitInJs(
/*owner=*/test_origin,
/*name=*/"Cthulhu",
/*bidding_url=*/
remote_test_server_.GetURL("a.test", "/interest_group/bidding_logic.js"),
/*ads=*/
{{{GURL("https://example.com/render"), /*metadata=*/absl::nullopt}}}));
URLLoaderMonitor url_loader_monitor;
EXPECT_EQ(nullptr,
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.
absl::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_FAILED, seller_status.error_code);
EXPECT_THAT(seller_status.cors_error_status,
Optional(network::CorsErrorStatus(
network::mojom::CorsError::kPreflightMissingAllowOriginHeader,
network::mojom::IPAddressSpace::kLocal,
network::mojom::IPAddressSpace::kUnknown)));
}
// 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);
// Use `https_server_` exclusively with hostname "b.test" for reports.
GURL bidder_report_to_url = https_server_->GetURL("b.test", "/bidder_report");
GURL seller_report_to_url = https_server_->GetURL("b.test", "/seller_report");
URLLoaderMonitor url_loader_monitor;
EXPECT_TRUE(JoinInterestGroupAndWaitInJs(
/*owner=*/test_origin,
/*name=*/bidder_report_to_url.spec(),
/*bidding_url=*/
remote_test_server_.GetURL(
"a.test", "/interest_group/bidding_logic_report_to_name.js"),
/*ads=*/
{{{GURL("https://example.com/render"), /*metadata=*/absl::nullopt}}}));
EXPECT_EQ(
"https://example.com/render",
RunAuctionAndWaitForURL(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)));
// 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);
const network::URLLoaderCompletionStatus& bidder_report_status =
url_loader_monitor.WaitForRequestCompletion(bidder_report_to_url);
EXPECT_EQ(net::ERR_FAILED, bidder_report_status.error_code);
EXPECT_THAT(bidder_report_status.cors_error_status,
Optional(network::CorsErrorStatus(
network::mojom::CorsError::kPreflightMissingAllowOriginHeader,
network::mojom::IPAddressSpace::kLocal,
network::mojom::IPAddressSpace::kUnknown)));
const network::URLLoaderCompletionStatus& seller_report_status =
url_loader_monitor.WaitForRequestCompletion(seller_report_to_url);
EXPECT_EQ(net::ERR_FAILED, seller_report_status.error_code);
EXPECT_THAT(seller_report_status.cors_error_status,
Optional(network::CorsErrorStatus(
network::mojom::CorsError::kPreflightMissingAllowOriginHeader,
network::mojom::IPAddressSpace::kLocal,
network::mojom::IPAddressSpace::kUnknown)));
}
// 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 trusted_bidding_signals_url_with_query = remote_test_server_.GetURL(
"a.test",
"/interest_group/trusted_bidding_signals.json?hostname=a.test&keys=key1");
GURL seller_url = remote_test_server_.GetURL(
"a.test", "/interest_group/decision_logic_report_to_seller_signals.js");
GURL ad_url = https_server_->GetURL("c.test", "/echo");
// While reports should should be made to these URLs in this test, their
// results don't matter, so there's no need for a test server respond to for
// 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");
URLLoaderMonitor url_loader_monitor;
ASSERT_TRUE(JoinInterestGroupAndWaitInJs(blink::InterestGroup(
/*expiry=*/base::Time(),
/*owner=*/test_origin,
/*name=*/bidder_report_to_url.spec(), bidder_url,
/*bidding_wasm_helper_url=*/absl::nullopt,
/*update_url=*/absl::nullopt, trusted_bidding_signals_url,
/*trusted_bidding_signals_keys=*/{{"key1"}},
/*user_bidding_signals=*/absl::nullopt,
/*ads=*/
{{{ad_url, /*metadata=*/absl::nullopt}}},
/*ad_components=*/absl::nullopt)));
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.GetRequestInfo(bidder_report_to_url)
->trusted_params->client_security_state->ip_address_space);
EXPECT_EQ(network::mojom::IPAddressSpace::kPublic,
url_loader_monitor.GetRequestInfo(seller_report_to_url)
->trusted_params->client_security_state->ip_address_space);
// Check that both reports reached the server.
WaitForURL(bidder_report_to_url);
WaitForURL(seller_report_to_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, and expecting the request to be
// blocked, and then adding another interest group and updating it from a
// private page, which should succeed. 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 = https_server_->GetURL(
"a.test", "/interest_group/daily_update_partial.json");
GURL initial_bidding_url = https_server_->GetURL(
"a.test", "/interest_group/initial_bidding_logic.js");
GURL new_bidding_url =
https_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 = https_server_->GetURL(
"a.test",
"/set-header?Content-Security-Policy: treat-as-public-address");
group_name = kPubliclyUpdateGroupName;
} else {
test_url = https_server_->GetURL("a.test", "/echo");
group_name = kLocallyUpdateGroupName;
}
ASSERT_TRUE(NavigateToURL(shell(), test_url));
ASSERT_TRUE(JoinInterestGroupAndWaitInJs(blink::InterestGroup(
/*expiry=*/base::Time(),
/*owner=*/url::Origin::Create(test_url), group_name,
initial_bidding_url,
/*bidding_wasm_helper_url=*/absl::nullopt, update_url,
/*trusted_bidding_signals_url=*/absl::nullopt,
/*trusted_bidding_signals_keys=*/absl::nullopt,
/*user_bidding_signals=*/absl::nullopt,
/*ads=*/
{{{GURL("https://example.com/render"), /*metadata=*/absl::nullopt}}},
/*ad_components=*/absl::nullopt)));
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::kLocal,
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 be blocked in the public address space case.
if (public_address_space) {
EXPECT_EQ(
net::ERR_FAILED,
url_loader_monitor.WaitForRequestCompletion(update_url).error_code);
} else {
EXPECT_EQ(
net::OK,
url_loader_monitor.WaitForRequestCompletion(update_url).error_code);
}
url_loader_monitor.ClearRequests();
}
// Wait for the kLocallyUpdateGroupName interest group to have an updated
// bidding URL, while expecting the kPubliclyUpdateGroupName to continue to
// have the original bidding URL. 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(
[&](const std::vector<StorageInterestGroup>& storage_groups) {
bool found_updated_group = false;
for (const auto& storage_group : storage_groups) {
const blink::InterestGroup& group = storage_group.interest_group;
if (group.name == kPubliclyUpdateGroupName) {
EXPECT_EQ(initial_bidding_url, group.bidding_url);
} else {
EXPECT_EQ(group.name, kLocallyUpdateGroupName);
found_updated_group = (new_bidding_url == group.bidding_url);
}
}
return found_updated_group;
}));
}
// 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.
IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
FeaturesEnabledForAllByPermissionsPolicy) {
// clang-format off
GURL test_url = https_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}"
")"
")");
// clang-format on
ASSERT_TRUE(NavigateToURL(shell(), test_url));
RenderFrameHost* main_frame = web_contents()->GetMainFrame();
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);
// The server JSON updates all fields that can be updated.
constexpr char kDailyUpdateUrlPath[] =
"/interest_group/daily_update_partial.json";
network_responder_->RegisterNetworkResponse(kDailyUpdateUrlPath,
base::StringPrintf(
R"(
{
"trustedBiddingSignalsKeys": ["new_key"],
}
)"));
GURL url;
url::Origin origin;
std::string host;
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) {
url = execution_target->GetLastCommittedURL();
origin = url::Origin::Create(url);
host = url.host();
WebContentsConsoleObserver console_observer(shell()->web_contents());
console_observer.SetPattern(WarningPermissionsPolicy("*", "*"));
EXPECT_TRUE(JoinInterestGroupAndWaitInJs(
blink::InterestGroup(
/*expiry=*/base::Time(),
/*owner=*/origin,
/*name=*/"cars",
/*bidding_url=*/
https_server_->GetURL(host, "/interest_group/bidding_logic.js"),
/*bidding_wasm_helper_url=*/absl::nullopt,
/*update_url=*/
https_server_->GetURL(host,
"/interest_group/daily_update_partial.json"),
/*trusted_bidding_signals_url=*/absl::nullopt,
/*trusted_bidding_signals_keys=*/absl::nullopt,
/*user_bidding_signals=*/absl::nullopt,
/*ads=*/
{{{GURL("https://example.com/render"),
/*metadata=*/absl::nullopt}}},
/*ad_components=*/absl::nullopt),
execution_target));
EXPECT_EQ("https://example.com/render",
RunAuctionAndWaitForURL(
JsReplace(
R"(
{
seller: $1,
decisionLogicUrl: $2,
interestGroupBuyers: [$1],
}
)",
origin,
https_server_->GetURL(
host, "/interest_group/decision_logic.js")),
execution_target));
EXPECT_EQ("done", UpdateInterestGroupsInJS(execution_target));
// The second UpdateInterestGroupsInJS will not add a warning message, since
// the same message has already been added and redundant messages will be
// discarded.
EXPECT_EQ("done", UpdateInterestGroupsInJS(execution_target));
EXPECT_TRUE(LeaveInterestGroupInJS(origin, "cars", execution_target));
// It seems discard_duplicates of AddConsoleMessage works differently on
// Android and other platforms. On Android, a message will be discarded if
// it's not unique across all frames in a page. On other platforms, a
// message will be discarded if it's not unique per origin (e.g., iframe
// a.test and iframe b.test can have the same message, while the same
// message from another a.test will be discarded).
#if BUILDFLAG(IS_ANDROID)
RenderFrameHost* execution_targets_with_message[] = {cross_origin_iframe};
#else
RenderFrameHost* execution_targets_with_message[] = {
cross_origin_iframe, inner_cross_origin_iframe,
same_origin_iframe_in_cross_origin_iframe};
#endif // BUILDFLAG(IS_ANDROID)
if (std::find(std::begin(execution_targets_with_message),
std::end(execution_targets_with_message), execution_target) !=
std::end(execution_targets_with_message)) {
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 {
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 = https_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()->GetMainFrame();
RenderFrameHost* iframe = ChildFrameAt(main_frame, 0);
for (auto* execution_target : {main_frame, iframe}) {
ExpectNotAllowedToJoinOrUpdateInterestGroup(origin, execution_target);
ExpectNotAllowedToRunAdAuction(
origin,
https_server_->GetURL("a.test", "/interest_group/decision_logic.js"),
execution_target);
ExpectNotAllowedToLeaveInterestGroup(origin, "cars", 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 = https_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()->GetMainFrame(), 0);
ExpectNotAllowedToJoinOrUpdateInterestGroup(origin, same_origin_iframe);
ExpectNotAllowedToRunAdAuction(
origin,
https_server_->GetURL("a.test", "/interest_group/decision_logic.js"),
same_origin_iframe);
ExpectNotAllowedToLeaveInterestGroup(origin, "cars", 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 = https_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()->GetMainFrame();
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 kDailyUpdateUrlPath[] =
"/interest_group/daily_update_partial.json";
network_responder_->RegisterNetworkResponse(kDailyUpdateUrlPath,
base::StringPrintf(
R"(
{
"trustedBiddingSignalsKeys": ["new_key"],
}
)"));
GURL url;
url::Origin origin;
std::string host;
RenderFrameHost* execution_targets[] = {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_TRUE(JoinInterestGroupAndWaitInJs(
blink::InterestGroup(
/*expiry=*/base::Time(),
/*owner=*/origin,
/*name=*/"cars",
/*bidding_url=*/
https_server_->GetURL(host, "/interest_group/bidding_logic.js"),
/*bidding_wasm_helper_url=*/absl::nullopt,
/*update_url=*/
https_server_->GetURL(host,
"/interest_group/daily_update_partial.json"),
/*trusted_bidding_signals_url=*/absl::nullopt,
/*trusted_bidding_signals_keys=*/absl::nullopt,
/*user_bidding_signals=*/absl::nullopt,
/*ads=*/
{{{GURL("https://example.com/render"),
/*metadata=*/absl::nullopt}}},
/*ad_components=*/absl::nullopt),
execution_target));
EXPECT_EQ("https://example.com/render",
RunAuctionAndWaitForURL(
JsReplace(
R"(
{
seller: $1,
decisionLogicUrl: $2,
interestGroupBuyers: [$1],
}
)",
origin,
https_server_->GetURL(
host, "/interest_group/decision_logic.js")),
execution_target));
EXPECT_EQ("done", UpdateInterestGroupsInJS(execution_target));
EXPECT_TRUE(LeaveInterestGroupInJS(origin, "cars", execution_target));
EXPECT_TRUE(console_observer.messages().empty());
}
}
// 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 = https_server_->GetURL("b.test", "/echo");
url::Origin other_origin = url::Origin::Create(other_url);
// clang-format off
GURL test_url = https_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()->GetMainFrame(), 0);
RenderFrameHost* inner_iframe = ChildFrameAt(outter_iframe, 0);
for (auto* execution_target : {outter_iframe, inner_iframe}) {
ExpectNotAllowedToJoinOrUpdateInterestGroup(other_origin, execution_target);
ExpectNotAllowedToRunAdAuction(
other_origin,
https_server_->GetURL("b.test", "/interest_group/decision_logic.js"),
execution_target);
ExpectNotAllowedToLeaveInterestGroup(other_origin, "cars",
execution_target);
}
test_url = https_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()->GetMainFrame(), 0);
ExpectNotAllowedToJoinOrUpdateInterestGroup(origin, same_origin_iframe);
ExpectNotAllowedToRunAdAuction(
origin,
https_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 = https_server_->GetURL("b.test", "/echo");
url::Origin other_origin = url::Origin::Create(other_url);
// clang-format off
GURL test_url = https_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()->GetMainFrame(), 0);
RenderFrameHost* iframe_ad_auction =
ChildFrameAt(web_contents()->GetMainFrame(), 1);
// Interest group APIs succeed and run ad auction fails for
// iframe_interest_group.
EXPECT_TRUE(JoinInterestGroupAndWaitInJs(
blink::InterestGroup(
/*expiry=*/base::Time(),
/*owner=*/other_origin,
/*name=*/"cars",
/*bidding_url=*/
https_server_->GetURL("b.test", "/interest_group/bidding_logic.js"),
/*bidding_wasm_helper_url=*/absl::nullopt,
/*update_url=*/
https_server_->GetURL("b.test",
"/interest_group/daily_update_partial.json"),
/*trusted_bidding_signals_url=*/absl::nullopt,
/*trusted_bidding_signals_keys=*/absl::nullopt,
/*user_bidding_signals=*/absl::nullopt,
/*ads=*/
{{{GURL("https://example.com/render"), /*metadata=*/absl::nullopt}}},
/*ad_components=*/absl::nullopt),
iframe_interest_group));
EXPECT_EQ("done", UpdateInterestGroupsInJS(iframe_interest_group));
ExpectNotAllowedToRunAdAuction(
other_origin,
https_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,
https_server_->GetURL("b.test",
"/interest_group/decision_logic.js")),
iframe_ad_auction));
ExpectNotAllowedToLeaveInterestGroup(other_origin, "cars", iframe_ad_auction);
EXPECT_TRUE(
LeaveInterestGroupInJS(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 = https_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()->GetMainFrame();
RenderFrameHost* iframe = ChildFrameAt(main_frame, 0);
for (auto* execution_target : {main_frame, iframe}) {
ExpectNotAllowedToJoinOrUpdateInterestGroup(origin, execution_target);
ExpectNotAllowedToRunAdAuction(
origin,
https_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, InvalidURN) {
GURL invalid_urn("urn:uuid:c36973b5-e5d9-de59-e4c4-364f137b3c7a");
EXPECT_EQ(absl::nullopt, ConvertFencedFrameURNToURLInJS(invalid_urn));
}
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"}}},
{blink::features::kAdInterestGroupAPIRestrictedPolicyByDefault, {}}},
/*disabled_features=*/{});
// TODO(crbug.com/1186444): When
// kAdInterestGroupAPIRestrictedPolicyByDefault is the default, we won't
// need to set it here.
}
protected:
base::test::ScopedFeatureList feature_list_;
};
// TODO(crbug.com/1289207): 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 = https_server_->GetURL("a.test", "/echo");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
const url::Origin test_origin = url::Origin::Create(test_url);
EXPECT_TRUE(JoinInterestGroupAndWaitInJs(blink::InterestGroup(
/*expiry=*/base::Time(),
/*owner=*/test_origin,
/*name=*/"cars",
/*bidding_url=*/
https_server_->GetURL("a.test", "/interest_group/bidding_logic.js"),
/*bidding_wasm_helper_url=*/absl::nullopt,
/*update_url=*/absl::nullopt,
/*trusted_bidding_signals_url=*/absl::nullopt,
/*trusted_bidding_signals_keys=*/absl::nullopt,
/*user_bidding_signals=*/absl::nullopt,
/*ads=*/
{{{GURL("https://example.com/render"),
"{ad:'metadata', here : [1,2] }"}}},
/*ad_components=*/absl::nullopt)));
// 1st auction -- before navigations
EXPECT_EQ("https://example.com/render",
RunAuctionAndWaitForURL(JsReplace(
R"({
seller: $1,
decisionLogicUrl: $2,
interestGroupBuyers: [$1],
})",
test_origin,
https_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 = https_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,
https_server_->GetURL("a.test",
"/interest_group/decision_logic.js"))));
// 3rd auction -- after navigations; should fail due to hitting the auction
// limit.
EXPECT_EQ(nullptr, RunAuctionAndWait(JsReplace(
R"({
seller: $1,
decisionLogicUrl: $2,
interestGroupBuyers: [$1],
})",
test_origin,
https_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 =
https_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()->GetMainFrame(), 0);
EXPECT_TRUE(JoinInterestGroupAndWaitInJs(blink::InterestGroup(
/*expiry=*/base::Time(),
/*owner=*/test_origin,
/*name=*/"cars",
/*bidding_url=*/
https_server_->GetURL("a.test", "/interest_group/bidding_logic.js"),
/*bidding_wasm_helper_url=*/absl::nullopt,
/*update_url=*/absl::nullopt,
/*trusted_bidding_signals_url=*/absl::nullopt,
/*trusted_bidding_signals_keys=*/absl::nullopt,
/*user_bidding_signals=*/absl::nullopt,
/*ads=*/
{{{GURL("https://example.com/render"),
"{ad:'metadata', here : [1,2] }"}}},
/*ad_components=*/absl::nullopt)));
// 1st auction -- in main frame
EXPECT_EQ("https://example.com/render",
RunAuctionAndWaitForURL(JsReplace(
R"({
seller: $1,
decisionLogicUrl: $2,
interestGroupBuyers: [$1],
})",
test_origin,
https_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,
https_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(
nullptr,
RunAuctionAndWait(JsReplace(
R"({
seller: $1,
decisionLogicUrl: $2,
interestGroupBuyers: [$1],
})",
test_origin,
https_server_->GetURL(
"a.test", "/interest_group/decision_logic.js")),
b_iframe));
}
} // namespace
} // namespace content