blob: 5f3dad268e2454dfebf071f36e55b345f13756a9 [file] [log] [blame]
// 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/resumable_drive_uploader.h"
#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#include "base/containers/span.h"
#include "base/functional/bind.h"
#include "base/json/json_writer.h"
#include "base/strings/string_number_conversions.h"
#include "base/test/bind.h"
#include "base/test/mock_callback.h"
#include "chrome/browser/save_to_drive/content_reader.h"
#include "chrome/browser/save_to_drive/drive_uploader.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/account_info.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 "mojo/public/cpp/base/big_buffer.h"
#include "net/http/http_request_headers.h"
#include "net/http/http_status_code.h"
#include "services/network/public/cpp/resource_request.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
namespace save_to_drive {
namespace {
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;
using testing::Return;
constexpr char kInitiationUrl[] =
"https://www.googleapis.com/upload/drive/v3beta/"
"files";
constexpr char kTestUploadUrl[] = "https://example.com/upload";
constexpr std::string_view kTestContent = "test_content";
constexpr std::string_view kTestFileId = "test_file_id";
constexpr std::string_view kTestTitle = "test_title";
constexpr std::string_view kSuccessFulResponseHeader =
"HTTP/1.1 200 OK\nContent-Type: application/json\n\n";
constexpr std::string_view kSuccessFulFinalResponseHeader =
"HTTP/1.1 200 OK\nContent-Type: application/json\nX-Goog-Upload-Status: "
"final\n\n";
constexpr std::string_view kSuccessFulActiveResponseHeader =
"HTTP/1.1 200 OK\nContent-Type: application/json\nX-Goog-Upload-Status: "
"active\n\n";
constexpr std::string_view kUnauthorizedErrorResponse =
"HTTP/1.1 401 Unauthorized\nContent-Type: application/json\n\n";
constexpr std::string_view kForbiddenErrorResponse =
"HTTP/1.1 403 Forbidden\nContent-Type: application/json\n\n";
constexpr std::string_view kNotFoundErrorResponse =
"HTTP/1.1 404 Not Found\nContent-Type: application/json\n\n";
constexpr std::string_view kInternalServerErrorResponse =
"HTTP/1.1 500 Internal Server Error\nContent-Type: application/json\n\n";
AccountInfo CreateAccountInfo() {
AccountInfo account_info;
account_info.email = "test@example.com";
account_info.account_id = CoreAccountId::FromGaiaId(GaiaId("12345"));
return account_info;
}
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));
};
// A test class for ResumableDriveUploader that allows for setting protected
// members.
class FakeResumableDriveUploader : public ResumableDriveUploader {
public:
FakeResumableDriveUploader(std::string title,
AccountInfo account_info,
ProgressCallback progress_callback,
Profile* profile,
ContentReader* content_reader)
: ResumableDriveUploader(std::move(title),
std::move(account_info),
std::move(progress_callback),
profile,
content_reader) {}
~FakeResumableDriveUploader() override = default;
void set_parent_folder(std::optional<Item> parent_folder) {
parent_folder_ = std::move(parent_folder);
}
};
class ResumableDriveUploaderTest : public testing::Test {
public:
void SetUp() override {
profile_ = IdentityTestEnvironmentProfileAdaptor::
CreateProfileForIdentityTestEnvironment();
adaptor_ =
std::make_unique<IdentityTestEnvironmentProfileAdaptor>(profile_.get());
uploader_ = std::make_unique<FakeResumableDriveUploader>(
std::string(kTestTitle), CreateAccountInfo(), progress_callback_.Get(),
profile_.get(), &mock_content_reader_);
uploader_->set_oauth_headers_for_testing(
{net::HttpRequestHeaders::kAuthorization, "Bearer test_token"});
uploader_->set_parent_folder(
DriveUploader::Item{.id = "test_folder_id", .name = "test_folder"});
}
protected:
void MockSuccessfulInitiation(
content::URLLoaderInterceptor::RequestParams* params) {
const auto& request = params->url_request;
EXPECT_EQ(request.method, "POST");
ASSERT_TRUE(request.request_body);
auto& elements = *request.request_body->elements();
ASSERT_EQ(elements.size(), 1u);
EXPECT_EQ(elements[0].As<network::DataElementBytes>().AsStringView(),
*base::WriteJson(base::Value::Dict()
.Set("name", kTestTitle)
.Set("parents", base::Value::List().Append(
"test_folder_id"))));
std::optional<std::string> content_length =
request.headers.GetHeader("X-Goog-Upload-Header-Content-Length");
ASSERT_TRUE(content_length);
EXPECT_EQ(*content_length, base::NumberToString(kTestContent.size()));
std::optional<std::string> upload_protocol =
request.headers.GetHeader("X-Goog-Upload-Protocol");
ASSERT_TRUE(upload_protocol);
EXPECT_EQ(*upload_protocol, "resumable");
std::optional<std::string> upload_command =
request.headers.GetHeader("X-Goog-Upload-Command");
ASSERT_TRUE(upload_command);
EXPECT_EQ(*upload_command, "start");
std::optional<std::string> upload_content_type =
request.headers.GetHeader("X-Goog-Upload-Header-Content-Type");
ASSERT_TRUE(upload_content_type);
EXPECT_EQ(*upload_content_type, "application/octet-stream");
content::URLLoaderInterceptor::WriteResponse(
base::StrCat({"HTTP/1.1 200 OK\nX-Goog-Upload-URL: ", kTestUploadUrl,
"\nX-Goog-Upload-Status: active\n\n"}),
"", params->client.get());
}
signin::IdentityTestEnvironment* test_env() {
return adaptor_->identity_test_env();
}
// Must be the first member.
content::BrowserTaskEnvironment task_environment_;
MockContentReader mock_content_reader_;
base::MockCallback<DriveUploader::ProgressCallback> progress_callback_;
std::unique_ptr<TestingProfile> profile_;
std::unique_ptr<IdentityTestEnvironmentProfileAdaptor> adaptor_;
std::unique_ptr<FakeResumableDriveUploader> uploader_;
};
TEST_F(ResumableDriveUploaderTest, UploadSuccess) {
EXPECT_CALL(mock_content_reader_, GetSize())
.WillRepeatedly(Return(kTestContent.size()));
EXPECT_CALL(mock_content_reader_, Read(0, kTestContent.size(), _))
.WillOnce([&](uint32_t, uint32_t, ContentReadCallback callback) {
std::move(callback).Run(
mojo_base::BigBuffer(base::as_byte_span(kTestContent)));
});
const content::URLLoaderInterceptor interceptor(base::BindLambdaForTesting(
[&](content::URLLoaderInterceptor::RequestParams* params) {
const auto& request = params->url_request;
if (request.url == GURL(kInitiationUrl)) {
MockSuccessfulInitiation(params);
return true;
}
if (request.url == GURL(kTestUploadUrl)) {
// Upload request.
EXPECT_EQ(request.method, "PUT");
if (!request.request_body) {
ADD_FAILURE() << "Request body is missing";
return false;
}
auto& elements = *request.request_body->elements();
if (elements.size() != 1u) {
ADD_FAILURE() << "Expected 1 element, got " << elements.size();
return false;
}
EXPECT_EQ(elements[0].As<network::DataElementBytes>().AsStringView(),
kTestContent);
content::URLLoaderInterceptor::WriteResponse(
kSuccessFulFinalResponseHeader,
*base::WriteJson(base::Value::Dict()
.Set("id", kTestFileId)
.Set("name", kTestTitle)),
params->client.get());
return true;
}
return false;
}));
EXPECT_CALL(
progress_callback_,
Run(AllOf(
Field(&SaveToDriveProgress::status,
SaveToDriveStatus::kUploadCompleted),
Field(&SaveToDriveProgress::error_type,
SaveToDriveErrorType::kNoError),
Field(&SaveToDriveProgress::drive_item_id, kTestFileId),
Field(&SaveToDriveProgress::file_size_bytes, kTestContent.size()),
Field(&SaveToDriveProgress::uploaded_bytes, kTestContent.size()),
Field(&SaveToDriveProgress::file_name, kTestTitle),
Field(&SaveToDriveProgress::parent_folder_name, "test_folder"))));
uploader_->UploadFile();
task_environment_.RunUntilIdle();
}
TEST_F(ResumableDriveUploaderTest, UploadSuccessInChunks) {
EXPECT_CALL(mock_content_reader_, GetSize())
.WillRepeatedly(Return(kTestContent.size()));
EXPECT_CALL(mock_content_reader_, Read(0, kTestContent.size(), _))
.WillOnce([&](uint32_t, uint32_t, ContentReadCallback callback) {
std::move(callback).Run(mojo_base::BigBuffer(
base::as_byte_span(kTestContent.substr(0, 4))));
});
EXPECT_CALL(mock_content_reader_, Read(4, kTestContent.size() - 4, _))
.WillOnce([&](uint32_t, uint32_t, ContentReadCallback callback) {
std::move(callback).Run(
mojo_base::BigBuffer(base::as_byte_span(kTestContent.substr(4))));
});
const content::URLLoaderInterceptor interceptor(base::BindLambdaForTesting(
[&](content::URLLoaderInterceptor::RequestParams* params) {
const auto& request = params->url_request;
if (request.url == GURL(kInitiationUrl)) {
MockSuccessfulInitiation(params);
return true;
}
if (request.url == GURL(kTestUploadUrl)) {
std::optional<std::string> offset =
request.headers.GetHeader("X-Goog-Upload-Offset");
if (!offset) {
ADD_FAILURE() << "X-Goog-Upload-Offset header is missing";
return false;
}
std::optional<std::string> upload_command =
request.headers.GetHeader("X-Goog-Upload-Command");
if (!upload_command) {
ADD_FAILURE() << "X-Goog-Upload-command header is missing";
return false;
}
if (offset == "0") {
EXPECT_EQ(*upload_command, "upload");
content::URLLoaderInterceptor::WriteResponse(
kSuccessFulActiveResponseHeader, "", params->client.get());
} else {
EXPECT_EQ(*upload_command, "upload, finalize");
content::URLLoaderInterceptor::WriteResponse(
kSuccessFulFinalResponseHeader,
*base::WriteJson(base::Value::Dict()
.Set("id", kTestFileId)
.Set("name", kTestTitle)),
params->client.get());
}
return true;
}
return false;
}));
EXPECT_CALL(progress_callback_,
Run(AllOf(Field(&SaveToDriveProgress::status,
SaveToDriveStatus::kUploadInProgress),
Field(&SaveToDriveProgress::error_type,
SaveToDriveErrorType::kNoError),
Field(&SaveToDriveProgress::uploaded_bytes, 4))));
EXPECT_CALL(
progress_callback_,
Run(AllOf(
Field(&SaveToDriveProgress::status,
SaveToDriveStatus::kUploadCompleted),
Field(&SaveToDriveProgress::error_type,
SaveToDriveErrorType::kNoError),
Field(&SaveToDriveProgress::drive_item_id, kTestFileId),
Field(&SaveToDriveProgress::file_size_bytes, kTestContent.size()),
Field(&SaveToDriveProgress::uploaded_bytes, kTestContent.size()),
Field(&SaveToDriveProgress::file_name, kTestTitle),
Field(&SaveToDriveProgress::parent_folder_name, "test_folder"))));
uploader_->UploadFile();
task_environment_.RunUntilIdle();
}
TEST_F(ResumableDriveUploaderTest, UploadFailsInitiationHttpError) {
EXPECT_CALL(mock_content_reader_, GetSize())
.WillRepeatedly(Return(kTestContent.size()));
const content::URLLoaderInterceptor interceptor(base::BindLambdaForTesting(
[&](content::URLLoaderInterceptor::RequestParams* params) {
content::URLLoaderInterceptor::WriteResponse(
kInternalServerErrorResponse, "{}", params->client.get());
return true;
}));
EXPECT_CALL(progress_callback_,
Run(AllOf(Field(&SaveToDriveProgress::status,
SaveToDriveStatus::kUploadFailed),
Field(&SaveToDriveProgress::error_type,
SaveToDriveErrorType::kUnknownError))));
uploader_->UploadFile();
task_environment_.RunUntilIdle();
}
TEST_F(ResumableDriveUploaderTest, UploadFailsInitiationNetworkError) {
EXPECT_CALL(mock_content_reader_, GetSize())
.WillRepeatedly(Return(kTestContent.size()));
EXPECT_CALL(progress_callback_,
Run(AllOf(Field(&SaveToDriveProgress::status,
SaveToDriveStatus::kUploadFailed),
Field(&SaveToDriveProgress::error_type,
SaveToDriveErrorType::kOffline))));
uploader_->UploadFile();
task_environment_.RunUntilIdle();
}
TEST_F(ResumableDriveUploaderTest, UploadFailsInitiationNoUploadUrlHeader) {
EXPECT_CALL(mock_content_reader_, GetSize())
.WillRepeatedly(Return(kTestContent.size()));
const content::URLLoaderInterceptor interceptor(base::BindLambdaForTesting(
[&](content::URLLoaderInterceptor::RequestParams* params) {
content::URLLoaderInterceptor::WriteResponse(kSuccessFulResponseHeader,
"", params->client.get());
return true;
}));
EXPECT_CALL(progress_callback_,
Run(AllOf(Field(&SaveToDriveProgress::status,
SaveToDriveStatus::kUploadFailed),
Field(&SaveToDriveProgress::error_type,
SaveToDriveErrorType::kUnknownError))));
uploader_->UploadFile();
task_environment_.RunUntilIdle();
}
TEST_F(ResumableDriveUploaderTest, UploadFailsContentReadError) {
EXPECT_CALL(mock_content_reader_, GetSize())
.WillRepeatedly(Return(kTestContent.size()));
EXPECT_CALL(mock_content_reader_, Read(_, _, _))
.WillOnce([&](uint32_t, uint32_t, ContentReadCallback callback) {
std::move(callback).Run(mojo_base::BigBuffer());
});
const content::URLLoaderInterceptor interceptor(base::BindLambdaForTesting(
[&](content::URLLoaderInterceptor::RequestParams* params) {
MockSuccessfulInitiation(params);
return true;
}));
EXPECT_CALL(progress_callback_,
Run(AllOf(Field(&SaveToDriveProgress::status,
SaveToDriveStatus::kUploadFailed),
Field(&SaveToDriveProgress::error_type,
SaveToDriveErrorType::kUnknownError))));
uploader_->UploadFile();
task_environment_.RunUntilIdle();
}
TEST_F(ResumableDriveUploaderTest, UploadFailsUploadHttpError) {
EXPECT_CALL(mock_content_reader_, GetSize())
.WillRepeatedly(Return(kTestContent.size()));
EXPECT_CALL(mock_content_reader_, Read(_, _, _))
.WillOnce([&](uint32_t, uint32_t, ContentReadCallback callback) {
std::move(callback).Run(
mojo_base::BigBuffer(base::as_byte_span(kTestContent)));
});
const content::URLLoaderInterceptor interceptor(base::BindLambdaForTesting(
[&](content::URLLoaderInterceptor::RequestParams* params) {
if (params->url_request.url == GURL(kInitiationUrl)) {
MockSuccessfulInitiation(params);
} else {
content::URLLoaderInterceptor::WriteResponse(
kInternalServerErrorResponse, "{}", params->client.get());
}
return true;
}));
EXPECT_CALL(progress_callback_,
Run(AllOf(Field(&SaveToDriveProgress::status,
SaveToDriveStatus::kUploadFailed),
Field(&SaveToDriveProgress::error_type,
SaveToDriveErrorType::kUnknownError))));
uploader_->UploadFile();
task_environment_.RunUntilIdle();
}
TEST_F(ResumableDriveUploaderTest, UploadFailsUnauthorized) {
EXPECT_CALL(mock_content_reader_, GetSize())
.WillRepeatedly(Return(kTestContent.size()));
EXPECT_CALL(mock_content_reader_, Read(_, _, _))
.WillOnce([&](uint32_t, uint32_t, ContentReadCallback callback) {
std::move(callback).Run(
mojo_base::BigBuffer(base::as_byte_span(kTestContent)));
});
const content::URLLoaderInterceptor interceptor(base::BindLambdaForTesting(
[&](content::URLLoaderInterceptor::RequestParams* params) {
if (params->url_request.url == GURL(kInitiationUrl)) {
MockSuccessfulInitiation(params);
} else {
content::URLLoaderInterceptor::WriteResponse(
kUnauthorizedErrorResponse, "{}", params->client.get());
}
return true;
}));
EXPECT_CALL(progress_callback_,
Run(AllOf(Field(&SaveToDriveProgress::status,
SaveToDriveStatus::kUploadFailed),
Field(&SaveToDriveProgress::error_type,
SaveToDriveErrorType::kOauthError))));
uploader_->UploadFile();
task_environment_.RunUntilIdle();
}
TEST_F(ResumableDriveUploaderTest, UploadFailsQuotaExceeded) {
EXPECT_CALL(mock_content_reader_, GetSize())
.WillRepeatedly(Return(kTestContent.size()));
EXPECT_CALL(mock_content_reader_, Read(_, _, _))
.WillOnce([&](uint32_t, uint32_t, ContentReadCallback callback) {
std::move(callback).Run(
mojo_base::BigBuffer(base::as_byte_span(kTestContent)));
});
const content::URLLoaderInterceptor interceptor(base::BindLambdaForTesting(
[&](content::URLLoaderInterceptor::RequestParams* params) {
if (params->url_request.url == GURL(kInitiationUrl)) {
MockSuccessfulInitiation(params);
} else {
content::URLLoaderInterceptor::WriteResponse(
kForbiddenErrorResponse,
R"({"error":{"errors":[{"reason":"quotaExceeded"}]}})",
params->client.get());
}
return true;
}));
EXPECT_CALL(progress_callback_,
Run(AllOf(Field(&SaveToDriveProgress::status,
SaveToDriveStatus::kUploadFailed),
Field(&SaveToDriveProgress::error_type,
SaveToDriveErrorType::kQuotaExceeded))));
uploader_->UploadFile();
task_environment_.RunUntilIdle();
}
TEST_F(ResumableDriveUploaderTest, UploadResumesAfterNotFound) {
int initiation_request_count = 0;
const content::URLLoaderInterceptor interceptor(base::BindLambdaForTesting(
[&](content::URLLoaderInterceptor::RequestParams* params) {
if (params->url_request.url == GURL(kInitiationUrl)) {
initiation_request_count++;
MockSuccessfulInitiation(params);
return true;
}
if (params->url_request.url != GURL(kTestUploadUrl)) {
return false;
}
if (initiation_request_count == 1) {
// First attempt fails with 404.
content::URLLoaderInterceptor::WriteResponse(
kNotFoundErrorResponse, "", params->client.get());
return true;
} else if (initiation_request_count == 2) {
// Second attempt succeeds.
content::URLLoaderInterceptor::WriteResponse(
kSuccessFulFinalResponseHeader,
*base::WriteJson(base::Value::Dict()
.Set("id", kTestFileId)
.Set("name", kTestTitle)),
params->client.get());
return true;
}
ADD_FAILURE() << "Unexpected request count: "
<< initiation_request_count;
return false;
}));
EXPECT_CALL(mock_content_reader_, GetSize())
.WillRepeatedly(Return(kTestContent.size()));
EXPECT_CALL(mock_content_reader_, Read(_, _, _))
.WillRepeatedly(([&](uint32_t, uint32_t, ContentReadCallback callback) {
std::move(callback).Run(
mojo_base::BigBuffer(base::as_byte_span(kTestContent)));
}));
EXPECT_CALL(progress_callback_,
Run(AllOf(Field(&SaveToDriveProgress::status,
SaveToDriveStatus::kUploadCompleted),
Field(&SaveToDriveProgress::error_type,
SaveToDriveErrorType::kNoError))));
uploader_->UploadFile();
task_environment_.RunUntilIdle();
EXPECT_EQ(initiation_request_count, 2);
}
TEST_F(ResumableDriveUploaderTest, UploadFailsNoOAuthToken) {
uploader_->set_oauth_headers_for_testing({});
EXPECT_CALL(progress_callback_,
Run(AllOf(Field(&SaveToDriveProgress::status,
SaveToDriveStatus::kUploadFailed),
Field(&SaveToDriveProgress::error_type,
SaveToDriveErrorType::kOauthError))));
uploader_->UploadFile();
task_environment_.RunUntilIdle();
}
TEST_F(ResumableDriveUploaderTest, UploadFailsNoParentFolder) {
uploader_->set_parent_folder(std::nullopt);
EXPECT_CALL(
progress_callback_,
Run(AllOf(
Field(&SaveToDriveProgress::status, SaveToDriveStatus::kUploadFailed),
Field(&SaveToDriveProgress::error_type,
SaveToDriveErrorType::kParentFolderSelectionFailed))));
uploader_->UploadFile();
task_environment_.RunUntilIdle();
}
} // namespace
} // namespace save_to_drive