refactor: extract inline tests to separate files in auth + storage adapters

This commit is contained in:
2026-05-31 11:16:18 +02:00
parent aff772f6d7
commit 6c88ac344c
7 changed files with 230 additions and 231 deletions

View File

@@ -47,25 +47,3 @@ impl TokenIssuer for JwtTokenIssuer {
Ok((SystemId::from_uuid(uuid), data.claims.role))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn issue_and_verify_roundtrip() {
let issuer = JwtTokenIssuer::new("test-secret-key-long-enough-32chars!!");
let user_id = SystemId::new();
let token = issuer.issue(&user_id, "user").await.unwrap();
let (verified_id, verified_role) = issuer.verify(&token).await.unwrap();
assert_eq!(verified_id, user_id);
assert_eq!(verified_role, "user");
}
#[tokio::test]
async fn rejects_invalid_token() {
let issuer = JwtTokenIssuer::new("test-secret-key-long-enough-32chars!!");
let result = issuer.verify("not.a.valid.jwt").await;
assert!(matches!(result, Err(DomainError::Unauthorized(_))));
}
}

View File

@@ -23,16 +23,3 @@ impl PasswordHasher for BcryptPasswordHasher {
.map_err(|e| DomainError::Internal(e.to_string()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn hash_and_verify_roundtrip() {
let h = BcryptPasswordHasher;
let hash = h.hash("mysecretpassword").await.unwrap();
assert!(h.verify("mysecretpassword", &hash).await.unwrap());
assert!(!h.verify("wrongpassword", &hash).await.unwrap());
}
}

View File

@@ -0,0 +1,2 @@
mod jwt;
mod password;

View File

@@ -0,0 +1,21 @@
use adapters_auth::JwtTokenIssuer;
use domain::errors::DomainError;
use domain::ports::TokenIssuer;
use domain::value_objects::SystemId;
#[tokio::test]
async fn issue_and_verify_roundtrip() {
let issuer = JwtTokenIssuer::new("test-secret-key-long-enough-32chars!!");
let user_id = SystemId::new();
let token = issuer.issue(&user_id, "user").await.unwrap();
let (verified_id, verified_role) = issuer.verify(&token).await.unwrap();
assert_eq!(verified_id, user_id);
assert_eq!(verified_role, "user");
}
#[tokio::test]
async fn rejects_invalid_token() {
let issuer = JwtTokenIssuer::new("test-secret-key-long-enough-32chars!!");
let result = issuer.verify("not.a.valid.jwt").await;
assert!(matches!(result, Err(DomainError::Unauthorized(_))));
}

View File

@@ -0,0 +1,10 @@
use adapters_auth::BcryptPasswordHasher;
use domain::ports::PasswordHasher;
#[tokio::test]
async fn hash_and_verify_roundtrip() {
let h = BcryptPasswordHasher;
let hash = h.hash("mysecretpassword").await.unwrap();
assert!(h.verify("mysecretpassword", &hash).await.unwrap());
assert!(!h.verify("wrongpassword", &hash).await.unwrap());
}

View File

@@ -146,199 +146,3 @@ impl StorageReader for ObjectStorageAdapter {
Ok(result)
}
}
#[cfg(test)]
mod tests {
use super::*;
use domain::ports::{StorageReader, StorageWriter};
use futures::stream;
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());
}
}

View File

@@ -0,0 +1,197 @@
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());
}