#![cfg(feature = "server")]
mod common;
use common::setup_test_server;
#[tokio::test]
async fn test_empty_body_roundtrip() {
let (client, _temp_dir, _server) = setup_test_server().await;
client
.create_bucket()
.bucket("empty-body-test")
.send()
.await
.expect("create bucket");
client
.put_object()
.bucket("empty-body-test")
.key("empty.bin")
.body(vec![].into())
.send()
.await
.expect("PUT 0-byte");
let got = client
.get_object()
.bucket("empty-body-test")
.key("empty.bin")
.send()
.await
.expect("GET empty");
let returned = got.body.collect().await.expect("collect").into_bytes();
assert_eq!(
returned.len(),
0,
"GET of 0-byte object should return empty body"
);
}
#[tokio::test]
async fn test_content_type_roundtrip() {
let (client, _temp_dir, _server) = setup_test_server().await;
client
.create_bucket()
.bucket("ct-roundtrip-test")
.send()
.await
.expect("create bucket");
let ct = "application/x-custom-type";
client
.put_object()
.bucket("ct-roundtrip-test")
.key("typed.bin")
.content_type(ct)
.body(b"ct test".to_vec().into())
.send()
.await
.expect("PUT");
let head = client
.head_object()
.bucket("ct-roundtrip-test")
.key("typed.bin")
.send()
.await
.expect("HEAD");
assert_eq!(head.content_type(), Some(ct), "HEAD content-type mismatch");
let get = client
.get_object()
.bucket("ct-roundtrip-test")
.key("typed.bin")
.send()
.await
.expect("GET");
assert_eq!(get.content_type(), Some(ct), "GET content-type mismatch");
}
#[tokio::test]
async fn test_user_metadata_roundtrip() {
let (client, _temp_dir, _server) = setup_test_server().await;
client
.create_bucket()
.bucket("meta-rt-test")
.send()
.await
.expect("create bucket");
client
.put_object()
.bucket("meta-rt-test")
.key("m.txt")
.metadata("custom", "val-42")
.body(b"x".to_vec().into())
.send()
.await
.expect("PUT");
let head = client
.head_object()
.bucket("meta-rt-test")
.key("m.txt")
.send()
.await
.expect("HEAD");
assert_eq!(
head.metadata().expect("meta").get("custom"),
Some(&"val-42".to_string()),
"HEAD should return user metadata"
);
let get = client
.get_object()
.bucket("meta-rt-test")
.key("m.txt")
.send()
.await
.expect("GET");
assert_eq!(
get.metadata().expect("meta").get("custom"),
Some(&"val-42".to_string()),
"GET should return user metadata"
);
}
#[tokio::test]
async fn test_overwrite_object() {
let (client, _temp_dir, _server) = setup_test_server().await;
client
.create_bucket()
.bucket("overwrite-test")
.send()
.await
.expect("create bucket");
client
.put_object()
.bucket("overwrite-test")
.key("ow.txt")
.body(b"v1".to_vec().into())
.send()
.await
.expect("first PUT");
client
.put_object()
.bucket("overwrite-test")
.key("ow.txt")
.body(b"v2".to_vec().into())
.send()
.await
.expect("second PUT");
let got = client
.get_object()
.bucket("overwrite-test")
.key("ow.txt")
.send()
.await
.expect("GET after overwrite");
let body = got.body.collect().await.expect("collect").into_bytes();
assert_eq!(
body.as_ref(),
b"v2",
"GET should return second version after overwrite"
);
}
#[tokio::test]
async fn test_head_object_conditional() {
let (client, _temp_dir, server) = setup_test_server().await;
let bucket_name = format!("head-conditional-{}", uuid::Uuid::new_v4());
client
.create_bucket()
.bucket(&bucket_name)
.send()
.await
.unwrap();
let content = b"test content for conditional head";
client
.put_object()
.bucket(&bucket_name)
.key("test.txt")
.body(aws_sdk_s3::primitives::ByteStream::from_static(content))
.send()
.await
.unwrap();
let head_result = client
.head_object()
.bucket(&bucket_name)
.key("test.txt")
.send()
.await
.unwrap();
let etag = head_result.e_tag().unwrap();
let result = client
.head_object()
.bucket(&bucket_name)
.key("test.txt")
.if_match(etag)
.send()
.await;
assert!(result.is_ok(), "HEAD with matching If-Match should succeed");
let result = client
.head_object()
.bucket(&bucket_name)
.key("test.txt")
.if_match("\"wrongetag\"")
.send()
.await;
assert!(
result.is_err(),
"HEAD with non-matching If-Match should fail"
);
let result = client
.head_object()
.bucket(&bucket_name)
.key("test.txt")
.if_none_match("\"differentetag\"")
.send()
.await;
assert!(
result.is_ok(),
"HEAD with non-matching If-None-Match should succeed"
);
let result = client
.head_object()
.bucket(&bucket_name)
.key("test.txt")
.if_none_match(etag)
.send()
.await;
assert!(
result.is_err(),
"HEAD with matching If-None-Match should return 304"
);
let http_client = reqwest::Client::new();
let base_url = format!("http://{}", server.addr);
let response = http_client
.head(format!("{}/{}/test.txt", base_url, bucket_name))
.header("If-Match", "\"wrongetag\"")
.send()
.await
.unwrap();
assert_eq!(
response.status(),
412,
"If-Match with wrong ETag should return 412"
);
let response = http_client
.head(format!("{}/{}/test.txt", base_url, bucket_name))
.header("If-None-Match", etag)
.send()
.await
.unwrap();
assert_eq!(
response.status(),
304,
"If-None-Match with same ETag should return 304"
);
}
#[tokio::test]
async fn test_put_object_streaming_zero_bytes() {
let (_client, _temp_dir, server) = setup_test_server().await;
let http = reqwest::Client::new();
let base = &server.base_url;
let bucket = "stream-zero-test";
let resp = http
.put(format!("{}/{}", base, bucket))
.send()
.await
.expect("create bucket");
assert!(resp.status().is_success(), "bucket creation failed");
let resp = http
.put(format!("{}/{}/empty.bin", base, bucket))
.body(Vec::<u8>::new())
.send()
.await
.expect("PUT empty");
assert_eq!(resp.status().as_u16(), 200, "PUT 0-byte should return 200");
assert!(
resp.headers().contains_key("etag"),
"PUT 0-byte should return ETag"
);
}
#[tokio::test]
async fn test_put_object_streaming_small() {
let (_client, _temp_dir, server) = setup_test_server().await;
let http = reqwest::Client::new();
let base = &server.base_url;
let bucket = "stream-small-test";
let resp = http
.put(format!("{}/{}", base, bucket))
.send()
.await
.expect("create bucket");
assert!(resp.status().is_success());
let resp = http
.put(format!("{}/{}/tiny.bin", base, bucket))
.body(vec![0x42u8])
.send()
.await
.expect("PUT 1-byte");
assert_eq!(resp.status().as_u16(), 200, "PUT 1-byte should return 200");
let resp = http
.get(format!("{}/{}/tiny.bin", base, bucket))
.send()
.await
.expect("GET 1-byte");
assert_eq!(resp.status().as_u16(), 200);
let body = resp.bytes().await.expect("bytes");
assert_eq!(body.as_ref(), &[0x42u8], "GET body should match PUT body");
}
#[tokio::test]
async fn test_put_object_streaming_medium() {
let (_client, _temp_dir, server) = setup_test_server().await;
let http = reqwest::Client::new();
let base = &server.base_url;
let bucket = "stream-medium-test";
let resp = http
.put(format!("{}/{}", base, bucket))
.send()
.await
.expect("create bucket");
assert!(resp.status().is_success());
let size = 10 * 1024 * 1024;
let data = vec![0xABu8; size];
let resp = http
.put(format!("{}/{}/medium.bin", base, bucket))
.body(data.clone())
.send()
.await
.expect("PUT 10MB");
assert_eq!(resp.status().as_u16(), 200, "PUT 10MB should return 200");
let resp = http
.get(format!("{}/{}/medium.bin", base, bucket))
.send()
.await
.expect("GET 10MB");
assert_eq!(resp.status().as_u16(), 200);
let body = resp.bytes().await.expect("bytes");
assert_eq!(body.len(), size, "GET should return exactly 10MB");
assert!(
body.iter().all(|&b| b == 0xAB),
"GET data should match PUT data"
);
}
#[tokio::test]
async fn test_put_object_streaming_with_metadata() {
let (_client, _temp_dir, server) = setup_test_server().await;
let http = reqwest::Client::new();
let base = &server.base_url;
let bucket = "stream-meta-test";
let resp = http
.put(format!("{}/{}", base, bucket))
.send()
.await
.expect("create bucket");
assert!(resp.status().is_success());
let resp = http
.put(format!("{}/{}/meta.txt", base, bucket))
.header("x-amz-meta-custom-key", "custom-value-123")
.header("x-amz-meta-another", "another-value")
.body("metadata test body")
.send()
.await
.expect("PUT with metadata");
assert_eq!(resp.status().as_u16(), 200);
let resp = http
.head(format!("{}/{}/meta.txt", base, bucket))
.send()
.await
.expect("HEAD");
assert_eq!(resp.status().as_u16(), 200);
assert_eq!(
resp.headers()
.get("x-amz-meta-custom-key")
.and_then(|v| v.to_str().ok()),
Some("custom-value-123"),
"custom metadata should be preserved"
);
assert_eq!(
resp.headers()
.get("x-amz-meta-another")
.and_then(|v| v.to_str().ok()),
Some("another-value"),
"second metadata key should be preserved"
);
}
#[tokio::test]
async fn test_put_object_streaming_content_type() {
let (_client, _temp_dir, server) = setup_test_server().await;
let http = reqwest::Client::new();
let base = &server.base_url;
let bucket = "stream-ct-test";
let resp = http
.put(format!("{}/{}", base, bucket))
.send()
.await
.expect("create bucket");
assert!(resp.status().is_success());
let resp = http
.put(format!("{}/{}/typed.json", base, bucket))
.header("Content-Type", "application/json")
.body(r#"{"key": "value"}"#)
.send()
.await
.expect("PUT with content-type");
assert_eq!(resp.status().as_u16(), 200);
let resp = http
.head(format!("{}/{}/typed.json", base, bucket))
.send()
.await
.expect("HEAD");
assert_eq!(resp.status().as_u16(), 200);
let ct = resp
.headers()
.get("content-type")
.and_then(|v| v.to_str().ok());
assert_eq!(
ct,
Some("application/json"),
"content-type should be preserved"
);
}