test: shared factories and command builders

This commit is contained in:
2026-10-25 22:08:17 +01:00
parent 2ac1d4e0cf
commit 51d92e08a7
58 changed files with 784 additions and 1344 deletions

View File

@@ -5,5 +5,5 @@ pub mod ports;
pub mod services;
pub mod value_objects;
#[cfg(feature = "test-helpers")]
#[cfg(any(test, feature = "test-helpers"))]
pub mod testing;

View File

@@ -1,39 +1,34 @@
use super::*;
use crate::testing::test_goal;
use crate::value_objects::UserId;
fn make_goal(year: u16, target: u32) -> Result<Goal, DomainError> {
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();
let g = test_goal(&UserId::generate(), 2024, 52);
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());
assert!(Goal::new(UserId::generate(), 2019, 10, GoalType::Movies).is_err());
}
#[test]
fn new_goal_rejects_zero_target() {
assert!(make_goal(2024, 0).is_err());
assert!(Goal::new(UserId::generate(), 2024, 0, GoalType::Movies).is_err());
}
#[test]
fn update_target_valid() {
let mut g = make_goal(2024, 10).unwrap();
let mut g = test_goal(&UserId::generate(), 2024, 10);
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();
let mut g = test_goal(&UserId::generate(), 2024, 10);
assert!(g.update_target(0).is_err());
}
@@ -52,7 +47,7 @@ fn from_persistence_preserves_fields() {
#[test]
fn percentage_calculation() {
let g = make_goal(2024, 100).unwrap();
let g = test_goal(&UserId::generate(), 2024, 100);
let wp = GoalWithProgress {
goal: g,
current_count: 50,
@@ -62,7 +57,7 @@ fn percentage_calculation() {
#[test]
fn percentage_caps_at_100() {
let g = make_goal(2024, 10).unwrap();
let g = test_goal(&UserId::generate(), 2024, 10);
let wp = GoalWithProgress {
goal: g,
current_count: 20,
@@ -72,7 +67,7 @@ fn percentage_caps_at_100() {
#[test]
fn is_complete() {
let g = make_goal(2024, 10).unwrap();
let g = test_goal(&UserId::generate(), 2024, 10);
let wp = GoalWithProgress {
goal: g,
current_count: 10,
@@ -82,7 +77,7 @@ fn is_complete() {
#[test]
fn is_not_complete() {
let g = make_goal(2024, 10).unwrap();
let g = test_goal(&UserId::generate(), 2024, 10);
let wp = GoalWithProgress {
goal: g,
current_count: 9,

View File

@@ -1,20 +1,10 @@
use super::*;
use crate::testing::{test_movie_with_director, test_movie_with_year, test_review, test_user};
use crate::value_objects::{Email, PasswordHash, UserId, Username};
fn make_user() -> User {
User::from_persistence(
UserId::generate(),
Email::new("a@b.com".to_string()).unwrap(),
Username::new("alice".to_string()).unwrap(),
PasswordHash::new("hash".to_string()).unwrap(),
UserRole::Standard,
UserProfile::default(),
)
}
#[test]
fn update_profile_sets_fields() {
let mut user = make_user();
let mut user = test_user("alice");
user.update_profile(UserProfile {
bio: Some("My bio".to_string()),
avatar_path: Some("avatars/abc".to_string()),
@@ -26,7 +16,7 @@ fn update_profile_sets_fields() {
#[test]
fn update_profile_clears_with_none() {
let mut user = make_user();
let mut user = test_user("alice");
user.update_profile(UserProfile {
bio: Some("bio".to_string()),
avatar_path: Some("path".to_string()),
@@ -39,19 +29,15 @@ fn update_profile_clears_with_none() {
// ── Movie ────────────────────────────────────────────────────────────────────
fn make_movie() -> Movie {
Movie::new(
#[test]
fn movie_new_sets_fields() {
let m = Movie::new(
Some(crate::value_objects::ExternalMetadataId::new("tt1234567".into()).unwrap()),
crate::value_objects::MovieTitle::new("Blade Runner".into()).unwrap(),
crate::value_objects::ReleaseYear::new(1982).unwrap(),
Some("Ridley Scott".into()),
Some(crate::value_objects::PosterPath::new("/poster.jpg".into()).unwrap()),
)
}
#[test]
fn movie_new_sets_fields() {
let m = make_movie();
);
assert_eq!(m.title().value(), "Blade Runner");
assert_eq!(m.release_year().value(), 1982);
assert_eq!(m.director(), Some("Ridley Scott"));
@@ -61,7 +47,7 @@ fn movie_new_sets_fields() {
#[test]
fn movie_update_poster() {
let mut m = make_movie();
let mut m = test_movie_with_year("Blade Runner", 1982);
let new_poster = crate::value_objects::PosterPath::new("/new.jpg".into()).unwrap();
m.update_poster(new_poster);
assert_eq!(m.poster_path().unwrap().value(), "/new.jpg");
@@ -69,31 +55,18 @@ fn movie_update_poster() {
// ── Review ───────────────────────────────────────────────────────────────────
use crate::value_objects::{Comment, MovieId, Rating, ReviewId};
fn make_review() -> Review {
Review::new(
MovieId::generate(),
UserId::generate(),
Rating::new(4).unwrap(),
Some(Comment::new("great".into()).unwrap()),
chrono::Utc::now().naive_utc(),
None,
false,
)
.unwrap()
}
use crate::value_objects::{MovieId, Rating, ReviewId};
#[test]
fn review_new_sets_local_source() {
let r = make_review();
let r = test_review(&MovieId::generate(), &UserId::generate(), 4);
assert_eq!(*r.source(), ReviewSource::Local);
assert!(!r.is_remote());
}
#[test]
fn review_stars() {
let r = make_review(); // rating=4
let r = test_review(&MovieId::generate(), &UserId::generate(), 4);
assert_eq!(r.stars(), [true, true, true, true, false]);
}
@@ -141,7 +114,7 @@ fn user_new() {
#[test]
fn user_update_password() {
let mut u = make_user();
let mut u = test_user("alice");
u.update_password(PasswordHash::new("new_hash".into()).unwrap());
assert_eq!(u.password_hash().value(), "new_hash");
}
@@ -197,7 +170,10 @@ fn movie_stats_construction() {
#[test]
fn feed_entry_display_name_from_email() {
let entry = DiaryEntry::new(make_movie(), make_review());
let entry = DiaryEntry::new(
test_movie_with_year("Test", 2000),
test_review(&MovieId::generate(), &UserId::generate(), 4),
);
let fe = FeedEntry::new(entry, "alice@example.com".into());
assert_eq!(fe.user_display_name(), "alice");
assert_eq!(fe.user_email(), "alice@example.com");
@@ -222,7 +198,7 @@ fn month_activity_construction() {
#[test]
fn movie_is_manual_match_same_title_year() {
let m = make_movie();
let m = test_movie_with_director("Blade Runner", 1982, "Ridley Scott");
let title = crate::value_objects::MovieTitle::new("Blade Runner".into()).unwrap();
let year = crate::value_objects::ReleaseYear::new(1982).unwrap();
assert!(m.is_manual_match(&title, &year, Some("ridley scott")));
@@ -230,7 +206,7 @@ fn movie_is_manual_match_same_title_year() {
#[test]
fn movie_is_manual_match_different_director_fails() {
let m = make_movie();
let m = test_movie_with_director("Blade Runner", 1982, "Ridley Scott");
let title = crate::value_objects::MovieTitle::new("Blade Runner".into()).unwrap();
let year = crate::value_objects::ReleaseYear::new(1982).unwrap();
assert!(!m.is_manual_match(&title, &year, Some("Denis Villeneuve")));

View File

@@ -1,37 +1,31 @@
use chrono::NaiveDateTime;
use crate::testing::test_review;
use crate::value_objects::{Comment, MovieId, Rating, UserId, WatchMedium};
use super::*;
fn make_review(watch_medium: Option<WatchMedium>) -> Review {
Review::new(
MovieId::generate(),
UserId::generate(),
Rating::new(4).unwrap(),
Some(Comment::new("great film".into()).unwrap()),
NaiveDateTime::parse_from_str("2024-06-15 20:00:00", "%Y-%m-%d %H:%M:%S").unwrap(),
watch_medium,
false,
)
.unwrap()
fn review_with_medium(watch_medium: Option<WatchMedium>) -> Review {
test_review(&MovieId::generate(), &UserId::generate(), 4).apply_edit(ReviewEdit {
comment: Some(Some(Comment::new("great film".into()).unwrap())),
watch_medium: Some(watch_medium),
..Default::default()
})
}
#[test]
fn new_review_stores_watch_medium() {
let review = make_review(Some(WatchMedium::Cinema));
let review = review_with_medium(Some(WatchMedium::Cinema));
assert_eq!(review.watch_medium(), Some(&WatchMedium::Cinema));
}
#[test]
fn new_review_without_medium() {
let review = make_review(None);
let review = review_with_medium(None);
assert_eq!(review.watch_medium(), None);
}
#[test]
fn apply_edit_updates_rating_only() {
let original = make_review(Some(WatchMedium::Streaming));
let original = review_with_medium(Some(WatchMedium::Streaming));
let edited = original.apply_edit(ReviewEdit {
rating: Some(Rating::new(2).unwrap()),
..Default::default()
@@ -43,7 +37,7 @@ fn apply_edit_updates_rating_only() {
#[test]
fn apply_edit_clears_comment() {
let original = make_review(None);
let original = review_with_medium(None);
let edited = original.apply_edit(ReviewEdit {
comment: Some(None),
..Default::default()
@@ -54,7 +48,7 @@ fn apply_edit_clears_comment() {
#[test]
fn apply_edit_sets_watch_medium() {
let original = make_review(None);
let original = review_with_medium(None);
let edited = original.apply_edit(ReviewEdit {
watch_medium: Some(Some(WatchMedium::Cinema)),
..Default::default()
@@ -64,7 +58,7 @@ fn apply_edit_sets_watch_medium() {
#[test]
fn apply_edit_clears_watch_medium() {
let original = make_review(Some(WatchMedium::Cinema));
let original = review_with_medium(Some(WatchMedium::Cinema));
let edited = original.apply_edit(ReviewEdit {
watch_medium: Some(None),
..Default::default()
@@ -74,7 +68,7 @@ fn apply_edit_clears_watch_medium() {
#[test]
fn apply_edit_toggles_spoilers() {
let original = make_review(None);
let original = review_with_medium(None);
let flag_on = original.apply_edit(ReviewEdit {
spoilers: Some(true),
..Default::default()
@@ -91,7 +85,7 @@ fn apply_edit_toggles_spoilers() {
#[test]
fn apply_edit_no_changes_preserves_all() {
let original = make_review(Some(WatchMedium::TV));
let original = review_with_medium(Some(WatchMedium::TV));
let edited = original.apply_edit(ReviewEdit::default());
assert_eq!(edited.rating().value(), 4);
assert_eq!(edited.comment().unwrap().value(), "great film");

View File

@@ -1,18 +1,9 @@
use super::*;
use crate::models::{Movie, Review, ReviewHistory};
use crate::value_objects::{MovieId, MovieTitle, Rating, ReleaseYear, UserId};
use crate::models::{Review, ReviewHistory};
use crate::testing::test_movie_with_year;
use crate::value_objects::{MovieId, Rating, 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(),
@@ -36,7 +27,7 @@ fn review_with_rating(movie_id: &MovieId, rating: u8, watched_at: NaiveDateTime)
#[test]
fn neutral_when_empty() {
let movie = make_movie();
let movie = test_movie_with_year("Test", 2024);
let history = ReviewHistory::new(movie, vec![]);
let trend = ReviewHistoryAnalyzer::rating_trend(&history).unwrap();
assert_eq!(trend, Trend::Neutral);
@@ -44,7 +35,7 @@ fn neutral_when_empty() {
#[test]
fn neutral_when_single_review() {
let movie = make_movie();
let movie = test_movie_with_year("Test", 2024);
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();
@@ -53,7 +44,7 @@ fn neutral_when_single_review() {
#[test]
fn improved_when_latest_above_average() {
let movie = make_movie();
let movie = test_movie_with_year("Test", 2024);
let viewings = vec![
review_with_rating(movie.id(), 2, dt(2024, 1, 1)),
review_with_rating(movie.id(), 3, dt(2024, 2, 1)),
@@ -66,7 +57,7 @@ fn improved_when_latest_above_average() {
#[test]
fn declined_when_latest_below_average() {
let movie = make_movie();
let movie = test_movie_with_year("Test", 2024);
let viewings = vec![
review_with_rating(movie.id(), 5, dt(2024, 1, 1)),
review_with_rating(movie.id(), 4, dt(2024, 2, 1)),

View File

@@ -0,0 +1,131 @@
use chrono::{NaiveDate, NaiveDateTime};
use crate::models::{
DiaryEntry, ExternalPersonId, Goal, GoalType, Movie, Person, Review, User, UserProfile,
UserRole,
};
use crate::value_objects::{
Email, ExternalMetadataId, MovieId, MovieTitle, PasswordHash, PersonId, Rating, ReleaseYear,
UserId, Username,
};
const DEFAULT_YEAR: u16 = 2000;
/// Fixed watch timestamp used by review factories (2024-03-15 20:00:00).
pub fn test_watched_at() -> NaiveDateTime {
NaiveDate::from_ymd_opt(2024, 3, 15)
.unwrap()
.and_hms_opt(20, 0, 0)
.unwrap()
}
pub fn test_movie(title: &str) -> Movie {
test_movie_with_year(title, DEFAULT_YEAR)
}
pub fn test_movie_with_year(title: &str, year: u16) -> Movie {
Movie::new(
None,
MovieTitle::new(title.to_string()).unwrap(),
ReleaseYear::new(year).unwrap(),
None,
None,
)
}
pub fn test_movie_with_director(title: &str, year: u16, director: &str) -> Movie {
Movie::new(
None,
MovieTitle::new(title.to_string()).unwrap(),
ReleaseYear::new(year).unwrap(),
Some(director.to_string()),
None,
)
}
pub fn test_movie_with_external_id(
title: &str,
year: u16,
external_id: ExternalMetadataId,
) -> Movie {
Movie::new(
Some(external_id),
MovieTitle::new(title.to_string()).unwrap(),
ReleaseYear::new(year).unwrap(),
None,
None,
)
}
pub fn test_review(movie_id: &MovieId, user_id: &UserId, rating: u8) -> Review {
build_review(movie_id, user_id, rating, false)
}
pub fn test_spoiler_review(movie_id: &MovieId, user_id: &UserId, rating: u8) -> Review {
build_review(movie_id, user_id, rating, true)
}
fn build_review(movie_id: &MovieId, user_id: &UserId, rating: u8, spoilers: bool) -> Review {
Review::new(
movie_id.clone(),
user_id.clone(),
Rating::new(rating).unwrap(),
None,
test_watched_at(),
None,
spoilers,
)
.unwrap()
}
pub fn test_user(username: &str) -> User {
User::new(
Email::new(format!("{username}@example.com")).unwrap(),
Username::new(username.to_string()).unwrap(),
PasswordHash::new("hashed-password".to_string()).unwrap(),
UserRole::Standard,
)
}
pub fn test_diary_entry(title: &str, rating: u8) -> DiaryEntry {
let movie = test_movie(title);
let review = test_review(movie.id(), &UserId::generate(), rating);
DiaryEntry::new(movie, review)
}
pub fn test_goal(user_id: &UserId, year: u16, target_count: u32) -> Goal {
Goal::new(user_id.clone(), year, target_count, GoalType::Movies).unwrap()
}
pub fn test_user_with_role(id: UserId, role: UserRole) -> User {
User::from_persistence(
id,
Email::new("role-test@example.com".to_string()).unwrap(),
Username::new("role_test_user".to_string()).unwrap(),
PasswordHash::new("hashed-password".to_string()).unwrap(),
role,
UserProfile::default(),
)
}
pub fn test_person(name: &str) -> Person {
let ext = ExternalPersonId::new(format!("test:{name}"));
Person::basic(
PersonId::from_external(&ext),
ext,
name.to_string(),
None,
None,
)
}
pub fn test_person_with_tmdb(tmdb_id: i64, name: &str, department: Option<&str>) -> Person {
let ext = ExternalPersonId::new(format!("tmdb:{tmdb_id}"));
Person::basic(
PersonId::from_external(&ext),
ext,
name.to_string(),
department.map(str::to_string),
None,
)
}

View File

@@ -1,3 +1,4 @@
mod factories;
mod fakes;
mod in_memory;
mod insights;
@@ -5,6 +6,7 @@ mod noops;
mod panics;
mod wrapup;
pub use factories::*;
pub use fakes::*;
pub use in_memory::*;
pub use insights::*;