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