use std::sync::Arc; use adapters_storage::ObjectStorageAdapter; use bytes::Bytes; use domain::errors::DomainError; use domain::ports::{DataStream, StorageReader, StorageWriter}; use futures::stream; use futures::stream::StreamExt; use object_store::memory::InMemory; fn make_adapter() -> ObjectStorageAdapter { ObjectStorageAdapter::new(Arc::new(InMemory::new()), "test").unwrap() } fn one_shot(data: &'static [u8]) -> DataStream { Box::pin(stream::once(async move { Ok(Bytes::from(data)) })) } #[tokio::test] async fn put_get_roundtrip() { let a = make_adapter(); a.put("hello.txt", one_shot(b"world")).await.unwrap(); let mut s = a.get("hello.txt").await.unwrap(); let mut out = Vec::new(); while let Some(chunk) = s.next().await { out.extend_from_slice(&chunk.unwrap()); } assert_eq!(out, b"world"); } #[tokio::test] async fn get_missing_is_not_found() { let a = make_adapter(); assert!(matches!( a.get("nope.txt").await, Err(DomainError::NotFound(_)) )); } #[tokio::test] async fn delete_is_idempotent() { let a = make_adapter(); a.delete("nope.txt").await.unwrap(); } #[tokio::test] async fn delete_removes_key() { let a = make_adapter(); a.put("file.txt", one_shot(b"data")).await.unwrap(); a.delete("file.txt").await.unwrap(); assert!(matches!( a.get("file.txt").await, Err(DomainError::NotFound(_)) )); } #[tokio::test] async fn list_returns_keys_under_prefix() { let a = make_adapter(); a.put("docs/readme.txt", one_shot(b"x")).await.unwrap(); a.put("docs/guide.txt", one_shot(b"y")).await.unwrap(); a.put("other/file.txt", one_shot(b"z")).await.unwrap(); let keys = a.list(Some("docs")).await.unwrap(); assert_eq!(keys.len(), 2); assert!(keys.iter().any(|k| k.ends_with("readme.txt"))); assert!(keys.iter().any(|k| k.ends_with("guide.txt"))); } #[tokio::test] async fn list_none_returns_all() { let a = make_adapter(); a.put("a.txt", one_shot(b"1")).await.unwrap(); a.put("b.txt", one_shot(b"2")).await.unwrap(); let keys = a.list(None).await.unwrap(); assert_eq!(keys.len(), 2); } #[tokio::test] async fn rejects_empty_key() { let a = make_adapter(); assert!(matches!( a.put("", one_shot(b"x")).await, Err(DomainError::Validation(_)) )); assert!(matches!(a.get("").await, Err(DomainError::Validation(_)))); assert!(matches!( a.delete("").await, Err(DomainError::Validation(_)) )); } #[tokio::test] async fn rejects_absolute_key() { let a = make_adapter(); assert!(matches!( a.put("/etc/passwd", one_shot(b"x")).await, Err(DomainError::Validation(_)) )); } #[tokio::test] async fn rejects_path_traversal() { let a = make_adapter(); assert!(matches!( a.get("../escape").await, Err(DomainError::Validation(_)) )); assert!(matches!( a.get("a/../../../etc").await, Err(DomainError::Validation(_)) )); } #[tokio::test] async fn rejects_dot_segment() { let a = make_adapter(); assert!(matches!( a.put("./file.txt", one_shot(b"x")).await, Err(DomainError::Validation(_)) )); } #[tokio::test] async fn rejects_invalid_list_prefix() { let a = make_adapter(); assert!(matches!( a.list(Some("")).await, Err(DomainError::Validation(_)) )); assert!(matches!( a.list(Some("../escape")).await, Err(DomainError::Validation(_)) )); } #[tokio::test] async fn put_overwrites_existing() { let a = make_adapter(); a.put("file.txt", one_shot(b"version1")).await.unwrap(); a.put("file.txt", one_shot(b"version2")).await.unwrap(); let mut s = a.get("file.txt").await.unwrap(); let mut out = Vec::new(); while let Some(chunk) = s.next().await { out.extend_from_slice(&chunk.unwrap()); } assert_eq!(out, b"version2"); } #[tokio::test] async fn list_returns_exact_key_paths() { let a = make_adapter(); a.put("docs/readme.txt", one_shot(b"x")).await.unwrap(); let mut keys = a.list(Some("docs")).await.unwrap(); keys.sort(); assert_eq!(keys, vec!["docs/readme.txt"]); } #[tokio::test] async fn put_bytes_get_bytes_roundtrip() { let a = make_adapter(); a.put_bytes("data.bin", Bytes::from("hello bytes")) .await .unwrap(); let got = a.get_bytes("data.bin").await.unwrap(); assert_eq!(got.as_ref(), b"hello bytes"); } #[tokio::test] async fn get_bytes_missing_is_not_found() { let a = make_adapter(); assert!(matches!( a.get_bytes("nope.bin").await, Err(DomainError::NotFound(_)) )); } #[test] fn new_rejects_traversal_prefix() { let result = ObjectStorageAdapter::new(Arc::new(InMemory::new()), "../evil"); assert!(matches!(result, Err(DomainError::Validation(_)))); } #[test] fn new_rejects_absolute_prefix() { let result = ObjectStorageAdapter::new(Arc::new(InMemory::new()), "/root"); assert!(matches!(result, Err(DomainError::Validation(_)))); } #[test] fn new_accepts_empty_prefix() { assert!(ObjectStorageAdapter::new(Arc::new(InMemory::new()), "").is_ok()); } #[test] fn new_accepts_valid_prefix() { assert!(ObjectStorageAdapter::new(Arc::new(InMemory::new()), "my-bucket/data").is_ok()); }