Files
cli/sunbeam-sdk/tests/test_s3.rs
Sienna Meridian Satterwhite f06a167496 feat: BuildKit client + integration test suite (651 tests)
BuildKitClient CLI wrapper for buildctl.
Docker compose stack (9 services) for integration testing.
Comprehensive test suite: wiremock tests for Matrix/La Suite/S3/client,
integration tests for Kratos/Hydra/Gitea/OpenSearch/Prometheus/Loki/
Grafana/LiveKit.

Bump: sunbeam-sdk v0.12.0
2026-03-21 20:35:59 +00:00

876 lines
27 KiB
Rust

#![cfg(feature = "integration")]
mod helpers;
use helpers::*;
use sunbeam_sdk::client::{AuthMethod, ServiceClient};
use sunbeam_sdk::storage::S3Client;
#[allow(unused_imports)]
use sunbeam_sdk::storage::types::*;
use wiremock::matchers::{method, path, query_param};
use wiremock::{Mock, MockServer, ResponseTemplate};
// ---------------------------------------------------------------------------
// Helper: build an S3Client pointed at the mock server
// ---------------------------------------------------------------------------
fn mock_client(server: &MockServer) -> S3Client {
S3Client::from_parts(server.uri(), AuthMethod::None)
}
// ===========================================================================
// Health & connectivity (real MinIO)
// ===========================================================================
const MINIO_URL: &str = "http://localhost:9000";
const MINIO_HEALTH: &str = "http://localhost:9000/minio/health/live";
#[tokio::test]
async fn minio_is_healthy() {
wait_for_healthy(MINIO_HEALTH, TIMEOUT).await;
let resp = reqwest::get(MINIO_HEALTH).await.unwrap();
assert!(resp.status().is_success());
}
// ===========================================================================
// Bucket operations
// ===========================================================================
// 1. create_bucket — PUT /{bucket}
#[tokio::test]
async fn create_bucket_success() {
let server = MockServer::start().await;
Mock::given(method("PUT"))
.and(path("/test-bucket"))
.respond_with(ResponseTemplate::new(200))
.expect(1)
.mount(&server)
.await;
let client = mock_client(&server);
client.create_bucket("test-bucket").await.unwrap();
}
// 2. delete_bucket — DELETE /{bucket}
#[tokio::test]
async fn delete_bucket_success() {
let server = MockServer::start().await;
Mock::given(method("DELETE"))
.and(path("/test-bucket"))
.respond_with(ResponseTemplate::new(204))
.expect(1)
.mount(&server)
.await;
let client = mock_client(&server);
client.delete_bucket("test-bucket").await.unwrap();
}
// 3. list_buckets — GET /
#[tokio::test]
async fn list_buckets_success() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/"))
.respond_with(
ResponseTemplate::new(200).set_body_json(serde_json::json!({
"Buckets": [
{"Name": "bucket-a", "CreationDate": "2025-01-01T00:00:00Z"},
{"Name": "bucket-b", "CreationDate": "2025-06-15T12:00:00Z"}
],
"Owner": {"ID": "owner-1", "DisplayName": "TestOwner"}
})),
)
.expect(1)
.mount(&server)
.await;
let client = mock_client(&server);
let resp = client.list_buckets().await.unwrap();
assert_eq!(resp.buckets.len(), 2);
assert_eq!(resp.buckets[0].name, "bucket-a");
assert_eq!(resp.buckets[1].name, "bucket-b");
let owner = resp.owner.unwrap();
assert_eq!(owner.display_name, Some("TestOwner".to_string()));
}
#[tokio::test]
async fn list_buckets_error_500() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/"))
.respond_with(ResponseTemplate::new(500).set_body_string("Internal Server Error"))
.expect(1)
.mount(&server)
.await;
let client = mock_client(&server);
let err = client.list_buckets().await.unwrap_err();
let msg = err.to_string();
assert!(msg.contains("500"), "expected 500 in error, got: {msg}");
}
#[tokio::test]
async fn list_buckets_empty() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/"))
.respond_with(
ResponseTemplate::new(200).set_body_json(serde_json::json!({
"Buckets": [],
})),
)
.expect(1)
.mount(&server)
.await;
let client = mock_client(&server);
let resp = client.list_buckets().await.unwrap();
assert!(resp.buckets.is_empty());
}
// 4. head_bucket — HEAD /{bucket}
#[tokio::test]
async fn head_bucket_exists() {
let server = MockServer::start().await;
Mock::given(method("HEAD"))
.and(path("/test-bucket"))
.respond_with(ResponseTemplate::new(200))
.expect(1)
.mount(&server)
.await;
let client = mock_client(&server);
assert!(client.head_bucket("test-bucket").await.unwrap());
}
#[tokio::test]
async fn head_bucket_not_found() {
let server = MockServer::start().await;
Mock::given(method("HEAD"))
.and(path("/no-such-bucket"))
.respond_with(ResponseTemplate::new(404))
.expect(1)
.mount(&server)
.await;
let client = mock_client(&server);
assert!(!client.head_bucket("no-such-bucket").await.unwrap());
}
#[tokio::test]
async fn head_bucket_403_returns_false() {
let server = MockServer::start().await;
Mock::given(method("HEAD"))
.and(path("/forbidden-bucket"))
.respond_with(ResponseTemplate::new(403))
.expect(1)
.mount(&server)
.await;
let client = mock_client(&server);
assert!(!client.head_bucket("forbidden-bucket").await.unwrap());
}
// 5. set_versioning — PUT /{bucket}?versioning
#[tokio::test]
async fn set_versioning_success() {
let server = MockServer::start().await;
Mock::given(method("PUT"))
.and(path("/test-bucket"))
.and(query_param("versioning", ""))
.respond_with(ResponseTemplate::new(200))
.expect(1)
.mount(&server)
.await;
let client = mock_client(&server);
let body = serde_json::json!({"Status": "Enabled"});
client.set_versioning("test-bucket", &body).await.unwrap();
}
// 6. set_lifecycle — PUT /{bucket}?lifecycle
#[tokio::test]
async fn set_lifecycle_success() {
let server = MockServer::start().await;
Mock::given(method("PUT"))
.and(path("/test-bucket"))
.and(query_param("lifecycle", ""))
.respond_with(ResponseTemplate::new(200))
.expect(1)
.mount(&server)
.await;
let client = mock_client(&server);
let body = serde_json::json!({"Rules": [{"Status": "Enabled"}]});
client.set_lifecycle("test-bucket", &body).await.unwrap();
}
// 7. set_cors — PUT /{bucket}?cors
#[tokio::test]
async fn set_cors_success() {
let server = MockServer::start().await;
Mock::given(method("PUT"))
.and(path("/test-bucket"))
.and(query_param("cors", ""))
.respond_with(ResponseTemplate::new(200))
.expect(1)
.mount(&server)
.await;
let client = mock_client(&server);
let body = serde_json::json!({"CORSRules": [{"AllowedOrigins": ["*"]}]});
client.set_cors("test-bucket", &body).await.unwrap();
}
// 8. get_acl — GET /{bucket}?acl
#[tokio::test]
async fn get_acl_success() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/test-bucket"))
.and(query_param("acl", ""))
.respond_with(
ResponseTemplate::new(200).set_body_json(serde_json::json!({
"Owner": {"ID": "owner-1"},
"Grants": [{"Permission": "FULL_CONTROL"}]
})),
)
.expect(1)
.mount(&server)
.await;
let client = mock_client(&server);
let acl = client.get_acl("test-bucket").await.unwrap();
assert_eq!(acl["Owner"]["ID"], "owner-1");
assert_eq!(acl["Grants"][0]["Permission"], "FULL_CONTROL");
}
// 9. set_policy — PUT /{bucket}?policy
#[tokio::test]
async fn set_policy_success() {
let server = MockServer::start().await;
Mock::given(method("PUT"))
.and(path("/test-bucket"))
.and(query_param("policy", ""))
.respond_with(ResponseTemplate::new(200))
.expect(1)
.mount(&server)
.await;
let client = mock_client(&server);
let body = serde_json::json!({
"Version": "2012-10-17",
"Statement": [{"Effect": "Allow", "Principal": "*", "Action": "s3:GetObject"}]
});
client.set_policy("test-bucket", &body).await.unwrap();
}
// ===========================================================================
// Object operations
// ===========================================================================
// 10. put_object — PUT /{bucket}/{key}
#[tokio::test]
async fn put_object_success() {
let server = MockServer::start().await;
Mock::given(method("PUT"))
.and(path("/test-bucket/hello.txt"))
.respond_with(ResponseTemplate::new(200))
.expect(1)
.mount(&server)
.await;
let client = mock_client(&server);
client
.put_object("test-bucket", "hello.txt", "text/plain", bytes::Bytes::from("hello world"))
.await
.unwrap();
}
#[tokio::test]
async fn put_object_error_403() {
let server = MockServer::start().await;
Mock::given(method("PUT"))
.and(path("/test-bucket/secret.txt"))
.respond_with(ResponseTemplate::new(403).set_body_string("Access Denied"))
.expect(1)
.mount(&server)
.await;
let client = mock_client(&server);
let err = client
.put_object("test-bucket", "secret.txt", "text/plain", bytes::Bytes::from("nope"))
.await
.unwrap_err();
let msg = err.to_string();
assert!(msg.contains("403"), "expected 403 in error, got: {msg}");
}
#[tokio::test]
async fn put_object_error_500() {
let server = MockServer::start().await;
Mock::given(method("PUT"))
.and(path("/test-bucket/fail.txt"))
.respond_with(ResponseTemplate::new(500).set_body_string("Internal Server Error"))
.expect(1)
.mount(&server)
.await;
let client = mock_client(&server);
let err = client
.put_object("test-bucket", "fail.txt", "text/plain", bytes::Bytes::from("data"))
.await
.unwrap_err();
let msg = err.to_string();
assert!(msg.contains("500"), "expected 500 in error, got: {msg}");
}
// 11. get_object — GET /{bucket}/{key}
#[tokio::test]
async fn get_object_success() {
let server = MockServer::start().await;
let payload = b"file contents here";
Mock::given(method("GET"))
.and(path("/test-bucket/hello.txt"))
.respond_with(ResponseTemplate::new(200).set_body_bytes(payload.to_vec()))
.expect(1)
.mount(&server)
.await;
let client = mock_client(&server);
let data = client.get_object("test-bucket", "hello.txt").await.unwrap();
assert_eq!(data.as_ref(), payload);
}
#[tokio::test]
async fn get_object_error_404() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/test-bucket/missing.txt"))
.respond_with(ResponseTemplate::new(404).set_body_string("NoSuchKey"))
.expect(1)
.mount(&server)
.await;
let client = mock_client(&server);
let err = client.get_object("test-bucket", "missing.txt").await.unwrap_err();
let msg = err.to_string();
assert!(msg.contains("404"), "expected 404 in error, got: {msg}");
}
#[tokio::test]
async fn get_object_error_500() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/test-bucket/broken.txt"))
.respond_with(ResponseTemplate::new(500).set_body_string("Internal Server Error"))
.expect(1)
.mount(&server)
.await;
let client = mock_client(&server);
let err = client.get_object("test-bucket", "broken.txt").await.unwrap_err();
let msg = err.to_string();
assert!(msg.contains("500"), "expected 500 in error, got: {msg}");
}
// 12. head_object — HEAD /{bucket}/{key}
#[tokio::test]
async fn head_object_exists() {
let server = MockServer::start().await;
Mock::given(method("HEAD"))
.and(path("/test-bucket/hello.txt"))
.respond_with(ResponseTemplate::new(200))
.expect(1)
.mount(&server)
.await;
let client = mock_client(&server);
assert!(client.head_object("test-bucket", "hello.txt").await.unwrap());
}
#[tokio::test]
async fn head_object_not_found() {
let server = MockServer::start().await;
Mock::given(method("HEAD"))
.and(path("/test-bucket/missing.txt"))
.respond_with(ResponseTemplate::new(404))
.expect(1)
.mount(&server)
.await;
let client = mock_client(&server);
assert!(!client.head_object("test-bucket", "missing.txt").await.unwrap());
}
// 13. delete_object — DELETE /{bucket}/{key}
#[tokio::test]
async fn delete_object_success() {
let server = MockServer::start().await;
Mock::given(method("DELETE"))
.and(path("/test-bucket/hello.txt"))
.respond_with(ResponseTemplate::new(204))
.expect(1)
.mount(&server)
.await;
let client = mock_client(&server);
client.delete_object("test-bucket", "hello.txt").await.unwrap();
}
// 14. copy_object — PUT /{bucket}/{key} with x-amz-copy-source header
#[tokio::test]
async fn copy_object_success() {
let server = MockServer::start().await;
Mock::given(method("PUT"))
.and(path("/dest-bucket/dest-key"))
.and(wiremock::matchers::header("x-amz-copy-source", "/src-bucket/src-key"))
.respond_with(ResponseTemplate::new(200))
.expect(1)
.mount(&server)
.await;
let client = mock_client(&server);
client
.copy_object("dest-bucket", "dest-key", "/src-bucket/src-key")
.await
.unwrap();
}
#[tokio::test]
async fn copy_object_error_403() {
let server = MockServer::start().await;
Mock::given(method("PUT"))
.and(path("/dest-bucket/dest-key"))
.respond_with(ResponseTemplate::new(403).set_body_string("Access Denied"))
.expect(1)
.mount(&server)
.await;
let client = mock_client(&server);
let err = client
.copy_object("dest-bucket", "dest-key", "/src-bucket/src-key")
.await
.unwrap_err();
let msg = err.to_string();
assert!(msg.contains("403"), "expected 403 in error, got: {msg}");
}
// 15. list_objects_v2 — GET /{bucket}?list-type=2
#[tokio::test]
async fn list_objects_v2_success() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/test-bucket"))
.and(query_param("list-type", "2"))
.respond_with(
ResponseTemplate::new(200).set_body_json(serde_json::json!({
"Name": "test-bucket",
"Prefix": null,
"MaxKeys": 1000,
"IsTruncated": false,
"Contents": [
{
"Key": "file1.txt",
"LastModified": "2025-01-01T00:00:00Z",
"ETag": "\"abc123\"",
"Size": 1024,
"StorageClass": "STANDARD"
},
{
"Key": "file2.txt",
"Size": 2048
}
]
})),
)
.expect(1)
.mount(&server)
.await;
let client = mock_client(&server);
let resp = client.list_objects_v2("test-bucket", None, None).await.unwrap();
assert_eq!(resp.name, "test-bucket");
assert_eq!(resp.contents.len(), 2);
assert_eq!(resp.contents[0].key, "file1.txt");
assert_eq!(resp.contents[0].size, Some(1024));
assert_eq!(resp.contents[1].key, "file2.txt");
assert_eq!(resp.is_truncated, Some(false));
}
#[tokio::test]
async fn list_objects_v2_with_prefix_and_max_keys() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/test-bucket"))
.and(query_param("list-type", "2"))
.and(query_param("prefix", "docs/"))
.and(query_param("max-keys", "10"))
.respond_with(
ResponseTemplate::new(200).set_body_json(serde_json::json!({
"Name": "test-bucket",
"Prefix": "docs/",
"MaxKeys": 10,
"IsTruncated": false,
"Contents": [
{"Key": "docs/readme.md", "Size": 512}
]
})),
)
.expect(1)
.mount(&server)
.await;
let client = mock_client(&server);
let resp = client
.list_objects_v2("test-bucket", Some("docs/"), Some(10))
.await
.unwrap();
assert_eq!(resp.prefix, Some("docs/".to_string()));
assert_eq!(resp.max_keys, Some(10));
assert_eq!(resp.contents.len(), 1);
assert_eq!(resp.contents[0].key, "docs/readme.md");
}
// 16. set_tags — PUT /{bucket}/{key}?tagging
#[tokio::test]
async fn set_tags_success() {
let server = MockServer::start().await;
Mock::given(method("PUT"))
.and(path("/test-bucket/hello.txt"))
.and(query_param("tagging", ""))
.respond_with(ResponseTemplate::new(200))
.expect(1)
.mount(&server)
.await;
let client = mock_client(&server);
let body = serde_json::json!({"TagSet": [{"Key": "env", "Value": "prod"}]});
client.set_tags("test-bucket", "hello.txt", &body).await.unwrap();
}
// 17. get_tags — GET /{bucket}/{key}?tagging
#[tokio::test]
async fn get_tags_success() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/test-bucket/hello.txt"))
.and(query_param("tagging", ""))
.respond_with(
ResponseTemplate::new(200).set_body_json(serde_json::json!({
"TagSet": [{"Key": "env", "Value": "prod"}]
})),
)
.expect(1)
.mount(&server)
.await;
let client = mock_client(&server);
let tags = client.get_tags("test-bucket", "hello.txt").await.unwrap();
assert_eq!(tags["TagSet"][0]["Key"], "env");
assert_eq!(tags["TagSet"][0]["Value"], "prod");
}
// ===========================================================================
// Multipart operations
// ===========================================================================
// 18. initiate_multipart — POST /{bucket}/{key}?uploads
#[tokio::test]
async fn initiate_multipart_success() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/test-bucket/large-file.bin"))
.and(query_param("uploads", ""))
.respond_with(
ResponseTemplate::new(200).set_body_json(serde_json::json!({
"Bucket": "test-bucket",
"Key": "large-file.bin",
"UploadId": "upload-abc-123"
})),
)
.expect(1)
.mount(&server)
.await;
let client = mock_client(&server);
let resp = client.initiate_multipart("test-bucket", "large-file.bin").await.unwrap();
assert_eq!(resp.bucket, "test-bucket");
assert_eq!(resp.key, "large-file.bin");
assert_eq!(resp.upload_id, "upload-abc-123");
}
// 19. upload_part — PUT /{bucket}/{key}?partNumber=N&uploadId=xxx
#[tokio::test]
async fn upload_part_success() {
let server = MockServer::start().await;
Mock::given(method("PUT"))
.and(path("/test-bucket/large-file.bin"))
.and(query_param("partNumber", "1"))
.and(query_param("uploadId", "upload-abc-123"))
.respond_with(
ResponseTemplate::new(200)
.append_header("ETag", "\"part-etag-1\""),
)
.expect(1)
.mount(&server)
.await;
let client = mock_client(&server);
let resp = client
.upload_part(
"test-bucket",
"large-file.bin",
"upload-abc-123",
1,
bytes::Bytes::from("part data chunk 1"),
)
.await
.unwrap();
assert_eq!(resp.etag, "\"part-etag-1\"");
assert_eq!(resp.part_number, 1);
}
#[tokio::test]
async fn upload_part_error_500() {
let server = MockServer::start().await;
Mock::given(method("PUT"))
.and(path("/test-bucket/large-file.bin"))
.and(query_param("partNumber", "2"))
.and(query_param("uploadId", "upload-abc-123"))
.respond_with(ResponseTemplate::new(500).set_body_string("Internal Server Error"))
.expect(1)
.mount(&server)
.await;
let client = mock_client(&server);
let err = client
.upload_part(
"test-bucket",
"large-file.bin",
"upload-abc-123",
2,
bytes::Bytes::from("data"),
)
.await
.unwrap_err();
let msg = err.to_string();
assert!(msg.contains("500"), "expected 500 in error, got: {msg}");
}
#[tokio::test]
async fn upload_part_no_etag_header() {
let server = MockServer::start().await;
Mock::given(method("PUT"))
.and(path("/test-bucket/large-file.bin"))
.and(query_param("partNumber", "3"))
.and(query_param("uploadId", "upload-abc-123"))
.respond_with(ResponseTemplate::new(200))
.expect(1)
.mount(&server)
.await;
let client = mock_client(&server);
let resp = client
.upload_part(
"test-bucket",
"large-file.bin",
"upload-abc-123",
3,
bytes::Bytes::from("data"),
)
.await
.unwrap();
// No ETag header → empty string fallback
assert_eq!(resp.etag, "");
assert_eq!(resp.part_number, 3);
}
// 20. complete_multipart — POST /{bucket}/{key}?uploadId=xxx
#[tokio::test]
async fn complete_multipart_success() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/test-bucket/large-file.bin"))
.and(query_param("uploadId", "upload-abc-123"))
.respond_with(
ResponseTemplate::new(200).set_body_json(serde_json::json!({
"Location": "https://s3.example.com/test-bucket/large-file.bin",
"Bucket": "test-bucket",
"Key": "large-file.bin",
"ETag": "\"final-etag-xyz\""
})),
)
.expect(1)
.mount(&server)
.await;
let client = mock_client(&server);
let parts = serde_json::json!({
"Parts": [
{"PartNumber": 1, "ETag": "\"part-etag-1\""},
{"PartNumber": 2, "ETag": "\"part-etag-2\""}
]
});
let resp = client
.complete_multipart("test-bucket", "large-file.bin", "upload-abc-123", &parts)
.await
.unwrap();
assert_eq!(resp.bucket, "test-bucket");
assert_eq!(resp.key, "large-file.bin");
assert_eq!(resp.etag, Some("\"final-etag-xyz\"".to_string()));
assert_eq!(
resp.location,
Some("https://s3.example.com/test-bucket/large-file.bin".to_string())
);
}
// 21. abort_multipart — DELETE /{bucket}/{key}?uploadId=xxx
#[tokio::test]
async fn abort_multipart_success() {
let server = MockServer::start().await;
Mock::given(method("DELETE"))
.and(path("/test-bucket/large-file.bin"))
.and(query_param("uploadId", "upload-abc-123"))
.respond_with(ResponseTemplate::new(204))
.expect(1)
.mount(&server)
.await;
let client = mock_client(&server);
client
.abort_multipart("test-bucket", "large-file.bin", "upload-abc-123")
.await
.unwrap();
}
// ===========================================================================
// Additional error paths
// ===========================================================================
#[tokio::test]
async fn create_bucket_error_409_conflict() {
let server = MockServer::start().await;
Mock::given(method("PUT"))
.and(path("/existing-bucket"))
.respond_with(ResponseTemplate::new(409).set_body_string("BucketAlreadyExists"))
.expect(1)
.mount(&server)
.await;
let client = mock_client(&server);
let err = client.create_bucket("existing-bucket").await.unwrap_err();
let msg = err.to_string();
assert!(msg.contains("409"), "expected 409 in error, got: {msg}");
}
#[tokio::test]
async fn delete_bucket_error_404() {
let server = MockServer::start().await;
Mock::given(method("DELETE"))
.and(path("/no-such-bucket"))
.respond_with(ResponseTemplate::new(404).set_body_string("NoSuchBucket"))
.expect(1)
.mount(&server)
.await;
let client = mock_client(&server);
let err = client.delete_bucket("no-such-bucket").await.unwrap_err();
let msg = err.to_string();
assert!(msg.contains("404"), "expected 404 in error, got: {msg}");
}
#[tokio::test]
async fn delete_object_error_403() {
let server = MockServer::start().await;
Mock::given(method("DELETE"))
.and(path("/test-bucket/protected.txt"))
.respond_with(ResponseTemplate::new(403).set_body_string("Access Denied"))
.expect(1)
.mount(&server)
.await;
let client = mock_client(&server);
let err = client.delete_object("test-bucket", "protected.txt").await.unwrap_err();
let msg = err.to_string();
assert!(msg.contains("403"), "expected 403 in error, got: {msg}");
}
#[tokio::test]
async fn get_acl_error_403() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/test-bucket"))
.and(query_param("acl", ""))
.respond_with(ResponseTemplate::new(403).set_body_string("Access Denied"))
.expect(1)
.mount(&server)
.await;
let client = mock_client(&server);
let err = client.get_acl("test-bucket").await.unwrap_err();
let msg = err.to_string();
assert!(msg.contains("403"), "expected 403 in error, got: {msg}");
}
#[tokio::test]
async fn set_versioning_error_403() {
let server = MockServer::start().await;
Mock::given(method("PUT"))
.and(path("/test-bucket"))
.and(query_param("versioning", ""))
.respond_with(ResponseTemplate::new(403).set_body_string("Access Denied"))
.expect(1)
.mount(&server)
.await;
let client = mock_client(&server);
let body = serde_json::json!({"Status": "Enabled"});
let err = client.set_versioning("test-bucket", &body).await.unwrap_err();
let msg = err.to_string();
assert!(msg.contains("403"), "expected 403 in error, got: {msg}");
}
#[tokio::test]
async fn copy_object_error_500() {
let server = MockServer::start().await;
Mock::given(method("PUT"))
.and(path("/dest-bucket/dest-key"))
.respond_with(ResponseTemplate::new(500).set_body_string("InternalError"))
.expect(1)
.mount(&server)
.await;
let client = mock_client(&server);
let err = client
.copy_object("dest-bucket", "dest-key", "/src-bucket/src-key")
.await
.unwrap_err();
let msg = err.to_string();
assert!(msg.contains("500"), "expected 500 in error, got: {msg}");
}
// ===========================================================================
// Client construction (kept from original)
// ===========================================================================
#[tokio::test]
async fn client_from_parts() {
let client = S3Client::from_parts(MINIO_URL.into(), AuthMethod::None);
assert_eq!(client.base_url(), MINIO_URL);
assert_eq!(client.service_name(), "s3");
}
#[tokio::test]
async fn client_connect_builds_url() {
let client = S3Client::connect("example.com");
assert_eq!(client.base_url(), "https://s3.example.com");
}
#[tokio::test]
async fn client_set_auth_does_not_panic() {
let mut client = S3Client::from_parts(MINIO_URL.into(), AuthMethod::None);
client.set_auth(AuthMethod::Bearer("tok".into()));
assert_eq!(client.base_url(), MINIO_URL);
}