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
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:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(_)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 { .. })));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user