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