test(application): fill unit test gaps — api_keys, profile, auth, thoughts, social
Some checks failed
lint / lint (push) Has been cancelled
test / unit (push) Has been cancelled
test / integration (push) Has been cancelled
lint / lint (pull_request) Failing after 5m7s
test / unit (pull_request) Successful in 16m2s
test / integration (pull_request) Failing after 16m57s

This commit is contained in:
2026-05-14 16:19:35 +02:00
parent d50c13a2db
commit ddd9b17ed7
5 changed files with 195 additions and 0 deletions

View File

@@ -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);
}
}

View File

@@ -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(_)));
}
}

View File

@@ -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<UserId> = (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);
}
}

View File

@@ -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 { .. })));
}
}

View File

@@ -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()));
}
}