blob: 6de79cbfceefbb5d751207ede5ea1fe0d7bf6383 [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "base/bind.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/simple_test_clock.h"
#include "chrome/browser/engagement/site_engagement_score.h"
#include "chrome/browser/engagement/site_engagement_service.h"
#include "chrome/browser/history/history_service_factory.h"
#include "chrome/browser/history/history_test_utils.h"
#include "chrome/browser/lookalikes/lookalike_url_interstitial_page.h"
#include "chrome/browser/lookalikes/lookalike_url_navigation_throttle.h"
#include "chrome/browser/lookalikes/lookalike_url_service.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_commands.h"
#include "chrome/common/chrome_features.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/security_interstitials/content/security_interstitial_page.h"
#include "components/security_interstitials/content/security_interstitial_tab_helper.h"
#include "components/security_interstitials/core/metrics_helper.h"
#include "components/ukm/test_ukm_recorder.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/test_navigation_observer.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/http_request.h"
#include "net/test/embedded_test_server/http_response.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "services/metrics/public/cpp/ukm_source.h"
#include "ui/base/window_open_disposition.h"
namespace {
using lookalikes::LookalikeUrlNavigationThrottle;
using lookalikes::LookalikeUrlService;
using security_interstitials::MetricsHelper;
using security_interstitials::SecurityInterstitialCommand;
using UkmEntry = ukm::builders::LookalikeUrl_NavigationSuggestion;
using NavigationSuggestionEvent =
lookalikes::LookalikeUrlNavigationThrottle::NavigationSuggestionEvent;
using MatchType = LookalikeUrlInterstitialPage::MatchType;
using UserAction = LookalikeUrlInterstitialPage::UserAction;
enum class UIStatus {
kDisabled,
kEnabledForSiteEngagement,
kEnabledForSiteEngagementAndTopDomains
};
// An engagement score above MEDIUM.
const int kHighEngagement = 20;
// An engagement score below MEDIUM.
const int kLowEngagement = 1;
// The UMA metric names registered by metrics_helper.
const char kInterstitialDecisionMetric[] = "interstitial.lookalike.decision";
const char kInterstitialInteractionMetric[] =
"interstitial.lookalike.interaction";
static std::unique_ptr<net::test_server::HttpResponse>
NetworkErrorResponseHandler(const net::test_server::HttpRequest& request) {
return std::unique_ptr<net::test_server::HttpResponse>(
new net::test_server::RawHttpResponse("", ""));
}
security_interstitials::SecurityInterstitialPage* GetCurrentInterstitial(
content::WebContents* web_contents) {
security_interstitials::SecurityInterstitialTabHelper* helper =
security_interstitials::SecurityInterstitialTabHelper::FromWebContents(
web_contents);
if (!helper) {
return nullptr;
}
return helper->GetBlockingPageForCurrentlyCommittedNavigationForTesting();
}
security_interstitials::SecurityInterstitialPage::TypeID GetInterstitialType(
content::WebContents* web_contents) {
security_interstitials::SecurityInterstitialPage* page =
GetCurrentInterstitial(web_contents);
if (!page) {
return nullptr;
}
return page->GetTypeForTesting();
}
// Sets the absolute Site Engagement |score| for the testing origin.
void SetEngagementScore(Browser* browser, const GURL& url, double score) {
SiteEngagementService::Get(browser->profile())
->ResetBaseScoreForURL(url, score);
}
bool IsUrlShowing(Browser* browser) {
return !browser->location_bar_model()->GetFormattedFullURL().empty();
}
// Simulates a link click navigation. We don't use
// ui_test_utils::NavigateToURL(const GURL&) because it simulates the user
// typing the URL, causing the site to have a site engagement score of at
// least LOW.
void NavigateToURL(Browser* browser, const GURL& url) {
NavigateParams params(browser, url, ui::PAGE_TRANSITION_LINK);
params.initiator_origin = url::Origin::Create(GURL("about:blank"));
params.disposition = WindowOpenDisposition::CURRENT_TAB;
params.is_renderer_initiated = true;
ui_test_utils::NavigateToURL(&params);
}
// Same as NavigateToUrl, but wait for the load to complete before returning.
void NavigateToURLSync(Browser* browser, const GURL& url) {
content::TestNavigationObserver navigation_observer(
browser->tab_strip_model()->GetActiveWebContents(), 1);
NavigateToURL(browser, url);
navigation_observer.Wait();
}
// Load given URL and verify that it loaded an interstitial and hid the URL.
void LoadAndCheckInterstitialAt(Browser* browser, const GURL& url) {
content::WebContents* web_contents =
browser->tab_strip_model()->GetActiveWebContents();
EXPECT_EQ(nullptr, GetCurrentInterstitial(web_contents));
NavigateToURLSync(browser, url);
EXPECT_EQ(LookalikeUrlInterstitialPage::kTypeForTesting,
GetInterstitialType(web_contents));
EXPECT_FALSE(IsUrlShowing(browser));
}
void SendInterstitialCommand(content::WebContents* web_contents,
SecurityInterstitialCommand command) {
GetCurrentInterstitial(web_contents)
->CommandReceived(base::NumberToString(command));
}
void SendInterstitialCommandSync(Browser* browser,
SecurityInterstitialCommand command) {
content::WebContents* web_contents =
browser->tab_strip_model()->GetActiveWebContents();
EXPECT_EQ(LookalikeUrlInterstitialPage::kTypeForTesting,
GetInterstitialType(web_contents));
content::TestNavigationObserver navigation_observer(web_contents, 1);
SendInterstitialCommand(web_contents, command);
navigation_observer.Wait();
EXPECT_EQ(nullptr, GetCurrentInterstitial(web_contents));
EXPECT_TRUE(IsUrlShowing(browser));
}
// Verify that no interstitial is shown, regardless of feature state.
void TestInterstitialNotShown(Browser* browser, const GURL& navigated_url) {
content::WebContents* web_contents =
browser->tab_strip_model()->GetActiveWebContents();
NavigateToURLSync(browser, navigated_url);
EXPECT_EQ(nullptr, GetCurrentInterstitial(web_contents));
// Navigate to an empty page. This will happen after any
// LookalikeUrlService tasks, so will effectively wait for those tasks to
// finish.
NavigateToURLSync(browser, GURL("about:blank"));
EXPECT_EQ(nullptr, GetCurrentInterstitial(web_contents));
}
} // namespace
class LookalikeUrlNavigationThrottleBrowserTest
: public InProcessBrowserTest,
public testing::WithParamInterface<UIStatus> {
protected:
void SetUp() override {
switch (ui_status()) {
case UIStatus::kDisabled:
feature_list_.InitAndDisableFeature(
features::kLookalikeUrlNavigationSuggestionsUI);
break;
case UIStatus::kEnabledForSiteEngagement:
feature_list_.InitAndEnableFeature(
features::kLookalikeUrlNavigationSuggestionsUI);
break;
case UIStatus::kEnabledForSiteEngagementAndTopDomains:
feature_list_.InitAndEnableFeatureWithParameters(
features::kLookalikeUrlNavigationSuggestionsUI,
{{"topsites", "true"}});
}
InProcessBrowserTest::SetUp();
}
void SetUpOnMainThread() override {
host_resolver()->AddRule("*", "127.0.0.1");
ASSERT_TRUE(embedded_test_server()->Start());
test_ukm_recorder_ = std::make_unique<ukm::TestAutoSetUkmRecorder>();
const base::Time kNow = base::Time::FromDoubleT(1000);
test_clock_.SetNow(kNow);
LookalikeUrlService* lookalike_service =
LookalikeUrlService::Get(browser()->profile());
lookalike_service->SetClockForTesting(&test_clock_);
}
GURL GetURL(const char* hostname) const {
return embedded_test_server()->GetURL(hostname, "/title1.html");
}
GURL GetURLWithoutPath(const char* hostname) const {
return GetURL(hostname).GetWithEmptyPath();
}
GURL GetLongRedirect(const char* via_hostname1,
const char* via_hostname2,
const char* dest_hostname) const {
GURL dest = GetURL(dest_hostname);
GURL mid = embedded_test_server()->GetURL(
via_hostname2, "/server-redirect?" + dest.spec());
return embedded_test_server()->GetURL(via_hostname1,
"/server-redirect?" + mid.spec());
}
// Checks that UKM recorded an event for each URL in |navigated_urls| with the
// given metric value.
template <typename T>
void CheckUkm(const std::vector<GURL>& navigated_urls,
const std::string& metric_name,
T metric_value) {
auto entries = test_ukm_recorder()->GetEntriesByName(UkmEntry::kEntryName);
ASSERT_EQ(navigated_urls.size(), entries.size());
int entry_count = 0;
for (const auto* const entry : entries) {
test_ukm_recorder()->ExpectEntrySourceHasUrl(entry,
navigated_urls[entry_count]);
test_ukm_recorder()->ExpectEntryMetric(entry, metric_name,
static_cast<int>(metric_value));
entry_count++;
}
}
// Checks that UKM did not record any lookalike URL metrics.
void CheckNoUkm() {
EXPECT_TRUE(
test_ukm_recorder()->GetEntriesByName(UkmEntry::kEntryName).empty());
}
// Returns true if the current test parameter should result in showing an
// interstitial for |expected_event|.
bool ShouldExpectInterstitial(
LookalikeUrlNavigationThrottle::NavigationSuggestionEvent expected_event)
const {
if (!ui_enabled()) {
return false;
}
if (expected_event == NavigationSuggestionEvent::kMatchSiteEngagement) {
return true;
}
if (expected_event == NavigationSuggestionEvent::kMatchTopSite &&
ui_status() == UIStatus::kEnabledForSiteEngagementAndTopDomains) {
return true;
}
return false;
}
// Tests that the histogram event |expected_event| is recorded. If the UI is
// enabled, additional events for interstitial display and link click will
// also be tested.
void TestMetricsRecordedAndMaybeInterstitialShown(
Browser* browser,
const GURL& navigated_url,
const GURL& expected_suggested_url,
LookalikeUrlNavigationThrottle::NavigationSuggestionEvent
expected_event) {
base::HistogramTester histograms;
if (!ShouldExpectInterstitial(expected_event)) {
TestInterstitialNotShown(browser, navigated_url);
histograms.ExpectTotalCount(
LookalikeUrlNavigationThrottle::kHistogramName, 1);
histograms.ExpectBucketCount(
LookalikeUrlNavigationThrottle::kHistogramName, expected_event, 1);
return;
}
history::HistoryService* const history_service =
HistoryServiceFactory::GetForProfile(
browser->profile(), ServiceAccessType::EXPLICIT_ACCESS);
ui_test_utils::WaitForHistoryToLoad(history_service);
LoadAndCheckInterstitialAt(browser, navigated_url);
SendInterstitialCommandSync(browser,
SecurityInterstitialCommand::CMD_DONT_PROCEED);
EXPECT_EQ(expected_suggested_url,
browser->tab_strip_model()->GetActiveWebContents()->GetURL());
// Clicking the link in the interstitial should also remove the original
// URL from history.
ui_test_utils::HistoryEnumerator enumerator(browser->profile());
EXPECT_FALSE(base::ContainsValue(enumerator.urls(), navigated_url));
histograms.ExpectTotalCount(LookalikeUrlNavigationThrottle::kHistogramName,
1);
histograms.ExpectBucketCount(LookalikeUrlNavigationThrottle::kHistogramName,
expected_event, 1);
histograms.ExpectTotalCount(kInterstitialDecisionMetric, 2);
histograms.ExpectBucketCount(kInterstitialDecisionMetric,
MetricsHelper::SHOW, 1);
histograms.ExpectBucketCount(kInterstitialDecisionMetric,
MetricsHelper::DONT_PROCEED, 1);
histograms.ExpectTotalCount(kInterstitialInteractionMetric, 1);
histograms.ExpectBucketCount(kInterstitialInteractionMetric,
MetricsHelper::TOTAL_VISITS, 1);
}
// Tests that the histogram event |expected_event| is recorded. If the UI is
// enabled, additional events for interstitial display and dismissal will also
// be tested.
void TestHistogramEventsRecordedWhenInterstitialIgnored(
Browser* browser,
base::HistogramTester* histograms,
const GURL& navigated_url,
LookalikeUrlNavigationThrottle::NavigationSuggestionEvent
expected_event) {
if (!ui_enabled()) {
TestInterstitialNotShown(browser, navigated_url);
histograms->ExpectTotalCount(
LookalikeUrlNavigationThrottle::kHistogramName, 1);
histograms->ExpectBucketCount(
LookalikeUrlNavigationThrottle::kHistogramName, expected_event, 1);
return;
}
history::HistoryService* const history_service =
HistoryServiceFactory::GetForProfile(
browser->profile(), ServiceAccessType::EXPLICIT_ACCESS);
ui_test_utils::WaitForHistoryToLoad(history_service);
LoadAndCheckInterstitialAt(browser, navigated_url);
// Clicking the ignore button in the interstitial should remove the
// interstitial and navigate to the original URL.
SendInterstitialCommandSync(browser,
SecurityInterstitialCommand::CMD_PROCEED);
EXPECT_EQ(navigated_url,
browser->tab_strip_model()->GetActiveWebContents()->GetURL());
// Clicking the link should cause the original URL to appear in history.
ui_test_utils::HistoryEnumerator enumerator(browser->profile());
EXPECT_TRUE(base::ContainsValue(enumerator.urls(), navigated_url));
histograms->ExpectTotalCount(LookalikeUrlNavigationThrottle::kHistogramName,
1);
histograms->ExpectBucketCount(
LookalikeUrlNavigationThrottle::kHistogramName, expected_event, 1);
histograms->ExpectTotalCount(kInterstitialDecisionMetric, 2);
histograms->ExpectBucketCount(kInterstitialDecisionMetric,
MetricsHelper::SHOW, 1);
histograms->ExpectBucketCount(kInterstitialDecisionMetric,
MetricsHelper::PROCEED, 1);
histograms->ExpectTotalCount(kInterstitialInteractionMetric, 1);
histograms->ExpectBucketCount(kInterstitialInteractionMetric,
MetricsHelper::TOTAL_VISITS, 1);
TestInterstitialNotShown(browser, navigated_url);
}
ukm::TestUkmRecorder* test_ukm_recorder() { return test_ukm_recorder_.get(); }
base::SimpleTestClock* test_clock() { return &test_clock_; }
virtual bool ui_enabled() const { return GetParam() != UIStatus::kDisabled; }
virtual UIStatus ui_status() const { return GetParam(); }
private:
base::test::ScopedFeatureList feature_list_;
std::unique_ptr<ukm::TestAutoSetUkmRecorder> test_ukm_recorder_;
base::SimpleTestClock test_clock_;
};
class LookalikeUrlInterstitialPageBrowserTest
: public LookalikeUrlNavigationThrottleBrowserTest {
protected:
bool ui_enabled() const override { return true; }
UIStatus ui_status() const override {
return UIStatus::kEnabledForSiteEngagementAndTopDomains;
}
};
INSTANTIATE_TEST_SUITE_P(
,
LookalikeUrlNavigationThrottleBrowserTest,
::testing::Values(UIStatus::kDisabled,
UIStatus::kEnabledForSiteEngagement,
UIStatus::kEnabledForSiteEngagementAndTopDomains));
// Navigating to a non-IDN shouldn't show an interstitial or record metrics.
IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest,
NonIdn_NoMatch) {
TestInterstitialNotShown(browser(), GetURL("google.com"));
CheckNoUkm();
}
// Navigating to a domain whose visual representation does not look like a
// top domain shouldn't show an interstitial or record metrics.
IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest,
NonTopDomainIdn_NoInterstitial) {
TestInterstitialNotShown(browser(), GetURL("éxample.com"));
CheckNoUkm();
}
// If the user has engaged with the domain before, metrics shouldn't be recorded
// and the interstitial shouldn't be shown, even if the domain is visually
// similar to a top domain.
IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest,
Idn_TopDomain_EngagedSite_NoMatch) {
const GURL url = GetURL("googlé.com");
SetEngagementScore(browser(), url, kHighEngagement);
TestInterstitialNotShown(browser(), url);
CheckNoUkm();
}
// Navigate to a domain whose visual representation looks like a top domain.
// This should record metrics. It should also show a lookalike warning
// interstitial if configured via a feature param.
IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest,
Idn_TopDomain_Match) {
const GURL kNavigatedUrl = GetURL("googlé.com");
const GURL kExpectedSuggestedUrl = GetURLWithoutPath("google.com");
// Even if the navigated site has a low engagement score, it should be
// considered for lookalike suggestions.
SetEngagementScore(browser(), kNavigatedUrl, kLowEngagement);
TestMetricsRecordedAndMaybeInterstitialShown(
browser(), kNavigatedUrl, kExpectedSuggestedUrl,
NavigationSuggestionEvent::kMatchTopSite);
CheckUkm({kNavigatedUrl}, "MatchType", MatchType::kTopSite);
}
// Same as Idn_TopDomain_Match, but this time the domain contains characters
// from different scripts, failing the checks in IDN spoof checker before
// reaching the top domain check. In this case, the end result is the same, but
// the reason we fall back to punycode is different.
IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest,
Idn_TopDomainMixedScript_Match) {
const GURL kNavigatedUrl = GetURL("аррӏе.com");
const GURL kExpectedSuggestedUrl = GetURLWithoutPath("apple.com");
// Even if the navigated site has a low engagement score, it should be
// considered for lookalike suggestions.
SetEngagementScore(browser(), kNavigatedUrl, kLowEngagement);
TestMetricsRecordedAndMaybeInterstitialShown(
browser(), kNavigatedUrl, kExpectedSuggestedUrl,
NavigationSuggestionEvent::kMatchTopSite);
CheckUkm({kNavigatedUrl}, "MatchType", MatchType::kTopSite);
}
// The navigated domain will fall back to punycode because it fails spoof checks
// in IDN spoof checker, but there won't be an interstitial because the domain
// doesn't match a top domain.
IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest,
Punycode_NoMatch) {
TestInterstitialNotShown(browser(), GetURL("ɴoτ-τoρ-ďoᛖaiɴ.com"));
CheckNoUkm();
}
// The navigated domain itself is a top domain or a subdomain of a top domain.
// Should not record metrics. The top domain list doesn't contain any IDN, so
// this only tests the case where the subdomains are IDNs.
IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest,
TopDomainIdnSubdomain_NoMatch) {
TestInterstitialNotShown(browser(), GetURL("tést.google.com"));
CheckNoUkm();
// blogspot.com is a private registry, so the eTLD+1 of "tést.blogspot.com" is
// itself, instead of just "blogspot.com". This is different than
// tést.google.com whose eTLD+1 is google.com, and it should be handled
// correctly.
TestInterstitialNotShown(browser(), GetURL("tést.blogspot.com"));
CheckNoUkm();
}
// Schemes other than HTTP and HTTPS should be ignored.
IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest,
TopDomainChromeUrl_NoMatch) {
TestInterstitialNotShown(browser(), GURL("chrome://googlé.com"));
CheckNoUkm();
}
// Navigate to a domain within an edit distance of 1 to an engaged domain.
// This should record metrics, but should not show a lookalike warning
// interstitial yet.
IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest,
EditDistance_EngagedDomain_Match) {
base::HistogramTester histograms;
SetEngagementScore(browser(), GURL("https://test-site.com"), kHighEngagement);
// The skeleton of this domain is one 1 edit away from the skeleton of
// test-site.com.
const GURL kNavigatedUrl = GetURL("best-sité.com");
// Even if the navigated site has a low engagement score, it should be
// considered for lookalike suggestions.
SetEngagementScore(browser(), kNavigatedUrl, kLowEngagement);
// Advance clock to force a fetch of new engaged sites list.
test_clock()->Advance(base::TimeDelta::FromHours(1));
TestInterstitialNotShown(browser(), kNavigatedUrl);
histograms.ExpectTotalCount(LookalikeUrlNavigationThrottle::kHistogramName,
1);
histograms.ExpectBucketCount(
LookalikeUrlNavigationThrottle::kHistogramName,
NavigationSuggestionEvent::kMatchEditDistanceSiteEngagement, 1);
CheckUkm({kNavigatedUrl}, "MatchType",
MatchType::kEditDistanceSiteEngagement);
}
// Navigate to a domain within an edit distance of 1 to a top domain.
// This should record metrics, but should not show a lookalike warning
// interstitial yet.
IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest,
EditDistance_TopDomain_Match) {
base::HistogramTester histograms;
// The skeleton of this domain, gooogle.corn, is one 1 edit away from
// google.corn, the skeleton of google.com.
const GURL kNavigatedUrl = GetURL("goooglé.com");
// Even if the navigated site has a low engagement score, it should be
// considered for lookalike suggestions.
SetEngagementScore(browser(), kNavigatedUrl, kLowEngagement);
TestInterstitialNotShown(browser(), kNavigatedUrl);
histograms.ExpectTotalCount(LookalikeUrlNavigationThrottle::kHistogramName,
1);
histograms.ExpectBucketCount(LookalikeUrlNavigationThrottle::kHistogramName,
NavigationSuggestionEvent::kMatchEditDistance,
1);
CheckUkm({kNavigatedUrl}, "MatchType", MatchType::kEditDistance);
}
// Tests negative examples for the edit distance.
IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest,
EditDistance_TopDomain_NoMatch) {
// Matches google.com.tr but only differs in registry.
TestInterstitialNotShown(browser(), GetURL("google.com.tw"));
CheckNoUkm();
// Matches bing.com but is a top domain itself.
TestInterstitialNotShown(browser(), GetURL("ning.com"));
CheckNoUkm();
// Matches ask.com but is too short.
TestInterstitialNotShown(browser(), GetURL("bsk.com"));
CheckNoUkm();
}
// Tests negative examples for the edit distance with engaged sites.
IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest,
EditDistance_SiteEngagement_NoMatch) {
SetEngagementScore(browser(), GURL("https://test-site.com.tr"),
kHighEngagement);
SetEngagementScore(browser(), GURL("https://1234.com"), kHighEngagement);
SetEngagementScore(browser(), GURL("https://gooogle.com"), kHighEngagement);
// Advance clock to force a fetch of new engaged sites list.
test_clock()->Advance(base::TimeDelta::FromHours(1));
// Matches test-site.com.tr but only differs in registry.
TestInterstitialNotShown(browser(), GetURL("test-site.com.tw"));
CheckNoUkm();
// Matches gooogle.com but is a top domain itself.
TestInterstitialNotShown(browser(), GetURL("google.com"));
CheckNoUkm();
// Matches 1234.com but is too short.
TestInterstitialNotShown(browser(), GetURL("123.com"));
CheckNoUkm();
}
// Test that the heuristics are triggered even with net errors.
IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest,
NetError_SiteEngagement_Interstitial) {
// Create a test server that returns invalid responses.
net::EmbeddedTestServer custom_test_server;
custom_test_server.RegisterRequestHandler(
base::BindRepeating(&NetworkErrorResponseHandler));
ASSERT_TRUE(custom_test_server.Start());
SetEngagementScore(browser(), GURL("http://site1.com"), kHighEngagement);
// Advance clock to force a fetch of new engaged sites list.
test_clock()->Advance(base::TimeDelta::FromHours(1));
TestMetricsRecordedAndMaybeInterstitialShown(
browser(), custom_test_server.GetURL("sité1.com", "/title1.html"),
custom_test_server.GetURL("site1.com", "/"),
NavigationSuggestionEvent::kMatchSiteEngagement);
}
// Same as NetError_SiteEngagement_Interstitial, but triggered by a top domain.
IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest,
NetError_TopDomain_Interstitial) {
// Create a test server that returns invalid responses.
net::EmbeddedTestServer custom_test_server;
custom_test_server.RegisterRequestHandler(
base::BindRepeating(&NetworkErrorResponseHandler));
ASSERT_TRUE(custom_test_server.Start());
TestMetricsRecordedAndMaybeInterstitialShown(
browser(), GetURL("googlé.com"), GetURLWithoutPath("google.com"),
NavigationSuggestionEvent::kMatchTopSite);
}
// Verify that, after dismissing a lookalike warning when enabled, the user
// sees a net error when applicable.
IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest,
NetError_SiteEngagement_NetErrorAfterDismiss) {
// Create a test server that returns invalid responses.
net::EmbeddedTestServer custom_test_server;
custom_test_server.RegisterRequestHandler(
base::BindRepeating(&NetworkErrorResponseHandler));
ASSERT_TRUE(custom_test_server.Start());
SetEngagementScore(browser(), GURL("http://site1.com"), kHighEngagement);
// Advance clock to force a fetch of new engaged sites list.
test_clock()->Advance(base::TimeDelta::FromHours(1));
NavigateToURLSync(browser(),
custom_test_server.GetURL("sité1.com", "/title1.html"));
if (ui_enabled()) {
SendInterstitialCommandSync(browser(),
SecurityInterstitialCommand::CMD_PROCEED);
}
EXPECT_GE(ui_test_utils::FindInPage(
browser()->tab_strip_model()->GetActiveWebContents(),
base::ASCIIToUTF16("ERR_EMPTY_RESPONSE"), true, true, nullptr,
nullptr),
1);
}
// Same as NetError_SiteEngagement_NetErrorAfterDismiss, but navigates to a top
// domain instead.
IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest,
NetError_TopDomain_NetErrorAfterDismiss) {
// Create a test server that returns invalid responses.
net::EmbeddedTestServer custom_test_server;
custom_test_server.RegisterRequestHandler(
base::BindRepeating(&NetworkErrorResponseHandler));
ASSERT_TRUE(custom_test_server.Start());
NavigateToURLSync(browser(),
custom_test_server.GetURL("googlé.com", "/title1.html"));
if (ShouldExpectInterstitial(NavigationSuggestionEvent::kMatchTopSite)) {
SendInterstitialCommandSync(browser(),
SecurityInterstitialCommand::CMD_PROCEED);
}
EXPECT_GE(ui_test_utils::FindInPage(
browser()->tab_strip_model()->GetActiveWebContents(),
base::ASCIIToUTF16("ERR_EMPTY_RESPONSE"), true, true, nullptr,
nullptr),
1);
}
// Navigate to a domain whose visual representation looks like a domain with a
// site engagement score above a certain threshold. This should record metrics.
// It should also show lookalike warning interstitial if configured via
// a feature param.
IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest,
Idn_SiteEngagement_Match) {
const char* const kEngagedSites[] = {
"http://site1.com", "http://www.site2.com", "http://sité3.com",
"http://www.sité4.com"};
for (const char* const kSite : kEngagedSites) {
SetEngagementScore(browser(), GURL(kSite), kHighEngagement);
}
// The domains here should not be private domains (e.g. site.test), otherwise
// they might test the wrong thing. Also note that site5.com is in the top
// domain list, so it shouldn't be used here.
const struct SiteEngagementTestCase {
const char* const navigated;
const char* const suggested;
} kSiteEngagementTestCases[] = {
{"sité1.com", "site1.com"},
{"mail.www.sité1.com", "site1.com"},
// Same as above two but ending with dots.
{"sité1.com.", "site1.com"},
{"mail.www.sité1.com.", "site1.com"},
// These should match since the comparison uses eTLD+1s.
{"sité2.com", "site2.com"},
{"mail.sité2.com", "site2.com"},
{"síté3.com", "sité3.com"},
{"mail.síté3.com", "sité3.com"},
{"síté4.com", "sité4.com"},
{"mail.síté4.com", "sité4.com"},
};
std::vector<GURL> ukm_urls;
for (const auto& test_case : kSiteEngagementTestCases) {
const GURL kNavigatedUrl = GetURL(test_case.navigated);
const GURL kExpectedSuggestedUrl = GetURLWithoutPath(test_case.suggested);
// Even if the navigated site has a low engagement score, it should be
// considered for lookalike suggestions.
SetEngagementScore(browser(), kNavigatedUrl, kLowEngagement);
// Advance the clock to force LookalikeUrlService to fetch a new engaged
// site list.
test_clock()->Advance(base::TimeDelta::FromHours(1));
TestMetricsRecordedAndMaybeInterstitialShown(
browser(), kNavigatedUrl, kExpectedSuggestedUrl,
NavigationSuggestionEvent::kMatchSiteEngagement);
ukm_urls.push_back(kNavigatedUrl);
CheckUkm(ukm_urls, "MatchType", MatchType::kSiteEngagement);
}
}
// Tests negative examples for all heuristics.
IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest,
NonUniqueDomains_NoMatch) {
// Unknown registry.
TestInterstitialNotShown(browser(), GetURL("google.cóm"));
CheckNoUkm();
// Engaged site is localhost, navigated site has unknown registry. This
// is intended to test that nonunique domains in the engaged site list is
// filtered out. However, it doesn't quite test that: We'll bail out early
// because the navigated site has unknown registry (and not because there is
// no engaged nonunique site).
SetEngagementScore(browser(), GURL("http://localhost6.localhost"),
kHighEngagement);
test_clock()->Advance(base::TimeDelta::FromHours(1));
// The skeleton of this URL is localhost6.localpost which is at one edit
// distance from localhost6.localhost. We use localpost here to prevent an
// early return in LookalikeUrlNavigationThrottle::HandleThrottleRequest().
TestInterstitialNotShown(browser(), GURL("http://localhóst6.localpost"));
CheckNoUkm();
}
// Navigate to a domain whose visual representation looks both like a domain
// with a site engagement score and also a top domain. This should record
// metrics for a site engagement match because of the order of checks. It should
// also show lookalike warning interstitial if configured via a feature param.
IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest,
Idn_SiteEngagementAndTopDomain_Match) {
const GURL kNavigatedUrl = GetURL("googlé.com");
const GURL kExpectedSuggestedUrl = GetURLWithoutPath("google.com");
SetEngagementScore(browser(), kNavigatedUrl, kLowEngagement);
SetEngagementScore(browser(), kExpectedSuggestedUrl, kHighEngagement);
// Advance the clock to force LookalikeUrlService to fetch a new engaged
// site list.
test_clock()->Advance(base::TimeDelta::FromHours(1));
TestMetricsRecordedAndMaybeInterstitialShown(
browser(), kNavigatedUrl, kExpectedSuggestedUrl,
NavigationSuggestionEvent::kMatchSiteEngagement);
CheckUkm({kNavigatedUrl}, "MatchType", MatchType::kSiteEngagement);
}
// Similar to Idn_SiteEngagement_Match, but tests a single domain. Also checks
// that the list of engaged sites in incognito and the main profile don't affect
// each other.
IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest,
Idn_SiteEngagement_Match_Incognito) {
const GURL kNavigatedUrl = GetURL("sité1.com");
const GURL kEngagedUrl = GetURLWithoutPath("site1.com");
// Set high engagement scores in the main profile and low engagement scores
// in incognito. Main profile should record metrics, incognito shouldn't.
Browser* incognito = CreateIncognitoBrowser();
LookalikeUrlService::Get(incognito->profile())
->SetClockForTesting(test_clock());
SetEngagementScore(browser(), kEngagedUrl, kHighEngagement);
SetEngagementScore(incognito, kEngagedUrl, kLowEngagement);
std::vector<GURL> ukm_urls;
// Main profile should record metrics because there are engaged sites.
{
// Advance the clock to force LookalikeUrlService to fetch a new engaged
// site list.
test_clock()->Advance(base::TimeDelta::FromHours(1));
TestMetricsRecordedAndMaybeInterstitialShown(
browser(), kNavigatedUrl, kEngagedUrl,
NavigationSuggestionEvent::kMatchSiteEngagement);
ukm_urls.push_back(kNavigatedUrl);
CheckUkm(ukm_urls, "MatchType", MatchType::kSiteEngagement);
}
// Incognito shouldn't record metrics because there are no engaged sites.
{
base::HistogramTester histograms;
test_clock()->Advance(base::TimeDelta::FromHours(1));
TestInterstitialNotShown(incognito, kNavigatedUrl);
histograms.ExpectTotalCount(LookalikeUrlNavigationThrottle::kHistogramName,
0);
}
// Now reverse the scores: Set low engagement in the main profile and high
// engagement in incognito.
SetEngagementScore(browser(), kEngagedUrl, kLowEngagement);
SetEngagementScore(incognito, kEngagedUrl, kHighEngagement);
// Incognito should start recording metrics and main profile should stop.
{
test_clock()->Advance(base::TimeDelta::FromHours(1));
TestMetricsRecordedAndMaybeInterstitialShown(
incognito, kNavigatedUrl, kEngagedUrl,
NavigationSuggestionEvent::kMatchSiteEngagement);
ukm_urls.push_back(kNavigatedUrl);
CheckUkm(ukm_urls, "MatchType", MatchType::kSiteEngagement);
}
// Main profile shouldn't record metrics because there are no engaged sites.
{
base::HistogramTester histograms;
test_clock()->Advance(base::TimeDelta::FromHours(1));
TestInterstitialNotShown(browser(), kNavigatedUrl);
histograms.ExpectTotalCount(LookalikeUrlNavigationThrottle::kHistogramName,
0);
}
}
// Test that navigations to a site with a high engagement score shouldn't
// record metrics or show interstitial.
IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest,
Idn_SiteEngagement_Match_IgnoreHighlyEngagedSite) {
base::HistogramTester histograms;
SetEngagementScore(browser(), GURL("http://site-not-in-top-domain-list.com"),
kHighEngagement);
const GURL high_engagement_url = GetURL("síte-not-ín-top-domaín-líst.com");
SetEngagementScore(browser(), high_engagement_url, kHighEngagement);
TestInterstitialNotShown(browser(), high_engagement_url);
histograms.ExpectTotalCount(LookalikeUrlNavigationThrottle::kHistogramName,
0);
}
// Test that an engaged site with a scheme other than HTTP or HTTPS should be
// ignored.
IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest,
Idn_SiteEngagement_IgnoreChromeUrl) {
base::HistogramTester histograms;
SetEngagementScore(browser(),
GURL("chrome://site-not-in-top-domain-list.com"),
kHighEngagement);
const GURL low_engagement_url("http://síte-not-ín-top-domaín-líst.com");
SetEngagementScore(browser(), low_engagement_url, kLowEngagement);
TestInterstitialNotShown(browser(), low_engagement_url);
histograms.ExpectTotalCount(LookalikeUrlNavigationThrottle::kHistogramName,
0);
}
// IDNs with a single label should be properly handled. There are two cases
// where this might occur:
// 1. The navigated URL is an IDN with a single label.
// 2. One of the engaged sites is an IDN with a single label.
// Neither of these should cause a crash.
IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest,
IdnWithSingleLabelShouldNotCauseACrash) {
base::HistogramTester histograms;
// Case 1: Navigating to an IDN with a single label shouldn't cause a crash.
TestInterstitialNotShown(browser(), GetURL("é"));
// Case 2: An IDN with a single label with a site engagement score shouldn't
// cause a crash.
SetEngagementScore(browser(), GURL("http://tést"), kHighEngagement);
TestInterstitialNotShown(browser(), GetURL("tést.com"));
histograms.ExpectTotalCount(LookalikeUrlNavigationThrottle::kHistogramName,
0);
CheckNoUkm();
}
// Ensure that dismissing the interstitial works, and the result is remembered
// in the current tab. This should record metrics on the first visit, but not
// the second.
IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest,
Interstitial_Dismiss) {
base::HistogramTester histograms;
const GURL kNavigatedUrl = GetURL("sité1.com");
const GURL kEngagedUrl = GetURL("site1.com");
SetEngagementScore(browser(), kNavigatedUrl, kLowEngagement);
SetEngagementScore(browser(), kEngagedUrl, kHighEngagement);
TestHistogramEventsRecordedWhenInterstitialIgnored(
browser(), &histograms, kNavigatedUrl,
NavigationSuggestionEvent::kMatchSiteEngagement);
CheckUkm({kNavigatedUrl}, "MatchType", MatchType::kSiteEngagement);
}
// Navigate to lookalike domains that redirect to benign domains and ensure that
// we display an interstitial along the way.
IN_PROC_BROWSER_TEST_F(LookalikeUrlInterstitialPageBrowserTest,
Interstitial_CapturesRedirects) {
{
// Verify it works when the lookalike domain is the first in the chain
const GURL kNavigatedUrl =
GetLongRedirect("googlé.com", "example.net", "example.com");
SetEngagementScore(browser(), kNavigatedUrl, kLowEngagement);
LoadAndCheckInterstitialAt(browser(), kNavigatedUrl);
}
// LoadAndCheckInterstitialAt assumes there's not an interstitial already
// showing (since otherwise it can't be sure that the navigation caused it).
NavigateToURLSync(browser(), GetURL("example.com"));
{
// ...or when it's later in the chain
const GURL kNavigatedUrl =
GetLongRedirect("example.net", "googlé.com", "example.com");
SetEngagementScore(browser(), kNavigatedUrl, kLowEngagement);
LoadAndCheckInterstitialAt(browser(), kNavigatedUrl);
}
NavigateToURLSync(browser(), GetURL("example.com"));
{
// ...or when it's last in the chain
const GURL kNavigatedUrl =
GetLongRedirect("example.net", "example.com", "googlé.com");
SetEngagementScore(browser(), kNavigatedUrl, kLowEngagement);
LoadAndCheckInterstitialAt(browser(), kNavigatedUrl);
}
}
// Verify that the user action in UKM is recorded properly when the interstitial
// is not shown.
IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest,
UkmRecordedWhenNoInterstitialShown) {
// UI tests are handled explicitly below.
if (ui_enabled())
return;
const GURL navigated_url = GetURL("googlé.com");
TestInterstitialNotShown(browser(), navigated_url);
CheckUkm({navigated_url}, "UserAction", UserAction::kInterstitialNotShown);
}
// Verify that the user action in UKM is recorded even when we navigate away
// from the interstitial without interacting with it.
IN_PROC_BROWSER_TEST_F(LookalikeUrlInterstitialPageBrowserTest,
UkmRecordedAfterNavigateAway) {
const GURL navigated_url = GetURL("googlé.com");
const GURL subsequent_url = GetURL("example.com");
LoadAndCheckInterstitialAt(browser(), navigated_url);
NavigateToURLSync(browser(), subsequent_url);
CheckUkm({navigated_url}, "UserAction", UserAction::kCloseOrBack);
}
// Verify that the user action in UKM is recorded properly when the user accepts
// the navigation suggestion.
IN_PROC_BROWSER_TEST_F(LookalikeUrlInterstitialPageBrowserTest,
UkmRecordedAfterSuggestionAccepted) {
const GURL navigated_url = GetURL("googlé.com");
LoadAndCheckInterstitialAt(browser(), navigated_url);
SendInterstitialCommandSync(browser(),
SecurityInterstitialCommand::CMD_DONT_PROCEED);
CheckUkm({navigated_url}, "UserAction", UserAction::kAcceptSuggestion);
}
// Verify that the user action in UKM is recorded properly when the user ignores
// the navigation suggestion.
IN_PROC_BROWSER_TEST_F(LookalikeUrlInterstitialPageBrowserTest,
UkmRecordedAfterSuggestionIgnored) {
const GURL navigated_url = GetURL("googlé.com");
LoadAndCheckInterstitialAt(browser(), navigated_url);
SendInterstitialCommandSync(browser(),
SecurityInterstitialCommand::CMD_PROCEED);
CheckUkm({navigated_url}, "UserAction", UserAction::kClickThrough);
}
// Verify that the URL shows normally on pages after a lookalike interstitial.
IN_PROC_BROWSER_TEST_F(LookalikeUrlInterstitialPageBrowserTest,
UrlShownAfterInterstitial) {
LoadAndCheckInterstitialAt(browser(), GetURL("googlé.com"));
// URL should be showing again when we navigate to a normal URL
NavigateToURLSync(browser(), GetURL("example.com"));
EXPECT_TRUE(IsUrlShowing(browser()));
}
// Verify that bypassing warnings in the main profile does not affect incognito.
IN_PROC_BROWSER_TEST_F(LookalikeUrlInterstitialPageBrowserTest,
MainProfileDoesNotAffectIncognito) {
const GURL kNavigatedUrl = GetURL("googlé.com");
// Set low engagement scores in the main profile and in incognito.
Browser* incognito = CreateIncognitoBrowser();
SetEngagementScore(browser(), kNavigatedUrl, kLowEngagement);
SetEngagementScore(incognito, kNavigatedUrl, kLowEngagement);
LoadAndCheckInterstitialAt(browser(), kNavigatedUrl);
// PROCEEDing will disable the interstitial on subsequent navigations
SendInterstitialCommandSync(browser(),
SecurityInterstitialCommand::CMD_PROCEED);
LoadAndCheckInterstitialAt(incognito, kNavigatedUrl);
}
// Verify that bypassing warnings in incognito does not affect the main profile.
IN_PROC_BROWSER_TEST_F(LookalikeUrlInterstitialPageBrowserTest,
IncognitoDoesNotAffectMainProfile) {
const GURL kNavigatedUrl = GetURL("sité1.com");
const GURL kEngagedUrl = GetURL("site1.com");
// Set engagement scores in the main profile and in incognito.
Browser* incognito = CreateIncognitoBrowser();
SetEngagementScore(browser(), kNavigatedUrl, kLowEngagement);
SetEngagementScore(incognito, kNavigatedUrl, kLowEngagement);
SetEngagementScore(browser(), kEngagedUrl, kHighEngagement);
SetEngagementScore(incognito, kEngagedUrl, kHighEngagement);
LoadAndCheckInterstitialAt(incognito, kNavigatedUrl);
// PROCEEDing will disable the interstitial on subsequent navigations
SendInterstitialCommandSync(incognito,
SecurityInterstitialCommand::CMD_PROCEED);
LoadAndCheckInterstitialAt(browser(), kNavigatedUrl);
}
// Verify reloading the page does not result in dismissing an interstitial.
// Regression test for crbug/941886.
IN_PROC_BROWSER_TEST_F(LookalikeUrlInterstitialPageBrowserTest,
RefreshDoesntDismiss) {
// Verify it works when the lookalike domain is the first in the chain
const GURL kNavigatedUrl =
GetLongRedirect("googlé.com", "example.net", "example.com");
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
SetEngagementScore(browser(), kNavigatedUrl, kLowEngagement);
LoadAndCheckInterstitialAt(browser(), kNavigatedUrl);
content::TestNavigationObserver navigation_observer(web_contents);
chrome::Reload(browser(), WindowOpenDisposition::CURRENT_TAB);
navigation_observer.Wait();
EXPECT_EQ(LookalikeUrlInterstitialPage::kTypeForTesting,
GetInterstitialType(web_contents));
EXPECT_FALSE(IsUrlShowing(browser()));
}