-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add integration tests for uploading multiple objects
- Loading branch information
1 parent
da0a39c
commit 225d42c
Showing
9 changed files
with
257 additions
and
47 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
[package] | ||
name = "test-common" | ||
version = "0.1.0" | ||
edition = "2021" | ||
publish = false | ||
|
||
[dependencies] | ||
tempfile = "3.12.0" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
/* | ||
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
#![cfg(target_family = "unix")] | ||
|
||
use std::{fs, io::Write}; | ||
use tempfile::{tempdir, TempDir}; | ||
|
||
/// Create a directory structure rooted at `recursion_root`, containing files with sizes | ||
/// specified in `files` | ||
/// | ||
/// For testing purposes, certain directories (and all files within them) can be made | ||
/// inaccessible by providing `inaccessible_dir_relative_paths`, which should be relative | ||
/// to `recursion_root`. | ||
pub fn create_test_dir( | ||
recursion_root: Option<&str>, | ||
files: Vec<(&str, usize)>, | ||
inaccessible_dir_relative_paths: &[&str], | ||
) -> TempDir { | ||
let temp_dir = match recursion_root { | ||
Some(root) => TempDir::with_prefix(root).unwrap(), | ||
None => tempdir().unwrap(), | ||
}; | ||
|
||
// Create the directory structure and files | ||
for (path, size) in files { | ||
let full_path = temp_dir.path().join(path); | ||
let parent = full_path.parent().unwrap(); | ||
|
||
// Create the parent directories if they don't exist | ||
fs::create_dir_all(parent).unwrap(); | ||
|
||
// Create the .jpg file with the specified size | ||
let mut file = fs::File::create(&full_path).unwrap(); | ||
file.write_all(&vec![0; size]).unwrap(); // Writing `size` byte | ||
} | ||
|
||
// Set the directories in `inaccessible_dir_relative_paths` to be inaccessible, | ||
// which will in turn render the files within those directories inaccessible | ||
for dir_relative_path in inaccessible_dir_relative_paths { | ||
let dir_path = temp_dir.path().join(*dir_relative_path); | ||
let mut permissions = fs::metadata(&dir_path).unwrap().permissions(); | ||
std::os::unix::fs::PermissionsExt::set_mode(&mut permissions, 0o000); // No permissions for anyone | ||
fs::set_permissions(dir_path, permissions).unwrap(); | ||
} | ||
|
||
temp_dir | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,191 @@ | ||
/* | ||
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
#![cfg(target_family = "unix")] | ||
|
||
use aws_s3_transfer_manager::{error::ErrorKind, types::FailedTransferPolicy}; | ||
use aws_sdk_s3::{ | ||
error::DisplayErrorContext, | ||
operation::{ | ||
complete_multipart_upload::CompleteMultipartUploadOutput, | ||
create_multipart_upload::CreateMultipartUploadOutput, upload_part::UploadPartOutput, | ||
}, | ||
Client, | ||
}; | ||
use aws_smithy_mocks_experimental::{mock, mock_client, RuleMode}; | ||
use test_common::create_test_dir; | ||
|
||
// Create an S3 client with mock behavior configured | ||
// | ||
// We intentionally avoid being specific about the expected input and output for mocks, | ||
// as long as the execution of uploading multiple objects completes successfully. | ||
// Setting expectations that are too precise can lead to brittle tests. | ||
fn mock_s3_client(bucket_name: String) -> Client { | ||
let upload_id = "test-upload-id".to_owned(); | ||
|
||
let create_mpu = mock!(aws_sdk_s3::Client::create_multipart_upload).then_output({ | ||
let upload_id = upload_id.clone(); | ||
move || { | ||
CreateMultipartUploadOutput::builder() | ||
.upload_id(upload_id.clone()) | ||
.build() | ||
} | ||
}); | ||
|
||
let upload_part = mock!(aws_sdk_s3::Client::upload_part) | ||
.match_requests({ | ||
let upload_id = upload_id.clone(); | ||
move |input| { | ||
input.upload_id.as_ref() == Some(&upload_id) && input.bucket() == Some(&bucket_name) | ||
} | ||
}) | ||
.then_output(|| UploadPartOutput::builder().build()); | ||
|
||
let complete_mpu = mock!(aws_sdk_s3::Client::complete_multipart_upload) | ||
.match_requests({ | ||
let upload_id = upload_id.clone(); | ||
move |r| r.upload_id.as_ref() == Some(&upload_id) | ||
}) | ||
.then_output(|| CompleteMultipartUploadOutput::builder().build()); | ||
|
||
mock_client!( | ||
aws_sdk_s3, | ||
RuleMode::MatchAny, | ||
&[create_mpu, upload_part, complete_mpu] | ||
) | ||
} | ||
|
||
#[tokio::test] | ||
async fn test_successful_multiple_objects_upload() { | ||
let recursion_root = "test"; | ||
let files = vec![ | ||
("sample.jpg", 1), | ||
("photos/2022/January/sample.jpg", 1), | ||
("photos/2022/February/sample1.jpg", 1), | ||
("photos/2022/February/sample2.jpg", 1), | ||
("photos/2022/February/sample3.jpg", 1), | ||
]; | ||
let test_dir = create_test_dir(Some(&recursion_root), files.clone(), &[]); | ||
|
||
let bucket_name = "test-bucket"; | ||
let config = aws_s3_transfer_manager::Config::builder() | ||
.client(mock_s3_client(bucket_name.to_owned())) | ||
.build(); | ||
let sut = aws_s3_transfer_manager::Client::new(config); | ||
|
||
let handle = sut | ||
.upload_objects() | ||
.bucket(bucket_name) | ||
.source(test_dir.path()) | ||
.recursive(true) | ||
.send() | ||
.await | ||
.unwrap(); | ||
|
||
let output = handle.join().await.unwrap(); | ||
assert_eq!(5, output.objects_uploaded()); | ||
assert!(output.failed_transfers().is_empty()); | ||
assert_eq!(5, output.total_bytes_transferred()); | ||
} | ||
|
||
#[tokio::test] | ||
async fn test_failed_upload_policy_continue() { | ||
let recursion_root = "test"; | ||
let files = vec![ | ||
("sample.jpg", 1), | ||
("photos/2022/January/sample.jpg", 1), | ||
("photos/2022/February/sample1.jpg", 1), | ||
("photos/2022/February/sample2.jpg", 1), | ||
("photos/2022/February/sample3.jpg", 1), | ||
]; | ||
// Make all files inaccessible under `photos/2022/February` | ||
let inaccessible_dir_relative_path = "photos/2022/February"; | ||
let test_dir = create_test_dir( | ||
Some(&recursion_root), | ||
files.clone(), | ||
&[inaccessible_dir_relative_path], | ||
); | ||
|
||
let bucket_name = "test-bucket"; | ||
let config = aws_s3_transfer_manager::Config::builder() | ||
.client(mock_s3_client(bucket_name.to_owned())) | ||
.build(); | ||
let sut = aws_s3_transfer_manager::Client::new(config); | ||
|
||
let handle = sut | ||
.upload_objects() | ||
.bucket(bucket_name) | ||
.source(test_dir.path()) | ||
.recursive(true) | ||
.failure_policy(FailedTransferPolicy::Continue) | ||
.send() | ||
.await | ||
.unwrap(); | ||
|
||
let output = handle.join().await.unwrap(); | ||
assert_eq!(2, output.objects_uploaded()); | ||
assert_eq!(1, output.failed_transfers().len()); // Cannot traverse inaccessible dir to count how many files are in it | ||
assert_eq!(2, output.total_bytes_transferred()); | ||
} | ||
|
||
/// Fail when source is not a directory | ||
#[tokio::test] | ||
async fn test_source_dir_not_valid() { | ||
let source = tempfile::NamedTempFile::new().unwrap(); | ||
|
||
let bucket_name = "test-bucket"; | ||
let config = aws_s3_transfer_manager::Config::builder() | ||
.client(mock_s3_client(bucket_name.to_owned())) | ||
.build(); | ||
let sut = aws_s3_transfer_manager::Client::new(config); | ||
|
||
let err = sut | ||
.upload_objects() | ||
.bucket(bucket_name) | ||
.source(source.path()) | ||
.send() | ||
.await | ||
.unwrap_err(); | ||
|
||
assert_eq!(&ErrorKind::InputInvalid, err.kind()); | ||
assert!(format!("{}", DisplayErrorContext(err)).contains("is not a directory")); | ||
} | ||
|
||
#[tokio::test] | ||
async fn test_error_when_custom_delimiter_appears_in_filename() { | ||
let recursion_root = "test"; | ||
let files = vec![ | ||
("sample.jpg", 1), | ||
("photos/2022-January/sample.jpg", 1), | ||
("photos/2022-February/sample1.jpg", 1), | ||
("photos/2022-February/sample2.jpg", 1), | ||
("photos/2022-February/sample3.jpg", 1), | ||
]; | ||
let test_dir = create_test_dir(Some(&recursion_root), files.clone(), &[]); | ||
|
||
let bucket_name = "test-bucket"; | ||
let config = aws_s3_transfer_manager::Config::builder() | ||
.client(mock_s3_client(bucket_name.to_owned())) | ||
.build(); | ||
let sut = aws_s3_transfer_manager::Client::new(config); | ||
|
||
// Getting `handle` is ok, i.e., tasks should be spawned from `UploadObjects::orchestrate` | ||
let handle = sut | ||
.upload_objects() | ||
.bucket(bucket_name) | ||
.source(test_dir.path()) | ||
.recursive(true) | ||
.delimiter("-") | ||
.send() | ||
.await | ||
.unwrap(); | ||
|
||
// But unwrapping it should fail | ||
let err = handle.join().await.unwrap_err(); | ||
|
||
assert_eq!(&ErrorKind::InputInvalid, err.kind()); | ||
assert!(format!("{}", DisplayErrorContext(err)) | ||
.contains("a custom delimiter `-` should not appear")); | ||
} |