init
Some checks failed
CI / ci (push) Failing after 1m48s

This commit is contained in:
2026-08-25 23:24:36 +02:00
commit 95739892de
466 changed files with 33918 additions and 0 deletions

View File

@@ -0,0 +1,2 @@
#[path = "activity/crud_test.rs"]
mod crud_test;

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

View File

@@ -0,0 +1,2 @@
#[path = "auth/refresh_test.rs"]
mod refresh_test;

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

View 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;

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

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

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

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

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

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

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

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

View File

@@ -0,0 +1,2 @@
#[path = "reminder/crud_test.rs"]
mod crud_test;

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

View 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;

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

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

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

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

View 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"));
}

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