blob: b1045c407a98a9f668b59368098b8522d0f31159 [file] [log] [blame]
// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/privacy_sandbox/privacy_sandbox_settings.h"
#include <memory>
#include <string>
#include <tuple>
#include <utility>
#include <vector>
#include "base/command_line.h"
#include "base/containers/contains.h"
#include "base/metrics/statistics_recorder.h"
#include "base/notreached.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/values.h"
#include "chrome/browser/browsing_data/chrome_browsing_data_remover_constants.h"
#include "chrome/browser/privacy_sandbox/privacy_sandbox_attestations/privacy_sandbox_attestations_mixin.h"
#include "chrome/browser/privacy_sandbox/privacy_sandbox_settings_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/test/base/mixin_based_in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/privacy_sandbox/privacy_sandbox_features.h"
#include "components/privacy_sandbox/privacy_sandbox_test_util.h"
#include "content/public/browser/browser_context.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/browsing_data_remover_test_util.h"
#include "content/public/test/fenced_frame_reporter_observer.h"
#include "content/public/test/fenced_frame_test_util.h"
#include "content/public/test/shared_storage_test_utils.h"
#include "content/public/test/test_frame_navigation_observer.h"
#include "content/public/test/test_host_resolver.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/controllable_http_response.h"
#include "net/test/embedded_test_server/http_request.h"
#include "net/test/embedded_test_server/http_response.h"
#include "services/network/public/cpp/features.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/common/fenced_frame/fenced_frame_utils.h"
namespace {
// The registerAdBeacon call in
// `chrome/test/data/interest_group/bidding_logic.js` will send
// "reserved.top_navigation" and "click" events to this URL.
constexpr char kReportingURL[] = "/_report_event_server.html";
// Used for event reporting to custom destination URLs.
constexpr char kCustomReportingURL[] = "/_custom_report_event_server.html";
// Used for reportWin() destination.
constexpr char kBidderReportURL[] = "/bidder_report";
// Used for reportResult() destination.
constexpr char kSellerReportURL[] = "/seller_report";
constexpr char kPrivateAggregationHostPipeResultHistogram[] =
"PrivacySandbox.PrivateAggregation.Host.PipeResult";
// Used to pattern match console error message emitted from
// `FencedFrameReporter::SendReportInternal()`. This error applies to
// `reportEvent()` and automatic beacons.
constexpr char kFencedFrameReportingDestinationNotAttested[] =
"The reporting destination * is not attested for *";
// Used to pattern match console error message emitted from
// `InterestGroupAuctionReporter::CheckReportUrl()`. This error applies to
// `reportWin()` and `reportResult()`.
constexpr char kInterestGroupReportingDestinationNotAttested[] =
"Worklet error: The reporting destination * is not attested for Protected "
"Audience.";
// The template to run `navigator.joinAdInterestGroup()`.
// $1: The ad render url.
// $2: The bidding logic url.
// $3: Name of the interest group.
// $4: Url that a custom macro reporting beacon is allowed to be sent to.
constexpr char kJoinAdInterestGroupScript[] = R"(
(async() => {
const page_origin = new URL($1).origin;
const bidding_url = new URL($2, page_origin);
const interest_group = {
name: $3,
owner: page_origin,
biddingLogicUrl: bidding_url,
ads: [{renderURL: $1, bid: 1, allowedReportingOrigins: [$4]}],
};
// Pick an arbitrarily high duration to guarantee that we never leave the
// ad interest group while the test runs. This join will fail silently
// because of attestations failure.
await navigator.joinAdInterestGroup(
interest_group, /*durationSeconds=*/3000000);
})()
)";
// The template to run `navigator.runAdAuction()`. Upon success, it also
// navigates the existing fenced frame with id "fenced_frame" in the page to the
// winning ad url.
// $1: The ad render url.
// $2: The decision logic url.
// $3: The url a reportResult beacon will be sent to if the decision logic
// is `decision_logic_report_to_seller_signals.js`.
constexpr char kRunAdAuctionAndNavigateFencedFrameScript[] = R"(
(async() => {
const page_origin = new URL($1).origin;
const auction_config = {
seller: page_origin,
interestGroupBuyers: [page_origin],
decisionLogicURL: new URL($2, page_origin),
sellerSignals: {reportTo: $3}
};
auction_config.resolveToConfig = true;
const fenced_frame_config = await navigator.runAdAuction(auction_config);
if (fenced_frame_config === null) {
return "null auction result";
} else if (!(fenced_frame_config instanceof FencedFrameConfig)) {
return "did not return a FencedFrameConfig";
} else {
document.getElementById("fenced_frame").config = fenced_frame_config;
return "success";
}
})()
)";
// Print more readable logs for PrivacySandboxSettingsEventReportingBrowserTest.
auto describe_params = [](const auto& info) {
return base::StrCat(
{"AttestedFor_", ConvertAttestedApiStatusToString(info.param)});
};
auto console_error_filter =
[](const content::WebContentsConsoleObserver::Message& message) {
return message.log_level == blink::mojom::ConsoleMessageLevel::kError;
};
} // namespace
class PrivacySandboxSettingsBrowserTest
: public MixinBasedInProcessBrowserTest {
public:
void SetUpOnMainThread() override {
host_resolver()->AddRule("*", "127.0.0.1");
https_server_.SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
https_server_.AddDefaultHandlers(GetChromeTestDataDir());
FinishSetUp();
}
// TODO(crbug.com/40285326): This fails with the field trial testing config.
void SetUpCommandLine(base::CommandLine* command_line) override {
MixinBasedInProcessBrowserTest::SetUpCommandLine(command_line);
command_line->AppendSwitch("disable-field-trial-config");
}
// Virtual so that derived classes can delay starting the server and/or
// register different handlers.
virtual void FinishSetUp() {
https_server_.RegisterRequestHandler(
base::BindRepeating(&PrivacySandboxSettingsBrowserTest::HandleRequest,
base::Unretained(this)));
content::SetupCrossSiteRedirector(&https_server_);
ASSERT_TRUE(https_server_.Start());
}
std::unique_ptr<net::test_server::HttpResponse> HandleRequest(
const net::test_server::HttpRequest& request) {
const GURL& url = request.GetURL();
if (url.path() == "/clear_site_data_header_cookies") {
auto response = std::make_unique<net::test_server::BasicHttpResponse>();
response->AddCustomHeader("Clear-Site-Data", "\"cookies\"");
response->set_code(net::HTTP_OK);
response->set_content_type("text/html");
response->set_content(std::string());
return std::move(response);
}
// Use the default handler for unrelated requests.
return nullptr;
}
void ClearAllCookies() {
content::BrowsingDataRemover* remover =
browser()->profile()->GetBrowsingDataRemover();
content::BrowsingDataRemoverCompletionObserver observer(remover);
remover->RemoveAndReply(
base::Time(), base::Time::Max(),
content::BrowsingDataRemover::DATA_TYPE_COOKIES,
content::BrowsingDataRemover::ORIGIN_TYPE_UNPROTECTED_WEB, &observer);
observer.BlockUntilCompletion();
}
privacy_sandbox::PrivacySandboxSettings* privacy_sandbox_settings() {
return PrivacySandboxSettingsFactory::GetForProfile(browser()->profile());
}
content::WebContents* web_contents() {
return browser()->tab_strip_model()->GetActiveWebContents();
}
protected:
net::EmbeddedTestServer https_server_{
net::test_server::EmbeddedTestServer::TYPE_HTTPS};
};
// Test that cookie clearings triggered by "Clear browsing data" will trigger
// an update to topics-data-accessible-since and invoke the corresponding
// observer method.
IN_PROC_BROWSER_TEST_F(PrivacySandboxSettingsBrowserTest, ClearAllCookies) {
EXPECT_EQ(base::Time(),
privacy_sandbox_settings()->TopicsDataAccessibleSince());
privacy_sandbox_test_util::MockPrivacySandboxObserver observer;
privacy_sandbox_settings()->AddObserver(&observer);
EXPECT_CALL(observer, OnTopicsDataAccessibleSinceUpdated());
ClearAllCookies();
EXPECT_NE(base::Time(),
privacy_sandbox_settings()->TopicsDataAccessibleSince());
}
// Test that cookie clearings triggered by Clear-Site-Data header won't trigger
// an update to topics-data-accessible-since or invoke the corresponding
// observer method.
IN_PROC_BROWSER_TEST_F(PrivacySandboxSettingsBrowserTest,
ClearSiteDataCookies) {
EXPECT_EQ(base::Time(),
privacy_sandbox_settings()->TopicsDataAccessibleSince());
privacy_sandbox_test_util::MockPrivacySandboxObserver observer;
privacy_sandbox_settings()->AddObserver(&observer);
EXPECT_CALL(observer, OnTopicsDataAccessibleSinceUpdated()).Times(0);
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(),
https_server_.GetURL("a.test", "/clear_site_data_header_cookies")));
EXPECT_EQ(base::Time(),
privacy_sandbox_settings()->TopicsDataAccessibleSince());
}
namespace {
enum class AttestedApiStatus {
kSharedStorage,
kProtectedAudience,
kProtectedAudienceAndPrivateAggregation,
kAttributionReporting,
kNone,
};
std::string ConvertAttestedApiStatusToString(
AttestedApiStatus attested_api_status) {
switch (attested_api_status) {
case AttestedApiStatus::kSharedStorage:
return "SharedStorage";
case AttestedApiStatus::kProtectedAudience:
return "ProtectedAudience";
case AttestedApiStatus::kProtectedAudienceAndPrivateAggregation:
return "ProtectedAudience_and_PrivateAggregation";
case AttestedApiStatus::kAttributionReporting:
return "AttributionReporting";
case AttestedApiStatus::kNone:
return "None";
default:
NOTREACHED();
}
}
} // namespace
class PrivacySandboxSettingsAttestationsBrowserTestBase
: public PrivacySandboxSettingsBrowserTest {
public:
PrivacySandboxSettingsAttestationsBrowserTestBase() = default;
content::test::FencedFrameTestHelper& fenced_frame_test_helper() {
return fenced_frame_test_helper_;
}
privacy_sandbox::PrivacySandboxAttestationsGatedAPISet
GetAttestationsGatedAPISet(AttestedApiStatus attested_api_status) {
switch (attested_api_status) {
case AttestedApiStatus::kSharedStorage:
return {privacy_sandbox::PrivacySandboxAttestationsGatedAPI::
kSharedStorage};
case AttestedApiStatus::kProtectedAudience:
return {privacy_sandbox::PrivacySandboxAttestationsGatedAPI::
kProtectedAudience};
case AttestedApiStatus::kProtectedAudienceAndPrivateAggregation:
return {privacy_sandbox::PrivacySandboxAttestationsGatedAPI::
kProtectedAudience,
privacy_sandbox::PrivacySandboxAttestationsGatedAPI::
kPrivateAggregation};
case AttestedApiStatus::kAttributionReporting:
return {privacy_sandbox::PrivacySandboxAttestationsGatedAPI::
kAttributionReporting};
case AttestedApiStatus::kNone:
return {};
default:
NOTREACHED();
}
}
void SetAttestations(std::vector<std::pair<std::string, AttestedApiStatus>>
hostname_strings_with_attestation_statuses) {
privacy_sandbox::PrivacySandboxAttestationsMap attestations_map;
for (const auto& hostname_and_status :
hostname_strings_with_attestation_statuses) {
attestations_map[net::SchemefulSite(
https_server_.GetOrigin(hostname_and_status.first))] =
GetAttestationsGatedAPISet(hostname_and_status.second);
}
privacy_sandbox::PrivacySandboxAttestations::GetInstance()
->SetAttestationsForTesting(std::move(attestations_map));
}
// Navigates the main frame, loads a fenced frame, then navigates the fenced
// frame by joining an ad interest group, running an ad auction, and setting
// the fenced frame's config to be the result of the auction.
// Note: The bidding and decision urls used in
// `NavigateFencedFrameUsingFledge()` are pointing to files under
// "content/test/data". This test file is in "browser/", so if the script is
// copied and directly executed here, the bidding and decision urls will be
// referring to files under "chrome/test/data".
content::RenderFrameHost* LoadPageThenLoadAndNavigateFencedFrameViaAdAuction(
const GURL& initial_url,
const GURL& fenced_frame_url) {
if (!ui_test_utils::NavigateToURL(browser(), initial_url)) {
return nullptr;
}
EXPECT_TRUE(
ExecJs(web_contents()->GetPrimaryMainFrame(),
"var fenced_frame = document.createElement('fencedframe');"
"fenced_frame.id = 'fenced_frame';"
"document.body.appendChild(fenced_frame);"));
auto* fenced_frame_rfh =
fenced_frame_test_helper().GetMostRecentlyAddedFencedFrame(
web_contents()->GetPrimaryMainFrame());
content::TestFrameNavigationObserver observer(fenced_frame_rfh);
fenced_frame_test_helper().NavigateFencedFrameUsingFledge(
web_contents()->GetPrimaryMainFrame(), fenced_frame_url,
"fenced_frame");
observer.Wait();
// Embedder-initiated fenced frame navigation uses a new browsing instance.
// Fenced frame RenderFrameHost is a new one after navigation, so we need
// to retrieve it.
return fenced_frame_test_helper().GetMostRecentlyAddedFencedFrame(
web_contents()->GetPrimaryMainFrame());
}
content::RenderFrameHost*
LoadPageThenLoadAndNavigateFencedFrameViaAdAuctionWithPrivateAggregation(
const std::string& primary_main_frame_hostname,
const std::string& fenced_frame_hostname) {
GURL initial_url(https_server_.GetURL(
primary_main_frame_hostname,
"/allow-all-join-ad-interest-group-run-ad-auction.html"));
GURL fenced_frame_url(https_server_.GetURL(
fenced_frame_hostname,
"/fenced_frames/"
"ad_with_fenced_frame_private_aggregation_reporting.html"));
return LoadPageThenLoadAndNavigateFencedFrameViaAdAuction(initial_url,
fenced_frame_url);
}
content::RenderFrameHost*
LoadPageThenLoadAndNavigateFencedFrameViaAdAuctionForEventReporting() {
GURL initial_url(https_server_.GetURL("a.test", "/empty.html"));
GURL fenced_frame_url(
https_server_.GetURL("a.test", "/fenced_frames/title1.html"));
return LoadPageThenLoadAndNavigateFencedFrameViaAdAuction(initial_url,
fenced_frame_url);
}
private:
privacy_sandbox::PrivacySandboxAttestationsMixin
privacy_sandbox_attestations_mixin_{&mixin_host_};
content::test::FencedFrameTestHelper fenced_frame_test_helper_;
};
class PrivacySandboxSettingsEventReportingBrowserTest
: public PrivacySandboxSettingsAttestationsBrowserTestBase,
public testing::WithParamInterface<AttestedApiStatus> {
public:
PrivacySandboxSettingsEventReportingBrowserTest() = default;
void FinishSetUp() override {
// Do not start the https server at this point to allow the tests to set up
// response listeners.
}
void SetUpOnMainThread() override {
// Allows all Privacy Sandbox prefs for testing.
privacy_sandbox_settings()->SetAllPrivacySandboxAllowedForTesting();
EXPECT_TRUE(
privacy_sandbox_settings()->IsAttributionReportingEverAllowed());
// Set up the observer to listen for console error messages.
PrivacySandboxSettingsAttestationsBrowserTestBase::SetUpOnMainThread();
console_error_observer_ =
std::make_unique<content::WebContentsConsoleObserver>(web_contents());
console_error_observer_->SetFilter(
base::BindRepeating(console_error_filter));
}
AttestedApiStatus GetReportingDestinationAttestationStatus() {
return GetParam();
}
bool IsReportingDestinationEnrolled() {
return GetReportingDestinationAttestationStatus() ==
AttestedApiStatus::kProtectedAudience;
}
protected:
std::unique_ptr<content::WebContentsConsoleObserver> console_error_observer_;
};
IN_PROC_BROWSER_TEST_P(PrivacySandboxSettingsEventReportingBrowserTest,
AutomaticBeaconDestinationEnrollment) {
// In order to check events reported over the network, we register an HTTP
// response interceptor for each reportEvent request we expect.
net::test_server::ControllableHttpResponse response(&https_server_,
kReportingURL);
ASSERT_TRUE(https_server_.Start());
// Set automatic beacon reporting destination to be attested according to the
// test parameter.
SetAttestations(
{std::make_pair("a.test", AttestedApiStatus::kProtectedAudience),
std::make_pair("d.test", GetReportingDestinationAttestationStatus())});
content::RenderFrameHost* fenced_frame_rfh =
LoadPageThenLoadAndNavigateFencedFrameViaAdAuctionForEventReporting();
ASSERT_NE(fenced_frame_rfh, nullptr);
// Set the automatic beacon.
constexpr char kBeaconMessage[] = "this is the message";
// Install the beacon observer to observe whether the beacon is queued to be
// sent later.
std::unique_ptr<content::test::FencedFrameReporterObserverForTesting>
beacon_observer = content::test::InstallFencedFrameReporterObserver(
fenced_frame_rfh,
content::AutomaticBeaconEvent(
blink::mojom::AutomaticBeaconType::kDeprecatedTopNavigation,
kBeaconMessage));
// Listen to the console error message from
// `FencedFrameReporter::SendReportInternal()`.
console_error_observer_->SetPattern(
kFencedFrameReportingDestinationNotAttested);
EXPECT_TRUE(ExecJs(
fenced_frame_rfh,
content::JsReplace(R"(
window.fence.setReportEventDataForAutomaticBeacons({
eventType: $1,
eventData: $2,
destination: ['buyer']
});
)",
blink::kDeprecatedFencedFrameTopNavigationBeaconType,
kBeaconMessage)));
// Commit a top-level navigation.
GURL navigation_url(https_server_.GetURL("a.test", "/title2.html"));
EXPECT_TRUE(
ExecJs(fenced_frame_rfh,
content::JsReplace("window.open($1, '_blank');", navigation_url)));
if (IsReportingDestinationEnrolled()) {
// The automatic beacon destination is considered enrolled if attested for
// Protected Audience.
response.WaitForRequest();
EXPECT_EQ(response.http_request()->content, kBeaconMessage);
} else {
// The console error is ignored if the reporting event is queued to be sent
// later when fenced frame url mapping is ready.
if (!beacon_observer->IsReportingEventQueued()) {
// The console message should state different attestation requirement
// based on the feature toggle.
ASSERT_TRUE(console_error_observer_->Wait());
EXPECT_EQ(console_error_observer_->messages().size(), 1u);
EXPECT_TRUE(base::Contains(console_error_observer_->GetMessageAt(0u),
"Protected Audience"));
}
// Verify the automatic beacon was not sent.
fenced_frame_test_helper().SendBasicRequest(
web_contents(), https_server_.GetURL("d.test", kReportingURL),
"response");
response.WaitForRequest();
EXPECT_EQ(response.http_request()->content, "response");
}
}
IN_PROC_BROWSER_TEST_P(PrivacySandboxSettingsEventReportingBrowserTest,
ReportEventDestinationEnrollment) {
// In order to check events reported over the network, we register an HTTP
// response interceptor for each reportEvent request we expect.
net::test_server::ControllableHttpResponse response(&https_server_,
kReportingURL);
ASSERT_TRUE(https_server_.Start());
// Set reportEvent reporting destination to be attested according to the
// test parameter.
SetAttestations(
{std::make_pair("a.test", AttestedApiStatus::kProtectedAudience),
std::make_pair("d.test", GetReportingDestinationAttestationStatus())});
content::RenderFrameHost* fenced_frame_rfh =
LoadPageThenLoadAndNavigateFencedFrameViaAdAuctionForEventReporting();
ASSERT_NE(fenced_frame_rfh, nullptr);
// Send the report to an enum destination.
constexpr char kBeaconMessage[] = "this is the message";
constexpr char kEventType[] = "click";
// Install the beacon observer to observe whether the beacon is queued to be
// sent later.
std::unique_ptr<content::test::FencedFrameReporterObserverForTesting>
beacon_observer = content::test::InstallFencedFrameReporterObserver(
fenced_frame_rfh,
content::DestinationEnumEvent(kEventType, kBeaconMessage));
// Listen to the console error message from
// `FencedFrameReporter::SendReportInternal()`.
console_error_observer_->SetPattern(
kFencedFrameReportingDestinationNotAttested);
EXPECT_TRUE(
ExecJs(fenced_frame_rfh, content::JsReplace(R"(
window.fence.reportEvent({
eventType: $1,
eventData: $2,
destination: ['buyer']
});
)",
kEventType, kBeaconMessage)));
if (IsReportingDestinationEnrolled()) {
// The reportEvent beacon destination is considered enrolled if attested for
// Protected Audience.
response.WaitForRequest();
EXPECT_EQ(response.http_request()->content, kBeaconMessage);
} else {
// The console error is ignored if the reporting event is queued to be sent
// later when fenced frame url mapping is ready.
if (!beacon_observer->IsReportingEventQueued()) {
// The console message should state different attestation requirement
// based on the feature toggle.
ASSERT_TRUE(console_error_observer_->Wait());
EXPECT_EQ(console_error_observer_->messages().size(), 1u);
EXPECT_TRUE(base::Contains(console_error_observer_->GetMessageAt(0u),
"Protected Audience"));
}
// Verify the reportEvent beacon was not sent.
fenced_frame_test_helper().SendBasicRequest(
web_contents(), https_server_.GetURL("d.test", kReportingURL),
"response");
response.WaitForRequest();
EXPECT_EQ(response.http_request()->content, "response");
}
}
IN_PROC_BROWSER_TEST_P(PrivacySandboxSettingsEventReportingBrowserTest,
ReportEventCustomURLDestinationEnrollment) {
// In order to check events reported over the network, we register an HTTP
// response interceptor for each reportEvent request we expect.
net::test_server::ControllableHttpResponse response(&https_server_,
kCustomReportingURL);
ASSERT_TRUE(https_server_.Start());
// Set custom url reporting destination to be attested according to the
// test parameter.
SetAttestations(
{std::make_pair("a.test", AttestedApiStatus::kProtectedAudience),
std::make_pair("d.test", GetReportingDestinationAttestationStatus())});
GURL initial_url(https_server_.GetURL("a.test", "/empty.html"));
GURL fenced_frame_url(
https_server_.GetURL("a.test", "/fenced_frames/title1.html"));
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), initial_url));
// Create the fenced frame.
EXPECT_TRUE(ExecJs(web_contents()->GetPrimaryMainFrame(),
"var fenced_frame = document.createElement('fencedframe');"
"fenced_frame.id = 'fenced_frame';"
"document.body.appendChild(fenced_frame);"));
content::RenderFrameHost* fenced_frame_rfh =
fenced_frame_test_helper().GetMostRecentlyAddedFencedFrame(
web_contents()->GetPrimaryMainFrame());
content::TestFrameNavigationObserver observer(fenced_frame_rfh);
// Send the report to a custom URL destination.
GURL destination_url = https_server_.GetURL("d.test", kCustomReportingURL);
EXPECT_TRUE(
ExecJs(web_contents()->GetPrimaryMainFrame(),
content::JsReplace(kJoinAdInterestGroupScript, fenced_frame_url,
"/interest_group/bidding_logic.js", "testAd",
destination_url)));
content::EvalJsResult auction_result =
EvalJs(web_contents()->GetPrimaryMainFrame(),
content::JsReplace(kRunAdAuctionAndNavigateFencedFrameScript,
fenced_frame_url,
"/interest_group/decision_logic.js", ""));
if (IsReportingDestinationEnrolled()) {
// The custom url destination is considered enrolled if attested for
// Protected Audience.
ASSERT_EQ(auction_result.ExtractString(), "success");
observer.Wait();
ASSERT_EQ(observer.last_committed_url(), fenced_frame_url);
// Embedder-initiated fenced frame navigation uses a new browsing instance.
// Fenced frame RenderFrameHost is a new one after navigation, so we need
// to retrieve it.
fenced_frame_rfh =
fenced_frame_test_helper().GetMostRecentlyAddedFencedFrame(
web_contents()->GetPrimaryMainFrame());
ASSERT_NE(fenced_frame_rfh, nullptr);
// Send the beacon.
EXPECT_TRUE(ExecJs(fenced_frame_rfh, content::JsReplace(R"(
window.fence.reportEvent({destinationURL: $1});
)",
destination_url)));
// Verify the beacon was sent as a GET request.
response.WaitForRequest();
EXPECT_EQ(response.http_request()->method, net::test_server::METHOD_GET);
} else {
// Joining ad interest group an url that is not enrolled in
// `allowedReportingOrigins` will fail silently. The auction later will not
// return a winning ad.
// TODO(xiaochenzh): The current behavior for `joinAdInterestGroup()` when
// urls in `allowedReportingOrigins` are not attested is to fail silently.
// Soon an error message will be added. This test should be updated to
// listen to this error then.
ASSERT_EQ(auction_result, "null auction result");
// Verify the beacon was not sent.
fenced_frame_test_helper().SendBasicRequest(
web_contents(), https_server_.GetURL("d.test", kCustomReportingURL),
"response");
response.WaitForRequest();
EXPECT_EQ(response.http_request()->content, "response");
}
}
// For beacons from `reportWin()`, the reporting destination is considered
// enrolled if and only if it is attested for Protected Audience. The relaxed
// attestations requirement of either Protected Audience or Attribution
// Reporting for post-impression beacons from M120 should not apply to beacons
// from `reportWin()`.
IN_PROC_BROWSER_TEST_P(PrivacySandboxSettingsEventReportingBrowserTest,
ReportWinDestinationEnrollment) {
// In order to check events reported over the network, we register an HTTP
// response interceptor for each reporting beacon we expect.
net::test_server::ControllableHttpResponse response(&https_server_,
kBidderReportURL);
ASSERT_TRUE(https_server_.Start());
// Set `reportWin()` reporting destination to be attested according to the
// test parameter.
SetAttestations(
{std::make_pair("a.test", AttestedApiStatus::kProtectedAudience),
std::make_pair("b.test", GetReportingDestinationAttestationStatus())});
GURL initial_url(https_server_.GetURL("a.test", "/empty.html"));
GURL fenced_frame_url(
https_server_.GetURL("a.test", "/fenced_frames/title1.html"));
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), initial_url));
// Create the fenced frame.
EXPECT_TRUE(ExecJs(web_contents()->GetPrimaryMainFrame(),
"var fenced_frame = document.createElement('fencedframe');"
"fenced_frame.id = 'fenced_frame';"
"document.body.appendChild(fenced_frame);"));
content::RenderFrameHost* fenced_frame_rfh =
fenced_frame_test_helper().GetMostRecentlyAddedFencedFrame(
web_contents()->GetPrimaryMainFrame());
content::TestFrameNavigationObserver observer(fenced_frame_rfh);
// The `reportWin()` beacon will be attempted to send to this url.
GURL bidder_report_to_url = https_server_.GetURL("b.test", kBidderReportURL);
// Listen to the console error message from
// `InterestGroupAuctionReporter::CheckReportUrl()`.
console_error_observer_->SetPattern(
kInterestGroupReportingDestinationNotAttested);
// Run the ad auction with the bidding url `bidding_logic_report_to_name.js`.
// This auction will attempt to send a `reportWin()` to the url in the
// InterestGroup name.
EXPECT_TRUE(ExecJs(
web_contents()->GetPrimaryMainFrame(),
content::JsReplace(kJoinAdInterestGroupScript, fenced_frame_url,
"/interest_group/bidding_logic_report_to_name.js",
bidder_report_to_url, fenced_frame_url)));
content::EvalJsResult auction_result =
EvalJs(web_contents()->GetPrimaryMainFrame(),
content::JsReplace(kRunAdAuctionAndNavigateFencedFrameScript,
fenced_frame_url,
"/interest_group/decision_logic.js", ""));
if (IsReportingDestinationEnrolled()) {
// For beacons from `reportWin()`, the reporting destination is considered
// enrolled if and only if it is attested for Protected Audience.
ASSERT_EQ(auction_result.ExtractString(), "success");
observer.Wait();
ASSERT_EQ(observer.last_committed_url(), fenced_frame_url);
// Verify the `reportWin()` beacon was sent.
response.WaitForRequest();
EXPECT_FALSE(response.http_request()->has_content);
EXPECT_EQ(response.http_request()->method,
net::test_server::HttpMethod::METHOD_GET);
} else {
// Verify the console messages states to require Protected Audience only.
// Note that two console messages are sent due to debug and normal
// reporting.
ASSERT_TRUE(console_error_observer_->Wait());
EXPECT_EQ(console_error_observer_->messages().size(), 2u);
EXPECT_TRUE(base::Contains(console_error_observer_->GetMessageAt(0u),
"Protected Audience"));
EXPECT_FALSE(base::Contains(console_error_observer_->GetMessageAt(0u),
"Attribution Reporting"));
EXPECT_TRUE(base::Contains(console_error_observer_->GetMessageAt(1u),
"Protected Audience"));
EXPECT_FALSE(base::Contains(console_error_observer_->GetMessageAt(1u),
"Attribution Reporting"));
// Verify the `reportWin()` beacon was not sent.
fenced_frame_test_helper().SendBasicRequest(
web_contents(), bidder_report_to_url, "response");
response.WaitForRequest();
EXPECT_EQ(response.http_request()->content, "response");
}
}
// For beacons from `reportResult()`, the reporting destination is considered
// enrolled if and only if it is attested for Protected Audience. The relaxed
// attestations requirement of either Protected Audience or Attribution
// Reporting for post-impression beacons from M120 should not apply to beacons
// from `reportResult()`.
IN_PROC_BROWSER_TEST_P(PrivacySandboxSettingsEventReportingBrowserTest,
ReportResultDestinationEnrollment) {
// In order to check events reported over the network, we register an HTTP
// response interceptor for each reporting beacon we expect.
net::test_server::ControllableHttpResponse response(&https_server_,
kSellerReportURL);
ASSERT_TRUE(https_server_.Start());
// Set `reportResult()` reporting destination to be attested according to the
// test parameter.
SetAttestations(
{std::make_pair("a.test", AttestedApiStatus::kProtectedAudience),
std::make_pair("b.test", GetReportingDestinationAttestationStatus())});
GURL initial_url(https_server_.GetURL("a.test", "/empty.html"));
GURL fenced_frame_url(
https_server_.GetURL("a.test", "/fenced_frames/title1.html"));
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), initial_url));
// Create the fenced frame.
EXPECT_TRUE(ExecJs(web_contents()->GetPrimaryMainFrame(),
"var fenced_frame = document.createElement('fencedframe');"
"fenced_frame.id = 'fenced_frame';"
"document.body.appendChild(fenced_frame);"));
content::RenderFrameHost* fenced_frame_rfh =
fenced_frame_test_helper().GetMostRecentlyAddedFencedFrame(
web_contents()->GetPrimaryMainFrame());
content::TestFrameNavigationObserver observer(fenced_frame_rfh);
// The `ReportResult()` beacon will be attempted to send to this url.
GURL seller_report_to_url = https_server_.GetURL("b.test", kSellerReportURL);
// Listen to the console error message from
// `InterestGroupAuctionReporter::CheckReportUrl()`.
console_error_observer_->SetPattern(
kInterestGroupReportingDestinationNotAttested);
// Run the ad auction with the decision url
// `decision_logic_report_to_seller_signals.js`. This auction will attempt to
// send a `ReportResult()` beacon to the url in the sellerSignals.
EXPECT_TRUE(
ExecJs(web_contents()->GetPrimaryMainFrame(),
content::JsReplace(kJoinAdInterestGroupScript, fenced_frame_url,
"/interest_group/bidding_logic.js", "testAd",
fenced_frame_url)));
content::EvalJsResult auction_result =
EvalJs(web_contents()->GetPrimaryMainFrame(),
content::JsReplace(
kRunAdAuctionAndNavigateFencedFrameScript, fenced_frame_url,
"/interest_group/decision_logic_report_to_seller_signals.js",
seller_report_to_url));
if (IsReportingDestinationEnrolled()) {
// For beacons from `reportResult()`, the reporting destination is
// considered enrolled if and only if it is attested for Protected Audience.
ASSERT_EQ(auction_result.ExtractString(), "success");
observer.Wait();
ASSERT_EQ(observer.last_committed_url(), fenced_frame_url);
// Verify the `reportResult()` beacon was sent.
response.WaitForRequest();
EXPECT_FALSE(response.http_request()->has_content);
EXPECT_EQ(response.http_request()->method,
net::test_server::HttpMethod::METHOD_GET);
} else {
// Verify the console message states to require Protected Audience only.
// Note that two console messages are sent due to debug and normal
// reporting.
ASSERT_TRUE(console_error_observer_->Wait());
EXPECT_EQ(console_error_observer_->messages().size(), 2u);
EXPECT_TRUE(base::Contains(console_error_observer_->GetMessageAt(0u),
"Protected Audience"));
EXPECT_FALSE(base::Contains(console_error_observer_->GetMessageAt(0u),
"Attribution Reporting"));
EXPECT_TRUE(base::Contains(console_error_observer_->GetMessageAt(1u),
"Protected Audience"));
EXPECT_FALSE(base::Contains(console_error_observer_->GetMessageAt(1u),
"Attribution Reporting"));
// Verify the `reportResult()` beacon was not sent.
fenced_frame_test_helper().SendBasicRequest(
web_contents(), seller_report_to_url, "response");
response.WaitForRequest();
EXPECT_EQ(response.http_request()->content, "response");
}
}
INSTANTIATE_TEST_SUITE_P(
PrivacySandboxSettingsEventReportingBrowserTest,
PrivacySandboxSettingsEventReportingBrowserTest,
testing::Values(AttestedApiStatus::kProtectedAudience,
AttestedApiStatus::kAttributionReporting,
AttestedApiStatus::kNone),
describe_params);
class PrivacySandboxSettingsAttestProtectedAudienceBrowserTest
: public PrivacySandboxSettingsAttestationsBrowserTestBase {
public:
PrivacySandboxSettingsAttestProtectedAudienceBrowserTest() {
feature_list_.InitWithFeatures(
/*enabled_features=*/
{network::features::kInterestGroupStorage,
blink::features::kAdInterestGroupAPI, blink::features::kFledge,
blink::features::kFledgeBiddingAndAuctionServer,
blink::features::kFencedFrames,
blink::features::kFencedFramesAPIChanges,
privacy_sandbox::kOverridePrivacySandboxSettingsLocalTesting},
/*disabled_features=*/{});
}
void FinishSetUp() override {
https_server_.RegisterRequestHandler(base::BindRepeating(
&PrivacySandboxSettingsAttestProtectedAudienceBrowserTest::
HandleWellKnownRequest,
base::Unretained(this)));
content::SetupCrossSiteRedirector(&https_server_);
ASSERT_TRUE(https_server_.Start());
}
std::unique_ptr<net::test_server::HttpResponse> HandleWellKnownRequest(
const net::test_server::HttpRequest& request) {
if (!base::StartsWith(request.relative_url,
"/.well-known/interest-group/permissions/?origin=")) {
return nullptr;
}
// .well-known requests should advertise they accept JSON responses.
const auto accept_header =
request.headers.find(net::HttpRequestHeaders::kAccept);
CHECK(accept_header != request.headers.end());
EXPECT_EQ(accept_header->second, "application/json");
auto response = std::make_unique<net::test_server::BasicHttpResponse>();
response->set_content_type("application/json");
response->set_content(R"({"joinAdInterestGroup" : true})");
response->AddCustomHeader("Access-Control-Allow-Origin", "*");
return response;
}
private:
base::test::ScopedFeatureList feature_list_;
};
class
PrivacySandboxSettingsAttestPrivateAggregationInProtectedAudienceBrowserTest
: public PrivacySandboxSettingsAttestProtectedAudienceBrowserTest {
public:
PrivacySandboxSettingsAttestPrivateAggregationInProtectedAudienceBrowserTest() {
feature_list_.InitWithFeatures(
/*enabled_features=*/
{blink::features::kPrivateAggregationApi},
/*disabled_features=*/{});
}
size_t GetTotalSampleCount(std::string_view histogram_name) {
auto buckets = histogram_tester_.GetAllSamples(histogram_name);
size_t count = 0;
for (const auto& bucket : buckets) {
count += bucket.count;
}
return count;
}
void WaitForHistogram(const std::string& histogram_name,
size_t expected_sample_count) {
// Continue if histogram was already recorded and has at least the expected
// number of samples.
if (base::StatisticsRecorder::FindHistogram(histogram_name) &&
GetTotalSampleCount(histogram_name) >= expected_sample_count) {
return;
}
// Else, wait until the histogram is recorded with enough samples.
base::RunLoop run_loop;
auto histogram_observer = std::make_unique<
base::StatisticsRecorder::ScopedHistogramSampleObserver>(
histogram_name,
base::BindLambdaForTesting([&](std::string_view histogram_name,
uint64_t name_hash,
base::HistogramBase::Sample32 sample) {
if (GetTotalSampleCount(histogram_name) >= expected_sample_count) {
run_loop.Quit();
}
}));
run_loop.Run();
}
protected:
base::HistogramTester histogram_tester_;
private:
base::test::ScopedFeatureList feature_list_;
};
IN_PROC_BROWSER_TEST_F(
PrivacySandboxSettingsAttestPrivateAggregationInProtectedAudienceBrowserTest,
SameOrigin_Enrolled_Success) {
SetAttestations({std::make_pair(
"a.test", AttestedApiStatus::kProtectedAudienceAndPrivateAggregation)});
content::RenderFrameHost* fenced_frame_rfh =
LoadPageThenLoadAndNavigateFencedFrameViaAdAuctionWithPrivateAggregation(
/*primary_main_frame_hostname=*/"a.test",
/*fenced_frame_hostname=*/"a.test");
ASSERT_NE(fenced_frame_rfh, nullptr);
WaitForHistogram(kPrivateAggregationHostPipeResultHistogram, 2);
histogram_tester_.ExpectUniqueSample(
kPrivateAggregationHostPipeResultHistogram,
content::GetPrivateAggregationHostPipeReportSuccessValue(), 2);
}
IN_PROC_BROWSER_TEST_F(
PrivacySandboxSettingsAttestPrivateAggregationInProtectedAudienceBrowserTest,
SameOrigin_NotEnrolled_Failure) {
SetAttestations(
{std::make_pair("a.test", AttestedApiStatus::kProtectedAudience)});
content::RenderFrameHost* fenced_frame_rfh =
LoadPageThenLoadAndNavigateFencedFrameViaAdAuctionWithPrivateAggregation(
/*primary_main_frame_hostname=*/"a.test",
/*fenced_frame_hostname=*/"a.test");
ASSERT_NE(fenced_frame_rfh, nullptr);
WaitForHistogram(kPrivateAggregationHostPipeResultHistogram, 2);
histogram_tester_.ExpectUniqueSample(
kPrivateAggregationHostPipeResultHistogram,
content::GetPrivateAggregationHostPipeApiDisabledValue(), 2);
}
// Verifies that joining interest groups and running auctions in the Protected
// Audience API are subject to attestation checks.
//
// navigtor.joinAdInterestGroup() doesn't have a separate attestation from
// navigator.runAdAuction() -- they both check the same kProtectedAudience
// attestation.
IN_PROC_BROWSER_TEST_F(PrivacySandboxSettingsAttestProtectedAudienceBrowserTest,
Join_RunAdAuction_Enrollment) {
struct TestCase {
AttestedApiStatus join_origin_attestation;
AttestedApiStatus run_origin_attestation;
bool expect_auction_succeeds;
} kTestCases[] = {
{/*join_origin_attestation=*/AttestedApiStatus::kProtectedAudience,
/*run_origin_attestation=*/AttestedApiStatus::kProtectedAudience,
/*expect_auction_succeeds=*/true},
{/*join_origin_attestation=*/AttestedApiStatus::kSharedStorage,
AttestedApiStatus::kProtectedAudience,
/*expect_auction_succeeds=*/false},
{/*join_origin_attestation=*/AttestedApiStatus::kProtectedAudience,
/*run_origin_attestation=*/AttestedApiStatus::kSharedStorage,
/*expect_auction_succeeds=*/false},
{/*join_origin_attestation=*/AttestedApiStatus::kSharedStorage,
/*run_origin_attestation=*/AttestedApiStatus::kSharedStorage,
/*expect_auction_succeeds=*/false},
};
for (const auto test_case : kTestCases) {
SCOPED_TRACE(::testing::Message()
<< "Join origin attestation "
<< static_cast<int>(test_case.join_origin_attestation)
<< " run origin attestation "
<< static_cast<int>(test_case.run_origin_attestation));
SetAttestations(
{std::make_pair("a.test", test_case.join_origin_attestation),
std::make_pair("b.test", test_case.run_origin_attestation)});
const GURL join_page = https_server_.GetURL("a.test", "/echo");
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), join_page));
EXPECT_TRUE(ExecJs(web_contents()->GetPrimaryMainFrame(),
content::JsReplace(R"(
(async() => {
const FLEDGE_BIDDING_URL = "/interest_group/bidding_logic.js";
const page_origin = new URL($1).origin;
const bidding_url = new URL(FLEDGE_BIDDING_URL, page_origin);
const interest_group = {
name: 'testAd1',
owner: page_origin,
biddingLogicUrl: bidding_url,
ads: [{renderURL: $1, bid: 1}],
};
// Pick an arbitrarily high duration to guarantee that we never leave
// the ad interest group while the test runs.
await navigator.joinAdInterestGroup(
interest_group, /*durationSeconds=*/3000000);
})())",
join_page)));
const GURL auction_page = https_server_.GetURL("b.test", "/echo");
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), auction_page));
content::EvalJsResult result =
EvalJs(web_contents()->GetPrimaryMainFrame(),
content::JsReplace(R"(
(async() => {
const FLEDGE_DECISION_URL = "/interest_group/decision_logic.js";
const page_origin = new URL($1).origin;
const join_origin = new URL($2).origin;
const auction_config = {
seller: page_origin,
interestGroupBuyers: [join_origin],
decisionLogicURL: new URL(FLEDGE_DECISION_URL, page_origin),
};
return await navigator.runAdAuction(auction_config);
})())",
auction_page, join_page));
if (test_case.expect_auction_succeeds) {
EXPECT_NE(base::Value(), result);
} else {
EXPECT_EQ(base::Value(), result);
}
}
}
IN_PROC_BROWSER_TEST_F(
PrivacySandboxSettingsAttestPrivateAggregationInProtectedAudienceBrowserTest,
CrossOrigin_Enrolled_Success) {
SetAttestations(
{std::make_pair(
"a.test",
AttestedApiStatus::kProtectedAudienceAndPrivateAggregation),
std::make_pair(
"b.test",
AttestedApiStatus::kProtectedAudienceAndPrivateAggregation)});
content::RenderFrameHost* fenced_frame_rfh =
LoadPageThenLoadAndNavigateFencedFrameViaAdAuctionWithPrivateAggregation(
/*primary_main_frame_hostname=*/"a.test",
/*fenced_frame_hostname=*/"b.test");
ASSERT_NE(fenced_frame_rfh, nullptr);
WaitForHistogram(kPrivateAggregationHostPipeResultHistogram, 2);
histogram_tester_.ExpectUniqueSample(
kPrivateAggregationHostPipeResultHistogram,
content::GetPrivateAggregationHostPipeReportSuccessValue(), 2);
}
IN_PROC_BROWSER_TEST_F(
PrivacySandboxSettingsAttestPrivateAggregationInProtectedAudienceBrowserTest,
CrossOrigin_NotEnrolled_Failure) {
SetAttestations(
{std::make_pair(
"a.test",
AttestedApiStatus::kProtectedAudienceAndPrivateAggregation),
std::make_pair("b.test", AttestedApiStatus::kProtectedAudience)});
content::RenderFrameHost* fenced_frame_rfh =
LoadPageThenLoadAndNavigateFencedFrameViaAdAuctionWithPrivateAggregation(
/*primary_main_frame_hostname=*/"a.test",
/*fenced_frame_hostname=*/"b.test");
ASSERT_NE(fenced_frame_rfh, nullptr);
WaitForHistogram(kPrivateAggregationHostPipeResultHistogram, 2);
histogram_tester_.ExpectUniqueSample(
kPrivateAggregationHostPipeResultHistogram,
content::GetPrivateAggregationHostPipeApiDisabledValue(), 2);
}