Commit 02c0633e authored by Alice Gong's avatar Alice Gong Committed by Chromium LUCI CQ

Add OAuth2 flow for Box: WholeFileUpload (for files <= 50MB only)

BUG=1157672

Change-Id: Iffd50344615d56c2b2a4a843e8a22eab9f4306f7
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2621923Reviewed-by: default avatarMarc-André Decoste <mad@chromium.org>
Reviewed-by: default avatarDominique Fauteux-Chapleau <domfc@chromium.org>
Commit-Queue: Alice Gong <alicego@google.com>
Cr-Commit-Position: refs/heads/master@{#844964}
parent f4d3a840
...@@ -6,4 +6,6 @@ ...@@ -6,4 +6,6 @@
namespace enterprise_connectors { namespace enterprise_connectors {
const char kFileSystemBoxEndpointApi[] = "https://api.box.com/"; const char kFileSystemBoxEndpointApi[] = "https://api.box.com/";
const char kFileSystemBoxEndpointWholeFileUpload[] =
"https://upload.box.com/api/2.0/files/content";
} // namespace enterprise_connectors } // namespace enterprise_connectors
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
namespace enterprise_connectors { namespace enterprise_connectors {
extern const char kFileSystemBoxEndpointApi[]; extern const char kFileSystemBoxEndpointApi[];
extern const char kFileSystemBoxEndpointWholeFileUpload[];
} // namespace enterprise_connectors } // namespace enterprise_connectors
#endif // CHROME_BROWSER_ENTERPRISE_CONNECTORS_FILE_SYSTEM_BOX_API_CALL_ENDPOINTS_H_ #endif // CHROME_BROWSER_ENTERPRISE_CONNECTORS_FILE_SYSTEM_BOX_API_CALL_ENDPOINTS_H_
...@@ -4,16 +4,23 @@ ...@@ -4,16 +4,23 @@
#include "chrome/browser/enterprise/connectors/file_system/box_api_call_flow.h" #include "chrome/browser/enterprise/connectors/file_system/box_api_call_flow.h"
#include <string>
#include "base/files/file_util.h"
#include "base/json/json_writer.h" #include "base/json/json_writer.h"
#include "base/task/post_task.h"
#include "base/values.h" #include "base/values.h"
#include "chrome/browser/enterprise/connectors/file_system/box_api_call_endpoints.h" #include "chrome/browser/enterprise/connectors/file_system/box_api_call_endpoints.h"
#include "net/base/escape.h" #include "net/base/escape.h"
#include "net/base/mime_util.h"
#include "net/http/http_status_code.h" #include "net/http/http_status_code.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "services/network/public/mojom/url_response_head.mojom.h" #include "services/network/public/mojom/url_response_head.mojom.h"
namespace { namespace {
static const char kParentFolderId[] = "0"; // Create folder at root. // Create folder at root.
static const char kParentFolderId[] = "0";
std::string ExtractFolderId(const base::Value& entry) { std::string ExtractFolderId(const base::Value& entry) {
const base::Value* folder_id = entry.FindPath("id"); const base::Value* folder_id = entry.FindPath("id");
...@@ -34,10 +41,39 @@ std::string ExtractFolderId(const base::Value& entry) { ...@@ -34,10 +41,39 @@ std::string ExtractFolderId(const base::Value& entry) {
return id; return id;
} }
std::string GetMimeType(base::FilePath file_path) {
auto ext = file_path.FinalExtension();
if (ext.front() == '.') {
ext.erase(ext.begin());
}
DCHECK(file_path.FinalExtension() != FILE_PATH_LITERAL("crdownload"));
std::string file_type;
bool result = net::GetMimeTypeFromExtension(ext, &file_type);
DCHECK(result || file_type.empty());
return file_type;
}
base::Value CreateSingleFieldDict(const std::string& key,
const std::string& value) {
base::Value dict(base::Value::Type::DICTIONARY);
dict.SetStringKey(key, value);
return dict;
}
} // namespace } // namespace
namespace enterprise_connectors { namespace enterprise_connectors {
// File size limit according to https://developer.box.com/guides/uploads/:
// - Chucked upload APIs is only supported for file size >= 20 MB;
// - Whole file upload API is only supported for file size <= 50 MB.
const size_t BoxApiCallFlow::kChunkFileUploadMinSize =
20 * 1024 * 1024; // 20 MB
const size_t BoxApiCallFlow::kWholeFileUploadMaxSize =
50 * 1024 * 1024; // 50 MB
BoxApiCallFlow::BoxApiCallFlow() = default; BoxApiCallFlow::BoxApiCallFlow() = default;
BoxApiCallFlow::~BoxApiCallFlow() = default; BoxApiCallFlow::~BoxApiCallFlow() = default;
...@@ -208,8 +244,173 @@ void BoxCreateUpstreamFolderApiCallFlow::OnJsonParsed( ...@@ -208,8 +244,173 @@ void BoxCreateUpstreamFolderApiCallFlow::OnJsonParsed(
<< result.error->data(); << result.error->data();
} }
// TODO(1157641): store folder_id in profile pref to handle indexing latency. // TODO(1157641): store folder_id in profile pref to handle indexing latency.
std::move(callback_).Run(!folder_id.empty(), net::HTTP_OK, folder_id); std::move(callback_).Run(!folder_id.empty(), net::HTTP_CREATED, folder_id);
return; return;
} }
////////////////////////////////////////////////////////////////////////////////
// WholeFileUpload
////////////////////////////////////////////////////////////////////////////////
// BoxApiCallFlow interface.
// API reference:
// https://developer.box.com/reference/post-files-content/
BoxWholeFileUploadApiCallFlow::BoxWholeFileUploadApiCallFlow(
TaskCallback callback,
const std::string& folder_id,
const base::FilePath& target_file_name,
const base::FilePath& local_file_path)
: folder_id_(folder_id),
target_file_name_(target_file_name),
local_file_path_(local_file_path),
file_mime_type_(GetMimeType(target_file_name)),
multipart_boundary_(net::GenerateMimeMultipartBoundary()),
callback_(std::move(callback)) {}
BoxWholeFileUploadApiCallFlow::~BoxWholeFileUploadApiCallFlow() = default;
void BoxWholeFileUploadApiCallFlow::Start(
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
const std::string& access_token) {
// Ensure that file extension was valid and file type was obtained.
if (file_mime_type_.empty()) {
DLOG(ERROR) << "Couldn't obtain proper file type for " << target_file_name_;
std::move(callback_).Run(false, 0);
}
// Forward the arguments via PostReadFileTask() then OnFileRead() into
// OAuth2CallFlow::Start().
PostReadFileTask(url_loader_factory, access_token);
}
void BoxWholeFileUploadApiCallFlow::PostReadFileTask(
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
const std::string& access_token) {
auto read_file_task = base::BindOnce(&BoxWholeFileUploadApiCallFlow::ReadFile,
local_file_path_);
auto read_file_reply =
base::BindOnce(&BoxWholeFileUploadApiCallFlow::OnFileRead,
factory_.GetWeakPtr(), url_loader_factory, access_token);
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::TaskPriority::USER_VISIBLE, base::MayBlock()},
std::move(read_file_task), std::move(read_file_reply));
}
base::Optional<std::string> BoxWholeFileUploadApiCallFlow::ReadFile(
const base::FilePath& path) {
std::string content;
return base::ReadFileToStringWithMaxSize(path, &content,
kWholeFileUploadMaxSize)
? base::Optional<std::string>(std::move(content))
: base::nullopt;
}
void BoxWholeFileUploadApiCallFlow::OnFileRead(
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
const std::string& access_token,
base::Optional<std::string> file_read) {
if (!file_read) {
DLOG(ERROR) << "[BoxApiCallFlow] WholeFileUpload read file failed";
std::move(callback_).Run(false, 0); // TODO(1165972): error handling
return;
}
DCHECK_LE(file_read->size(), kWholeFileUploadMaxSize);
file_content_ = std::move(*file_read);
// Continue to the original call flow after file has been read.
OAuth2ApiCallFlow::Start(url_loader_factory, access_token);
}
GURL BoxWholeFileUploadApiCallFlow::CreateApiCallUrl() {
return GURL(kFileSystemBoxEndpointWholeFileUpload);
}
std::string BoxWholeFileUploadApiCallFlow::CreateApiCallBody() {
CHECK(!folder_id_.empty());
CHECK(!target_file_name_.empty());
CHECK(!file_mime_type_.empty());
CHECK(!multipart_boundary_.empty());
base::Value attr(base::Value::Type::DICTIONARY);
attr.SetStringKey("name", target_file_name_.MaybeAsASCII());
attr.SetKey("parent", CreateSingleFieldDict("id", folder_id_));
std::string attr_json;
base::JSONWriter::Write(attr, &attr_json);
std::string body;
net::AddMultipartValueForUpload("attributes", attr_json, multipart_boundary_,
"application/json", &body);
net::AddMultipartValueForUploadWithFileName(
"file", target_file_name_.MaybeAsASCII(), file_content_,
multipart_boundary_, file_mime_type_, &body);
net::AddMultipartFinalDelimiterForUpload(multipart_boundary_, &body);
return body;
}
// Header format for multipart/form-data reference:
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type
std::string BoxWholeFileUploadApiCallFlow::CreateApiCallBodyContentType() {
std::string content_type = "multipart/form-data; boundary=";
content_type.append(multipart_boundary_);
return content_type;
}
bool BoxWholeFileUploadApiCallFlow::IsExpectedSuccessCode(int code) const {
return code == net::HTTP_CREATED;
}
void BoxWholeFileUploadApiCallFlow::ProcessApiCallSuccess(
const network::mojom::URLResponseHead* head,
std::unique_ptr<std::string> body) {
if (!base::PathExists(local_file_path_)) {
// If the file is deleted by some other thread, how can we be sure what we
// read and uploaded was correct?! So report as error. Otherwise, it is
// considered successful to
// attempt to delete a file that does not exist by base::DeleteFile().
DLOG(ERROR) << "[BoxApiCallFlow] Whole File Upload: temporary local file "
"no longer exists!";
OnFileDeleted(false);
return;
}
PostDeleteFileTask();
}
void BoxWholeFileUploadApiCallFlow::PostDeleteFileTask() {
auto delete_file_task = base::BindOnce(&base::DeleteFile, local_file_path_);
auto delete_file_reply = base::BindOnce(
&BoxWholeFileUploadApiCallFlow::OnFileDeleted, factory_.GetWeakPtr());
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::TaskPriority::USER_VISIBLE, base::MayBlock()},
std::move(delete_file_task), std::move(delete_file_reply));
}
void BoxWholeFileUploadApiCallFlow::OnFileDeleted(bool success) {
if (!success) {
DLOG(ERROR) << "[BoxApiCallFlow] WholeFileUpload failed to delete "
"temporary local file "
<< local_file_path_;
}
std::move(callback_).Run(success, net::HTTP_CREATED);
}
void BoxWholeFileUploadApiCallFlow::ProcessApiCallFailure(
int net_error,
const network::mojom::URLResponseHead* head,
std::unique_ptr<std::string> body) {
auto response_code = head->headers->response_code();
DLOG(ERROR) << "[BoxApiCallFlow] WholeFileUpload failed. Error code "
<< response_code << " header: " << head->headers->raw_headers();
if (!body->empty()) {
DLOG(ERROR) << "Body: " << *body;
}
// TODO(1165972): decide whether to queue up the file to retry later, or also
// delete like in ProcessApiCallSuccess()
std::move(callback_).Run(false, response_code);
}
} // namespace enterprise_connectors } // namespace enterprise_connectors
...@@ -26,6 +26,11 @@ class BoxApiCallFlow : public OAuth2ApiCallFlow { ...@@ -26,6 +26,11 @@ class BoxApiCallFlow : public OAuth2ApiCallFlow {
std::string CreateApiCallBodyContentType() override; std::string CreateApiCallBodyContentType() override;
net::PartialNetworkTrafficAnnotationTag GetNetworkTrafficAnnotationTag() net::PartialNetworkTrafficAnnotationTag GetNetworkTrafficAnnotationTag()
override; override;
// Used by BoxApiCallFlow inherited classes and FileSystemDownloadController
// to determine whether to use WholeFileUpload or ChunkedFileUpload
static const size_t kChunkFileUploadMinSize;
static const size_t kWholeFileUploadMaxSize;
}; };
// Helper for finding the downloads folder in box. // Helper for finding the downloads folder in box.
...@@ -80,6 +85,74 @@ class BoxCreateUpstreamFolderApiCallFlow : public BoxApiCallFlow { ...@@ -80,6 +85,74 @@ class BoxCreateUpstreamFolderApiCallFlow : public BoxApiCallFlow {
base::WeakPtrFactory<BoxCreateUpstreamFolderApiCallFlow> weak_factory_{this}; base::WeakPtrFactory<BoxCreateUpstreamFolderApiCallFlow> weak_factory_{this};
}; };
// Helper for uploading a small (<= kWholeFileUploadMaxSize) file to upstream
// downloads folder in box.
class BoxWholeFileUploadApiCallFlow : public BoxApiCallFlow {
public:
using TaskCallback = base::OnceCallback<void(bool, int)>;
BoxWholeFileUploadApiCallFlow(TaskCallback callback,
const std::string& folder_id,
const base::FilePath& target_file_name,
const base::FilePath& local_file_path);
~BoxWholeFileUploadApiCallFlow() override;
// Overrides OAuth2ApiCallFlow::Start() to first read local file content
// before kicking off OAuth2ApiCallFlow::Start().
void Start(scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
const std::string& access_token) override;
protected:
// BoxApiCallFlow interface.
GURL CreateApiCallUrl() override;
std::string CreateApiCallBody() override;
std::string CreateApiCallBodyContentType() override;
bool IsExpectedSuccessCode(int code) const override;
void ProcessApiCallSuccess(const network::mojom::URLResponseHead* head,
std::unique_ptr<std::string> body) override;
void ProcessApiCallFailure(int net_error,
const network::mojom::URLResponseHead* head,
std::unique_ptr<std::string> body) override;
private:
// Post a task to ThreadPool to read the local file, forward the
// parameters from Start() into OnFileRead(), which is the callback that then
// kicks off OAuth2CallFlow::Start() after file content is read.
void PostReadFileTask(
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
const std::string& access_token);
// Post a task to ThreadPool to delete the local file, after calls to
// Box's whole file upload API, with callback OnFileDeleted(), which reports
// success back to original thread via callback_.
void PostDeleteFileTask();
// Helper functions to read and delete the local file.
// Task posted to ThreadPool to read the local file. Return type is
// base::Optional in case file is read successfully but the file content is
// really empty.
static base::Optional<std::string> ReadFile(const base::FilePath& path);
// Callback attached in PostReadFileTask(). Take in read file content and
// kick off OAuth2CallFlow::Start().
void OnFileRead(
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
const std::string& access_token,
base::Optional<std::string> content);
// Callback attached in PostDeleteFileTask(). Report success back to original
// thread via callback_.
void OnFileDeleted(bool result);
const std::string folder_id_;
const base::FilePath target_file_name_;
const base::FilePath local_file_path_;
const std::string file_mime_type_;
const std::string multipart_boundary_;
std::string file_content_;
// Callback from the controller to report success.
TaskCallback callback_;
base::WeakPtrFactory<BoxWholeFileUploadApiCallFlow> factory_{this};
};
} // namespace enterprise_connectors } // namespace enterprise_connectors
#endif // CHROME_BROWSER_ENTERPRISE_CONNECTORS_FILE_SYSTEM_BOX_API_CALL_FLOW_H_ #endif // CHROME_BROWSER_ENTERPRISE_CONNECTORS_FILE_SYSTEM_BOX_API_CALL_FLOW_H_
...@@ -9,6 +9,8 @@ ...@@ -9,6 +9,8 @@
#include <memory> #include <memory>
#include "base/bind.h" #include "base/bind.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/json/json_writer.h" #include "base/json/json_writer.h"
#include "base/run_loop.h" #include "base/run_loop.h"
#include "base/test/task_environment.h" #include "base/test/task_environment.h"
...@@ -17,6 +19,7 @@ ...@@ -17,6 +19,7 @@
#include "services/data_decoder/public/cpp/test_support/in_process_data_decoder.h" #include "services/data_decoder/public/cpp/test_support/in_process_data_decoder.h"
#include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h" #include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h"
#include "services/network/public/mojom/url_response_head.mojom.h" #include "services/network/public/mojom/url_response_head.mojom.h"
#include "services/network/test/test_url_loader_factory.h"
#include "services/network/test/test_utils.h" #include "services/network/test/test_utils.h"
#include "testing/gmock/include/gmock/gmock.h" #include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h" #include "testing/gtest/include/gtest/gtest.h"
...@@ -26,10 +29,24 @@ namespace enterprise_connectors { ...@@ -26,10 +29,24 @@ namespace enterprise_connectors {
template <typename ApiCallMiniClass> template <typename ApiCallMiniClass>
class BoxApiCallFlowTest : public testing::Test { class BoxApiCallFlowTest : public testing::Test {
protected: protected:
base::test::SingleThreadTaskEnvironment task_environment_;
std::unique_ptr<ApiCallMiniClass> flow_; std::unique_ptr<ApiCallMiniClass> flow_;
bool processed_success_ = false; bool processed_success_ = false;
int response_code_ = -1;
};
template <typename ApiCallMiniClass>
class BoxFolderApiCallFlowTest : public BoxApiCallFlowTest<ApiCallMiniClass> {
protected:
void OnResponse(bool success,
int response_code,
const std::string& folder_id) {
BoxApiCallFlowTest<ApiCallMiniClass>::processed_success_ = success;
BoxApiCallFlowTest<ApiCallMiniClass>::response_code_ = response_code;
processed_folder_id_ = folder_id;
}
base::test::SingleThreadTaskEnvironment task_environment_;
std::string processed_folder_id_ = "default id"; std::string processed_folder_id_ = "default id";
}; };
...@@ -44,13 +61,10 @@ class BoxFindUpstreamFolderApiCallFlowForTest ...@@ -44,13 +61,10 @@ class BoxFindUpstreamFolderApiCallFlowForTest
using BoxFindUpstreamFolderApiCallFlow::CreateApiCallUrl; using BoxFindUpstreamFolderApiCallFlow::CreateApiCallUrl;
using BoxFindUpstreamFolderApiCallFlow::ProcessApiCallFailure; using BoxFindUpstreamFolderApiCallFlow::ProcessApiCallFailure;
using BoxFindUpstreamFolderApiCallFlow::ProcessApiCallSuccess; using BoxFindUpstreamFolderApiCallFlow::ProcessApiCallSuccess;
private:
DISALLOW_COPY_AND_ASSIGN(BoxFindUpstreamFolderApiCallFlowForTest);
}; };
class BoxFindUpstreamFolderApiCallFlowTest class BoxFindUpstreamFolderApiCallFlowTest
: public BoxApiCallFlowTest<BoxFindUpstreamFolderApiCallFlowForTest> { : public BoxFolderApiCallFlowTest<BoxFindUpstreamFolderApiCallFlowForTest> {
protected: protected:
void SetUp() override { void SetUp() override {
flow_ = std::make_unique<BoxFindUpstreamFolderApiCallFlowForTest>( flow_ = std::make_unique<BoxFindUpstreamFolderApiCallFlowForTest>(
...@@ -58,13 +72,6 @@ class BoxFindUpstreamFolderApiCallFlowTest ...@@ -58,13 +72,6 @@ class BoxFindUpstreamFolderApiCallFlowTest
factory_.GetWeakPtr())); factory_.GetWeakPtr()));
} }
void OnResponse(bool success,
int response_code,
const std::string& folder_id) {
processed_success_ = success;
processed_folder_id_ = folder_id;
}
base::WeakPtrFactory<BoxFindUpstreamFolderApiCallFlowTest> factory_{this}; base::WeakPtrFactory<BoxFindUpstreamFolderApiCallFlowTest> factory_{this};
}; };
...@@ -119,11 +126,52 @@ TEST_F(BoxFindUpstreamFolderApiCallFlowTest_ProcessApiCallSuccess, ...@@ -119,11 +126,52 @@ TEST_F(BoxFindUpstreamFolderApiCallFlowTest_ProcessApiCallSuccess,
std::make_unique<std::string>(body)); std::make_unique<std::string>(body));
base::RunLoop().RunUntilIdle(); base::RunLoop().RunUntilIdle();
ASSERT_TRUE(processed_success_) << body; ASSERT_TRUE(processed_success_) << body;
ASSERT_EQ(response_code_, net::HTTP_OK);
ASSERT_EQ(processed_folder_id_, "");
}
TEST_F(BoxFindUpstreamFolderApiCallFlowTest_ProcessApiCallSuccess, NoFolderId) {
std::string body(R"({
"entries": [
{
"etag": 1,
"type": "folder",
"sequence_id": 3,
"name": "ChromeDownloads"
}
]
})");
flow_->ProcessApiCallSuccess(head_.get(),
std::make_unique<std::string>(body));
base::RunLoop().RunUntilIdle();
ASSERT_FALSE(processed_success_);
ASSERT_EQ(response_code_, net::HTTP_OK);
ASSERT_EQ(processed_folder_id_, "");
}
TEST_F(BoxFindUpstreamFolderApiCallFlowTest_ProcessApiCallSuccess,
EmptyFolderId) {
std::string body(R"({
"entries": [
{
"id": ,
"etag": 1,
"type": "folder",
"sequence_id": 3,
"name": "ChromeDownloads"
}
]
})");
flow_->ProcessApiCallSuccess(head_.get(),
std::make_unique<std::string>(body));
base::RunLoop().RunUntilIdle();
ASSERT_FALSE(processed_success_);
ASSERT_EQ(response_code_, net::HTTP_OK);
ASSERT_EQ(processed_folder_id_, ""); ASSERT_EQ(processed_folder_id_, "");
} }
TEST_F(BoxFindUpstreamFolderApiCallFlowTest_ProcessApiCallSuccess, TEST_F(BoxFindUpstreamFolderApiCallFlowTest_ProcessApiCallSuccess,
ValidEntries) { IntegerFolderId) {
std::string body(R"({ std::string body(R"({
"entries": [ "entries": [
{ {
...@@ -139,9 +187,52 @@ TEST_F(BoxFindUpstreamFolderApiCallFlowTest_ProcessApiCallSuccess, ...@@ -139,9 +187,52 @@ TEST_F(BoxFindUpstreamFolderApiCallFlowTest_ProcessApiCallSuccess,
std::make_unique<std::string>(body)); std::make_unique<std::string>(body));
base::RunLoop().RunUntilIdle(); base::RunLoop().RunUntilIdle();
ASSERT_TRUE(processed_success_); ASSERT_TRUE(processed_success_);
ASSERT_EQ(response_code_, net::HTTP_OK);
ASSERT_EQ(processed_folder_id_, "12345"); ASSERT_EQ(processed_folder_id_, "12345");
} }
TEST_F(BoxFindUpstreamFolderApiCallFlowTest_ProcessApiCallSuccess,
StringFolderId) {
std::string body(R"({
"entries": [
{
"id": "12345",
"etag": 1,
"type": "folder",
"sequence_id": 3,
"name": "ChromeDownloads"
}
]
})");
flow_->ProcessApiCallSuccess(head_.get(),
std::make_unique<std::string>(body));
base::RunLoop().RunUntilIdle();
ASSERT_TRUE(processed_success_);
ASSERT_EQ(response_code_, net::HTTP_OK);
ASSERT_EQ(processed_folder_id_, "12345");
}
TEST_F(BoxFindUpstreamFolderApiCallFlowTest_ProcessApiCallSuccess,
FloatFolderId) {
std::string body(R"({
"entries": [
{
"id": 123.5,
"etag": 1,
"type": "folder",
"sequence_id": 3,
"name": "ChromeDownloads"
}
]
})");
flow_->ProcessApiCallSuccess(head_.get(),
std::make_unique<std::string>(body));
base::RunLoop().RunUntilIdle();
ASSERT_FALSE(processed_success_);
ASSERT_EQ(response_code_, net::HTTP_OK);
ASSERT_EQ(processed_folder_id_, "");
}
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
// CreateUpstreamFolder // CreateUpstreamFolder
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
...@@ -155,13 +246,11 @@ class BoxCreateUpstreamFolderApiCallFlowForTest ...@@ -155,13 +246,11 @@ class BoxCreateUpstreamFolderApiCallFlowForTest
using BoxCreateUpstreamFolderApiCallFlow::IsExpectedSuccessCode; using BoxCreateUpstreamFolderApiCallFlow::IsExpectedSuccessCode;
using BoxCreateUpstreamFolderApiCallFlow::ProcessApiCallFailure; using BoxCreateUpstreamFolderApiCallFlow::ProcessApiCallFailure;
using BoxCreateUpstreamFolderApiCallFlow::ProcessApiCallSuccess; using BoxCreateUpstreamFolderApiCallFlow::ProcessApiCallSuccess;
private:
DISALLOW_COPY_AND_ASSIGN(BoxCreateUpstreamFolderApiCallFlowForTest);
}; };
class BoxCreateUpstreamFolderApiCallFlowTest class BoxCreateUpstreamFolderApiCallFlowTest
: public BoxApiCallFlowTest<BoxCreateUpstreamFolderApiCallFlowForTest> { : public BoxFolderApiCallFlowTest<
BoxCreateUpstreamFolderApiCallFlowForTest> {
protected: protected:
void SetUp() override { void SetUp() override {
flow_ = std::make_unique<BoxCreateUpstreamFolderApiCallFlowForTest>( flow_ = std::make_unique<BoxCreateUpstreamFolderApiCallFlowForTest>(
...@@ -169,13 +258,6 @@ class BoxCreateUpstreamFolderApiCallFlowTest ...@@ -169,13 +258,6 @@ class BoxCreateUpstreamFolderApiCallFlowTest
factory_.GetWeakPtr())); factory_.GetWeakPtr()));
} }
void OnResponse(bool success,
int response_code,
const std::string& folder_id) {
processed_success_ = success;
processed_folder_id_ = folder_id;
}
base::WeakPtrFactory<BoxCreateUpstreamFolderApiCallFlowTest> factory_{this}; base::WeakPtrFactory<BoxCreateUpstreamFolderApiCallFlowTest> factory_{this};
}; };
...@@ -258,7 +340,205 @@ TEST_F(BoxCreateUpstreamFolderApiCallFlowTest_ProcessApiCallSuccess, Normal) { ...@@ -258,7 +340,205 @@ TEST_F(BoxCreateUpstreamFolderApiCallFlowTest_ProcessApiCallSuccess, Normal) {
std::make_unique<std::string>(body)); std::make_unique<std::string>(body));
base::RunLoop().RunUntilIdle(); base::RunLoop().RunUntilIdle();
ASSERT_TRUE(processed_success_); ASSERT_TRUE(processed_success_);
ASSERT_EQ(response_code_, net::HTTP_CREATED);
ASSERT_EQ(processed_folder_id_, "12345"); ASSERT_EQ(processed_folder_id_, "12345");
} }
////////////////////////////////////////////////////////////////////////////////
// WholeFileUpload
////////////////////////////////////////////////////////////////////////////////
class BoxWholeFileUploadApiCallFlowForTest
: public BoxWholeFileUploadApiCallFlow {
public:
using BoxWholeFileUploadApiCallFlow::BoxWholeFileUploadApiCallFlow;
using BoxWholeFileUploadApiCallFlow::CreateApiCallBody;
using BoxWholeFileUploadApiCallFlow::CreateApiCallBodyContentType;
using BoxWholeFileUploadApiCallFlow::CreateApiCallUrl;
using BoxWholeFileUploadApiCallFlow::IsExpectedSuccessCode;
using BoxWholeFileUploadApiCallFlow::ProcessApiCallFailure;
using BoxWholeFileUploadApiCallFlow::ProcessApiCallSuccess;
};
class BoxWholeFileUploadApiCallFlowTest
: public BoxApiCallFlowTest<BoxWholeFileUploadApiCallFlowForTest> {
protected:
void SetUp() override {
if (!temp_dir_.CreateUniqueTempDir()) {
FAIL() << "Failed to create temporary directory for testing";
}
file_path_ = temp_dir_.GetPath().Append(file_name_);
flow_ = std::make_unique<BoxWholeFileUploadApiCallFlowForTest>(
base::BindOnce(&BoxWholeFileUploadApiCallFlowTest::OnResponse,
factory_.GetWeakPtr()),
folder_id_, file_name_, file_path_);
}
void OnResponse(bool success, int response_code) {
processed_success_ = success;
response_code_ = response_code;
if (quit_closure_)
std::move(quit_closure_).Run();
}
std::string folder_id_{"13579"};
const base::FilePath file_name_{
FILE_PATH_LITERAL("box_whole_file_upload_test.txt")};
base::FilePath file_path_;
base::ScopedTempDir temp_dir_;
base::test::TaskEnvironment task_environment_;
base::OnceClosure quit_closure_;
base::WeakPtrFactory<BoxWholeFileUploadApiCallFlowTest> factory_{this};
};
TEST_F(BoxWholeFileUploadApiCallFlowTest, CreateApiCallUrl) {
GURL url("https://upload.box.com/api/2.0/files/content");
ASSERT_EQ(flow_->CreateApiCallUrl(), url);
}
TEST_F(BoxWholeFileUploadApiCallFlowTest, CreateApiCallBodyAndContentType) {
std::string content_type = flow_->CreateApiCallBodyContentType();
std::string expected_type("multipart/form-data; boundary=");
ASSERT_EQ(content_type.substr(0, expected_type.size()), expected_type);
// Body format for multipart/form-data reference:
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type
// Request body fields reference:
// https://developer.box.com/reference/post-files-content/
std::string multipart_boundary =
"--" + content_type.substr(expected_type.size());
std::string expected_body(multipart_boundary + "\r\n");
expected_body +=
"Content-Disposition: form-data; name=\"attributes\"\r\n"
"Content-Type: application/json\r\n\r\n"
"{\"name\":\"";
expected_body +=
file_name_.AsUTF8Unsafe() + // AsUTF8Unsafe() to compile on Windows
"\","
"\"parent\":{\"id\":\"" +
folder_id_ + "\"}}\r\n";
expected_body += multipart_boundary + "\r\n";
expected_body += "Content-Disposition: form-data; name=\"file\"; filename=\"";
expected_body +=
file_name_.AsUTF8Unsafe() + // AsUTF8Unsafe() to compile on Windows
"\"\r\nContent-Type: text/plain\r\n\r\n\r\n";
expected_body += multipart_boundary + "--\r\n";
std::string body = flow_->CreateApiCallBody();
ASSERT_EQ(body, expected_body);
}
TEST_F(BoxWholeFileUploadApiCallFlowTest, IsExpectedSuccessCode) {
ASSERT_TRUE(flow_->IsExpectedSuccessCode(201));
ASSERT_FALSE(flow_->IsExpectedSuccessCode(400));
ASSERT_FALSE(flow_->IsExpectedSuccessCode(403));
ASSERT_FALSE(flow_->IsExpectedSuccessCode(404));
ASSERT_FALSE(flow_->IsExpectedSuccessCode(409));
}
TEST_F(BoxWholeFileUploadApiCallFlowTest, ProcessApiCallSuccess) {
// Create a temporary file to be deleted in ProcessApiCallSuccess().
if (!base::WriteFile(file_path_, "BoxWholeFileUploadApiCallFlowTest")) {
FAIL() << "Failed to create file " << file_path_;
}
std::string body; // Dummy body since we don't read from body for now.
auto http_head = network::CreateURLResponseHead(net::HTTP_CREATED);
// Because we post tasks to base::ThreadPool, cannot use
// base::RunLoop().RunUntilIdle().
base::RunLoop run_loop;
quit_closure_ = run_loop.QuitClosure();
flow_->ProcessApiCallSuccess(http_head.get(),
std::make_unique<std::string>(body));
run_loop.Run();
ASSERT_EQ(response_code_, net::HTTP_CREATED);
ASSERT_TRUE(processed_success_) << "Failed with file " << file_path_;
ASSERT_FALSE(base::PathExists(file_path_)); // Make sure file is deleted.
}
TEST_F(BoxWholeFileUploadApiCallFlowTest,
ProcessApiCallSuccess_NoFileToDelete) {
std::string body; // Dummy body since we don't read from body for now.
auto http_head = network::CreateURLResponseHead(net::HTTP_CREATED);
ASSERT_FALSE(base::PathExists(file_path_)); // Make sure file doesn't exist.
// Because we post tasks to base::ThreadPool, cannot use
// base::RunLoop().RunUntilIdle().
base::RunLoop run_loop;
quit_closure_ = run_loop.QuitClosure();
flow_->ProcessApiCallSuccess(http_head.get(),
std::make_unique<std::string>(body));
run_loop.Run();
ASSERT_EQ(response_code_, net::HTTP_CREATED);
ASSERT_FALSE(processed_success_); // Should fail file deletion.
}
TEST_F(BoxWholeFileUploadApiCallFlowTest, ProcessApiCallFailure) {
std::string body; // Dummy body since we don't read from body here.
auto http_head = network::CreateURLResponseHead(net::HTTP_CONFLICT);
base::RunLoop run_loop;
quit_closure_ = run_loop.QuitClosure();
flow_->ProcessApiCallFailure(0, http_head.get(),
std::make_unique<std::string>(body));
run_loop.Run();
ASSERT_EQ(response_code_, net::HTTP_CONFLICT);
ASSERT_FALSE(processed_success_);
}
class BoxWholeFileUploadApiCallFlowFileReadTest
: public BoxWholeFileUploadApiCallFlowTest {
public:
BoxWholeFileUploadApiCallFlowFileReadTest()
: url_factory_(
base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>(
&test_url_loader_factory_)) {}
protected:
network::TestURLLoaderFactory test_url_loader_factory_;
scoped_refptr<network::SharedURLLoaderFactory> url_factory_;
};
TEST_F(BoxWholeFileUploadApiCallFlowFileReadTest, GoodUpload) {
if (!base::WriteFile(file_path_,
"BoxWholeFileUploadApiCallFlowFileReadTest")) {
FAIL() << "Failed to create temporary file " << file_path_;
}
test_url_loader_factory_.AddResponse(
"https://upload.box.com/api/2.0/files/content",
std::string(), // Dummy body since we are not reading from body.
net::HTTP_CREATED);
base::RunLoop run_loop;
quit_closure_ = run_loop.QuitClosure();
flow_->Start(url_factory_, "dummytoken");
run_loop.Run();
ASSERT_EQ(response_code_, net::HTTP_CREATED);
ASSERT_TRUE(processed_success_) << "Failed with file " << file_path_;
ASSERT_FALSE(base::PathExists(file_path_));
}
TEST_F(BoxWholeFileUploadApiCallFlowFileReadTest, NoFile) {
ASSERT_FALSE(base::PathExists(file_path_));
base::RunLoop run_loop;
quit_closure_ = run_loop.QuitClosure();
flow_->Start(url_factory_, "dummytoken");
run_loop.Run();
// There should be no HTTP response code, because it should already fail when
// reading file, before making any actual API calls.
ASSERT_EQ(response_code_, 0);
ASSERT_FALSE(processed_success_);
}
} // namespace enterprise_connectors } // namespace enterprise_connectors
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment