| // Copyright 2024 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <string> |
| #include <vector> |
| |
| #include "base/functional/bind.h" |
| #include "base/values.h" |
| #include "chrome/test/base/chrome_test_utils.h" |
| #include "chrome/test/base/platform_browser_test.h" |
| #include "content/public/browser/cookie_access_details.h" |
| #include "content/public/browser/global_routing_id.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/test_devtools_protocol_client.h" |
| #include "net/dns/mock_host_resolver.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| namespace { |
| |
| using CookieOperation = content::CookieAccessDetails::Type; |
| |
| class FrameCookieAccessObserver : public content::WebContentsObserver { |
| public: |
| explicit FrameCookieAccessObserver( |
| content::WebContents* web_contents, |
| content::RenderFrameHost* render_frame_host, |
| CookieOperation access_type) |
| : WebContentsObserver(web_contents), |
| rfh_token_(render_frame_host->GetGlobalFrameToken()), |
| access_type_(access_type) {} |
| |
| // Wait until the frame accesses cookies. |
| void Wait() { run_loop_.Run(); } |
| |
| // WebContentsObserver override |
| void OnCookiesAccessed(content::RenderFrameHost* render_frame_host, |
| const content::CookieAccessDetails& details) override { |
| if (details.type == access_type_ && |
| render_frame_host->GetGlobalFrameToken() == rfh_token_) { |
| run_loop_.Quit(); |
| } |
| } |
| |
| private: |
| const content::GlobalRenderFrameHostToken rfh_token_; |
| const CookieOperation access_type_; |
| base::RunLoop run_loop_; |
| }; |
| |
| } // namespace |
| |
| class BtmBounceTrackingDevToolsIssueTest |
| : public content::TestDevToolsProtocolClient, |
| public PlatformBrowserTest { |
| protected: |
| void SetUpOnMainThread() override { |
| PlatformBrowserTest::SetUpOnMainThread(); |
| host_resolver()->AddRule("*", "127.0.0.1"); |
| embedded_test_server()->AddDefaultHandlers(GetChromeTestDataDir()); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| } |
| |
| void WaitForIssueAndCheckTrackingSites( |
| const std::vector<std::string>& sites) { |
| auto is_dips_issue = [](const base::Value::Dict& params) { |
| return *(params.FindStringByDottedPath("issue.code")) == |
| "BounceTrackingIssue"; |
| }; |
| |
| // Wait for notification of a Bounce Tracking Issue. |
| base::Value::Dict params = WaitForMatchingNotification( |
| "Audits.issueAdded", base::BindRepeating(is_dips_issue)); |
| ASSERT_EQ(*params.FindStringByDottedPath("issue.code"), |
| "BounceTrackingIssue"); |
| |
| base::Value::Dict* bounce_tracking_issue_details = |
| params.FindDictByDottedPath("issue.details.bounceTrackingIssueDetails"); |
| ASSERT_TRUE(bounce_tracking_issue_details); |
| |
| std::vector<std::string> tracking_sites; |
| base::Value::List* tracking_sites_list = |
| bounce_tracking_issue_details->FindList("trackingSites"); |
| if (tracking_sites_list) { |
| for (const auto& val : *tracking_sites_list) { |
| tracking_sites.push_back(val.GetString()); |
| } |
| } |
| |
| // Verify the reported tracking sites match the expected sites. |
| EXPECT_THAT(tracking_sites, testing::ElementsAreArray(sites)); |
| |
| // Clear existing notifications so subsequent calls don't fail by checking |
| // `sites` against old notifications. |
| ClearNotifications(); |
| } |
| |
| void TearDownOnMainThread() override { |
| DetachProtocolClient(); |
| PlatformBrowserTest::TearDownOnMainThread(); |
| } |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(BtmBounceTrackingDevToolsIssueTest, |
| BounceTrackingDevToolsIssue) { |
| content::WebContents* web_contents = |
| chrome_test_utils::GetActiveWebContents(this); |
| |
| // Visit initial page on a.test. |
| ASSERT_TRUE(content::NavigateToURL( |
| web_contents, embedded_test_server()->GetURL("a.test", "/title1.html"))); |
| |
| // Open DevTools and enable Audit domain. |
| AttachToWebContents(web_contents); |
| SendCommandSync("Audits.enable"); |
| ClearNotifications(); |
| |
| // Navigate with a click (not a redirect) to b.test, which S-redirects to |
| // c.test. |
| ASSERT_TRUE(content::NavigateToURLFromRenderer( |
| web_contents, |
| embedded_test_server()->GetURL( |
| "b.test", "/cross-site-with-cookie/c.test/title1.html"), |
| embedded_test_server()->GetURL("c.test", "/title1.html"))); |
| WaitForIssueAndCheckTrackingSites({"b.test"}); |
| |
| // Write a cookie via JS on c.test. |
| content::RenderFrameHost* frame = web_contents->GetPrimaryMainFrame(); |
| FrameCookieAccessObserver cookie_observer(web_contents, frame, |
| CookieOperation::kChange); |
| ASSERT_TRUE(content::ExecJs(frame, "document.cookie = 'foo=bar';", |
| content::EXECUTE_SCRIPT_NO_USER_GESTURE)); |
| cookie_observer.Wait(); |
| |
| // Navigate without a click (i.e. by C-redirecting) to d.test. |
| ASSERT_TRUE(content::NavigateToURLFromRendererWithoutUserGesture( |
| web_contents, embedded_test_server()->GetURL("d.test", "/title1.html"))); |
| WaitForIssueAndCheckTrackingSites({"c.test"}); |
| |
| // Navigate without a click (i.e. by C-redirecting) to e.test, which |
| // S-redirects to f.test, which S-redirects to g.test. |
| ASSERT_TRUE(content::NavigateToURLFromRendererWithoutUserGesture( |
| web_contents, |
| embedded_test_server()->GetURL( |
| "e.test", |
| "/cross-site-with-cookie/f.test/cross-site-with-cookie/g.test/" |
| "title1.html"), |
| embedded_test_server()->GetURL("g.test", "/title1.html"))); |
| // Note d.test is not listed as a potentially tracking site since it did not |
| // write cookies before bouncing the user. |
| WaitForIssueAndCheckTrackingSites({"e.test", "f.test"}); |
| } |