// Copyright (c) 2011 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/string_util.h" #include "base/time.h" #include "base/utf_string_conversions.h" #include "build/build_config.h" #include "chrome/browser/autocomplete/autocomplete_match.h" #include "chrome/browser/autocomplete/search_provider.h" #include "chrome/browser/history/history.h" #include "chrome/browser/prefs/pref_service.h" #include "chrome/browser/search_engines/template_url.h" #include "chrome/browser/search_engines/template_url_model.h" #include "chrome/common/net/test_url_fetcher_factory.h" #include "chrome/common/pref_names.h" #include "chrome/test/testing_browser_process.h" #include "chrome/test/testing_browser_process_test.h" #include "chrome/test/testing_profile.h" #include "content/browser/browser_thread.h" #include "net/url_request/url_request_status.h" #include "testing/gtest/include/gtest/gtest.h" // The following environment is configured for these tests: // . The TemplateURL default_t_url_ is set as the default provider. // . The TemplateURL keyword_t_url_ is added to the TemplateURLModel. This // TemplateURL has a valid suggest and search URL. // . The URL created by using the search term term1_ with default_t_url_ is // added to history. // . The URL created by using the search term keyword_term_ with keyword_t_url_ // is added to history. // . test_factory_ is set as the URLFetcher::Factory. class SearchProviderTest : public TestingBrowserProcessTest, public AutocompleteProvider::ACProviderListener { public: SearchProviderTest() : default_t_url_(NULL), term1_(UTF8ToUTF16("term1")), keyword_t_url_(NULL), keyword_term_(UTF8ToUTF16("keyword")), io_thread_(BrowserThread::IO), quit_when_done_(false) { io_thread_.Start(); } // See description above class for what this registers. virtual void SetUp(); virtual void TearDown(); protected: // Returns an AutocompleteMatch in provider_'s set of matches that matches // |url|. If there is no matching URL, an empty match is returned. AutocompleteMatch FindMatchWithDestination(const GURL& url); // ACProviderListener method. If we're waiting for the provider to finish, // this exits the message loop. virtual void OnProviderUpdate(bool updated_matches); // Runs a nested message loop until provider_ is done. The message loop is // exited by way of OnProviderUPdate. void RunTillProviderDone(); // Invokes Start on provider_, then runs all pending tasks. void QueryForInput(const string16& text, bool prevent_inline_autocomplete, bool minimal_changes); // Notifies the URLFetcher for the suggest query corresponding to the default // search provider that it's done. // Be sure and wrap calls to this in ASSERT_NO_FATAL_FAILURE. void FinishDefaultSuggestQuery(); // See description above class for details of these fields. TemplateURL* default_t_url_; const string16 term1_; GURL term1_url_; TemplateURL* keyword_t_url_; const string16 keyword_term_; GURL keyword_url_; MessageLoopForUI message_loop_; BrowserThread io_thread_; // URLFetcher::Factory implementation registered. TestURLFetcherFactory test_factory_; // Profile we use. TestingProfile profile_; // The provider. scoped_refptr<SearchProvider> provider_; // If true, OnProviderUpdate exits out of the current message loop. bool quit_when_done_; DISALLOW_COPY_AND_ASSIGN(SearchProviderTest); }; void SearchProviderTest::SetUp() { SearchProvider::set_query_suggest_immediately(true); // We need both the history service and template url model loaded. profile_.CreateHistoryService(true, false); profile_.CreateTemplateURLModel(); TemplateURLModel* turl_model = profile_.GetTemplateURLModel(); // Reset the default TemplateURL. default_t_url_ = new TemplateURL(); default_t_url_->SetURL("http://defaultturl/{searchTerms}", 0, 0); default_t_url_->SetSuggestionsURL("http://defaultturl2/{searchTerms}", 0, 0); turl_model->Add(default_t_url_); turl_model->SetDefaultSearchProvider(default_t_url_); TemplateURLID default_provider_id = default_t_url_->id(); ASSERT_NE(0, default_provider_id); // Add url1, with search term term1_. HistoryService* history = profile_.GetHistoryService(Profile::EXPLICIT_ACCESS); term1_url_ = GURL(default_t_url_->url()->ReplaceSearchTerms( *default_t_url_, term1_, 0, string16())); history->AddPageWithDetails(term1_url_, string16(), 1, 1, base::Time::Now(), false, history::SOURCE_BROWSED); history->SetKeywordSearchTermsForURL(term1_url_, default_t_url_->id(), term1_); // Create another TemplateURL. keyword_t_url_ = new TemplateURL(); keyword_t_url_->set_keyword(ASCIIToUTF16("k")); keyword_t_url_->SetURL("http://keyword/{searchTerms}", 0, 0); keyword_t_url_->SetSuggestionsURL("http://suggest_keyword/{searchTerms}", 0, 0); profile_.GetTemplateURLModel()->Add(keyword_t_url_); ASSERT_NE(0, keyword_t_url_->id()); // Add a page and search term for keyword_t_url_. keyword_url_ = GURL(keyword_t_url_->url()->ReplaceSearchTerms( *keyword_t_url_, keyword_term_, 0, string16())); history->AddPageWithDetails(keyword_url_, string16(), 1, 1, base::Time::Now(), false, history::SOURCE_BROWSED); history->SetKeywordSearchTermsForURL(keyword_url_, keyword_t_url_->id(), keyword_term_); // Keywords are updated by the InMemoryHistoryBackend only after the message // has been processed on the history thread. Block until history processes all // requests to ensure the InMemoryDatabase is the state we expect it. profile_.BlockUntilHistoryProcessesPendingRequests(); provider_ = new SearchProvider(this, &profile_); URLFetcher::set_factory(&test_factory_); } void SearchProviderTest::OnProviderUpdate(bool updated_matches) { if (quit_when_done_ && provider_->done()) { quit_when_done_ = false; message_loop_.Quit(); } } void SearchProviderTest::RunTillProviderDone() { if (provider_->done()) return; quit_when_done_ = true; #if defined(OS_MACOSX) message_loop_.Run(); #else message_loop_.Run(NULL); #endif } void SearchProviderTest::QueryForInput(const string16& text, bool prevent_inline_autocomplete, bool minimal_changes) { // Start a query. AutocompleteInput input(text, string16(), prevent_inline_autocomplete, false, true, AutocompleteInput::ALL_MATCHES); provider_->Start(input, minimal_changes); // RunAllPending so that the task scheduled by SearchProvider to create the // URLFetchers runs. message_loop_.RunAllPending(); } void SearchProviderTest::TearDown() { message_loop_.RunAllPending(); URLFetcher::set_factory(NULL); // Shutdown the provider before the profile. provider_ = NULL; } AutocompleteMatch SearchProviderTest::FindMatchWithDestination( const GURL& url) { for (ACMatches::const_iterator i = provider_->matches().begin(); i != provider_->matches().end(); ++i) { if (i->destination_url == url) return *i; } return AutocompleteMatch(NULL, 1, false, AutocompleteMatch::HISTORY_URL); } void SearchProviderTest::FinishDefaultSuggestQuery() { TestURLFetcher* default_fetcher = test_factory_.GetFetcherByID( SearchProvider::kDefaultProviderURLFetcherID); ASSERT_TRUE(default_fetcher); // Tell the SearchProvider the default suggest query is done. default_fetcher->delegate()->OnURLFetchComplete( default_fetcher, GURL(), net::URLRequestStatus(), 200, ResponseCookies(), std::string()); } // Tests ----------------------------------------------------------------------- // Make sure we query history for the default provider and a URLFetcher is // created for the default provider suggest results. TEST_F(SearchProviderTest, QueryDefaultProvider) { string16 term = term1_.substr(0, term1_.size() - 1); QueryForInput(term, false, false); // Make sure the default providers suggest service was queried. TestURLFetcher* fetcher = test_factory_.GetFetcherByID( SearchProvider::kDefaultProviderURLFetcherID); ASSERT_TRUE(fetcher); // And the URL matches what we expected. GURL expected_url = GURL(default_t_url_->suggestions_url()-> ReplaceSearchTerms(*default_t_url_, term, 0, string16())); ASSERT_TRUE(fetcher->original_url() == expected_url); // Tell the SearchProvider the suggest query is done. fetcher->delegate()->OnURLFetchComplete( fetcher, GURL(), net::URLRequestStatus(), 200, ResponseCookies(), std::string()); fetcher = NULL; // Run till the history results complete. RunTillProviderDone(); // The SearchProvider is done. Make sure it has a result for the history // term term1. AutocompleteMatch term1_match = FindMatchWithDestination(term1_url_); EXPECT_TRUE(!term1_match.destination_url.is_empty()); // Term1 should have a description. EXPECT_FALSE(term1_match.description.empty()); GURL what_you_typed_url = GURL(default_t_url_->url()->ReplaceSearchTerms( *default_t_url_, term, 0, string16())); AutocompleteMatch what_you_typed_match = FindMatchWithDestination(what_you_typed_url); EXPECT_TRUE(!what_you_typed_match.destination_url.is_empty()); EXPECT_TRUE(what_you_typed_match.description.empty()); // The match for term1 should be more relevant than the what you typed result. EXPECT_GT(term1_match.relevance, what_you_typed_match.relevance); } TEST_F(SearchProviderTest, HonorPreventInlineAutocomplete) { string16 term = term1_.substr(0, term1_.size() - 1); QueryForInput(term, true, false); ASSERT_FALSE(provider_->matches().empty()); ASSERT_EQ(AutocompleteMatch::SEARCH_WHAT_YOU_TYPED, provider_->matches()[0].type); } // Issues a query that matches the registered keyword and makes sure history // is queried as well as URLFetchers getting created. TEST_F(SearchProviderTest, QueryKeywordProvider) { string16 term = keyword_term_.substr(0, keyword_term_.size() - 1); QueryForInput(keyword_t_url_->keyword() + UTF8ToUTF16(" ") + term, false, false); // Make sure the default providers suggest service was queried. TestURLFetcher* default_fetcher = test_factory_.GetFetcherByID( SearchProvider::kDefaultProviderURLFetcherID); ASSERT_TRUE(default_fetcher); // Tell the SearchProvider the default suggest query is done. default_fetcher->delegate()->OnURLFetchComplete( default_fetcher, GURL(), net::URLRequestStatus(), 200, ResponseCookies(), std::string()); default_fetcher = NULL; // Make sure the keyword providers suggest service was queried. TestURLFetcher* keyword_fetcher = test_factory_.GetFetcherByID( SearchProvider::kKeywordProviderURLFetcherID); ASSERT_TRUE(keyword_fetcher); // And the URL matches what we expected. GURL expected_url = GURL(keyword_t_url_->suggestions_url()-> ReplaceSearchTerms(*keyword_t_url_, term, 0, string16())); ASSERT_TRUE(keyword_fetcher->original_url() == expected_url); // Tell the SearchProvider the keyword suggest query is done. keyword_fetcher->delegate()->OnURLFetchComplete( keyword_fetcher, GURL(), net::URLRequestStatus(), 200, ResponseCookies(), std::string()); keyword_fetcher = NULL; // Run till the history results complete. RunTillProviderDone(); // The SearchProvider is done. Make sure it has a result for the history // term keyword. AutocompleteMatch match = FindMatchWithDestination(keyword_url_); ASSERT_TRUE(!match.destination_url.is_empty()); // The match should have a TemplateURL. EXPECT_TRUE(match.template_url); // The fill into edit should contain the keyword. EXPECT_EQ(keyword_t_url_->keyword() + char16(' ') + keyword_term_, match.fill_into_edit); } TEST_F(SearchProviderTest, DontSendPrivateDataToSuggest) { // None of the following input strings should be sent to the suggest server, // because they may contain private data. const char* inputs[] = { "username:password", "http://username:password", "https://username:password", "username:password@hostname", "http://username:password@hostname/", "file://filename", "data://data", "unknownscheme:anything", "http://hostname/?query=q", "http://hostname/path#ref", "https://hostname/path", }; for (size_t i = 0; i < arraysize(inputs); ++i) { QueryForInput(ASCIIToUTF16(inputs[i]), false, false); // Make sure the default providers suggest service was not queried. ASSERT_TRUE(test_factory_.GetFetcherByID( SearchProvider::kDefaultProviderURLFetcherID) == NULL); // Run till the history results complete. RunTillProviderDone(); } } // Make sure FinalizeInstantQuery works. TEST_F(SearchProviderTest, FinalizeInstantQuery) { PrefService* service = profile_.GetPrefs(); service->SetBoolean(prefs::kInstantEnabled, true); QueryForInput(ASCIIToUTF16("foo"), false, false); // Wait until history and the suggest query complete. profile_.BlockUntilHistoryProcessesPendingRequests(); ASSERT_NO_FATAL_FAILURE(FinishDefaultSuggestQuery()); // When instant is enabled the provider isn't done until it hears from // instant. EXPECT_FALSE(provider_->done()); // Tell the provider instant is done. provider_->FinalizeInstantQuery(ASCIIToUTF16("foo"), ASCIIToUTF16("bar")); // The provider should now be done. EXPECT_TRUE(provider_->done()); // There should be two matches, one for what you typed, the other for // 'foobar'. EXPECT_EQ(2u, provider_->matches().size()); GURL instant_url = GURL(default_t_url_->url()->ReplaceSearchTerms( *default_t_url_, ASCIIToUTF16("foobar"), 0, string16())); AutocompleteMatch instant_match = FindMatchWithDestination(instant_url); EXPECT_TRUE(!instant_match.destination_url.is_empty()); // And the 'foobar' match should have a description. EXPECT_FALSE(instant_match.description.empty()); // Make sure the what you typed match has no description. GURL what_you_typed_url = GURL(default_t_url_->url()->ReplaceSearchTerms( *default_t_url_, ASCIIToUTF16("foo"), 0, string16())); AutocompleteMatch what_you_typed_match = FindMatchWithDestination(what_you_typed_url); EXPECT_TRUE(!what_you_typed_match.destination_url.is_empty()); EXPECT_TRUE(what_you_typed_match.description.empty()); // The instant search should be more relevant. EXPECT_GT(instant_match.relevance, what_you_typed_match.relevance); } // Make sure that if FinalizeInstantQuery is invoked before suggest results // return, the suggest text from FinalizeInstantQuery is remembered. TEST_F(SearchProviderTest, RememberInstantQuery) { PrefService* service = profile_.GetPrefs(); service->SetBoolean(prefs::kInstantEnabled, true); QueryForInput(ASCIIToUTF16("foo"), false, false); // Finalize the instant query immediately. provider_->FinalizeInstantQuery(ASCIIToUTF16("foo"), ASCIIToUTF16("bar")); // There should be two matches, one for what you typed, the other for // 'foobar'. EXPECT_EQ(2u, provider_->matches().size()); GURL instant_url = GURL(default_t_url_->url()->ReplaceSearchTerms( *default_t_url_, ASCIIToUTF16("foobar"), 0, string16())); AutocompleteMatch instant_match = FindMatchWithDestination(instant_url); EXPECT_FALSE(instant_match.destination_url.is_empty()); // Wait until history and the suggest query complete. profile_.BlockUntilHistoryProcessesPendingRequests(); ASSERT_NO_FATAL_FAILURE(FinishDefaultSuggestQuery()); // Provider should be done. EXPECT_TRUE(provider_->done()); // There should be two matches, one for what you typed, the other for // 'foobar'. EXPECT_EQ(2u, provider_->matches().size()); instant_match = FindMatchWithDestination(instant_url); EXPECT_FALSE(instant_match.destination_url.is_empty()); // And the 'foobar' match should have a description. EXPECT_FALSE(instant_match.description.empty()); } // Make sure that if trailing whitespace is added to the text supplied to // AutocompleteInput the default suggest text is cleared. TEST_F(SearchProviderTest, DifferingText) { PrefService* service = profile_.GetPrefs(); service->SetBoolean(prefs::kInstantEnabled, true); QueryForInput(ASCIIToUTF16("foo"), false, false); // Wait until history and the suggest query complete. profile_.BlockUntilHistoryProcessesPendingRequests(); ASSERT_NO_FATAL_FAILURE(FinishDefaultSuggestQuery()); // Finalize the instant query immediately. provider_->FinalizeInstantQuery(ASCIIToUTF16("foo"), ASCIIToUTF16("bar")); // Query with input that ends up getting trimmed to be the same as was // originally supplied. QueryForInput(ASCIIToUTF16("foo "), false, true); // There should only one match, for what you typed. EXPECT_EQ(1u, provider_->matches().size()); GURL instant_url = GURL(default_t_url_->url()->ReplaceSearchTerms( *default_t_url_, ASCIIToUTF16("foo"), 0, string16())); AutocompleteMatch instant_match = FindMatchWithDestination(instant_url); EXPECT_FALSE(instant_match.destination_url.is_empty()); } TEST_F(SearchProviderTest, DontAutocompleteURLLikeTerms) { profile_.CreateAutocompleteClassifier(); string16 term(ASCIIToUTF16("docs.google.com")); HistoryService* history = profile_.GetHistoryService(Profile::EXPLICIT_ACCESS); GURL url = GURL(default_t_url_->url()->ReplaceSearchTerms( *default_t_url_, term, 0, string16())); history->AddPageWithDetails( url, string16(), 1, 1, base::Time::Now(), false, history::SOURCE_BROWSED); history->SetKeywordSearchTermsForURL(url, default_t_url_->id(), term); // Add the term as a url. history->AddPageWithDetails( GURL("http://docs.google.com"), string16(), 1, 1, base::Time::Now(), false, history::SOURCE_BROWSED); profile_.BlockUntilHistoryProcessesPendingRequests(); QueryForInput(ASCIIToUTF16("docs"), false, false); // Wait until history and the suggest query complete. profile_.BlockUntilHistoryProcessesPendingRequests(); ASSERT_NO_FATAL_FAILURE(FinishDefaultSuggestQuery()); // Provider should be done. EXPECT_TRUE(provider_->done()); // There should be two matches, one for what you typed, the other for // 'docs.google.com'. The search term should have a lower priority than the // what you typed match. ASSERT_EQ(2u, provider_->matches().size()); AutocompleteMatch term_match = FindMatchWithDestination(url); GURL what_you_typed_url = GURL(default_t_url_->url()->ReplaceSearchTerms( *default_t_url_, ASCIIToUTF16("docs"), 0, string16())); AutocompleteMatch what_you_typed_match = FindMatchWithDestination(what_you_typed_url); EXPECT_GT(what_you_typed_match.relevance, term_match.relevance); }