Amazon S3 examples using SDK for Rust (original) (raw)
Cargo.toml for testing examples.
[package]
name = "testing-examples"
version = "0.1.0"
authors = [
"John Disanti <jdisanti@amazon.com>",
"Doug Schwartz <dougsch@amazon.com>",
]
edition = "2021"
[dependencies]
async-trait = "0.1.51"
aws-config = { version = "1.0.1", features = ["behavior-version-latest"] }
aws-credential-types = { version = "1.0.1", features = [ "hardcoded-credentials", ] }
aws-sdk-s3 = { version = "1.4.0" }
aws-smithy-types = { version = "1.0.1" }
aws-smithy-runtime = { version = "1.0.1", features = ["test-util"] }
aws-smithy-runtime-api = { version = "1.0.1", features = ["test-util"] }
aws-types = { version = "1.0.1" }
clap = { version = "4.4", features = ["derive"] }
http = "0.2.9"
mockall = "0.11.4"
serde_json = "1"
tokio = { version = "1.20.1", features = ["full"] }
tracing-subscriber = { version = "0.3.15", features = ["env-filter"] }
[[bin]]
name = "main"
path = "src/main.rs"
Unit testing example using automock and a service wrapper.
use aws_sdk_s3 as s3;
#[allow(unused_imports)]
use mockall::automock;
use s3::operation::list_objects_v2::{ListObjectsV2Error, ListObjectsV2Output};
#[cfg(test)]
pub use MockS3Impl as S3;
#[cfg(not(test))]
pub use S3Impl as S3;
#[allow(dead_code)]
pub struct S3Impl {
inner: s3::Client,
}
#[cfg_attr(test, automock)]
impl S3Impl {
#[allow(dead_code)]
pub fn new(inner: s3::Client) -> Self {
Self { inner }
}
#[allow(dead_code)]
pub async fn list_objects(
&self,
bucket: &str,
prefix: &str,
continuation_token: Option<String>,
) -> Result<ListObjectsV2Output, s3::error::SdkError<ListObjectsV2Error>> {
self.inner
.list_objects_v2()
.bucket(bucket)
.prefix(prefix)
.set_continuation_token(continuation_token)
.send()
.await
}
}
#[allow(dead_code)]
pub async fn determine_prefix_file_size(
// Now we take a reference to our trait object instead of the S3 client
// s3_list: ListObjectsService,
s3_list: S3,
bucket: &str,
prefix: &str,
) -> Result<usize, s3::Error> {
let mut next_token: Option<String> = None;
let mut total_size_bytes = 0;
loop {
let result = s3_list
.list_objects(bucket, prefix, next_token.take())
.await?;
// Add up the file sizes we got back
for object in result.contents() {
total_size_bytes += object.size().unwrap_or(0) as usize;
}
// Handle pagination, and break the loop if there are no more pages
next_token = result.next_continuation_token.clone();
if next_token.is_none() {
break;
}
}
Ok(total_size_bytes)
}
#[cfg(test)]
mod test {
use super::*;
use mockall::predicate::eq;
#[tokio::test]
async fn test_single_page() {
let mut mock = MockS3Impl::default();
mock.expect_list_objects()
.with(eq("test-bucket"), eq("test-prefix"), eq(None))
.return_once(|_, _, _| {
Ok(ListObjectsV2Output::builder()
.set_contents(Some(vec![
// Mock content for ListObjectsV2 response
s3::types::Object::builder().size(5).build(),
s3::types::Object::builder().size(2).build(),
]))
.build())
});
// Run the code we want to test with it
let size = determine_prefix_file_size(mock, "test-bucket", "test-prefix")
.await
.unwrap();
// Verify we got the correct total size back
assert_eq!(7, size);
}
#[tokio::test]
async fn test_multiple_pages() {
// Create the Mock instance with two pages of objects now
let mut mock = MockS3Impl::default();
mock.expect_list_objects()
.with(eq("test-bucket"), eq("test-prefix"), eq(None))
.return_once(|_, _, _| {
Ok(ListObjectsV2Output::builder()
.set_contents(Some(vec![
// Mock content for ListObjectsV2 response
s3::types::Object::builder().size(5).build(),
s3::types::Object::builder().size(2).build(),
]))
.set_next_continuation_token(Some("next".to_string()))
.build())
});
mock.expect_list_objects()
.with(
eq("test-bucket"),
eq("test-prefix"),
eq(Some("next".to_string())),
)
.return_once(|_, _, _| {
Ok(ListObjectsV2Output::builder()
.set_contents(Some(vec![
// Mock content for ListObjectsV2 response
s3::types::Object::builder().size(3).build(),
s3::types::Object::builder().size(9).build(),
]))
.build())
});
// Run the code we want to test with it
let size = determine_prefix_file_size(mock, "test-bucket", "test-prefix")
.await
.unwrap();
assert_eq!(19, size);
}
}
Integration testing example using StaticReplayClient.
use aws_sdk_s3 as s3;
#[allow(dead_code)]
pub async fn determine_prefix_file_size(
// Now we take a reference to our trait object instead of the S3 client
// s3_list: ListObjectsService,
s3: s3::Client,
bucket: &str,
prefix: &str,
) -> Result<usize, s3::Error> {
let mut next_token: Option<String> = None;
let mut total_size_bytes = 0;
loop {
let result = s3
.list_objects_v2()
.prefix(prefix)
.bucket(bucket)
.set_continuation_token(next_token.take())
.send()
.await?;
// Add up the file sizes we got back
for object in result.contents() {
total_size_bytes += object.size().unwrap_or(0) as usize;
}
// Handle pagination, and break the loop if there are no more pages
next_token = result.next_continuation_token.clone();
if next_token.is_none() {
break;
}
}
Ok(total_size_bytes)
}
#[allow(dead_code)]
fn make_s3_test_credentials() -> s3::config::Credentials {
s3::config::Credentials::new(
"ATESTCLIENT",
"astestsecretkey",
Some("atestsessiontoken".to_string()),
None,
"",
)
}
#[cfg(test)]
mod test {
use super::*;
use aws_config::BehaviorVersion;
use aws_sdk_s3 as s3;
use aws_smithy_runtime::client::http::test_util::{ReplayEvent, StaticReplayClient};
use aws_smithy_types::body::SdkBody;
#[tokio::test]
async fn test_single_page() {
let page_1 = ReplayEvent::new(
http::Request::builder()
.method("GET")
.uri("https://test-bucket.s3.us-east-1.amazonaws.com/?list-type=2&prefix=test-prefix")
.body(SdkBody::empty())
.unwrap(),
http::Response::builder()
.status(200)
.body(SdkBody::from(include_str!("./testing/response_1.xml")))
.unwrap(),
);
let replay_client = StaticReplayClient::new(vec![page_1]);
let client: s3::Client = s3::Client::from_conf(
s3::Config::builder()
.behavior_version(BehaviorVersion::latest())
.credentials_provider(make_s3_test_credentials())
.region(s3::config::Region::new("us-east-1"))
.http_client(replay_client.clone())
.build(),
);
// Run the code we want to test with it
let size = determine_prefix_file_size(client, "test-bucket", "test-prefix")
.await
.unwrap();
// Verify we got the correct total size back
assert_eq!(7, size);
replay_client.assert_requests_match(&[]);
}
#[tokio::test]
async fn test_multiple_pages() {
let page_1 = ReplayEvent::new(
http::Request::builder()
.method("GET")
.uri("https://test-bucket.s3.us-east-1.amazonaws.com/?list-type=2&prefix=test-prefix")
.body(SdkBody::empty())
.unwrap(),
http::Response::builder()
.status(200)
.body(SdkBody::from(include_str!("./testing/response_multi_1.xml")))
.unwrap(),
);
let page_2 = ReplayEvent::new(
http::Request::builder()
.method("GET")
.uri("https://test-bucket.s3.us-east-1.amazonaws.com/?list-type=2&prefix=test-prefix&continuation-token=next")
.body(SdkBody::empty())
.unwrap(),
http::Response::builder()
.status(200)
.body(SdkBody::from(include_str!("./testing/response_multi_2.xml")))
.unwrap(),
);
let replay_client = StaticReplayClient::new(vec![page_1, page_2]);
let client: s3::Client = s3::Client::from_conf(
s3::Config::builder()
.behavior_version(BehaviorVersion::latest())
.credentials_provider(make_s3_test_credentials())
.region(s3::config::Region::new("us-east-1"))
.http_client(replay_client.clone())
.build(),
);
// Run the code we want to test with it
let size = determine_prefix_file_size(client, "test-bucket", "test-prefix")
.await
.unwrap();
assert_eq!(19, size);
replay_client.assert_requests_match(&[]);
}
}