198 lines
5.2 KiB
Rust
198 lines
5.2 KiB
Rust
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());
|
|
}
|