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/test/histogram_tester.h"
12 #include "base/thread_task_runner_handle.h"
13 #include "google_apis/gaia/fake_oauth2_token_service.h"
14 #include "google_apis/gaia/gaia_constants.h"
15 #include "net/http/http_response_headers.h"
16 #include "net/url_request/test_url_fetcher_factory.h"
17 #include "net/url_request/url_request_test_util.h"
18 #include "sync/api/attachments/attachment.h"
19 #include "sync/internal_api/public/attachments/attachment_uploader_impl.h"
20 #include "sync/internal_api/public/attachments/attachment_util.h"
21 #include "sync/internal_api/public/base/model_type.h"
22 #include "testing/gtest/include/gtest/gtest.h"
23 #include "third_party/leveldatabase/src/util/crc32c.h"
29 const char kAccountId
[] = "attachments@gmail.com";
30 const char kAccessToken
[] = "access.token";
31 const char kAttachmentServerUrl
[] = "http://attachments.com/";
32 const char kAttachmentContent
[] = "attachment.content";
33 const char kStoreBirthday
[] = "z00000000-0000-007b-0000-0000000004d2";
34 const syncer::ModelType kModelType
= syncer::ModelType::ARTICLES
;
36 // MockOAuth2TokenService remembers last request for access token and verifies
37 // that only one request is active at a time.
38 // Call RespondToAccessTokenRequest to respond to it.
39 class MockOAuth2TokenService
: public FakeOAuth2TokenService
{
41 MockOAuth2TokenService() : num_invalidate_token_(0) {}
43 ~MockOAuth2TokenService() override
{}
45 void RespondToAccessTokenRequest(GoogleServiceAuthError error
);
47 int num_invalidate_token() const { return num_invalidate_token_
; }
50 void FetchOAuth2Token(RequestImpl
* request
,
51 const std::string
& account_id
,
52 net::URLRequestContextGetter
* getter
,
53 const std::string
& client_id
,
54 const std::string
& client_secret
,
55 const ScopeSet
& scopes
) override
;
57 void InvalidateOAuth2Token(const std::string
& account_id
,
58 const std::string
& client_id
,
59 const ScopeSet
& scopes
,
60 const std::string
& access_token
) override
;
63 base::WeakPtr
<RequestImpl
> last_request_
;
64 int num_invalidate_token_
;
67 void MockOAuth2TokenService::RespondToAccessTokenRequest(
68 GoogleServiceAuthError error
) {
69 EXPECT_TRUE(last_request_
!= NULL
);
70 std::string access_token
;
71 base::Time expiration_time
;
72 if (error
== GoogleServiceAuthError::AuthErrorNone()) {
73 access_token
= kAccessToken
;
74 expiration_time
= base::Time::Max();
76 base::MessageLoop::current()->PostTask(
78 base::Bind(&OAuth2TokenService::RequestImpl::InformConsumer
,
85 void MockOAuth2TokenService::FetchOAuth2Token(
87 const std::string
& account_id
,
88 net::URLRequestContextGetter
* getter
,
89 const std::string
& client_id
,
90 const std::string
& client_secret
,
91 const ScopeSet
& scopes
) {
92 // Only one request at a time is allowed.
93 EXPECT_TRUE(last_request_
== NULL
);
94 last_request_
= request
->AsWeakPtr();
97 void MockOAuth2TokenService::InvalidateOAuth2Token(
98 const std::string
& account_id
,
99 const std::string
& client_id
,
100 const ScopeSet
& scopes
,
101 const std::string
& access_token
) {
102 ++num_invalidate_token_
;
105 class TokenServiceProvider
106 : public OAuth2TokenServiceRequest::TokenServiceProvider
,
107 base::NonThreadSafe
{
109 TokenServiceProvider(OAuth2TokenService
* token_service
);
111 // OAuth2TokenService::TokenServiceProvider implementation.
112 scoped_refptr
<base::SingleThreadTaskRunner
> GetTokenServiceTaskRunner()
114 OAuth2TokenService
* GetTokenService() override
;
117 ~TokenServiceProvider() override
;
119 scoped_refptr
<base::SingleThreadTaskRunner
> task_runner_
;
120 OAuth2TokenService
* token_service_
;
123 TokenServiceProvider::TokenServiceProvider(OAuth2TokenService
* token_service
)
124 : task_runner_(base::ThreadTaskRunnerHandle::Get()),
125 token_service_(token_service
) {
126 DCHECK(token_service_
);
129 TokenServiceProvider::~TokenServiceProvider() {
132 scoped_refptr
<base::SingleThreadTaskRunner
>
133 TokenServiceProvider::GetTokenServiceTaskRunner() {
137 OAuth2TokenService
* TokenServiceProvider::GetTokenService() {
138 DCHECK(task_runner_
->BelongsToCurrentThread());
139 return token_service_
;
144 class AttachmentDownloaderImplTest
: public testing::Test
{
146 typedef std::map
<AttachmentId
, AttachmentDownloader::DownloadResult
>
149 enum HashHeaderType
{
155 AttachmentDownloaderImplTest()
156 : num_completed_downloads_(0),
158 Attachment::Create(new base::RefCountedStaticMemory(
160 strlen(kAttachmentContent
))).GetId()) {}
162 void SetUp() override
;
163 void TearDown() override
;
165 AttachmentDownloader
* downloader() { return attachment_downloader_
.get(); }
167 MockOAuth2TokenService
* token_service() { return token_service_
.get(); }
169 int num_completed_downloads() { return num_completed_downloads_
; }
171 const AttachmentId
attachment_id() const { return attachment_id_
; }
173 AttachmentDownloader::DownloadCallback
download_callback(
174 const AttachmentId
& id
) {
175 return base::Bind(&AttachmentDownloaderImplTest::DownloadDone
,
176 base::Unretained(this),
180 // Respond with |response_code| and hash header of type |hash_header_type|.
181 void CompleteDownload(int response_code
, HashHeaderType hash_header_type
);
183 void DownloadDone(const AttachmentId
& attachment_id
,
184 const AttachmentDownloader::DownloadResult
& result
,
185 scoped_ptr
<Attachment
> attachment
);
187 void VerifyDownloadResult(const AttachmentId
& attachment_id
,
188 const AttachmentDownloader::DownloadResult
& result
);
190 void RunMessageLoop();
193 static void AddHashHeader(HashHeaderType hash_header_type
,
194 net::TestURLFetcher
* fetcher
);
196 base::MessageLoopForIO message_loop_
;
197 scoped_refptr
<net::URLRequestContextGetter
> url_request_context_getter_
;
198 net::TestURLFetcherFactory url_fetcher_factory_
;
199 scoped_ptr
<MockOAuth2TokenService
> token_service_
;
200 scoped_ptr
<AttachmentDownloader
> attachment_downloader_
;
201 ResultsMap download_results_
;
202 int num_completed_downloads_
;
203 const AttachmentId attachment_id_
;
206 void AttachmentDownloaderImplTest::SetUp() {
207 url_request_context_getter_
=
208 new net::TestURLRequestContextGetter(message_loop_
.message_loop_proxy());
209 url_fetcher_factory_
.set_remove_fetcher_on_delete(true);
210 token_service_
.reset(new MockOAuth2TokenService());
211 token_service_
->AddAccount(kAccountId
);
212 scoped_refptr
<OAuth2TokenServiceRequest::TokenServiceProvider
>
213 token_service_provider(new TokenServiceProvider(token_service_
.get()));
215 OAuth2TokenService::ScopeSet scopes
;
216 scopes
.insert(GaiaConstants::kChromeSyncOAuth2Scope
);
217 attachment_downloader_
= AttachmentDownloader::Create(
218 GURL(kAttachmentServerUrl
), url_request_context_getter_
, kAccountId
,
219 scopes
, token_service_provider
, std::string(kStoreBirthday
), kModelType
);
222 void AttachmentDownloaderImplTest::TearDown() {
226 void AttachmentDownloaderImplTest::CompleteDownload(
228 HashHeaderType hash_header_type
) {
229 // TestURLFetcherFactory remembers last active URLFetcher.
230 net::TestURLFetcher
* fetcher
= url_fetcher_factory_
.GetFetcherByID(0);
231 // There should be outstanding url fetch request.
232 EXPECT_TRUE(fetcher
!= NULL
);
233 fetcher
->set_status(net::URLRequestStatus());
234 fetcher
->set_response_code(response_code
);
235 if (response_code
== net::HTTP_OK
) {
236 fetcher
->SetResponseString(kAttachmentContent
);
238 AddHashHeader(hash_header_type
, fetcher
);
240 // Call URLFetcherDelegate.
241 net::URLFetcherDelegate
* delegate
= fetcher
->delegate();
242 delegate
->OnURLFetchComplete(fetcher
);
244 // Once result is processed URLFetcher should be deleted.
245 fetcher
= url_fetcher_factory_
.GetFetcherByID(0);
246 EXPECT_TRUE(fetcher
== NULL
);
249 void AttachmentDownloaderImplTest::DownloadDone(
250 const AttachmentId
& attachment_id
,
251 const AttachmentDownloader::DownloadResult
& result
,
252 scoped_ptr
<Attachment
> attachment
) {
253 download_results_
.insert(std::make_pair(attachment_id
, result
));
254 if (result
== AttachmentDownloader::DOWNLOAD_SUCCESS
) {
255 // Successful download should be accompanied by valid attachment with
256 // matching id and valid data.
257 EXPECT_TRUE(attachment
!= NULL
);
258 EXPECT_EQ(attachment_id
, attachment
->GetId());
260 scoped_refptr
<base::RefCountedMemory
> data
= attachment
->GetData();
261 std::string
data_as_string(data
->front_as
<char>(), data
->size());
262 EXPECT_EQ(data_as_string
, kAttachmentContent
);
264 EXPECT_TRUE(attachment
== NULL
);
266 ++num_completed_downloads_
;
269 void AttachmentDownloaderImplTest::VerifyDownloadResult(
270 const AttachmentId
& attachment_id
,
271 const AttachmentDownloader::DownloadResult
& result
) {
272 ResultsMap::const_iterator iter
= download_results_
.find(attachment_id
);
273 EXPECT_TRUE(iter
!= download_results_
.end());
274 EXPECT_EQ(iter
->second
, result
);
277 void AttachmentDownloaderImplTest::RunMessageLoop() {
278 base::RunLoop run_loop
;
279 run_loop
.RunUntilIdle();
282 void AttachmentDownloaderImplTest::AddHashHeader(
283 HashHeaderType hash_header_type
,
284 net::TestURLFetcher
* fetcher
) {
285 std::string header
= "X-Goog-Hash: crc32c=";
286 scoped_refptr
<net::HttpResponseHeaders
> headers(
287 new net::HttpResponseHeaders(""));
288 switch (hash_header_type
) {
289 case HASH_HEADER_NONE
:
291 case HASH_HEADER_VALID
:
292 header
+= AttachmentUploaderImpl::FormatCrc32cHash(leveldb::crc32c::Value(
293 kAttachmentContent
, strlen(kAttachmentContent
)));
294 headers
->AddHeader(header
);
296 case HASH_HEADER_INVALID
:
297 header
+= "BOGUS1==";
298 headers
->AddHeader(header
);
301 fetcher
->set_response_headers(headers
);
304 TEST_F(AttachmentDownloaderImplTest
, HappyCase
) {
305 AttachmentId id1
= attachment_id();
306 // DownloadAttachment should trigger RequestAccessToken.
307 downloader()->DownloadAttachment(id1
, download_callback(id1
));
309 // Return valid access token.
310 token_service()->RespondToAccessTokenRequest(
311 GoogleServiceAuthError::AuthErrorNone());
313 // Catch histogram entries.
314 base::HistogramTester histogram_tester
;
315 // Check that there is outstanding URLFetcher request and complete it.
316 CompleteDownload(net::HTTP_OK
, HASH_HEADER_VALID
);
317 // Verify that the response code was logged properly.
318 histogram_tester
.ExpectUniqueSample("Sync.Attachments.DownloadResponseCode",
320 // Verify that callback was called for the right id with the right result.
321 VerifyDownloadResult(id1
, AttachmentDownloader::DOWNLOAD_SUCCESS
);
324 TEST_F(AttachmentDownloaderImplTest
, SameIdMultipleDownloads
) {
325 AttachmentId id1
= attachment_id();
326 base::HistogramTester histogram_tester
;
327 // Call DownloadAttachment two times for the same id.
328 downloader()->DownloadAttachment(id1
, download_callback(id1
));
329 downloader()->DownloadAttachment(id1
, download_callback(id1
));
331 // Return valid access token.
332 token_service()->RespondToAccessTokenRequest(
333 GoogleServiceAuthError::AuthErrorNone());
335 // Start one more download after access token is received.
336 downloader()->DownloadAttachment(id1
, download_callback(id1
));
337 // Complete URLFetcher request.
338 CompleteDownload(net::HTTP_OK
, HASH_HEADER_VALID
);
339 // Verify that all download requests completed.
340 VerifyDownloadResult(id1
, AttachmentDownloader::DOWNLOAD_SUCCESS
);
341 EXPECT_EQ(3, num_completed_downloads());
343 // Let's download the same attachment again.
344 downloader()->DownloadAttachment(id1
, download_callback(id1
));
346 // Verify that it didn't finish prematurely.
347 EXPECT_EQ(3, num_completed_downloads());
348 // Return valid access token.
349 token_service()->RespondToAccessTokenRequest(
350 GoogleServiceAuthError::AuthErrorNone());
352 // Complete URLFetcher request.
353 CompleteDownload(net::HTTP_OK
, HASH_HEADER_VALID
);
354 // Verify that all download requests completed.
355 VerifyDownloadResult(id1
, AttachmentDownloader::DOWNLOAD_SUCCESS
);
356 EXPECT_EQ(4, num_completed_downloads());
357 histogram_tester
.ExpectUniqueSample("Sync.Attachments.DownloadResponseCode",
361 TEST_F(AttachmentDownloaderImplTest
, RequestAccessTokenFails
) {
362 AttachmentId id1
= attachment_id();
363 AttachmentId id2
= AttachmentId::Create(id1
.GetSize(), id1
.GetCrc32c());
364 // Trigger first RequestAccessToken.
365 downloader()->DownloadAttachment(id1
, download_callback(id1
));
367 // Return valid access token.
368 token_service()->RespondToAccessTokenRequest(
369 GoogleServiceAuthError::AuthErrorNone());
371 // Trigger second RequestAccessToken.
372 downloader()->DownloadAttachment(id2
, download_callback(id2
));
374 // Fail RequestAccessToken.
375 token_service()->RespondToAccessTokenRequest(
376 GoogleServiceAuthError(GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS
));
378 // Only id2 should fail.
379 VerifyDownloadResult(id2
, AttachmentDownloader::DOWNLOAD_TRANSIENT_ERROR
);
380 // Complete request for id1.
381 CompleteDownload(net::HTTP_OK
, HASH_HEADER_VALID
);
382 VerifyDownloadResult(id1
, AttachmentDownloader::DOWNLOAD_SUCCESS
);
385 TEST_F(AttachmentDownloaderImplTest
, URLFetcher_BadToken
) {
386 AttachmentId id1
= attachment_id();
387 downloader()->DownloadAttachment(id1
, download_callback(id1
));
389 // Return valid access token.
390 token_service()->RespondToAccessTokenRequest(
391 GoogleServiceAuthError::AuthErrorNone());
393 // Fail URLFetcher. This should trigger download failure and access token
395 base::HistogramTester histogram_tester
;
396 CompleteDownload(net::HTTP_UNAUTHORIZED
, HASH_HEADER_VALID
);
397 EXPECT_EQ(1, token_service()->num_invalidate_token());
398 VerifyDownloadResult(id1
, AttachmentDownloader::DOWNLOAD_TRANSIENT_ERROR
);
399 histogram_tester
.ExpectUniqueSample("Sync.Attachments.DownloadResponseCode",
400 net::HTTP_UNAUTHORIZED
, 1);
403 TEST_F(AttachmentDownloaderImplTest
, URLFetcher_ServiceUnavailable
) {
404 AttachmentId id1
= attachment_id();
405 downloader()->DownloadAttachment(id1
, download_callback(id1
));
407 // Return valid access token.
408 token_service()->RespondToAccessTokenRequest(
409 GoogleServiceAuthError::AuthErrorNone());
411 // Fail URLFetcher. This should trigger download failure. Access token
412 // shouldn't be invalidated.
413 base::HistogramTester histogram_tester
;
414 CompleteDownload(net::HTTP_SERVICE_UNAVAILABLE
, HASH_HEADER_VALID
);
415 EXPECT_EQ(0, token_service()->num_invalidate_token());
416 VerifyDownloadResult(id1
, AttachmentDownloader::DOWNLOAD_TRANSIENT_ERROR
);
417 histogram_tester
.ExpectUniqueSample("Sync.Attachments.DownloadResponseCode",
418 net::HTTP_SERVICE_UNAVAILABLE
, 1);
421 // Verify that if no hash is present on the response the downloader accepts the
422 // received attachment.
423 TEST_F(AttachmentDownloaderImplTest
, NoHash
) {
424 AttachmentId id1
= attachment_id();
425 downloader()->DownloadAttachment(id1
, download_callback(id1
));
427 token_service()->RespondToAccessTokenRequest(
428 GoogleServiceAuthError::AuthErrorNone());
430 CompleteDownload(net::HTTP_OK
, HASH_HEADER_NONE
);
431 VerifyDownloadResult(id1
, AttachmentDownloader::DOWNLOAD_SUCCESS
);
434 // Verify that if an invalid hash is present on the response the downloader
435 // treats it as a transient error.
436 TEST_F(AttachmentDownloaderImplTest
, InvalidHash
) {
437 AttachmentId id1
= attachment_id();
438 downloader()->DownloadAttachment(id1
, download_callback(id1
));
440 token_service()->RespondToAccessTokenRequest(
441 GoogleServiceAuthError::AuthErrorNone());
443 CompleteDownload(net::HTTP_OK
, HASH_HEADER_INVALID
);
444 VerifyDownloadResult(id1
, AttachmentDownloader::DOWNLOAD_TRANSIENT_ERROR
);
447 // Verify that when the hash from the attachment id does not match the one on
448 // the response the result is an unspecified error.
449 TEST_F(AttachmentDownloaderImplTest
, IdHashDoesNotMatch
) {
450 // id1 has the wrong crc32c.
451 AttachmentId id1
= AttachmentId::Create(attachment_id().GetSize(), 12345);
452 downloader()->DownloadAttachment(id1
, download_callback(id1
));
454 token_service()->RespondToAccessTokenRequest(
455 GoogleServiceAuthError::AuthErrorNone());
457 CompleteDownload(net::HTTP_OK
, HASH_HEADER_VALID
);
458 VerifyDownloadResult(id1
, AttachmentDownloader::DOWNLOAD_UNSPECIFIED_ERROR
);
462 // Verify that extract fails when there is no headers object.
463 TEST_F(AttachmentDownloaderImplTest
, ExtractCrc32c_NoHeaders
) {
465 ASSERT_FALSE(AttachmentDownloaderImpl::ExtractCrc32c(nullptr, &extracted
));
468 // Verify that extract fails when there is no crc32c value.
469 TEST_F(AttachmentDownloaderImplTest
, ExtractCrc32c_Empty
) {
471 raw
+= "HTTP/1.1 200 OK\n";
473 raw
+= "X-Goog-HASH: crc32c=\n";
475 std::replace(raw
.begin(), raw
.end(), '\n', '\0');
476 scoped_refptr
<net::HttpResponseHeaders
> headers(
477 new net::HttpResponseHeaders(raw
));
480 AttachmentDownloaderImpl::ExtractCrc32c(headers
.get(), &extracted
));
483 // Verify that extract finds the first crc32c and ignores others.
484 TEST_F(AttachmentDownloaderImplTest
, ExtractCrc32c_First
) {
485 const std::string expected_encoded
= "z8SuHQ==";
486 const uint32_t expected
= 3485773341;
488 raw
+= "HTTP/1.1 200 OK\n";
490 // Ignored because it's the wrong header.
491 raw
+= "X-Goog-Hashes: crc32c=AAAAAA==\n";
492 // Header name matches. The md5 item is ignored.
493 raw
+= "X-Goog-HASH: md5=rL0Y20zC+Fzt72VPzMSk2A==,crc32c=" +
494 expected_encoded
+ "\n";
495 // Ignored because we already found a crc32c in the one above.
496 raw
+= "X-Goog-HASH: crc32c=AAAAAA==\n";
498 std::replace(raw
.begin(), raw
.end(), '\n', '\0');
499 scoped_refptr
<net::HttpResponseHeaders
> headers(
500 new net::HttpResponseHeaders(raw
));
503 AttachmentDownloaderImpl::ExtractCrc32c(headers
.get(), &extracted
));
504 ASSERT_EQ(expected
, extracted
);
507 // Verify that extract fails when encoded value is too long.
508 TEST_F(AttachmentDownloaderImplTest
, ExtractCrc32c_TooLong
) {
510 raw
+= "HTTP/1.1 200 OK\n";
512 raw
+= "X-Goog-HASH: crc32c=AAAAAAAA\n";
514 std::replace(raw
.begin(), raw
.end(), '\n', '\0');
515 scoped_refptr
<net::HttpResponseHeaders
> headers(
516 new net::HttpResponseHeaders(raw
));
519 AttachmentDownloaderImpl::ExtractCrc32c(headers
.get(), &extracted
));
522 // Verify that extract fails if there is no crc32c.
523 TEST_F(AttachmentDownloaderImplTest
, ExtractCrc32c_None
) {
525 raw
+= "HTTP/1.1 200 OK\n";
527 raw
+= "X-Goog-Hash: md5=rL0Y20zC+Fzt72VPzMSk2A==\n";
529 std::replace(raw
.begin(), raw
.end(), '\n', '\0');
530 scoped_refptr
<net::HttpResponseHeaders
> headers(
531 new net::HttpResponseHeaders(raw
));
534 AttachmentDownloaderImpl::ExtractCrc32c(headers
.get(), &extracted
));
537 } // namespace syncer