changes
All checks were successful
CI / ci (push) Successful in 19m38s

This commit is contained in:
2026-08-26 20:55:30 +02:00
parent a557c183e9
commit 23d052278a
523 changed files with 24448 additions and 2005 deletions

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

View File

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

View File

@@ -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(),

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

View File

@@ -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))

View File

@@ -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
}

View File

@@ -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(),

View File

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