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,58 @@
use chrono::{DateTime, FixedOffset};
use domain::entry::Date;
use domain::user::Timezone;
fn instant(text: &str) -> DateTime<FixedOffset> {
text.parse().unwrap()
}
fn zone(name: &str) -> Timezone {
Timezone::new(name).unwrap()
}
#[test]
fn an_instant_resolves_to_the_calendar_day_in_the_users_zone() {
let date = Date::from_instant(
&instant("2025-03-15T09:00:00+13:00"),
&zone("Pacific/Auckland"),
);
assert_eq!(date.value().to_string(), "2025-03-15");
}
#[test]
fn the_users_zone_decides_the_day_not_the_offset_the_client_sent() {
let same_moment = instant("2025-03-15T23:00:00+00:00");
let in_auckland = Date::from_instant(&same_moment, &zone("Pacific/Auckland"));
let in_warsaw = Date::from_instant(&same_moment, &zone("Europe/Warsaw"));
assert_eq!(in_auckland.value().to_string(), "2025-03-16");
assert_eq!(in_warsaw.value().to_string(), "2025-03-16");
}
#[test]
fn an_evening_in_a_far_eastern_zone_is_not_the_utc_day() {
let evening = instant("2025-03-14T20:00:00+00:00");
let date = Date::from_instant(&evening, &zone("Pacific/Auckland"));
assert_eq!(date.value().to_string(), "2025-03-15");
}
#[test]
fn a_day_knows_the_day_before_it() {
let date = Date::from_instant(&instant("2025-03-01T12:00:00+00:00"), &zone("UTC"));
assert_eq!(date.previous().value().to_string(), "2025-02-28");
}
#[test]
fn days_between_two_dates_is_their_gap() {
let earlier = Date::from_instant(&instant("2025-03-01T12:00:00+00:00"), &zone("UTC"));
let later = Date::from_instant(&instant("2025-03-05T12:00:00+00:00"), &zone("UTC"));
assert_eq!(later.days_since(&earlier), 4);
assert_eq!(earlier.days_since(&later), -4);
}

View File

@@ -0,0 +1,57 @@
use domain::entry::{DayMood, Mood};
#[test]
fn a_day_of_one_awful_and_one_rad_sits_in_the_middle() {
let day = DayMood::of(&[Mood::Awful, Mood::Rad]).unwrap();
assert!((day.value() - 3.0).abs() < f64::EPSILON);
assert_eq!(day.rounded(), Mood::Meh);
}
#[test]
fn a_day_with_no_entries_has_no_mood() {
assert!(DayMood::of(&[]).is_none());
}
#[test]
fn one_bad_day_among_four_middling_ones_still_shows() {
let mostly_meh = DayMood::of(&[Mood::Meh, Mood::Meh, Mood::Meh, Mood::Meh, Mood::Awful])
.unwrap()
.value();
let all_meh = DayMood::of(&[Mood::Meh, Mood::Meh, Mood::Meh, Mood::Meh, Mood::Meh])
.unwrap()
.value();
assert!(mostly_meh < all_meh);
}
#[test]
fn a_single_entry_is_the_whole_day() {
let day = DayMood::of(&[Mood::Bad]).unwrap();
assert!((day.value() - 2.0).abs() < f64::EPSILON);
assert_eq!(day.rounded(), Mood::Bad);
}
#[test]
fn a_day_exactly_between_two_moods_shows_the_better_one() {
let day = DayMood::of(&[Mood::Meh, Mood::Good]).unwrap();
assert!((day.value() - 3.5).abs() < f64::EPSILON);
assert_eq!(day.rounded(), Mood::Good);
}
#[test]
fn every_whole_mean_rounds_to_its_own_mood() {
let pairs = [
(Mood::Awful, Mood::Awful),
(Mood::Bad, Mood::Bad),
(Mood::Meh, Mood::Meh),
(Mood::Good, Mood::Good),
(Mood::Rad, Mood::Rad),
];
for (logged, expected) in pairs {
assert_eq!(DayMood::of(&[logged, logged]).unwrap().rounded(), expected);
}
}

View File

@@ -1,8 +1,6 @@
use chrono::{FixedOffset, TimeZone, Utc};
use domain::activity::ActivityId;
use domain::attachment::{PhotoId, VoiceMemoId};
use domain::entry::{Content, Mood, MoodEntry, MoodEntryData};
use domain::entry::{Mood, MoodEntry, MoodEntryData};
use domain::user::UserId;
fn sample_logged_at() -> chrono::DateTime<FixedOffset> {
@@ -19,10 +17,6 @@ fn new_entry_has_required_fields() {
assert_eq!(*entry.user_id(), user_id);
assert_eq!(entry.mood(), Mood::Good);
assert!(entry.activities().is_empty());
assert!(entry.content().is_none());
assert!(entry.photos().is_empty());
assert!(entry.voice_memos().is_empty());
}
#[test]
@@ -36,39 +30,10 @@ fn update_mood_changes_value_and_touches_updated_at() {
assert!(*entry.updated_at() >= before);
}
#[test]
fn set_activities_deduplicates_and_sorts() {
let mut entry = MoodEntry::new(UserId::generate(), Mood::Meh, sample_logged_at());
let a = ActivityId::generate();
let b = ActivityId::generate();
entry.set_activities(vec![b.clone(), a.clone(), b.clone()]);
assert_eq!(entry.activities().len(), 2);
let ids = entry.activities();
assert!(ids[0] <= ids[1]);
}
#[test]
fn set_content_updates_and_clears() {
let mut entry = MoodEntry::new(UserId::generate(), Mood::Good, sample_logged_at());
let content = Content::new("great day").unwrap();
entry.set_content(Some(content));
assert!(entry.content().is_some());
entry.set_content(None);
assert!(entry.content().is_none());
}
#[test]
fn from_persistence_reconstructs_all_fields() {
let id = domain::entry::MoodEntryId::generate();
let user_id = UserId::generate();
let activity_id = ActivityId::generate();
let photo_id = PhotoId::generate();
let voice_memo_id = VoiceMemoId::generate();
let content = Content::new("test").unwrap();
let now = Utc::now();
let entry = MoodEntry::from_persistence(MoodEntryData {
@@ -76,10 +41,6 @@ fn from_persistence_reconstructs_all_fields() {
user_id: user_id.clone(),
mood: Mood::Bad,
logged_at: sample_logged_at(),
activities: vec![activity_id.clone()],
content: Some(content),
photos: vec![photo_id.clone()],
voice_memos: vec![voice_memo_id.clone()],
created_at: now,
updated_at: now,
});
@@ -87,10 +48,7 @@ fn from_persistence_reconstructs_all_fields() {
assert_eq!(*entry.id(), id);
assert_eq!(*entry.user_id(), user_id);
assert_eq!(entry.mood(), Mood::Bad);
assert_eq!(entry.activities().len(), 1);
assert!(entry.content().is_some());
assert_eq!(entry.photos().len(), 1);
assert_eq!(entry.voice_memos().len(), 1);
assert_eq!(*entry.logged_at(), sample_logged_at());
}
#[test]
@@ -105,32 +63,3 @@ fn update_logged_at_changes_timestamp() {
assert_eq!(*entry.logged_at(), new_time);
}
#[test]
fn set_photos_replaces_list() {
let mut entry = MoodEntry::new(UserId::generate(), Mood::Good, sample_logged_at());
let p1 = PhotoId::generate();
let p2 = PhotoId::generate();
entry.set_photos(vec![p1.clone(), p2.clone()]);
assert_eq!(entry.photos().len(), 2);
entry.set_photos(vec![p1.clone()]);
assert_eq!(entry.photos().len(), 1);
entry.set_photos(vec![]);
assert!(entry.photos().is_empty());
}
#[test]
fn set_voice_memos_replaces_list() {
let mut entry = MoodEntry::new(UserId::generate(), Mood::Good, sample_logged_at());
let m1 = VoiceMemoId::generate();
let m2 = VoiceMemoId::generate();
entry.set_voice_memos(vec![m1.clone(), m2.clone()]);
assert_eq!(entry.voice_memos().len(), 2);
entry.set_voice_memos(vec![]);
assert!(entry.voice_memos().is_empty());
}