| // Copyright 2025 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/save_to_drive/drive_uploader.h" |
| |
| #include <memory> |
| #include <string> |
| #include <utility> |
| |
| #include "base/json/json_writer.h" |
| #include "base/test/bind.h" |
| #include "base/test/gmock_callback_support.h" |
| #include "base/test/mock_callback.h" |
| #include "base/test/task_environment.h" |
| #include "base/test/test_future.h" |
| #include "base/time/time.h" |
| #include "chrome/browser/save_to_drive/content_reader.h" |
| #include "chrome/browser/signin/identity_test_environment_profile_adaptor.h" |
| #include "chrome/common/extensions/api/pdf_viewer_private.h" |
| #include "chrome/test/base/testing_profile.h" |
| #include "components/signin/public/identity_manager/identity_test_environment.h" |
| #include "content/public/test/browser_task_environment.h" |
| #include "content/public/test/url_loader_interceptor.h" |
| #include "google_apis/gaia/core_account_id.h" |
| #include "google_apis/gaia/gaia_urls.h" |
| #include "google_apis/gaia/google_service_auth_error.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| namespace save_to_drive { |
| |
| namespace { |
| |
| using base::test::TestFuture; |
| using extensions::api::pdf_viewer_private::SaveToDriveErrorType; |
| using extensions::api::pdf_viewer_private::SaveToDriveProgress; |
| using extensions::api::pdf_viewer_private::SaveToDriveStatus; |
| using testing::_; |
| using testing::AllOf; |
| using testing::Field; |
| |
| constexpr std::string_view kErrorResponseHeader = |
| "HTTP/1.1 500 Internal Server Error\nContent-Type: application/json\n\n"; |
| constexpr std::string_view kParentFolderUrl = |
| "https://www.googleapis.com/drive/v3beta/" |
| "files?create_as_client_folder=true"; |
| constexpr std::string_view kSuccessFulResponseHeader = |
| "HTTP/1.1 200 OK\nContent-Type: text/html\n\n"; |
| constexpr std::string_view kTestFileId = "test_file_id"; |
| constexpr std::string_view kTestFolderName = "test_folder"; |
| |
| class MockContentReader : public ContentReader { |
| public: |
| MOCK_METHOD(void, Open, (OpenCallback callback), (override)); |
| MOCK_METHOD(size_t, GetSize, (), (override)); |
| MOCK_METHOD(void, |
| Read, |
| (uint32_t offset, uint32_t size, ContentReadCallback callback), |
| (override)); |
| MOCK_METHOD(void, Close, (), (override)); |
| }; |
| |
| class FakeDriveUploader : public DriveUploader { |
| public: |
| FakeDriveUploader(std::string title, |
| AccountInfo account_info, |
| ProgressCallback progress_callback, |
| Profile* profile, |
| ContentReader* content_reader) |
| : DriveUploader(DriveUploaderType::kUnknown, |
| std::move(title), |
| std::move(account_info), |
| std::move(progress_callback), |
| profile, |
| content_reader) {} |
| FakeDriveUploader(const FakeDriveUploader&) = delete; |
| FakeDriveUploader& operator=(const FakeDriveUploader&) = delete; |
| ~FakeDriveUploader() override = default; |
| |
| MOCK_METHOD(void, UploadFile, (), (override)); |
| |
| void NotifyUploadInProgress(size_t uploaded_bytes, size_t total_bytes) { |
| DriveUploader::NotifyUploadInProgress(uploaded_bytes, total_bytes); |
| } |
| |
| const std::optional<Item>& parent_folder() const { return parent_folder_; } |
| }; |
| |
| class DriveUploaderTest : public testing::Test { |
| public: |
| DriveUploaderTest() |
| : profile_(IdentityTestEnvironmentProfileAdaptor:: |
| CreateProfileForIdentityTestEnvironment()), |
| adaptor_(std::make_unique<IdentityTestEnvironmentProfileAdaptor>( |
| profile_.get())) {} |
| DriveUploaderTest(const DriveUploaderTest&) = delete; |
| DriveUploaderTest& operator=(const DriveUploaderTest&) = delete; |
| ~DriveUploaderTest() override = default; |
| |
| protected: |
| signin::IdentityTestEnvironment* test_env() { |
| return adaptor_->identity_test_env(); |
| } |
| |
| content::BrowserTaskEnvironment task_environment_{ |
| base::test::TaskEnvironment::TimeSource::MOCK_TIME}; |
| std::unique_ptr<TestingProfile> profile_; |
| std::unique_ptr<IdentityTestEnvironmentProfileAdaptor> adaptor_; |
| base::MockCallback<DriveUploader::ProgressCallback> progress_callback_; |
| MockContentReader mock_content_reader_; |
| }; |
| |
| TEST_F(DriveUploaderTest, FetchAccessTokenSuccess) { |
| auto account_info = test_env()->MakePrimaryAccountAvailable( |
| "test@example.com", signin::ConsentLevel::kSignin); |
| auto uploader = std::make_unique<FakeDriveUploader>( |
| "test_title", account_info, progress_callback_.Get(), profile_.get(), |
| &mock_content_reader_); |
| |
| EXPECT_CALL(progress_callback_, |
| Run(AllOf(Field(&SaveToDriveProgress::status, |
| SaveToDriveStatus::kFetchOauth), |
| Field(&SaveToDriveProgress::error_type, |
| SaveToDriveErrorType::kNoError)))); |
| |
| uploader->Start(); |
| test_env()->WaitForAccessTokenRequestIfNecessaryAndRespondWithToken( |
| "test_token", base::Time::Max()); |
| } |
| |
| TEST_F(DriveUploaderTest, FetchAccessTokenFailure) { |
| auto account_info = test_env()->MakePrimaryAccountAvailable( |
| "test@example.com", signin::ConsentLevel::kSignin); |
| auto uploader = std::make_unique<FakeDriveUploader>( |
| "test_title", account_info, progress_callback_.Get(), profile_.get(), |
| &mock_content_reader_); |
| |
| EXPECT_CALL(progress_callback_, |
| Run(AllOf(Field(&SaveToDriveProgress::status, |
| SaveToDriveStatus::kUploadFailed), |
| Field(&SaveToDriveProgress::error_type, |
| SaveToDriveErrorType::kOauthError)))); |
| |
| uploader->Start(); |
| test_env()->WaitForAccessTokenRequestIfNecessaryAndRespondWithError( |
| GoogleServiceAuthError(GoogleServiceAuthError::CONNECTION_FAILED)); |
| } |
| |
| TEST_F(DriveUploaderTest, NoRefreshToken) { |
| AccountInfo account_info; |
| account_info.email = "test@example.com"; |
| account_info.account_id = CoreAccountId::FromGaiaId(GaiaId("12345")); |
| |
| auto uploader = std::make_unique<FakeDriveUploader>( |
| "test_title", account_info, progress_callback_.Get(), profile_.get(), |
| &mock_content_reader_); |
| |
| EXPECT_CALL(progress_callback_, |
| Run(AllOf(Field(&SaveToDriveProgress::status, |
| SaveToDriveStatus::kUploadFailed), |
| Field(&SaveToDriveProgress::error_type, |
| SaveToDriveErrorType::kOauthError)))); |
| |
| uploader->Start(); |
| } |
| |
| TEST_F(DriveUploaderTest, OnRefreshTokenRemovedForAccount) { |
| auto account_info = test_env()->MakePrimaryAccountAvailable( |
| "test@example.com", signin::ConsentLevel::kSignin); |
| auto uploader = std::make_unique<FakeDriveUploader>( |
| "test_title", account_info, progress_callback_.Get(), profile_.get(), |
| &mock_content_reader_); |
| |
| // The `progress_callback_` is expected to be run twice: once during the |
| // initial `Start()` call as refresh token for the primary account is not |
| // configured, and again when the refresh token is removed for the account. |
| EXPECT_CALL(progress_callback_, |
| Run(AllOf(Field(&SaveToDriveProgress::status, |
| SaveToDriveStatus::kUploadFailed), |
| Field(&SaveToDriveProgress::error_type, |
| SaveToDriveErrorType::kOauthError)))) |
| .Times(2); |
| |
| uploader->Start(); |
| test_env()->RemoveRefreshTokenForAccount(account_info.account_id); |
| } |
| |
| TEST_F(DriveUploaderTest, NotifyUploadInProgressIsRateLimited) { |
| auto account_info = test_env()->MakePrimaryAccountAvailable( |
| "test@example.com", signin::ConsentLevel::kSignin); |
| auto uploader = std::make_unique<FakeDriveUploader>( |
| "test_title", account_info, progress_callback_.Get(), profile_.get(), |
| &mock_content_reader_); |
| const size_t kTotalBytes = 1000; |
| |
| // First call should trigger a callback. |
| EXPECT_CALL( |
| progress_callback_, |
| Run(AllOf(Field(&SaveToDriveProgress::status, |
| SaveToDriveStatus::kUploadInProgress), |
| Field(&SaveToDriveProgress::uploaded_bytes, 100u), |
| Field(&SaveToDriveProgress::file_size_bytes, kTotalBytes)))) |
| .Times(1); |
| uploader->NotifyUploadInProgress(100, kTotalBytes); |
| |
| // Subsequent calls within the interval should be ignored. |
| EXPECT_CALL(progress_callback_, Run(_)).Times(0); |
| uploader->NotifyUploadInProgress(200, kTotalBytes); |
| |
| task_environment_.FastForwardBy(base::Milliseconds(499)); |
| uploader->NotifyUploadInProgress(300, kTotalBytes); |
| |
| // After the interval, the next call should trigger a callback. |
| task_environment_.FastForwardBy(base::Milliseconds(1)); |
| EXPECT_CALL( |
| progress_callback_, |
| Run(AllOf(Field(&SaveToDriveProgress::status, |
| SaveToDriveStatus::kUploadInProgress), |
| Field(&SaveToDriveProgress::uploaded_bytes, 400u), |
| Field(&SaveToDriveProgress::file_size_bytes, kTotalBytes)))) |
| .Times(1); |
| uploader->NotifyUploadInProgress(400, kTotalBytes); |
| } |
| |
| class FetchParentFolderTest : public DriveUploaderTest { |
| public: |
| FetchParentFolderTest() = default; |
| FetchParentFolderTest(const FetchParentFolderTest&) = delete; |
| FetchParentFolderTest& operator=(const FetchParentFolderTest&) = delete; |
| ~FetchParentFolderTest() override = default; |
| |
| void VerifyParentFolderResponse(std::string_view response_header, |
| std::string_view response_body, |
| std::optional<DriveUploader::Item> expected) { |
| auto account_info = test_env()->MakePrimaryAccountAvailable( |
| "test@example.com", signin::ConsentLevel::kSignin); |
| const content::URLLoaderInterceptor interceptor(base::BindLambdaForTesting( |
| [&](content::URLLoaderInterceptor::RequestParams* params) { |
| if (params->url_request.url.spec() == kParentFolderUrl) { |
| content::URLLoaderInterceptor::WriteResponse( |
| response_header, response_body, params->client.get()); |
| return true; |
| } |
| return false; |
| })); |
| |
| EXPECT_CALL(progress_callback_, Run(Field(&SaveToDriveProgress::status, |
| SaveToDriveStatus::kFetchOauth))); |
| TestFuture<void> future; |
| FakeDriveUploader uploader("test_title", std::move(account_info), |
| progress_callback_.Get(), profile_.get(), |
| &mock_content_reader_); |
| |
| if (expected.has_value()) { |
| EXPECT_CALL(progress_callback_, |
| Run(Field(&SaveToDriveProgress::status, |
| SaveToDriveStatus::kFetchParentFolder))) |
| .WillOnce(base::test::RunOnceClosure(future.GetCallback())); |
| EXPECT_CALL(uploader, UploadFile()); |
| } else { |
| EXPECT_CALL( |
| progress_callback_, |
| Run(AllOf(Field(&SaveToDriveProgress::status, |
| SaveToDriveStatus::kUploadFailed), |
| Field(&SaveToDriveProgress::error_type, |
| SaveToDriveErrorType::kParentFolderSelectionFailed)))) |
| .WillOnce(base::test::RunOnceClosure(future.GetCallback())); |
| } |
| |
| uploader.Start(); |
| |
| test_env()->WaitForAccessTokenRequestIfNecessaryAndRespondWithToken( |
| "access_token", base::Time::Now() + base::Hours(1)); |
| |
| EXPECT_TRUE(future.Wait()); |
| |
| // Verify that the parent folder is set correctly. |
| const std::optional<DriveUploader::Item>& parent_folder = |
| uploader.parent_folder(); |
| EXPECT_EQ(parent_folder.has_value(), expected.has_value()); |
| |
| if (expected.has_value()) { |
| EXPECT_EQ(parent_folder->id, expected->id); |
| EXPECT_EQ(parent_folder->name, expected->name); |
| } |
| } |
| }; |
| |
| TEST_F(FetchParentFolderTest, Success) { |
| base::Value::Dict response; |
| response.Set("id", kTestFileId); |
| response.Set("name", kTestFolderName); |
| std::optional<std::string> response_string = base::WriteJson(response); |
| ASSERT_TRUE(response_string.has_value()); |
| VerifyParentFolderResponse( |
| kSuccessFulResponseHeader, *response_string, |
| DriveUploader::Item{.id = std::string(kTestFileId), |
| .name = std::string(kTestFolderName)}); |
| } |
| |
| TEST_F(FetchParentFolderTest, EmptyResponse) { |
| VerifyParentFolderResponse(kSuccessFulResponseHeader, "", std::nullopt); |
| } |
| |
| TEST_F(FetchParentFolderTest, InvalidJson) { |
| VerifyParentFolderResponse(kSuccessFulResponseHeader, "{\"id=", std::nullopt); |
| } |
| |
| TEST_F(FetchParentFolderTest, InternalError) { |
| VerifyParentFolderResponse(kErrorResponseHeader, "{}", std::nullopt); |
| } |
| |
| TEST_F(FetchParentFolderTest, MissingId) { |
| base::Value::Dict response; |
| response.Set("name", kTestFolderName); |
| std::optional<std::string> response_string = base::WriteJson(response); |
| ASSERT_TRUE(response_string.has_value()); |
| VerifyParentFolderResponse(kSuccessFulResponseHeader, *response_string, |
| std::nullopt); |
| } |
| |
| TEST_F(FetchParentFolderTest, MissingName) { |
| base::Value::Dict response; |
| response.Set("id", kTestFileId); |
| std::optional<std::string> response_string = base::WriteJson(response); |
| ASSERT_TRUE(response_string.has_value()); |
| VerifyParentFolderResponse(kSuccessFulResponseHeader, *response_string, |
| std::nullopt); |
| } |
| |
| } // namespace |
| |
| } // namespace save_to_drive |