use std::process::{Command, Stdio};
fn s7cmd() -> Command {
Command::new(env!("CARGO_BIN_EXE_s7cmd"))
}
fn run(cmd: &mut Command) -> (bool, String, String, Option<i32>) {
let output = cmd
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.expect("failed to spawn s7cmd binary");
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
(
output.status.success(),
stdout,
stderr,
output.status.code(),
)
}
const FAKE_BUCKET: &str = "s3://nonexistent-bucket-for-s7cmd-dry-run-tests";
#[test]
fn cp_dry_run_exits_zero_and_logs_message() {
let (ok, _stdout, stderr, code) = run(s7cmd().args([
"cp",
"--dry-run",
&format!("{FAKE_BUCKET}/src"),
&format!("{FAKE_BUCKET}/dst"),
]));
assert!(ok, "cp --dry-run must exit 0; stderr={stderr}");
assert_eq!(code, Some(0));
assert!(stderr.contains("[dry-run]"));
assert!(stderr.contains("would copy"));
}
#[test]
fn mv_dry_run_exits_zero_and_logs_messages() {
let (ok, _stdout, stderr, code) = run(s7cmd().args([
"mv",
"--dry-run",
&format!("{FAKE_BUCKET}/src"),
&format!("{FAKE_BUCKET}/dst"),
]));
assert!(ok, "mv --dry-run must exit 0; stderr={stderr}");
assert_eq!(code, Some(0));
assert!(stderr.contains("[dry-run]"));
assert!(stderr.contains("would copy"));
assert!(stderr.contains("would delete source object"));
}
#[test]
fn rm_dry_run_exits_zero_and_logs_message() {
let (ok, _stdout, stderr, code) =
run(s7cmd().args(["rm", "--dry-run", &format!("{FAKE_BUCKET}/key")]));
assert!(ok, "rm --dry-run must exit 0; stderr={stderr}");
assert_eq!(code, Some(0));
assert!(stderr.contains("[dry-run]"));
assert!(stderr.contains("would delete object"));
}
#[test]
fn create_bucket_dry_run_exits_zero_and_logs_message() {
let (ok, _stdout, stderr, code) =
run(s7cmd().args(["create-bucket", "--dry-run", FAKE_BUCKET]));
assert!(ok, "create-bucket --dry-run must exit 0; stderr={stderr}");
assert_eq!(code, Some(0));
assert!(stderr.contains("[dry-run]"));
assert!(stderr.contains("would create bucket"));
}
#[test]
fn create_bucket_dry_run_with_tagging_logs_both_lines() {
let (ok, _stdout, stderr, _code) = run(s7cmd().args([
"create-bucket",
"--dry-run",
"--tagging",
"env=test&team=sre",
FAKE_BUCKET,
]));
assert!(ok);
assert!(
stderr.contains("would create bucket"),
"missing 'would create bucket': {stderr}"
);
assert!(
stderr.contains("would put bucket tagging"),
"missing 'would put bucket tagging': {stderr}"
);
}
#[test]
fn put_bucket_cors_dry_run_exits_zero_and_logs_message() {
let cfg = tempfile::NamedTempFile::new().unwrap();
std::fs::write(cfg.path(), r#"{"CORSRules":[]}"#).unwrap();
let (ok, _stdout, stderr, code) = run(s7cmd().args([
"put-bucket-cors",
"--dry-run",
FAKE_BUCKET,
cfg.path().to_str().unwrap(),
]));
assert!(ok, "put-bucket-cors --dry-run must exit 0; stderr={stderr}");
assert_eq!(code, Some(0));
assert!(stderr.contains("[dry-run]"));
assert!(stderr.contains("would put bucket CORS"));
}
#[test]
fn put_bucket_cors_dry_run_validates_json_before_short_circuit() {
let cfg = tempfile::NamedTempFile::new().unwrap();
std::fs::write(cfg.path(), b"not valid json {").unwrap();
let (ok, _stdout, stderr, _code) = run(s7cmd().args([
"put-bucket-cors",
"--dry-run",
FAKE_BUCKET,
cfg.path().to_str().unwrap(),
]));
assert!(!ok, "malformed JSON must still fail under --dry-run");
assert!(
stderr.to_lowercase().contains("json") || stderr.to_lowercase().contains("parse"),
"expected JSON parse error; got: {stderr}"
);
}
#[test]
fn delete_bucket_cors_dry_run_exits_zero_and_logs_message() {
let (ok, _stdout, stderr, code) =
run(s7cmd().args(["delete-bucket-cors", "--dry-run", FAKE_BUCKET]));
assert!(ok, "stderr={stderr}");
assert_eq!(code, Some(0));
assert!(stderr.contains("[dry-run]"));
assert!(stderr.contains("would delete bucket CORS"));
}
const FAKE_OBJECT: &str = "s3://nonexistent-bucket-for-s7cmd-dry-run-tests/key";
fn write_json(body: &str) -> tempfile::NamedTempFile {
let tmp = tempfile::NamedTempFile::new().unwrap();
std::fs::write(tmp.path(), body).unwrap();
tmp
}
fn assert_dry_run(stderr: &str, ok: bool, code: Option<i32>, phrase: &str) {
assert!(ok, "stderr: {stderr}");
assert_eq!(code, Some(0));
assert!(
stderr.contains("[dry-run]"),
"missing [dry-run] in stderr: {stderr}"
);
assert!(
stderr.contains(phrase),
"stderr missing '{phrase}': {stderr}"
);
}
#[test]
fn delete_bucket_dry_run_smoke() {
let (ok, _, stderr, code) = run(s7cmd().args(["delete-bucket", "--dry-run", FAKE_BUCKET]));
assert_dry_run(&stderr, ok, code, "would delete bucket");
}
#[test]
fn delete_bucket_encryption_dry_run_smoke() {
let (ok, _, stderr, code) =
run(s7cmd().args(["delete-bucket-encryption", "--dry-run", FAKE_BUCKET]));
assert_dry_run(&stderr, ok, code, "would delete bucket encryption");
}
#[test]
fn delete_bucket_lifecycle_configuration_dry_run_smoke() {
let (ok, _, stderr, code) = run(s7cmd().args([
"delete-bucket-lifecycle-configuration",
"--dry-run",
FAKE_BUCKET,
]));
assert_dry_run(&stderr, ok, code, "would delete bucket lifecycle");
}
#[test]
fn delete_bucket_policy_dry_run_smoke() {
let (ok, _, stderr, code) =
run(s7cmd().args(["delete-bucket-policy", "--dry-run", FAKE_BUCKET]));
assert_dry_run(&stderr, ok, code, "would delete bucket policy");
}
#[test]
fn delete_bucket_tagging_dry_run_smoke() {
let (ok, _, stderr, code) =
run(s7cmd().args(["delete-bucket-tagging", "--dry-run", FAKE_BUCKET]));
assert_dry_run(&stderr, ok, code, "would delete bucket tagging");
}
#[test]
fn delete_bucket_website_dry_run_smoke() {
let (ok, _, stderr, code) =
run(s7cmd().args(["delete-bucket-website", "--dry-run", FAKE_BUCKET]));
assert_dry_run(&stderr, ok, code, "would delete bucket website");
}
#[test]
fn delete_object_tagging_dry_run_smoke() {
let (ok, _, stderr, code) =
run(s7cmd().args(["delete-object-tagging", "--dry-run", FAKE_OBJECT]));
assert_dry_run(&stderr, ok, code, "would delete object tagging");
}
#[test]
fn delete_object_tagging_dry_run_with_version_id_includes_version_field() {
let (ok, _, stderr, code) = run(s7cmd().args([
"delete-object-tagging",
"--dry-run",
"--source-version-id",
"abc123",
FAKE_OBJECT,
]));
assert_dry_run(&stderr, ok, code, "would delete object tagging");
assert!(
stderr.contains("abc123"),
"version_id should appear in dry-run log: {stderr}"
);
}
#[test]
fn delete_public_access_block_dry_run_smoke() {
let (ok, _, stderr, code) =
run(s7cmd().args(["delete-public-access-block", "--dry-run", FAKE_BUCKET]));
assert_dry_run(&stderr, ok, code, "would delete public access block");
}
#[test]
fn put_bucket_encryption_dry_run_smoke() {
let cfg = write_json(r#"{"Rules":[]}"#);
let (ok, _, stderr, code) = run(s7cmd().args([
"put-bucket-encryption",
"--dry-run",
FAKE_BUCKET,
cfg.path().to_str().unwrap(),
]));
assert_dry_run(&stderr, ok, code, "would put bucket encryption");
}
#[test]
fn put_bucket_lifecycle_configuration_dry_run_smoke() {
let cfg = write_json(r#"{"Rules":[]}"#);
let (ok, _, stderr, code) = run(s7cmd().args([
"put-bucket-lifecycle-configuration",
"--dry-run",
FAKE_BUCKET,
cfg.path().to_str().unwrap(),
]));
assert_dry_run(&stderr, ok, code, "would put bucket lifecycle");
}
#[test]
fn put_bucket_logging_dry_run_smoke() {
let cfg = write_json("{}");
let (ok, _, stderr, code) = run(s7cmd().args([
"put-bucket-logging",
"--dry-run",
FAKE_BUCKET,
cfg.path().to_str().unwrap(),
]));
assert_dry_run(&stderr, ok, code, "would put bucket logging");
}
#[test]
fn put_bucket_notification_configuration_dry_run_smoke() {
let cfg = write_json("{}");
let (ok, _, stderr, code) = run(s7cmd().args([
"put-bucket-notification-configuration",
"--dry-run",
FAKE_BUCKET,
cfg.path().to_str().unwrap(),
]));
assert_dry_run(&stderr, ok, code, "would put bucket notification");
}
#[test]
fn put_bucket_policy_dry_run_smoke() {
let cfg = write_json(r#"{"Version":"2012-10-17","Statement":[]}"#);
let (ok, _, stderr, code) = run(s7cmd().args([
"put-bucket-policy",
"--dry-run",
FAKE_BUCKET,
cfg.path().to_str().unwrap(),
]));
assert_dry_run(&stderr, ok, code, "would put bucket policy");
}
#[test]
fn put_bucket_tagging_dry_run_smoke() {
let (ok, _, stderr, code) = run(s7cmd().args([
"put-bucket-tagging",
"--dry-run",
FAKE_BUCKET,
"--tagging",
"env=test",
]));
assert_dry_run(&stderr, ok, code, "would put bucket tagging");
}
#[test]
fn put_bucket_versioning_enabled_dry_run_smoke() {
let (ok, _, stderr, code) = run(s7cmd().args([
"put-bucket-versioning",
"--dry-run",
"--enabled",
FAKE_BUCKET,
]));
assert_dry_run(&stderr, ok, code, "would put bucket versioning");
assert!(stderr.contains("Enabled"), "stderr: {stderr}");
}
#[test]
fn put_bucket_versioning_suspended_dry_run_smoke() {
let (ok, _, stderr, code) = run(s7cmd().args([
"put-bucket-versioning",
"--dry-run",
"--suspended",
FAKE_BUCKET,
]));
assert_dry_run(&stderr, ok, code, "would put bucket versioning");
assert!(stderr.contains("Suspended"), "stderr: {stderr}");
}
#[test]
fn put_bucket_website_dry_run_smoke() {
let cfg = write_json("{}");
let (ok, _, stderr, code) = run(s7cmd().args([
"put-bucket-website",
"--dry-run",
FAKE_BUCKET,
cfg.path().to_str().unwrap(),
]));
assert_dry_run(&stderr, ok, code, "would put bucket website");
}
#[test]
fn put_object_tagging_dry_run_smoke() {
let (ok, _, stderr, code) = run(s7cmd().args([
"put-object-tagging",
"--dry-run",
FAKE_OBJECT,
"--tagging",
"env=test",
]));
assert_dry_run(&stderr, ok, code, "would put object tagging");
}
#[test]
fn put_object_tagging_dry_run_with_version_id_includes_version_field() {
let (ok, _, stderr, code) = run(s7cmd().args([
"put-object-tagging",
"--dry-run",
FAKE_OBJECT,
"--tagging",
"env=test",
"--source-version-id",
"ver1",
]));
assert_dry_run(&stderr, ok, code, "would put object tagging");
assert!(
stderr.contains("ver1"),
"version_id should appear in dry-run log: {stderr}"
);
}
#[test]
fn put_public_access_block_dry_run_smoke() {
let cfg = write_json("{}");
let (ok, _, stderr, code) = run(s7cmd().args([
"put-public-access-block",
"--dry-run",
FAKE_BUCKET,
cfg.path().to_str().unwrap(),
]));
assert_dry_run(&stderr, ok, code, "would put public access block");
}
#[test]
fn rm_dry_run_with_version_id_includes_version_field() {
let (ok, _, stderr, code) =
run(s7cmd().args(["rm", "--dry-run", "--source-version-id", "v42", FAKE_OBJECT]));
assert_dry_run(&stderr, ok, code, "would delete object");
assert!(
stderr.contains("v42"),
"version_id must appear in dry-run log: {stderr}"
);
}
#[test]
fn get_bucket_cors_help_does_not_expose_dry_run() {
let (ok, stdout, _stderr, _code) = run(s7cmd().args(["get-bucket-cors", "--help"]));
assert!(ok);
assert!(
!stdout.contains("--dry-run"),
"get-* must not expose --dry-run; help: {stdout}"
);
}
#[test]
fn head_bucket_help_does_not_expose_dry_run() {
let (ok, stdout, _stderr, _code) = run(s7cmd().args(["head-bucket", "--help"]));
assert!(ok);
assert!(
!stdout.contains("--dry-run"),
"head-* must not expose --dry-run; help: {stdout}"
);
}
#[test]
fn head_object_help_does_not_expose_dry_run() {
let (ok, stdout, _stderr, _code) = run(s7cmd().args(["head-object", "--help"]));
assert!(ok);
assert!(!stdout.contains("--dry-run"));
}
#[test]
fn cp_help_lists_dry_run() {
let (ok, stdout, _stderr, _code) = run(s7cmd().args(["cp", "--help"]));
assert!(ok);
assert!(stdout.contains("--dry-run"), "cp --help missing --dry-run");
}
#[test]
fn rm_help_lists_dry_run() {
let (ok, stdout, _stderr, _code) = run(s7cmd().args(["rm", "--help"]));
assert!(ok);
assert!(stdout.contains("--dry-run"));
}
#[test]
fn delete_bucket_help_lists_dry_run() {
let (ok, stdout, _stderr, _code) = run(s7cmd().args(["delete-bucket", "--help"]));
assert!(ok);
assert!(stdout.contains("--dry-run"));
}