2
crates/application/tests/activity.rs
Normal file
2
crates/application/tests/activity.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
#[path = "activity/crud_test.rs"]
|
||||
mod crud_test;
|
||||
220
crates/application/tests/activity/crud_test.rs
Normal file
220
crates/application/tests/activity/crud_test.rs
Normal file
@@ -0,0 +1,220 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::activity::{ActivityName, CategoryName};
|
||||
use domain::ports::ActivityCommandPort;
|
||||
use domain::testing::{InMemoryStore, test_activity, test_user};
|
||||
|
||||
use domain::user::UserId;
|
||||
|
||||
use application::activity::commands::{
|
||||
CreateActivityCommand, RenameActivityCommand, SetCategoryCommand,
|
||||
};
|
||||
use application::activity::use_cases::{
|
||||
archive_activity, create_activity, delete_activity, get_activity, list_activities,
|
||||
rename_activity, set_category,
|
||||
};
|
||||
|
||||
fn store_and_events() -> Arc<InMemoryStore> {
|
||||
Arc::new(InMemoryStore::new())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn creates_activity() {
|
||||
let store = store_and_events();
|
||||
let user = test_user("alice");
|
||||
|
||||
let deps = create_activity::Deps {
|
||||
activities: store.clone(),
|
||||
events: store.clone(),
|
||||
};
|
||||
|
||||
let cmd = CreateActivityCommand {
|
||||
user_id: user.id().clone(),
|
||||
name: ActivityName::new("gaming").unwrap(),
|
||||
category: Some(CategoryName::new("hobbies").unwrap()),
|
||||
};
|
||||
|
||||
let activity = create_activity::execute(cmd, &deps).await.unwrap();
|
||||
|
||||
assert_eq!(activity.name().value(), "gaming");
|
||||
assert_eq!(activity.category().map(|c| c.value()), Some("hobbies"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn renames_activity() {
|
||||
let store = store_and_events();
|
||||
let user = test_user("alice");
|
||||
let activity = test_activity(user.id().clone(), "gaming");
|
||||
store.save(&activity).await.unwrap();
|
||||
|
||||
let deps = rename_activity::Deps {
|
||||
command: store.clone(),
|
||||
query: store.clone(),
|
||||
events: store.clone(),
|
||||
};
|
||||
|
||||
let cmd = RenameActivityCommand {
|
||||
activity_id: activity.id().clone(),
|
||||
new_name: ActivityName::new("video games").unwrap(),
|
||||
};
|
||||
|
||||
rename_activity::execute(cmd, user.id().clone(), &deps)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let fetched = get_activity::execute(
|
||||
activity.id().clone(),
|
||||
user.id().clone(),
|
||||
&get_activity::Deps {
|
||||
query: store.clone(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(fetched.name().value(), "video games");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn sets_category() {
|
||||
let store = store_and_events();
|
||||
let user = test_user("alice");
|
||||
let activity = test_activity(user.id().clone(), "gaming");
|
||||
store.save(&activity).await.unwrap();
|
||||
|
||||
let deps = set_category::Deps {
|
||||
command: store.clone(),
|
||||
query: store.clone(),
|
||||
};
|
||||
|
||||
let cmd = SetCategoryCommand {
|
||||
activity_id: activity.id().clone(),
|
||||
category: Some(CategoryName::new("hobbies").unwrap()),
|
||||
};
|
||||
|
||||
set_category::execute(cmd, user.id().clone(), &deps)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let fetched = get_activity::execute(
|
||||
activity.id().clone(),
|
||||
user.id().clone(),
|
||||
&get_activity::Deps {
|
||||
query: store.clone(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(fetched.category().map(|c| c.value()), Some("hobbies"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn archives_and_unarchives() {
|
||||
let store = store_and_events();
|
||||
let user = test_user("alice");
|
||||
let activity = test_activity(user.id().clone(), "gaming");
|
||||
store.save(&activity).await.unwrap();
|
||||
|
||||
let deps = archive_activity::Deps {
|
||||
command: store.clone(),
|
||||
query: store.clone(),
|
||||
events: store.clone(),
|
||||
};
|
||||
|
||||
archive_activity::archive(activity.id().clone(), user.id().clone(), &deps)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let list_deps = list_activities::Deps {
|
||||
query: store.clone(),
|
||||
};
|
||||
let active = list_activities::active_only(user.id().clone(), &list_deps)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(active.is_empty());
|
||||
|
||||
archive_activity::unarchive(activity.id().clone(), user.id().clone(), &deps)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let active = list_activities::active_only(user.id().clone(), &list_deps)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(active.len(), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn deletes_activity() {
|
||||
let store = store_and_events();
|
||||
let user = test_user("alice");
|
||||
let activity = test_activity(user.id().clone(), "gaming");
|
||||
store.save(&activity).await.unwrap();
|
||||
|
||||
let deps = delete_activity::Deps {
|
||||
command: store.clone(),
|
||||
query: store.clone(),
|
||||
};
|
||||
|
||||
delete_activity::execute(activity.id().clone(), user.id().clone(), &deps)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(store.activity_count(), 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn rename_rejects_different_user() {
|
||||
let store = store_and_events();
|
||||
let user = test_user("alice");
|
||||
let activity = test_activity(user.id().clone(), "gaming");
|
||||
store.save(&activity).await.unwrap();
|
||||
|
||||
let deps = rename_activity::Deps {
|
||||
command: store.clone(),
|
||||
query: store.clone(),
|
||||
events: store.clone(),
|
||||
};
|
||||
|
||||
let cmd = RenameActivityCommand {
|
||||
activity_id: activity.id().clone(),
|
||||
new_name: ActivityName::new("hacking").unwrap(),
|
||||
};
|
||||
|
||||
let result = rename_activity::execute(cmd, UserId::generate(), &deps).await;
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delete_rejects_different_user() {
|
||||
let store = store_and_events();
|
||||
let user = test_user("alice");
|
||||
let activity = test_activity(user.id().clone(), "gaming");
|
||||
store.save(&activity).await.unwrap();
|
||||
|
||||
let deps = delete_activity::Deps {
|
||||
command: store.clone(),
|
||||
query: store.clone(),
|
||||
};
|
||||
|
||||
let result = delete_activity::execute(activity.id().clone(), UserId::generate(), &deps).await;
|
||||
assert!(result.is_err());
|
||||
assert_eq!(store.activity_count(), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn archive_rejects_different_user() {
|
||||
let store = store_and_events();
|
||||
let user = test_user("alice");
|
||||
let activity = test_activity(user.id().clone(), "gaming");
|
||||
store.save(&activity).await.unwrap();
|
||||
|
||||
let deps = archive_activity::Deps {
|
||||
command: store.clone(),
|
||||
query: store.clone(),
|
||||
events: store.clone(),
|
||||
};
|
||||
|
||||
let result = archive_activity::archive(activity.id().clone(), UserId::generate(), &deps).await;
|
||||
assert!(result.is_err());
|
||||
}
|
||||
2
crates/application/tests/auth.rs
Normal file
2
crates/application/tests/auth.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
#[path = "auth/refresh_test.rs"]
|
||||
mod refresh_test;
|
||||
78
crates/application/tests/auth/refresh_test.rs
Normal file
78
crates/application/tests/auth/refresh_test.rs
Normal file
@@ -0,0 +1,78 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use chrono::{Duration, Utc};
|
||||
|
||||
use domain::auth::{RefreshSession, RefreshSessionId};
|
||||
use domain::testing::InMemoryStore;
|
||||
use domain::user::UserId;
|
||||
|
||||
use application::auth::use_cases::refresh;
|
||||
|
||||
fn store_with_session(user_id: UserId, ttl_seconds: i64) -> (Arc<InMemoryStore>, String) {
|
||||
let store = Arc::new(InMemoryStore::new());
|
||||
let session = RefreshSession::new(user_id, ttl_seconds);
|
||||
let token = session.token().to_string();
|
||||
store.refresh_sessions.write().unwrap().push(session);
|
||||
(store, token)
|
||||
}
|
||||
|
||||
fn deps(store: &Arc<InMemoryStore>) -> refresh::Deps {
|
||||
refresh::Deps {
|
||||
auth_service: store.clone(),
|
||||
refresh_session_command: store.clone(),
|
||||
refresh_session_query: store.clone(),
|
||||
refresh_token_ttl_seconds: 86400,
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn rotates_refresh_token() {
|
||||
let user_id = UserId::generate();
|
||||
let (store, token) = store_with_session(user_id, 86400);
|
||||
|
||||
let result = refresh::execute(&token, &deps(&store)).await.unwrap();
|
||||
|
||||
assert!(!result.access_token.token().is_empty());
|
||||
assert_ne!(result.refresh_token, token);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn rejects_unknown_token() {
|
||||
let store = Arc::new(InMemoryStore::new());
|
||||
let result = refresh::execute("nonexistent-token", &deps(&store)).await;
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn rejects_expired_token() {
|
||||
let store = Arc::new(InMemoryStore::new());
|
||||
let user_id = UserId::generate();
|
||||
let session = RefreshSession::from_persistence(
|
||||
RefreshSessionId::generate(),
|
||||
user_id,
|
||||
"expired-token".into(),
|
||||
Utc::now() - Duration::seconds(1),
|
||||
Utc::now() - Duration::hours(1),
|
||||
);
|
||||
store.refresh_sessions.write().unwrap().push(session);
|
||||
|
||||
let result = refresh::execute("expired-token", &deps(&store)).await;
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn old_token_is_revoked_after_rotation() {
|
||||
let user_id = UserId::generate();
|
||||
let (store, token) = store_with_session(user_id, 86400);
|
||||
|
||||
refresh::execute(&token, &deps(&store)).await.unwrap();
|
||||
|
||||
let old_session = store
|
||||
.refresh_sessions
|
||||
.read()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.find(|s| s.token() == token)
|
||||
.cloned();
|
||||
assert!(old_session.is_none());
|
||||
}
|
||||
23
crates/application/tests/entry.rs
Normal file
23
crates/application/tests/entry.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
#[path = "entry/create_entry_test.rs"]
|
||||
mod create_entry_test;
|
||||
|
||||
#[path = "entry/update_entry_test.rs"]
|
||||
mod update_entry_test;
|
||||
|
||||
#[path = "entry/delete_entry_test.rs"]
|
||||
mod delete_entry_test;
|
||||
|
||||
#[path = "entry/get_entry_test.rs"]
|
||||
mod get_entry_test;
|
||||
|
||||
#[path = "entry/list_entries_test.rs"]
|
||||
mod list_entries_test;
|
||||
|
||||
#[path = "entry/filter_test.rs"]
|
||||
mod filter_test;
|
||||
|
||||
#[path = "entry/mood_stats_test.rs"]
|
||||
mod mood_stats_test;
|
||||
|
||||
#[path = "entry/replace_activity_test.rs"]
|
||||
mod replace_activity_test;
|
||||
62
crates/application/tests/entry/create_entry_test.rs
Normal file
62
crates/application/tests/entry/create_entry_test.rs
Normal file
@@ -0,0 +1,62 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::entry::Mood;
|
||||
use domain::testing::{test_entry, test_logged_at, test_user};
|
||||
|
||||
use application::entry::commands::CreateEntryCommand;
|
||||
use application::entry::use_cases::create_entry;
|
||||
|
||||
fn deps() -> (Arc<domain::testing::InMemoryStore>, create_entry::Deps) {
|
||||
let store = Arc::new(domain::testing::InMemoryStore::new());
|
||||
let deps = create_entry::Deps {
|
||||
entries: store.clone(),
|
||||
events: store.clone(),
|
||||
};
|
||||
(store, deps)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn creates_entry_with_required_fields() {
|
||||
let (store, deps) = deps();
|
||||
let user = test_user("alice");
|
||||
|
||||
let cmd = CreateEntryCommand {
|
||||
user_id: user.id().clone(),
|
||||
mood: Mood::Good,
|
||||
logged_at: test_logged_at(),
|
||||
activities: vec![],
|
||||
content: None,
|
||||
photos: vec![],
|
||||
voice_memos: vec![],
|
||||
};
|
||||
|
||||
let entry = create_entry::execute(cmd, &deps).await.unwrap();
|
||||
|
||||
assert_eq!(entry.mood(), Mood::Good);
|
||||
assert_eq!(store.entry_count(), 1);
|
||||
assert_eq!(store.published_events().len(), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn creates_entry_with_all_optional_fields() {
|
||||
let (store, deps) = deps();
|
||||
let user = test_user("bob");
|
||||
let activity = domain::testing::test_activity(user.id().clone(), "gaming");
|
||||
|
||||
let cmd = CreateEntryCommand {
|
||||
user_id: user.id().clone(),
|
||||
mood: Mood::Rad,
|
||||
logged_at: test_logged_at(),
|
||||
activities: vec![activity.id().clone()],
|
||||
content: Some(domain::testing::test_content("great day")),
|
||||
photos: vec![],
|
||||
voice_memos: vec![],
|
||||
};
|
||||
|
||||
let entry = create_entry::execute(cmd, &deps).await.unwrap();
|
||||
|
||||
assert_eq!(entry.mood(), Mood::Rad);
|
||||
assert_eq!(entry.activities().len(), 1);
|
||||
assert!(entry.content().is_some());
|
||||
assert_eq!(store.entry_count(), 1);
|
||||
}
|
||||
69
crates/application/tests/entry/delete_entry_test.rs
Normal file
69
crates/application/tests/entry/delete_entry_test.rs
Normal file
@@ -0,0 +1,69 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::entry::Mood;
|
||||
use domain::ports::MoodEntryCommandPort;
|
||||
use domain::testing::{InMemoryStore, test_entry, test_user};
|
||||
use domain::user::UserId;
|
||||
|
||||
use application::entry::use_cases::delete_entry;
|
||||
|
||||
#[tokio::test]
|
||||
async fn deletes_existing_entry() {
|
||||
let store = Arc::new(InMemoryStore::new());
|
||||
let user = test_user("alice");
|
||||
let entry = test_entry(user.id().clone(), Mood::Good);
|
||||
store.save(&entry).await.unwrap();
|
||||
|
||||
let deps = delete_entry::Deps {
|
||||
command: store.clone(),
|
||||
query: store.clone(),
|
||||
events: store.clone(),
|
||||
media_storage: store.clone(),
|
||||
};
|
||||
|
||||
delete_entry::execute(entry.id().clone(), user.id().clone(), &deps)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(store.entry_count(), 0);
|
||||
assert_eq!(store.published_events().len(), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn deleting_nonexistent_entry_fails() {
|
||||
let store = Arc::new(InMemoryStore::new());
|
||||
let deps = delete_entry::Deps {
|
||||
command: store.clone(),
|
||||
query: store.clone(),
|
||||
events: store.clone(),
|
||||
media_storage: store.clone(),
|
||||
};
|
||||
|
||||
let result = delete_entry::execute(
|
||||
domain::entry::MoodEntryId::generate(),
|
||||
UserId::generate(),
|
||||
&deps,
|
||||
)
|
||||
.await;
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn rejects_delete_by_different_user() {
|
||||
let store = Arc::new(InMemoryStore::new());
|
||||
let user = test_user("alice");
|
||||
let entry = test_entry(user.id().clone(), Mood::Good);
|
||||
store.save(&entry).await.unwrap();
|
||||
|
||||
let deps = delete_entry::Deps {
|
||||
command: store.clone(),
|
||||
query: store.clone(),
|
||||
events: store.clone(),
|
||||
media_storage: store.clone(),
|
||||
};
|
||||
|
||||
let other_user = UserId::generate();
|
||||
let result = delete_entry::execute(entry.id().clone(), other_user, &deps).await;
|
||||
assert!(result.is_err());
|
||||
assert_eq!(store.entry_count(), 1);
|
||||
}
|
||||
67
crates/application/tests/entry/filter_test.rs
Normal file
67
crates/application/tests/entry/filter_test.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::entry::Mood;
|
||||
use domain::ports::MoodEntryCommandPort;
|
||||
use domain::testing::{InMemoryStore, test_activity, test_entry, test_user};
|
||||
|
||||
use application::entry::queries::{FilterByActivityQuery, FilterByMoodQuery};
|
||||
use application::entry::use_cases::{filter_by_activity, filter_by_mood};
|
||||
|
||||
#[tokio::test]
|
||||
async fn filters_by_mood() {
|
||||
let store = Arc::new(InMemoryStore::new());
|
||||
let user = test_user("alice");
|
||||
|
||||
store
|
||||
.save(&test_entry(user.id().clone(), Mood::Good))
|
||||
.await
|
||||
.unwrap();
|
||||
store
|
||||
.save(&test_entry(user.id().clone(), Mood::Good))
|
||||
.await
|
||||
.unwrap();
|
||||
store
|
||||
.save(&test_entry(user.id().clone(), Mood::Bad))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let deps = filter_by_mood::Deps {
|
||||
query: store.clone(),
|
||||
};
|
||||
|
||||
let query = FilterByMoodQuery {
|
||||
user_id: user.id().clone(),
|
||||
mood: Mood::Good,
|
||||
};
|
||||
|
||||
let entries = filter_by_mood::execute(query, &deps).await.unwrap();
|
||||
assert_eq!(entries.len(), 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn filters_by_activity() {
|
||||
let store = Arc::new(InMemoryStore::new());
|
||||
let user = test_user("alice");
|
||||
let activity = test_activity(user.id().clone(), "exercise");
|
||||
|
||||
let mut tagged = test_entry(user.id().clone(), Mood::Good);
|
||||
tagged.set_activities(vec![activity.id().clone()]);
|
||||
store.save(&tagged).await.unwrap();
|
||||
|
||||
store
|
||||
.save(&test_entry(user.id().clone(), Mood::Meh))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let deps = filter_by_activity::Deps {
|
||||
query: store.clone(),
|
||||
};
|
||||
|
||||
let query = FilterByActivityQuery {
|
||||
user_id: user.id().clone(),
|
||||
activity_id: activity.id().clone(),
|
||||
};
|
||||
|
||||
let entries = filter_by_activity::execute(query, &deps).await.unwrap();
|
||||
assert_eq!(entries.len(), 1);
|
||||
}
|
||||
57
crates/application/tests/entry/get_entry_test.rs
Normal file
57
crates/application/tests/entry/get_entry_test.rs
Normal file
@@ -0,0 +1,57 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::entry::Mood;
|
||||
use domain::ports::MoodEntryCommandPort;
|
||||
use domain::testing::{InMemoryStore, test_entry, test_user};
|
||||
use domain::user::UserId;
|
||||
|
||||
use application::entry::use_cases::get_entry;
|
||||
|
||||
#[tokio::test]
|
||||
async fn returns_existing_entry() {
|
||||
let store = Arc::new(InMemoryStore::new());
|
||||
let user = test_user("alice");
|
||||
let entry = test_entry(user.id().clone(), Mood::Good);
|
||||
store.save(&entry).await.unwrap();
|
||||
|
||||
let deps = get_entry::Deps {
|
||||
query: store.clone(),
|
||||
};
|
||||
|
||||
let result = get_entry::execute(entry.id().clone(), user.id().clone(), &deps)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(result.mood(), Mood::Good);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn returns_error_for_nonexistent_entry() {
|
||||
let store = Arc::new(InMemoryStore::new());
|
||||
let deps = get_entry::Deps {
|
||||
query: store.clone(),
|
||||
};
|
||||
|
||||
let result = get_entry::execute(
|
||||
domain::entry::MoodEntryId::generate(),
|
||||
UserId::generate(),
|
||||
&deps,
|
||||
)
|
||||
.await;
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn rejects_access_by_different_user() {
|
||||
let store = Arc::new(InMemoryStore::new());
|
||||
let user = test_user("alice");
|
||||
let entry = test_entry(user.id().clone(), Mood::Good);
|
||||
store.save(&entry).await.unwrap();
|
||||
|
||||
let deps = get_entry::Deps {
|
||||
query: store.clone(),
|
||||
};
|
||||
|
||||
let other_user = UserId::generate();
|
||||
let result = get_entry::execute(entry.id().clone(), other_user, &deps).await;
|
||||
assert!(result.is_err());
|
||||
}
|
||||
60
crates/application/tests/entry/list_entries_test.rs
Normal file
60
crates/application/tests/entry/list_entries_test.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::entry::Mood;
|
||||
use domain::ports::MoodEntryCommandPort;
|
||||
use domain::testing::{InMemoryStore, test_entry, test_user};
|
||||
|
||||
use application::entry::queries::ListEntriesQuery;
|
||||
use application::entry::use_cases::list_entries;
|
||||
|
||||
#[tokio::test]
|
||||
async fn lists_entries_for_user() {
|
||||
let store = Arc::new(InMemoryStore::new());
|
||||
let alice = test_user("alice");
|
||||
let bob = test_user("bob");
|
||||
|
||||
store
|
||||
.save(&test_entry(alice.id().clone(), Mood::Good))
|
||||
.await
|
||||
.unwrap();
|
||||
store
|
||||
.save(&test_entry(alice.id().clone(), Mood::Rad))
|
||||
.await
|
||||
.unwrap();
|
||||
store
|
||||
.save(&test_entry(bob.id().clone(), Mood::Meh))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let deps = list_entries::Deps {
|
||||
query: store.clone(),
|
||||
};
|
||||
|
||||
let query = ListEntriesQuery {
|
||||
limit: None,
|
||||
offset: None,
|
||||
user_id: alice.id().clone(),
|
||||
range: None,
|
||||
};
|
||||
|
||||
let entries = list_entries::execute(query, &deps).await.unwrap();
|
||||
assert_eq!(entries.len(), 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn returns_empty_for_user_with_no_entries() {
|
||||
let store = Arc::new(InMemoryStore::new());
|
||||
let deps = list_entries::Deps {
|
||||
query: store.clone(),
|
||||
};
|
||||
|
||||
let query = ListEntriesQuery {
|
||||
limit: None,
|
||||
offset: None,
|
||||
user_id: test_user("nobody").id().clone(),
|
||||
range: None,
|
||||
};
|
||||
|
||||
let entries = list_entries::execute(query, &deps).await.unwrap();
|
||||
assert!(entries.is_empty());
|
||||
}
|
||||
62
crates/application/tests/entry/mood_stats_test.rs
Normal file
62
crates/application/tests/entry/mood_stats_test.rs
Normal file
@@ -0,0 +1,62 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::entry::Mood;
|
||||
use domain::ports::MoodEntryCommandPort;
|
||||
use domain::testing::{InMemoryStore, test_entry_days_ago, test_user};
|
||||
|
||||
use application::entry::queries::MoodStatsQuery;
|
||||
use application::entry::use_cases::get_mood_stats;
|
||||
|
||||
#[tokio::test]
|
||||
async fn computes_stats_across_entries() {
|
||||
let store = Arc::new(InMemoryStore::new());
|
||||
let user = test_user("alice");
|
||||
|
||||
store
|
||||
.save(&test_entry_days_ago(user.id().clone(), Mood::Good, 0))
|
||||
.await
|
||||
.unwrap();
|
||||
store
|
||||
.save(&test_entry_days_ago(user.id().clone(), Mood::Rad, 1))
|
||||
.await
|
||||
.unwrap();
|
||||
store
|
||||
.save(&test_entry_days_ago(user.id().clone(), Mood::Meh, 2))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let deps = get_mood_stats::Deps {
|
||||
query: store.clone(),
|
||||
};
|
||||
|
||||
let query = MoodStatsQuery {
|
||||
user_id: user.id().clone(),
|
||||
range: None,
|
||||
};
|
||||
|
||||
let stats = get_mood_stats::execute(query, &deps).await.unwrap();
|
||||
|
||||
assert_eq!(stats.total_entries, 3);
|
||||
assert!(stats.average.is_some());
|
||||
assert_eq!(stats.current_streak, 3);
|
||||
assert!(!stats.frequency.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn empty_entries_give_zero_stats() {
|
||||
let store = Arc::new(InMemoryStore::new());
|
||||
let deps = get_mood_stats::Deps {
|
||||
query: store.clone(),
|
||||
};
|
||||
|
||||
let query = MoodStatsQuery {
|
||||
user_id: test_user("nobody").id().clone(),
|
||||
range: None,
|
||||
};
|
||||
|
||||
let stats = get_mood_stats::execute(query, &deps).await.unwrap();
|
||||
|
||||
assert_eq!(stats.total_entries, 0);
|
||||
assert!(stats.average.is_none());
|
||||
assert_eq!(stats.current_streak, 0);
|
||||
}
|
||||
71
crates/application/tests/entry/replace_activity_test.rs
Normal file
71
crates/application/tests/entry/replace_activity_test.rs
Normal file
@@ -0,0 +1,71 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::activity::ActivityId;
|
||||
use domain::ports::ActivityCommandPort;
|
||||
use domain::testing::{InMemoryStore, test_activity, test_user};
|
||||
use domain::user::UserId;
|
||||
|
||||
use application::entry::use_cases::replace_activity;
|
||||
|
||||
#[tokio::test]
|
||||
async fn replaces_activity_on_entries() {
|
||||
let store = Arc::new(InMemoryStore::new());
|
||||
let user = test_user("alice");
|
||||
let old = test_activity(user.id().clone(), "gaming");
|
||||
let new = test_activity(user.id().clone(), "video games");
|
||||
store.save(&old).await.unwrap();
|
||||
store.save(&new).await.unwrap();
|
||||
|
||||
let deps = replace_activity::Deps {
|
||||
entry_command: store.clone(),
|
||||
activity_query: store.clone(),
|
||||
};
|
||||
|
||||
let result =
|
||||
replace_activity::execute(user.id().clone(), old.id().clone(), new.id().clone(), &deps)
|
||||
.await;
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn rejects_nonexistent_target_activity() {
|
||||
let store = Arc::new(InMemoryStore::new());
|
||||
let user = test_user("alice");
|
||||
|
||||
let deps = replace_activity::Deps {
|
||||
entry_command: store.clone(),
|
||||
activity_query: store.clone(),
|
||||
};
|
||||
|
||||
let result = replace_activity::execute(
|
||||
user.id().clone(),
|
||||
ActivityId::generate(),
|
||||
ActivityId::generate(),
|
||||
&deps,
|
||||
)
|
||||
.await;
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn rejects_target_owned_by_different_user() {
|
||||
let store = Arc::new(InMemoryStore::new());
|
||||
let alice = test_user("alice");
|
||||
let bob = test_user("bob");
|
||||
let bobs_activity = test_activity(bob.id().clone(), "gaming");
|
||||
store.save(&bobs_activity).await.unwrap();
|
||||
|
||||
let deps = replace_activity::Deps {
|
||||
entry_command: store.clone(),
|
||||
activity_query: store.clone(),
|
||||
};
|
||||
|
||||
let result = replace_activity::execute(
|
||||
alice.id().clone(),
|
||||
ActivityId::generate(),
|
||||
bobs_activity.id().clone(),
|
||||
&deps,
|
||||
)
|
||||
.await;
|
||||
assert!(result.is_err());
|
||||
}
|
||||
93
crates/application/tests/entry/update_entry_test.rs
Normal file
93
crates/application/tests/entry/update_entry_test.rs
Normal file
@@ -0,0 +1,93 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::entry::Mood;
|
||||
use domain::ports::MoodEntryCommandPort;
|
||||
use domain::testing::{InMemoryStore, test_entry, test_logged_at, test_user};
|
||||
use domain::user::UserId;
|
||||
|
||||
use application::entry::commands::UpdateEntryCommand;
|
||||
use application::entry::use_cases::update_entry;
|
||||
|
||||
async fn setup() -> (
|
||||
Arc<InMemoryStore>,
|
||||
update_entry::Deps,
|
||||
domain::entry::MoodEntry,
|
||||
UserId,
|
||||
) {
|
||||
let store = Arc::new(InMemoryStore::new());
|
||||
let user = test_user("alice");
|
||||
let entry = test_entry(user.id().clone(), Mood::Meh);
|
||||
store.save(&entry).await.unwrap();
|
||||
|
||||
let deps = update_entry::Deps {
|
||||
command: store.clone(),
|
||||
query: store.clone(),
|
||||
media_storage: store.clone(),
|
||||
events: store.clone(),
|
||||
};
|
||||
|
||||
(store, deps, entry, user.id().clone())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn updates_mood() {
|
||||
let (store, deps, entry, user_id) = setup().await;
|
||||
|
||||
let cmd = UpdateEntryCommand {
|
||||
entry_id: entry.id().clone(),
|
||||
mood: Mood::Rad,
|
||||
logged_at: test_logged_at(),
|
||||
activities: vec![],
|
||||
content: None,
|
||||
photos: vec![],
|
||||
voice_memos: vec![],
|
||||
};
|
||||
|
||||
let updated = update_entry::execute(cmd, user_id, &deps).await.unwrap();
|
||||
|
||||
assert_eq!(updated.mood(), Mood::Rad);
|
||||
assert_eq!(store.published_events().len(), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn updating_nonexistent_entry_fails() {
|
||||
let store = Arc::new(InMemoryStore::new());
|
||||
let deps = update_entry::Deps {
|
||||
command: store.clone(),
|
||||
query: store.clone(),
|
||||
media_storage: store.clone(),
|
||||
events: store.clone(),
|
||||
};
|
||||
|
||||
let cmd = UpdateEntryCommand {
|
||||
entry_id: domain::entry::MoodEntryId::generate(),
|
||||
mood: Mood::Good,
|
||||
logged_at: test_logged_at(),
|
||||
activities: vec![],
|
||||
content: None,
|
||||
photos: vec![],
|
||||
voice_memos: vec![],
|
||||
};
|
||||
|
||||
let result = update_entry::execute(cmd, UserId::generate(), &deps).await;
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn rejects_update_by_different_user() {
|
||||
let (_store, deps, entry, _user_id) = setup().await;
|
||||
|
||||
let cmd = UpdateEntryCommand {
|
||||
entry_id: entry.id().clone(),
|
||||
mood: Mood::Rad,
|
||||
logged_at: test_logged_at(),
|
||||
activities: vec![],
|
||||
content: None,
|
||||
photos: vec![],
|
||||
voice_memos: vec![],
|
||||
};
|
||||
|
||||
let other_user = UserId::generate();
|
||||
let result = update_entry::execute(cmd, other_user, &deps).await;
|
||||
assert!(result.is_err());
|
||||
}
|
||||
2
crates/application/tests/reminder.rs
Normal file
2
crates/application/tests/reminder.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
#[path = "reminder/crud_test.rs"]
|
||||
mod crud_test;
|
||||
119
crates/application/tests/reminder/crud_test.rs
Normal file
119
crates/application/tests/reminder/crud_test.rs
Normal file
@@ -0,0 +1,119 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use chrono::NaiveTime;
|
||||
|
||||
use domain::ports::ReminderCommandPort;
|
||||
use domain::reminder::DaySchedule;
|
||||
use domain::testing::{InMemoryStore, test_reminder, test_user};
|
||||
|
||||
use application::reminder::commands::{CreateReminderCommand, UpdateReminderCommand};
|
||||
use application::reminder::use_cases::{
|
||||
create_reminder, delete_reminder, get_reminder, list_reminders, update_reminder,
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn creates_reminder() {
|
||||
let store = Arc::new(InMemoryStore::new());
|
||||
let user = test_user("alice");
|
||||
|
||||
let deps = create_reminder::Deps {
|
||||
reminders: store.clone(),
|
||||
events: store.clone(),
|
||||
};
|
||||
|
||||
let time = NaiveTime::from_hms_opt(20, 0, 0).unwrap();
|
||||
let cmd = CreateReminderCommand {
|
||||
user_id: user.id().clone(),
|
||||
schedule: DaySchedule::every_day_at(time),
|
||||
};
|
||||
|
||||
let reminder = create_reminder::execute(cmd, &deps).await.unwrap();
|
||||
|
||||
assert!(reminder.is_enabled());
|
||||
assert_eq!(store.published_events().len(), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn updates_and_disables_reminder() {
|
||||
let store = Arc::new(InMemoryStore::new());
|
||||
let user = test_user("alice");
|
||||
let reminder = test_reminder(user.id().clone());
|
||||
store.save(&reminder).await.unwrap();
|
||||
|
||||
let deps = update_reminder::Deps {
|
||||
command: store.clone(),
|
||||
query: store.clone(),
|
||||
events: store.clone(),
|
||||
};
|
||||
|
||||
let cmd = UpdateReminderCommand {
|
||||
reminder_id: reminder.id().clone(),
|
||||
schedule: None,
|
||||
enabled: Some(false),
|
||||
};
|
||||
|
||||
let updated = update_reminder::execute(cmd, user.id().clone(), &deps)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(!updated.is_enabled());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn lists_reminders_for_user() {
|
||||
let store = Arc::new(InMemoryStore::new());
|
||||
let user = test_user("alice");
|
||||
store.save(&test_reminder(user.id().clone())).await.unwrap();
|
||||
store.save(&test_reminder(user.id().clone())).await.unwrap();
|
||||
|
||||
let deps = list_reminders::Deps {
|
||||
query: store.clone(),
|
||||
};
|
||||
let reminders = list_reminders::execute(user.id().clone(), &deps)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(reminders.len(), 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn gets_single_reminder() {
|
||||
let store = Arc::new(InMemoryStore::new());
|
||||
let user = test_user("alice");
|
||||
let reminder = test_reminder(user.id().clone());
|
||||
store.save(&reminder).await.unwrap();
|
||||
|
||||
let deps = get_reminder::Deps {
|
||||
query: store.clone(),
|
||||
};
|
||||
let result = get_reminder::execute(reminder.id().clone(), user.id().clone(), &deps)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(*result.id(), *reminder.id());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn deletes_reminder() {
|
||||
let store = Arc::new(InMemoryStore::new());
|
||||
let user = test_user("alice");
|
||||
let reminder = test_reminder(user.id().clone());
|
||||
store.save(&reminder).await.unwrap();
|
||||
|
||||
let deps = delete_reminder::Deps {
|
||||
command: store.clone(),
|
||||
query: store.clone(),
|
||||
events: store.clone(),
|
||||
};
|
||||
|
||||
delete_reminder::execute(reminder.id().clone(), user.id().clone(), &deps)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let list_deps = list_reminders::Deps {
|
||||
query: store.clone(),
|
||||
};
|
||||
let reminders = list_reminders::execute(user.id().clone(), &list_deps)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(reminders.is_empty());
|
||||
}
|
||||
17
crates/application/tests/user.rs
Normal file
17
crates/application/tests/user.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
#[path = "user/register_test.rs"]
|
||||
mod register_test;
|
||||
|
||||
#[path = "user/login_test.rs"]
|
||||
mod login_test;
|
||||
|
||||
#[path = "user/profile_test.rs"]
|
||||
mod profile_test;
|
||||
|
||||
#[path = "user/change_password_test.rs"]
|
||||
mod change_password_test;
|
||||
|
||||
#[path = "user/clear_data_test.rs"]
|
||||
mod clear_data_test;
|
||||
|
||||
#[path = "user/delete_user_test.rs"]
|
||||
mod delete_user_test;
|
||||
67
crates/application/tests/user/change_password_test.rs
Normal file
67
crates/application/tests/user/change_password_test.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::ports::UserCommandPort;
|
||||
use domain::testing::{FakePasswordHasher, InMemoryStore};
|
||||
use domain::user::{Email, PasswordHash, User, Username};
|
||||
|
||||
use application::user::commands::ChangePasswordCommand;
|
||||
use application::user::use_cases::change_password;
|
||||
|
||||
async fn setup() -> (Arc<InMemoryStore>, change_password::Deps, User) {
|
||||
let store = Arc::new(InMemoryStore::new());
|
||||
let user = User::new(
|
||||
Username::new("alice").unwrap(),
|
||||
Email::new("alice@example.com").unwrap(),
|
||||
PasswordHash::new("hashed:secret".into()),
|
||||
);
|
||||
store.save(&user).await.unwrap();
|
||||
|
||||
let deps = change_password::Deps {
|
||||
user_command: store.clone(),
|
||||
user_query: store.clone(),
|
||||
password_hasher: Arc::new(FakePasswordHasher),
|
||||
};
|
||||
|
||||
(store, deps, user)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn changes_password_with_correct_current() {
|
||||
let (_store, deps, user) = setup().await;
|
||||
|
||||
let cmd = ChangePasswordCommand {
|
||||
user_id: user.id().clone(),
|
||||
current_password: "secret".into(),
|
||||
new_password: "new-secret".into(),
|
||||
};
|
||||
|
||||
change_password::execute(cmd, &deps).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn rejects_wrong_current_password() {
|
||||
let (_store, deps, user) = setup().await;
|
||||
|
||||
let cmd = ChangePasswordCommand {
|
||||
user_id: user.id().clone(),
|
||||
current_password: "wrong".into(),
|
||||
new_password: "new-secret".into(),
|
||||
};
|
||||
|
||||
let result = change_password::execute(cmd, &deps).await;
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn rejects_nonexistent_user() {
|
||||
let (_store, deps, _user) = setup().await;
|
||||
|
||||
let cmd = ChangePasswordCommand {
|
||||
user_id: domain::user::UserId::generate(),
|
||||
current_password: "secret".into(),
|
||||
new_password: "new-secret".into(),
|
||||
};
|
||||
|
||||
let result = change_password::execute(cmd, &deps).await;
|
||||
assert!(result.is_err());
|
||||
}
|
||||
32
crates/application/tests/user/clear_data_test.rs
Normal file
32
crates/application/tests/user/clear_data_test.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::entry::Mood;
|
||||
use domain::ports::{ActivityCommandPort, MoodEntryCommandPort, ReminderCommandPort};
|
||||
use domain::testing::{InMemoryStore, test_activity, test_entry, test_reminder, test_user};
|
||||
|
||||
use application::user::use_cases::clear_data;
|
||||
|
||||
#[tokio::test]
|
||||
async fn clears_all_user_data() {
|
||||
let store = Arc::new(InMemoryStore::new());
|
||||
let user = test_user("alice");
|
||||
let entry = test_entry(user.id().clone(), Mood::Good);
|
||||
let activity = test_activity(user.id().clone(), "gaming");
|
||||
let reminder = test_reminder(user.id().clone());
|
||||
|
||||
MoodEntryCommandPort::save(&*store, &entry).await.unwrap();
|
||||
ActivityCommandPort::save(&*store, &activity).await.unwrap();
|
||||
ReminderCommandPort::save(&*store, &reminder).await.unwrap();
|
||||
|
||||
let deps = clear_data::Deps {
|
||||
entry_query: store.clone(),
|
||||
cascade: store.clone(),
|
||||
media_storage: store.clone(),
|
||||
};
|
||||
|
||||
clear_data::execute(user.id().clone(), &deps).await.unwrap();
|
||||
|
||||
assert_eq!(store.entry_count(), 0);
|
||||
assert_eq!(store.activity_count(), 0);
|
||||
assert_eq!(store.reminder_count(), 0);
|
||||
}
|
||||
50
crates/application/tests/user/delete_user_test.rs
Normal file
50
crates/application/tests/user/delete_user_test.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::entry::Mood;
|
||||
use domain::ports::{
|
||||
ActivityCommandPort, MoodEntryCommandPort, ReminderCommandPort, UserCommandPort,
|
||||
};
|
||||
use domain::testing::{InMemoryStore, test_activity, test_entry, test_reminder, test_user};
|
||||
use domain::user::UserId;
|
||||
|
||||
use application::user::use_cases::delete_user;
|
||||
|
||||
fn deps(store: &Arc<InMemoryStore>) -> delete_user::Deps {
|
||||
delete_user::Deps {
|
||||
user_query: store.clone(),
|
||||
entry_query: store.clone(),
|
||||
cascade: store.clone(),
|
||||
media_storage: store.clone(),
|
||||
events: store.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn deletes_user_and_all_data() {
|
||||
let store = Arc::new(InMemoryStore::new());
|
||||
let user = test_user("alice");
|
||||
let entry = test_entry(user.id().clone(), Mood::Good);
|
||||
let activity = test_activity(user.id().clone(), "gaming");
|
||||
let reminder = test_reminder(user.id().clone());
|
||||
|
||||
UserCommandPort::save(&*store, &user).await.unwrap();
|
||||
MoodEntryCommandPort::save(&*store, &entry).await.unwrap();
|
||||
ActivityCommandPort::save(&*store, &activity).await.unwrap();
|
||||
ReminderCommandPort::save(&*store, &reminder).await.unwrap();
|
||||
|
||||
delete_user::execute(user.id().clone(), &deps(&store))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(store.entry_count(), 0);
|
||||
assert_eq!(store.activity_count(), 0);
|
||||
assert_eq!(store.reminder_count(), 0);
|
||||
assert_eq!(store.user_count(), 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn rejects_nonexistent_user() {
|
||||
let store = Arc::new(InMemoryStore::new());
|
||||
let result = delete_user::execute(UserId::generate(), &deps(&store)).await;
|
||||
assert!(result.is_err());
|
||||
}
|
||||
82
crates/application/tests/user/login_test.rs
Normal file
82
crates/application/tests/user/login_test.rs
Normal file
@@ -0,0 +1,82 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::ports::UserCommandPort;
|
||||
use domain::testing::{FakePasswordHasher, InMemoryStore};
|
||||
use domain::user::{Email, PasswordHash, User, Username};
|
||||
|
||||
use application::auth::use_cases::login;
|
||||
use application::user::commands::LoginCommand;
|
||||
|
||||
async fn setup() -> (Arc<InMemoryStore>, login::Deps) {
|
||||
let store = Arc::new(InMemoryStore::new());
|
||||
let user = User::new(
|
||||
Username::new("alice").unwrap(),
|
||||
Email::new("alice@example.com").unwrap(),
|
||||
PasswordHash::new("hashed:secret".into()),
|
||||
);
|
||||
store.save(&user).await.unwrap();
|
||||
|
||||
let deps = login::Deps {
|
||||
user_query: store.clone(),
|
||||
password_hasher: Arc::new(FakePasswordHasher),
|
||||
auth_service: store.clone(),
|
||||
refresh_session_command: store.clone(),
|
||||
refresh_token_ttl_seconds: 86400,
|
||||
};
|
||||
|
||||
(store, deps)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn login_with_username() {
|
||||
let (_, deps) = setup().await;
|
||||
|
||||
let cmd = LoginCommand {
|
||||
identifier: "alice".into(),
|
||||
password: "secret".into(),
|
||||
};
|
||||
|
||||
let result = login::execute(cmd, &deps).await.unwrap();
|
||||
assert_eq!(result.user.username().value(), "alice");
|
||||
assert!(!result.access_token.token().is_empty());
|
||||
assert!(!result.refresh_token.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn login_with_email() {
|
||||
let (_, deps) = setup().await;
|
||||
|
||||
let cmd = LoginCommand {
|
||||
identifier: "alice@example.com".into(),
|
||||
password: "secret".into(),
|
||||
};
|
||||
|
||||
let result = login::execute(cmd, &deps).await.unwrap();
|
||||
assert_eq!(result.user.username().value(), "alice");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn wrong_password_fails() {
|
||||
let (_, deps) = setup().await;
|
||||
|
||||
let cmd = LoginCommand {
|
||||
identifier: "alice".into(),
|
||||
password: "wrong".into(),
|
||||
};
|
||||
|
||||
let result = login::execute(cmd, &deps).await;
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn nonexistent_user_fails() {
|
||||
let (_, deps) = setup().await;
|
||||
|
||||
let cmd = LoginCommand {
|
||||
identifier: "nobody".into(),
|
||||
password: "secret".into(),
|
||||
};
|
||||
|
||||
let result = login::execute(cmd, &deps).await;
|
||||
assert!(result.is_err());
|
||||
}
|
||||
47
crates/application/tests/user/profile_test.rs
Normal file
47
crates/application/tests/user/profile_test.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::ports::UserCommandPort;
|
||||
use domain::testing::{InMemoryStore, test_user};
|
||||
use domain::user::{DisplayName, Timezone};
|
||||
|
||||
use application::user::commands::UpdateProfileCommand;
|
||||
use application::user::use_cases::{get_profile, update_profile};
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_profile_returns_user() {
|
||||
let store = Arc::new(InMemoryStore::new());
|
||||
let user = test_user("alice");
|
||||
store.save(&user).await.unwrap();
|
||||
|
||||
let deps = get_profile::Deps {
|
||||
user_query: store.clone(),
|
||||
};
|
||||
|
||||
let result = get_profile::execute(user.id().clone(), &deps)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(result.username().value(), "alice");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn updates_display_name_and_timezone() {
|
||||
let store = Arc::new(InMemoryStore::new());
|
||||
let user = test_user("alice");
|
||||
store.save(&user).await.unwrap();
|
||||
|
||||
let deps = update_profile::Deps {
|
||||
user_command: store.clone(),
|
||||
user_query: store.clone(),
|
||||
};
|
||||
|
||||
let cmd = UpdateProfileCommand {
|
||||
user_id: user.id().clone(),
|
||||
display_name: Some(DisplayName::new("Alice K").unwrap()),
|
||||
timezone: Some(Timezone::new("Europe/Warsaw").unwrap()),
|
||||
};
|
||||
|
||||
let updated = update_profile::execute(cmd, &deps).await.unwrap();
|
||||
|
||||
assert_eq!(updated.display_name().map(|d| d.value()), Some("Alice K"));
|
||||
assert_eq!(updated.timezone().map(|t| t.value()), Some("Europe/Warsaw"));
|
||||
}
|
||||
79
crates/application/tests/user/register_test.rs
Normal file
79
crates/application/tests/user/register_test.rs
Normal file
@@ -0,0 +1,79 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::testing::{FakePasswordHasher, InMemoryStore};
|
||||
use domain::user::Username;
|
||||
|
||||
use application::user::commands::RegisterCommand;
|
||||
use application::user::use_cases::register;
|
||||
|
||||
fn deps() -> (Arc<InMemoryStore>, register::Deps) {
|
||||
let store = Arc::new(InMemoryStore::new());
|
||||
let deps = register::Deps {
|
||||
user_command: store.clone(),
|
||||
user_query: store.clone(),
|
||||
activity_command: store.clone(),
|
||||
password_hasher: Arc::new(FakePasswordHasher),
|
||||
events: store.clone(),
|
||||
preset: config::PresetConfig::default(),
|
||||
};
|
||||
(store, deps)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn registers_user_and_seeds_activities() {
|
||||
let (store, deps) = deps();
|
||||
|
||||
let cmd = RegisterCommand {
|
||||
username: Username::new("alice").unwrap(),
|
||||
email: domain::user::Email::new("alice@example.com").unwrap(),
|
||||
password: "secret123".into(),
|
||||
};
|
||||
|
||||
let user = register::execute(cmd, &deps).await.unwrap();
|
||||
|
||||
assert_eq!(user.username().value(), "alice");
|
||||
assert!(store.activity_count() > 0);
|
||||
assert_eq!(store.published_events().len(), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn rejects_duplicate_username() {
|
||||
let (_, deps) = deps();
|
||||
|
||||
let cmd1 = RegisterCommand {
|
||||
username: Username::new("alice").unwrap(),
|
||||
email: domain::user::Email::new("alice@example.com").unwrap(),
|
||||
password: "secret".into(),
|
||||
};
|
||||
register::execute(cmd1, &deps).await.unwrap();
|
||||
|
||||
let cmd2 = RegisterCommand {
|
||||
username: Username::new("alice").unwrap(),
|
||||
email: domain::user::Email::new("alice2@example.com").unwrap(),
|
||||
password: "secret".into(),
|
||||
};
|
||||
let result = register::execute(cmd2, &deps).await;
|
||||
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn rejects_duplicate_email() {
|
||||
let (_, deps) = deps();
|
||||
|
||||
let cmd1 = RegisterCommand {
|
||||
username: Username::new("alice").unwrap(),
|
||||
email: domain::user::Email::new("alice@example.com").unwrap(),
|
||||
password: "secret".into(),
|
||||
};
|
||||
register::execute(cmd1, &deps).await.unwrap();
|
||||
|
||||
let cmd2 = RegisterCommand {
|
||||
username: Username::new("bob").unwrap(),
|
||||
email: domain::user::Email::new("alice@example.com").unwrap(),
|
||||
password: "secret".into(),
|
||||
};
|
||||
let result = register::execute(cmd2, &deps).await;
|
||||
|
||||
assert!(result.is_err());
|
||||
}
|
||||
Reference in New Issue
Block a user