From 956e51530e8156a9fa825f825fbf83af087f7e3b Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Wed, 10 Jun 2026 02:55:47 +0200 Subject: [PATCH] refactor: move domain inline tests to separate files under tests/ Match the application crate convention: each source file references its tests via #[cfg(test)] #[path = "tests/filename.rs"] mod tests; with the test code in a sibling tests/ directory. - events.rs -> tests/events.rs - value_objects.rs -> tests/value_objects.rs - models/mod.rs -> models/tests/mod.rs (renamed from tests.rs) - models/person.rs -> models/tests/person.rs - models/goal.rs -> models/tests/goal.rs - models/watch_event.rs -> models/tests/watch_event.rs - services/review_history.rs -> services/tests/review_history.rs --- crates/domain/src/events.rs | 20 +-- crates/domain/src/models/goal.rs | 95 +----------- crates/domain/src/models/mod.rs | 2 +- crates/domain/src/models/person.rs | 59 +------ crates/domain/src/models/tests/goal.rs | 91 +++++++++++ .../src/models/{tests.rs => tests/mod.rs} | 0 crates/domain/src/models/tests/person.rs | 55 +++++++ crates/domain/src/models/tests/watch_event.rs | 136 +++++++++++++++++ crates/domain/src/models/watch_event.rs | 139 +---------------- crates/domain/src/services/review_history.rs | 80 +--------- .../src/services/tests/review_history.rs | 76 +++++++++ crates/domain/src/tests/events.rs | 16 ++ crates/domain/src/tests/value_objects.rs | 141 +++++++++++++++++ crates/domain/src/value_objects.rs | 144 +----------------- 14 files changed, 528 insertions(+), 526 deletions(-) create mode 100644 crates/domain/src/models/tests/goal.rs rename crates/domain/src/models/{tests.rs => tests/mod.rs} (100%) create mode 100644 crates/domain/src/models/tests/person.rs create mode 100644 crates/domain/src/models/tests/watch_event.rs create mode 100644 crates/domain/src/services/tests/review_history.rs create mode 100644 crates/domain/src/tests/events.rs create mode 100644 crates/domain/src/tests/value_objects.rs diff --git a/crates/domain/src/events.rs b/crates/domain/src/events.rs index 047cea1..67397f8 100644 --- a/crates/domain/src/events.rs +++ b/crates/domain/src/events.rs @@ -135,21 +135,5 @@ impl EventEnvelope { } #[cfg(test)] -mod tests { - use super::*; - use crate::value_objects::UserId; - - #[test] - fn follow_accepted_matches() { - let uid = UserId::from_uuid(uuid::Uuid::new_v4()); - let event = DomainEvent::FollowAccepted { - local_user_id: uid.clone(), - remote_actor_url: "https://remote.example/users/alice".to_string(), - outbox_url: "https://remote.example/users/alice/outbox".to_string(), - }; - let DomainEvent::FollowAccepted { outbox_url, .. } = event else { - panic!("wrong variant"); - }; - assert_eq!(outbox_url, "https://remote.example/users/alice/outbox"); - } -} +#[path = "tests/events.rs"] +mod tests; diff --git a/crates/domain/src/models/goal.rs b/crates/domain/src/models/goal.rs index 7b295ce..534f0dc 100644 --- a/crates/domain/src/models/goal.rs +++ b/crates/domain/src/models/goal.rs @@ -111,96 +111,5 @@ impl GoalWithProgress { } #[cfg(test)] -mod tests { - use super::*; - use crate::value_objects::UserId; - - fn make_goal(year: u16, target: u32) -> Result { - Goal::new(UserId::generate(), year, target, GoalType::Movies) - } - - #[test] - fn new_goal_valid() { - let g = make_goal(2024, 52); - assert!(g.is_ok()); - let g = g.unwrap(); - assert_eq!(g.year(), 2024); - assert_eq!(g.target_count(), 52); - } - - #[test] - fn new_goal_rejects_year_before_2020() { - assert!(make_goal(2019, 10).is_err()); - } - - #[test] - fn new_goal_rejects_zero_target() { - assert!(make_goal(2024, 0).is_err()); - } - - #[test] - fn update_target_valid() { - let mut g = make_goal(2024, 10).unwrap(); - assert!(g.update_target(50).is_ok()); - assert_eq!(g.target_count(), 50); - } - - #[test] - fn update_target_rejects_zero() { - let mut g = make_goal(2024, 10).unwrap(); - assert!(g.update_target(0).is_err()); - } - - #[test] - fn from_persistence_preserves_fields() { - let id = GoalId::generate(); - let uid = UserId::generate(); - let ts = chrono::Utc::now().naive_utc(); - let g = Goal::from_persistence(id.clone(), uid.clone(), 2025, 42, GoalType::Movies, ts); - assert_eq!(*g.id(), id); - assert_eq!(*g.user_id(), uid); - assert_eq!(g.year(), 2025); - assert_eq!(g.target_count(), 42); - assert_eq!(g.created_at(), &ts); - } - - #[test] - fn percentage_calculation() { - let g = make_goal(2024, 100).unwrap(); - let wp = GoalWithProgress { - goal: g, - current_count: 50, - }; - assert!((wp.percentage() - 50.0).abs() < f64::EPSILON); - } - - #[test] - fn percentage_caps_at_100() { - let g = make_goal(2024, 10).unwrap(); - let wp = GoalWithProgress { - goal: g, - current_count: 20, - }; - assert!((wp.percentage() - 100.0).abs() < f64::EPSILON); - } - - #[test] - fn is_complete() { - let g = make_goal(2024, 10).unwrap(); - let wp = GoalWithProgress { - goal: g, - current_count: 10, - }; - assert!(wp.is_complete()); - } - - #[test] - fn is_not_complete() { - let g = make_goal(2024, 10).unwrap(); - let wp = GoalWithProgress { - goal: g, - current_count: 9, - }; - assert!(!wp.is_complete()); - } -} +#[path = "tests/goal.rs"] +mod tests; diff --git a/crates/domain/src/models/mod.rs b/crates/domain/src/models/mod.rs index ef166c4..67ddb06 100644 --- a/crates/domain/src/models/mod.rs +++ b/crates/domain/src/models/mod.rs @@ -92,5 +92,5 @@ pub enum ExportFormat { } #[cfg(test)] -#[path = "tests.rs"] +#[path = "tests/mod.rs"] mod tests; diff --git a/crates/domain/src/models/person.rs b/crates/domain/src/models/person.rs index f06a729..f907cf7 100644 --- a/crates/domain/src/models/person.rs +++ b/crates/domain/src/models/person.rs @@ -113,60 +113,5 @@ pub struct CrewCredit { } #[cfg(test)] -mod tests { - use super::*; - - #[test] - fn person_new() { - let ext = ExternalPersonId::new("tmdb:12345"); - let pid = PersonId::from_external(&ext); - let p = Person::new( - pid, - ext, - "Keanu Reeves".into(), - Some("Acting".into()), - Some("/profiles/keanu.jpg".into()), - ); - assert_eq!(p.name(), "Keanu Reeves"); - assert_eq!(p.known_for_department(), Some("Acting")); - assert_eq!(p.profile_path(), Some("/profiles/keanu.jpg")); - assert_eq!(p.external_id().value(), "tmdb:12345"); - assert_eq!(p.external_id().tmdb_id(), Some(12345)); - } - - #[test] - fn person_id_from_external() { - let ext = ExternalPersonId::new("tmdb:99999"); - let pid = PersonId::from_external(&ext); - // UUIDv5 is deterministic — just ensure it's a valid uuid - let _ = pid.value(); - } - - #[test] - fn person_id_deterministic() { - let ext = ExternalPersonId::new("tmdb:42"); - let a = PersonId::from_external(&ext); - let b = PersonId::from_external(&ext); - assert_eq!(a, b); - } - - #[test] - fn person_credits_default_empty() { - let ext = ExternalPersonId::new("tmdb:1"); - let pid = PersonId::from_external(&ext); - let p = Person::new(pid, ext, "Test".into(), None, None); - let credits = PersonCredits { - person: p, - cast: vec![], - crew: vec![], - }; - assert!(credits.cast.is_empty()); - assert!(credits.crew.is_empty()); - } - - #[test] - fn external_person_id_tmdb_id_none_for_other() { - let ext = ExternalPersonId::new("imdb:nm0000206"); - assert_eq!(ext.tmdb_id(), None); - } -} +#[path = "tests/person.rs"] +mod tests; diff --git a/crates/domain/src/models/tests/goal.rs b/crates/domain/src/models/tests/goal.rs new file mode 100644 index 0000000..c3575b4 --- /dev/null +++ b/crates/domain/src/models/tests/goal.rs @@ -0,0 +1,91 @@ +use super::*; +use crate::value_objects::UserId; + +fn make_goal(year: u16, target: u32) -> Result { + Goal::new(UserId::generate(), year, target, GoalType::Movies) +} + +#[test] +fn new_goal_valid() { + let g = make_goal(2024, 52); + assert!(g.is_ok()); + let g = g.unwrap(); + assert_eq!(g.year(), 2024); + assert_eq!(g.target_count(), 52); +} + +#[test] +fn new_goal_rejects_year_before_2020() { + assert!(make_goal(2019, 10).is_err()); +} + +#[test] +fn new_goal_rejects_zero_target() { + assert!(make_goal(2024, 0).is_err()); +} + +#[test] +fn update_target_valid() { + let mut g = make_goal(2024, 10).unwrap(); + assert!(g.update_target(50).is_ok()); + assert_eq!(g.target_count(), 50); +} + +#[test] +fn update_target_rejects_zero() { + let mut g = make_goal(2024, 10).unwrap(); + assert!(g.update_target(0).is_err()); +} + +#[test] +fn from_persistence_preserves_fields() { + let id = GoalId::generate(); + let uid = UserId::generate(); + let ts = chrono::Utc::now().naive_utc(); + let g = Goal::from_persistence(id.clone(), uid.clone(), 2025, 42, GoalType::Movies, ts); + assert_eq!(*g.id(), id); + assert_eq!(*g.user_id(), uid); + assert_eq!(g.year(), 2025); + assert_eq!(g.target_count(), 42); + assert_eq!(g.created_at(), &ts); +} + +#[test] +fn percentage_calculation() { + let g = make_goal(2024, 100).unwrap(); + let wp = GoalWithProgress { + goal: g, + current_count: 50, + }; + assert!((wp.percentage() - 50.0).abs() < f64::EPSILON); +} + +#[test] +fn percentage_caps_at_100() { + let g = make_goal(2024, 10).unwrap(); + let wp = GoalWithProgress { + goal: g, + current_count: 20, + }; + assert!((wp.percentage() - 100.0).abs() < f64::EPSILON); +} + +#[test] +fn is_complete() { + let g = make_goal(2024, 10).unwrap(); + let wp = GoalWithProgress { + goal: g, + current_count: 10, + }; + assert!(wp.is_complete()); +} + +#[test] +fn is_not_complete() { + let g = make_goal(2024, 10).unwrap(); + let wp = GoalWithProgress { + goal: g, + current_count: 9, + }; + assert!(!wp.is_complete()); +} diff --git a/crates/domain/src/models/tests.rs b/crates/domain/src/models/tests/mod.rs similarity index 100% rename from crates/domain/src/models/tests.rs rename to crates/domain/src/models/tests/mod.rs diff --git a/crates/domain/src/models/tests/person.rs b/crates/domain/src/models/tests/person.rs new file mode 100644 index 0000000..a4114f9 --- /dev/null +++ b/crates/domain/src/models/tests/person.rs @@ -0,0 +1,55 @@ +use super::*; + +#[test] +fn person_new() { + let ext = ExternalPersonId::new("tmdb:12345"); + let pid = PersonId::from_external(&ext); + let p = Person::new( + pid, + ext, + "Keanu Reeves".into(), + Some("Acting".into()), + Some("/profiles/keanu.jpg".into()), + ); + assert_eq!(p.name(), "Keanu Reeves"); + assert_eq!(p.known_for_department(), Some("Acting")); + assert_eq!(p.profile_path(), Some("/profiles/keanu.jpg")); + assert_eq!(p.external_id().value(), "tmdb:12345"); + assert_eq!(p.external_id().tmdb_id(), Some(12345)); +} + +#[test] +fn person_id_from_external() { + let ext = ExternalPersonId::new("tmdb:99999"); + let pid = PersonId::from_external(&ext); + // UUIDv5 is deterministic — just ensure it's a valid uuid + let _ = pid.value(); +} + +#[test] +fn person_id_deterministic() { + let ext = ExternalPersonId::new("tmdb:42"); + let a = PersonId::from_external(&ext); + let b = PersonId::from_external(&ext); + assert_eq!(a, b); +} + +#[test] +fn person_credits_default_empty() { + let ext = ExternalPersonId::new("tmdb:1"); + let pid = PersonId::from_external(&ext); + let p = Person::new(pid, ext, "Test".into(), None, None); + let credits = PersonCredits { + person: p, + cast: vec![], + crew: vec![], + }; + assert!(credits.cast.is_empty()); + assert!(credits.crew.is_empty()); +} + +#[test] +fn external_person_id_tmdb_id_none_for_other() { + let ext = ExternalPersonId::new("imdb:nm0000206"); + assert_eq!(ext.tmdb_id(), None); +} diff --git a/crates/domain/src/models/tests/watch_event.rs b/crates/domain/src/models/tests/watch_event.rs new file mode 100644 index 0000000..de495a8 --- /dev/null +++ b/crates/domain/src/models/tests/watch_event.rs @@ -0,0 +1,136 @@ +use super::*; + +fn ts() -> NaiveDateTime { + chrono::NaiveDate::from_ymd_opt(2024, 6, 1) + .unwrap() + .and_hms_opt(12, 0, 0) + .unwrap() +} + +#[test] +fn watch_event_new_has_pending_status() { + let e = WatchEvent::new( + UserId::generate(), + "Dune".into(), + Some(2021), + None, + WatchEventSource::Jellyfin, + ts(), + None, + ); + assert_eq!(*e.status(), WatchEventStatus::Pending); +} + +#[test] +fn watch_event_getters() { + let uid = UserId::generate(); + let mid = MovieId::generate(); + let e = WatchEvent::new( + uid.clone(), + "Arrival".into(), + Some(2016), + Some("ext123".into()), + WatchEventSource::Plex, + ts(), + Some(mid.clone()), + ); + assert_eq!(*e.user_id(), uid); + assert_eq!(e.title(), "Arrival"); + assert_eq!(e.year(), Some(2016)); + assert_eq!(e.external_metadata_id(), Some("ext123")); + assert_eq!(*e.source(), WatchEventSource::Plex); + assert_eq!(e.watched_at(), &ts()); + assert_eq!(*e.movie_id().unwrap(), mid); +} + +#[test] +fn webhook_token_new() { + let uid = UserId::generate(); + let t = WebhookToken::new( + uid.clone(), + "hash123".into(), + WatchEventSource::Jellyfin, + Some("my server".into()), + ); + assert_eq!(*t.user_id(), uid); + assert_eq!(t.token_hash(), "hash123"); + assert_eq!(*t.provider(), WatchEventSource::Jellyfin); + assert_eq!(t.label(), Some("my server")); + assert!(t.last_used_at().is_none()); +} + +#[test] +fn webhook_token_from_persistence() { + let id = WebhookTokenId::generate(); + let uid = UserId::generate(); + let created = ts(); + let used = chrono::NaiveDate::from_ymd_opt(2024, 7, 1) + .unwrap() + .and_hms_opt(0, 0, 0) + .unwrap(); + let t = WebhookToken::from_persistence( + id.clone(), + uid.clone(), + "h".into(), + WatchEventSource::Plex, + None, + created, + Some(used), + ); + assert_eq!(*t.id(), id); + assert_eq!(*t.user_id(), uid); + assert_eq!(t.token_hash(), "h"); + assert_eq!(*t.provider(), WatchEventSource::Plex); + assert_eq!(t.label(), None); + assert_eq!(t.created_at(), &created); + assert_eq!(t.last_used_at(), Some(&used)); +} + +#[test] +fn watch_event_source_display() { + assert_eq!(WatchEventSource::Jellyfin.to_string(), "jellyfin"); + assert_eq!(WatchEventSource::Plex.to_string(), "plex"); +} + +#[test] +fn watch_event_source_from_str() { + assert_eq!( + "jellyfin".parse::().unwrap(), + WatchEventSource::Jellyfin + ); + assert_eq!( + "plex".parse::().unwrap(), + WatchEventSource::Plex + ); + assert!("unknown".parse::().is_err()); +} + +#[test] +fn watch_event_status_display() { + assert_eq!(WatchEventStatus::Pending.to_string(), "pending"); + assert_eq!(WatchEventStatus::Confirmed.to_string(), "confirmed"); + assert_eq!(WatchEventStatus::Dismissed.to_string(), "dismissed"); +} + +#[test] +fn watch_event_status_from_str() { + for s in ["pending", "confirmed", "dismissed"] { + let parsed: WatchEventStatus = s.parse().unwrap(); + assert_eq!(parsed.to_string(), s); + } + assert!("bogus".parse::().is_err()); +} + +#[test] +fn parsed_playback_event_fields() { + let p = ParsedPlaybackEvent { + title: "Matrix".into(), + year: Some(1999), + tmdb_id: Some("603".into()), + imdb_id: Some("tt0133093".into()), + }; + assert_eq!(p.title, "Matrix"); + assert_eq!(p.year, Some(1999)); + assert_eq!(p.tmdb_id.as_deref(), Some("603")); + assert_eq!(p.imdb_id.as_deref(), Some("tt0133093")); +} diff --git a/crates/domain/src/models/watch_event.rs b/crates/domain/src/models/watch_event.rs index 8120ea6..37b76c0 100644 --- a/crates/domain/src/models/watch_event.rs +++ b/crates/domain/src/models/watch_event.rs @@ -236,141 +236,6 @@ pub struct ParsedPlaybackEvent { } #[cfg(test)] -mod tests { - use super::*; +#[path = "tests/watch_event.rs"] +mod tests; - fn ts() -> NaiveDateTime { - chrono::NaiveDate::from_ymd_opt(2024, 6, 1) - .unwrap() - .and_hms_opt(12, 0, 0) - .unwrap() - } - - #[test] - fn watch_event_new_has_pending_status() { - let e = WatchEvent::new( - UserId::generate(), - "Dune".into(), - Some(2021), - None, - WatchEventSource::Jellyfin, - ts(), - None, - ); - assert_eq!(*e.status(), WatchEventStatus::Pending); - } - - #[test] - fn watch_event_getters() { - let uid = UserId::generate(); - let mid = MovieId::generate(); - let e = WatchEvent::new( - uid.clone(), - "Arrival".into(), - Some(2016), - Some("ext123".into()), - WatchEventSource::Plex, - ts(), - Some(mid.clone()), - ); - assert_eq!(*e.user_id(), uid); - assert_eq!(e.title(), "Arrival"); - assert_eq!(e.year(), Some(2016)); - assert_eq!(e.external_metadata_id(), Some("ext123")); - assert_eq!(*e.source(), WatchEventSource::Plex); - assert_eq!(e.watched_at(), &ts()); - assert_eq!(*e.movie_id().unwrap(), mid); - } - - #[test] - fn webhook_token_new() { - let uid = UserId::generate(); - let t = WebhookToken::new( - uid.clone(), - "hash123".into(), - WatchEventSource::Jellyfin, - Some("my server".into()), - ); - assert_eq!(*t.user_id(), uid); - assert_eq!(t.token_hash(), "hash123"); - assert_eq!(*t.provider(), WatchEventSource::Jellyfin); - assert_eq!(t.label(), Some("my server")); - assert!(t.last_used_at().is_none()); - } - - #[test] - fn webhook_token_from_persistence() { - let id = WebhookTokenId::generate(); - let uid = UserId::generate(); - let created = ts(); - let used = chrono::NaiveDate::from_ymd_opt(2024, 7, 1) - .unwrap() - .and_hms_opt(0, 0, 0) - .unwrap(); - let t = WebhookToken::from_persistence( - id.clone(), - uid.clone(), - "h".into(), - WatchEventSource::Plex, - None, - created, - Some(used), - ); - assert_eq!(*t.id(), id); - assert_eq!(*t.user_id(), uid); - assert_eq!(t.token_hash(), "h"); - assert_eq!(*t.provider(), WatchEventSource::Plex); - assert_eq!(t.label(), None); - assert_eq!(t.created_at(), &created); - assert_eq!(t.last_used_at(), Some(&used)); - } - - #[test] - fn watch_event_source_display() { - assert_eq!(WatchEventSource::Jellyfin.to_string(), "jellyfin"); - assert_eq!(WatchEventSource::Plex.to_string(), "plex"); - } - - #[test] - fn watch_event_source_from_str() { - assert_eq!( - "jellyfin".parse::().unwrap(), - WatchEventSource::Jellyfin - ); - assert_eq!( - "plex".parse::().unwrap(), - WatchEventSource::Plex - ); - assert!("unknown".parse::().is_err()); - } - - #[test] - fn watch_event_status_display() { - assert_eq!(WatchEventStatus::Pending.to_string(), "pending"); - assert_eq!(WatchEventStatus::Confirmed.to_string(), "confirmed"); - assert_eq!(WatchEventStatus::Dismissed.to_string(), "dismissed"); - } - - #[test] - fn watch_event_status_from_str() { - for s in ["pending", "confirmed", "dismissed"] { - let parsed: WatchEventStatus = s.parse().unwrap(); - assert_eq!(parsed.to_string(), s); - } - assert!("bogus".parse::().is_err()); - } - - #[test] - fn parsed_playback_event_fields() { - let p = ParsedPlaybackEvent { - title: "Matrix".into(), - year: Some(1999), - tmdb_id: Some("603".into()), - imdb_id: Some("tt0133093".into()), - }; - assert_eq!(p.title, "Matrix"); - assert_eq!(p.year, Some(1999)); - assert_eq!(p.tmdb_id.as_deref(), Some("603")); - assert_eq!(p.imdb_id.as_deref(), Some("tt0133093")); - } -} diff --git a/crates/domain/src/services/review_history.rs b/crates/domain/src/services/review_history.rs index 4a60b53..38f2ba0 100644 --- a/crates/domain/src/services/review_history.rs +++ b/crates/domain/src/services/review_history.rs @@ -53,81 +53,5 @@ impl ReviewHistoryAnalyzer { } #[cfg(test)] -mod tests { - use super::*; - use crate::models::{Movie, Review, ReviewHistory}; - use crate::value_objects::{MovieId, MovieTitle, Rating, ReleaseYear, UserId}; - use chrono::{NaiveDate, NaiveDateTime, NaiveTime}; - - fn make_movie() -> Movie { - Movie::new( - None, - MovieTitle::new("Test".into()).unwrap(), - ReleaseYear::new(2024).unwrap(), - None, - None, - ) - } - - fn dt(year: i32, month: u32, day: u32) -> NaiveDateTime { - NaiveDateTime::new( - NaiveDate::from_ymd_opt(year, month, day).unwrap(), - NaiveTime::from_hms_opt(12, 0, 0).unwrap(), - ) - } - - fn review_with_rating(movie_id: &MovieId, rating: u8, watched_at: NaiveDateTime) -> Review { - let user_id = UserId::generate(); - Review::new( - movie_id.clone(), - user_id, - Rating::new(rating).unwrap(), - None, - watched_at, - ) - .unwrap() - } - - #[test] - fn neutral_when_empty() { - let movie = make_movie(); - let history = ReviewHistory::new(movie, vec![]); - let trend = ReviewHistoryAnalyzer::rating_trend(&history).unwrap(); - assert_eq!(trend, Trend::Neutral); - } - - #[test] - fn neutral_when_single_review() { - let movie = make_movie(); - let r = review_with_rating(movie.id(), 4, dt(2024, 1, 1)); - let history = ReviewHistory::new(movie, vec![r]); - let trend = ReviewHistoryAnalyzer::rating_trend(&history).unwrap(); - assert_eq!(trend, Trend::Neutral); - } - - #[test] - fn improved_when_latest_above_average() { - let movie = make_movie(); - let viewings = vec![ - review_with_rating(movie.id(), 2, dt(2024, 1, 1)), - review_with_rating(movie.id(), 3, dt(2024, 2, 1)), - review_with_rating(movie.id(), 5, dt(2024, 3, 1)), - ]; - let history = ReviewHistory::new(movie, viewings); - let trend = ReviewHistoryAnalyzer::rating_trend(&history).unwrap(); - assert_eq!(trend, Trend::Improved); - } - - #[test] - fn declined_when_latest_below_average() { - let movie = make_movie(); - let viewings = vec![ - review_with_rating(movie.id(), 5, dt(2024, 1, 1)), - review_with_rating(movie.id(), 4, dt(2024, 2, 1)), - review_with_rating(movie.id(), 2, dt(2024, 3, 1)), - ]; - let history = ReviewHistory::new(movie, viewings); - let trend = ReviewHistoryAnalyzer::rating_trend(&history).unwrap(); - assert_eq!(trend, Trend::Declined); - } -} +#[path = "tests/review_history.rs"] +mod tests; diff --git a/crates/domain/src/services/tests/review_history.rs b/crates/domain/src/services/tests/review_history.rs new file mode 100644 index 0000000..8e0edac --- /dev/null +++ b/crates/domain/src/services/tests/review_history.rs @@ -0,0 +1,76 @@ +use super::*; +use crate::models::{Movie, Review, ReviewHistory}; +use crate::value_objects::{MovieId, MovieTitle, Rating, ReleaseYear, UserId}; +use chrono::{NaiveDate, NaiveDateTime, NaiveTime}; + +fn make_movie() -> Movie { + Movie::new( + None, + MovieTitle::new("Test".into()).unwrap(), + ReleaseYear::new(2024).unwrap(), + None, + None, + ) +} + +fn dt(year: i32, month: u32, day: u32) -> NaiveDateTime { + NaiveDateTime::new( + NaiveDate::from_ymd_opt(year, month, day).unwrap(), + NaiveTime::from_hms_opt(12, 0, 0).unwrap(), + ) +} + +fn review_with_rating(movie_id: &MovieId, rating: u8, watched_at: NaiveDateTime) -> Review { + let user_id = UserId::generate(); + Review::new( + movie_id.clone(), + user_id, + Rating::new(rating).unwrap(), + None, + watched_at, + ) + .unwrap() +} + +#[test] +fn neutral_when_empty() { + let movie = make_movie(); + let history = ReviewHistory::new(movie, vec![]); + let trend = ReviewHistoryAnalyzer::rating_trend(&history).unwrap(); + assert_eq!(trend, Trend::Neutral); +} + +#[test] +fn neutral_when_single_review() { + let movie = make_movie(); + let r = review_with_rating(movie.id(), 4, dt(2024, 1, 1)); + let history = ReviewHistory::new(movie, vec![r]); + let trend = ReviewHistoryAnalyzer::rating_trend(&history).unwrap(); + assert_eq!(trend, Trend::Neutral); +} + +#[test] +fn improved_when_latest_above_average() { + let movie = make_movie(); + let viewings = vec![ + review_with_rating(movie.id(), 2, dt(2024, 1, 1)), + review_with_rating(movie.id(), 3, dt(2024, 2, 1)), + review_with_rating(movie.id(), 5, dt(2024, 3, 1)), + ]; + let history = ReviewHistory::new(movie, viewings); + let trend = ReviewHistoryAnalyzer::rating_trend(&history).unwrap(); + assert_eq!(trend, Trend::Improved); +} + +#[test] +fn declined_when_latest_below_average() { + let movie = make_movie(); + let viewings = vec![ + review_with_rating(movie.id(), 5, dt(2024, 1, 1)), + review_with_rating(movie.id(), 4, dt(2024, 2, 1)), + review_with_rating(movie.id(), 2, dt(2024, 3, 1)), + ]; + let history = ReviewHistory::new(movie, viewings); + let trend = ReviewHistoryAnalyzer::rating_trend(&history).unwrap(); + assert_eq!(trend, Trend::Declined); +} diff --git a/crates/domain/src/tests/events.rs b/crates/domain/src/tests/events.rs new file mode 100644 index 0000000..0cd4a64 --- /dev/null +++ b/crates/domain/src/tests/events.rs @@ -0,0 +1,16 @@ +use super::*; +use crate::value_objects::UserId; + +#[test] +fn follow_accepted_matches() { + let uid = UserId::from_uuid(uuid::Uuid::new_v4()); + let event = DomainEvent::FollowAccepted { + local_user_id: uid.clone(), + remote_actor_url: "https://remote.example/users/alice".to_string(), + outbox_url: "https://remote.example/users/alice/outbox".to_string(), + }; + let DomainEvent::FollowAccepted { outbox_url, .. } = event else { + panic!("wrong variant"); + }; + assert_eq!(outbox_url, "https://remote.example/users/alice/outbox"); +} diff --git a/crates/domain/src/tests/value_objects.rs b/crates/domain/src/tests/value_objects.rs new file mode 100644 index 0000000..92ed75e --- /dev/null +++ b/crates/domain/src/tests/value_objects.rs @@ -0,0 +1,141 @@ +use super::*; + +#[test] +fn movie_id_generate_unique() { + let a = MovieId::generate(); + let b = MovieId::generate(); + assert_ne!(a, b); +} + +#[test] +fn rating_valid_range() { + assert!(Rating::new(0).is_ok()); + assert!(Rating::new(5).is_ok()); + assert_eq!(Rating::new(3).unwrap().value(), 3); +} + +#[test] +fn rating_invalid() { + assert!(Rating::new(6).is_err()); + assert!(Rating::new(255).is_err()); +} + +#[test] +fn movie_title_valid() { + let t = MovieTitle::new("Test".into()); + assert!(t.is_ok()); + assert_eq!(t.unwrap().value(), "Test"); +} + +#[test] +fn movie_title_empty_rejected() { + assert!(MovieTitle::new("".into()).is_err()); + assert!(MovieTitle::new(" ".into()).is_err()); +} + +#[test] +fn release_year_valid() { + assert!(ReleaseYear::new(2024).is_ok()); + assert_eq!(ReleaseYear::new(1888).unwrap().value(), 1888); +} + +#[test] +fn release_year_too_early() { + assert!(ReleaseYear::new(1887).is_err()); +} + +#[test] +fn email_valid() { + let e = Email::new("a@b.com".into()); + assert!(e.is_ok()); + assert_eq!(e.unwrap().value(), "a@b.com"); +} + +#[test] +fn email_invalid() { + assert!(Email::new("invalid".into()).is_err()); + assert!(Email::new("".into()).is_err()); +} + +#[test] +fn username_valid() { + let u = Username::new("test".into()); + assert!(u.is_ok()); + assert_eq!(u.unwrap().value(), "test"); +} + +#[test] +fn username_lowercases() { + assert_eq!(Username::new("Alice".into()).unwrap().value(), "alice"); +} + +#[test] +fn username_rejects_too_short() { + assert!(Username::new("a".into()).is_err()); +} + +#[test] +fn username_rejects_special_chars() { + assert!(Username::new("no spaces".into()).is_err()); + assert!(Username::new("no@at".into()).is_err()); +} + +#[test] +fn poster_path_valid() { + let p = PosterPath::new("path/to/poster".into()); + assert!(p.is_ok()); + assert_eq!(p.unwrap().value(), "path/to/poster"); +} + +#[test] +fn poster_path_empty_rejected() { + assert!(PosterPath::new("".into()).is_err()); +} + +#[test] +fn comment_valid() { + let c = Comment::new("nice movie".into()); + assert!(c.is_ok()); + assert_eq!(c.unwrap().value(), "nice movie"); +} + +#[test] +fn comment_empty_is_ok() { + // empty comment allowed — only max-length checked + assert!(Comment::new("".into()).is_ok()); +} + +#[test] +fn external_metadata_id_valid() { + let e = ExternalMetadataId::new("tt1234567".into()); + assert!(e.is_ok()); + assert_eq!(e.unwrap().value(), "tt1234567"); +} + +#[test] +fn external_metadata_id_empty_rejected() { + assert!(ExternalMetadataId::new("".into()).is_err()); + assert!(ExternalMetadataId::new(" ".into()).is_err()); +} + +#[test] +fn password_hash_valid() { + assert!(PasswordHash::new("hash".into()).is_ok()); +} + +#[test] +fn password_hash_empty_rejected() { + assert!(PasswordHash::new("".into()).is_err()); +} + +#[test] +fn poster_url_valid() { + let u = PosterUrl::new("https://img.com/poster.jpg".into()); + assert!(u.is_ok()); + assert_eq!(u.unwrap().value(), "https://img.com/poster.jpg"); +} + +#[test] +fn poster_url_empty_rejected() { + assert!(PosterUrl::new("".into()).is_err()); +} diff --git a/crates/domain/src/value_objects.rs b/crates/domain/src/value_objects.rs index 0d78342..49f54fc 100644 --- a/crates/domain/src/value_objects.rs +++ b/crates/domain/src/value_objects.rs @@ -253,146 +253,6 @@ impl PosterUrl { } #[cfg(test)] -mod tests { - use super::*; +#[path = "tests/value_objects.rs"] +mod tests; - #[test] - fn movie_id_generate_unique() { - let a = MovieId::generate(); - let b = MovieId::generate(); - assert_ne!(a, b); - } - - #[test] - fn rating_valid_range() { - assert!(Rating::new(0).is_ok()); - assert!(Rating::new(5).is_ok()); - assert_eq!(Rating::new(3).unwrap().value(), 3); - } - - #[test] - fn rating_invalid() { - assert!(Rating::new(6).is_err()); - assert!(Rating::new(255).is_err()); - } - - #[test] - fn movie_title_valid() { - let t = MovieTitle::new("Test".into()); - assert!(t.is_ok()); - assert_eq!(t.unwrap().value(), "Test"); - } - - #[test] - fn movie_title_empty_rejected() { - assert!(MovieTitle::new("".into()).is_err()); - assert!(MovieTitle::new(" ".into()).is_err()); - } - - #[test] - fn release_year_valid() { - assert!(ReleaseYear::new(2024).is_ok()); - assert_eq!(ReleaseYear::new(1888).unwrap().value(), 1888); - } - - #[test] - fn release_year_too_early() { - assert!(ReleaseYear::new(1887).is_err()); - } - - #[test] - fn email_valid() { - let e = Email::new("a@b.com".into()); - assert!(e.is_ok()); - assert_eq!(e.unwrap().value(), "a@b.com"); - } - - #[test] - fn email_invalid() { - assert!(Email::new("invalid".into()).is_err()); - assert!(Email::new("".into()).is_err()); - } - - #[test] - fn username_valid() { - let u = Username::new("test".into()); - assert!(u.is_ok()); - assert_eq!(u.unwrap().value(), "test"); - } - - #[test] - fn username_lowercases() { - assert_eq!(Username::new("Alice".into()).unwrap().value(), "alice"); - } - - #[test] - fn username_rejects_too_short() { - assert!(Username::new("a".into()).is_err()); - } - - #[test] - fn username_rejects_special_chars() { - assert!(Username::new("no spaces".into()).is_err()); - assert!(Username::new("no@at".into()).is_err()); - } - - #[test] - fn poster_path_valid() { - let p = PosterPath::new("path/to/poster".into()); - assert!(p.is_ok()); - assert_eq!(p.unwrap().value(), "path/to/poster"); - } - - #[test] - fn poster_path_empty_rejected() { - assert!(PosterPath::new("".into()).is_err()); - } - - #[test] - fn comment_valid() { - let c = Comment::new("nice movie".into()); - assert!(c.is_ok()); - assert_eq!(c.unwrap().value(), "nice movie"); - } - - #[test] - fn comment_empty_is_ok() { - // empty comment allowed — only max-length checked - assert!(Comment::new("".into()).is_ok()); - } - - #[test] - fn external_metadata_id_valid() { - let e = ExternalMetadataId::new("tt1234567".into()); - assert!(e.is_ok()); - assert_eq!(e.unwrap().value(), "tt1234567"); - } - - #[test] - fn external_metadata_id_empty_rejected() { - assert!(ExternalMetadataId::new("".into()).is_err()); - assert!(ExternalMetadataId::new(" ".into()).is_err()); - } - - #[test] - fn password_hash_valid() { - assert!(PasswordHash::new("hash".into()).is_ok()); - } - - #[test] - fn password_hash_empty_rejected() { - assert!(PasswordHash::new("".into()).is_err()); - } - - #[test] - fn poster_url_valid() { - let u = PosterUrl::new("https://img.com/poster.jpg".into()); - assert!(u.is_ok()); - assert_eq!(u.unwrap().value(), "https://img.com/poster.jpg"); - } - - #[test] - fn poster_url_empty_rejected() { - assert!(PosterUrl::new("".into()).is_err()); - } -}