diff options
author | pavely@chromium.org <pavely@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-06-26 00:33:12 +0000 |
---|---|---|
committer | pavely@chromium.org <pavely@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-06-26 00:33:12 +0000 |
commit | ce00f0b009a5aa00061e8ca8f4b758748aecbcd8 (patch) | |
tree | ffbe26827755d5b85ed87447122e2a990ae5b563 /sync | |
parent | 0cfd3ef80dffd40aa118139720c1b1ca58e830f7 (diff) | |
download | chromium_src-ce00f0b009a5aa00061e8ca8f4b758748aecbcd8.zip chromium_src-ce00f0b009a5aa00061e8ca8f4b758748aecbcd8.tar.gz chromium_src-ce00f0b009a5aa00061e8ca8f4b758748aecbcd8.tar.bz2 |
AttachmentDownloader implementation.
Supports auth.
BUG=376073
R=maniscalco@chromium.org
Review URL: https://codereview.chromium.org/345233002
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@279878 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'sync')
5 files changed, 616 insertions, 41 deletions
diff --git a/sync/api/attachments/attachment_downloader.cc b/sync/api/attachments/attachment_downloader.cc index a0108f7..7fd32cd 100644 --- a/sync/api/attachments/attachment_downloader.cc +++ b/sync/api/attachments/attachment_downloader.cc @@ -4,9 +4,30 @@ #include "sync/api/attachments/attachment_downloader.h" +#include "sync/internal_api/public/attachments/attachment_downloader_impl.h" + namespace syncer { AttachmentDownloader::~AttachmentDownloader() { } +// Factory function for creating AttachmentDownloaderImpl. +// It is introduced to avoid SYNC_EXPORT-ing AttachmentDownloaderImpl since it +// inherits from OAuth2TokenService::Consumer which is not exported. +scoped_ptr<AttachmentDownloader> AttachmentDownloader::Create( + const std::string& url_prefix, + const scoped_refptr<net::URLRequestContextGetter>& + url_request_context_getter, + const std::string& account_id, + const OAuth2TokenService::ScopeSet scopes, + scoped_ptr<OAuth2TokenServiceRequest::TokenServiceProvider> + token_service_provider) { + return scoped_ptr<AttachmentDownloader>( + new AttachmentDownloaderImpl(url_prefix, + url_request_context_getter, + account_id, + scopes, + token_service_provider.Pass())); +} + } // namespace syncer diff --git a/sync/api/attachments/attachment_downloader.h b/sync/api/attachments/attachment_downloader.h index 0473899..718284c 100644 --- a/sync/api/attachments/attachment_downloader.h +++ b/sync/api/attachments/attachment_downloader.h @@ -7,9 +7,14 @@ #include "base/callback.h" #include "base/memory/scoped_ptr.h" +#include "google_apis/gaia/oauth2_token_service_request.h" #include "sync/api/attachments/attachment.h" #include "sync/base/sync_export.h" +namespace net { +class URLRequestContextGetter; +} // namespace net + namespace syncer { // AttachmentDownloader is responsible for downloading attachments from server. @@ -34,6 +39,26 @@ class SYNC_EXPORT AttachmentDownloader { // DownloadResult is not DOWNLOAD_SUCCESS then attachment pointer is NULL. virtual void DownloadAttachment(const AttachmentId& attachment_id, const DownloadCallback& callback) = 0; + + // Create instance of AttachmentDownloaderImpl. + // |url_prefix| is the URL prefix (including trailing slash) to be used when + // downloading attachments. + // + // |url_request_context_getter| provides a URLRequestContext. + // + // |account_id| is the account id to use for downloads. + // + // |scopes| is the set of scopes to use for downloads. + // + // |token_service_provider| provides an OAuth2 token service. + static scoped_ptr<AttachmentDownloader> Create( + const std::string& url_prefix, + const scoped_refptr<net::URLRequestContextGetter>& + url_request_context_getter, + const std::string& account_id, + const OAuth2TokenService::ScopeSet scopes, + scoped_ptr<OAuth2TokenServiceRequest::TokenServiceProvider> + token_service_provider); }; } // namespace syncer diff --git a/sync/internal_api/attachments/attachment_downloader_impl.cc b/sync/internal_api/attachments/attachment_downloader_impl.cc index c89b376..18c49d8 100644 --- a/sync/internal_api/attachments/attachment_downloader_impl.cc +++ b/sync/internal_api/attachments/attachment_downloader_impl.cc @@ -6,27 +6,199 @@ #include "base/bind.h" #include "base/message_loop/message_loop.h" +#include "net/base/load_flags.h" +#include "net/http/http_status_code.h" +#include "net/url_request/url_fetcher.h" +#include "sync/protocol/sync.pb.h" +#include "url/gurl.h" namespace syncer { -AttachmentDownloaderImpl::AttachmentDownloaderImpl() { +struct AttachmentDownloaderImpl::DownloadState { + public: + DownloadState(const AttachmentId& attachment_id, + const AttachmentUrl& attachment_url); + + AttachmentId attachment_id; + AttachmentUrl attachment_url; + // |access_token| needed to invalidate if downloading attachment fails with + // HTTP_UNAUTHORIZED. + std::string access_token; + scoped_ptr<net::URLFetcher> url_fetcher; + std::vector<DownloadCallback> user_callbacks; +}; + +AttachmentDownloaderImpl::DownloadState::DownloadState( + const AttachmentId& attachment_id, + const AttachmentUrl& attachment_url) + : attachment_id(attachment_id), attachment_url(attachment_url) { +} + +AttachmentDownloaderImpl::AttachmentDownloaderImpl( + const std::string& url_prefix, + const scoped_refptr<net::URLRequestContextGetter>& + url_request_context_getter, + const std::string& account_id, + const OAuth2TokenService::ScopeSet& scopes, + scoped_ptr<OAuth2TokenServiceRequest::TokenServiceProvider> + token_service_provider) + : OAuth2TokenService::Consumer("attachment-downloader-impl"), + url_prefix_(url_prefix), + url_request_context_getter_(url_request_context_getter), + account_id_(account_id), + oauth2_scopes_(scopes), + token_service_provider_(token_service_provider.Pass()) { + DCHECK(token_service_provider_); + DCHECK(url_request_context_getter_); + DCHECK(!url_prefix_.empty()); } AttachmentDownloaderImpl::~AttachmentDownloaderImpl() { - DCHECK(CalledOnValidThread()); } void AttachmentDownloaderImpl::DownloadAttachment( const AttachmentId& attachment_id, const DownloadCallback& callback) { DCHECK(CalledOnValidThread()); - // No real implementation yet. Fail every request with - // DOWNLOAD_UNSPECIFIED_ERROR. - scoped_ptr<Attachment> attachment; - base::MessageLoop::current()->PostTask( - FROM_HERE, - base::Bind( - callback, DOWNLOAD_UNSPECIFIED_ERROR, base::Passed(&attachment))); + + AttachmentUrl url = GetAttachmentUrl(attachment_id); + + StateMap::iterator iter = state_map_.find(url); + if (iter == state_map_.end()) { + // There is no request started for this attachment id. Let's create + // DownloadState and request access token for it. + scoped_ptr<DownloadState> new_download_state( + new DownloadState(attachment_id, url)); + iter = state_map_.add(url, new_download_state.Pass()).first; + RequestAccessToken(iter->second); + } + DownloadState* download_state = iter->second; + DCHECK(download_state->attachment_id == attachment_id); + download_state->user_callbacks.push_back(callback); +} + +void AttachmentDownloaderImpl::OnGetTokenSuccess( + const OAuth2TokenService::Request* request, + const std::string& access_token, + const base::Time& expiration_time) { + DCHECK(CalledOnValidThread()); + DCHECK(request == access_token_request_.get()); + access_token_request_.reset(); + StateList::const_iterator iter; + // Start downloads for all download requests waiting for access token. + for (iter = requests_waiting_for_access_token_.begin(); + iter != requests_waiting_for_access_token_.end(); + ++iter) { + DownloadState* download_state = *iter; + download_state->access_token = access_token; + download_state->url_fetcher = + CreateFetcher(download_state->attachment_url, access_token).Pass(); + download_state->url_fetcher->Start(); + } + requests_waiting_for_access_token_.clear(); +} + +void AttachmentDownloaderImpl::OnGetTokenFailure( + const OAuth2TokenService::Request* request, + const GoogleServiceAuthError& error) { + DCHECK(CalledOnValidThread()); + DCHECK(request == access_token_request_.get()); + access_token_request_.reset(); + StateList::const_iterator iter; + // Without access token all downloads fail. + for (iter = requests_waiting_for_access_token_.begin(); + iter != requests_waiting_for_access_token_.end(); + ++iter) { + DownloadState* download_state = *iter; + scoped_refptr<base::RefCountedString> null_attachment_data; + ReportResult( + *download_state, DOWNLOAD_UNSPECIFIED_ERROR, null_attachment_data); + DCHECK(state_map_.find(download_state->attachment_url) != state_map_.end()); + state_map_.erase(download_state->attachment_url); + } + requests_waiting_for_access_token_.clear(); +} + +void AttachmentDownloaderImpl::OnURLFetchComplete( + const net::URLFetcher* source) { + DCHECK(CalledOnValidThread()); + + // Find DownloadState by url. + AttachmentUrl url = source->GetOriginalURL().spec(); + StateMap::iterator iter = state_map_.find(url); + DCHECK(iter != state_map_.end()); + const DownloadState& download_state = *iter->second; + DCHECK(source == download_state.url_fetcher.get()); + + DownloadResult result = DOWNLOAD_UNSPECIFIED_ERROR; + scoped_refptr<base::RefCountedString> attachment_data; + + if (source->GetResponseCode() == net::HTTP_OK) { + result = DOWNLOAD_SUCCESS; + std::string data_as_string; + source->GetResponseAsString(&data_as_string); + attachment_data = base::RefCountedString::TakeString(&data_as_string); + } else if (source->GetResponseCode() == net::HTTP_UNAUTHORIZED) { + OAuth2TokenServiceRequest::InvalidateToken(token_service_provider_.get(), + account_id_, + oauth2_scopes_, + download_state.access_token); + // TODO(pavely): crbug/380437. This is transient error. Request new access + // token for this DownloadState. The only trick is to do it with exponential + // backoff. + } + ReportResult(download_state, result, attachment_data); + state_map_.erase(iter); +} + +AttachmentDownloaderImpl::AttachmentUrl +AttachmentDownloaderImpl::GetAttachmentUrl(const AttachmentId& attachment_id) { + return url_prefix_ + attachment_id.GetProto().unique_id(); +} + +scoped_ptr<net::URLFetcher> AttachmentDownloaderImpl::CreateFetcher( + const AttachmentUrl& url, + const std::string& access_token) { + scoped_ptr<net::URLFetcher> url_fetcher( + net::URLFetcher::Create(GURL(url), net::URLFetcher::GET, this)); + const std::string auth_header("Authorization: Bearer " + access_token); + url_fetcher->AddExtraRequestHeader(auth_header); + url_fetcher->SetRequestContext(url_request_context_getter_.get()); + url_fetcher->SetLoadFlags(net::LOAD_DO_NOT_SAVE_COOKIES | + net::LOAD_DO_NOT_SEND_COOKIES | + net::LOAD_DISABLE_CACHE); + // TODO(maniscalco): Set an appropriate headers (User-Agent, what else?) on + // the request (bug 371521). + return url_fetcher.Pass(); +} + +void AttachmentDownloaderImpl::RequestAccessToken( + DownloadState* download_state) { + requests_waiting_for_access_token_.push_back(download_state); + // Start access token request if there is no active one. + if (access_token_request_ == NULL) { + access_token_request_ = OAuth2TokenServiceRequest::CreateAndStart( + token_service_provider_.get(), account_id_, oauth2_scopes_, this); + } +} + +void AttachmentDownloaderImpl::ReportResult( + const DownloadState& download_state, + const DownloadResult& result, + const scoped_refptr<base::RefCountedString>& attachment_data) { + std::vector<DownloadCallback>::const_iterator iter; + for (iter = download_state.user_callbacks.begin(); + iter != download_state.user_callbacks.end(); + ++iter) { + scoped_ptr<Attachment> attachment; + if (result == DOWNLOAD_SUCCESS) { + attachment.reset(new Attachment(Attachment::CreateWithId( + download_state.attachment_id, attachment_data))); + } + + base::MessageLoop::current()->PostTask( + FROM_HERE, base::Bind(*iter, result, base::Passed(&attachment))); + } } } // namespace syncer diff --git a/sync/internal_api/attachments/attachment_downloader_impl_unittest.cc b/sync/internal_api/attachments/attachment_downloader_impl_unittest.cc index 240aae17..f8d1393 100644 --- a/sync/internal_api/attachments/attachment_downloader_impl_unittest.cc +++ b/sync/internal_api/attachments/attachment_downloader_impl_unittest.cc @@ -2,60 +2,355 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#include "sync/internal_api/public/attachments/attachment_downloader_impl.h" +#include "sync/api/attachments/attachment_downloader.h" #include "base/bind.h" +#include "base/memory/weak_ptr.h" #include "base/message_loop/message_loop.h" #include "base/run_loop.h" +#include "google_apis/gaia/fake_oauth2_token_service.h" +#include "google_apis/gaia/gaia_constants.h" +#include "net/url_request/test_url_fetcher_factory.h" +#include "net/url_request/url_request_test_util.h" #include "sync/api/attachments/attachment.h" #include "testing/gtest/include/gtest/gtest.h" namespace syncer { -class AttachmentDownloaderImplTest : public testing::Test { +namespace { + +const char kAccountId[] = "attachments@gmail.com"; +const char kAccessToken[] = "access.token"; +const char kAttachmentServerUrl[] = "http://attachments.com/"; +const char kAttachmentContent[] = "attachment.content"; + +// MockOAuth2TokenService remembers last request for access token and verifies +// that only one request is active at a time. +// Call RespondToAccessTokenRequest to respond to it. +class MockOAuth2TokenService : public FakeOAuth2TokenService { + public: + MockOAuth2TokenService() : num_invalidate_token_(0) {} + + virtual ~MockOAuth2TokenService() {} + + void RespondToAccessTokenRequest(GoogleServiceAuthError error); + + int num_invalidate_token() const { return num_invalidate_token_; } + protected: - AttachmentDownloaderImplTest() {} + virtual void FetchOAuth2Token(RequestImpl* request, + const std::string& account_id, + net::URLRequestContextGetter* getter, + const std::string& client_id, + const std::string& client_secret, + const ScopeSet& scopes) OVERRIDE; - virtual void SetUp() OVERRIDE {} + virtual void InvalidateOAuth2Token(const std::string& account_id, + const std::string& client_id, + const ScopeSet& scopes, + const std::string& access_token) OVERRIDE; - virtual void TearDown() OVERRIDE {} + private: + base::WeakPtr<RequestImpl> last_request_; + int num_invalidate_token_; +}; - AttachmentDownloader* downloader() { - return &attachment_downloader_; +void MockOAuth2TokenService::RespondToAccessTokenRequest( + GoogleServiceAuthError error) { + EXPECT_TRUE(last_request_ != NULL); + std::string access_token; + base::Time expiration_time; + if (error == GoogleServiceAuthError::AuthErrorNone()) { + access_token = kAccessToken; + expiration_time = base::Time::Max(); } + base::MessageLoop::current()->PostTask( + FROM_HERE, + base::Bind(&OAuth2TokenService::RequestImpl::InformConsumer, + last_request_, + error, + access_token, + expiration_time)); +} + +void MockOAuth2TokenService::FetchOAuth2Token( + RequestImpl* request, + const std::string& account_id, + net::URLRequestContextGetter* getter, + const std::string& client_id, + const std::string& client_secret, + const ScopeSet& scopes) { + // Only one request at a time is allowed. + EXPECT_TRUE(last_request_ == NULL); + last_request_ = request->AsWeakPtr(); +} + +void MockOAuth2TokenService::InvalidateOAuth2Token( + const std::string& account_id, + const std::string& client_id, + const ScopeSet& scopes, + const std::string& access_token) { + ++num_invalidate_token_; +} + +class TokenServiceProvider + : public OAuth2TokenServiceRequest::TokenServiceProvider, + base::NonThreadSafe { + public: + TokenServiceProvider(OAuth2TokenService* token_service); + virtual ~TokenServiceProvider(); + + // OAuth2TokenService::TokenServiceProvider implementation. + virtual scoped_refptr<base::SingleThreadTaskRunner> + GetTokenServiceTaskRunner() OVERRIDE; + virtual OAuth2TokenService* GetTokenService() OVERRIDE; + + private: + scoped_refptr<base::SingleThreadTaskRunner> task_runner_; + OAuth2TokenService* token_service_; +}; + +TokenServiceProvider::TokenServiceProvider(OAuth2TokenService* token_service) + : task_runner_(base::MessageLoopProxy::current()), + token_service_(token_service) { + DCHECK(token_service_); +} + +TokenServiceProvider::~TokenServiceProvider() { +} + +scoped_refptr<base::SingleThreadTaskRunner> +TokenServiceProvider::GetTokenServiceTaskRunner() { + return task_runner_; +} + +OAuth2TokenService* TokenServiceProvider::GetTokenService() { + DCHECK(task_runner_->BelongsToCurrentThread()); + return token_service_; +} + +} // namespace + +class AttachmentDownloaderImplTest : public testing::Test { + protected: + typedef std::map<AttachmentId, AttachmentDownloader::DownloadResult> + ResultsMap; + + AttachmentDownloaderImplTest() : num_completed_downloads_(0) {} + + virtual void SetUp() OVERRIDE; + virtual void TearDown() OVERRIDE; + + AttachmentDownloader* downloader() { return attachment_downloader_.get(); } + + MockOAuth2TokenService* token_service() { return token_service_.get(); } - AttachmentDownloader::DownloadCallback download_callback() { + int num_completed_downloads() { return num_completed_downloads_; } + + AttachmentDownloader::DownloadCallback download_callback( + const AttachmentId& id) { return base::Bind(&AttachmentDownloaderImplTest::DownloadDone, - base::Unretained(this)); + base::Unretained(this), + id); } - const std::vector<AttachmentDownloader::DownloadResult>& download_results() { - return download_results_; - } + void CompleteDownload(int response_code); - void RunMessageLoop() { - base::RunLoop run_loop; - run_loop.RunUntilIdle(); - } + void DownloadDone(const AttachmentId& attachment_id, + const AttachmentDownloader::DownloadResult& result, + scoped_ptr<Attachment> attachment); + + void VerifyDownloadResult(const AttachmentId& attachment_id, + const AttachmentDownloader::DownloadResult& result); + + void RunMessageLoop(); private: - void DownloadDone(const AttachmentDownloader::DownloadResult& result, - scoped_ptr<Attachment> attachment) { - download_results_.push_back(result); + base::MessageLoopForIO message_loop_; + scoped_refptr<net::URLRequestContextGetter> url_request_context_getter_; + net::TestURLFetcherFactory url_fetcher_factory_; + scoped_ptr<MockOAuth2TokenService> token_service_; + scoped_ptr<AttachmentDownloader> attachment_downloader_; + ResultsMap download_results_; + int num_completed_downloads_; +}; + +void AttachmentDownloaderImplTest::SetUp() { + url_request_context_getter_ = + new net::TestURLRequestContextGetter(message_loop_.message_loop_proxy()); + url_fetcher_factory_.set_remove_fetcher_on_delete(true); + token_service_.reset(new MockOAuth2TokenService()); + token_service_->AddAccount(kAccountId); + scoped_ptr<OAuth2TokenServiceRequest::TokenServiceProvider> + token_service_provider(new TokenServiceProvider(token_service_.get())); + + OAuth2TokenService::ScopeSet scopes; + scopes.insert(GaiaConstants::kChromeSyncOAuth2Scope); + attachment_downloader_ = + AttachmentDownloader::Create(kAttachmentServerUrl, + url_request_context_getter_, + kAccountId, + scopes, + token_service_provider.Pass()); +} + +void AttachmentDownloaderImplTest::TearDown() { + RunMessageLoop(); +} + +void AttachmentDownloaderImplTest::CompleteDownload(int response_code) { + // TestURLFetcherFactory remembers last active URLFetcher. + net::TestURLFetcher* fetcher = url_fetcher_factory_.GetFetcherByID(0); + // There should be outstanding url fetch request. + EXPECT_TRUE(fetcher != NULL); + fetcher->set_status(net::URLRequestStatus()); + fetcher->set_response_code(response_code); + if (response_code == net::HTTP_OK) { + fetcher->SetResponseString(kAttachmentContent); + } + // Call URLFetcherDelegate. + net::URLFetcherDelegate* delegate = fetcher->delegate(); + delegate->OnURLFetchComplete(fetcher); + RunMessageLoop(); + // Once result is processed URLFetcher should be deleted. + fetcher = url_fetcher_factory_.GetFetcherByID(0); + EXPECT_TRUE(fetcher == NULL); +} + +void AttachmentDownloaderImplTest::DownloadDone( + const AttachmentId& attachment_id, + const AttachmentDownloader::DownloadResult& result, + scoped_ptr<Attachment> attachment) { + download_results_.insert(std::make_pair(attachment_id, result)); + if (result == AttachmentDownloader::DOWNLOAD_SUCCESS) { + // Successful download should be accompanied by valid attachment with + // matching id and valid data. + EXPECT_TRUE(attachment != NULL); + EXPECT_EQ(attachment_id, attachment->GetId()); + + scoped_refptr<base::RefCountedMemory> data = attachment->GetData(); + std::string data_as_string(data->front_as<char>(), data->size()); + EXPECT_EQ(data_as_string, kAttachmentContent); + } else { + EXPECT_TRUE(attachment == NULL); } + ++num_completed_downloads_; +} - base::MessageLoop message_loop_; - AttachmentDownloaderImpl attachment_downloader_; - std::vector<AttachmentDownloader::DownloadResult> download_results_; -}; +void AttachmentDownloaderImplTest::VerifyDownloadResult( + const AttachmentId& attachment_id, + const AttachmentDownloader::DownloadResult& result) { + ResultsMap::const_iterator iter = download_results_.find(attachment_id); + EXPECT_TRUE(iter != download_results_.end()); + EXPECT_EQ(iter->second, result); +} + +void AttachmentDownloaderImplTest::RunMessageLoop() { + base::RunLoop run_loop; + run_loop.RunUntilIdle(); +} + +TEST_F(AttachmentDownloaderImplTest, HappyCase) { + AttachmentId id1 = AttachmentId::Create(); + // DownloadAttachment should trigger RequestAccessToken. + downloader()->DownloadAttachment(id1, download_callback(id1)); + RunMessageLoop(); + // Return valid access token. + token_service()->RespondToAccessTokenRequest( + GoogleServiceAuthError::AuthErrorNone()); + RunMessageLoop(); + // Check that there is outstanding URLFetcher request and complete it. + CompleteDownload(net::HTTP_OK); + // Verify that callback was called for the right id with the right result. + VerifyDownloadResult(id1, AttachmentDownloader::DOWNLOAD_SUCCESS); +} + +TEST_F(AttachmentDownloaderImplTest, SameIdMultipleDownloads) { + AttachmentId id1 = AttachmentId::Create(); + // Call DownloadAttachment two times for the same id. + downloader()->DownloadAttachment(id1, download_callback(id1)); + downloader()->DownloadAttachment(id1, download_callback(id1)); + RunMessageLoop(); + // Return valid access token. + token_service()->RespondToAccessTokenRequest( + GoogleServiceAuthError::AuthErrorNone()); + RunMessageLoop(); + // Start one more download after access token is received. + downloader()->DownloadAttachment(id1, download_callback(id1)); + // Complete URLFetcher request. + CompleteDownload(net::HTTP_OK); + // Verify that all download requests completed. + VerifyDownloadResult(id1, AttachmentDownloader::DOWNLOAD_SUCCESS); + EXPECT_EQ(3, num_completed_downloads()); + + // Let's download the same attachment again. + downloader()->DownloadAttachment(id1, download_callback(id1)); + RunMessageLoop(); + // Verify that it didn't finish prematurely. + EXPECT_EQ(3, num_completed_downloads()); + // Return valid access token. + token_service()->RespondToAccessTokenRequest( + GoogleServiceAuthError::AuthErrorNone()); + RunMessageLoop(); + // Complete URLFetcher request. + CompleteDownload(net::HTTP_OK); + // Verify that all download requests completed. + VerifyDownloadResult(id1, AttachmentDownloader::DOWNLOAD_SUCCESS); + EXPECT_EQ(4, num_completed_downloads()); +} + +TEST_F(AttachmentDownloaderImplTest, RequestAccessTokenFails) { + AttachmentId id1 = AttachmentId::Create(); + AttachmentId id2 = AttachmentId::Create(); + // Trigger first RequestAccessToken. + downloader()->DownloadAttachment(id1, download_callback(id1)); + RunMessageLoop(); + // Return valid access token. + token_service()->RespondToAccessTokenRequest( + GoogleServiceAuthError::AuthErrorNone()); + RunMessageLoop(); + // Trigger second RequestAccessToken. + downloader()->DownloadAttachment(id2, download_callback(id2)); + RunMessageLoop(); + // Fail RequestAccessToken. + token_service()->RespondToAccessTokenRequest( + GoogleServiceAuthError(GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS)); + RunMessageLoop(); + // Only id2 should fail. + VerifyDownloadResult(id2, AttachmentDownloader::DOWNLOAD_UNSPECIFIED_ERROR); + // Complete request for id1. + CompleteDownload(net::HTTP_OK); + VerifyDownloadResult(id1, AttachmentDownloader::DOWNLOAD_SUCCESS); +} -TEST_F(AttachmentDownloaderImplTest, DownloadAttachment_Fail) { - AttachmentId attachment_id = AttachmentId::Create(); - downloader()->DownloadAttachment(attachment_id, download_callback()); +TEST_F(AttachmentDownloaderImplTest, URLFetcher_BadToken) { + AttachmentId id1 = AttachmentId::Create(); + downloader()->DownloadAttachment(id1, download_callback(id1)); + RunMessageLoop(); + // Return valid access token. + token_service()->RespondToAccessTokenRequest( + GoogleServiceAuthError::AuthErrorNone()); + RunMessageLoop(); + // Fail URLFetcher. This should trigger download failure and access token + // invalidation. + CompleteDownload(net::HTTP_UNAUTHORIZED); + EXPECT_EQ(1, token_service()->num_invalidate_token()); + VerifyDownloadResult(id1, AttachmentDownloader::DOWNLOAD_UNSPECIFIED_ERROR); +} + +TEST_F(AttachmentDownloaderImplTest, URLFetcher_ServiceUnavailable) { + AttachmentId id1 = AttachmentId::Create(); + downloader()->DownloadAttachment(id1, download_callback(id1)); + RunMessageLoop(); + // Return valid access token. + token_service()->RespondToAccessTokenRequest( + GoogleServiceAuthError::AuthErrorNone()); RunMessageLoop(); - EXPECT_EQ(1U, download_results().size()); - EXPECT_EQ(AttachmentDownloader::DOWNLOAD_UNSPECIFIED_ERROR, - download_results()[0]); + // Fail URLFetcher. This should trigger download failure. Access token + // shouldn't be invalidated. + CompleteDownload(net::HTTP_SERVICE_UNAVAILABLE); + EXPECT_EQ(0, token_service()->num_invalidate_token()); + VerifyDownloadResult(id1, AttachmentDownloader::DOWNLOAD_UNSPECIFIED_ERROR); } } // namespace syncer diff --git a/sync/internal_api/public/attachments/attachment_downloader_impl.h b/sync/internal_api/public/attachments/attachment_downloader_impl.h index 757e66b0..e3fee399e 100644 --- a/sync/internal_api/public/attachments/attachment_downloader_impl.h +++ b/sync/internal_api/public/attachments/attachment_downloader_impl.h @@ -5,23 +5,85 @@ #ifndef SYNC_INTERNAL_API_PUBLIC_ATTACHMENTS_ATTACHMENT_DOWNLOADER_IMPL_H_ #define SYNC_INTERNAL_API_PUBLIC_ATTACHMENTS_ATTACHMENT_DOWNLOADER_IMPL_H_ +#include "base/containers/scoped_ptr_hash_map.h" #include "base/threading/non_thread_safe.h" +#include "google_apis/gaia/oauth2_token_service_request.h" +#include "net/url_request/url_fetcher_delegate.h" +#include "net/url_request/url_request_context_getter.h" #include "sync/api/attachments/attachment_downloader.h" namespace syncer { // An implementation of AttachmentDownloader. -class SYNC_EXPORT AttachmentDownloaderImpl : public AttachmentDownloader, - public base::NonThreadSafe { +class AttachmentDownloaderImpl : public AttachmentDownloader, + public OAuth2TokenService::Consumer, + public net::URLFetcherDelegate, + public base::NonThreadSafe { public: - AttachmentDownloaderImpl(); + // |url_prefix| is the URL prefix (including trailing slash) to be used when + // downloading attachments. + // + // |url_request_context_getter| provides a URLRequestContext. + // + // |account_id| is the account id to use for downloads. + // + // |scopes| is the set of scopes to use for downloads. + // + // |token_service_provider| provides an OAuth2 token service. + AttachmentDownloaderImpl( + const std::string& url_prefix, + const scoped_refptr<net::URLRequestContextGetter>& + url_request_context_getter, + const std::string& account_id, + const OAuth2TokenService::ScopeSet& scopes, + scoped_ptr<OAuth2TokenServiceRequest::TokenServiceProvider> + token_service_provider); virtual ~AttachmentDownloaderImpl(); // AttachmentDownloader implementation. virtual void DownloadAttachment(const AttachmentId& attachment_id, const DownloadCallback& callback) OVERRIDE; + // OAuth2TokenService::Consumer implementation. + virtual void OnGetTokenSuccess(const OAuth2TokenService::Request* request, + const std::string& access_token, + const base::Time& expiration_time) OVERRIDE; + virtual void OnGetTokenFailure(const OAuth2TokenService::Request* request, + const GoogleServiceAuthError& error) OVERRIDE; + + // net::URLFetcherDelegate implementation. + virtual void OnURLFetchComplete(const net::URLFetcher* source) OVERRIDE; + private: + struct DownloadState; + typedef std::string AttachmentUrl; + typedef base::ScopedPtrHashMap<AttachmentUrl, DownloadState> StateMap; + typedef std::vector<DownloadState*> StateList; + + AttachmentUrl GetAttachmentUrl(const AttachmentId& attachment_id); + scoped_ptr<net::URLFetcher> CreateFetcher(const AttachmentUrl& url, + const std::string& access_token); + void RequestAccessToken(DownloadState* download_state); + void ReportResult( + const DownloadState& download_state, + const DownloadResult& result, + const scoped_refptr<base::RefCountedString>& attachment_data); + + std::string url_prefix_; + scoped_refptr<net::URLRequestContextGetter> url_request_context_getter_; + + std::string account_id_; + OAuth2TokenService::ScopeSet oauth2_scopes_; + scoped_ptr<OAuth2TokenServiceRequest::TokenServiceProvider> + token_service_provider_; + scoped_ptr<OAuth2TokenService::Request> access_token_request_; + + StateMap state_map_; + // |requests_waiting_for_access_token_| only keeps references to DownloadState + // objects while access token request is pending. It doesn't control objects' + // lifetime. + StateList requests_waiting_for_access_token_; + DISALLOW_COPY_AND_ASSIGN(AttachmentDownloaderImpl); }; |