#![cfg(e2e_test)]
#[cfg(test)]
mod common;
#[cfg(test)]
mod tests {
use super::*;
use common::*;
use std::process::{Command, Output, Stdio};
const EXIT_CODE_SUCCESS: i32 = 0;
const MULTIPART_SIZE: usize = 9 * 1024 * 1024;
fn run_s3util(args: &[&str]) -> Output {
Command::new(env!("CARGO_BIN_EXE_s3util"))
.args(args)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.unwrap()
}
async fn cp_with_annotations(source: &str, target: &str, extra: &[&str]) -> StatsCount {
let helper = TestHelper::new().await;
let mut args = vec![
"s3util",
"cp",
"--source-profile",
"s3util-e2e-test",
"--target-profile",
"s3util-e2e-test",
"--enable-sync-object-annotations",
];
args.extend_from_slice(extra);
args.push(source);
args.push(target);
helper.cp_test_data(args).await
}
#[tokio::test]
async fn cp_sync_annotations_basic() {
TestHelper::init_dummy_tracing_subscriber();
let helper = TestHelper::new().await;
let bucket1 = TestHelper::generate_bucket_name();
let bucket2 = TestHelper::generate_bucket_name();
helper.create_bucket(&bucket1, REGION).await;
helper.create_bucket(&bucket2, REGION).await;
helper
.put_object(&bucket1, "test_object", b"test_object_content".to_vec())
.await;
helper
.put_object(&bucket1, "test_object2", b"test_object_2_content".to_vec())
.await;
helper
.put_object_annotation(
&bucket1,
"test_object",
None,
"test_annotation_name1",
"test_annotation_value1",
None,
)
.await;
helper
.put_object_annotation(
&bucket1,
"test_object",
None,
"test_annotation_name2",
"test_annotation_value2",
None,
)
.await;
helper
.put_object_annotation(
&bucket1,
"test_object2",
None,
"test_annotation_name3",
"test_annotation_value3",
None,
)
.await;
let source1 = format!("s3://{}/test_object", bucket1);
let target1 = format!("s3://{}/test_object", bucket2);
let stats = cp_with_annotations(&source1, &target1, &[]).await;
assert_eq!(stats.sync_complete, 1);
assert_eq!(stats.sync_error, 0);
assert_eq!(stats.sync_warning, 0);
assert_eq!(stats.e_tag_verified, 1);
let source2 = format!("s3://{}/test_object2", bucket1);
let target2 = format!("s3://{}/test_object2", bucket2);
let stats = cp_with_annotations(&source2, &target2, &[]).await;
assert_eq!(stats.sync_complete, 1);
assert_eq!(stats.sync_error, 0);
assert_eq!(
helper
.get_object_annotation(&bucket2, "test_object", None, "test_annotation_name1")
.await,
"test_annotation_value1"
);
assert_eq!(
helper
.get_object_annotation(&bucket2, "test_object", None, "test_annotation_name2")
.await,
"test_annotation_value2"
);
assert_eq!(
helper
.get_object_annotation(&bucket2, "test_object2", None, "test_annotation_name3")
.await,
"test_annotation_value3"
);
helper.delete_bucket_with_cascade(&bucket1).await;
helper.delete_bucket_with_cascade(&bucket2).await;
}
#[tokio::test]
async fn cp_sync_annotations_renamed_key() {
TestHelper::init_dummy_tracing_subscriber();
let helper = TestHelper::new().await;
let bucket1 = TestHelper::generate_bucket_name();
let bucket2 = TestHelper::generate_bucket_name();
helper.create_bucket(&bucket1, REGION).await;
helper.create_bucket(&bucket2, REGION).await;
helper
.put_object(
&bucket1,
"prefix/test_object",
b"test_object_content".to_vec(),
)
.await;
helper
.put_object_annotation(
&bucket1,
"prefix/test_object",
None,
"test_annotation_name1",
"test_annotation_value1",
None,
)
.await;
let source = format!("s3://{}/prefix/test_object", bucket1);
let target = format!("s3://{}/prefix2/renamed_object", bucket2);
let stats = cp_with_annotations(&source, &target, &[]).await;
assert_eq!(stats.sync_complete, 1);
assert_eq!(stats.sync_error, 0);
assert_eq!(
helper
.get_object_annotation(
&bucket2,
"prefix2/renamed_object",
None,
"test_annotation_name1"
)
.await,
"test_annotation_value1"
);
helper.delete_bucket_with_cascade(&bucket1).await;
helper.delete_bucket_with_cascade(&bucket2).await;
}
#[tokio::test]
async fn cp_sync_annotations_dry_run() {
TestHelper::init_dummy_tracing_subscriber();
let helper = TestHelper::new().await;
let bucket1 = TestHelper::generate_bucket_name();
let bucket2 = TestHelper::generate_bucket_name();
helper.create_bucket(&bucket1, REGION).await;
helper.create_bucket(&bucket2, REGION).await;
helper
.put_object(&bucket1, "test_object", b"test_object_content".to_vec())
.await;
helper
.put_object_annotation(
&bucket1,
"test_object",
None,
"test_annotation_name1",
"test_annotation_value1",
None,
)
.await;
let source = format!("s3://{}/test_object", bucket1);
let target = format!("s3://{}/test_object", bucket2);
let output = run_s3util(&[
"cp",
"--source-profile",
"s3util-e2e-test",
"--target-profile",
"s3util-e2e-test",
"--enable-sync-object-annotations",
"--dry-run",
&source,
&target,
]);
assert_eq!(
output.status.code(),
Some(EXIT_CODE_SUCCESS),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("[dry-run] would copy object annotation."),
"dry-run must list annotations to copy; stderr: {stderr}"
);
assert!(
stderr.contains("test_annotation_name1"),
"dry-run must name the annotation; stderr: {stderr}"
);
assert!(!helper.is_object_exist(&bucket2, "test_object", None).await);
assert!(
!helper
.is_object_annotation_exist(&bucket2, "test_object", None, "test_annotation_name1")
.await
);
helper.delete_bucket_with_cascade(&bucket1).await;
helper.delete_bucket_with_cascade(&bucket2).await;
}
#[tokio::test]
async fn cp_sync_annotations_dry_run_server_side_copy() {
TestHelper::init_dummy_tracing_subscriber();
let helper = TestHelper::new().await;
let bucket1 = TestHelper::generate_bucket_name();
let bucket2 = TestHelper::generate_bucket_name();
helper.create_bucket(&bucket1, REGION).await;
helper.create_bucket(&bucket2, REGION).await;
helper
.put_object(&bucket1, "test_object", b"test_object_content".to_vec())
.await;
helper
.put_object_annotation(
&bucket1,
"test_object",
None,
"test_annotation_name1",
"test_annotation_value1",
None,
)
.await;
let source = format!("s3://{}/test_object", bucket1);
let target = format!("s3://{}/test_object", bucket2);
let output = run_s3util(&[
"cp",
"--source-profile",
"s3util-e2e-test",
"--target-profile",
"s3util-e2e-test",
"--enable-sync-object-annotations",
"--server-side-copy",
"--dry-run",
&source,
&target,
]);
assert_eq!(
output.status.code(),
Some(EXIT_CODE_SUCCESS),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("[dry-run] would copy object annotation."),
"dry-run must list annotations to copy; stderr: {stderr}"
);
assert!(
stderr.contains("test_annotation_name1"),
"dry-run must name the annotation; stderr: {stderr}"
);
assert!(!helper.is_object_exist(&bucket2, "test_object", None).await);
assert!(
!helper
.is_object_annotation_exist(&bucket2, "test_object", None, "test_annotation_name1")
.await
);
helper.delete_bucket_with_cascade(&bucket1).await;
helper.delete_bucket_with_cascade(&bucket2).await;
}
#[tokio::test]
async fn cp_sync_annotations_kms() {
TestHelper::init_dummy_tracing_subscriber();
let helper = TestHelper::new().await;
let bucket1 = TestHelper::generate_bucket_name();
let bucket2 = TestHelper::generate_bucket_name();
helper.create_bucket(&bucket1, REGION).await;
helper.create_bucket(&bucket2, REGION).await;
helper
.put_object_kms(&bucket1, "test_object", b"test_object_content".to_vec())
.await;
helper
.put_object_annotation(
&bucket1,
"test_object",
None,
"test_annotation_name1",
"test_annotation_value1",
None,
)
.await;
let source = format!("s3://{}/test_object", bucket1);
let target = format!("s3://{}/test_object", bucket2);
let stats = cp_with_annotations(&source, &target, &[]).await;
assert_eq!(stats.sync_complete, 1);
assert_eq!(stats.sync_error, 0);
assert_eq!(stats.e_tag_verified, 0);
assert_eq!(
helper
.get_object_annotation(&bucket2, "test_object", None, "test_annotation_name1")
.await,
"test_annotation_value1"
);
helper.delete_bucket_with_cascade(&bucket1).await;
helper.delete_bucket_with_cascade(&bucket2).await;
}
#[tokio::test]
async fn cp_sync_annotations_sha256() {
TestHelper::init_dummy_tracing_subscriber();
let helper = TestHelper::new().await;
let bucket1 = TestHelper::generate_bucket_name();
let bucket2 = TestHelper::generate_bucket_name();
helper.create_bucket(&bucket1, REGION).await;
helper.create_bucket(&bucket2, REGION).await;
helper
.put_object(&bucket1, "test_object", b"test_object_content".to_vec())
.await;
helper
.put_object_annotation(
&bucket1,
"test_object",
None,
TEST_ANNOTATION_NAME,
TEST_ANNOTATION_VALUE,
Some(TEST_ANNOTATION_VALUE_SHA2.to_string()),
)
.await;
let source = format!("s3://{}/test_object", bucket1);
let target = format!("s3://{}/test_object", bucket2);
let stats = cp_with_annotations(&source, &target, &[]).await;
assert_eq!(stats.sync_complete, 1);
assert_eq!(stats.sync_error, 0);
assert_eq!(
helper
.get_object_annotation(&bucket2, "test_object", None, TEST_ANNOTATION_NAME)
.await,
TEST_ANNOTATION_VALUE
);
helper.delete_bucket_with_cascade(&bucket1).await;
helper.delete_bucket_with_cascade(&bucket2).await;
}
#[tokio::test]
async fn cp_sync_annotations_server_side_copy() {
TestHelper::init_dummy_tracing_subscriber();
let helper = TestHelper::new().await;
let bucket1 = TestHelper::generate_bucket_name();
let bucket2 = TestHelper::generate_bucket_name();
helper.create_bucket(&bucket1, REGION).await;
helper.create_bucket(&bucket2, REGION).await;
helper
.put_object(&bucket1, "test_object", b"test_object_content".to_vec())
.await;
helper
.put_object_annotation(
&bucket1,
"test_object",
None,
"test_annotation_name1",
"test_annotation_value1",
None,
)
.await;
let source = format!("s3://{}/test_object", bucket1);
let target = format!("s3://{}/test_object", bucket2);
let stats = cp_with_annotations(&source, &target, &["--server-side-copy"]).await;
assert_eq!(stats.sync_complete, 1);
assert_eq!(stats.sync_error, 0);
assert_eq!(
helper
.get_object_annotation(&bucket2, "test_object", None, "test_annotation_name1")
.await,
"test_annotation_value1"
);
helper.delete_bucket_with_cascade(&bucket1).await;
helper.delete_bucket_with_cascade(&bucket2).await;
}
#[tokio::test]
async fn cp_sync_annotations_multipart() {
TestHelper::init_dummy_tracing_subscriber();
let helper = TestHelper::new().await;
let bucket1 = TestHelper::generate_bucket_name();
let bucket2 = TestHelper::generate_bucket_name();
helper.create_bucket(&bucket1, REGION).await;
helper.create_bucket(&bucket2, REGION).await;
helper
.put_object(&bucket1, "large_file", vec![0_u8; MULTIPART_SIZE])
.await;
helper
.put_object_annotation(
&bucket1,
"large_file",
None,
"test_annotation_name1",
"test_annotation_value1",
None,
)
.await;
let source = format!("s3://{}/large_file", bucket1);
let target = format!("s3://{}/large_file", bucket2);
let stats = cp_with_annotations(&source, &target, &[]).await;
assert_eq!(stats.sync_complete, 1);
assert_eq!(stats.sync_error, 0);
assert_eq!(
helper
.get_object_annotation(&bucket2, "large_file", None, "test_annotation_name1")
.await,
"test_annotation_value1"
);
helper.delete_bucket_with_cascade(&bucket1).await;
helper.delete_bucket_with_cascade(&bucket2).await;
}
#[tokio::test]
async fn cp_sync_annotations_multipart_server_side_copy() {
TestHelper::init_dummy_tracing_subscriber();
let helper = TestHelper::new().await;
let bucket1 = TestHelper::generate_bucket_name();
let bucket2 = TestHelper::generate_bucket_name();
helper.create_bucket(&bucket1, REGION).await;
helper.create_bucket(&bucket2, REGION).await;
helper
.put_object(&bucket1, "large_file", vec![0_u8; MULTIPART_SIZE])
.await;
helper
.put_object_annotation(
&bucket1,
"large_file",
None,
"test_annotation_name1",
"test_annotation_value1",
None,
)
.await;
let source = format!("s3://{}/large_file", bucket1);
let target = format!("s3://{}/large_file", bucket2);
let stats = cp_with_annotations(&source, &target, &["--server-side-copy"]).await;
assert_eq!(stats.sync_complete, 1);
assert_eq!(stats.sync_error, 0);
assert_eq!(
helper
.get_object_annotation(&bucket2, "large_file", None, "test_annotation_name1")
.await,
"test_annotation_value1"
);
helper.delete_bucket_with_cascade(&bucket1).await;
helper.delete_bucket_with_cascade(&bucket2).await;
}
#[tokio::test]
async fn cp_sync_annotations_multipart_server_side_copy_without_flag() {
TestHelper::init_dummy_tracing_subscriber();
let helper = TestHelper::new().await;
let bucket1 = TestHelper::generate_bucket_name();
let bucket2 = TestHelper::generate_bucket_name();
helper.create_bucket(&bucket1, REGION).await;
helper.create_bucket(&bucket2, REGION).await;
helper
.put_object(&bucket1, "large_file", vec![0_u8; MULTIPART_SIZE])
.await;
helper
.put_object_annotation(
&bucket1,
"large_file",
None,
"test_annotation_name1",
"test_annotation_value1",
None,
)
.await;
let source = format!("s3://{}/large_file", bucket1);
let target = format!("s3://{}/large_file", bucket2);
let stats = helper
.cp_test_data(vec![
"s3util",
"cp",
"--source-profile",
"s3util-e2e-test",
"--target-profile",
"s3util-e2e-test",
"--server-side-copy",
&source,
&target,
])
.await;
assert_eq!(stats.sync_complete, 1);
assert_eq!(stats.sync_error, 0);
assert!(
!helper
.is_object_annotation_exist(&bucket2, "large_file", None, "test_annotation_name1")
.await
);
helper.delete_bucket_with_cascade(&bucket1).await;
helper.delete_bucket_with_cascade(&bucket2).await;
}
#[tokio::test]
async fn cp_sync_annotations_source_version_id() {
TestHelper::init_dummy_tracing_subscriber();
let helper = TestHelper::new().await;
let bucket1 = TestHelper::generate_bucket_name();
let bucket2 = TestHelper::generate_bucket_name();
helper.create_bucket(&bucket1, REGION).await;
helper.enable_bucket_versioning(&bucket1).await;
helper.create_bucket(&bucket2, REGION).await;
let v1 = helper
.put_object_with_version_id(&bucket1, "test_object", b"version1".to_vec())
.await
.expect("versioned bucket must return a version id");
helper
.put_object_annotation(
&bucket1,
"test_object",
Some(v1.clone()),
"test_annotation_name1",
"value_for_v1",
None,
)
.await;
let _v2 = helper
.put_object_with_version_id(&bucket1, "test_object", b"version2".to_vec())
.await;
helper
.put_object_annotation(
&bucket1,
"test_object",
None,
"test_annotation_name1",
"value_for_v2",
None,
)
.await;
let source = format!("s3://{}/test_object", bucket1);
let target = format!("s3://{}/test_object", bucket2);
let stats = cp_with_annotations(&source, &target, &["--source-version-id", &v1]).await;
assert_eq!(stats.sync_complete, 1);
assert_eq!(stats.sync_error, 0);
assert_eq!(
helper
.get_object_annotation(&bucket2, "test_object", None, "test_annotation_name1")
.await,
"value_for_v1"
);
helper.delete_bucket_with_cascade(&bucket1).await;
helper.delete_bucket_with_cascade(&bucket2).await;
}
#[tokio::test]
async fn cp_sync_annotations_disable_check_annotation_etag() {
TestHelper::init_dummy_tracing_subscriber();
let helper = TestHelper::new().await;
let bucket1 = TestHelper::generate_bucket_name();
let bucket2 = TestHelper::generate_bucket_name();
helper.create_bucket(&bucket1, REGION).await;
helper.create_bucket(&bucket2, REGION).await;
helper
.put_object(&bucket1, "test_object", b"test_object_content".to_vec())
.await;
helper
.put_object_annotation(
&bucket1,
"test_object",
None,
"test_annotation_name1",
"test_annotation_value1",
None,
)
.await;
let source = format!("s3://{}/test_object", bucket1);
let target = format!("s3://{}/test_object", bucket2);
let stats = cp_with_annotations(&source, &target, &[]).await;
assert_eq!(stats.sync_error, 0);
helper
.put_object_annotation(
&bucket1,
"test_object",
None,
"test_annotation_name1",
"test_annotation_value1_modified",
None,
)
.await;
let stats =
cp_with_annotations(&source, &target, &["--disable-check-annotation-etag"]).await;
assert_eq!(stats.sync_complete, 1);
assert_eq!(stats.sync_error, 0);
assert_eq!(
helper
.get_object_annotation(&bucket2, "test_object", None, "test_annotation_name1")
.await,
"test_annotation_value1_modified"
);
helper.delete_bucket_with_cascade(&bucket1).await;
helper.delete_bucket_with_cascade(&bucket2).await;
}
#[tokio::test]
async fn cp_sync_annotations_overwrite_existing_target() {
TestHelper::init_dummy_tracing_subscriber();
let helper = TestHelper::new().await;
let bucket1 = TestHelper::generate_bucket_name();
let bucket2 = TestHelper::generate_bucket_name();
helper.create_bucket(&bucket1, REGION).await;
helper.create_bucket(&bucket2, REGION).await;
helper
.put_object(&bucket1, "test_object", b"test_object_content".to_vec())
.await;
helper
.put_object_annotation(
&bucket1,
"test_object",
None,
"test_annotation_name1",
"test_annotation_value1",
None,
)
.await;
helper
.put_object_annotation(
&bucket1,
"test_object",
None,
"test_annotation_name2",
"test_annotation_value2",
None,
)
.await;
let source = format!("s3://{}/test_object", bucket1);
let target = format!("s3://{}/test_object", bucket2);
let stats = cp_with_annotations(&source, &target, &[]).await;
assert_eq!(stats.sync_error, 0);
helper
.delete_object_annotation(&bucket1, "test_object", None, "test_annotation_name2")
.await;
helper
.put_object_annotation(
&bucket1,
"test_object",
None,
"test_annotation_name_added",
"test_annotation_value_added",
None,
)
.await;
let stats = cp_with_annotations(&source, &target, &[]).await;
assert_eq!(stats.sync_complete, 1);
assert_eq!(stats.sync_error, 0);
assert_eq!(
helper
.get_object_annotation(&bucket2, "test_object", None, "test_annotation_name1")
.await,
"test_annotation_value1"
);
assert_eq!(
helper
.get_object_annotation(&bucket2, "test_object", None, "test_annotation_name_added")
.await,
"test_annotation_value_added"
);
assert!(
!helper
.is_object_annotation_exist(&bucket2, "test_object", None, "test_annotation_name2")
.await
);
helper.delete_bucket_with_cascade(&bucket1).await;
helper.delete_bucket_with_cascade(&bucket2).await;
}
#[tokio::test]
async fn mv_sync_annotations() {
TestHelper::init_dummy_tracing_subscriber();
let helper = TestHelper::new().await;
let bucket1 = TestHelper::generate_bucket_name();
let bucket2 = TestHelper::generate_bucket_name();
helper.create_bucket(&bucket1, REGION).await;
helper.create_bucket(&bucket2, REGION).await;
helper
.put_object(&bucket1, "test_object", b"test_object_content".to_vec())
.await;
helper
.put_object_annotation(
&bucket1,
"test_object",
None,
"test_annotation_name1",
"test_annotation_value1",
None,
)
.await;
helper
.put_object_annotation(
&bucket1,
"test_object",
None,
"test_annotation_name2",
"test_annotation_value2",
None,
)
.await;
let source = format!("s3://{}/test_object", bucket1);
let target = format!("s3://{}/test_object", bucket2);
let output = run_s3util(&[
"mv",
"--source-profile",
"s3util-e2e-test",
"--target-profile",
"s3util-e2e-test",
"--enable-sync-object-annotations",
&source,
&target,
]);
assert_eq!(
output.status.code(),
Some(EXIT_CODE_SUCCESS),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(
helper
.get_object_annotation(&bucket2, "test_object", None, "test_annotation_name1")
.await,
"test_annotation_value1"
);
assert_eq!(
helper
.get_object_annotation(&bucket2, "test_object", None, "test_annotation_name2")
.await,
"test_annotation_value2"
);
assert!(!helper.is_object_exist(&bucket1, "test_object", None).await);
helper.delete_bucket_with_cascade(&bucket1).await;
helper.delete_bucket_with_cascade(&bucket2).await;
}
}