refactor: extract inline tests to separate files in auth + storage adapters
This commit is contained in:
@@ -47,25 +47,3 @@ impl TokenIssuer for JwtTokenIssuer {
|
|||||||
Ok((SystemId::from_uuid(uuid), data.claims.role))
|
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(_))));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -23,16 +23,3 @@ impl PasswordHasher for BcryptPasswordHasher {
|
|||||||
.map_err(|e| DomainError::Internal(e.to_string()))
|
.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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
2
crates/adapters/auth/tests/auth_tests.rs
Normal file
2
crates/adapters/auth/tests/auth_tests.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
mod jwt;
|
||||||
|
mod password;
|
||||||
21
crates/adapters/auth/tests/jwt.rs
Normal file
21
crates/adapters/auth/tests/jwt.rs
Normal 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(_))));
|
||||||
|
}
|
||||||
10
crates/adapters/auth/tests/password.rs
Normal file
10
crates/adapters/auth/tests/password.rs
Normal 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());
|
||||||
|
}
|
||||||
@@ -146,199 +146,3 @@ impl StorageReader for ObjectStorageAdapter {
|
|||||||
Ok(result)
|
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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
197
crates/adapters/storage/tests/adapter_tests.rs
Normal file
197
crates/adapters/storage/tests/adapter_tests.rs
Normal 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());
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user