230
crates/application/tests/entry/calendar_test.rs
Normal file
230
crates/application/tests/entry/calendar_test.rs
Normal file
@@ -0,0 +1,230 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use chrono::{DateTime, FixedOffset};
|
||||
|
||||
use domain::entry::{DateRange, Mood, MoodEntry};
|
||||
use domain::ports::{MoodEntryCommandPort, UserCommandPort};
|
||||
use domain::testing::{InMemoryStore, test_user};
|
||||
use domain::user::{Timezone, User};
|
||||
|
||||
use application::entry::use_cases::get_calendar::{self, CalendarDay};
|
||||
|
||||
fn user_in(name: &str, timezone: &str) -> User {
|
||||
let mut user = test_user(name);
|
||||
user.update_timezone(Some(Timezone::new(timezone).unwrap()));
|
||||
user
|
||||
}
|
||||
|
||||
fn at(instant: &str) -> DateTime<FixedOffset> {
|
||||
DateTime::parse_from_rfc3339(instant).unwrap()
|
||||
}
|
||||
|
||||
async fn calendar_of(store: &Arc<InMemoryStore>, user: &User) -> Vec<CalendarDay> {
|
||||
let deps = get_calendar::Deps {
|
||||
query: store.clone(),
|
||||
dimensions: Vec::new(),
|
||||
cycles: store.clone(),
|
||||
preferences: store.clone(),
|
||||
users: store.clone(),
|
||||
};
|
||||
|
||||
let range = DateRange::new(
|
||||
at("2026-08-01T00:00:00+00:00"),
|
||||
at("2026-08-31T23:59:59+00:00"),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
get_calendar::execute(user.id().clone(), range, &deps)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
async fn logged(store: &Arc<InMemoryStore>, user: &User, mood: Mood, instant: &str) {
|
||||
let entry = MoodEntry::new(user.id().clone(), mood, at(instant));
|
||||
MoodEntryCommandPort::save(store.as_ref(), &entry)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_day_of_one_awful_and_one_rad_does_not_report_rad() {
|
||||
let store = Arc::new(InMemoryStore::new());
|
||||
let user = user_in("alice", "Europe/Warsaw");
|
||||
UserCommandPort::save(store.as_ref(), &user).await.unwrap();
|
||||
|
||||
logged(&store, &user, Mood::Awful, "2026-08-20T09:00:00+02:00").await;
|
||||
logged(&store, &user, Mood::Rad, "2026-08-20T21:00:00+02:00").await;
|
||||
|
||||
let days = calendar_of(&store, &user).await;
|
||||
|
||||
assert_eq!(days.len(), 1);
|
||||
let mood = days[0].day_mood.expect("the day has entries");
|
||||
assert_eq!(mood.rounded(), Mood::Meh);
|
||||
assert!((mood.value() - 3.0).abs() < f64::EPSILON);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn only_days_that_were_logged_appear() {
|
||||
let store = Arc::new(InMemoryStore::new());
|
||||
let user = user_in("alice", "Europe/Warsaw");
|
||||
UserCommandPort::save(store.as_ref(), &user).await.unwrap();
|
||||
|
||||
logged(&store, &user, Mood::Good, "2026-08-20T09:00:00+02:00").await;
|
||||
logged(&store, &user, Mood::Bad, "2026-08-24T09:00:00+02:00").await;
|
||||
|
||||
let days = calendar_of(&store, &user).await;
|
||||
let dates: Vec<String> = days.iter().map(|day| day.date.to_string()).collect();
|
||||
|
||||
assert_eq!(dates, ["2026-08-20", "2026-08-24"]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_late_evening_entry_belongs_to_the_day_the_user_was_living() {
|
||||
let store = Arc::new(InMemoryStore::new());
|
||||
let user = user_in("alice", "Europe/Warsaw");
|
||||
UserCommandPort::save(store.as_ref(), &user).await.unwrap();
|
||||
|
||||
logged(&store, &user, Mood::Good, "2026-08-20T23:30:00+00:00").await;
|
||||
|
||||
let days = calendar_of(&store, &user).await;
|
||||
let dates: Vec<String> = days.iter().map(|day| day.date.to_string()).collect();
|
||||
|
||||
assert_eq!(dates, ["2026-08-21"]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn every_entry_on_a_day_moves_its_mood() {
|
||||
let store = Arc::new(InMemoryStore::new());
|
||||
let user = user_in("alice", "Europe/Warsaw");
|
||||
UserCommandPort::save(store.as_ref(), &user).await.unwrap();
|
||||
|
||||
logged(&store, &user, Mood::Meh, "2026-08-20T08:00:00+02:00").await;
|
||||
logged(&store, &user, Mood::Meh, "2026-08-20T12:00:00+02:00").await;
|
||||
logged(&store, &user, Mood::Meh, "2026-08-20T16:00:00+02:00").await;
|
||||
logged(&store, &user, Mood::Meh, "2026-08-20T20:00:00+02:00").await;
|
||||
logged(&store, &user, Mood::Awful, "2026-08-20T23:00:00+02:00").await;
|
||||
|
||||
let days = calendar_of(&store, &user).await;
|
||||
let mood = days[0].day_mood.expect("the day has entries");
|
||||
|
||||
assert!(mood.value() < 3.0, "the awful entry was discarded");
|
||||
}
|
||||
|
||||
async fn tracking_cycle_from(store: &Arc<InMemoryStore>, user: &User, starts: &[&str]) {
|
||||
let preference_deps = application::user::use_cases::set_preferences::Deps {
|
||||
command: store.clone(),
|
||||
query: store.clone(),
|
||||
};
|
||||
application::user::use_cases::set_preferences::execute(
|
||||
user.id().clone(),
|
||||
true,
|
||||
&preference_deps,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let cycle_deps = application::cycle::use_cases::record_cycle_start::Deps {
|
||||
command: store.clone(),
|
||||
preferences: store.clone(),
|
||||
};
|
||||
for start in starts {
|
||||
application::cycle::use_cases::record_cycle_start::execute(
|
||||
user.id().clone(),
|
||||
domain::entry::Date::from_persistence(start.parse().unwrap()),
|
||||
&cycle_deps,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_day_carries_no_cycle_day_when_the_cycle_is_not_tracked() {
|
||||
let store = Arc::new(InMemoryStore::new());
|
||||
let user = user_in("alice", "Europe/Warsaw");
|
||||
UserCommandPort::save(store.as_ref(), &user).await.unwrap();
|
||||
tracking_cycle_from(&store, &user, &["2026-08-11"]).await;
|
||||
|
||||
let turn_off = application::user::use_cases::set_preferences::Deps {
|
||||
command: store.clone(),
|
||||
query: store.clone(),
|
||||
};
|
||||
application::user::use_cases::set_preferences::execute(user.id().clone(), false, &turn_off)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
logged(&store, &user, Mood::Good, "2026-08-20T09:00:00+02:00").await;
|
||||
|
||||
let days = calendar_of(&store, &user).await;
|
||||
|
||||
assert!(
|
||||
days[0].cycle_day.is_none(),
|
||||
"a recorded start must stay invisible while tracking is off"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_tracked_day_carries_the_cycle_day_derived_for_it() {
|
||||
let store = Arc::new(InMemoryStore::new());
|
||||
let user = user_in("alice", "Europe/Warsaw");
|
||||
UserCommandPort::save(store.as_ref(), &user).await.unwrap();
|
||||
tracking_cycle_from(&store, &user, &["2026-08-11"]).await;
|
||||
|
||||
logged(&store, &user, Mood::Good, "2026-08-20T09:00:00+02:00").await;
|
||||
|
||||
let days = calendar_of(&store, &user).await;
|
||||
|
||||
assert_eq!(days[0].cycle_day.map(|day| day.value()), Some(10));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_day_before_the_first_recorded_start_has_no_cycle_day() {
|
||||
let store = Arc::new(InMemoryStore::new());
|
||||
let user = user_in("alice", "Europe/Warsaw");
|
||||
UserCommandPort::save(store.as_ref(), &user).await.unwrap();
|
||||
tracking_cycle_from(&store, &user, &["2026-08-25"]).await;
|
||||
|
||||
logged(&store, &user, Mood::Good, "2026-08-20T09:00:00+02:00").await;
|
||||
|
||||
let days = calendar_of(&store, &user).await;
|
||||
|
||||
assert!(days[0].cycle_day.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn correcting_a_start_changes_the_cycle_day_of_every_affected_date() {
|
||||
let store = Arc::new(InMemoryStore::new());
|
||||
let user = user_in("alice", "Europe/Warsaw");
|
||||
UserCommandPort::save(store.as_ref(), &user).await.unwrap();
|
||||
tracking_cycle_from(&store, &user, &["2026-08-11"]).await;
|
||||
|
||||
logged(&store, &user, Mood::Good, "2026-08-20T09:00:00+02:00").await;
|
||||
logged(&store, &user, Mood::Meh, "2026-08-22T09:00:00+02:00").await;
|
||||
|
||||
let before: Vec<u16> = calendar_of(&store, &user)
|
||||
.await
|
||||
.iter()
|
||||
.filter_map(|day| day.cycle_day.map(|cycle| cycle.value()))
|
||||
.collect();
|
||||
|
||||
let forget = application::cycle::use_cases::forget_cycle_start::Deps {
|
||||
command: store.clone(),
|
||||
};
|
||||
application::cycle::use_cases::forget_cycle_start::execute(
|
||||
user.id().clone(),
|
||||
domain::entry::Date::from_persistence("2026-08-11".parse().unwrap()),
|
||||
&forget,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
tracking_cycle_from(&store, &user, &["2026-08-13"]).await;
|
||||
|
||||
let after: Vec<u16> = calendar_of(&store, &user)
|
||||
.await
|
||||
.iter()
|
||||
.filter_map(|day| day.cycle_day.map(|cycle| cycle.value()))
|
||||
.collect();
|
||||
|
||||
assert_eq!(before, [10, 12]);
|
||||
assert_eq!(after, [8, 10], "one correction moved both days");
|
||||
}
|
||||
@@ -1,15 +1,23 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::dimension::{DimensionKind, DimensionValue};
|
||||
use domain::entry::Mood;
|
||||
use domain::testing::{test_entry, test_logged_at, test_user};
|
||||
use domain::ports::EntryDimensionPort;
|
||||
use domain::testing::{InMemoryDimensionStore, InMemoryStore, 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());
|
||||
fn deps() -> (Arc<InMemoryStore>, create_entry::Deps) {
|
||||
let store = Arc::new(InMemoryStore::new());
|
||||
let content: Arc<dyn EntryDimensionPort> =
|
||||
Arc::new(InMemoryDimensionStore::new(DimensionKind::Content));
|
||||
let activities: Arc<dyn EntryDimensionPort> =
|
||||
Arc::new(InMemoryDimensionStore::new(DimensionKind::Activities));
|
||||
|
||||
let deps = create_entry::Deps {
|
||||
entries: store.clone(),
|
||||
dimensions: vec![content, activities],
|
||||
events: store.clone(),
|
||||
};
|
||||
(store, deps)
|
||||
@@ -24,10 +32,7 @@ async fn creates_entry_with_required_fields() {
|
||||
user_id: user.id().clone(),
|
||||
mood: Mood::Good,
|
||||
logged_at: test_logged_at(),
|
||||
activities: vec![],
|
||||
content: None,
|
||||
photos: vec![],
|
||||
voice_memos: vec![],
|
||||
dimensions: vec![],
|
||||
};
|
||||
|
||||
let entry = create_entry::execute(cmd, &deps).await.unwrap();
|
||||
@@ -38,7 +43,7 @@ async fn creates_entry_with_required_fields() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn creates_entry_with_all_optional_fields() {
|
||||
async fn creates_entry_with_optional_dimensions() {
|
||||
let (store, deps) = deps();
|
||||
let user = test_user("bob");
|
||||
let activity = domain::testing::test_activity(user.id().clone(), "gaming");
|
||||
@@ -47,16 +52,14 @@ async fn creates_entry_with_all_optional_fields() {
|
||||
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![],
|
||||
dimensions: vec![
|
||||
DimensionValue::Content(domain::testing::test_content("great day")),
|
||||
DimensionValue::activities(vec![activity.id().clone()]),
|
||||
],
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ async fn deletes_existing_entry() {
|
||||
store.save(&entry).await.unwrap();
|
||||
|
||||
let deps = delete_entry::Deps {
|
||||
dimensions: vec![],
|
||||
command: store.clone(),
|
||||
query: store.clone(),
|
||||
events: store.clone(),
|
||||
@@ -33,6 +34,7 @@ async fn deletes_existing_entry() {
|
||||
async fn deleting_nonexistent_entry_fails() {
|
||||
let store = Arc::new(InMemoryStore::new());
|
||||
let deps = delete_entry::Deps {
|
||||
dimensions: vec![],
|
||||
command: store.clone(),
|
||||
query: store.clone(),
|
||||
events: store.clone(),
|
||||
@@ -56,6 +58,7 @@ async fn rejects_delete_by_different_user() {
|
||||
store.save(&entry).await.unwrap();
|
||||
|
||||
let deps = delete_entry::Deps {
|
||||
dimensions: vec![],
|
||||
command: store.clone(),
|
||||
query: store.clone(),
|
||||
events: store.clone(),
|
||||
|
||||
139
crates/application/tests/entry/dimension_test.rs
Normal file
139
crates/application/tests/entry/dimension_test.rs
Normal file
@@ -0,0 +1,139 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::activity::ActivityId;
|
||||
use domain::attachment::{PhotoId, VoiceMemoId};
|
||||
use domain::dimension::{DimensionKind, DimensionValue};
|
||||
use domain::entry::{Mood, MoodEntry};
|
||||
use domain::ports::EntryDimensionPort;
|
||||
use domain::testing::{InMemoryDimensionStore, test_content, test_logged_at, test_user};
|
||||
|
||||
use application::entry::commands::CreateEntryCommand;
|
||||
use application::entry::composition::EntryComposer;
|
||||
use application::entry::use_cases::create_entry;
|
||||
|
||||
struct Dimensions {
|
||||
content: Arc<InMemoryDimensionStore>,
|
||||
activities: Arc<InMemoryDimensionStore>,
|
||||
photos: Arc<InMemoryDimensionStore>,
|
||||
voice_memos: Arc<InMemoryDimensionStore>,
|
||||
}
|
||||
|
||||
impl Dimensions {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
content: Arc::new(InMemoryDimensionStore::new(DimensionKind::Content)),
|
||||
activities: Arc::new(InMemoryDimensionStore::new(DimensionKind::Activities)),
|
||||
photos: Arc::new(InMemoryDimensionStore::new(DimensionKind::Photos)),
|
||||
voice_memos: Arc::new(InMemoryDimensionStore::new(DimensionKind::VoiceMemos)),
|
||||
}
|
||||
}
|
||||
|
||||
fn ports(&self) -> Vec<Arc<dyn EntryDimensionPort>> {
|
||||
vec![
|
||||
self.content.clone(),
|
||||
self.activities.clone(),
|
||||
self.photos.clone(),
|
||||
self.voice_memos.clone(),
|
||||
]
|
||||
}
|
||||
|
||||
fn composer(&self) -> EntryComposer {
|
||||
EntryComposer::new(self.ports())
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn content_saved_for_an_entry_is_returned_when_that_entry_is_composed() {
|
||||
let dimensions = Dimensions::new();
|
||||
let user = test_user("alice");
|
||||
let entry = MoodEntry::new(user.id().clone(), Mood::Good, test_logged_at());
|
||||
|
||||
dimensions.content.put(
|
||||
entry.id(),
|
||||
DimensionValue::Content(test_content("went for a walk")),
|
||||
);
|
||||
|
||||
let composed = dimensions.composer().compose(vec![entry]).await.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
composed[0].content().map(|c| c.value()),
|
||||
Some("went for a walk")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn an_entry_with_no_content_composes_with_no_dimensions() {
|
||||
let dimensions = Dimensions::new();
|
||||
let user = test_user("carol");
|
||||
let entry = MoodEntry::new(user.id().clone(), Mood::Meh, test_logged_at());
|
||||
|
||||
let composed = dimensions.composer().compose(vec![entry]).await.unwrap();
|
||||
|
||||
assert!(composed[0].dimensions.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn each_entry_in_a_page_composes_with_only_its_own_content() {
|
||||
let dimensions = Dimensions::new();
|
||||
let user = test_user("dave");
|
||||
|
||||
let first = MoodEntry::new(user.id().clone(), Mood::Good, test_logged_at());
|
||||
let second = MoodEntry::new(user.id().clone(), Mood::Bad, test_logged_at());
|
||||
let third = MoodEntry::new(user.id().clone(), Mood::Rad, test_logged_at());
|
||||
|
||||
dimensions
|
||||
.content
|
||||
.put(first.id(), DimensionValue::Content(test_content("mine")));
|
||||
dimensions
|
||||
.content
|
||||
.put(third.id(), DimensionValue::Content(test_content("theirs")));
|
||||
|
||||
let composed = dimensions
|
||||
.composer()
|
||||
.compose(vec![first, second, third])
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(composed[0].content().map(|c| c.value()), Some("mine"));
|
||||
assert!(composed[1].dimensions.is_empty());
|
||||
assert_eq!(composed[2].content().map(|c| c.value()), Some("theirs"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn an_entry_composes_every_kind_of_dimension_it_has() {
|
||||
let dimensions = Dimensions::new();
|
||||
let store = Arc::new(domain::testing::InMemoryStore::new());
|
||||
let user = test_user("erin");
|
||||
|
||||
let activity_id = ActivityId::generate();
|
||||
let photo_id = PhotoId::generate();
|
||||
let voice_memo_id = VoiceMemoId::generate();
|
||||
|
||||
let deps = create_entry::Deps {
|
||||
entries: store.clone(),
|
||||
dimensions: dimensions.ports(),
|
||||
events: store.clone(),
|
||||
};
|
||||
|
||||
let cmd = CreateEntryCommand {
|
||||
user_id: user.id().clone(),
|
||||
mood: Mood::Rad,
|
||||
logged_at: test_logged_at(),
|
||||
dimensions: vec![
|
||||
DimensionValue::Content(test_content("a full day")),
|
||||
DimensionValue::Activities(vec![activity_id.clone()]),
|
||||
DimensionValue::Photos(vec![photo_id.clone()]),
|
||||
DimensionValue::VoiceMemos(vec![voice_memo_id.clone()]),
|
||||
],
|
||||
};
|
||||
|
||||
let entry = create_entry::execute(cmd, &deps).await.unwrap();
|
||||
|
||||
let composed = dimensions.composer().compose(vec![entry]).await.unwrap();
|
||||
let composed = &composed[0];
|
||||
|
||||
assert_eq!(composed.content().map(|c| c.value()), Some("a full day"));
|
||||
assert_eq!(composed.activities(), [activity_id]);
|
||||
assert_eq!(composed.photos(), [photo_id]);
|
||||
assert_eq!(composed.voice_memos(), [voice_memo_id]);
|
||||
}
|
||||
@@ -44,9 +44,9 @@ async fn filters_by_activity() {
|
||||
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()]);
|
||||
let tagged = test_entry(user.id().clone(), Mood::Good);
|
||||
store.save(&tagged).await.unwrap();
|
||||
store.assign_activities(tagged.id(), vec![activity.id().clone()]);
|
||||
|
||||
store
|
||||
.save(&test_entry(user.id().clone(), Mood::Meh))
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::entry::Mood;
|
||||
use domain::ports::MoodEntryCommandPort;
|
||||
use domain::ports::{MoodEntryCommandPort, UserCommandPort};
|
||||
use domain::testing::{InMemoryStore, test_entry_days_ago, test_user};
|
||||
use domain::user::Timezone;
|
||||
|
||||
use application::entry::queries::MoodStatsQuery;
|
||||
use application::entry::use_cases::get_mood_stats;
|
||||
@@ -10,23 +11,32 @@ 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");
|
||||
let user = user_in("alice", "Europe/Warsaw");
|
||||
|
||||
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();
|
||||
MoodEntryCommandPort::save(
|
||||
&*store,
|
||||
&test_entry_days_ago(user.id().clone(), Mood::Good, 0),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
MoodEntryCommandPort::save(
|
||||
&*store,
|
||||
&test_entry_days_ago(user.id().clone(), Mood::Rad, 1),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
MoodEntryCommandPort::save(
|
||||
&*store,
|
||||
&test_entry_days_ago(user.id().clone(), Mood::Meh, 2),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
UserCommandPort::save(&*store, &user).await.unwrap();
|
||||
|
||||
let deps = get_mood_stats::Deps {
|
||||
query: store.clone(),
|
||||
users: store.clone(),
|
||||
};
|
||||
|
||||
let query = MoodStatsQuery {
|
||||
@@ -43,14 +53,18 @@ async fn computes_stats_across_entries() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn empty_entries_give_zero_stats() {
|
||||
async fn a_user_with_no_entries_has_zero_stats() {
|
||||
let store = Arc::new(InMemoryStore::new());
|
||||
let user = user_in("newcomer", "Europe/Warsaw");
|
||||
UserCommandPort::save(&*store, &user).await.unwrap();
|
||||
|
||||
let deps = get_mood_stats::Deps {
|
||||
query: store.clone(),
|
||||
users: store.clone(),
|
||||
};
|
||||
|
||||
let query = MoodStatsQuery {
|
||||
user_id: test_user("nobody").id().clone(),
|
||||
user_id: user.id().clone(),
|
||||
range: None,
|
||||
};
|
||||
|
||||
@@ -60,3 +74,9 @@ async fn empty_entries_give_zero_stats() {
|
||||
assert!(stats.average.is_none());
|
||||
assert_eq!(stats.current_streak, 0);
|
||||
}
|
||||
|
||||
fn user_in(name: &str, timezone: &str) -> domain::user::User {
|
||||
let mut user = test_user(name);
|
||||
user.update_timezone(Some(Timezone::new(timezone).unwrap()));
|
||||
user
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
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 domain::entry::Mood;
|
||||
use domain::ports::{ActivityCommandPort, MoodEntryCommandPort, MoodEntryQueryPort};
|
||||
use domain::testing::{InMemoryStore, test_activity, test_entry, test_user};
|
||||
|
||||
use application::entry::use_cases::replace_activity;
|
||||
|
||||
@@ -13,18 +13,27 @@ async fn replaces_activity_on_entries() {
|
||||
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();
|
||||
ActivityCommandPort::save(&*store, &old).await.unwrap();
|
||||
ActivityCommandPort::save(&*store, &new).await.unwrap();
|
||||
|
||||
let entry = test_entry(user.id().clone(), Mood::Good);
|
||||
MoodEntryCommandPort::save(&*store, &entry).await.unwrap();
|
||||
store.assign_activities(entry.id(), vec![old.id().clone()]);
|
||||
|
||||
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());
|
||||
replace_activity::execute(user.id().clone(), old.id().clone(), new.id().clone(), &deps)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let now_tagged_new = store.find_by_activity(user.id(), new.id()).await.unwrap();
|
||||
let still_tagged_old = store.find_by_activity(user.id(), old.id()).await.unwrap();
|
||||
|
||||
assert_eq!(now_tagged_new.len(), 1);
|
||||
assert!(still_tagged_old.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -53,7 +62,9 @@ async fn rejects_target_owned_by_different_user() {
|
||||
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();
|
||||
ActivityCommandPort::save(&*store, &bobs_activity)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let deps = replace_activity::Deps {
|
||||
entry_command: store.clone(),
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use chrono::{FixedOffset, TimeZone};
|
||||
|
||||
use domain::dimension::DimensionKind;
|
||||
use domain::entry::Mood;
|
||||
use domain::ports::EntryDimensionPort;
|
||||
use domain::ports::MoodEntryCommandPort;
|
||||
use domain::testing::{InMemoryStore, test_entry, test_logged_at, test_user};
|
||||
use domain::testing::{
|
||||
InMemoryDimensionStore, InMemoryStore, test_entry, test_logged_at, test_user,
|
||||
};
|
||||
use domain::user::UserId;
|
||||
|
||||
use application::entry::commands::UpdateEntryCommand;
|
||||
@@ -21,6 +27,10 @@ async fn setup() -> (
|
||||
|
||||
let deps = update_entry::Deps {
|
||||
command: store.clone(),
|
||||
dimensions: vec![
|
||||
Arc::new(InMemoryDimensionStore::new(DimensionKind::Content))
|
||||
as Arc<dyn EntryDimensionPort>,
|
||||
],
|
||||
query: store.clone(),
|
||||
media_storage: store.clone(),
|
||||
events: store.clone(),
|
||||
@@ -36,11 +46,8 @@ async fn updates_mood() {
|
||||
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![],
|
||||
logged_at: Some(test_logged_at()),
|
||||
dimensions: vec![],
|
||||
};
|
||||
|
||||
let updated = update_entry::execute(cmd, user_id, &deps).await.unwrap();
|
||||
@@ -54,6 +61,10 @@ async fn updating_nonexistent_entry_fails() {
|
||||
let store = Arc::new(InMemoryStore::new());
|
||||
let deps = update_entry::Deps {
|
||||
command: store.clone(),
|
||||
dimensions: vec![
|
||||
Arc::new(InMemoryDimensionStore::new(DimensionKind::Content))
|
||||
as Arc<dyn EntryDimensionPort>,
|
||||
],
|
||||
query: store.clone(),
|
||||
media_storage: store.clone(),
|
||||
events: store.clone(),
|
||||
@@ -62,11 +73,8 @@ async fn updating_nonexistent_entry_fails() {
|
||||
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![],
|
||||
logged_at: Some(test_logged_at()),
|
||||
dimensions: vec![],
|
||||
};
|
||||
|
||||
let result = update_entry::execute(cmd, UserId::generate(), &deps).await;
|
||||
@@ -80,14 +88,48 @@ async fn rejects_update_by_different_user() {
|
||||
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![],
|
||||
logged_at: Some(test_logged_at()),
|
||||
dimensions: vec![],
|
||||
};
|
||||
|
||||
let other_user = UserId::generate();
|
||||
let result = update_entry::execute(cmd, other_user, &deps).await;
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn an_update_carrying_no_date_leaves_the_entry_where_it_was() {
|
||||
let (_store, deps, entry, user_id) = setup().await;
|
||||
|
||||
let cmd = UpdateEntryCommand {
|
||||
entry_id: entry.id().clone(),
|
||||
mood: Mood::Rad,
|
||||
logged_at: None,
|
||||
dimensions: vec![],
|
||||
};
|
||||
|
||||
let updated = update_entry::execute(cmd, user_id, &deps).await.unwrap();
|
||||
|
||||
assert_eq!(updated.logged_at(), &test_logged_at());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn an_update_carrying_a_date_moves_the_entry_to_it() {
|
||||
let (_store, deps, entry, user_id) = setup().await;
|
||||
|
||||
let moved_to = FixedOffset::east_opt(3600)
|
||||
.unwrap()
|
||||
.with_ymd_and_hms(2024, 1, 2, 9, 30, 0)
|
||||
.unwrap();
|
||||
|
||||
let cmd = UpdateEntryCommand {
|
||||
entry_id: entry.id().clone(),
|
||||
mood: Mood::Rad,
|
||||
logged_at: Some(moved_to),
|
||||
dimensions: vec![],
|
||||
};
|
||||
|
||||
let updated = update_entry::execute(cmd, user_id, &deps).await.unwrap();
|
||||
|
||||
assert_eq!(updated.logged_at(), &moved_to);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user