#![cfg(feature = "s3-tests")]
use bytes::Bytes;
use ferro_storage::{Disk, PutOptions, Storage, StorageConfig};
use std::time::Duration;
fn setup() -> Storage {
let config = StorageConfig::from_env();
Storage::with_storage_config(config)
}
fn s3_disk_or_skip() -> Option<Disk> {
if std::env::var("AWS_BUCKET").is_err() {
eprintln!("Skipping S3 integration test: AWS_BUCKET not set");
return None;
}
let storage = setup();
storage.disk("s3").ok()
}
#[tokio::test]
async fn test_put_get_delete() {
let Some(disk) = s3_disk_or_skip() else {
return;
};
let path = "integration-test/hello.txt";
let content = Bytes::from("hello s3");
disk.put(path, content.clone()).await.unwrap();
let retrieved = disk.get(path).await.unwrap();
assert_eq!(retrieved, content);
disk.delete(path).await.unwrap();
let exists = disk.exists(path).await.unwrap();
assert!(!exists);
}
#[tokio::test]
async fn test_exists_missing() {
let Some(disk) = s3_disk_or_skip() else {
return;
};
let exists = disk
.exists("integration-test/nonexistent-key-12345")
.await
.unwrap();
assert!(!exists);
}
#[tokio::test]
async fn test_put_with_content_type() {
let Some(disk) = s3_disk_or_skip() else {
return;
};
let path = "integration-test/image.png";
let content = Bytes::from(b"\x89PNG\r\n".as_ref());
let options = PutOptions::new().content_type("image/png");
disk.put_with_options(path, content, options).await.unwrap();
let meta = disk.metadata(path).await.unwrap();
assert_eq!(meta.mime_type.as_deref(), Some("image/png"));
disk.delete(path).await.unwrap();
}
#[tokio::test]
async fn test_size() {
let Some(disk) = s3_disk_or_skip() else {
return;
};
let path = "integration-test/size-test.txt";
let content = Bytes::from("0123456789");
disk.put(path, content).await.unwrap();
let size = disk.size(path).await.unwrap();
assert_eq!(size, 10);
disk.delete(path).await.unwrap();
}
#[tokio::test]
async fn test_copy() {
let Some(disk) = s3_disk_or_skip() else {
return;
};
let original = "integration-test/copy-original.txt";
let copy = "integration-test/copy-destination.txt";
let content = Bytes::from("copy test content");
disk.put(original, content.clone()).await.unwrap();
disk.copy(original, copy).await.unwrap();
let retrieved = disk.get(copy).await.unwrap();
assert_eq!(retrieved, content);
disk.delete(original).await.unwrap();
disk.delete(copy).await.unwrap();
}
#[tokio::test]
async fn test_url() {
let Some(disk) = s3_disk_or_skip() else {
return;
};
let path = "integration-test/url-test.txt";
let bucket = std::env::var("AWS_BUCKET").unwrap_or_default();
disk.put(path, Bytes::from("url test")).await.unwrap();
let url = disk.url(path).await.unwrap();
assert!(!url.is_empty());
let url_base = std::env::var("AWS_URL").unwrap_or_default();
if url_base.is_empty() {
assert!(
url.contains(&bucket),
"URL should contain bucket name: {url}"
);
}
disk.delete(path).await.unwrap();
}
#[tokio::test]
async fn test_temporary_url() {
let Some(disk) = s3_disk_or_skip() else {
return;
};
let path = "integration-test/presigned-test.txt";
disk.put(path, Bytes::from("presigned content"))
.await
.unwrap();
let url = disk
.temporary_url(path, Duration::from_secs(300))
.await
.unwrap();
assert!(
url.contains("X-Amz-Signature") || url.contains("x-amz-signature"),
"presigned URL should contain X-Amz-Signature: {url}"
);
disk.delete(path).await.unwrap();
}
#[tokio::test]
async fn test_files_and_directories() {
let Some(disk) = s3_disk_or_skip() else {
return;
};
let file_a = "integration-test/dir/a.txt";
let file_b = "integration-test/dir/sub/b.txt";
disk.put(file_a, Bytes::from("file a")).await.unwrap();
disk.put(file_b, Bytes::from("file b")).await.unwrap();
let files = disk.files("integration-test/dir").await.unwrap();
assert!(
files.iter().any(|k| k.contains("a.txt")),
"files() should include a.txt: {files:?}"
);
assert!(
!files.iter().any(|k| k.contains("b.txt")),
"files() should not include b.txt in subdir: {files:?}"
);
let dirs = disk.directories("integration-test/dir").await.unwrap();
assert!(
dirs.iter().any(|d| d.contains("sub")),
"directories() should include sub: {dirs:?}"
);
let all = disk.all_files("integration-test/dir").await.unwrap();
assert!(
all.iter().any(|k| k.contains("a.txt")),
"all_files() should include a.txt: {all:?}"
);
assert!(
all.iter().any(|k| k.contains("b.txt")),
"all_files() should include b.txt: {all:?}"
);
disk.delete(file_a).await.unwrap();
disk.delete(file_b).await.unwrap();
}
#[tokio::test]
async fn test_make_delete_directory() {
let Some(disk) = s3_disk_or_skip() else {
return;
};
disk.make_directory("integration-test/newdir")
.await
.unwrap();
let marker_exists = disk.exists("integration-test/newdir/.keep").await.unwrap();
assert!(
marker_exists,
".keep marker should exist after make_directory"
);
disk.delete_directory("integration-test/newdir")
.await
.unwrap();
let marker_gone = disk.exists("integration-test/newdir/.keep").await.unwrap();
assert!(
!marker_gone,
".keep marker should be gone after delete_directory"
);
}