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() : num_completed_downloads_(0) {}
157 void SetUp() override
;
158 void TearDown() override
;
160 AttachmentDownloader
* downloader() { return attachment_downloader_
.get(); }
162 MockOAuth2TokenService
* token_service() { return token_service_
.get(); }
164 int num_completed_downloads() { return num_completed_downloads_
; }
166 AttachmentDownloader::DownloadCallback
download_callback(
167 const AttachmentId
& id
) {
168 return base::Bind(&AttachmentDownloaderImplTest::DownloadDone
,
169 base::Unretained(this),
173 // Respond with |response_code| and hash header of type |hash_header_type|.
174 void CompleteDownload(int response_code
, HashHeaderType hash_header_type
);
176 void DownloadDone(const AttachmentId
& attachment_id
,
177 const AttachmentDownloader::DownloadResult
& result
,
178 scoped_ptr
<Attachment
> attachment
);
180 void VerifyDownloadResult(const AttachmentId
& attachment_id
,
181 const AttachmentDownloader::DownloadResult
& result
);
183 void RunMessageLoop();
186 static void AddHashHeader(HashHeaderType hash_header_type
,
187 net::TestURLFetcher
* fetcher
);
189 base::MessageLoopForIO message_loop_
;
190 scoped_refptr
<net::URLRequestContextGetter
> url_request_context_getter_
;
191 net::TestURLFetcherFactory url_fetcher_factory_
;
192 scoped_ptr
<MockOAuth2TokenService
> token_service_
;
193 scoped_ptr
<AttachmentDownloader
> attachment_downloader_
;
194 ResultsMap download_results_
;
195 int num_completed_downloads_
;
198 void AttachmentDownloaderImplTest::SetUp() {
199 url_request_context_getter_
=
200 new net::TestURLRequestContextGetter(message_loop_
.message_loop_proxy());
201 url_fetcher_factory_
.set_remove_fetcher_on_delete(true);
202 token_service_
.reset(new MockOAuth2TokenService());
203 token_service_
->AddAccount(kAccountId
);
204 scoped_refptr
<OAuth2TokenServiceRequest::TokenServiceProvider
>
205 token_service_provider(new TokenServiceProvider(token_service_
.get()));
207 OAuth2TokenService::ScopeSet scopes
;
208 scopes
.insert(GaiaConstants::kChromeSyncOAuth2Scope
);
209 attachment_downloader_
= AttachmentDownloader::Create(
210 GURL(kAttachmentServerUrl
), url_request_context_getter_
, kAccountId
,
211 scopes
, token_service_provider
, std::string(kStoreBirthday
), kModelType
);
214 void AttachmentDownloaderImplTest::TearDown() {
218 void AttachmentDownloaderImplTest::CompleteDownload(
220 HashHeaderType hash_header_type
) {
221 // TestURLFetcherFactory remembers last active URLFetcher.
222 net::TestURLFetcher
* fetcher
= url_fetcher_factory_
.GetFetcherByID(0);
223 // There should be outstanding url fetch request.
224 EXPECT_TRUE(fetcher
!= NULL
);
225 fetcher
->set_status(net::URLRequestStatus());
226 fetcher
->set_response_code(response_code
);
227 if (response_code
== net::HTTP_OK
) {
228 fetcher
->SetResponseString(kAttachmentContent
);
230 AddHashHeader(hash_header_type
, fetcher
);
232 // Call URLFetcherDelegate.
233 net::URLFetcherDelegate
* delegate
= fetcher
->delegate();
234 delegate
->OnURLFetchComplete(fetcher
);
236 // Once result is processed URLFetcher should be deleted.
237 fetcher
= url_fetcher_factory_
.GetFetcherByID(0);
238 EXPECT_TRUE(fetcher
== NULL
);
241 void AttachmentDownloaderImplTest::DownloadDone(
242 const AttachmentId
& attachment_id
,
243 const AttachmentDownloader::DownloadResult
& result
,
244 scoped_ptr
<Attachment
> attachment
) {
245 download_results_
.insert(std::make_pair(attachment_id
, result
));
246 if (result
== AttachmentDownloader::DOWNLOAD_SUCCESS
) {
247 // Successful download should be accompanied by valid attachment with
248 // matching id and valid data.
249 EXPECT_TRUE(attachment
!= NULL
);
250 EXPECT_EQ(attachment_id
, attachment
->GetId());
252 scoped_refptr
<base::RefCountedMemory
> data
= attachment
->GetData();
253 std::string
data_as_string(data
->front_as
<char>(), data
->size());
254 EXPECT_EQ(data_as_string
, kAttachmentContent
);
256 EXPECT_TRUE(attachment
== NULL
);
258 ++num_completed_downloads_
;
261 void AttachmentDownloaderImplTest::VerifyDownloadResult(
262 const AttachmentId
& attachment_id
,
263 const AttachmentDownloader::DownloadResult
& result
) {
264 ResultsMap::const_iterator iter
= download_results_
.find(attachment_id
);
265 EXPECT_TRUE(iter
!= download_results_
.end());
266 EXPECT_EQ(iter
->second
, result
);
269 void AttachmentDownloaderImplTest::RunMessageLoop() {
270 base::RunLoop run_loop
;
271 run_loop
.RunUntilIdle();
274 void AttachmentDownloaderImplTest::AddHashHeader(
275 HashHeaderType hash_header_type
,
276 net::TestURLFetcher
* fetcher
) {
277 std::string header
= "X-Goog-Hash: crc32c=";
278 scoped_refptr
<net::HttpResponseHeaders
> headers(
279 new net::HttpResponseHeaders(""));
280 switch (hash_header_type
) {
281 case HASH_HEADER_NONE
:
283 case HASH_HEADER_VALID
:
284 header
+= AttachmentUploaderImpl::FormatCrc32cHash(leveldb::crc32c::Value(
285 kAttachmentContent
, strlen(kAttachmentContent
)));
286 headers
->AddHeader(header
);
288 case HASH_HEADER_INVALID
:
289 header
+= "BOGUS1==";
290 headers
->AddHeader(header
);
293 fetcher
->set_response_headers(headers
);
296 TEST_F(AttachmentDownloaderImplTest
, HappyCase
) {
297 AttachmentId id1
= AttachmentId::Create();
298 // DownloadAttachment should trigger RequestAccessToken.
299 downloader()->DownloadAttachment(id1
, download_callback(id1
));
301 // Return valid access token.
302 token_service()->RespondToAccessTokenRequest(
303 GoogleServiceAuthError::AuthErrorNone());
305 // Catch histogram entries.
306 base::HistogramTester histogram_tester
;
307 // Check that there is outstanding URLFetcher request and complete it.
308 CompleteDownload(net::HTTP_OK
, HASH_HEADER_VALID
);
309 // Verify that the response code was logged properly.
310 histogram_tester
.ExpectUniqueSample("Sync.Attachments.DownloadResponseCode",
312 // Verify that callback was called for the right id with the right result.
313 VerifyDownloadResult(id1
, AttachmentDownloader::DOWNLOAD_SUCCESS
);
316 TEST_F(AttachmentDownloaderImplTest
, SameIdMultipleDownloads
) {
317 AttachmentId id1
= AttachmentId::Create();
318 base::HistogramTester histogram_tester
;
319 // Call DownloadAttachment two times for the same id.
320 downloader()->DownloadAttachment(id1
, download_callback(id1
));
321 downloader()->DownloadAttachment(id1
, download_callback(id1
));
323 // Return valid access token.
324 token_service()->RespondToAccessTokenRequest(
325 GoogleServiceAuthError::AuthErrorNone());
327 // Start one more download after access token is received.
328 downloader()->DownloadAttachment(id1
, download_callback(id1
));
329 // Complete URLFetcher request.
330 CompleteDownload(net::HTTP_OK
, HASH_HEADER_VALID
);
331 // Verify that all download requests completed.
332 VerifyDownloadResult(id1
, AttachmentDownloader::DOWNLOAD_SUCCESS
);
333 EXPECT_EQ(3, num_completed_downloads());
335 // Let's download the same attachment again.
336 downloader()->DownloadAttachment(id1
, download_callback(id1
));
338 // Verify that it didn't finish prematurely.
339 EXPECT_EQ(3, num_completed_downloads());
340 // Return valid access token.
341 token_service()->RespondToAccessTokenRequest(
342 GoogleServiceAuthError::AuthErrorNone());
344 // Complete URLFetcher request.
345 CompleteDownload(net::HTTP_OK
, HASH_HEADER_VALID
);
346 // Verify that all download requests completed.
347 VerifyDownloadResult(id1
, AttachmentDownloader::DOWNLOAD_SUCCESS
);
348 EXPECT_EQ(4, num_completed_downloads());
349 histogram_tester
.ExpectUniqueSample("Sync.Attachments.DownloadResponseCode",
353 TEST_F(AttachmentDownloaderImplTest
, RequestAccessTokenFails
) {
354 AttachmentId id1
= AttachmentId::Create();
355 AttachmentId id2
= AttachmentId::Create();
356 // Trigger first RequestAccessToken.
357 downloader()->DownloadAttachment(id1
, download_callback(id1
));
359 // Return valid access token.
360 token_service()->RespondToAccessTokenRequest(
361 GoogleServiceAuthError::AuthErrorNone());
363 // Trigger second RequestAccessToken.
364 downloader()->DownloadAttachment(id2
, download_callback(id2
));
366 // Fail RequestAccessToken.
367 token_service()->RespondToAccessTokenRequest(
368 GoogleServiceAuthError(GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS
));
370 // Only id2 should fail.
371 VerifyDownloadResult(id2
, AttachmentDownloader::DOWNLOAD_TRANSIENT_ERROR
);
372 // Complete request for id1.
373 CompleteDownload(net::HTTP_OK
, HASH_HEADER_VALID
);
374 VerifyDownloadResult(id1
, AttachmentDownloader::DOWNLOAD_SUCCESS
);
377 TEST_F(AttachmentDownloaderImplTest
, URLFetcher_BadToken
) {
378 AttachmentId id1
= AttachmentId::Create();
379 downloader()->DownloadAttachment(id1
, download_callback(id1
));
381 // Return valid access token.
382 token_service()->RespondToAccessTokenRequest(
383 GoogleServiceAuthError::AuthErrorNone());
385 // Fail URLFetcher. This should trigger download failure and access token
387 base::HistogramTester histogram_tester
;
388 CompleteDownload(net::HTTP_UNAUTHORIZED
, HASH_HEADER_VALID
);
389 EXPECT_EQ(1, token_service()->num_invalidate_token());
390 VerifyDownloadResult(id1
, AttachmentDownloader::DOWNLOAD_TRANSIENT_ERROR
);
391 histogram_tester
.ExpectUniqueSample("Sync.Attachments.DownloadResponseCode",
392 net::HTTP_UNAUTHORIZED
, 1);
395 TEST_F(AttachmentDownloaderImplTest
, URLFetcher_ServiceUnavailable
) {
396 AttachmentId id1
= AttachmentId::Create();
397 downloader()->DownloadAttachment(id1
, download_callback(id1
));
399 // Return valid access token.
400 token_service()->RespondToAccessTokenRequest(
401 GoogleServiceAuthError::AuthErrorNone());
403 // Fail URLFetcher. This should trigger download failure. Access token
404 // shouldn't be invalidated.
405 base::HistogramTester histogram_tester
;
406 CompleteDownload(net::HTTP_SERVICE_UNAVAILABLE
, HASH_HEADER_VALID
);
407 EXPECT_EQ(0, token_service()->num_invalidate_token());
408 VerifyDownloadResult(id1
, AttachmentDownloader::DOWNLOAD_TRANSIENT_ERROR
);
409 histogram_tester
.ExpectUniqueSample("Sync.Attachments.DownloadResponseCode",
410 net::HTTP_SERVICE_UNAVAILABLE
, 1);
413 // Verify that if no hash is present on the response the downloader accepts the
414 // received attachment.
415 TEST_F(AttachmentDownloaderImplTest
, NoHash
) {
416 AttachmentId id1
= AttachmentId::Create();
417 downloader()->DownloadAttachment(id1
, download_callback(id1
));
419 token_service()->RespondToAccessTokenRequest(
420 GoogleServiceAuthError::AuthErrorNone());
422 CompleteDownload(net::HTTP_OK
, HASH_HEADER_NONE
);
423 VerifyDownloadResult(id1
, AttachmentDownloader::DOWNLOAD_SUCCESS
);
426 // Verify that if an invalid hash is present on the response the downloader
427 // treats it as a transient error.
428 TEST_F(AttachmentDownloaderImplTest
, InvalidHash
) {
429 AttachmentId id1
= AttachmentId::Create();
430 downloader()->DownloadAttachment(id1
, download_callback(id1
));
432 token_service()->RespondToAccessTokenRequest(
433 GoogleServiceAuthError::AuthErrorNone());
435 CompleteDownload(net::HTTP_OK
, HASH_HEADER_INVALID
);
436 VerifyDownloadResult(id1
, AttachmentDownloader::DOWNLOAD_TRANSIENT_ERROR
);
439 // Verify that extract fails when there is no headers object.
440 TEST_F(AttachmentDownloaderImplTest
, ExtractCrc32c_NoHeaders
) {
442 ASSERT_FALSE(AttachmentDownloaderImpl::ExtractCrc32c(nullptr, &extracted
));
445 // Verify that extract fails when there is no crc32c value.
446 TEST_F(AttachmentDownloaderImplTest
, ExtractCrc32c_Empty
) {
448 raw
+= "HTTP/1.1 200 OK\n";
450 raw
+= "X-Goog-HASH: crc32c=\n";
452 std::replace(raw
.begin(), raw
.end(), '\n', '\0');
453 scoped_refptr
<net::HttpResponseHeaders
> headers(
454 new net::HttpResponseHeaders(raw
));
457 AttachmentDownloaderImpl::ExtractCrc32c(headers
.get(), &extracted
));
460 // Verify that extract finds the first crc32c and ignores others.
461 TEST_F(AttachmentDownloaderImplTest
, ExtractCrc32c_First
) {
462 const std::string expected_encoded
= "z8SuHQ==";
463 const uint32_t expected
= 3485773341;
465 raw
+= "HTTP/1.1 200 OK\n";
467 // Ignored because it's the wrong header.
468 raw
+= "X-Goog-Hashes: crc32c=AAAAAA==\n";
469 // Header name matches. The md5 item is ignored.
470 raw
+= "X-Goog-HASH: md5=rL0Y20zC+Fzt72VPzMSk2A==,crc32c=" +
471 expected_encoded
+ "\n";
472 // Ignored because we already found a crc32c in the one above.
473 raw
+= "X-Goog-HASH: crc32c=AAAAAA==\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
));
481 ASSERT_EQ(expected
, extracted
);
484 // Verify that extract fails when encoded value is too long.
485 TEST_F(AttachmentDownloaderImplTest
, ExtractCrc32c_TooLong
) {
487 raw
+= "HTTP/1.1 200 OK\n";
489 raw
+= "X-Goog-HASH: crc32c=AAAAAAAA\n";
491 std::replace(raw
.begin(), raw
.end(), '\n', '\0');
492 scoped_refptr
<net::HttpResponseHeaders
> headers(
493 new net::HttpResponseHeaders(raw
));
496 AttachmentDownloaderImpl::ExtractCrc32c(headers
.get(), &extracted
));
499 // Verify that extract fails if there is no crc32c.
500 TEST_F(AttachmentDownloaderImplTest
, ExtractCrc32c_None
) {
502 raw
+= "HTTP/1.1 200 OK\n";
504 raw
+= "X-Goog-Hash: md5=rL0Y20zC+Fzt72VPzMSk2A==\n";
506 std::replace(raw
.begin(), raw
.end(), '\n', '\0');
507 scoped_refptr
<net::HttpResponseHeaders
> headers(
508 new net::HttpResponseHeaders(raw
));
511 AttachmentDownloaderImpl::ExtractCrc32c(headers
.get(), &extracted
));
514 } // namespace syncer