From 6c88ac344cafaf970dff7b0e71d7c78ebda08d55 Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Sun, 31 May 2026 11:16:18 +0200 Subject: [PATCH] refactor: extract inline tests to separate files in auth + storage adapters --- crates/adapters/auth/src/jwt.rs | 22 -- crates/adapters/auth/src/password.rs | 13 -- crates/adapters/auth/tests/auth_tests.rs | 2 + crates/adapters/auth/tests/jwt.rs | 21 ++ crates/adapters/auth/tests/password.rs | 10 + crates/adapters/storage/src/adapter.rs | 196 ----------------- .../adapters/storage/tests/adapter_tests.rs | 197 ++++++++++++++++++ 7 files changed, 230 insertions(+), 231 deletions(-) create mode 100644 crates/adapters/auth/tests/auth_tests.rs create mode 100644 crates/adapters/auth/tests/jwt.rs create mode 100644 crates/adapters/auth/tests/password.rs create mode 100644 crates/adapters/storage/tests/adapter_tests.rs diff --git a/crates/adapters/auth/src/jwt.rs b/crates/adapters/auth/src/jwt.rs index 3f7b210..18c839c 100644 --- a/crates/adapters/auth/src/jwt.rs +++ b/crates/adapters/auth/src/jwt.rs @@ -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(_)))); - } -} diff --git a/crates/adapters/auth/src/password.rs b/crates/adapters/auth/src/password.rs index 0d688fc..bc3eb32 100644 --- a/crates/adapters/auth/src/password.rs +++ b/crates/adapters/auth/src/password.rs @@ -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()); - } -} diff --git a/crates/adapters/auth/tests/auth_tests.rs b/crates/adapters/auth/tests/auth_tests.rs new file mode 100644 index 0000000..7135d58 --- /dev/null +++ b/crates/adapters/auth/tests/auth_tests.rs @@ -0,0 +1,2 @@ +mod jwt; +mod password; diff --git a/crates/adapters/auth/tests/jwt.rs b/crates/adapters/auth/tests/jwt.rs new file mode 100644 index 0000000..36de8de --- /dev/null +++ b/crates/adapters/auth/tests/jwt.rs @@ -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(_)))); +} diff --git a/crates/adapters/auth/tests/password.rs b/crates/adapters/auth/tests/password.rs new file mode 100644 index 0000000..d0e8c42 --- /dev/null +++ b/crates/adapters/auth/tests/password.rs @@ -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()); +} diff --git a/crates/adapters/storage/src/adapter.rs b/crates/adapters/storage/src/adapter.rs index 215b3f1..2999bc7 100644 --- a/crates/adapters/storage/src/adapter.rs +++ b/crates/adapters/storage/src/adapter.rs @@ -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()); - } -} diff --git a/crates/adapters/storage/tests/adapter_tests.rs b/crates/adapters/storage/tests/adapter_tests.rs new file mode 100644 index 0000000..9960fd3 --- /dev/null +++ b/crates/adapters/storage/tests/adapter_tests.rs @@ -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()); +}