blob: cf642048a9cf25aadab272db045857783349a076 [file] [log] [blame]
Yao Xiaoef457b42022-10-19 22:37:431// Copyright 2022 The Chromium Authors
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include <vector>
6
Yao Xiao549be4472023-01-03 22:53:127#include "base/test/bind.h"
Yao Xiaoef457b42022-10-19 22:37:438#include "base/test/scoped_feature_list.h"
Yao Xiao9c54b3e2023-03-14 04:25:049#include "content/browser/browsing_topics/test_util.h"
Yao Xiaoef457b42022-10-19 22:37:4310#include "content/browser/web_contents/web_contents_impl.h"
11#include "content/public/browser/browser_context.h"
12#include "content/public/browser/navigation_handle.h"
13#include "content/public/browser/render_process_host.h"
14#include "content/public/browser/web_contents.h"
15#include "content/public/browser/web_contents_observer.h"
16#include "content/public/test/browser_test.h"
17#include "content/public/test/browser_test_utils.h"
18#include "content/public/test/content_browser_test.h"
Scott Violet99861992023-02-08 01:20:1219#include "content/public/test/content_browser_test_content_browser_client.h"
Yao Xiaoef457b42022-10-19 22:37:4320#include "content/public/test/content_browser_test_utils.h"
21#include "content/public/test/test_navigation_observer.h"
Yao Xiao549be4472023-01-03 22:53:1222#include "content/public/test/url_loader_interceptor.h"
Yao Xiaoef457b42022-10-19 22:37:4323#include "content/shell/browser/shell.h"
24#include "content/test/content_browser_test_utils_internal.h"
25#include "net/dns/mock_host_resolver.h"
26#include "net/test/embedded_test_server/default_handlers.h"
Yao Xiao9c54b3e2023-03-14 04:25:0427#include "net/test/embedded_test_server/request_handler_util.h"
Sandor Majorf42e6bd62025-02-28 00:12:0428#include "services/network/public/cpp/features.h"
Yao Xiaoef457b42022-10-19 22:37:4329#include "testing/gmock/include/gmock/gmock.h"
30#include "testing/gtest/include/gtest/gtest.h"
31#include "third_party/blink/public/common/features.h"
32#include "third_party/blink/public/mojom/browsing_topics/browsing_topics.mojom.h"
33#include "url/gurl.h"
34
35namespace content {
36
37namespace {
38
Scott Violet99861992023-02-08 01:20:1239class FixedTopicsContentBrowserClient
40 : public ContentBrowserTestContentBrowserClient {
Yao Xiaoef457b42022-10-19 22:37:4341 public:
Yao Xiao9c789ea2022-10-26 14:46:5542 bool HandleTopicsWebApi(
Yao Xiaoef457b42022-10-19 22:37:4343 const url::Origin& context_origin,
Yao Xiao9c789ea2022-10-26 14:46:5544 content::RenderFrameHost* main_frame,
45 browsing_topics::ApiCallerSource caller_source,
46 bool get_topics,
47 bool observe,
48 std::vector<blink::mojom::EpochTopicPtr>& topics) override {
Yao Xiaoef457b42022-10-19 22:37:4349 blink::mojom::EpochTopicPtr result_topic = blink::mojom::EpochTopic::New();
50 result_topic->topic = 1;
51 result_topic->config_version = "chrome.1";
52 result_topic->taxonomy_version = "1";
53 result_topic->model_version = "2";
54 result_topic->version = "chrome.1:1:2";
55
Yao Xiao9c789ea2022-10-26 14:46:5556 topics.push_back(std::move(result_topic));
57
58 return true;
Yao Xiaoef457b42022-10-19 22:37:4359 }
60
Yao Xiaodd39a3e2023-05-27 16:21:0661 int NumVersionsInTopicsEpochs(
62 content::RenderFrameHost* main_frame) const override {
63 return 1;
64 }
65
Yao Xiaoef457b42022-10-19 22:37:4366 StoragePartitionConfig GetStoragePartitionConfigForSite(
67 BrowserContext* browser_context,
68 const GURL& site) override {
W. James MacLean0b6117952024-07-04 12:05:4269 // Use a different StoragePartition for URLs from b.test (to test the fix
70 // for crbug.com/40855090). Note that the port should be removed from site
71 // to simplify the comparison, because non-default ports are included in
72 // site URLs when kOriginKeyedProcessesByDefault is enabled.
73 GURL::Replacements replacements;
74 replacements.ClearPort();
75 if (site.ReplaceComponents(replacements) == GURL("https://b.test/")) {
Yao Xiaoef457b42022-10-19 22:37:4376 return StoragePartitionConfig::Create(browser_context,
77 /*partition_domain=*/"b.test",
78 /*partition_name=*/"test_partition",
79 /*in_memory=*/false);
80 }
81 return StoragePartitionConfig::CreateDefault(browser_context);
82 }
83};
84
85} // namespace
86
87class BrowsingTopicsBrowserTest : public ContentBrowserTest {
88 public:
89 BrowsingTopicsBrowserTest() {
90 feature_list_.InitWithFeatures({features::kPrivacySandboxAdsAPIsOverride,
Sandor Majorf42e6bd62025-02-28 00:12:0491 network::features::kBrowsingTopics},
Yao Xiaoef457b42022-10-19 22:37:4392 /*disabled_features=*/{});
93 }
94
95 void SetUpOnMainThread() override {
96 ContentBrowserTest::SetUpOnMainThread();
97
98 host_resolver()->AddRule("*", "127.0.0.1");
99 https_server_.SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
100 net::test_server::RegisterDefaultHandlers(&https_server_);
101 https_server_.ServeFilesFromSourceDirectory("content/test/data");
102
103 content::SetupCrossSiteRedirector(&https_server_);
104 ASSERT_TRUE(https_server_.Start());
105
Scott Violet99861992023-02-08 01:20:12106 browser_client_ = std::make_unique<FixedTopicsContentBrowserClient>();
Yao Xiao549be4472023-01-03 22:53:12107
108 url_loader_monitor_ =
109 std::make_unique<URLLoaderInterceptor>(base::BindLambdaForTesting(
110 [&](URLLoaderInterceptor::RequestParams* params) -> bool {
111 last_request_is_topics_request_ =
112 params->url_request.browsing_topics;
113
Chris Fredricksona11bbfce2024-07-31 23:34:24114 last_topics_header_ =
115 params->url_request.headers.GetHeader("Sec-Browsing-Topics");
Yao Xiao549be4472023-01-03 22:53:12116
117 return false;
118 }));
Yao Xiaoef457b42022-10-19 22:37:43119 }
120
121 void TearDownOnMainThread() override {
Scott Violet99861992023-02-08 01:20:12122 browser_client_.reset();
Yao Xiao549be4472023-01-03 22:53:12123 url_loader_monitor_.reset();
Yao Xiaoef457b42022-10-19 22:37:43124 }
125
126 WebContents* web_contents() { return shell()->web_contents(); }
127
Yao Xiao549be4472023-01-03 22:53:12128 bool last_request_is_topics_request() const {
129 return last_request_is_topics_request_;
130 }
131
Arthur Sonzognic686e8f2024-01-11 08:36:37132 const std::optional<std::string>& last_topics_header() const {
Yao Xiao549be4472023-01-03 22:53:12133 return last_topics_header_;
134 }
135
Yao Xiaoef457b42022-10-19 22:37:43136 std::string InvokeTopicsAPI(const ToRenderFrameHost& adapter) {
137 return EvalJs(adapter, R"(
138 if (!(document.browsingTopics instanceof Function)) {
139 'not a function';
140 } else {
141 document.browsingTopics()
142 .then(topics => {
143 let result = "[";
144 for (const topic of topics) {
145 result += JSON.stringify(topic, Object.keys(topic).sort()) + ";"
146 }
147 result += "]";
148 return result;
149 })
150 .catch(error => error.message);
151 }
152 )")
153 .ExtractString();
154 ;
155 }
156
157 protected:
158 base::test::ScopedFeatureList feature_list_;
159
160 net::EmbeddedTestServer https_server_{
161 net::test_server::EmbeddedTestServer::TYPE_HTTPS};
162
Scott Violet99861992023-02-08 01:20:12163 std::unique_ptr<FixedTopicsContentBrowserClient> browser_client_;
Yao Xiao549be4472023-01-03 22:53:12164
165 bool last_request_is_topics_request_ = false;
166
167 std::unique_ptr<base::RunLoop> resource_request_url_waiter_;
Arthur Sonzognic686e8f2024-01-11 08:36:37168 std::optional<std::string> last_topics_header_;
Yao Xiao549be4472023-01-03 22:53:12169
170 std::unique_ptr<URLLoaderInterceptor> url_loader_monitor_;
Yao Xiaoef457b42022-10-19 22:37:43171};
172
173IN_PROC_BROWSER_TEST_F(BrowsingTopicsBrowserTest, TopicsAPI) {
174 // a.test will end up on the default storage partition.
175 GURL main_frame_url = https_server_.GetURL("a.test", "/hello.html");
176
177 EXPECT_TRUE(NavigateToURL(shell(), main_frame_url));
178
179 EXPECT_EQ(
180 "[{\"configVersion\":\"chrome.1\",\"modelVersion\":\"2\","
181 "\"taxonomyVersion\":\"1\",\"topic\":1,\"version\":\"chrome.1:1:2\"};]",
182 InvokeTopicsAPI(web_contents()));
183}
184
185IN_PROC_BROWSER_TEST_F(
186 BrowsingTopicsBrowserTest,
187 TopicsAPI_InvokedFromFrameWithNonDefaultStoragePartition) {
188 // b.test will end up on a non-default storage partition.
189 GURL main_frame_url = https_server_.GetURL("b.test", "/hello.html");
190
191 EXPECT_TRUE(NavigateToURL(shell(), main_frame_url));
192
193 EXPECT_EQ("[]", InvokeTopicsAPI(web_contents()));
194}
195
Alison Gale59c007a2024-04-20 03:05:40196// TODO(crbug.com/40245082): migrate to WPT.
Yao Xiao549be4472023-01-03 22:53:12197IN_PROC_BROWSER_TEST_F(BrowsingTopicsBrowserTest,
198 Fetch_TopicsHeaderNotVisibleInServiceWorker) {
199 GURL main_frame_url = https_server_.GetURL(
200 "a.test", "/browsing_topics/service_worker_factory.html");
201 GURL worker_script_url = https_server_.GetURL(
202 "a.test", "/browsing_topics/topics_service_worker.js");
203 GURL fetch_url = https_server_.GetURL("a.test", "/empty.html");
204
205 EXPECT_TRUE(NavigateToURL(shell(), main_frame_url));
206
207 EXPECT_EQ("ok",
208 EvalJs(shell()->web_contents(),
209 JsReplace("setupServiceWorker($1)", worker_script_url)));
210
211 // Reload the page to let it be controlled by the service worker.
212 EXPECT_TRUE(NavigateToURL(shell(), main_frame_url));
213
214 // Initiate a topics fetch() request from the Window context. Verify that the
215 // topics header is not visible in the service worker during the interception.
216 EXPECT_EQ("null", EvalJs(shell()->web_contents(), content::JsReplace(
217 R"(
218 new Promise((resolve, reject) => {
219 navigator.serviceWorker.addEventListener('message', e => {
220 if (e.data.url == $1) {
221 resolve(e.data.topicsHeader);
222 }
223 });
224
225 fetch($1, {browsingTopics: true});
226 });
227 )",
228 fetch_url)));
229}
230
231IN_PROC_BROWSER_TEST_F(BrowsingTopicsBrowserTest, TopicsHeaderForWindowFetch) {
232 GURL main_frame_url = https_server_.GetURL(
233 "a.test", "/browsing_topics/service_worker_factory.html");
234 GURL fetch_url = https_server_.GetURL("a.test", "/empty.html");
235
236 EXPECT_TRUE(NavigateToURL(shell(), main_frame_url));
237
238 EXPECT_TRUE(ExecJs(
239 shell()->web_contents(),
240 content::JsReplace("fetch($1, {browsingTopics: true})", fetch_url)));
241
242 EXPECT_TRUE(last_request_is_topics_request());
243 EXPECT_TRUE(last_topics_header());
Yao Xiaodd39a3e2023-05-27 16:21:06244 EXPECT_EQ(last_topics_header().value(),
Josh Karlin7b3aa1c2023-06-01 17:01:17245 "(1);v=chrome.1:1:2, ();p=P00000000000");
Yao Xiao549be4472023-01-03 22:53:12246}
247
248IN_PROC_BROWSER_TEST_F(BrowsingTopicsBrowserTest,
249 TopicsNotAllowedForServiceWorkerFetch) {
250 GURL main_frame_url = https_server_.GetURL(
251 "a.test", "/browsing_topics/service_worker_factory.html");
252 GURL worker_script_url = https_server_.GetURL(
253 "a.test", "/browsing_topics/topics_service_worker.js");
254 GURL fetch_url = https_server_.GetURL("a.test", "/empty.html");
255
256 EXPECT_TRUE(NavigateToURL(shell(), main_frame_url));
257
258 EXPECT_EQ("ok",
259 EvalJs(shell()->web_contents(),
260 JsReplace("setupServiceWorker($1)", worker_script_url)));
261
262 // Reload the page to let it be controlled by the service worker.
263 EXPECT_TRUE(NavigateToURL(shell(), main_frame_url));
264
265 // Initiate a topics fetch request from the service worker. Verify that it
266 // doesn't contain the topics header.
267 EXPECT_TRUE(ExecJs(shell()->web_contents(), content::JsReplace(
268 R"(
269 new Promise((resolve, reject) => {
270 navigator.serviceWorker.addEventListener('message', e => {
271 if (e.data.finishedFetch) {
272 resolve();
273 }
274 });
275
276 navigator.serviceWorker.controller.postMessage({
277 fetchUrl: $1
278 });
279 });
280 )",
281 fetch_url)));
282
283 EXPECT_FALSE(last_request_is_topics_request());
284 EXPECT_FALSE(last_topics_header());
285}
286
Yao Xiao9c54b3e2023-03-14 04:25:04287IN_PROC_BROWSER_TEST_F(BrowsingTopicsBrowserTest,
288 EmbedderOptInStatus_StaticIframe_NoBrowsingTopicsAttr) {
289 base::StringPairs topics_attribute_replacement;
290 topics_attribute_replacement.emplace_back(
291 "{{MAYBE_BROWSING_TOPICS_ATTRIBUTE}}", "");
292
293 GURL iframe_url = https_server_.GetURL("b.test", "/title1.html");
294 topics_attribute_replacement.emplace_back("{{SRC_URL}}", iframe_url.spec());
295
296 GURL main_frame_url = https_server_.GetURL(
297 "a.test", net::test_server::GetFilePathWithReplacements(
298 "/browsing_topics/page_with_iframe.html",
299 topics_attribute_replacement));
300
301 // Wait for the main page and the iframe navigation.
302 IframeBrowsingTopicsAttributeWatcher navigation_observer(
303 shell()->web_contents(),
304 /*expected_number_of_navigations=*/2);
305
306 NavigationController::LoadURLParams params(main_frame_url);
307 shell()->web_contents()->GetController().LoadURLWithParams(params);
308
309 navigation_observer.WaitForNavigationFinished();
310
311 EXPECT_EQ(navigation_observer.last_navigation_url(), iframe_url);
312 EXPECT_FALSE(navigation_observer
313 .last_navigation_has_iframe_browsing_topics_attribute());
314}
315
316IN_PROC_BROWSER_TEST_F(BrowsingTopicsBrowserTest,
317 EmbedderOptInStatus_StaticIframe_HasBrowsingTopicsAttr) {
318 base::StringPairs topics_attribute_replacement;
319 topics_attribute_replacement.emplace_back(
320 "{{MAYBE_BROWSING_TOPICS_ATTRIBUTE}}", "browsingtopics");
321
322 GURL iframe_url = https_server_.GetURL("b.test", "/title1.html");
323 topics_attribute_replacement.emplace_back("{{SRC_URL}}", iframe_url.spec());
324
325 GURL main_frame_url = https_server_.GetURL(
326 "a.test", net::test_server::GetFilePathWithReplacements(
327 "/browsing_topics/page_with_iframe.html",
328 topics_attribute_replacement));
329
330 // Wait for the main page and the iframe navigation.
331 IframeBrowsingTopicsAttributeWatcher navigation_observer(
332 shell()->web_contents(),
333 /*expected_number_of_navigations=*/2);
334
335 NavigationController::LoadURLParams params(main_frame_url);
336 shell()->web_contents()->GetController().LoadURLWithParams(params);
337
338 navigation_observer.WaitForNavigationFinished();
339
340 EXPECT_EQ(navigation_observer.last_navigation_url(), iframe_url);
341 EXPECT_TRUE(navigation_observer
342 .last_navigation_has_iframe_browsing_topics_attribute());
343}
344
345IN_PROC_BROWSER_TEST_F(
346 BrowsingTopicsBrowserTest,
347 EmbedderOptInStatus_AppendIframeElement_BrowsingTopicsAttrIsFalse) {
348 GURL main_frame_url = https_server_.GetURL("a.test", "/title1.html");
349 EXPECT_TRUE(NavigateToURL(shell(), main_frame_url));
350
351 GURL iframe_url = https_server_.GetURL("b.test", "/title1.html");
352
353 // Wait for the iframe navigation.
354 IframeBrowsingTopicsAttributeWatcher navigation_observer(
355 shell()->web_contents());
356
357 ExecuteScriptAsync(shell()->web_contents(), content::JsReplace(R"(
358 const iframe = document.createElement("iframe");
359 iframe.browsingTopics = false;
360 iframe.src = $1;
361 document.body.appendChild(iframe);
362 )",
363 iframe_url));
364
365 navigation_observer.WaitForNavigationFinished();
366
367 EXPECT_EQ(navigation_observer.last_navigation_url(), iframe_url);
368 EXPECT_FALSE(navigation_observer
369 .last_navigation_has_iframe_browsing_topics_attribute());
370}
371
372IN_PROC_BROWSER_TEST_F(
373 BrowsingTopicsBrowserTest,
374 EmbedderOptInStatus_AppendIframeElement_BrowsingTopicsAttrIsTrue) {
375 GURL main_frame_url = https_server_.GetURL("a.test", "/title1.html");
376 EXPECT_TRUE(NavigateToURL(shell(), main_frame_url));
377
378 GURL iframe_url = https_server_.GetURL("b.test", "/title1.html");
379
380 // Wait for the iframe navigation.
381 IframeBrowsingTopicsAttributeWatcher navigation_observer(
382 shell()->web_contents());
383
384 ExecuteScriptAsync(shell()->web_contents(), content::JsReplace(R"(
385 const iframe = document.createElement("iframe");
386 iframe.browsingTopics = true;
387 iframe.src = $1;
388 document.body.appendChild(iframe);
389 )",
390 iframe_url));
391
392 navigation_observer.WaitForNavigationFinished();
393
394 EXPECT_EQ(navigation_observer.last_navigation_url(), iframe_url);
395 EXPECT_TRUE(navigation_observer
396 .last_navigation_has_iframe_browsing_topics_attribute());
397}
398
399IN_PROC_BROWSER_TEST_F(BrowsingTopicsBrowserTest,
400 EmbedderOptInStatus_BrowsingTopicsAttrUpdated) {
401 GURL main_frame_url = https_server_.GetURL("a.test", "/title1.html");
402 EXPECT_TRUE(NavigateToURL(shell(), main_frame_url));
403
404 // Append a frame without a `browsingTopics` attribute. Expect that the
405 // navigation doesn't see the flag.
406 {
407 GURL iframe_url = https_server_.GetURL("b.test", "/title1.html");
408
409 // Wait for the iframe navigation.
410 IframeBrowsingTopicsAttributeWatcher navigation_observer(
411 shell()->web_contents());
412
413 ExecuteScriptAsync(shell()->web_contents(), content::JsReplace(R"(
414 const iframe0 = document.createElement("iframe");
415 iframe0.src = $1;
416 document.body.appendChild(iframe0);
417 )",
418 iframe_url));
419
420 navigation_observer.WaitForNavigationFinished();
421
422 EXPECT_EQ(navigation_observer.last_navigation_url(), iframe_url);
423 EXPECT_FALSE(navigation_observer
424 .last_navigation_has_iframe_browsing_topics_attribute());
425 }
426
427 // Set the `browsingTopics` attribute to true. Expect that the next navigation
428 // sees the flag.
429 {
430 GURL iframe_url = https_server_.GetURL("c.test", "/title1.html");
431
432 // Wait for the iframe navigation.
433 IframeBrowsingTopicsAttributeWatcher navigation_observer(
434 shell()->web_contents());
435
436 ExecuteScriptAsync(shell()->web_contents(), content::JsReplace(R"(
437 iframe0.browsingTopics = true;
438 iframe0.src = $1;
439 )",
440 iframe_url));
441
442 navigation_observer.WaitForNavigationFinished();
443
444 EXPECT_EQ(navigation_observer.last_navigation_url(), iframe_url);
445 EXPECT_TRUE(navigation_observer
446 .last_navigation_has_iframe_browsing_topics_attribute());
447 }
448}
449
Yao Xiaoef457b42022-10-19 22:37:43450} // namespace content