blob: eecb030d5d75a1e7f9aa4ddd19f0fc098c0b0894 [file] [log] [blame]
// 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"});
}