1// Copyright 2014 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5#include "base/command_line.h" 6#include "base/message_loop/message_loop.h" 7#include "base/strings/utf_string_conversions.h" 8#include "components/metrics/proto/omnibox_event.pb.h" 9#include "components/omnibox/autocomplete_match.h" 10#include "components/omnibox/autocomplete_scheme_classifier.h" 11#include "components/omnibox/keyword_provider.h" 12#include "components/search_engines/search_engines_switches.h" 13#include "components/search_engines/template_url.h" 14#include "components/search_engines/template_url_service.h" 15#include "testing/gtest/include/gtest/gtest.h" 16#include "url/gurl.h" 17 18using base::ASCIIToUTF16; 19 20namespace { 21 22class TestingSchemeClassifier : public AutocompleteSchemeClassifier { 23 public: 24 virtual metrics::OmniboxInputType::Type GetInputTypeForScheme( 25 const std::string& scheme) const OVERRIDE { 26 if (net::URLRequest::IsHandledProtocol(scheme)) 27 return metrics::OmniboxInputType::URL; 28 return metrics::OmniboxInputType::INVALID; 29 } 30}; 31 32} // namespace 33 34class KeywordProviderTest : public testing::Test { 35 protected: 36 template<class ResultType> 37 struct MatchType { 38 const ResultType member; 39 bool allowed_to_be_default_match; 40 }; 41 42 template<class ResultType> 43 struct TestData { 44 const base::string16 input; 45 const size_t num_results; 46 const MatchType<ResultType> output[3]; 47 }; 48 49 KeywordProviderTest() : kw_provider_(NULL) { } 50 virtual ~KeywordProviderTest() { } 51 52 virtual void SetUp(); 53 virtual void TearDown(); 54 55 template<class ResultType> 56 void RunTest(TestData<ResultType>* keyword_cases, 57 int num_cases, 58 ResultType AutocompleteMatch::* member); 59 60 protected: 61 static const TemplateURLService::Initializer kTestData[]; 62 63 scoped_refptr<KeywordProvider> kw_provider_; 64 scoped_ptr<TemplateURLService> model_; 65}; 66 67// static 68const TemplateURLService::Initializer KeywordProviderTest::kTestData[] = { 69 { "aa", "aa.com?foo={searchTerms}", "aa" }, 70 { "aaaa", "http://aaaa/?aaaa=1&b={searchTerms}&c", "aaaa" }, 71 { "aaaaa", "{searchTerms}", "aaaaa" }, 72 { "ab", "bogus URL {searchTerms}", "ab" }, 73 { "weasel", "weasel{searchTerms}weasel", "weasel" }, 74 { "www", " +%2B?={searchTerms}foo ", "www" }, 75 { "nonsub", "http://nonsubstituting-keyword.com/", "nonsub" }, 76 { "z", "{searchTerms}=z", "z" }, 77}; 78 79void KeywordProviderTest::SetUp() { 80 model_.reset(new TemplateURLService(kTestData, arraysize(kTestData))); 81 kw_provider_ = new KeywordProvider(NULL, model_.get()); 82} 83 84void KeywordProviderTest::TearDown() { 85 model_.reset(); 86 kw_provider_ = NULL; 87} 88 89template<class ResultType> 90void KeywordProviderTest::RunTest(TestData<ResultType>* keyword_cases, 91 int num_cases, 92 ResultType AutocompleteMatch::* member) { 93 ACMatches matches; 94 for (int i = 0; i < num_cases; ++i) { 95 SCOPED_TRACE(keyword_cases[i].input); 96 AutocompleteInput input(keyword_cases[i].input, base::string16::npos, 97 base::string16(), GURL(), 98 metrics::OmniboxEventProto::INVALID_SPEC, true, 99 false, true, true, TestingSchemeClassifier()); 100 kw_provider_->Start(input, false); 101 EXPECT_TRUE(kw_provider_->done()); 102 matches = kw_provider_->matches(); 103 ASSERT_EQ(keyword_cases[i].num_results, matches.size()); 104 for (size_t j = 0; j < matches.size(); ++j) { 105 EXPECT_EQ(keyword_cases[i].output[j].member, matches[j].*member); 106 EXPECT_EQ(keyword_cases[i].output[j].allowed_to_be_default_match, 107 matches[j].allowed_to_be_default_match); 108 } 109 } 110} 111 112TEST_F(KeywordProviderTest, Edit) { 113 const MatchType<base::string16> kEmptyMatch = { base::string16(), false }; 114 TestData<base::string16> edit_cases[] = { 115 // Searching for a nonexistent prefix should give nothing. 116 { ASCIIToUTF16("Not Found"), 0, 117 { kEmptyMatch, kEmptyMatch, kEmptyMatch } }, 118 { ASCIIToUTF16("aaaaaNot Found"), 0, 119 { kEmptyMatch, kEmptyMatch, kEmptyMatch } }, 120 121 // Check that tokenization only collapses whitespace between first tokens, 122 // no-query-input cases have a space appended, and action is not escaped. 123 { ASCIIToUTF16("z"), 1, 124 { { ASCIIToUTF16("z "), true }, kEmptyMatch, kEmptyMatch } }, 125 { ASCIIToUTF16("z \t"), 1, 126 { { ASCIIToUTF16("z "), true }, kEmptyMatch, kEmptyMatch } }, 127 128 // Check that exact, substituting keywords with a verbatim search term 129 // don't generate a result. (These are handled by SearchProvider.) 130 { ASCIIToUTF16("z foo"), 0, 131 { kEmptyMatch, kEmptyMatch, kEmptyMatch } }, 132 { ASCIIToUTF16("z a b c++"), 0, 133 { kEmptyMatch, kEmptyMatch, kEmptyMatch } }, 134 135 // Matches should be limited to three, and sorted in quality order, not 136 // alphabetical. 137 { ASCIIToUTF16("aaa"), 2, 138 { { ASCIIToUTF16("aaaa "), false }, 139 { ASCIIToUTF16("aaaaa "), false }, 140 kEmptyMatch } }, 141 { ASCIIToUTF16("a 1 2 3"), 3, 142 { { ASCIIToUTF16("aa 1 2 3"), false }, 143 { ASCIIToUTF16("ab 1 2 3"), false }, 144 { ASCIIToUTF16("aaaa 1 2 3"), false } } }, 145 { ASCIIToUTF16("www.a"), 3, 146 { { ASCIIToUTF16("aa "), false }, 147 { ASCIIToUTF16("ab "), false }, 148 { ASCIIToUTF16("aaaa "), false } } }, 149 // Exact matches should prevent returning inexact matches. Also, the 150 // verbatim query for this keyword match should not be returned. (It's 151 // returned by SearchProvider.) 152 { ASCIIToUTF16("aaaa foo"), 0, 153 { kEmptyMatch, kEmptyMatch, kEmptyMatch } }, 154 { ASCIIToUTF16("www.aaaa foo"), 0, 155 { kEmptyMatch, kEmptyMatch, kEmptyMatch } }, 156 157 // Clean up keyword input properly. "http" and "https" are the only 158 // allowed schemes. 159 { ASCIIToUTF16("www"), 1, 160 { { ASCIIToUTF16("www "), true }, kEmptyMatch, kEmptyMatch }}, 161 { ASCIIToUTF16("www."), 0, 162 { kEmptyMatch, kEmptyMatch, kEmptyMatch } }, 163 { ASCIIToUTF16("www.w w"), 2, 164 { { ASCIIToUTF16("www w"), false }, 165 { ASCIIToUTF16("weasel w"), false }, 166 kEmptyMatch } }, 167 { ASCIIToUTF16("http://www"), 1, 168 { { ASCIIToUTF16("www "), true }, kEmptyMatch, kEmptyMatch } }, 169 { ASCIIToUTF16("http://www."), 0, 170 { kEmptyMatch, kEmptyMatch, kEmptyMatch } }, 171 { ASCIIToUTF16("ftp: blah"), 0, 172 { kEmptyMatch, kEmptyMatch, kEmptyMatch } }, 173 { ASCIIToUTF16("mailto:z"), 0, 174 { kEmptyMatch, kEmptyMatch, kEmptyMatch } }, 175 { ASCIIToUTF16("ftp://z"), 0, 176 { kEmptyMatch, kEmptyMatch, kEmptyMatch } }, 177 { ASCIIToUTF16("https://z"), 1, 178 { { ASCIIToUTF16("z "), true }, kEmptyMatch, kEmptyMatch } }, 179 180 // Non-substituting keywords, whether typed fully or not 181 // should not add a space. 182 { ASCIIToUTF16("nonsu"), 1, 183 { { ASCIIToUTF16("nonsub"), false }, kEmptyMatch, kEmptyMatch } }, 184 { ASCIIToUTF16("nonsub"), 1, 185 { { ASCIIToUTF16("nonsub"), true }, kEmptyMatch, kEmptyMatch } }, 186 }; 187 188 RunTest<base::string16>(edit_cases, arraysize(edit_cases), 189 &AutocompleteMatch::fill_into_edit); 190} 191 192TEST_F(KeywordProviderTest, URL) { 193 const MatchType<GURL> kEmptyMatch = { GURL(), false }; 194 TestData<GURL> url_cases[] = { 195 // No query input -> empty destination URL. 196 { ASCIIToUTF16("z"), 1, 197 { { GURL(), true }, kEmptyMatch, kEmptyMatch } }, 198 { ASCIIToUTF16("z \t"), 1, 199 { { GURL(), true }, kEmptyMatch, kEmptyMatch } }, 200 201 // Check that tokenization only collapses whitespace between first tokens 202 // and query input, but not rest of URL, is escaped. 203 { ASCIIToUTF16("w bar +baz"), 2, 204 { { GURL(" +%2B?=bar+%2Bbazfoo "), false }, 205 { GURL("bar+%2Bbaz=z"), false }, 206 kEmptyMatch } }, 207 208 // Substitution should work with various locations of the "%s". 209 { ASCIIToUTF16("aaa 1a2b"), 2, 210 { { GURL("http://aaaa/?aaaa=1&b=1a2b&c"), false }, 211 { GURL("1a2b"), false }, 212 kEmptyMatch } }, 213 { ASCIIToUTF16("a 1 2 3"), 3, 214 { { GURL("aa.com?foo=1+2+3"), false }, 215 { GURL("bogus URL 1+2+3"), false }, 216 { GURL("http://aaaa/?aaaa=1&b=1+2+3&c"), false } } }, 217 { ASCIIToUTF16("www.w w"), 2, 218 { { GURL(" +%2B?=wfoo "), false }, 219 { GURL("weaselwweasel"), false }, 220 kEmptyMatch } }, 221 }; 222 223 RunTest<GURL>(url_cases, arraysize(url_cases), 224 &AutocompleteMatch::destination_url); 225} 226 227TEST_F(KeywordProviderTest, Contents) { 228 const MatchType<base::string16> kEmptyMatch = { base::string16(), false }; 229 TestData<base::string16> contents_cases[] = { 230 // No query input -> substitute "<enter query>" into contents. 231 { ASCIIToUTF16("z"), 1, 232 { { ASCIIToUTF16("Search z for <enter query>"), true }, 233 kEmptyMatch, kEmptyMatch } }, 234 { ASCIIToUTF16("z \t"), 1, 235 { { ASCIIToUTF16("Search z for <enter query>"), true }, 236 kEmptyMatch, kEmptyMatch } }, 237 238 // Exact keyword matches with remaining text should return nothing. 239 { ASCIIToUTF16("www.www www"), 0, 240 { kEmptyMatch, kEmptyMatch, kEmptyMatch } }, 241 { ASCIIToUTF16("z a b c++"), 0, 242 { kEmptyMatch, kEmptyMatch, kEmptyMatch } }, 243 244 // Exact keyword matches with remaining text when the keyword is an 245 // extension keyword should return something. This is tested in 246 // chrome/browser/extensions/api/omnibox/omnibox_apitest.cc's 247 // in OmniboxApiTest's Basic test. 248 249 // Substitution should work with various locations of the "%s". 250 { ASCIIToUTF16("aaa"), 2, 251 { { ASCIIToUTF16("Search aaaa for <enter query>"), false }, 252 { ASCIIToUTF16("Search aaaaa for <enter query>"), false }, 253 kEmptyMatch} }, 254 { ASCIIToUTF16("www.w w"), 2, 255 { { ASCIIToUTF16("Search www for w"), false }, 256 { ASCIIToUTF16("Search weasel for w"), false }, 257 kEmptyMatch } }, 258 // Also, check that tokenization only collapses whitespace between first 259 // tokens and contents are not escaped or unescaped. 260 { ASCIIToUTF16("a 1 2+ 3"), 3, 261 { { ASCIIToUTF16("Search aa for 1 2+ 3"), false }, 262 { ASCIIToUTF16("Search ab for 1 2+ 3"), false }, 263 { ASCIIToUTF16("Search aaaa for 1 2+ 3"), false } } }, 264 }; 265 266 RunTest<base::string16>(contents_cases, arraysize(contents_cases), 267 &AutocompleteMatch::contents); 268} 269 270TEST_F(KeywordProviderTest, AddKeyword) { 271 TemplateURLData data; 272 data.short_name = ASCIIToUTF16("Test"); 273 base::string16 keyword(ASCIIToUTF16("foo")); 274 data.SetKeyword(keyword); 275 data.SetURL("http://www.google.com/foo?q={searchTerms}"); 276 TemplateURL* template_url = new TemplateURL(data); 277 model_->Add(template_url); 278 ASSERT_TRUE(template_url == model_->GetTemplateURLForKeyword(keyword)); 279} 280 281TEST_F(KeywordProviderTest, RemoveKeyword) { 282 base::string16 url(ASCIIToUTF16("http://aaaa/?aaaa=1&b={searchTerms}&c")); 283 model_->Remove(model_->GetTemplateURLForKeyword(ASCIIToUTF16("aaaa"))); 284 ASSERT_TRUE(model_->GetTemplateURLForKeyword(ASCIIToUTF16("aaaa")) == NULL); 285} 286 287TEST_F(KeywordProviderTest, GetKeywordForInput) { 288 EXPECT_EQ(ASCIIToUTF16("aa"), 289 kw_provider_->GetKeywordForText(ASCIIToUTF16("aa"))); 290 EXPECT_EQ(base::string16(), 291 kw_provider_->GetKeywordForText(ASCIIToUTF16("aafoo"))); 292 EXPECT_EQ(base::string16(), 293 kw_provider_->GetKeywordForText(ASCIIToUTF16("aa foo"))); 294} 295 296TEST_F(KeywordProviderTest, GetSubstitutingTemplateURLForInput) { 297 struct { 298 const std::string text; 299 const size_t cursor_position; 300 const bool allow_exact_keyword_match; 301 const std::string expected_url; 302 const std::string updated_text; 303 const size_t updated_cursor_position; 304 } cases[] = { 305 { "foo", base::string16::npos, true, "", "foo", base::string16::npos }, 306 { "aa foo", base::string16::npos, true, "aa.com?foo={searchTerms}", "foo", 307 base::string16::npos }, 308 309 // Cursor adjustment. 310 { "aa foo", base::string16::npos, true, "aa.com?foo={searchTerms}", "foo", 311 base::string16::npos }, 312 { "aa foo", 4u, true, "aa.com?foo={searchTerms}", "foo", 1u }, 313 // Cursor at the end. 314 { "aa foo", 6u, true, "aa.com?foo={searchTerms}", "foo", 3u }, 315 // Cursor before the first character of the remaining text. 316 { "aa foo", 3u, true, "aa.com?foo={searchTerms}", "foo", 0u }, 317 318 // Trailing space. 319 { "aa foo ", 7u, true, "aa.com?foo={searchTerms}", "foo ", 4u }, 320 // Trailing space without remaining text, cursor in the middle. 321 { "aa ", 3u, true, "aa.com?foo={searchTerms}", "", base::string16::npos }, 322 // Trailing space without remaining text, cursor at the end. 323 { "aa ", 4u, true, "aa.com?foo={searchTerms}", "", base::string16::npos }, 324 // Extra space after keyword, cursor at the end. 325 { "aa foo ", 8u, true, "aa.com?foo={searchTerms}", "foo ", 4u }, 326 // Extra space after keyword, cursor in the middle. 327 { "aa foo ", 3u, true, "aa.com?foo={searchTerms}", "foo ", 0 }, 328 // Extra space after keyword, no trailing space, cursor at the end. 329 { "aa foo", 7u, true, "aa.com?foo={searchTerms}", "foo", 3u }, 330 // Extra space after keyword, no trailing space, cursor in the middle. 331 { "aa foo", 5u, true, "aa.com?foo={searchTerms}", "foo", 1u }, 332 333 // Disallow exact keyword match. 334 { "aa foo", base::string16::npos, false, "", "aa foo", 335 base::string16::npos }, 336 }; 337 for (size_t i = 0; i < ARRAYSIZE_UNSAFE(cases); i++) { 338 AutocompleteInput input( 339 ASCIIToUTF16(cases[i].text), cases[i].cursor_position, base::string16(), 340 GURL(), metrics::OmniboxEventProto::INVALID_SPEC, false, false, 341 cases[i].allow_exact_keyword_match, true, TestingSchemeClassifier()); 342 const TemplateURL* url = 343 KeywordProvider::GetSubstitutingTemplateURLForInput(model_.get(), 344 &input); 345 if (cases[i].expected_url.empty()) 346 EXPECT_FALSE(url); 347 else 348 EXPECT_EQ(cases[i].expected_url, url->url()); 349 EXPECT_EQ(ASCIIToUTF16(cases[i].updated_text), input.text()); 350 EXPECT_EQ(cases[i].updated_cursor_position, input.cursor_position()); 351 } 352} 353 354// If extra query params are specified on the command line, they should be 355// reflected (only) in the default search provider's destination URL. 356TEST_F(KeywordProviderTest, ExtraQueryParams) { 357 CommandLine::ForCurrentProcess()->AppendSwitchASCII( 358 switches::kExtraSearchQueryParams, "a=b"); 359 360 TestData<GURL> url_cases[] = { 361 { ASCIIToUTF16("a 1 2 3"), 3, 362 { { GURL("aa.com?a=b&foo=1+2+3"), false }, 363 { GURL("bogus URL 1+2+3"), false }, 364 { GURL("http://aaaa/?aaaa=1&b=1+2+3&c"), false } } }, 365 }; 366 367 RunTest<GURL>(url_cases, arraysize(url_cases), 368 &AutocompleteMatch::destination_url); 369} 370