blob: cf642048a9cf25aadab272db045857783349a076 [file] [log] [blame]
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <vector>
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
#include "content/browser/browsing_topics/test_util.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_observer.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_content_browser_client.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/public/test/test_navigation_observer.h"
#include "content/public/test/url_loader_interceptor.h"
#include "content/shell/browser/shell.h"
#include "content/test/content_browser_test_utils_internal.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/default_handlers.h"
#include "net/test/embedded_test_server/request_handler_util.h"
#include "services/network/public/cpp/features.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/mojom/browsing_topics/browsing_topics.mojom.h"
#include "url/gurl.h"
namespace content {
namespace {
class FixedTopicsContentBrowserClient
: public ContentBrowserTestContentBrowserClient {
public:
bool HandleTopicsWebApi(
const url::Origin& context_origin,
content::RenderFrameHost* main_frame,
browsing_topics::ApiCallerSource caller_source,
bool get_topics,
bool observe,
std::vector<blink::mojom::EpochTopicPtr>& topics) override {
blink::mojom::EpochTopicPtr result_topic = blink::mojom::EpochTopic::New();
result_topic->topic = 1;
result_topic->config_version = "chrome.1";
result_topic->taxonomy_version = "1";
result_topic->model_version = "2";
result_topic->version = "chrome.1:1:2";
topics.push_back(std::move(result_topic));
return true;
}
int NumVersionsInTopicsEpochs(
content::RenderFrameHost* main_frame) const override {
return 1;
}
StoragePartitionConfig GetStoragePartitionConfigForSite(
BrowserContext* browser_context,
const GURL& site) override {
// Use a different StoragePartition for URLs from b.test (to test the fix
// for crbug.com/40855090). Note that the port should be removed from site
// to simplify the comparison, because non-default ports are included in
// site URLs when kOriginKeyedProcessesByDefault is enabled.
GURL::Replacements replacements;
replacements.ClearPort();
if (site.ReplaceComponents(replacements) == GURL("https://b.test/")) {
return StoragePartitionConfig::Create(browser_context,
/*partition_domain=*/"b.test",
/*partition_name=*/"test_partition",
/*in_memory=*/false);
}
return StoragePartitionConfig::CreateDefault(browser_context);
}
};
} // namespace
class BrowsingTopicsBrowserTest : public ContentBrowserTest {
public:
BrowsingTopicsBrowserTest() {
feature_list_.InitWithFeatures({features::kPrivacySandboxAdsAPIsOverride,
network::features::kBrowsingTopics},
/*disabled_features=*/{});
}
void SetUpOnMainThread() override {
ContentBrowserTest::SetUpOnMainThread();
host_resolver()->AddRule("*", "127.0.0.1");
https_server_.SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
net::test_server::RegisterDefaultHandlers(&https_server_);
https_server_.ServeFilesFromSourceDirectory("content/test/data");
content::SetupCrossSiteRedirector(&https_server_);
ASSERT_TRUE(https_server_.Start());
browser_client_ = std::make_unique<FixedTopicsContentBrowserClient>();
url_loader_monitor_ =
std::make_unique<URLLoaderInterceptor>(base::BindLambdaForTesting(
[&](URLLoaderInterceptor::RequestParams* params) -> bool {
last_request_is_topics_request_ =
params->url_request.browsing_topics;
last_topics_header_ =
params->url_request.headers.GetHeader("Sec-Browsing-Topics");
return false;
}));
}
void TearDownOnMainThread() override {
browser_client_.reset();
url_loader_monitor_.reset();
}
WebContents* web_contents() { return shell()->web_contents(); }
bool last_request_is_topics_request() const {
return last_request_is_topics_request_;
}
const std::optional<std::string>& last_topics_header() const {
return last_topics_header_;
}
std::string InvokeTopicsAPI(const ToRenderFrameHost& adapter) {
return EvalJs(adapter, R"(
if (!(document.browsingTopics instanceof Function)) {
'not a function';
} else {
document.browsingTopics()
.then(topics => {
let result = "[";
for (const topic of topics) {
result += JSON.stringify(topic, Object.keys(topic).sort()) + ";"
}
result += "]";
return result;
})
.catch(error => error.message);
}
)")
.ExtractString();
;
}
protected:
base::test::ScopedFeatureList feature_list_;
net::EmbeddedTestServer https_server_{
net::test_server::EmbeddedTestServer::TYPE_HTTPS};
std::unique_ptr<FixedTopicsContentBrowserClient> browser_client_;
bool last_request_is_topics_request_ = false;
std::unique_ptr<base::RunLoop> resource_request_url_waiter_;
std::optional<std::string> last_topics_header_;
std::unique_ptr<URLLoaderInterceptor> url_loader_monitor_;
};
IN_PROC_BROWSER_TEST_F(BrowsingTopicsBrowserTest, TopicsAPI) {
// a.test will end up on the default storage partition.
GURL main_frame_url = https_server_.GetURL("a.test", "/hello.html");
EXPECT_TRUE(NavigateToURL(shell(), main_frame_url));
EXPECT_EQ(
"[{\"configVersion\":\"chrome.1\",\"modelVersion\":\"2\","
"\"taxonomyVersion\":\"1\",\"topic\":1,\"version\":\"chrome.1:1:2\"};]",
InvokeTopicsAPI(web_contents()));
}
IN_PROC_BROWSER_TEST_F(
BrowsingTopicsBrowserTest,
TopicsAPI_InvokedFromFrameWithNonDefaultStoragePartition) {
// b.test will end up on a non-default storage partition.
GURL main_frame_url = https_server_.GetURL("b.test", "/hello.html");
EXPECT_TRUE(NavigateToURL(shell(), main_frame_url));
EXPECT_EQ("[]", InvokeTopicsAPI(web_contents()));
}
// TODO(crbug.com/40245082): migrate to WPT.
IN_PROC_BROWSER_TEST_F(BrowsingTopicsBrowserTest,
Fetch_TopicsHeaderNotVisibleInServiceWorker) {
GURL main_frame_url = https_server_.GetURL(
"a.test", "/browsing_topics/service_worker_factory.html");
GURL worker_script_url = https_server_.GetURL(
"a.test", "/browsing_topics/topics_service_worker.js");
GURL fetch_url = https_server_.GetURL("a.test", "/empty.html");
EXPECT_TRUE(NavigateToURL(shell(), main_frame_url));
EXPECT_EQ("ok",
EvalJs(shell()->web_contents(),
JsReplace("setupServiceWorker($1)", worker_script_url)));
// Reload the page to let it be controlled by the service worker.
EXPECT_TRUE(NavigateToURL(shell(), main_frame_url));
// Initiate a topics fetch() request from the Window context. Verify that the
// topics header is not visible in the service worker during the interception.
EXPECT_EQ("null", EvalJs(shell()->web_contents(), content::JsReplace(
R"(
new Promise((resolve, reject) => {
navigator.serviceWorker.addEventListener('message', e => {
if (e.data.url == $1) {
resolve(e.data.topicsHeader);
}
});
fetch($1, {browsingTopics: true});
});
)",
fetch_url)));
}
IN_PROC_BROWSER_TEST_F(BrowsingTopicsBrowserTest, TopicsHeaderForWindowFetch) {
GURL main_frame_url = https_server_.GetURL(
"a.test", "/browsing_topics/service_worker_factory.html");
GURL fetch_url = https_server_.GetURL("a.test", "/empty.html");
EXPECT_TRUE(NavigateToURL(shell(), main_frame_url));
EXPECT_TRUE(ExecJs(
shell()->web_contents(),
content::JsReplace("fetch($1, {browsingTopics: true})", fetch_url)));
EXPECT_TRUE(last_request_is_topics_request());
EXPECT_TRUE(last_topics_header());
EXPECT_EQ(last_topics_header().value(),
"(1);v=chrome.1:1:2, ();p=P00000000000");
}
IN_PROC_BROWSER_TEST_F(BrowsingTopicsBrowserTest,
TopicsNotAllowedForServiceWorkerFetch) {
GURL main_frame_url = https_server_.GetURL(
"a.test", "/browsing_topics/service_worker_factory.html");
GURL worker_script_url = https_server_.GetURL(
"a.test", "/browsing_topics/topics_service_worker.js");
GURL fetch_url = https_server_.GetURL("a.test", "/empty.html");
EXPECT_TRUE(NavigateToURL(shell(), main_frame_url));
EXPECT_EQ("ok",
EvalJs(shell()->web_contents(),
JsReplace("setupServiceWorker($1)", worker_script_url)));
// Reload the page to let it be controlled by the service worker.
EXPECT_TRUE(NavigateToURL(shell(), main_frame_url));
// Initiate a topics fetch request from the service worker. Verify that it
// doesn't contain the topics header.
EXPECT_TRUE(ExecJs(shell()->web_contents(), content::JsReplace(
R"(
new Promise((resolve, reject) => {
navigator.serviceWorker.addEventListener('message', e => {
if (e.data.finishedFetch) {
resolve();
}
});
navigator.serviceWorker.controller.postMessage({
fetchUrl: $1
});
});
)",
fetch_url)));
EXPECT_FALSE(last_request_is_topics_request());
EXPECT_FALSE(last_topics_header());
}
IN_PROC_BROWSER_TEST_F(BrowsingTopicsBrowserTest,
EmbedderOptInStatus_StaticIframe_NoBrowsingTopicsAttr) {
base::StringPairs topics_attribute_replacement;
topics_attribute_replacement.emplace_back(
"{{MAYBE_BROWSING_TOPICS_ATTRIBUTE}}", "");
GURL iframe_url = https_server_.GetURL("b.test", "/title1.html");
topics_attribute_replacement.emplace_back("{{SRC_URL}}", iframe_url.spec());
GURL main_frame_url = https_server_.GetURL(
"a.test", net::test_server::GetFilePathWithReplacements(
"/browsing_topics/page_with_iframe.html",
topics_attribute_replacement));
// Wait for the main page and the iframe navigation.
IframeBrowsingTopicsAttributeWatcher navigation_observer(
shell()->web_contents(),
/*expected_number_of_navigations=*/2);
NavigationController::LoadURLParams params(main_frame_url);
shell()->web_contents()->GetController().LoadURLWithParams(params);
navigation_observer.WaitForNavigationFinished();
EXPECT_EQ(navigation_observer.last_navigation_url(), iframe_url);
EXPECT_FALSE(navigation_observer
.last_navigation_has_iframe_browsing_topics_attribute());
}
IN_PROC_BROWSER_TEST_F(BrowsingTopicsBrowserTest,
EmbedderOptInStatus_StaticIframe_HasBrowsingTopicsAttr) {
base::StringPairs topics_attribute_replacement;
topics_attribute_replacement.emplace_back(
"{{MAYBE_BROWSING_TOPICS_ATTRIBUTE}}", "browsingtopics");
GURL iframe_url = https_server_.GetURL("b.test", "/title1.html");
topics_attribute_replacement.emplace_back("{{SRC_URL}}", iframe_url.spec());
GURL main_frame_url = https_server_.GetURL(
"a.test", net::test_server::GetFilePathWithReplacements(
"/browsing_topics/page_with_iframe.html",
topics_attribute_replacement));
// Wait for the main page and the iframe navigation.
IframeBrowsingTopicsAttributeWatcher navigation_observer(
shell()->web_contents(),
/*expected_number_of_navigations=*/2);
NavigationController::LoadURLParams params(main_frame_url);
shell()->web_contents()->GetController().LoadURLWithParams(params);
navigation_observer.WaitForNavigationFinished();
EXPECT_EQ(navigation_observer.last_navigation_url(), iframe_url);
EXPECT_TRUE(navigation_observer
.last_navigation_has_iframe_browsing_topics_attribute());
}
IN_PROC_BROWSER_TEST_F(
BrowsingTopicsBrowserTest,
EmbedderOptInStatus_AppendIframeElement_BrowsingTopicsAttrIsFalse) {
GURL main_frame_url = https_server_.GetURL("a.test", "/title1.html");
EXPECT_TRUE(NavigateToURL(shell(), main_frame_url));
GURL iframe_url = https_server_.GetURL("b.test", "/title1.html");
// Wait for the iframe navigation.
IframeBrowsingTopicsAttributeWatcher navigation_observer(
shell()->web_contents());
ExecuteScriptAsync(shell()->web_contents(), content::JsReplace(R"(
const iframe = document.createElement("iframe");
iframe.browsingTopics = false;
iframe.src = $1;
document.body.appendChild(iframe);
)",
iframe_url));
navigation_observer.WaitForNavigationFinished();
EXPECT_EQ(navigation_observer.last_navigation_url(), iframe_url);
EXPECT_FALSE(navigation_observer
.last_navigation_has_iframe_browsing_topics_attribute());
}
IN_PROC_BROWSER_TEST_F(
BrowsingTopicsBrowserTest,
EmbedderOptInStatus_AppendIframeElement_BrowsingTopicsAttrIsTrue) {
GURL main_frame_url = https_server_.GetURL("a.test", "/title1.html");
EXPECT_TRUE(NavigateToURL(shell(), main_frame_url));
GURL iframe_url = https_server_.GetURL("b.test", "/title1.html");
// Wait for the iframe navigation.
IframeBrowsingTopicsAttributeWatcher navigation_observer(
shell()->web_contents());
ExecuteScriptAsync(shell()->web_contents(), content::JsReplace(R"(
const iframe = document.createElement("iframe");
iframe.browsingTopics = true;
iframe.src = $1;
document.body.appendChild(iframe);
)",
iframe_url));
navigation_observer.WaitForNavigationFinished();
EXPECT_EQ(navigation_observer.last_navigation_url(), iframe_url);
EXPECT_TRUE(navigation_observer
.last_navigation_has_iframe_browsing_topics_attribute());
}
IN_PROC_BROWSER_TEST_F(BrowsingTopicsBrowserTest,
EmbedderOptInStatus_BrowsingTopicsAttrUpdated) {
GURL main_frame_url = https_server_.GetURL("a.test", "/title1.html");
EXPECT_TRUE(NavigateToURL(shell(), main_frame_url));
// Append a frame without a `browsingTopics` attribute. Expect that the
// navigation doesn't see the flag.
{
GURL iframe_url = https_server_.GetURL("b.test", "/title1.html");
// Wait for the iframe navigation.
IframeBrowsingTopicsAttributeWatcher navigation_observer(
shell()->web_contents());
ExecuteScriptAsync(shell()->web_contents(), content::JsReplace(R"(
const iframe0 = document.createElement("iframe");
iframe0.src = $1;
document.body.appendChild(iframe0);
)",
iframe_url));
navigation_observer.WaitForNavigationFinished();
EXPECT_EQ(navigation_observer.last_navigation_url(), iframe_url);
EXPECT_FALSE(navigation_observer
.last_navigation_has_iframe_browsing_topics_attribute());
}
// Set the `browsingTopics` attribute to true. Expect that the next navigation
// sees the flag.
{
GURL iframe_url = https_server_.GetURL("c.test", "/title1.html");
// Wait for the iframe navigation.
IframeBrowsingTopicsAttributeWatcher navigation_observer(
shell()->web_contents());
ExecuteScriptAsync(shell()->web_contents(), content::JsReplace(R"(
iframe0.browsingTopics = true;
iframe0.src = $1;
)",
iframe_url));
navigation_observer.WaitForNavigationFinished();
EXPECT_EQ(navigation_observer.last_navigation_url(), iframe_url);
EXPECT_TRUE(navigation_observer
.last_navigation_has_iframe_browsing_topics_attribute());
}
}
} // namespace content