blob: f73a24810be155fc245832888efb5ee6fa148896 [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 "base/memory/raw_ptr.h"
#include "base/rand_util.h"
#include "base/run_loop.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/simple_test_clock.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "chrome/browser/browsing_data/browsing_data_important_sites_util.h"
#include "chrome/browser/browsing_data/chrome_browsing_data_remover_constants.h"
#include "chrome/browser/content_settings/host_content_settings_map_factory.h"
#include "chrome/browser/dips/dips_bounce_detector.h"
#include "chrome/browser/dips/dips_features.h"
#include "chrome/browser/dips/dips_service.h"
#include "chrome/browser/dips/dips_service_factory.h"
#include "chrome/browser/dips/dips_storage.h"
#include "chrome/browser/dips/dips_test_utils.h"
#include "chrome/browser/dips/dips_utils.h"
#include "chrome/test/base/chrome_test_utils.h"
#include "components/content_settings/core/browser/host_content_settings_map.h"
#include "components/ukm/test_ukm_recorder.h"
#include "content/public/browser/browser_context.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/hit_test_region_observer.h"
#include "content/public/test/test_launcher.h"
#include "net/dns/mock_host_resolver.h"
#include "third_party/blink/public/common/switches.h"
#if !BUILDFLAG(IS_ANDROID)
#include "chrome/browser/ui/browser.h"
#include "chrome/test/base/ui_test_utils.h"
#endif // !BUILDFLAG(IS_ANDROID)
using content::CookieAccessDetails;
using content::NavigationHandle;
using content::RenderFrameHost;
using content::WebContents;
using testing::Optional;
using testing::Pair;
using testing::Optional;
using testing::Pair;
namespace {
class FrameCookieAccessObserver : public content::WebContentsObserver {
public:
explicit FrameCookieAccessObserver(WebContents* web_contents,
RenderFrameHost* render_frame_host)
: WebContentsObserver(web_contents),
render_frame_host_(render_frame_host) {}
// 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 (render_frame_host_ == render_frame_host) {
run_loop_.Quit();
}
}
private:
const raw_ptr<content::RenderFrameHost> render_frame_host_;
base::RunLoop run_loop_;
};
// Histogram names
constexpr char kTimeToInteraction[] =
"Privacy.DIPS.TimeFromStorageToInteraction.Standard";
constexpr char kTimeToStorage[] =
"Privacy.DIPS.TimeFromInteractionToStorage.Standard";
#if !BUILDFLAG(IS_ANDROID)
constexpr char kTimeToInteraction_OTR_Block3PC[] =
"Privacy.DIPS.TimeFromStorageToInteraction.OffTheRecord_Block3PC";
#endif
} // namespace
class DIPSTabHelperBrowserTest : public PlatformBrowserTest,
public testing::WithParamInterface<bool> {
protected:
void SetUp() override {
if (IsPersistentStorageEnabled()) {
scoped_feature_list_.InitAndEnableFeatureWithParameters(
dips::kFeature,
{{"persist_database", "true"}, {"triggering_action", "bounce"}});
} else {
scoped_feature_list_.InitAndEnableFeatureWithParameters(
dips::kFeature, {{"triggering_action", "bounce"}});
}
PlatformBrowserTest::SetUp();
}
void SetUpCommandLine(base::CommandLine* command_line) override {
// Prevents flakiness by handling clicks even before content is drawn.
command_line->AppendSwitch(blink::switches::kAllowPreCommitInput);
}
void SetUpOnMainThread() override {
ASSERT_TRUE(embedded_test_server()->Start());
host_resolver()->AddRule("a.test", "127.0.0.1");
host_resolver()->AddRule("b.test", "127.0.0.1");
host_resolver()->AddRule("c.test", "127.0.0.1");
host_resolver()->AddRule("d.test", "127.0.0.1");
DIPSWebContentsObserver::FromWebContents(GetActiveWebContents())
->SetClockForTesting(&test_clock_);
DIPSService* dips_service = DIPSServiceFactory::GetForBrowserContext(
GetActiveWebContents()->GetBrowserContext());
dips_service->WaitForInitCompleteForTesting();
}
WebContents* GetActiveWebContents() {
if (!web_contents_) {
web_contents_ = chrome_test_utils::GetActiveWebContents(this);
}
return web_contents_;
}
void BlockUntilHelperProcessesPendingRequests() {
base::SequenceBound<DIPSStorage>* storage =
DIPSServiceFactory::GetForBrowserContext(
GetActiveWebContents()->GetBrowserContext())
->storage();
storage->FlushPostedTasksForTesting();
}
void SetDIPSTime(base::Time time) { test_clock_.SetNow(time); }
void StateForURL(const GURL& url, StateForURLCallback callback) {
DIPSService* dips_service = DIPSServiceFactory::GetForBrowserContext(
GetActiveWebContents()->GetBrowserContext());
dips_service->storage()
->AsyncCall(&DIPSStorage::Read)
.WithArgs(url)
.Then(std::move(callback));
}
absl::optional<StateValue> GetDIPSState(const GURL& url) {
absl::optional<StateValue> state;
StateForURL(url, base::BindLambdaForTesting([&](DIPSState loaded_state) {
if (loaded_state.was_loaded()) {
state = loaded_state.ToStateValue();
}
}));
BlockUntilHelperProcessesPendingRequests();
return state;
}
[[nodiscard]] bool NavigateToURLAndWaitForCookieWrite(const GURL& url) {
URLCookieAccessObserver observer(GetActiveWebContents(), url,
CookieAccessDetails::Type::kChange);
bool success = content::NavigateToURL(GetActiveWebContents(), url);
if (!success) {
return false;
}
observer.Wait();
return true;
}
bool IsPersistentStorageEnabled() { return GetParam(); }
base::Clock* test_clock() { return &test_clock_; }
// Make GetActiveWebContents() return the given value instead of the default.
// Helpful for tests that use other WebContents (e.g. in incognito windows).
void OverrideActiveWebContents(WebContents* web_contents) {
web_contents_ = web_contents;
}
void EndRedirectChain() {
WebContents* web_contents = GetActiveWebContents();
DIPSService* dips_service = DIPSServiceFactory::GetForBrowserContext(
web_contents->GetBrowserContext());
GURL expected_url = web_contents->GetLastCommittedURL();
RedirectChainObserver chain_observer(dips_service, expected_url);
// Performing a browser-based navigation terminates the current redirect
// chain.
ASSERT_TRUE(content::NavigateToURL(
web_contents,
embedded_test_server()->GetURL("a.test", "/title1.html")));
chain_observer.Wait();
}
private:
raw_ptr<WebContents, DanglingUntriaged> web_contents_ = nullptr;
base::SimpleTestClock test_clock_;
base::test::ScopedFeatureList scoped_feature_list_;
};
IN_PROC_BROWSER_TEST_P(DIPSTabHelperBrowserTest,
InteractionsRecordedInAncestorFrames) {
GURL url_a = embedded_test_server()->GetURL("a.test", "/iframe_blank.html");
GURL url_b = embedded_test_server()->GetURL("b.test", "/title1.html");
const std::string kIframeId = "test"; // defined in iframe_blank.html
base::Time time = base::Time::FromDoubleT(1);
content::WebContents* web_contents = GetActiveWebContents();
// The top-level page is on a.test.
ASSERT_TRUE(content::NavigateToURL(web_contents, url_a));
// Before clicking, no DIPS state for either site.
EXPECT_FALSE(GetDIPSState(url_a).has_value());
EXPECT_FALSE(GetDIPSState(url_b).has_value());
// Click on the a.test top-level site.
SetDIPSTime(time);
UserActivationObserver observer_a(web_contents,
web_contents->GetPrimaryMainFrame());
SimulateMouseClick(web_contents, 0, blink::WebMouseEvent::Button::kLeft);
observer_a.Wait();
// User interaction is recorded for a.test (the top-level frame).
absl::optional<StateValue> state_a = GetDIPSState(url_a);
ASSERT_TRUE(state_a.has_value());
EXPECT_FALSE(state_a->site_storage_times.has_value());
EXPECT_EQ(absl::make_optional(time), state_a->user_interaction_times->first);
// Update the top-level page to have an iframe pointing to b.test.
ASSERT_TRUE(content::NavigateIframeToURL(web_contents, kIframeId, url_b));
content::RenderFrameHost* iframe = content::FrameMatchingPredicate(
web_contents->GetPrimaryPage(),
base::BindRepeating(&content::FrameIsChildOfMainFrame));
// Wait until we can click on the iframe.
content::WaitForHitTestData(iframe);
// Click on the b.test iframe.
base::Time frame_interaction_time =
time + DIPSBounceDetector::kTimestampUpdateInterval;
SetDIPSTime(frame_interaction_time);
UserActivationObserver observer_b(web_contents, iframe);
// TODO(crbug.com/1386142): Remove the ExecJs workaround once
// SimulateMouseClickOrTapElementWithId is able to activate iframes on Android
#if !BUILDFLAG(IS_ANDROID)
content::SimulateMouseClickOrTapElementWithId(web_contents, kIframeId);
#else
ASSERT_TRUE(content::ExecJs(iframe, "// empty script to activate iframe"));
#endif
observer_b.Wait();
// User interaction on the top-level is updated by interacting with b.test
// (the iframe).
state_a = GetDIPSState(url_a);
ASSERT_TRUE(state_a.has_value());
EXPECT_FALSE(state_a->site_storage_times.has_value());
EXPECT_EQ(absl::make_optional(frame_interaction_time),
state_a->user_interaction_times->second);
// The iframe site doesn't have any state.
EXPECT_FALSE(GetDIPSState(url_b).has_value());
}
IN_PROC_BROWSER_TEST_P(DIPSTabHelperBrowserTest,
MultipleUserInteractionsRecorded) {
GURL url = embedded_test_server()->GetURL("a.test", "/title1.html");
base::Time time = base::Time::FromDoubleT(1);
content::WebContents* web_contents = GetActiveWebContents();
SetDIPSTime(time);
// Navigate to a.test.
ASSERT_TRUE(content::NavigateToURL(web_contents, url));
content::RenderFrameHost* frame = web_contents->GetPrimaryMainFrame();
content::WaitForHitTestData(frame); // Wait until we can click.
// Before clicking, there's no DIPS state for the site.
EXPECT_FALSE(GetDIPSState(url).has_value());
UserActivationObserver observer1(web_contents, frame);
SimulateMouseClick(web_contents, 0, blink::WebMouseEvent::Button::kLeft);
observer1.Wait();
// One instance of user interaction is recorded.
absl::optional<StateValue> state_1 = GetDIPSState(url);
ASSERT_TRUE(state_1.has_value());
EXPECT_FALSE(state_1->site_storage_times.has_value());
EXPECT_EQ(absl::make_optional(time), state_1->user_interaction_times->first);
EXPECT_EQ(state_1->user_interaction_times->first,
state_1->user_interaction_times->second);
SetDIPSTime(time + DIPSBounceDetector::kTimestampUpdateInterval +
base::Seconds(10));
UserActivationObserver observer_2(web_contents, frame);
SimulateMouseClick(web_contents, 0, blink::WebMouseEvent::Button::kLeft);
observer_2.Wait();
// A second, different, instance of user interaction is recorded for the same
// site.
absl::optional<StateValue> state_2 = GetDIPSState(url);
ASSERT_TRUE(state_2.has_value());
EXPECT_FALSE(state_2->site_storage_times.has_value());
EXPECT_NE(state_2->user_interaction_times->second,
state_2->user_interaction_times->first);
EXPECT_EQ(absl::make_optional(time), state_2->user_interaction_times->first);
EXPECT_EQ(
absl::make_optional(time + DIPSBounceDetector::kTimestampUpdateInterval +
base::Seconds(10)),
state_2->user_interaction_times->second);
}
IN_PROC_BROWSER_TEST_P(DIPSTabHelperBrowserTest, StorageRecordedInSingleFrame) {
// We host the iframe content on an HTTPS server, because for it to write a
// cookie, the cookie needs to be SameSite=None and Secure.
net::EmbeddedTestServer https_server(net::EmbeddedTestServer::TYPE_HTTPS);
https_server.SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
https_server.AddDefaultHandlers(
base::FilePath(FILE_PATH_LITERAL("chrome/test/data")));
ASSERT_TRUE(https_server.Start());
GURL url_a = embedded_test_server()->GetURL("a.test", "/iframe_blank.html");
GURL url_b = https_server.GetURL("b.test", "/title1.html");
const std::string kIframeId = "test"; // defined in iframe_blank.html
base::Time time = base::Time::FromDoubleT(1);
content::WebContents* web_contents = GetActiveWebContents();
// The top-level page is on a.test, containing an iframe pointing at b.test.
ASSERT_TRUE(content::NavigateToURL(web_contents, url_a));
ASSERT_TRUE(content::NavigateIframeToURL(web_contents, kIframeId, url_b));
content::RenderFrameHost* iframe = content::FrameMatchingPredicate(
web_contents->GetPrimaryPage(),
base::BindRepeating(&content::FrameIsChildOfMainFrame));
// Initially, no DIPS state for either site.
EXPECT_FALSE(GetDIPSState(url_a).has_value());
EXPECT_FALSE(GetDIPSState(url_b).has_value());
// Write a cookie in the b.test iframe.
SetDIPSTime(time);
FrameCookieAccessObserver observer(web_contents, iframe);
ASSERT_TRUE(content::ExecJs(
iframe, "document.cookie = 'foo=bar; SameSite=None; Secure';",
content::EXECUTE_SCRIPT_NO_USER_GESTURE));
observer.Wait();
// Nothing recorded for a.test (the top-level frame).
absl::optional<StateValue> state_a = GetDIPSState(url_a);
EXPECT_FALSE(state_a.has_value());
// Nothing recorded for b.test (the iframe), since we don't record non main
// frame URLs to DIPS State.
absl::optional<StateValue> state_b = GetDIPSState(url_b);
EXPECT_FALSE(state_b.has_value());
}
IN_PROC_BROWSER_TEST_P(DIPSTabHelperBrowserTest,
StorageNotRecordedForThirdPartySubresource) {
// We host the "image" on an HTTPS server, because for it to write a
// cookie, the cookie needs to be SameSite=None and Secure.
net::EmbeddedTestServer https_server(net::EmbeddedTestServer::TYPE_HTTPS);
https_server.SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
https_server.AddDefaultHandlers(
base::FilePath(FILE_PATH_LITERAL("chrome/test/data")));
ASSERT_TRUE(https_server.Start());
GURL page_url = embedded_test_server()->GetURL("a.test", "/title1.html");
GURL image_url =
https_server.GetURL("b.test", "/set-cookie?foo=bar;Secure;SameSite=None");
content::WebContents* web_contents = GetActiveWebContents();
base::Time time = base::Time::FromDoubleT(1);
SetDIPSTime(time);
// Set SameSite=None cookie on b.test.
ASSERT_TRUE(content::NavigateToURL(
web_contents, https_server.GetURL(
"b.test", "/set-cookie?foo=bar;Secure;SameSite=None")));
ASSERT_TRUE(GetDIPSState(image_url).has_value());
EXPECT_EQ(GetDIPSState(image_url).value().site_storage_times->second, time);
// Navigate top-level page to a.test.
ASSERT_TRUE(content::NavigateToURL(web_contents, page_url));
// Advance time and cause a third-party cookie read by loading an "image" from
// b.test.
SetDIPSTime(time + base::Seconds(10));
FrameCookieAccessObserver observer(web_contents,
web_contents->GetPrimaryMainFrame());
ASSERT_TRUE(content::ExecJs(web_contents,
content::JsReplace(
R"(
let img = document.createElement('img');
img.src = $1;
document.body.appendChild(img);)",
image_url),
content::EXECUTE_SCRIPT_NO_USER_GESTURE));
observer.Wait();
// Nothing recorded for a.test (the top-level frame).
EXPECT_FALSE(GetDIPSState(page_url).has_value());
// The last site storage timestamp for b.test (the site hosting the image)
// should be unchanged, since we don't record cookie accesses from loading
// third-party resources.
EXPECT_EQ(GetDIPSState(image_url).value().site_storage_times->second, time);
}
IN_PROC_BROWSER_TEST_P(DIPSTabHelperBrowserTest, MultipleSiteStoragesRecorded) {
GURL url = embedded_test_server()->GetURL("a.test", "/set-cookie?foo=bar");
base::Time time = base::Time::FromDoubleT(1);
SetDIPSTime(time);
// Navigating to this URL sets a cookie.
ASSERT_TRUE(NavigateToURLAndWaitForCookieWrite(url));
// One instance of site storage is recorded.
absl::optional<StateValue> state_1 = GetDIPSState(url);
ASSERT_TRUE(state_1.has_value());
EXPECT_FALSE(state_1->user_interaction_times.has_value());
EXPECT_EQ(absl::make_optional(time), state_1->site_storage_times->first);
EXPECT_EQ(state_1->site_storage_times->second,
state_1->site_storage_times->first);
SetDIPSTime(time + base::Seconds(10));
// Navigate to the URL again to rewrite the cookie.
ASSERT_TRUE(NavigateToURLAndWaitForCookieWrite(url));
// A second, different, instance of site storage is recorded for the same
// site.
absl::optional<StateValue> state_2 = GetDIPSState(url);
ASSERT_TRUE(state_2.has_value());
EXPECT_FALSE(state_2->user_interaction_times.has_value());
EXPECT_NE(state_2->site_storage_times->second,
state_2->site_storage_times->first);
EXPECT_EQ(absl::make_optional(time), state_2->site_storage_times->first);
EXPECT_EQ(absl::make_optional(time + base::Seconds(10)),
state_2->site_storage_times->second);
}
IN_PROC_BROWSER_TEST_P(DIPSTabHelperBrowserTest, Histograms_StorageThenClick) {
base::HistogramTester histograms;
GURL url = embedded_test_server()->GetURL("a.test", "/set-cookie?foo=bar");
base::Time time = base::Time::FromDoubleT(1);
content::WebContents* web_contents = GetActiveWebContents();
SetDIPSTime(time);
// Navigating to this URL sets a cookie.
ASSERT_TRUE(NavigateToURLAndWaitForCookieWrite(url));
// Wait until we can click.
content::WaitForHitTestData(web_contents->GetPrimaryMainFrame());
BlockUntilHelperProcessesPendingRequests();
histograms.ExpectTotalCount(kTimeToInteraction, 0);
histograms.ExpectTotalCount(kTimeToStorage, 0);
SetDIPSTime(time + base::Seconds(10));
UserActivationObserver observer(web_contents,
web_contents->GetPrimaryMainFrame());
SimulateMouseClick(web_contents, 0, blink::WebMouseEvent::Button::kLeft);
observer.Wait();
BlockUntilHelperProcessesPendingRequests();
histograms.ExpectTotalCount(kTimeToInteraction, 1);
histograms.ExpectTotalCount(kTimeToStorage, 0);
histograms.ExpectUniqueTimeSample(kTimeToInteraction, base::Seconds(10), 1);
}
IN_PROC_BROWSER_TEST_P(DIPSTabHelperBrowserTest,
Histograms_StorageThenClick_Incognito) {
// TODO(crbug.com/1380410): Enable this test on Android once the logic to
// create an Incognito browser without regard to platform is public.
#if !BUILDFLAG(IS_ANDROID)
base::HistogramTester histograms;
GURL url = embedded_test_server()->GetURL("a.test", "/set-cookie?foo=bar");
base::Time time = base::Time::FromDoubleT(1);
Browser* browser = CreateIncognitoBrowser();
content::WebContents* web_contents =
browser->tab_strip_model()->GetActiveWebContents();
// Make our helper methods use the incognito WebContents.
OverrideActiveWebContents(web_contents);
DIPSWebContentsObserver::FromWebContents(web_contents)
->SetClockForTesting(test_clock());
SetDIPSTime(time);
// Navigating to this URL sets a cookie.
ASSERT_TRUE(NavigateToURLAndWaitForCookieWrite(url));
// Wait until we can click.
content::WaitForHitTestData(web_contents->GetPrimaryMainFrame());
BlockUntilHelperProcessesPendingRequests();
histograms.ExpectTotalCount(kTimeToInteraction, 0);
histograms.ExpectTotalCount(kTimeToInteraction_OTR_Block3PC, 0);
histograms.ExpectTotalCount(kTimeToStorage, 0);
SetDIPSTime(time + base::Seconds(10));
UserActivationObserver observer(web_contents,
web_contents->GetPrimaryMainFrame());
SimulateMouseClick(web_contents, 0, blink::WebMouseEvent::Button::kLeft);
observer.Wait();
BlockUntilHelperProcessesPendingRequests();
histograms.ExpectTotalCount(kTimeToInteraction, 0);
// Incognito Mode defaults to blocking third-party cookies.
histograms.ExpectTotalCount(kTimeToInteraction_OTR_Block3PC, 1);
histograms.ExpectTotalCount(kTimeToStorage, 0);
histograms.ExpectUniqueTimeSample(kTimeToInteraction_OTR_Block3PC,
base::Seconds(10), 1);
#endif
}
IN_PROC_BROWSER_TEST_P(DIPSTabHelperBrowserTest, Histograms_ClickThenStorage) {
base::HistogramTester histograms;
base::Time time = base::Time::FromDoubleT(1);
content::WebContents* web_contents = GetActiveWebContents();
ASSERT_TRUE(content::NavigateToURL(
web_contents, embedded_test_server()->GetURL("a.test", "/title1.html")));
content::RenderFrameHost* frame = web_contents->GetPrimaryMainFrame();
content::WaitForHitTestData(frame); // wait until we can click.
SetDIPSTime(time);
UserActivationObserver click_observer(web_contents, frame);
SimulateMouseClick(web_contents, 0, blink::WebMouseEvent::Button::kLeft);
click_observer.Wait();
BlockUntilHelperProcessesPendingRequests();
histograms.ExpectTotalCount(kTimeToInteraction, 0);
histograms.ExpectTotalCount(kTimeToStorage, 0);
// Write a cookie now that the click has been handled.
SetDIPSTime(time + base::Seconds(10));
FrameCookieAccessObserver cookie_observer(web_contents, frame);
ASSERT_TRUE(content::ExecJs(frame, "document.cookie = 'foo=bar';",
content::EXECUTE_SCRIPT_NO_USER_GESTURE));
cookie_observer.Wait();
BlockUntilHelperProcessesPendingRequests();
histograms.ExpectTotalCount(kTimeToInteraction, 0);
histograms.ExpectTotalCount(kTimeToStorage, 1);
histograms.ExpectUniqueTimeSample(kTimeToStorage, base::Seconds(10), 1);
}
IN_PROC_BROWSER_TEST_P(DIPSTabHelperBrowserTest,
Histograms_MultipleStoragesThenClick) {
base::HistogramTester histograms;
GURL url = embedded_test_server()->GetURL("a.test", "/set-cookie?foo=bar");
base::Time time = base::Time::FromDoubleT(1);
content::WebContents* web_contents = GetActiveWebContents();
SetDIPSTime(time);
// Navigating to this URL sets a cookie.
ASSERT_TRUE(NavigateToURLAndWaitForCookieWrite(url));
BlockUntilHelperProcessesPendingRequests();
// Navigate to the URL, setting the cookie again.
SetDIPSTime(time + base::Seconds(3));
ASSERT_TRUE(NavigateToURLAndWaitForCookieWrite(url));
content::RenderFrameHost* frame = web_contents->GetPrimaryMainFrame();
// Wait until we can click.
content::WaitForHitTestData(frame);
BlockUntilHelperProcessesPendingRequests();
// Verify both cookie writes were recorded.
absl::optional<StateValue> state = GetDIPSState(url);
ASSERT_TRUE(state.has_value());
EXPECT_NE(state->site_storage_times->first,
state->site_storage_times->second);
EXPECT_EQ(absl::make_optional(time), state->site_storage_times->first);
EXPECT_EQ(absl::make_optional(time + base::Seconds(3)),
state->site_storage_times->second);
EXPECT_FALSE(state->user_interaction_times.has_value());
histograms.ExpectTotalCount(kTimeToInteraction, 0);
histograms.ExpectTotalCount(kTimeToStorage, 0);
SetDIPSTime(time + base::Seconds(10));
UserActivationObserver observer(web_contents, frame);
SimulateMouseClick(web_contents, 0, blink::WebMouseEvent::Button::kLeft);
observer.Wait();
BlockUntilHelperProcessesPendingRequests();
histograms.ExpectTotalCount(kTimeToInteraction, 1);
histograms.ExpectTotalCount(kTimeToStorage, 0);
// Unlike for TimeToStorage metrics, we want to know the time from the
// first site storage, not the most recent, so the reported time delta
// should be 10 seconds (not 7).
histograms.ExpectUniqueTimeSample(kTimeToInteraction, base::Seconds(10), 1);
}
IN_PROC_BROWSER_TEST_P(DIPSTabHelperBrowserTest,
Histograms_MultipleClicksThenStorage) {
base::HistogramTester histograms;
GURL url = embedded_test_server()->GetURL("a.test", "/title1.html");
base::Time time = base::Time::FromDoubleT(1);
content::WebContents* web_contents = GetActiveWebContents();
ASSERT_TRUE(content::NavigateToURL(web_contents, url));
content::RenderFrameHost* frame = web_contents->GetPrimaryMainFrame();
content::WaitForHitTestData(frame); // Wait until we can click.
// Click once.
SetDIPSTime(time);
UserActivationObserver click_observer1(web_contents, frame);
SimulateMouseClick(web_contents, 0, blink::WebMouseEvent::Button::kLeft);
click_observer1.Wait();
BlockUntilHelperProcessesPendingRequests();
// Click a second time.
SetDIPSTime(time + DIPSBounceDetector::kTimestampUpdateInterval +
base::Seconds(3));
UserActivationObserver click_observer_2(web_contents, frame);
SimulateMouseClick(web_contents, 0, blink::WebMouseEvent::Button::kLeft);
click_observer_2.Wait();
BlockUntilHelperProcessesPendingRequests();
// Verify both clicks were recorded.
absl::optional<StateValue> state = GetDIPSState(url);
ASSERT_TRUE(state.has_value());
EXPECT_NE(state->user_interaction_times->first,
state->user_interaction_times->second);
EXPECT_EQ(absl::make_optional(time), state->user_interaction_times->first);
EXPECT_EQ(
absl::make_optional(time + DIPSBounceDetector::kTimestampUpdateInterval +
base::Seconds(3)),
state->user_interaction_times->second);
EXPECT_FALSE(state->site_storage_times.has_value());
histograms.ExpectTotalCount(kTimeToInteraction, 0);
histograms.ExpectTotalCount(kTimeToStorage, 0);
// Write a cookie now that both clicks have been handled.
SetDIPSTime(time + DIPSBounceDetector::kTimestampUpdateInterval +
base::Seconds(10));
FrameCookieAccessObserver cookie_observer(web_contents, frame);
ASSERT_TRUE(content::ExecJs(frame, "document.cookie = 'foo=bar';",
content::EXECUTE_SCRIPT_NO_USER_GESTURE));
cookie_observer.Wait();
BlockUntilHelperProcessesPendingRequests();
histograms.ExpectTotalCount(kTimeToInteraction, 0);
histograms.ExpectTotalCount(kTimeToStorage, 1);
// Unlike for TimeToInteraction metrics, we want to know the time from the
// most recent user interaction, not the first, so the reported time delta
// should be 7 seconds (not 10).
histograms.ExpectUniqueTimeSample(kTimeToStorage, base::Seconds(7), 1);
}
IN_PROC_BROWSER_TEST_P(DIPSTabHelperBrowserTest,
ChromeBrowsingDataRemover_Basic) {
content::WebContents* web_contents = GetActiveWebContents();
base::Time interaction_time = base::Time::Now() - base::Seconds(10);
SetDIPSTime(interaction_time);
// Perform a click to get a.test added to the DIPS DB.
ASSERT_TRUE(content::NavigateToURL(
web_contents, embedded_test_server()->GetURL("a.test", "/title1.html")));
UserActivationObserver observer(web_contents,
web_contents->GetPrimaryMainFrame());
SimulateMouseClick(web_contents, 0, blink::WebMouseEvent::Button::kLeft);
observer.Wait();
// Verify it was added.
absl::optional<StateValue> state_initial =
GetDIPSState(GURL("http://a.test"));
ASSERT_TRUE(state_initial.has_value());
ASSERT_TRUE(state_initial->user_interaction_times.has_value());
EXPECT_EQ(state_initial->user_interaction_times->first, interaction_time);
// Remove browsing data for the past day.
uint64_t remove_mask = chrome_browsing_data_remover::DATA_TYPE_HISTORY |
chrome_browsing_data_remover::DATA_TYPE_SITE_DATA;
std::unique_ptr<content::BrowsingDataFilterBuilder> filter_builder(
content::BrowsingDataFilterBuilder::Create(
content::BrowsingDataFilterBuilder::Mode::kPreserve));
content::BrowsingDataRemover* remover =
GetActiveWebContents()->GetBrowserContext()->GetBrowsingDataRemover();
base::RunLoop run_loop;
browsing_data_important_sites_util::Remove(
remove_mask, content::BrowsingDataRemover::ORIGIN_TYPE_UNPROTECTED_WEB,
browsing_data::TimePeriod::LAST_DAY, std::move(filter_builder), remover,
base::IgnoreArgs<uint64_t>(run_loop.QuitClosure()));
run_loop.Run();
// Verify that the user interaction has been cleared from the DIPS DB.
absl::optional<StateValue> state_final = GetDIPSState(GURL("http://a.test"));
EXPECT_FALSE(state_final.has_value());
}
INSTANTIATE_TEST_SUITE_P(All, DIPSTabHelperBrowserTest, ::testing::Bool());
// TODO(crbug.com/654704): Android does not support PRE_ tests.
#if !BUILDFLAG(IS_ANDROID)
class DIPSPrepopulateTest : public PlatformBrowserTest {
void SetUp() override {
if (content::IsPreTest() && GetTestPreCount() % 2 != 0) {
// Alternate between disabling and enabling DIPS in `PRE_` tests.
// Only disable explicitly since the feature is on by default.
feature_list_.InitAndDisableFeature(dips::kFeature);
} else {
feature_list_.InitAndEnableFeatureWithParameters(
dips::kFeature, {{"persist_database", "true"}});
}
PlatformBrowserTest::SetUp();
}
void SetUpCommandLine(base::CommandLine* command_line) override {
// Prevents flakiness by handling clicks even before content is drawn.
command_line->AppendSwitch(blink::switches::kAllowPreCommitInput);
}
void SetUpOnMainThread() override {
ASSERT_TRUE(embedded_test_server()->Start());
host_resolver()->AddRule("a.test", "127.0.0.1");
host_resolver()->AddRule("b.test", "127.0.0.1");
host_resolver()->AddRule("c.test", "127.0.0.1");
dips_service = DIPSServiceFactory::GetForBrowserContext(
chrome_test_utils::GetActiveWebContents(this)->GetBrowserContext());
if (dips_service) {
storage = dips_service->storage();
dips_service->WaitForInitCompleteForTesting();
}
}
protected:
absl::optional<StateValue> GetDIPSState(const GURL& url) {
// Holds since this is only called in the non-PRE test where
// DIPS is enabled (and DIPS service and storage exists);
DCHECK(storage);
absl::optional<StateValue> state;
storage->AsyncCall(&DIPSStorage::Read)
.WithArgs(url)
.Then(base::BindLambdaForTesting([&](const DIPSState& loaded_state) {
if (loaded_state.was_loaded()) {
state = loaded_state.ToStateValue();
}
}));
storage->FlushPostedTasksForTesting();
return state;
}
void FlushLossyWebsiteSettings() {
HostContentSettingsMapFactory::GetForProfile(
chrome_test_utils::GetActiveWebContents(this)->GetBrowserContext())
->FlushLossyWebsiteSettings();
}
raw_ptr<DIPSService, DanglingUntriaged> dips_service;
raw_ptr<base::SequenceBound<DIPSStorage>, DanglingUntriaged> storage;
private:
base::test::ScopedFeatureList feature_list_;
};
IN_PROC_BROWSER_TEST_F(DIPSPrepopulateTest, PRE_PrepopulateTest) {
ASSERT_EQ(dips_service, nullptr); // Verify that DIPS is off.
// Simulate the user typing the URL to visit the page, which will record site
// engagement.
ASSERT_TRUE(ui_test_utils::NavigateToURLBlockUntilNavigationsComplete(
browser(), embedded_test_server()->GetURL("c.test", "/title1.html"), 1));
FlushLossyWebsiteSettings();
}
IN_PROC_BROWSER_TEST_F(DIPSPrepopulateTest, PrepopulateTest) {
ASSERT_NE(dips_service, nullptr); // Verify that DIPS is on.
// Since there was previous site engagement, the DIPS DB should be
// prepopulated with a user interaction timestamp.
auto state = GetDIPSState(GURL("http://c.test"));
ASSERT_TRUE(state.has_value());
EXPECT_TRUE(state->user_interaction_times.has_value());
}
IN_PROC_BROWSER_TEST_F(DIPSPrepopulateTest,
PRE_PRE_PRE_PrepopulateExactlyOnce) {
ASSERT_EQ(dips_service, nullptr); // Verify that DIPS is off.
// Record site engagement on a.test.
ASSERT_TRUE(ui_test_utils::NavigateToURLBlockUntilNavigationsComplete(
browser(), embedded_test_server()->GetURL("a.test", "/title1.html"), 1));
FlushLossyWebsiteSettings();
}
IN_PROC_BROWSER_TEST_F(DIPSPrepopulateTest, PRE_PRE_PrepopulateExactlyOnce) {
// Verify that a.test is prepopulated with the earlier interaction.
auto state = GetDIPSState(GURL("http://a.test"));
ASSERT_TRUE(state.has_value());
EXPECT_TRUE(state->user_interaction_times.has_value());
}
IN_PROC_BROWSER_TEST_F(DIPSPrepopulateTest, PRE_PrepopulateExactlyOnce) {
ASSERT_EQ(dips_service, nullptr); // Verify that DIPS is off.
// Record site engagement on b.test.
ASSERT_TRUE(ui_test_utils::NavigateToURLBlockUntilNavigationsComplete(
browser(), embedded_test_server()->GetURL("b.test", "/title1.html"), 1));
FlushLossyWebsiteSettings();
}
// TODO (crbug.com/1418692): Rework this test to work without enabling and
// disabling the DIPS feature, as opening a profile with the feature disabled
// now causes any existing db files it has to be removed.
IN_PROC_BROWSER_TEST_F(DIPSPrepopulateTest, DISABLED_PrepopulateExactlyOnce) {
ASSERT_NE(dips_service, nullptr); // Verify that DIPS is on.
// Only the sites that were prepopulated the first time is in the database.
auto a_state = GetDIPSState(GURL("http://a.test"));
ASSERT_TRUE(a_state.has_value());
EXPECT_TRUE(a_state->user_interaction_times.has_value());
auto b_state = GetDIPSState(GURL("http://b.test"));
EXPECT_FALSE(b_state.has_value());
}
#endif // !BUILDFLAG(IS_ANDROID)
// Makes a long URL involving several stateful stateful bounces on b.test,
// ultimately landing on c.test. Returns both the full redirect URL and the URL
// for the landing page. The landing page URL has a param appended to it to
// ensure it's unique to URLs from previous calls (to prevent caching).
std::pair<GURL, GURL> MakeRedirectAndFinalUrl(net::EmbeddedTestServer* server) {
uint64_t unique_value = base::RandUint64();
std::string final_dest =
base::StrCat({"/title1.html?i=", base::NumberToString(unique_value)});
std::string redirect_path =
"/cross-site-with-cookie/b.test/cross-site-with-cookie/"
"b.test/cross-site-with-cookie/b.test/cross-site-with-cookie/"
"b.test/cross-site-with-cookie/b.test/cross-site-with-cookie/"
"b.test/cross-site-with-cookie/b.test/cross-site-with-cookie/"
"b.test/cross-site-with-cookie/b.test/cross-site-with-cookie/c.test";
redirect_path += final_dest;
return std::make_pair(server->GetURL("b.test", redirect_path),
server->GetURL("c.test", final_dest));
}
// Attempt to detect flakiness in waiting for DIPS storage by repeatedly
// visiting long redirect chains, deleting the relevant rows, and verifying the
// rows don't come back.
IN_PROC_BROWSER_TEST_P(DIPSTabHelperBrowserTest,
DetectRedirectHandlingFlakiness) {
WebContents* web_contents = GetActiveWebContents();
auto* dips_storage = DIPSServiceFactory::GetForBrowserContext(
web_contents->GetBrowserContext())
->storage();
for (int i = 0; i < 10; i++) {
const base::Time bounce_time = base::Time::FromDoubleT(i + 1);
SetDIPSTime(bounce_time);
LOG(INFO) << "*** i=" << i << " ***";
// Make b.test statefully bounce.
ASSERT_TRUE(content::NavigateToURL(
web_contents,
embedded_test_server()->GetURL("a.test", "/title1.html")));
auto [redirect_url, final_url] =
MakeRedirectAndFinalUrl(embedded_test_server());
ASSERT_TRUE(content::NavigateToURLFromRenderer(web_contents, redirect_url,
final_url));
// End the chain so the bounce is recorded.
EndRedirectChain();
// Verify the bounces were recorded.
absl::optional<StateValue> b_state = GetDIPSState(GURL("http://b.test"));
ASSERT_TRUE(b_state.has_value());
ASSERT_THAT(b_state->site_storage_times,
Optional(Pair(bounce_time, bounce_time)));
ASSERT_THAT(b_state->bounce_times,
Optional(Pair(bounce_time, bounce_time)));
ASSERT_THAT(b_state->stateful_bounce_times,
Optional(Pair(bounce_time, bounce_time)));
dips_storage->AsyncCall(&DIPSStorage::RemoveRows)
.WithArgs(std::vector<std::string>{"b.test"});
// Verify the row was removed before repeating the test. If we did not
// correctly wait for the whole chain to be handled before removing the row
// for b.test, it will likely be written again and this check will fail.
// (And if a write happens after this check, it will include a stale
// timestamp and will cause one the of the checks above to fail on the next
// loop iteration.)
ASSERT_FALSE(GetDIPSState(GURL("http://b.test")).has_value());
}
}
IN_PROC_BROWSER_TEST_P(DIPSTabHelperBrowserTest,
UserClearedSitesAreNotReportedToUKM) {
ukm::TestAutoSetUkmRecorder ukm_recorder;
content::WebContents* web_contents = GetActiveWebContents();
DIPSService* dips_service = DIPSServiceFactory::GetForBrowserContext(
web_contents->GetBrowserContext());
// A time more than an hour ago.
base::Time old_bounce_time = base::Time::Now() - base::Hours(2);
// A time within the past hour.
base::Time recent_bounce_time = base::Time::Now() - base::Minutes(10);
SetDIPSTime(old_bounce_time);
// Make b.test statefully bounce to c.test.
ASSERT_TRUE(content::NavigateToURL(
web_contents, embedded_test_server()->GetURL("a.test", "/title1.html")));
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")));
// End the chain so the bounce is recorded.
ASSERT_TRUE(content::NavigateToURL(
web_contents, embedded_test_server()->GetURL("a.test", "/title1.html")));
SetDIPSTime(recent_bounce_time);
// Make c.test statefully bounce to d.test.
ASSERT_TRUE(content::NavigateToURL(
web_contents, embedded_test_server()->GetURL("a.test", "/title1.html")));
ASSERT_TRUE(content::NavigateToURLFromRenderer(
web_contents,
embedded_test_server()->GetURL(
"c.test", "/cross-site-with-cookie/d.test/title1.html"),
embedded_test_server()->GetURL("d.test", "/title1.html")));
EndRedirectChain();
// Verify the bounces were recorded. b.test:
absl::optional<StateValue> state = GetDIPSState(GURL("http://b.test"));
ASSERT_TRUE(state.has_value());
ASSERT_THAT(state->stateful_bounce_times,
Optional(Pair(old_bounce_time, old_bounce_time)));
ASSERT_EQ(state->user_interaction_times, absl::nullopt);
// c.test:
state = GetDIPSState(GURL("http://c.test"));
ASSERT_TRUE(state.has_value());
ASSERT_THAT(state->stateful_bounce_times,
Optional(Pair(recent_bounce_time, recent_bounce_time)));
ASSERT_EQ(state->user_interaction_times, absl::nullopt);
// Remove browsing data for the past hour. This should include c.test but not
// b.test.
base::RunLoop run_loop;
browsing_data_important_sites_util::Remove(
chrome_browsing_data_remover::DATA_TYPE_HISTORY |
chrome_browsing_data_remover::DATA_TYPE_SITE_DATA,
content::BrowsingDataRemover::ORIGIN_TYPE_UNPROTECTED_WEB,
browsing_data::TimePeriod::LAST_HOUR,
content::BrowsingDataFilterBuilder::Create(
content::BrowsingDataFilterBuilder::Mode::kPreserve),
web_contents->GetBrowserContext()->GetBrowsingDataRemover(),
base::IgnoreArgs<uint64_t>(run_loop.QuitClosure()));
run_loop.Run();
// Verify only the DIPS record for c.test was deleted.
ASSERT_TRUE(GetDIPSState(GURL("http://b.test")).has_value());
ASSERT_FALSE(GetDIPSState(GURL("http://c.test")).has_value());
// Trigger the DIPS timer which will delete tracker data.
SetDIPSTime(recent_bounce_time + dips::kGracePeriod.Get() +
base::Milliseconds(1));
dips_service->OnTimerFiredForTesting();
dips_service->storage()->FlushPostedTasksForTesting();
base::RunLoop().RunUntilIdle();
// Verify that both DIPS records are now gone.
ASSERT_FALSE(GetDIPSState(GURL("http://b.test")).has_value());
ASSERT_FALSE(GetDIPSState(GURL("http://c.test")).has_value());
// Only b.test was reported to UKM.
EXPECT_THAT(ukm_recorder, EntryUrlsAre("DIPS.Deletion", {"http://b.test/"}));
}