#![cfg(e2e_test)]
mod common;
use common::{REGION, TestHelper, create_sized_file, create_temp_dir, generate_bucket_name};
use std::process::{Command, Stdio};
const EXIT_CODE_SUCCESS: i32 = 0;
const EXIT_CODE_ERROR: i32 = 1;
const EXIT_CODE_WARNING: i32 = 3;
const EXIT_CODE_CLAP_ARG_ERROR: i32 = 2;
#[tokio::test]
async fn exit_code_success_on_normal_cp() {
let helper = TestHelper::new().await;
let bucket = generate_bucket_name();
helper.create_bucket(&bucket, REGION).await;
let local_dir = create_temp_dir();
let test_file = create_sized_file(&local_dir, "ok.bin", 1024);
let target = format!("s3://{}/ok.bin", bucket);
let status = std::process::Command::new("cargo")
.args([
"run",
"--quiet",
"--",
"cp",
"--target-profile",
"s7cmd-e2e-test",
test_file.to_str().unwrap(),
&target,
])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.unwrap();
assert_eq!(
status.code(),
Some(EXIT_CODE_SUCCESS),
"successful cp must exit 0, got: {status}"
);
helper.delete_bucket_with_cascade(&bucket).await;
let _ = std::fs::remove_dir_all(&local_dir);
}
#[tokio::test]
async fn exit_code_error_on_missing_bucket() {
let bucket = format!("nonexistent-{}", uuid::Uuid::new_v4());
let local_dir = create_temp_dir();
let test_file = create_sized_file(&local_dir, "err.bin", 1024);
let target = format!("s3://{}/err.bin", bucket);
let status = std::process::Command::new("cargo")
.args([
"run",
"--quiet",
"--",
"cp",
"--target-profile",
"s7cmd-e2e-test",
test_file.to_str().unwrap(),
&target,
])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.unwrap();
assert_eq!(
status.code(),
Some(EXIT_CODE_ERROR),
"cp to nonexistent bucket must exit 1, got: {status}"
);
let _ = std::fs::remove_dir_all(&local_dir);
}
#[tokio::test]
async fn auto_complete_shell_emits_script_and_exits_zero() {
let output = Command::new(env!("CARGO_BIN_EXE_s7cmd"))
.args(["--auto-complete-shell", "bash"])
.stdout(Stdio::piped())
.stderr(Stdio::null())
.stdin(Stdio::null())
.output()
.unwrap();
assert_eq!(
output.status.code(),
Some(EXIT_CODE_SUCCESS),
"--auto-complete-shell must exit 0, got: {}",
output.status
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("s7cmd"),
"expected bash completion output mentioning 's7cmd', got first 200 chars: {}",
&stdout.chars().take(200).collect::<String>()
);
}
#[tokio::test]
async fn auto_complete_shell_zsh() {
let output = Command::new(env!("CARGO_BIN_EXE_s7cmd"))
.args(["--auto-complete-shell", "zsh"])
.stdout(Stdio::piped())
.stderr(Stdio::null())
.stdin(Stdio::null())
.output()
.unwrap();
assert_eq!(
output.status.code(),
Some(EXIT_CODE_SUCCESS),
"--auto-complete-shell zsh must exit 0, got: {}",
output.status
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("#compdef s7cmd"),
"expected zsh completion output containing '#compdef s7cmd', got first 200 chars: {}",
&stdout.chars().take(200).collect::<String>()
);
}
#[tokio::test]
async fn auto_complete_shell_fish() {
let output = Command::new(env!("CARGO_BIN_EXE_s7cmd"))
.args(["--auto-complete-shell", "fish"])
.stdout(Stdio::piped())
.stderr(Stdio::null())
.stdin(Stdio::null())
.output()
.unwrap();
assert_eq!(
output.status.code(),
Some(EXIT_CODE_SUCCESS),
"--auto-complete-shell fish must exit 0, got: {}",
output.status
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("complete -c s7cmd"),
"expected fish completion output containing 'complete -c s7cmd', got first 200 chars: {}",
&stdout.chars().take(200).collect::<String>()
);
}
#[tokio::test]
async fn invalid_multipart_threshold_exits_with_clap_error() {
let local_dir = create_temp_dir();
let test_file = create_sized_file(&local_dir, "x.bin", 64);
let status = std::process::Command::new("cargo")
.args([
"run",
"--quiet",
"--",
"cp",
"--multipart-threshold",
"1KiB",
test_file.to_str().unwrap(),
"s3://any-bucket/key",
])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.unwrap();
assert_eq!(
status.code(),
Some(EXIT_CODE_CLAP_ARG_ERROR),
"invalid --multipart-threshold must exit with clap's arg-error code ({EXIT_CODE_CLAP_ARG_ERROR}), got: {status}"
);
let _ = std::fs::remove_dir_all(&local_dir);
}
#[tokio::test]
async fn unknown_flag_exits_with_clap_arg_error() {
let status = std::process::Command::new("cargo")
.args([
"run",
"--quiet",
"--",
"cp",
"--this-flag-does-not-exist",
"local.txt",
"s3://any-bucket/key",
])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.unwrap();
assert_eq!(
status.code(),
Some(EXIT_CODE_CLAP_ARG_ERROR),
"unknown flag must exit with clap's arg-error code ({EXIT_CODE_CLAP_ARG_ERROR}), got: {status}"
);
}
#[tokio::test]
async fn exit_code_warning_on_etag_mismatch_after_chunksize_change() {
let helper = TestHelper::new().await;
let bucket = generate_bucket_name();
helper.create_bucket(&bucket, REGION).await;
let local_dir = create_temp_dir();
let upload_file = create_sized_file(&local_dir, "warn.bin", 9 * 1024 * 1024);
let s3_path = format!("s3://{}/warn.bin", bucket);
let upload_status = std::process::Command::new("cargo")
.args([
"run",
"--quiet",
"--",
"cp",
"--target-profile",
"s7cmd-e2e-test",
"--multipart-threshold",
"5MiB",
"--multipart-chunksize",
"5MiB",
upload_file.to_str().unwrap(),
&s3_path,
])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.unwrap();
assert_eq!(
upload_status.code(),
Some(EXIT_CODE_SUCCESS),
"warning-test setup upload must succeed first, got: {upload_status}"
);
let dl_file = local_dir.join("warn_dl.bin");
let dl_status = std::process::Command::new("cargo")
.args([
"run",
"--quiet",
"--",
"cp",
"--source-profile",
"s7cmd-e2e-test",
&s3_path,
dl_file.to_str().unwrap(),
])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.unwrap();
assert_eq!(
dl_status.code(),
Some(EXIT_CODE_WARNING),
"ETag mismatch from chunksize change must exit 3, got: {dl_status}"
);
helper.delete_bucket_with_cascade(&bucket).await;
let _ = std::fs::remove_dir_all(&local_dir);
}