blob: 678a0e23074e3e0423a0abfb13639efe3428b0eb [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/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/infobars/infobar_observer.h"
#include "chrome/browser/infobars/infobar_service.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_commands.h"
#include "chrome/browser/ui/omnibox/alternate_nav_infobar_delegate.h"
#include "chrome/browser/ui/omnibox/lookalike_url_navigation_observer.h"
#include "chrome/browser/ui/omnibox/lookalike_url_service.h"
#include "chrome/browser/ui/tabs/tab_strip_model.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/infobars/core/infobar.h"
#include "components/infobars/core/infobar_delegate.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 UkmEntry = ukm::builders::LookalikeUrl_NavigationSuggestion;
using NavigationSuggestionEvent =
LookalikeUrlNavigationObserver::NavigationSuggestionEvent;
enum class UIEnabled { kDisabled, kEnabled };
// An engagement score above MEDIUM.
const int kHighEngagement = 20;
// An engagement score below MEDIUM.
const int kLowEngagement = 1;
// The domains here should not 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.
struct SiteEngagementTestCase {
const char* const navigated;
const char* const suggested;
} kSiteEngagementTestCases[] = {
{"sité1.com", "site1.com"},
{"mail.www.sité1.com", "site1.com"},
// These should match since the comparison uses eTLD+1s.
{"sité2.com", "www.site2.com"},
{"mail.sité2.com", "www.site2.com"},
{"síté3.com", "sité3.com"},
{"mail.síté3.com", "sité3.com"},
{"síté4.com", "www.sité4.com"},
{"mail.síté4.com", "www.sité4.com"},
};
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("", ""));
}
} // namespace
class LookalikeUrlNavigationObserverBrowserTest
: public InProcessBrowserTest,
public testing::WithParamInterface<UIEnabled> {
protected:
// Sets the absolute Site Engagement |score| for the testing origin.
static void SetEngagementScore(Browser* browser,
const GURL& url,
double score) {
SiteEngagementService::Get(browser->profile())
->ResetBaseScoreForURL(url, score);
}
void SetUp() override {
if (ui_enabled()) {
feature_list_.InitAndEnableFeature(
features::kLookalikeUrlNavigationSuggestionsUI);
} else {
feature_list_.InitAndDisableFeature(
features::kLookalikeUrlNavigationSuggestionsUI);
}
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_);
lookalike_service->ClearEngagedSitesForTesting();
}
GURL GetURL(const char* hostname) const {
return embedded_test_server()->GetURL(hostname, "/title1.html");
}
// Checks that UKM recorded a metric for each URL in |navigated_urls|.
void CheckUkm(const std::vector<GURL>& navigated_urls,
LookalikeUrlNavigationObserver::MatchType match_type) {
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, "MatchType",
static_cast<int>(match_type));
entry_count++;
}
}
// Checks that UKM did not record any lookalike URL metrics.
void CheckNoUkm() {
EXPECT_TRUE(
test_ukm_recorder()->GetEntriesByName(UkmEntry::kEntryName).empty());
}
void TestInfobarNotShown(Browser* browser, const GURL& navigated_url) {
content::WebContents* web_contents =
browser->tab_strip_model()->GetActiveWebContents();
InfoBarService* infobar_service =
InfoBarService::FromWebContents(web_contents);
{
content::TestNavigationObserver navigation_observer(web_contents, 1);
NavigateToURL(browser, navigated_url);
navigation_observer.Wait();
EXPECT_EQ(0u, infobar_service->infobar_count());
}
{
// Navigate to an empty page. This will happen after any
// LookalikeUrlService tasks, so will effectively wait for those tasks to
// finish.
content::TestNavigationObserver navigation_observer(web_contents, 1);
NavigateToURL(browser, GURL("about:blank"));
navigation_observer.Wait();
EXPECT_EQ(0u, infobar_service->infobar_count());
}
}
// Tests that the histogram event |expected_event| is recorded. If the UI is
// enabled, additinal events for infobar display and link click will also be
// tested.
void TestHistogramEventsRecordedAndInfobarVisibility(
Browser* browser,
base::HistogramTester* histograms,
const GURL& navigated_url,
const GURL& expected_suggested_url,
LookalikeUrlNavigationObserver::NavigationSuggestionEvent
expected_event) {
if (ui_enabled()) {
// If the feature is enabled, the UI will be displayed. Expect extra
// histogram entries for kInfobarShown and kLinkClicked events.
TestInfobarShown(browser, navigated_url, expected_suggested_url);
histograms->ExpectTotalCount(
LookalikeUrlNavigationObserver::kHistogramName, 3);
histograms->ExpectBucketCount(
LookalikeUrlNavigationObserver::kHistogramName,
LookalikeUrlNavigationObserver::NavigationSuggestionEvent::
kInfobarShown,
1);
histograms->ExpectBucketCount(
LookalikeUrlNavigationObserver::kHistogramName,
LookalikeUrlNavigationObserver::NavigationSuggestionEvent::
kLinkClicked,
1);
histograms->ExpectBucketCount(
LookalikeUrlNavigationObserver::kHistogramName, expected_event, 1);
return;
}
TestInfobarNotShown(browser, navigated_url);
histograms->ExpectTotalCount(LookalikeUrlNavigationObserver::kHistogramName,
1);
histograms->ExpectBucketCount(
LookalikeUrlNavigationObserver::kHistogramName, expected_event, 1);
}
ukm::TestUkmRecorder* test_ukm_recorder() { return test_ukm_recorder_.get(); }
base::SimpleTestClock* test_clock() { return &test_clock_; }
private:
bool ui_enabled() const { return GetParam() == UIEnabled::kEnabled; }
// 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.
static 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);
}
// Checks that navigating to |navigated_url| results in displaying a
// navigation suggesting that says "Did you mean to go to
// |expected_suggested_url|?". Both |navigated_url| and
// |expected_suggested_url| can be ASCII or IDN.
static void TestInfobarShown(Browser* browser,
const GURL& navigated_url,
const GURL& expected_suggested_url) {
history::HistoryService* const history_service =
HistoryServiceFactory::GetForProfile(
browser->profile(), ServiceAccessType::EXPLICIT_ACCESS);
ui_test_utils::WaitForHistoryToLoad(history_service);
content::WebContents* web_contents =
browser->tab_strip_model()->GetActiveWebContents();
InfoBarService* infobar_service =
InfoBarService::FromWebContents(web_contents);
InfoBarObserver infobar_added_observer(
infobar_service, InfoBarObserver::Type::kInfoBarAdded);
NavigateToURL(browser, navigated_url);
infobar_added_observer.Wait();
infobars::InfoBar* infobar = infobar_service->infobar_at(0);
EXPECT_EQ(infobars::InfoBarDelegate::ALTERNATE_NAV_INFOBAR_DELEGATE,
infobar->delegate()->GetIdentifier());
// Clicking the link in the infobar should remove the infobar and navigate
// to the suggested URL.
InfoBarObserver infobar_removed_observer(
infobar_service, InfoBarObserver::Type::kInfoBarRemoved);
AlternateNavInfoBarDelegate* infobar_delegate =
static_cast<AlternateNavInfoBarDelegate*>(infobar->delegate());
infobar_delegate->LinkClicked(WindowOpenDisposition::CURRENT_TAB);
infobar_removed_observer.Wait();
EXPECT_EQ(0u, infobar_service->infobar_count());
EXPECT_EQ(expected_suggested_url, web_contents->GetURL());
// Clicking the link in the infobar should also remove the original URL from
// history.
ui_test_utils::HistoryEnumerator enumerator(browser->profile());
EXPECT_FALSE(base::ContainsValue(enumerator.urls(), navigated_url));
}
base::test::ScopedFeatureList feature_list_;
std::unique_ptr<ukm::TestAutoSetUkmRecorder> test_ukm_recorder_;
base::SimpleTestClock test_clock_;
};
INSTANTIATE_TEST_CASE_P(,
LookalikeUrlNavigationObserverBrowserTest,
::testing::Values(UIEnabled::kDisabled,
UIEnabled::kEnabled));
// Navigating to a non-IDN shouldn't show an infobar or record metrics.
IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationObserverBrowserTest,
NonIdn_NoMatch) {
TestInfobarNotShown(browser(), GetURL("google.com"));
CheckNoUkm();
}
// Navigating to a domain whose visual representation does not look like a
// top domain shouldn't show an infobar or record metrics.
IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationObserverBrowserTest,
NonTopDomainIdn_NoInfobar) {
TestInfobarNotShown(browser(), GetURL("éxample.com"));
CheckNoUkm();
}
// If the user has engaged with the domain before, metrics shouldn't be recorded
// and the infobar shouldn't be shown, even if the domain is visually similar
// to a top domain.
IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationObserverBrowserTest,
Idn_TopDomain_EngagedSite_NoMatch) {
const GURL url = GetURL("googlé.com");
SetEngagementScore(browser(), url, kHighEngagement);
TestInfobarNotShown(browser(), url);
CheckNoUkm();
}
// Navigate to a domain whose visual representation looks like a top domain.
// This should record metrics. It should also show a "Did you mean to go to ..."
// infobar if configured via a feature param.
IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationObserverBrowserTest,
Idn_TopDomain_Match) {
base::HistogramTester histograms;
const GURL kNavigatedUrl = GetURL("googlé.com");
const GURL kExpectedSuggestedUrl = GetURL("google.com");
// Even if the navigated site has a low engagement score, it should be
// considered for lookalike suggestions.
SetEngagementScore(browser(), kNavigatedUrl, kLowEngagement);
TestHistogramEventsRecordedAndInfobarVisibility(
browser(), &histograms, kNavigatedUrl, kExpectedSuggestedUrl,
NavigationSuggestionEvent::kMatchTopSite);
CheckUkm({kNavigatedUrl},
LookalikeUrlNavigationObserver::MatchType::kTopSite);
}
// 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(LookalikeUrlNavigationObserverBrowserTest,
TopDomainIdnSubdomain_NoMatch) {
TestInfobarNotShown(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.
TestInfobarNotShown(browser(), GetURL("tést.blogspot.com"));
CheckNoUkm();
}
// Schemes other than HTTP and HTTPS should be ignored.
IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationObserverBrowserTest,
TopDomainChromeUrl_NoMatch) {
TestInfobarNotShown(browser(), GURL("chrome://googlé.com"));
CheckNoUkm();
}
// Navigate to a domain within an edit distance of 1 to a top domain.
// This should record metrics. It should also show a "Did you mean to go to ..."
// infobar if configured via a feature param.
IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationObserverBrowserTest,
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");
const GURL kExpectedSuggestedUrl = GetURL("google.com");
// Even if the navigated site has a low engagement score, it should be
// considered for lookalike suggestions.
SetEngagementScore(browser(), kNavigatedUrl, kLowEngagement);
TestHistogramEventsRecordedAndInfobarVisibility(
browser(), &histograms, kNavigatedUrl, kExpectedSuggestedUrl,
NavigationSuggestionEvent::kMatchEditDistance);
CheckUkm({kNavigatedUrl},
LookalikeUrlNavigationObserver::MatchType::kEditDistance);
}
// Tests negative examples for the edit distance.
IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationObserverBrowserTest,
EditDistance_TopDomain_NoMatch) {
// Matches google.com.tr but only differs in registry.
TestInfobarNotShown(browser(), GetURL("google.com.tw"));
CheckNoUkm();
// Matches bing.com but is a top domain itself.
TestInfobarNotShown(browser(), GetURL("ning.com"));
CheckNoUkm();
// Matches ask.com but is too short.
TestInfobarNotShown(browser(), GetURL("bsk.com"));
CheckNoUkm();
}
// Test that the heuristics aren't triggered on net errors.
IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationObserverBrowserTest,
NetError_NoMatch) {
// 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());
// Matches google.com but page returns an invalid response.
TestInfobarNotShown(browser(),
custom_test_server.GetURL("gooogle.com", "/title1.html"));
CheckNoUkm();
TestInfobarNotShown(browser(),
custom_test_server.GetURL("googlé.com", "/title1.html"));
CheckNoUkm();
SetEngagementScore(browser(), GURL("http://site1.com"), kHighEngagement);
TestInfobarNotShown(browser(),
custom_test_server.GetURL("sité1.com", "/title1.html"));
CheckNoUkm();
}
// 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 a "Did you mean to go to ..." infobar if configured via
// a feature param.
IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationObserverBrowserTest,
Idn_SiteEngagement_Match) {
const char* const kEngagedSites[] = {
"http://site1.com", "http://www.site2.com", "http://sité3.com",
"http://www.sité4.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());
for (const char* const kSite : kEngagedSites) {
SetEngagementScore(browser(), GURL(kSite), kHighEngagement);
SetEngagementScore(incognito, GURL(kSite), kLowEngagement);
}
// Main profile should record metrics because there are engaged sites.
std::vector<GURL> ukm_urls;
for (const auto& test_case : kSiteEngagementTestCases) {
base::HistogramTester histograms;
const GURL kNavigatedUrl = GetURL(test_case.navigated);
const GURL kExpectedSuggestedUrl = GetURL(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));
TestHistogramEventsRecordedAndInfobarVisibility(
browser(), &histograms, kNavigatedUrl, kExpectedSuggestedUrl,
NavigationSuggestionEvent::kMatchSiteEngagement);
ukm_urls.push_back(kNavigatedUrl);
CheckUkm(ukm_urls,
LookalikeUrlNavigationObserver::MatchType::kSiteEngagement);
}
// Incognito shouldn't record metrics because there are no engaged sites.
for (const auto& test_case : kSiteEngagementTestCases) {
base::HistogramTester histograms;
const GURL kNavigatedUrl = GetURL(test_case.navigated);
test_clock()->Advance(base::TimeDelta::FromHours(1));
TestInfobarNotShown(incognito, kNavigatedUrl);
histograms.ExpectTotalCount(LookalikeUrlNavigationObserver::kHistogramName,
0);
}
// Now reverse the scores: Set low engagement in the main profile and high
// engagement in incognito.
for (const char* const kSite : kEngagedSites) {
SetEngagementScore(browser(), GURL(kSite), kLowEngagement);
SetEngagementScore(incognito, GURL(kSite), kHighEngagement);
}
// Incognito should start recording metrics and main profile should stop.
for (const auto& test_case : kSiteEngagementTestCases) {
base::HistogramTester histograms;
const GURL kNavigatedUrl = GetURL(test_case.navigated);
const GURL kExpectedSuggestedUrl = GetURL(test_case.suggested);
// Even if the navigated site has a low engagement score, it should be
// considered for lookalike suggestions.
SetEngagementScore(incognito, kNavigatedUrl, kLowEngagement);
// Advance the clock to force LookalikeUrlService to fetch a new engaged
// site list.
test_clock()->Advance(base::TimeDelta::FromHours(1));
TestHistogramEventsRecordedAndInfobarVisibility(
incognito, &histograms, kNavigatedUrl, kExpectedSuggestedUrl,
NavigationSuggestionEvent::kMatchSiteEngagement);
ukm_urls.push_back(kNavigatedUrl);
CheckUkm(ukm_urls,
LookalikeUrlNavigationObserver::MatchType::kSiteEngagement);
}
// Main profile shouldn't record metrics because there are no engaged sites.
for (const auto& test_case : kSiteEngagementTestCases) {
base::HistogramTester histograms;
const GURL kNavigatedUrl = GetURL(test_case.navigated);
test_clock()->Advance(base::TimeDelta::FromHours(1));
TestInfobarNotShown(browser(), kNavigatedUrl);
histograms.ExpectTotalCount(LookalikeUrlNavigationObserver::kHistogramName,
0);
}
}
// Test that navigations to a site with a high engagement score shouldn't
// record metrics or show infobar.
IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationObserverBrowserTest,
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);
TestInfobarNotShown(browser(), high_engagement_url);
histograms.ExpectTotalCount(LookalikeUrlNavigationObserver::kHistogramName,
0);
}
// Test that an engaged site with a scheme other than HTTP or HTTPS should be
// ignored.
IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationObserverBrowserTest,
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);
TestInfobarNotShown(browser(), low_engagement_url);
histograms.ExpectTotalCount(LookalikeUrlNavigationObserver::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(LookalikeUrlNavigationObserverBrowserTest,
IdnWithSingleLabelShouldNotCauseACrash) {
base::HistogramTester histograms;
// Case 1: Navigating to an IDN with a single label shouldn't cause a crash.
TestInfobarNotShown(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);
TestInfobarNotShown(browser(), GetURL("tést.com"));
histograms.ExpectTotalCount(LookalikeUrlNavigationObserver::kHistogramName,
0);
CheckNoUkm();
}