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