diff --git a/crates/application/src/use_cases/api_keys.rs b/crates/application/src/use_cases/api_keys.rs index 2f8e63c..1f0ef56 100644 --- a/crates/application/src/use_cases/api_keys.rs +++ b/crates/application/src/use_cases/api_keys.rs @@ -27,3 +27,52 @@ fn sha256_hex(s: &str) -> String { let hash = Sha256::digest(s.as_bytes()); hex::encode(hash) } + +#[cfg(test)] +mod tests { + use super::*; + use domain::{testing::TestStore, value_objects::UserId}; + + #[tokio::test] + async fn create_key_saves_hashed_not_raw() { + let store = TestStore::default(); + let uid = UserId::new(); + let (key, raw) = create_api_key(&store, &uid, "my-key".to_string()).await.unwrap(); + assert_ne!(key.key_hash, raw, "stored hash must differ from raw key"); + assert!(!key.key_hash.is_empty()); + assert_eq!(key.name, "my-key"); + assert_eq!(key.user_id, uid); + assert_eq!(store.api_keys.lock().unwrap().len(), 1); + } + + #[tokio::test] + async fn raw_key_verifies_against_stored_hash() { + use sha2::{Digest, Sha256}; + let store = TestStore::default(); + let uid = UserId::new(); + let (key, raw) = create_api_key(&store, &uid, "test".to_string()).await.unwrap(); + let expected_hash = hex::encode(Sha256::digest(raw.as_bytes())); + assert_eq!(key.key_hash, expected_hash); + } + + #[tokio::test] + async fn delete_key_removes_it() { + let store = TestStore::default(); + let uid = UserId::new(); + let (key, _) = create_api_key(&store, &uid, "k".to_string()).await.unwrap(); + delete_api_key(&store, &uid, &key.id).await.unwrap(); + assert!(store.api_keys.lock().unwrap().is_empty()); + } + + #[tokio::test] + async fn list_keys_returns_only_own_keys() { + let store = TestStore::default(); + let alice = UserId::new(); + let bob = UserId::new(); + create_api_key(&store, &alice, "a".to_string()).await.unwrap(); + create_api_key(&store, &bob, "b".to_string()).await.unwrap(); + let alice_keys = list_api_keys(&store, &alice).await.unwrap(); + assert_eq!(alice_keys.len(), 1); + assert_eq!(alice_keys[0].user_id, alice); + } +} diff --git a/crates/application/src/use_cases/auth.rs b/crates/application/src/use_cases/auth.rs index d57caa7..9244c52 100644 --- a/crates/application/src/use_cases/auth.rs +++ b/crates/application/src/use_cases/auth.rs @@ -124,4 +124,19 @@ mod tests { assert_eq!(events.len(), 1); assert!(matches!(events[0], DomainEvent::UserRegistered { .. })); } + + #[tokio::test] + async fn login_fails_for_nonexistent_user() { + let store = TestStore::default(); + let err = login(&store, &FakeHasher, &FakeAuth, LoginInput { email: "ghost@ex.com".into(), password: "pass".into() }).await.unwrap_err(); + assert!(matches!(err, DomainError::Unauthorized)); + } + + #[tokio::test] + async fn register_rejects_duplicate_email() { + let store = TestStore::default(); + register(&store, &FakeHasher, &FakeAuth, &NoOpEventPublisher, input()).await.unwrap(); + let err = register(&store, &FakeHasher, &FakeAuth, &NoOpEventPublisher, RegisterInput { username: "alice2".into(), email: "alice@ex.com".into(), password: "pass2".into() }).await.unwrap_err(); + assert!(matches!(err, DomainError::Conflict(_))); + } } diff --git a/crates/application/src/use_cases/profile.rs b/crates/application/src/use_cases/profile.rs index 006ba54..3f52803 100644 --- a/crates/application/src/use_cases/profile.rs +++ b/crates/application/src/use_cases/profile.rs @@ -35,3 +35,63 @@ pub async fn set_top_friends(top_friends: &dyn TopFriendRepository, user_id: &Us let friends: Vec<(UserId, i16)> = friend_ids.into_iter().enumerate().map(|(i, id)| (id, (i + 1) as i16)).collect(); top_friends.set_top_friends(user_id, friends).await } + +#[cfg(test)] +mod tests { + use super::*; + use domain::{ + errors::DomainError, + models::user::User, + testing::TestStore, + value_objects::{Email, PasswordHash, UserId, Username}, + }; + + fn make_user() -> User { + User::new_local( + UserId::new(), + Username::new("alice").unwrap(), + Email::new("alice@ex.com").unwrap(), + PasswordHash("h".into()), + ) + } + + #[tokio::test] + async fn set_top_friends_rejects_more_than_eight() { + let store = TestStore::default(); + let uid = UserId::new(); + let friends: Vec = (0..9).map(|_| UserId::new()).collect(); + let err = set_top_friends(&store, &uid, friends).await.unwrap_err(); + assert!(matches!(err, DomainError::InvalidInput(_))); + } + + #[tokio::test] + async fn set_top_friends_assigns_sequential_positions() { + let store = TestStore::default(); + let uid = UserId::new(); + let f1 = UserId::new(); + let f2 = UserId::new(); + let f3 = UserId::new(); + set_top_friends(&store, &uid, vec![f1.clone(), f2.clone(), f3.clone()]).await.unwrap(); + let tf = store.top_friends.lock().unwrap(); + assert_eq!(tf.len(), 3); + let pos_f1 = tf.iter().find(|t| t.friend_id == f1).map(|t| t.position).unwrap(); + let pos_f2 = tf.iter().find(|t| t.friend_id == f2).map(|t| t.position).unwrap(); + assert!(pos_f1 < pos_f2, "f1 should come before f2"); + } + + #[tokio::test] + async fn get_user_by_username_returns_not_found_for_missing_user() { + let store = TestStore::default(); + let err = get_user_by_username(&store, "nobody").await.unwrap_err(); + assert!(matches!(err, DomainError::NotFound)); + } + + #[tokio::test] + async fn get_user_by_username_returns_correct_user() { + let store = TestStore::default(); + let user = make_user(); + store.users.lock().unwrap().push(user.clone()); + let found = get_user_by_username(&store, "alice").await.unwrap(); + assert_eq!(found.id, user.id); + } +} diff --git a/crates/application/src/use_cases/social.rs b/crates/application/src/use_cases/social.rs index 2a467a8..0486896 100644 --- a/crates/application/src/use_cases/social.rs +++ b/crates/application/src/use_cases/social.rs @@ -136,4 +136,37 @@ mod tests { assert_eq!(events.len(), 1); assert!(matches!(events[0], DomainEvent::UserUnblocked { .. })); } + + #[tokio::test] + async fn block_user_saves_block_and_publishes_event() { + let store = TestStore::default(); + let alice = user("alice"); + let bob = user("bob"); + block_user(&store, &store, &alice.id, &bob.id).await.unwrap(); + assert_eq!(store.blocks.lock().unwrap().len(), 1); + let events = store.events.lock().unwrap(); + assert!(events.iter().any(|e| matches!(e, DomainEvent::UserBlocked { blocker_id, .. } if blocker_id == &alice.id))); + } + + #[tokio::test] + async fn cannot_block_self() { + let store = TestStore::default(); + let alice = user("alice"); + let err = block_user(&store, &store, &alice.id, &alice.id).await.unwrap_err(); + assert!(matches!(err, DomainError::InvalidInput(_))); + } + + #[tokio::test] + async fn boost_and_unboost() { + let store = TestStore::default(); + let alice = user("alice"); + let tid = ThoughtId::new(); + boost_thought(&store, &store, &alice.id, &tid).await.unwrap(); + assert_eq!(store.boosts.lock().unwrap().len(), 1); + unboost_thought(&store, &store, &alice.id, &tid).await.unwrap(); + assert!(store.boosts.lock().unwrap().is_empty()); + let events = store.events.lock().unwrap(); + assert!(events.iter().any(|e| matches!(e, DomainEvent::BoostAdded { .. }))); + assert!(events.iter().any(|e| matches!(e, DomainEvent::BoostRemoved { .. }))); + } } diff --git a/crates/application/src/use_cases/thoughts.rs b/crates/application/src/use_cases/thoughts.rs index 48d0470..2d7f098 100644 --- a/crates/application/src/use_cases/thoughts.rs +++ b/crates/application/src/use_cases/thoughts.rs @@ -119,4 +119,42 @@ mod tests { let err = delete_thought(&store, &NoOpEventPublisher, &out.thought.id, &bob.id).await.unwrap_err(); assert!(matches!(err, DomainError::NotFound)); } + + #[tokio::test] + async fn edit_thought_changes_content_and_emits_event() { + let store = TestStore::default(); + let alice = user(); + store.users.lock().unwrap().push(alice.clone()); + let out = create_thought(&store, &store, &store, input(alice.id.clone())).await.unwrap(); + let tid = out.thought.id.clone(); + + edit_thought(&store, &store, &tid, &alice.id, "updated".to_string()).await.unwrap(); + + let saved = store.thoughts.lock().unwrap().iter().find(|t| t.id == tid).unwrap().clone(); + assert_eq!(saved.content.as_str(), "updated"); + + let events = store.events.lock().unwrap(); + assert!(events.iter().any(|e| matches!(e, DomainEvent::ThoughtUpdated { thought_id, .. } if thought_id == &tid))); + } + + #[tokio::test] + async fn create_reply_sets_in_reply_to_id() { + let store = TestStore::default(); + let alice = user(); + store.users.lock().unwrap().push(alice.clone()); + let original = create_thought(&store, &store, &NoOpEventPublisher, input(alice.id.clone())).await.unwrap().thought; + + create_thought(&store, &store, &NoOpEventPublisher, CreateThoughtInput { + user_id: alice.id.clone(), + content: "reply".into(), + in_reply_to_id: Some(original.id.clone()), + visibility: None, + content_warning: None, + sensitive: false, + }).await.unwrap(); + + let thoughts = store.thoughts.lock().unwrap(); + let reply = thoughts.iter().find(|t| t.content.as_str() == "reply").unwrap(); + assert_eq!(reply.in_reply_to_id, Some(original.id.clone())); + } }