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/run_loop.h"
6#include "chrome/browser/chromeos/geolocation/geoposition.h"
7#include "chrome/browser/chromeos/timezone/timezone_provider.h"
8#include "content/public/test/test_browser_thread_bundle.h"
9#include "net/http/http_response_headers.h"
10#include "net/http/http_status_code.h"
11#include "net/url_request/test_url_fetcher_factory.h"
12#include "net/url_request/url_fetcher_impl.h"
13#include "net/url_request/url_request_status.h"
14#include "testing/gtest/include/gtest/gtest.h"
15
16namespace {
17
18const int kRequestRetryIntervalMilliSeconds = 200;
19
20// This should be different from default to prevent TimeZoneRequest
21// from modifying it.
22const char kTestTimeZoneProviderUrl[] =
23    "https://localhost/maps/api/timezone/json?";
24
25const char kSimpleResponseBody[] =
26    "{\n"
27    "    \"dstOffset\" : 0.0,\n"
28    "    \"rawOffset\" : -28800.0,\n"
29    "    \"status\" : \"OK\",\n"
30    "    \"timeZoneId\" : \"America/Los_Angeles\",\n"
31    "    \"timeZoneName\" : \"Pacific Standard Time\"\n"
32    "}";
33
34struct SimpleRequest {
35  SimpleRequest()
36      : url("https://localhost/maps/api/timezone/"
37            "json?location=39.603481,-119.682251&timestamp=1331161200&sensor="
38            "false"),
39        http_response(kSimpleResponseBody) {
40    position.latitude = 39.6034810;
41    position.longitude = -119.6822510;
42    position.accuracy = 1;
43    position.error_code = 0;
44    position.timestamp = base::Time::FromTimeT(1331161200);
45    position.status = chromeos::Geoposition::STATUS_NONE;
46    EXPECT_EQ(
47        "latitude=39.603481, longitude=-119.682251, accuracy=1.000000, "
48        "error_code=0, error_message='', status=0 (NONE)",
49        position.ToString());
50
51    timezone.dstOffset = 0;
52    timezone.rawOffset = -28800;
53    timezone.timeZoneId = "America/Los_Angeles";
54    timezone.timeZoneName = "Pacific Standard Time";
55    timezone.error_message.erase();
56    timezone.status = chromeos::TimeZoneResponseData::OK;
57    EXPECT_EQ(
58        "dstOffset=0.000000, rawOffset=-28800.000000, "
59        "timeZoneId='America/Los_Angeles', timeZoneName='Pacific Standard "
60        "Time', error_message='', status=0 (OK)",
61        timezone.ToStringForDebug());
62  }
63
64  GURL url;
65  chromeos::Geoposition position;
66  std::string http_response;
67  chromeos::TimeZoneResponseData timezone;
68};
69
70}  // anonymous namespace
71
72namespace chromeos {
73
74// This is helper class for net::FakeURLFetcherFactory.
75class TestTimeZoneAPIURLFetcherCallback {
76 public:
77  TestTimeZoneAPIURLFetcherCallback(const GURL& url,
78                                    const size_t require_retries,
79                                    const std::string& response,
80                                    TimeZoneProvider* provider)
81      : url_(url),
82        require_retries_(require_retries),
83        response_(response),
84        factory_(NULL),
85        attempts_(0),
86        provider_(provider) {}
87
88  scoped_ptr<net::FakeURLFetcher> CreateURLFetcher(
89      const GURL& url,
90      net::URLFetcherDelegate* delegate,
91      const std::string& response_data,
92      net::HttpStatusCode response_code,
93      net::URLRequestStatus::Status status) {
94    EXPECT_EQ(provider_->requests_.size(), 1U);
95
96    TimeZoneRequest* timezone_request = provider_->requests_[0];
97
98    const base::TimeDelta base_retry_interval =
99        base::TimeDelta::FromMilliseconds(kRequestRetryIntervalMilliSeconds);
100    timezone_request->set_retry_sleep_on_server_error_for_testing(
101        base_retry_interval);
102    timezone_request->set_retry_sleep_on_bad_response_for_testing(
103        base_retry_interval);
104
105    ++attempts_;
106    if (attempts_ > require_retries_) {
107      response_code = net::HTTP_OK;
108      status = net::URLRequestStatus::SUCCESS;
109      factory_->SetFakeResponse(url, response_, response_code, status);
110    }
111    scoped_ptr<net::FakeURLFetcher> fetcher(new net::FakeURLFetcher(
112        url, delegate, response_, response_code, status));
113    scoped_refptr<net::HttpResponseHeaders> download_headers =
114        new net::HttpResponseHeaders(std::string());
115    download_headers->AddHeader("Content-Type: application/json");
116    fetcher->set_response_headers(download_headers);
117    return fetcher.Pass();
118  }
119
120  void Initialize(net::FakeURLFetcherFactory* factory) {
121    factory_ = factory;
122    factory_->SetFakeResponse(url_,
123                              std::string(),
124                              net::HTTP_INTERNAL_SERVER_ERROR,
125                              net::URLRequestStatus::FAILED);
126  }
127
128  size_t attempts() const { return attempts_; }
129
130 private:
131  const GURL url_;
132  // Respond with OK on required retry attempt.
133  const size_t require_retries_;
134  std::string response_;
135  net::FakeURLFetcherFactory* factory_;
136  size_t attempts_;
137  TimeZoneProvider* provider_;
138
139  DISALLOW_COPY_AND_ASSIGN(TestTimeZoneAPIURLFetcherCallback);
140};
141
142// This implements fake TimeZone API remote endpoint.
143// Response data is served to TimeZoneProvider via
144// net::FakeURLFetcher.
145class TimeZoneAPIFetcherFactory {
146 public:
147  TimeZoneAPIFetcherFactory(const GURL& url,
148                            const std::string& response,
149                            const size_t require_retries,
150                            TimeZoneProvider* provider) {
151    url_callback_.reset(new TestTimeZoneAPIURLFetcherCallback(
152        url, require_retries, response, provider));
153    net::URLFetcherImpl::set_factory(NULL);
154    fetcher_factory_.reset(new net::FakeURLFetcherFactory(
155        NULL,
156        base::Bind(&TestTimeZoneAPIURLFetcherCallback::CreateURLFetcher,
157                   base::Unretained(url_callback_.get()))));
158    url_callback_->Initialize(fetcher_factory_.get());
159  }
160
161  size_t attempts() const { return url_callback_->attempts(); }
162
163 private:
164  scoped_ptr<TestTimeZoneAPIURLFetcherCallback> url_callback_;
165  scoped_ptr<net::FakeURLFetcherFactory> fetcher_factory_;
166
167  DISALLOW_COPY_AND_ASSIGN(TimeZoneAPIFetcherFactory);
168};
169
170class TimeZoneReceiver {
171 public:
172  TimeZoneReceiver() : server_error_(false) {}
173
174  void OnRequestDone(scoped_ptr<TimeZoneResponseData> timezone,
175                     bool server_error) {
176    timezone_ = timezone.Pass();
177    server_error_ = server_error;
178
179    message_loop_runner_->Quit();
180  }
181
182  void WaitUntilRequestDone() {
183    message_loop_runner_.reset(new base::RunLoop);
184    message_loop_runner_->Run();
185  }
186
187  const TimeZoneResponseData* timezone() const { return timezone_.get(); }
188  bool server_error() const { return server_error_; }
189
190 private:
191  scoped_ptr<TimeZoneResponseData> timezone_;
192  bool server_error_;
193  scoped_ptr<base::RunLoop> message_loop_runner_;
194};
195
196class TimeZoneTest : public testing::Test {
197 private:
198  content::TestBrowserThreadBundle thread_bundle_;
199};
200
201TEST_F(TimeZoneTest, ResponseOK) {
202  TimeZoneProvider provider(NULL, GURL(kTestTimeZoneProviderUrl));
203  const SimpleRequest simple_request;
204
205  TimeZoneAPIFetcherFactory url_factory(simple_request.url,
206                                        simple_request.http_response,
207                                        0 /* require_retries */,
208                                        &provider);
209
210  TimeZoneReceiver receiver;
211
212  provider.RequestTimezone(simple_request.position,
213                           false,
214                           base::TimeDelta::FromSeconds(1),
215                           base::Bind(&TimeZoneReceiver::OnRequestDone,
216                                      base::Unretained(&receiver)));
217  receiver.WaitUntilRequestDone();
218
219  EXPECT_EQ(simple_request.timezone.ToStringForDebug(),
220            receiver.timezone()->ToStringForDebug());
221  EXPECT_FALSE(receiver.server_error());
222  EXPECT_EQ(1U, url_factory.attempts());
223}
224
225TEST_F(TimeZoneTest, ResponseOKWithRetries) {
226  TimeZoneProvider provider(NULL, GURL(kTestTimeZoneProviderUrl));
227  const SimpleRequest simple_request;
228
229  TimeZoneAPIFetcherFactory url_factory(simple_request.url,
230                                        simple_request.http_response,
231                                        3 /* require_retries */,
232                                        &provider);
233
234  TimeZoneReceiver receiver;
235
236  provider.RequestTimezone(simple_request.position,
237                           false,
238                           base::TimeDelta::FromSeconds(1),
239                           base::Bind(&TimeZoneReceiver::OnRequestDone,
240                                      base::Unretained(&receiver)));
241  receiver.WaitUntilRequestDone();
242  EXPECT_EQ(simple_request.timezone.ToStringForDebug(),
243            receiver.timezone()->ToStringForDebug());
244  EXPECT_FALSE(receiver.server_error());
245  EXPECT_EQ(4U, url_factory.attempts());
246}
247
248TEST_F(TimeZoneTest, InvalidResponse) {
249  TimeZoneProvider provider(NULL, GURL(kTestTimeZoneProviderUrl));
250  const SimpleRequest simple_request;
251
252  TimeZoneAPIFetcherFactory url_factory(simple_request.url,
253                                        "invalid JSON string",
254                                        0 /* require_retries */,
255                                        &provider);
256
257  TimeZoneReceiver receiver;
258
259  const int timeout_seconds = 1;
260  size_t expected_retries = static_cast<size_t>(
261      timeout_seconds * 1000 / kRequestRetryIntervalMilliSeconds);
262  ASSERT_GE(expected_retries, 2U);
263
264  provider.RequestTimezone(simple_request.position,
265                           false,
266                           base::TimeDelta::FromSeconds(timeout_seconds),
267                           base::Bind(&TimeZoneReceiver::OnRequestDone,
268                                      base::Unretained(&receiver)));
269  receiver.WaitUntilRequestDone();
270  EXPECT_EQ(
271      "dstOffset=0.000000, rawOffset=0.000000, timeZoneId='', timeZoneName='', "
272      "error_message='TimeZone provider at 'https://localhost/' : JSONReader "
273      "failed: Line: 1, column: 1, Unexpected token..', status=6 "
274      "(REQUEST_ERROR)",
275      receiver.timezone()->ToStringForDebug());
276  EXPECT_FALSE(receiver.server_error());
277  EXPECT_GE(url_factory.attempts(), 2U);
278  if (url_factory.attempts() > expected_retries + 1) {
279    LOG(WARNING) << "TimeZoneTest::InvalidResponse: Too many attempts ("
280                 << url_factory.attempts() << "), no more then "
281                 << expected_retries + 1 << " expected.";
282  }
283  if (url_factory.attempts() < expected_retries - 1) {
284    LOG(WARNING) << "TimeZoneTest::InvalidResponse: Too less attempts ("
285                 << url_factory.attempts() << "), greater then "
286                 << expected_retries - 1 << " expected.";
287  }
288}
289
290}  // namespace chromeos
291