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());
|
||||
}
|
||||
Reference in New Issue
Block a user