1// Copyright 2013 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 <string>
6
7#include "base/basictypes.h"
8#include "base/bind.h"
9#include "base/command_line.h"
10#include "base/memory/scoped_ptr.h"
11#include "base/run_loop.h"
12#include "base/strings/utf_string_conversions.h"
13#include "chrome/browser/profiles/profile_manager.h"
14#include "chrome/browser/ui/app_list/search/chrome_search_result.h"
15#include "chrome/browser/ui/app_list/search/webstore/webstore_provider.h"
16#include "chrome/browser/ui/app_list/search/webstore/webstore_result.h"
17#include "chrome/common/chrome_switches.h"
18#include "chrome/test/base/in_process_browser_test.h"
19#include "content/public/browser/browser_thread.h"
20#include "extensions/common/extension.h"
21#include "net/test/embedded_test_server/embedded_test_server.h"
22#include "net/test/embedded_test_server/http_request.h"
23#include "net/test/embedded_test_server/http_response.h"
24
25using content::BrowserThread;
26using extensions::Manifest;
27using net::test_server::BasicHttpResponse;
28using net::test_server::HttpRequest;
29using net::test_server::HttpResponse;
30using net::test_server::EmbeddedTestServer;
31
32namespace app_list {
33namespace test {
34namespace {
35
36// Mock results.
37const char kOneResult[] = "{"
38    "\"search_url\": \"http://host/search\","
39    "\"results\":["
40      "{"
41        "\"id\": \"app1_id\","
42        "\"localized_name\": \"app1 name\","
43        "\"icon_url\": \"http://host/icon\","
44        "\"is_paid\": false"
45      "}"
46    "]}";
47
48const char kThreeResults[] = "{"
49    "\"search_url\": \"http://host/search\","
50    "\"results\":["
51      "{"
52        "\"id\": \"app1_id\","
53        "\"localized_name\": \"one\","
54        "\"icon_url\": \"http://host/icon1\","
55        "\"is_paid\": true,"
56        "\"item_type\": \"PLATFORM_APP\""
57      "},"
58      "{"
59        "\"id\": \"app2_id\","
60        "\"localized_name\": \"two\","
61        "\"icon_url\": \"http://host/icon2\","
62        "\"is_paid\": false,"
63        "\"item_type\": \"HOSTED_APP\""
64      "},"
65      "{"
66        "\"id\": \"app3_id\","
67        "\"localized_name\": \"three\","
68        "\"icon_url\": \"http://host/icon3\","
69        "\"is_paid\": false,"
70        "\"item_type\": \"LEGACY_PACKAGED_APP\""
71      "}"
72    "]}";
73
74struct ParsedSearchResult {
75  const char* id;
76  const char* title;
77  const char* icon_url;
78  bool is_paid;
79  Manifest::Type item_type;
80  size_t num_actions;
81};
82
83ParsedSearchResult kParsedOneResult[] = {{"app1_id", "app1 name",
84                                          "http://host/icon", false,
85                                          Manifest::TYPE_UNKNOWN, 1}};
86
87ParsedSearchResult kParsedThreeResults[] = {
88    {"app1_id", "one", "http://host/icon1", true, Manifest::TYPE_PLATFORM_APP,
89     1},
90    {"app2_id", "two", "http://host/icon2", false, Manifest::TYPE_HOSTED_APP,
91     2},
92    {"app3_id", "three", "http://host/icon3", false,
93     Manifest::TYPE_LEGACY_PACKAGED_APP, 1}};
94
95}  // namespace
96
97class WebstoreProviderTest : public InProcessBrowserTest {
98 public:
99  WebstoreProviderTest() {}
100  virtual ~WebstoreProviderTest() {}
101
102  // InProcessBrowserTest overrides:
103  virtual void SetUpOnMainThread() OVERRIDE {
104    test_server_.reset(new EmbeddedTestServer);
105
106    ASSERT_TRUE(test_server_->InitializeAndWaitUntilReady());
107    test_server_->RegisterRequestHandler(
108        base::Bind(&WebstoreProviderTest::HandleRequest,
109                   base::Unretained(this)));
110    CommandLine::ForCurrentProcess()->AppendSwitchASCII(
111        switches::kAppsGalleryURL, test_server_->base_url().spec());
112    CommandLine::ForCurrentProcess()->AppendSwitch(
113        switches::kEnableEphemeralApps);
114
115    webstore_provider_.reset(new WebstoreProvider(
116        ProfileManager::GetActiveUserProfile(), NULL));
117    webstore_provider_->set_webstore_search_fetched_callback(
118        base::Bind(&WebstoreProviderTest::OnSearchResultsFetched,
119                   base::Unretained(this)));
120    // TODO(mukai): add test cases for throttling.
121    webstore_provider_->set_use_throttling(false);
122  }
123
124  virtual void TearDownOnMainThread() OVERRIDE {
125    EXPECT_TRUE(test_server_->ShutdownAndWaitUntilComplete());
126    test_server_.reset();
127  }
128
129  void RunQuery(const std::string& query,
130                const std::string& mock_server_response) {
131    webstore_provider_->Start(base::UTF8ToUTF16(query));
132
133    if (webstore_provider_->webstore_search_ && !mock_server_response.empty()) {
134      mock_server_response_ = mock_server_response;
135
136      DCHECK(!run_loop_);
137      run_loop_.reset(new base::RunLoop);
138      run_loop_->Run();
139      run_loop_.reset();
140
141      mock_server_response_.clear();
142    }
143
144    webstore_provider_->Stop();
145  }
146
147  std::string GetResultTitles() const {
148    std::string results;
149    for (SearchProvider::Results::const_iterator it =
150             webstore_provider_->results().begin();
151         it != webstore_provider_->results().end();
152         ++it) {
153      if (!results.empty())
154        results += ',';
155      results += base::UTF16ToUTF8((*it)->title());
156    }
157    return results;
158  }
159
160  void VerifyResults(const ParsedSearchResult* expected_results,
161                     size_t expected_result_size) {
162    ASSERT_EQ(expected_result_size, webstore_provider_->results().size());
163    for (size_t i = 0; i < expected_result_size; ++i) {
164      const SearchResult* result = webstore_provider_->results()[i];
165      ASSERT_EQ(extensions::Extension::GetBaseURLFromExtensionId(
166                    expected_results[i].id).spec(),
167                result->id());
168      EXPECT_EQ(std::string(expected_results[i].title),
169                base::UTF16ToUTF8(result->title()));
170
171      // Ensure the number of action buttons is appropriate for the item type.
172      EXPECT_EQ(expected_results[i].num_actions, result->actions().size());
173
174      const WebstoreResult* webstore_result =
175          static_cast<const WebstoreResult*>(result);
176      EXPECT_EQ(expected_results[i].id, webstore_result->app_id());
177      EXPECT_EQ(expected_results[i].icon_url,
178                webstore_result->icon_url().spec());
179      EXPECT_EQ(expected_results[i].is_paid, webstore_result->is_paid());
180      EXPECT_EQ(expected_results[i].item_type, webstore_result->item_type());
181    }
182  }
183
184  void RunQueryAndVerify(const std::string& query,
185                         const std::string& mock_server_response,
186                         const ParsedSearchResult* expected_results,
187                         size_t expected_result_size) {
188    RunQuery(query, mock_server_response);
189    VerifyResults(expected_results, expected_result_size);
190  }
191
192  WebstoreProvider* webstore_provider() { return webstore_provider_.get(); }
193
194 private:
195  scoped_ptr<HttpResponse> HandleRequest(const HttpRequest& request) {
196    scoped_ptr<BasicHttpResponse> response(new BasicHttpResponse);
197
198    if (request.relative_url.find("/jsonsearch?") != std::string::npos) {
199      if (mock_server_response_ == "404") {
200        response->set_code(net::HTTP_NOT_FOUND);
201      } else if (mock_server_response_ == "500") {
202        response->set_code(net::HTTP_INTERNAL_SERVER_ERROR);
203      } else {
204        response->set_code(net::HTTP_OK);
205        response->set_content(mock_server_response_);
206      }
207    }
208
209    return response.PassAs<HttpResponse>();
210  }
211
212  void OnSearchResultsFetched() {
213    if (run_loop_)
214      run_loop_->Quit();
215  }
216
217  scoped_ptr<EmbeddedTestServer> test_server_;
218  scoped_ptr<base::RunLoop> run_loop_;
219
220  std::string mock_server_response_;
221
222  scoped_ptr<WebstoreProvider> webstore_provider_;
223
224  DISALLOW_COPY_AND_ASSIGN(WebstoreProviderTest);
225};
226
227// Flaky on CrOS and Windows: http://crbug.com/246136.
228// TODO(erg): linux_aura bringup: http://crbug.com/163931
229#if defined(OS_WIN) || defined(OS_LINUX)
230#define MAYBE_Basic DISABLED_Basic
231#else
232#define MAYBE_Basic Basic
233#endif
234IN_PROC_BROWSER_TEST_F(WebstoreProviderTest, MAYBE_Basic) {
235  struct {
236    const char* query;
237    const char* mock_server_response;
238    const char* expected_result_titles;
239    const ParsedSearchResult* expected_results;
240    size_t expected_result_size;
241  } kTestCases[] = {
242    // "Search in web store" result with query text itself is used for
243    // synchronous placeholder, bad server response etc.
244    {"synchronous", "", "synchronous", NULL, 0 },
245    {"404", "404", "404", NULL, 0 },
246    {"500", "500", "500", NULL, 0 },
247    {"bad json", "invalid json", "bad json", NULL, 0 },
248    // Good results.
249    {"1 result", kOneResult, "app1 name", kParsedOneResult, 1 },
250    {"3 result", kThreeResults, "one,two,three", kParsedThreeResults, 3 },
251  };
252
253  for (size_t i = 0; i < ARRAYSIZE_UNSAFE(kTestCases); ++i) {
254    if (kTestCases[i].expected_result_titles) {
255      RunQuery(kTestCases[i].query, kTestCases[i].mock_server_response);
256      ASSERT_EQ(kTestCases[i].expected_result_titles, GetResultTitles())
257          << "Case " << i << ": q=" << kTestCases[i].query;
258
259      if (kTestCases[i].expected_results) {
260        VerifyResults(kTestCases[i].expected_results,
261                      kTestCases[i].expected_result_size);
262      }
263    }
264  }
265}
266
267IN_PROC_BROWSER_TEST_F(WebstoreProviderTest, NoSearchForSensitiveData) {
268  // None of the following input strings should be accepted because they may
269  // contain private data.
270  const char* inputs[] = {
271    // file: scheme is bad.
272    "file://filename",
273    "FILE://filename",
274    // URLs with usernames, ports, queries or refs are bad.
275    "http://username:password@hostname/",
276    "http://www.example.com:1000",
277    "http://foo:1000",
278    "http://hostname/?query=q",
279    "http://hostname/path#ref",
280    // A https URL with path is bad.
281    "https://hostname/path",
282  };
283
284  for (size_t i = 0; i < arraysize(inputs); ++i) {
285    RunQueryAndVerify(inputs[i], kOneResult, NULL, 0);
286  }
287}
288
289IN_PROC_BROWSER_TEST_F(WebstoreProviderTest, NoSearchForShortQueries) {
290  RunQueryAndVerify("a", kOneResult, NULL, 0);
291  RunQueryAndVerify("ab", kOneResult, NULL, 0);
292  RunQueryAndVerify("abc", kOneResult, kParsedOneResult, 1);
293}
294
295// Flaky on CrOS and Windows: http://crbug.com/246136.
296#if defined(OS_WIN) || defined(OS_CHROMEOS)
297#define MAYBE_SearchCache DISABLED_SearchCache
298#else
299#define MAYBE_SearchCache SearchCache
300#endif
301IN_PROC_BROWSER_TEST_F(WebstoreProviderTest, MAYBE_SearchCache) {
302  RunQueryAndVerify("foo", kOneResult, kParsedOneResult, 1);
303
304  // No result is provided but the provider gets the result from the cache.
305  RunQueryAndVerify("foo", "", kParsedOneResult, 1);
306}
307
308}  // namespace test
309}  // namespace app_list
310