#![cfg(e2e_test)]
mod common;
use std::io::Read;
use common::{REGION, TestHelper, generate_bucket_name, run, s7cmd_cmd};
const PROFILE: &str = "s7cmd-e2e-test";
fn http_get(url: &str) -> (u16, Vec<u8>) {
match ureq::get(url).call() {
Ok(resp) => {
let status = resp.status();
let mut body = Vec::new();
resp.into_reader()
.read_to_end(&mut body)
.expect("read presigned-URL body");
(status, body)
}
Err(ureq::Error::Status(code, _)) => (code, Vec::new()),
Err(e) => panic!("HTTP transport error fetching presigned URL: {e}"),
}
}
#[tokio::test]
async fn presign_get_returns_url_that_downloads_object_body() {
let helper = TestHelper::new().await;
let bucket = generate_bucket_name();
helper.create_bucket(&bucket, REGION).await;
let key = "presign-test.txt";
let body = b"hello presigned world".to_vec();
helper.put_object(&bucket, key, body.clone()).await;
let object_arg = format!("s3://{bucket}/{key}");
let (code, stdout, stderr) = run(s7cmd_cmd().args([
"presign",
"--target-profile",
PROFILE,
"--target-region",
REGION,
&object_arg,
]));
let url = stdout.trim().to_string();
let success = code == Some(0);
let (http_status, fetched_body) = if success {
http_get(&url)
} else {
(0, Vec::new())
};
helper.delete_bucket_with_cascade(&bucket).await;
assert!(success, "presign should succeed; stderr: {stderr}");
assert_eq!(code, Some(0));
assert!(
url.starts_with("https://"),
"presigned URL must be HTTPS; got: {url}"
);
assert!(
url.contains(&bucket),
"presigned URL must contain the bucket name; got: {url}"
);
assert!(
url.contains(key),
"presigned URL must contain the key; got: {url}"
);
assert!(
url.contains("X-Amz-Signature="),
"presigned URL must carry a SigV4 signature; got: {url}"
);
assert!(
url.contains("X-Amz-Expires=3600"),
"default --expires-in must be 3600; got: {url}"
);
assert_eq!(http_status, 200, "GET on presigned URL must return 200");
assert_eq!(
fetched_body, body,
"presigned URL must return the original object body"
);
}
#[tokio::test]
async fn presign_expires_in_propagates_to_url_query() {
let helper = TestHelper::new().await;
let bucket = generate_bucket_name();
helper.create_bucket(&bucket, REGION).await;
let key = "expires-test.txt";
helper.put_object(&bucket, key, b"x".to_vec()).await;
let object_arg = format!("s3://{bucket}/{key}");
let (code, stdout, stderr) = run(s7cmd_cmd().args([
"presign",
"--target-profile",
PROFILE,
"--target-region",
REGION,
"--expires-in",
"120",
&object_arg,
]));
helper.delete_bucket_with_cascade(&bucket).await;
assert_eq!(code, Some(0), "presign should succeed; stderr: {stderr}");
let url = stdout.trim().to_string();
assert!(
url.contains("X-Amz-Expires=120"),
"presigned URL must carry the requested --expires-in; got: {url}"
);
}
#[tokio::test]
async fn presign_for_missing_key_succeeds_then_get_404s() {
let helper = TestHelper::new().await;
let bucket = generate_bucket_name();
helper.create_bucket(&bucket, REGION).await;
let object_arg = format!("s3://{bucket}/nonexistent-key");
let (code, stdout, stderr) = run(s7cmd_cmd().args([
"presign",
"--target-profile",
PROFILE,
"--target-region",
REGION,
&object_arg,
]));
let url = stdout.trim().to_string();
let success = code == Some(0);
let http_status = if success { http_get(&url).0 } else { 0 };
helper.delete_bucket_with_cascade(&bucket).await;
assert!(
success,
"presign for missing key must succeed (signing is local); stderr: {stderr}"
);
assert_eq!(
http_status, 404,
"fetching presigned URL for a missing key must return 404"
);
}
#[tokio::test]
async fn presign_for_missing_bucket_succeeds_then_get_404s() {
let nonexistent = format!("s7cmd-nonexistent-{}", uuid::Uuid::new_v4());
let object_arg = format!("s3://{nonexistent}/key");
let (code, stdout, stderr) = run(s7cmd_cmd().args([
"presign",
"--target-profile",
PROFILE,
"--target-region",
REGION,
&object_arg,
]));
assert_eq!(
code,
Some(0),
"presign for missing bucket must succeed (signing is local); stderr: {stderr}"
);
let url = stdout.trim().to_string();
let (http_status, _) = http_get(&url);
assert_eq!(
http_status, 404,
"fetching presigned URL for missing bucket must return 404"
);
}
#[tokio::test]
async fn presign_short_expiry_url_rejected_after_window() {
let helper = TestHelper::new().await;
let bucket = generate_bucket_name();
helper.create_bucket(&bucket, REGION).await;
let key = "short-expiry.txt";
helper.put_object(&bucket, key, b"transient".to_vec()).await;
let object_arg = format!("s3://{bucket}/{key}");
let (code, stdout, stderr) = run(s7cmd_cmd().args([
"presign",
"--target-profile",
PROFILE,
"--target-region",
REGION,
"--expires-in",
"1",
&object_arg,
]));
let success = code == Some(0);
let url = stdout.trim().to_string();
tokio::time::sleep(std::time::Duration::from_secs(3)).await;
let http_status = if success { http_get(&url).0 } else { 0 };
helper.delete_bucket_with_cascade(&bucket).await;
assert!(success, "presign should succeed; stderr: {stderr}");
assert_eq!(
http_status, 403,
"expired presigned URL must return 403 AccessDenied; got: {http_status}"
);
}
#[tokio::test]
async fn presign_bucket_only_target_exits_1() {
let bucket = format!("s7cmd-nonexistent-{}", uuid::Uuid::new_v4());
let object_arg = format!("s3://{bucket}");
let (code, _stdout, _stderr) = run(s7cmd_cmd().args([
"presign",
"--target-profile",
PROFILE,
"--target-region",
REGION,
&object_arg,
]));
assert_eq!(code, Some(1), "bucket-only path should exit 1 (validation)");
}