use crypsol_storage::*;
#[test]
fn test_constants() {
assert_eq!(PROFILE_IMAGE_SIZE, 200);
assert_eq!(THUMBNAIL_SIZE, 50);
assert_eq!(MAX_PRESIGN_EXPIRY_SECS, 604_800);
}
#[test]
fn test_allowed_image_types() {
assert!(ALLOWED_IMAGE_TYPES.contains(&"image/jpeg"));
assert!(ALLOWED_IMAGE_TYPES.contains(&"image/png"));
assert!(ALLOWED_IMAGE_TYPES.contains(&"image/gif"));
assert!(ALLOWED_IMAGE_TYPES.contains(&"image/webp"));
assert!(!ALLOWED_IMAGE_TYPES.contains(&"image/bmp"));
assert!(!ALLOWED_IMAGE_TYPES.contains(&"image/svg+xml"));
}
#[test]
fn test_validate_content_type_valid() {
assert!(validate_content_type("image/jpeg").is_ok());
assert!(validate_content_type("image/png").is_ok());
assert!(validate_content_type("image/gif").is_ok());
assert!(validate_content_type("image/webp").is_ok());
}
#[test]
fn test_validate_content_type_invalid() {
assert!(validate_content_type("application/pdf").is_err());
assert!(validate_content_type("text/plain").is_err());
assert!(validate_content_type("image/bmp").is_err());
assert!(validate_content_type("text/html").is_err());
assert!(validate_content_type("").is_err());
}
#[test]
fn test_validate_file_size_within_limit() {
assert!(validate_file_size(1024 * 1024, 5 * 1024 * 1024).is_ok());
}
#[test]
fn test_validate_file_size_exactly_at_limit() {
assert!(validate_file_size(5, 5).is_ok());
}
#[test]
fn test_validate_file_size_exceeds_limit() {
assert!(validate_file_size(6, 5).is_err());
}
#[test]
fn test_validate_file_size_zero() {
assert!(validate_file_size(0, 5).is_ok());
}
#[test]
fn test_validate_expiry_valid() {
assert!(validate_expiry(1).is_ok());
assert!(validate_expiry(3600).is_ok());
assert!(validate_expiry(604_800).is_ok());
}
#[test]
fn test_validate_expiry_zero() {
assert!(validate_expiry(0).is_err());
}
#[test]
fn test_validate_expiry_too_large() {
assert!(validate_expiry(604_801).is_err());
assert!(validate_expiry(u64::MAX).is_err());
}
#[test]
fn test_generate_storage_key_format() {
let key = generate_storage_key("profiles", "jpg");
assert!(key.starts_with("profiles/"));
assert!(key.ends_with(".jpg"));
assert!(key.contains('/'));
}
#[test]
fn test_generate_storage_key_uniqueness() {
let key1 = generate_storage_key("test", "png");
let key2 = generate_storage_key("test", "png");
assert_ne!(key1, key2);
}
#[test]
fn test_generate_thumbnail_key() {
let key = "profiles/2025/01/21/abc123.jpg";
let thumb = generate_thumbnail_key(key);
assert_eq!(thumb, "profiles/2025/01/21/abc123_thumb.jpg");
}
#[test]
fn test_generate_thumbnail_key_no_extension() {
let key = "profiles/2025/01/21/abc123";
let thumb = generate_thumbnail_key(key);
assert_eq!(thumb, "profiles/2025/01/21/abc123_thumb");
}
#[test]
fn test_generate_thumbnail_key_multiple_dots() {
let key = "profiles/2025/01/21/file.name.jpg";
let thumb = generate_thumbnail_key(key);
assert_eq!(thumb, "profiles/2025/01/21/file.name_thumb.jpg");
}
#[test]
fn test_image_upload_config_default() {
let config = ImageUploadConfig::default();
assert_eq!(config.width, PROFILE_IMAGE_SIZE);
assert_eq!(config.height, PROFILE_IMAGE_SIZE);
assert_eq!(config.thumbnail_width, THUMBNAIL_SIZE);
assert_eq!(config.thumbnail_height, THUMBNAIL_SIZE);
assert_eq!(config.folder, "profiles");
assert!(!config.maintain_aspect_ratio);
}
#[test]
fn test_storage_config_default() {
let config = StorageConfig::default();
assert_eq!(config.max_file_size, 5 * 1024 * 1024);
assert_eq!(config.max_download_size, 100 * 1024 * 1024);
assert_eq!(config.max_image_alloc, 50 * 1024 * 1024);
assert_eq!(config.max_image_dimension, 10_000);
assert_eq!(config.default_cache_control, "max-age=31536000");
}
#[test]
fn test_upload_result_fields() {
let result = UploadResult {
url: "https://example.com/img.jpg".to_string(),
thumbnail_url: "https://example.com/img_thumb.jpg".to_string(),
key: "img.jpg".to_string(),
thumbnail_key: "img_thumb.jpg".to_string(),
};
assert!(!result.url.is_empty());
assert!(!result.thumbnail_url.is_empty());
}
#[test]
fn test_file_upload_result_fields() {
let result = FileUploadResult {
url: "https://example.com/doc.pdf".to_string(),
key: "docs/doc.pdf".to_string(),
content_type: "application/pdf".to_string(),
};
assert_eq!(result.content_type, "application/pdf");
}
#[test]
fn test_presigned_upload_result_fields() {
let result = PresignedUploadResult {
presigned_url: "https://s3.amazonaws.com/bucket/key?sig=abc".to_string(),
key: "uploads/file.pdf".to_string(),
public_url: "https://cdn.example.com/uploads/file.pdf".to_string(),
expires_at: 1700000000,
};
assert!(!result.presigned_url.is_empty());
assert!(result.expires_at > 0);
}
#[test]
fn test_download_result_fields() {
let result = DownloadResult {
data: bytes::Bytes::from_static(b"hello"),
content_type: "text/plain".to_string(),
content_length: Some(5),
};
assert_eq!(result.data.len(), 5);
assert_eq!(result.content_type, "text/plain");
assert_eq!(result.content_length, Some(5));
}
#[test]
fn test_download_result_no_content_length() {
let result = DownloadResult {
data: bytes::Bytes::new(),
content_type: "application/octet-stream".to_string(),
content_length: None,
};
assert!(result.content_length.is_none());
}
#[test]
fn test_error_display() {
let err = Error::FileTooLarge(10_000_000, 5_000_000);
let msg = format!("{err}");
assert!(msg.contains("10000000"));
assert!(msg.contains("5000000"));
let err = Error::InvalidExpiry(999_999, 604_800);
let msg = format!("{err}");
assert!(msg.contains("999999"));
assert!(msg.contains("604800"));
let err = Error::InvalidFileType("text/html".to_string(), "image/jpeg".to_string());
let msg = format!("{err}");
assert!(msg.contains("text/html"));
let err = Error::PresignNotSupported;
let msg = format!("{err}");
assert!(msg.contains("not supported"));
}
#[cfg(feature = "local")]
mod local_tests {
use super::*;
use tempfile::TempDir;
fn make_service() -> (StorageService<LocalBackend>, TempDir) {
let dir = TempDir::new().unwrap();
let backend = LocalBackend::new(dir.path(), "http://localhost:9999/files");
let service = StorageService::new(backend, StorageConfig::default());
(service, dir)
}
#[tokio::test]
async fn test_local_upload_download_delete() {
let (service, _dir) = make_service();
let data = b"hello world";
let result = service
.upload_file(data, "text/plain", "docs", "txt")
.await
.unwrap();
assert!(result.url.starts_with("http://localhost:9999/files/"));
assert!(result.key.starts_with("docs/"));
assert!(result.key.ends_with(".txt"));
let downloaded = service.download_object(&result.key).await.unwrap();
assert_eq!(downloaded.data.as_ref(), b"hello world");
assert!(service.file_exists(&result.key).await.unwrap());
service.delete_file(&result.key).await.unwrap();
assert!(!service.file_exists(&result.key).await.unwrap());
}
#[tokio::test]
async fn test_local_public_url() {
let (service, _dir) = make_service();
let url = service.public_url("my/file.jpg");
assert_eq!(url, "http://localhost:9999/files/my/file.jpg");
}
#[tokio::test]
async fn test_local_extract_key_from_url() {
let (service, _dir) = make_service();
let key = service.extract_key_from_url("http://localhost:9999/files/my/file.jpg");
assert_eq!(key, Some("my/file.jpg".to_string()));
}
#[tokio::test]
async fn test_local_presigned_not_supported() {
let (service, _dir) = make_service();
let err = service.presigned_get_url("key", 3600).await.unwrap_err();
assert!(matches!(err, Error::PresignNotSupported));
}
#[tokio::test]
async fn test_local_download_size_limit() {
let (service, _dir) = make_service();
let big_data = vec![0u8; 1024];
let small_config = StorageConfig {
max_download_size: 100,
max_file_size: 10 * 1024 * 1024,
..StorageConfig::default()
};
let backend =
LocalBackend::new(service.backend().base_dir(), "http://localhost:9999/files");
let limited_service = StorageService::new(backend, small_config);
let result = limited_service
.upload_file(&big_data, "application/octet-stream", "test", "bin")
.await
.unwrap();
let err = limited_service
.download_object(&result.key)
.await
.unwrap_err();
assert!(matches!(err, Error::FileTooLarge(_, _)));
}
#[tokio::test]
async fn test_local_rejects_path_traversal() {
let dir = TempDir::new().unwrap();
let backend = LocalBackend::new(dir.path(), "http://localhost:9999/files");
let err = backend
.put_object(
"../escape.txt",
bytes::Bytes::from_static(b"data"),
"text/plain",
&PutOptions::default(),
)
.await;
assert!(err.is_err());
let err = backend.get_object("../../etc/passwd").await;
assert!(err.is_err());
let err = backend.delete_object("../escape.txt").await;
assert!(err.is_err());
let err = backend.exists("/absolute/path").await;
assert!(err.is_err());
}
#[tokio::test]
async fn test_rejects_oversized_output_dimensions() {
let (service, _dir) = make_service();
let result = service
.upload_image_with_key(
b"not-a-real-image",
"image/jpeg",
"test/key.jpg",
999_999,
999_999,
50,
50,
)
.await;
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
Error::InvalidDimensions(999_999, 999_999, 10_000)
));
}
#[tokio::test]
async fn test_rejects_zero_output_dimensions() {
let (service, _dir) = make_service();
let result = service
.upload_image_with_key(b"data", "image/jpeg", "test/key.jpg", 0, 200, 50, 50)
.await;
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
Error::InvalidDimensions(0, 200, _)
));
}
#[tokio::test]
async fn test_local_test_connection_success() {
let dir = TempDir::new().unwrap();
let backend = LocalBackend::new(dir.path(), "http://localhost:9999/files");
assert!(backend.test_connection().await.is_ok());
}
#[tokio::test]
async fn test_local_test_connection_missing_dir() {
let backend = LocalBackend::new(
"/tmp/does_not_exist_crypsol_test_xyz",
"http://localhost:9999/files",
);
assert!(backend.test_connection().await.is_err());
}
#[tokio::test]
async fn test_service_test_connection() {
let dir = TempDir::new().unwrap();
let backend = LocalBackend::new(dir.path(), "http://localhost:9999/files");
let service = StorageService::new(backend, StorageConfig::default());
assert!(service.test_connection().await.is_ok());
}
}