blob: dc7d18977c0fb80d4d22a705d5cbc72a8f768c0f [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 "components/omnibox/browser/document_provider.h"
#include "base/json/json_reader.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/scoped_feature_list.h"
#include "base/time/time_to_iso8601.h"
#include "base/values.h"
#include "build/build_config.h"
#include "components/omnibox/browser/autocomplete_provider.h"
#include "components/omnibox/browser/autocomplete_provider_listener.h"
#include "components/omnibox/browser/mock_autocomplete_provider_client.h"
#include "components/omnibox/browser/omnibox_field_trial.h"
#include "components/omnibox/browser/omnibox_pref_names.h"
#include "components/omnibox/browser/test_scheme_classifier.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/testing_pref_service.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace {
using testing::Return;
class FakeAutocompleteProviderClient : public MockAutocompleteProviderClient {
public:
FakeAutocompleteProviderClient()
: template_url_service_(new TemplateURLService(nullptr, 0)) {
pref_service_.registry()->RegisterBooleanPref(
omnibox::kDocumentSuggestEnabled, true);
}
bool SearchSuggestEnabled() const override { return true; }
TemplateURLService* GetTemplateURLService() override {
return template_url_service_.get();
}
TemplateURLService* GetTemplateURLService() const override {
return template_url_service_.get();
}
PrefService* GetPrefs() override { return &pref_service_; }
private:
std::unique_ptr<TemplateURLService> template_url_service_;
TestingPrefServiceSimple pref_service_;
DISALLOW_COPY_AND_ASSIGN(FakeAutocompleteProviderClient);
};
} // namespace
class DocumentProviderTest : public testing::Test,
public AutocompleteProviderListener {
public:
DocumentProviderTest();
void SetUp() override;
protected:
// AutocompleteProviderListener:
void OnProviderUpdate(bool updated_matches) override;
std::unique_ptr<FakeAutocompleteProviderClient> client_;
scoped_refptr<DocumentProvider> provider_;
TemplateURL* default_template_url_;
private:
DISALLOW_COPY_AND_ASSIGN(DocumentProviderTest);
};
DocumentProviderTest::DocumentProviderTest() {}
void DocumentProviderTest::SetUp() {
client_.reset(new FakeAutocompleteProviderClient());
TemplateURLService* turl_model = client_->GetTemplateURLService();
turl_model->Load();
TemplateURLData data;
data.SetShortName(base::ASCIIToUTF16("t"));
data.SetURL("https://www.google.com/?q={searchTerms}");
data.suggestions_url = "https://www.google.com/complete/?q={searchTerms}";
default_template_url_ = turl_model->Add(std::make_unique<TemplateURL>(data));
turl_model->SetUserSelectedDefaultSearchProvider(default_template_url_);
provider_ = DocumentProvider::Create(client_.get(), this);
}
void DocumentProviderTest::OnProviderUpdate(bool updated_matches) {
// No action required.
}
TEST_F(DocumentProviderTest, CheckFeatureBehindFlag) {
base::test::ScopedFeatureList feature_list;
feature_list.InitAndDisableFeature(omnibox::kDocumentProvider);
EXPECT_FALSE(provider_->IsDocumentProviderAllowed(client_.get()));
}
TEST_F(DocumentProviderTest, CheckFeaturePrerequisiteNoIncognito) {
base::test::ScopedFeatureList feature_list;
feature_list.InitAndEnableFeature(omnibox::kDocumentProvider);
EXPECT_CALL(*client_.get(), SearchSuggestEnabled())
.WillRepeatedly(Return(true));
EXPECT_CALL(*client_.get(), IsAuthenticated()).WillRepeatedly(Return(true));
EXPECT_CALL(*client_.get(), IsSyncActive()).WillRepeatedly(Return(true));
EXPECT_CALL(*client_.get(), IsOffTheRecord()).WillRepeatedly(Return(false));
// Feature starts enabled.
EXPECT_TRUE(provider_->IsDocumentProviderAllowed(client_.get()));
// Feature should be disabled in incognito.
EXPECT_CALL(*client_.get(), IsOffTheRecord()).WillRepeatedly(Return(true));
EXPECT_FALSE(provider_->IsDocumentProviderAllowed(client_.get()));
}
TEST_F(DocumentProviderTest, CheckFeaturePrerequisiteNoSync) {
base::test::ScopedFeatureList feature_list;
feature_list.InitAndEnableFeature(omnibox::kDocumentProvider);
EXPECT_CALL(*client_.get(), SearchSuggestEnabled())
.WillRepeatedly(Return(true));
EXPECT_CALL(*client_.get(), IsAuthenticated()).WillRepeatedly(Return(true));
EXPECT_CALL(*client_.get(), IsSyncActive()).WillRepeatedly(Return(true));
EXPECT_CALL(*client_.get(), IsOffTheRecord()).WillRepeatedly(Return(false));
// Feature starts enabled.
EXPECT_TRUE(provider_->IsDocumentProviderAllowed(client_.get()));
// Feature should be disabled without active sync.
EXPECT_CALL(*client_.get(), IsSyncActive()).WillOnce(Return(false));
EXPECT_FALSE(provider_->IsDocumentProviderAllowed(client_.get()));
}
TEST_F(DocumentProviderTest, CheckFeaturePrerequisiteClientSettingOff) {
base::test::ScopedFeatureList feature_list;
feature_list.InitAndEnableFeature(omnibox::kDocumentProvider);
EXPECT_CALL(*client_.get(), SearchSuggestEnabled())
.WillRepeatedly(Return(true));
EXPECT_CALL(*client_.get(), IsAuthenticated()).WillRepeatedly(Return(true));
EXPECT_CALL(*client_.get(), IsSyncActive()).WillRepeatedly(Return(true));
EXPECT_CALL(*client_.get(), IsOffTheRecord()).WillRepeatedly(Return(false));
// Feature starts enabled.
EXPECT_TRUE(provider_->IsDocumentProviderAllowed(client_.get()));
// Disabling toggle in chrome://settings should be respected.
PrefService* fake_prefs = client_->GetPrefs();
fake_prefs->SetBoolean(omnibox::kDocumentSuggestEnabled, false);
EXPECT_FALSE(provider_->IsDocumentProviderAllowed(client_.get()));
fake_prefs->SetBoolean(omnibox::kDocumentSuggestEnabled, true);
}
TEST_F(DocumentProviderTest, CheckFeaturePrerequisiteDefaultSearch) {
base::test::ScopedFeatureList feature_list;
feature_list.InitAndEnableFeature(omnibox::kDocumentProvider);
EXPECT_CALL(*client_.get(), SearchSuggestEnabled())
.WillRepeatedly(Return(true));
EXPECT_CALL(*client_.get(), IsAuthenticated()).WillRepeatedly(Return(true));
EXPECT_CALL(*client_.get(), IsSyncActive()).WillRepeatedly(Return(true));
EXPECT_CALL(*client_.get(), IsOffTheRecord()).WillRepeatedly(Return(false));
// Feature starts enabled.
EXPECT_TRUE(provider_->IsDocumentProviderAllowed(client_.get()));
// Switching default search disables it.
TemplateURLService* template_url_service = client_->GetTemplateURLService();
TemplateURLData data;
data.SetShortName(base::ASCIIToUTF16("t"));
data.SetURL("https://www.notgoogle.com/?q={searchTerms}");
data.suggestions_url = "https://www.notgoogle.com/complete/?q={searchTerms}";
TemplateURL* new_default_provider =
template_url_service->Add(std::make_unique<TemplateURL>(data));
template_url_service->SetUserSelectedDefaultSearchProvider(
new_default_provider);
EXPECT_FALSE(provider_->IsDocumentProviderAllowed(client_.get()));
template_url_service->SetUserSelectedDefaultSearchProvider(
default_template_url_);
template_url_service->Remove(new_default_provider);
}
TEST_F(DocumentProviderTest, CheckFeaturePrerequisiteServerBackoff) {
base::test::ScopedFeatureList feature_list;
feature_list.InitAndEnableFeature(omnibox::kDocumentProvider);
EXPECT_CALL(*client_.get(), SearchSuggestEnabled())
.WillRepeatedly(Return(true));
EXPECT_CALL(*client_.get(), IsAuthenticated()).WillRepeatedly(Return(true));
EXPECT_CALL(*client_.get(), IsSyncActive()).WillRepeatedly(Return(true));
EXPECT_CALL(*client_.get(), IsOffTheRecord()).WillRepeatedly(Return(false));
// Feature starts enabled.
EXPECT_TRUE(provider_->IsDocumentProviderAllowed(client_.get()));
// Server setting backoff flag disables it.
provider_->backoff_for_session_ = true;
EXPECT_FALSE(provider_->IsDocumentProviderAllowed(client_.get()));
provider_->backoff_for_session_ = false;
}
TEST_F(DocumentProviderTest, IsInputLikelyURL) {
base::test::ScopedFeatureList feature_list;
feature_list.InitAndEnableFeature(omnibox::kDocumentProvider);
auto IsInputLikelyURL_Wrapper = [](const std::string& input_ascii) {
const AutocompleteInput autocomplete_input(
base::ASCIIToUTF16(input_ascii), metrics::OmniboxEventProto::OTHER,
TestSchemeClassifier());
return DocumentProvider::IsInputLikelyURL(autocomplete_input);
};
EXPECT_TRUE(IsInputLikelyURL_Wrapper("htt"));
EXPECT_TRUE(IsInputLikelyURL_Wrapper("http"));
EXPECT_TRUE(IsInputLikelyURL_Wrapper("https"));
EXPECT_TRUE(IsInputLikelyURL_Wrapper("https://"));
EXPECT_TRUE(IsInputLikelyURL_Wrapper("http://web.site"));
EXPECT_TRUE(IsInputLikelyURL_Wrapper("https://web.site"));
EXPECT_TRUE(IsInputLikelyURL_Wrapper("https://web.site"));
EXPECT_TRUE(IsInputLikelyURL_Wrapper("w"));
EXPECT_TRUE(IsInputLikelyURL_Wrapper("www."));
EXPECT_TRUE(IsInputLikelyURL_Wrapper("www.web.site"));
EXPECT_TRUE(IsInputLikelyURL_Wrapper("chrome://extensions"));
EXPECT_FALSE(IsInputLikelyURL_Wrapper("https certificate"));
EXPECT_FALSE(IsInputLikelyURL_Wrapper("www website hosting"));
EXPECT_FALSE(IsInputLikelyURL_Wrapper("text query"));
}
TEST_F(DocumentProviderTest, ParseDocumentSearchResults) {
const char kGoodJSONResponse[] = R"({
"results": [
{
"title": "Document 1",
"url": "https://documentprovider.tld/doc?id=1",
"score": 1234,
"originalUrl": "https://shortened.url"
},
{
"title": "Document 2",
"url": "https://documentprovider.tld/doc?id=2"
}
]
})";
std::unique_ptr<base::DictionaryValue> response =
base::DictionaryValue::From(base::JSONReader::Read(kGoodJSONResponse));
ASSERT_TRUE(response != nullptr);
ACMatches matches;
provider_->ParseDocumentSearchResults(*response, &matches);
EXPECT_EQ(matches.size(), 2u);
EXPECT_EQ(matches[0].contents, base::ASCIIToUTF16("Document 1"));
EXPECT_EQ(matches[0].destination_url,
GURL("https://documentprovider.tld/doc?id=1"));
EXPECT_EQ(matches[0].relevance, 1234); // Server-specified.
EXPECT_EQ(matches[0].stripped_destination_url, GURL("https://shortened.url"));
EXPECT_EQ(matches[1].contents, base::ASCIIToUTF16("Document 2"));
EXPECT_EQ(matches[1].destination_url,
GURL("https://documentprovider.tld/doc?id=2"));
EXPECT_EQ(matches[1].relevance, 700); // From study default.
EXPECT_TRUE(matches[1].stripped_destination_url.is_empty());
ASSERT_FALSE(provider_->backoff_for_session_);
}
TEST_F(DocumentProviderTest, ParseDocumentSearchResultsBreakTies) {
const char kGoodJSONResponseWithTies[] = R"({
"results": [
{
"title": "Document 1",
"url": "https://documentprovider.tld/doc?id=1",
"score": 1234,
"originalUrl": "https://shortened.url"
},
{
"title": "Document 2",
"score": 1234,
"url": "https://documentprovider.tld/doc?id=2"
},
{
"title": "Document 3",
"score": 1234,
"url": "https://documentprovider.tld/doc?id=3"
}
]
})";
std::unique_ptr<base::DictionaryValue> response = base::DictionaryValue::From(
base::JSONReader::Read(kGoodJSONResponseWithTies));
ASSERT_TRUE(response != nullptr);
ACMatches matches;
provider_->ParseDocumentSearchResults(*response, &matches);
EXPECT_EQ(matches.size(), 3u);
// Server is suggesting relevances of [1234, 1234, 1234]
// We should break ties to [1234, 1233, 1232]
EXPECT_EQ(matches[0].contents, base::ASCIIToUTF16("Document 1"));
EXPECT_EQ(matches[0].destination_url,
GURL("https://documentprovider.tld/doc?id=1"));
EXPECT_EQ(matches[0].relevance, 1234); // As the server specified.
EXPECT_EQ(matches[0].stripped_destination_url, GURL("https://shortened.url"));
EXPECT_EQ(matches[1].contents, base::ASCIIToUTF16("Document 2"));
EXPECT_EQ(matches[1].destination_url,
GURL("https://documentprovider.tld/doc?id=2"));
EXPECT_EQ(matches[1].relevance, 1233); // Tie demoted
EXPECT_TRUE(matches[1].stripped_destination_url.is_empty());
EXPECT_EQ(matches[2].contents, base::ASCIIToUTF16("Document 3"));
EXPECT_EQ(matches[2].destination_url,
GURL("https://documentprovider.tld/doc?id=3"));
EXPECT_EQ(matches[2].relevance, 1232); // Tie demoted, twice.
EXPECT_TRUE(matches[2].stripped_destination_url.is_empty());
ASSERT_FALSE(provider_->backoff_for_session_);
}
TEST_F(DocumentProviderTest, ParseDocumentSearchResultsBreakTiesCascade) {
const char kGoodJSONResponseWithTies[] = R"({
"results": [
{
"title": "Document 1",
"url": "https://documentprovider.tld/doc?id=1",
"score": 1234,
"originalUrl": "https://shortened.url"
},
{
"title": "Document 2",
"score": 1234,
"url": "https://documentprovider.tld/doc?id=2"
},
{
"title": "Document 3",
"score": 1233,
"url": "https://documentprovider.tld/doc?id=3"
}
]
})";
std::unique_ptr<base::DictionaryValue> response = base::DictionaryValue::From(
base::JSONReader::Read(kGoodJSONResponseWithTies));
ASSERT_TRUE(response != nullptr);
ACMatches matches;
provider_->ParseDocumentSearchResults(*response, &matches);
EXPECT_EQ(matches.size(), 3u);
// Server is suggesting relevances of [1233, 1234, 1233, 1000, 1000]
// We should break ties to [1234, 1233, 1232, 1000, 999]
EXPECT_EQ(matches[0].contents, base::ASCIIToUTF16("Document 1"));
EXPECT_EQ(matches[0].destination_url,
GURL("https://documentprovider.tld/doc?id=1"));
EXPECT_EQ(matches[0].relevance, 1234); // As the server specified.
EXPECT_EQ(matches[0].stripped_destination_url, GURL("https://shortened.url"));
EXPECT_EQ(matches[1].contents, base::ASCIIToUTF16("Document 2"));
EXPECT_EQ(matches[1].destination_url,
GURL("https://documentprovider.tld/doc?id=2"));
EXPECT_EQ(matches[1].relevance, 1233); // Tie demoted
EXPECT_TRUE(matches[1].stripped_destination_url.is_empty());
EXPECT_EQ(matches[2].contents, base::ASCIIToUTF16("Document 3"));
EXPECT_EQ(matches[2].destination_url,
GURL("https://documentprovider.tld/doc?id=3"));
// Document 2's demotion caused an implicit tie.
// Ensure we demote this one as well.
EXPECT_EQ(matches[2].relevance, 1232);
EXPECT_TRUE(matches[2].stripped_destination_url.is_empty());
ASSERT_FALSE(provider_->backoff_for_session_);
}
TEST_F(DocumentProviderTest, ParseDocumentSearchResultsBreakTiesZeroLimit) {
const char kGoodJSONResponseWithTies[] = R"({
"results": [
{
"title": "Document 1",
"url": "https://documentprovider.tld/doc?id=1",
"score": 1,
"originalUrl": "https://shortened.url"
},
{
"title": "Document 2",
"score": 1,
"url": "https://documentprovider.tld/doc?id=2"
},
{
"title": "Document 3",
"score": 1,
"url": "https://documentprovider.tld/doc?id=3"
}
]
})";
std::unique_ptr<base::DictionaryValue> response = base::DictionaryValue::From(
base::JSONReader::Read(kGoodJSONResponseWithTies));
ASSERT_TRUE(response != nullptr);
ACMatches matches;
provider_->ParseDocumentSearchResults(*response, &matches);
EXPECT_EQ(matches.size(), 3u);
// Server is suggesting relevances of [1, 1, 1]
// We should break ties, but not below zero, to [1, 0, 0]
EXPECT_EQ(matches[0].contents, base::ASCIIToUTF16("Document 1"));
EXPECT_EQ(matches[0].destination_url,
GURL("https://documentprovider.tld/doc?id=1"));
EXPECT_EQ(matches[0].relevance, 1); // As the server specified.
EXPECT_EQ(matches[0].stripped_destination_url, GURL("https://shortened.url"));
EXPECT_EQ(matches[1].contents, base::ASCIIToUTF16("Document 2"));
EXPECT_EQ(matches[1].destination_url,
GURL("https://documentprovider.tld/doc?id=2"));
EXPECT_EQ(matches[1].relevance, 0); // Tie demoted
EXPECT_TRUE(matches[1].stripped_destination_url.is_empty());
EXPECT_EQ(matches[2].contents, base::ASCIIToUTF16("Document 3"));
EXPECT_EQ(matches[2].destination_url,
GURL("https://documentprovider.tld/doc?id=3"));
// Tie is demoted further.
EXPECT_EQ(matches[2].relevance, 0);
EXPECT_TRUE(matches[2].stripped_destination_url.is_empty());
ASSERT_FALSE(provider_->backoff_for_session_);
}
TEST_F(DocumentProviderTest, ParseDocumentSearchResultsWithBackoff) {
// Response where the server wishes to trigger backoff.
const char kBackoffJSONResponse[] = R"({
"error": {
"code": 503,
"message": "Not eligible to query, see retry info.",
"status": "UNAVAILABLE",
"details": [
{
"@type": "type.googleapis.com/google.rpc.RetryInfo",
"retryDelay": "100000s"
},
]
}
})";
ASSERT_FALSE(provider_->backoff_for_session_);
std::unique_ptr<base::DictionaryValue> backoff_response =
base::DictionaryValue::From(base::JSONReader::Read(
kBackoffJSONResponse, base::JSON_ALLOW_TRAILING_COMMAS));
ASSERT_TRUE(backoff_response != nullptr);
ACMatches matches;
provider_->ParseDocumentSearchResults(*backoff_response, &matches);
ASSERT_TRUE(provider_->backoff_for_session_);
}
TEST_F(DocumentProviderTest, ParseDocumentSearchResultsWithIneligibleFlag) {
// Response where the server wishes to trigger backoff.
const char kIneligibleJSONResponse[] = R"({
"error": {
"code": 403,
"message": "Not eligible to query due to admin disabled Chrome search settings.",
"status": "PERMISSION_DENIED",
}
})";
// Same as above, but the message doesn't match. We should accept this
// response, but it isn't expected to trigger backoff.
const char kMismatchedMessageJSON[] = R"({
"error": {
"code": 403,
"message": "Some other thing went wrong.",
"status": "PERMISSION_DENIED",
}
})";
ACMatches matches;
ASSERT_FALSE(provider_->backoff_for_session_);
// First, parse an invalid response - shouldn't prohibit future requests
// from working but also shouldn't trigger backoff.
std::unique_ptr<base::DictionaryValue> bad_response =
base::DictionaryValue::From(base::JSONReader::Read(
kMismatchedMessageJSON, base::JSON_ALLOW_TRAILING_COMMAS));
ASSERT_TRUE(bad_response != nullptr);
provider_->ParseDocumentSearchResults(*bad_response, &matches);
ASSERT_FALSE(provider_->backoff_for_session_);
// Now parse a response that does trigger backoff.
std::unique_ptr<base::DictionaryValue> backoff_response =
base::DictionaryValue::From(base::JSONReader::Read(
kIneligibleJSONResponse, base::JSON_ALLOW_TRAILING_COMMAS));
ASSERT_TRUE(backoff_response != nullptr);
provider_->ParseDocumentSearchResults(*backoff_response, &matches);
ASSERT_TRUE(provider_->backoff_for_session_);
}
// This test is affected by an iOS 10 simulator bug: https://crbug.com/782033
// and may get wrong timezone on Win7: https://crbug.com/856119
#if !defined(OS_IOS) && !defined(OS_WIN)
TEST_F(DocumentProviderTest, GenerateLastModifiedString) {
base::Time::Exploded local_exploded = {0};
local_exploded.year = 2018;
local_exploded.month = 8;
local_exploded.day_of_month = 27;
local_exploded.hour = 3;
local_exploded.minute = 18;
local_exploded.second = 54;
base::Time local_now;
EXPECT_TRUE(base::Time::FromLocalExploded(local_exploded, &local_now));
base::Time modified_today = local_now + base::TimeDelta::FromHours(-1);
base::Time modified_this_year = local_now + base::TimeDelta::FromDays(-8);
base::Time modified_last_year = local_now + base::TimeDelta::FromDays(-365);
// GenerateLastModifiedString should accept any parseable timestamp, but use
// ISO8601 UTC timestamp strings since the service returns them in practice.
EXPECT_EQ(DocumentProvider::GenerateLastModifiedString(
base::TimeToISO8601(modified_today), local_now),
base::ASCIIToUTF16("2:18 AM"));
EXPECT_EQ(DocumentProvider::GenerateLastModifiedString(
base::TimeToISO8601(modified_this_year), local_now),
base::ASCIIToUTF16("Aug 19"));
EXPECT_EQ(DocumentProvider::GenerateLastModifiedString(
base::TimeToISO8601(modified_last_year), local_now),
base::ASCIIToUTF16("8/27/17"));
}
#endif // !defined(OS_IOS)