test: shared factories and command builders
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")));
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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)),
|
||||
|
||||
131
crates/domain/src/testing/factories.rs
Normal file
131
crates/domain/src/testing/factories.rs
Normal 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,
|
||||
)
|
||||
}
|
||||
@@ -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::*;
|
||||
|
||||
Reference in New Issue
Block a user