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.
5 #include "sync/internal_api/public/attachments/attachment_downloader_impl.h"
8 #include "base/memory/weak_ptr.h"
9 #include "base/message_loop/message_loop.h"
10 #include "base/run_loop.h"
11 #include "base/thread_task_runner_handle.h"
12 #include "google_apis/gaia/fake_oauth2_token_service.h"
13 #include "google_apis/gaia/gaia_constants.h"
14 #include "net/http/http_response_headers.h"
15 #include "net/url_request/test_url_fetcher_factory.h"
16 #include "net/url_request/url_request_test_util.h"
17 #include "sync/api/attachments/attachment.h"
18 #include "sync/internal_api/public/attachments/attachment_uploader_impl.h"
19 #include "testing/gtest/include/gtest/gtest.h"
25 const char kAccountId[] = "attachments@gmail.com";
26 const char kAccessToken[] = "access.token";
27 const char kAttachmentServerUrl[] = "http://attachments.com/";
28 const char kAttachmentContent[] = "attachment.content";
30 // MockOAuth2TokenService remembers last request for access token and verifies
31 // that only one request is active at a time.
32 // Call RespondToAccessTokenRequest to respond to it.
33 class MockOAuth2TokenService : public FakeOAuth2TokenService {
35 MockOAuth2TokenService() : num_invalidate_token_(0) {}
37 ~MockOAuth2TokenService() override {}
39 void RespondToAccessTokenRequest(GoogleServiceAuthError error);
41 int num_invalidate_token() const { return num_invalidate_token_; }
44 void FetchOAuth2Token(RequestImpl* request,
45 const std::string& account_id,
46 net::URLRequestContextGetter* getter,
47 const std::string& client_id,
48 const std::string& client_secret,
49 const ScopeSet& scopes) override;
51 void InvalidateOAuth2Token(const std::string& account_id,
52 const std::string& client_id,
53 const ScopeSet& scopes,
54 const std::string& access_token) override;
57 base::WeakPtr<RequestImpl> last_request_;
58 int num_invalidate_token_;
61 void MockOAuth2TokenService::RespondToAccessTokenRequest(
62 GoogleServiceAuthError error) {
63 EXPECT_TRUE(last_request_ != NULL);
64 std::string access_token;
65 base::Time expiration_time;
66 if (error == GoogleServiceAuthError::AuthErrorNone()) {
67 access_token = kAccessToken;
68 expiration_time = base::Time::Max();
70 base::MessageLoop::current()->PostTask(
72 base::Bind(&OAuth2TokenService::RequestImpl::InformConsumer,
79 void MockOAuth2TokenService::FetchOAuth2Token(
81 const std::string& account_id,
82 net::URLRequestContextGetter* getter,
83 const std::string& client_id,
84 const std::string& client_secret,
85 const ScopeSet& scopes) {
86 // Only one request at a time is allowed.
87 EXPECT_TRUE(last_request_ == NULL);
88 last_request_ = request->AsWeakPtr();
91 void MockOAuth2TokenService::InvalidateOAuth2Token(
92 const std::string& account_id,
93 const std::string& client_id,
94 const ScopeSet& scopes,
95 const std::string& access_token) {
96 ++num_invalidate_token_;
99 class TokenServiceProvider
100 : public OAuth2TokenServiceRequest::TokenServiceProvider,
101 base::NonThreadSafe {
103 TokenServiceProvider(OAuth2TokenService* token_service);
105 // OAuth2TokenService::TokenServiceProvider implementation.
106 scoped_refptr<base::SingleThreadTaskRunner> GetTokenServiceTaskRunner()
108 OAuth2TokenService* GetTokenService() override;
111 ~TokenServiceProvider() override;
113 scoped_refptr<base::SingleThreadTaskRunner> task_runner_;
114 OAuth2TokenService* token_service_;
117 TokenServiceProvider::TokenServiceProvider(OAuth2TokenService* token_service)
118 : task_runner_(base::ThreadTaskRunnerHandle::Get()),
119 token_service_(token_service) {
120 DCHECK(token_service_);
123 TokenServiceProvider::~TokenServiceProvider() {
126 scoped_refptr<base::SingleThreadTaskRunner>
127 TokenServiceProvider::GetTokenServiceTaskRunner() {
131 OAuth2TokenService* TokenServiceProvider::GetTokenService() {
132 DCHECK(task_runner_->BelongsToCurrentThread());
133 return token_service_;
138 class AttachmentDownloaderImplTest : public testing::Test {
140 typedef std::map<AttachmentId, AttachmentDownloader::DownloadResult>
143 enum HashHeaderType {
149 AttachmentDownloaderImplTest() : num_completed_downloads_(0) {}
151 virtual void SetUp() override;
152 virtual void TearDown() override;
154 AttachmentDownloader* downloader() { return attachment_downloader_.get(); }
156 MockOAuth2TokenService* token_service() { return token_service_.get(); }
158 int num_completed_downloads() { return num_completed_downloads_; }
160 AttachmentDownloader::DownloadCallback download_callback(
161 const AttachmentId& id) {
162 return base::Bind(&AttachmentDownloaderImplTest::DownloadDone,
163 base::Unretained(this),
167 // Respond with |response_code| and hash header of type |hash_header_type|.
168 void CompleteDownload(int response_code, HashHeaderType hash_header_type);
170 void DownloadDone(const AttachmentId& attachment_id,
171 const AttachmentDownloader::DownloadResult& result,
172 scoped_ptr<Attachment> attachment);
174 void VerifyDownloadResult(const AttachmentId& attachment_id,
175 const AttachmentDownloader::DownloadResult& result);
177 void RunMessageLoop();
180 static void AddHashHeader(HashHeaderType hash_header_type,
181 net::TestURLFetcher* fetcher);
183 base::MessageLoopForIO message_loop_;
184 scoped_refptr<net::URLRequestContextGetter> url_request_context_getter_;
185 net::TestURLFetcherFactory url_fetcher_factory_;
186 scoped_ptr<MockOAuth2TokenService> token_service_;
187 scoped_ptr<AttachmentDownloader> attachment_downloader_;
188 ResultsMap download_results_;
189 int num_completed_downloads_;
192 void AttachmentDownloaderImplTest::SetUp() {
193 url_request_context_getter_ =
194 new net::TestURLRequestContextGetter(message_loop_.message_loop_proxy());
195 url_fetcher_factory_.set_remove_fetcher_on_delete(true);
196 token_service_.reset(new MockOAuth2TokenService());
197 token_service_->AddAccount(kAccountId);
198 scoped_refptr<OAuth2TokenServiceRequest::TokenServiceProvider>
199 token_service_provider(new TokenServiceProvider(token_service_.get()));
201 OAuth2TokenService::ScopeSet scopes;
202 scopes.insert(GaiaConstants::kChromeSyncOAuth2Scope);
203 attachment_downloader_ =
204 AttachmentDownloader::Create(GURL(kAttachmentServerUrl),
205 url_request_context_getter_,
208 token_service_provider);
211 void AttachmentDownloaderImplTest::TearDown() {
215 void AttachmentDownloaderImplTest::CompleteDownload(
217 HashHeaderType hash_header_type) {
218 // TestURLFetcherFactory remembers last active URLFetcher.
219 net::TestURLFetcher* fetcher = url_fetcher_factory_.GetFetcherByID(0);
220 // There should be outstanding url fetch request.
221 EXPECT_TRUE(fetcher != NULL);
222 fetcher->set_status(net::URLRequestStatus());
223 fetcher->set_response_code(response_code);
224 if (response_code == net::HTTP_OK) {
225 fetcher->SetResponseString(kAttachmentContent);
227 AddHashHeader(hash_header_type, fetcher);
229 // Call URLFetcherDelegate.
230 net::URLFetcherDelegate* delegate = fetcher->delegate();
231 delegate->OnURLFetchComplete(fetcher);
233 // Once result is processed URLFetcher should be deleted.
234 fetcher = url_fetcher_factory_.GetFetcherByID(0);
235 EXPECT_TRUE(fetcher == NULL);
238 void AttachmentDownloaderImplTest::DownloadDone(
239 const AttachmentId& attachment_id,
240 const AttachmentDownloader::DownloadResult& result,
241 scoped_ptr<Attachment> attachment) {
242 download_results_.insert(std::make_pair(attachment_id, result));
243 if (result == AttachmentDownloader::DOWNLOAD_SUCCESS) {
244 // Successful download should be accompanied by valid attachment with
245 // matching id and valid data.
246 EXPECT_TRUE(attachment != NULL);
247 EXPECT_EQ(attachment_id, attachment->GetId());
249 scoped_refptr<base::RefCountedMemory> data = attachment->GetData();
250 std::string data_as_string(data->front_as<char>(), data->size());
251 EXPECT_EQ(data_as_string, kAttachmentContent);
253 EXPECT_TRUE(attachment == NULL);
255 ++num_completed_downloads_;
258 void AttachmentDownloaderImplTest::VerifyDownloadResult(
259 const AttachmentId& attachment_id,
260 const AttachmentDownloader::DownloadResult& result) {
261 ResultsMap::const_iterator iter = download_results_.find(attachment_id);
262 EXPECT_TRUE(iter != download_results_.end());
263 EXPECT_EQ(iter->second, result);
266 void AttachmentDownloaderImplTest::RunMessageLoop() {
267 base::RunLoop run_loop;
268 run_loop.RunUntilIdle();
271 void AttachmentDownloaderImplTest::AddHashHeader(
272 HashHeaderType hash_header_type,
273 net::TestURLFetcher* fetcher) {
274 std::string header = "X-Goog-Hash: crc32c=";
275 scoped_refptr<net::HttpResponseHeaders> headers(
276 new net::HttpResponseHeaders(""));
277 switch (hash_header_type) {
278 case HASH_HEADER_NONE:
280 case HASH_HEADER_VALID:
281 header += AttachmentUploaderImpl::ComputeCrc32cHash(
282 kAttachmentContent, strlen(kAttachmentContent));
283 headers->AddHeader(header);
285 case HASH_HEADER_INVALID:
286 header += "BOGUS1==";
287 headers->AddHeader(header);
290 fetcher->set_response_headers(headers);
293 TEST_F(AttachmentDownloaderImplTest, HappyCase) {
294 AttachmentId id1 = AttachmentId::Create();
295 // DownloadAttachment should trigger RequestAccessToken.
296 downloader()->DownloadAttachment(id1, download_callback(id1));
298 // Return valid access token.
299 token_service()->RespondToAccessTokenRequest(
300 GoogleServiceAuthError::AuthErrorNone());
302 // Check that there is outstanding URLFetcher request and complete it.
303 CompleteDownload(net::HTTP_OK, HASH_HEADER_VALID);
304 // Verify that callback was called for the right id with the right result.
305 VerifyDownloadResult(id1, AttachmentDownloader::DOWNLOAD_SUCCESS);
308 TEST_F(AttachmentDownloaderImplTest, SameIdMultipleDownloads) {
309 AttachmentId id1 = AttachmentId::Create();
310 // Call DownloadAttachment two times for the same id.
311 downloader()->DownloadAttachment(id1, download_callback(id1));
312 downloader()->DownloadAttachment(id1, download_callback(id1));
314 // Return valid access token.
315 token_service()->RespondToAccessTokenRequest(
316 GoogleServiceAuthError::AuthErrorNone());
318 // Start one more download after access token is received.
319 downloader()->DownloadAttachment(id1, download_callback(id1));
320 // Complete URLFetcher request.
321 CompleteDownload(net::HTTP_OK, HASH_HEADER_VALID);
322 // Verify that all download requests completed.
323 VerifyDownloadResult(id1, AttachmentDownloader::DOWNLOAD_SUCCESS);
324 EXPECT_EQ(3, num_completed_downloads());
326 // Let's download the same attachment again.
327 downloader()->DownloadAttachment(id1, download_callback(id1));
329 // Verify that it didn't finish prematurely.
330 EXPECT_EQ(3, num_completed_downloads());
331 // Return valid access token.
332 token_service()->RespondToAccessTokenRequest(
333 GoogleServiceAuthError::AuthErrorNone());
335 // Complete URLFetcher request.
336 CompleteDownload(net::HTTP_OK, HASH_HEADER_VALID);
337 // Verify that all download requests completed.
338 VerifyDownloadResult(id1, AttachmentDownloader::DOWNLOAD_SUCCESS);
339 EXPECT_EQ(4, num_completed_downloads());
342 TEST_F(AttachmentDownloaderImplTest, RequestAccessTokenFails) {
343 AttachmentId id1 = AttachmentId::Create();
344 AttachmentId id2 = AttachmentId::Create();
345 // Trigger first RequestAccessToken.
346 downloader()->DownloadAttachment(id1, download_callback(id1));
348 // Return valid access token.
349 token_service()->RespondToAccessTokenRequest(
350 GoogleServiceAuthError::AuthErrorNone());
352 // Trigger second RequestAccessToken.
353 downloader()->DownloadAttachment(id2, download_callback(id2));
355 // Fail RequestAccessToken.
356 token_service()->RespondToAccessTokenRequest(
357 GoogleServiceAuthError(GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS));
359 // Only id2 should fail.
360 VerifyDownloadResult(id2, AttachmentDownloader::DOWNLOAD_TRANSIENT_ERROR);
361 // Complete request for id1.
362 CompleteDownload(net::HTTP_OK, HASH_HEADER_VALID);
363 VerifyDownloadResult(id1, AttachmentDownloader::DOWNLOAD_SUCCESS);
366 TEST_F(AttachmentDownloaderImplTest, URLFetcher_BadToken) {
367 AttachmentId id1 = AttachmentId::Create();
368 downloader()->DownloadAttachment(id1, download_callback(id1));
370 // Return valid access token.
371 token_service()->RespondToAccessTokenRequest(
372 GoogleServiceAuthError::AuthErrorNone());
374 // Fail URLFetcher. This should trigger download failure and access token
376 CompleteDownload(net::HTTP_UNAUTHORIZED, HASH_HEADER_VALID);
377 EXPECT_EQ(1, token_service()->num_invalidate_token());
378 VerifyDownloadResult(id1, AttachmentDownloader::DOWNLOAD_TRANSIENT_ERROR);
381 TEST_F(AttachmentDownloaderImplTest, URLFetcher_ServiceUnavailable) {
382 AttachmentId id1 = AttachmentId::Create();
383 downloader()->DownloadAttachment(id1, download_callback(id1));
385 // Return valid access token.
386 token_service()->RespondToAccessTokenRequest(
387 GoogleServiceAuthError::AuthErrorNone());
389 // Fail URLFetcher. This should trigger download failure. Access token
390 // shouldn't be invalidated.
391 CompleteDownload(net::HTTP_SERVICE_UNAVAILABLE, HASH_HEADER_VALID);
392 EXPECT_EQ(0, token_service()->num_invalidate_token());
393 VerifyDownloadResult(id1, AttachmentDownloader::DOWNLOAD_TRANSIENT_ERROR);
396 // Verify that if no hash is present on the response the downloader accepts the
397 // received attachment.
398 TEST_F(AttachmentDownloaderImplTest, NoHash) {
399 AttachmentId id1 = AttachmentId::Create();
400 downloader()->DownloadAttachment(id1, download_callback(id1));
402 token_service()->RespondToAccessTokenRequest(
403 GoogleServiceAuthError::AuthErrorNone());
405 CompleteDownload(net::HTTP_OK, HASH_HEADER_NONE);
406 VerifyDownloadResult(id1, AttachmentDownloader::DOWNLOAD_SUCCESS);
409 // Verify that if an invalid hash is present on the response the downloader
410 // treats it as a transient error.
411 TEST_F(AttachmentDownloaderImplTest, InvalidHash) {
412 AttachmentId id1 = AttachmentId::Create();
413 downloader()->DownloadAttachment(id1, download_callback(id1));
415 token_service()->RespondToAccessTokenRequest(
416 GoogleServiceAuthError::AuthErrorNone());
418 CompleteDownload(net::HTTP_OK, HASH_HEADER_INVALID);
419 VerifyDownloadResult(id1, AttachmentDownloader::DOWNLOAD_TRANSIENT_ERROR);
423 // Verify that extract fails when there is no crc32c value.
424 TEST_F(AttachmentDownloaderImplTest, ExtractCrc32c_Empty) {
426 raw += "HTTP/1.1 200 OK\n";
428 raw += "X-Goog-HASH: crc32c=\n";
430 std::replace(raw.begin(), raw.end(), '\n', '\0');
431 scoped_refptr<net::HttpResponseHeaders> headers(
432 new net::HttpResponseHeaders(raw));
433 std::string extracted;
434 ASSERT_FALSE(AttachmentDownloaderImpl::ExtractCrc32c(*headers, &extracted));
437 // Verify that extract finds the first crc32c and ignores others.
438 TEST_F(AttachmentDownloaderImplTest, ExtractCrc32c_First) {
439 const std::string expected = "z8SuHQ==";
441 raw += "HTTP/1.1 200 OK\n";
443 // Ignored because it's the wrong header.
444 raw += "X-Goog-Hashes: crc32c=AAAAAA==\n";
445 // Header name matches. The md5 item is ignored.
446 raw += "X-Goog-HASH: md5=rL0Y20zC+Fzt72VPzMSk2A==,crc32c=" + expected + "\n";
447 // Ignored because we already found a crc32c in the one above.
448 raw += "X-Goog-HASH: crc32c=AAAAAA==\n";
450 std::replace(raw.begin(), raw.end(), '\n', '\0');
451 scoped_refptr<net::HttpResponseHeaders> headers(
452 new net::HttpResponseHeaders(raw));
453 std::string extracted;
454 ASSERT_TRUE(AttachmentDownloaderImpl::ExtractCrc32c(*headers, &extracted));
455 ASSERT_EQ(expected, extracted);
458 // Verify that extract fails if there is no crc32c.
459 TEST_F(AttachmentDownloaderImplTest, ExtractCrc32c_None) {
461 raw += "HTTP/1.1 200 OK\n";
463 raw += "X-Goog-Hash: md5=rL0Y20zC+Fzt72VPzMSk2A==\n";
465 std::replace(raw.begin(), raw.end(), '\n', '\0');
466 scoped_refptr<net::HttpResponseHeaders> headers(
467 new net::HttpResponseHeaders(raw));
468 std::string extracted;
469 ASSERT_FALSE(AttachmentDownloaderImpl::ExtractCrc32c(*headers, &extracted));
472 } // namespace syncer