diff --git a/crates/adapters/export/Cargo.toml b/crates/adapters/export/Cargo.toml index edcdcfc3..3259145a 100644 --- a/crates/adapters/export/Cargo.toml +++ b/crates/adapters/export/Cargo.toml @@ -13,5 +13,6 @@ bytes = { workspace = true } async-stream = { workspace = true } [dev-dependencies] +domain = { workspace = true, features = ["test-helpers"] } uuid = { workspace = true } tokio = { workspace = true } diff --git a/crates/adapters/export/src/tests/lib.rs b/crates/adapters/export/src/tests/lib.rs index 83b82b83..674dd7e8 100644 --- a/crates/adapters/export/src/tests/lib.rs +++ b/crates/adapters/export/src/tests/lib.rs @@ -1,8 +1,11 @@ use super::ExportAdapter; use domain::{ - models::{DiaryEntry, ExportFormat, Movie, Review}, + models::{DiaryEntry, ExportFormat, Movie, ReviewEdit}, ports::DiaryExporter, - value_objects::{ExternalMetadataId, MovieTitle, Rating, ReleaseYear}, + testing::{ + test_movie_with_director, test_movie_with_external_id, test_movie_with_year, test_review, + }, + value_objects::{Comment, ExternalMetadataId, UserId}, }; async fn collect_stream( @@ -26,55 +29,19 @@ fn entry_stream( Box::pin(futures::stream::iter(entries.into_iter().map(Ok))) } -fn make_entry( - title: &str, - year: u16, - director: Option<&str>, - rating: u8, - comment: Option<&str>, -) -> DiaryEntry { - make_entry_full(title, year, director, rating, comment, None) -} - -fn make_entry_full( - title: &str, - year: u16, - director: Option<&str>, - rating: u8, - comment: Option<&str>, - external_id: Option<&str>, -) -> DiaryEntry { - let movie = Movie::new( - external_id.map(|id| ExternalMetadataId::new(id.to_string()).unwrap()), - MovieTitle::new(title.to_string()).unwrap(), - ReleaseYear::new(year).unwrap(), - director.map(str::to_string), - None, - ); - let user_id = domain::value_objects::UserId::from_uuid(uuid::Uuid::new_v4()); - let review = Review::new( - movie.id().clone(), - user_id, - Rating::new(rating).unwrap(), - comment.map(|c| domain::value_objects::Comment::new(c.to_string()).unwrap()), - chrono::NaiveDate::from_ymd_opt(2024, 3, 15) - .unwrap() - .and_hms_opt(0, 0, 0) - .unwrap(), - None, - false, - ) - .unwrap(); +fn entry_for(movie: Movie, rating: u8, comment: Option<&str>) -> DiaryEntry { + let review = test_review(movie.id(), &UserId::generate(), rating).apply_edit(ReviewEdit { + comment: Some(comment.map(|c| Comment::new(c.to_string()).unwrap())), + ..Default::default() + }); DiaryEntry::new(movie, review) } #[tokio::test] async fn csv_has_header_and_one_row() { let adapter = ExportAdapter; - let entry = make_entry( - "Inception", - 2010, - Some("Christopher Nolan"), + let entry = entry_for( + test_movie_with_director("Inception", 2010, "Christopher Nolan"), 5, Some("great"), ); @@ -95,7 +62,7 @@ async fn csv_has_header_and_one_row() { #[tokio::test] async fn csv_escapes_commas_in_title() { let adapter = ExportAdapter; - let entry = make_entry("Tár, A Film", 2022, None, 4, None); + let entry = entry_for(test_movie_with_year("Tár, A Film", 2022), 4, None); let bytes = collect_stream(adapter.stream_entries(entry_stream(vec![entry]), ExportFormat::Csv)).await; let text = String::from_utf8(bytes).unwrap(); @@ -105,7 +72,11 @@ async fn csv_escapes_commas_in_title() { #[tokio::test] async fn json_is_valid_array() { let adapter = ExportAdapter; - let entry = make_entry("Dune", 2021, Some("Denis Villeneuve"), 5, None); + let entry = entry_for( + test_movie_with_director("Dune", 2021, "Denis Villeneuve"), + 5, + None, + ); let bytes = collect_stream(adapter.stream_entries(entry_stream(vec![entry]), ExportFormat::Json)).await; let arr: Vec = serde_json::from_slice(&bytes).unwrap(); @@ -120,20 +91,29 @@ async fn json_is_valid_array() { #[tokio::test] async fn external_metadata_id_included_when_present() { let adapter = ExportAdapter; - let entry = make_entry_full("Alien", 1979, None, 5, None, Some("tt0078748")); + let entry = entry_for( + test_movie_with_external_id( + "Alien", + 1979, + ExternalMetadataId::new("tt0078748".to_string()).unwrap(), + ), + 5, + None, + ); let bytes = collect_stream(adapter.stream_entries(entry_stream(vec![entry]), ExportFormat::Json)).await; let arr: Vec = serde_json::from_slice(&bytes).unwrap(); assert_eq!(arr[0]["external_metadata_id"], "tt0078748"); let bytes = collect_stream(adapter.stream_entries( - entry_stream(vec![make_entry_full( - "Alien", - 1979, - None, + entry_stream(vec![entry_for( + test_movie_with_external_id( + "Alien", + 1979, + ExternalMetadataId::new("tt0078748".to_string()).unwrap(), + ), 5, None, - Some("tt0078748"), )]), ExportFormat::Csv, )) diff --git a/crates/adapters/http-axum/src/extractors/tests.rs b/crates/adapters/http-axum/src/extractors/tests.rs index 71dd3670..af6d7b20 100644 --- a/crates/adapters/http-axum/src/extractors/tests.rs +++ b/crates/adapters/http-axum/src/extractors/tests.rs @@ -966,26 +966,6 @@ impl UserRepository for ErroringUserRepo { } } -/// Builds a bare `User` with the given role, for the admin/non-admin extractor -/// tests — only `id()` and `role()` are read by `authorize_admin::execute`. -fn make_user_with_role(id: UserId, role: domain::models::UserRole) -> domain::models::User { - domain::models::User::from_persistence( - id, - domain::value_objects::Email::new("extractor-test@example.com".into()).unwrap(), - domain::value_objects::Username::new("extractor_test_user".into()).unwrap(), - domain::value_objects::PasswordHash::new("hashed".into()).unwrap(), - role, - domain::models::UserProfile { - display_name: None, - bio: None, - avatar_path: None, - banner_path: None, - also_known_as: None, - profile_fields: vec![], - }, - ) -} - /// Auth stub that accepts any bearer token and resolves it to a fixed user id, /// for tests that need to authenticate as a specific user rather than merely /// exercise the unauthenticated-request path. @@ -1154,7 +1134,7 @@ async fn admin_api_user_allows_an_admin_user() { let user_repo = InMemoryUserRepository::new(); user_repo.store.lock().unwrap().insert( id.value(), - make_user_with_role(id.clone(), domain::models::UserRole::Admin), + domain::testing::test_user_with_role(id.clone(), domain::models::UserRole::Admin), ); let app = router_admin_api(test_app_state_with_user( user_repo as _, @@ -1179,7 +1159,7 @@ async fn admin_api_user_rejects_a_non_admin_user() { let user_repo = InMemoryUserRepository::new(); user_repo.store.lock().unwrap().insert( id.value(), - make_user_with_role(id.clone(), domain::models::UserRole::Standard), + domain::testing::test_user_with_role(id.clone(), domain::models::UserRole::Standard), ); let app = router_admin_api(test_app_state_with_user( user_repo as _, @@ -1245,7 +1225,7 @@ async fn admin_user_allows_an_admin_user() { let user_repo = InMemoryUserRepository::new(); user_repo.store.lock().unwrap().insert( id.value(), - make_user_with_role(id.clone(), domain::models::UserRole::Admin), + domain::testing::test_user_with_role(id.clone(), domain::models::UserRole::Admin), ); let app = router_admin_cookie(test_app_state_with_user( user_repo as _, @@ -1270,7 +1250,7 @@ async fn admin_user_rejects_a_non_admin_user() { let user_repo = InMemoryUserRepository::new(); user_repo.store.lock().unwrap().insert( id.value(), - make_user_with_role(id.clone(), domain::models::UserRole::Standard), + domain::testing::test_user_with_role(id.clone(), domain::models::UserRole::Standard), ); let app = router_admin_cookie(test_app_state_with_user( user_repo as _, diff --git a/crates/adapters/rss/Cargo.toml b/crates/adapters/rss/Cargo.toml index 73207cc5..e5024f23 100644 --- a/crates/adapters/rss/Cargo.toml +++ b/crates/adapters/rss/Cargo.toml @@ -8,4 +8,5 @@ rss-feed = { package = "rss", version = "2", features = ["atom"] } domain = { workspace = true } [dev-dependencies] +domain = { workspace = true, features = ["test-helpers"] } chrono = { workspace = true } diff --git a/crates/adapters/rss/src/tests/lib.rs b/crates/adapters/rss/src/tests/lib.rs index 3b282f5e..7f567f55 100644 --- a/crates/adapters/rss/src/tests/lib.rs +++ b/crates/adapters/rss/src/tests/lib.rs @@ -1,6 +1,7 @@ use super::*; -use domain::models::{Movie, Review}; -use domain::value_objects::{Comment, MovieTitle, Rating, ReleaseYear, UserId}; +use domain::models::ReviewEdit; +use domain::testing::{test_movie_with_year, test_review}; +use domain::value_objects::{Comment, UserId}; fn channel<'a>(title: &'a str) -> FeedChannel<'a> { FeedChannel { @@ -11,23 +12,12 @@ fn channel<'a>(title: &'a str) -> FeedChannel<'a> { } fn entry(spoilers: bool) -> DiaryEntry { - let movie = Movie::new( - None, - MovieTitle::new("Blade Runner".into()).unwrap(), - ReleaseYear::new(1982).unwrap(), - None, - None, - ); - let review = Review::new( - movie.id().clone(), - UserId::generate(), - Rating::new(4).unwrap(), - Some(Comment::new("replicants".into()).unwrap()), - chrono::Utc::now().naive_utc(), - None, - spoilers, - ) - .unwrap(); + let movie = test_movie_with_year("Blade Runner", 1982); + let review = test_review(movie.id(), &UserId::generate(), 4).apply_edit(ReviewEdit { + comment: Some(Some(Comment::new("replicants".into()).unwrap())), + spoilers: Some(spoilers), + ..Default::default() + }); DiaryEntry::new(movie, review) } diff --git a/crates/adapters/sqlite/Cargo.toml b/crates/adapters/sqlite/Cargo.toml index 373715e7..de6cd663 100644 --- a/crates/adapters/sqlite/Cargo.toml +++ b/crates/adapters/sqlite/Cargo.toml @@ -22,4 +22,5 @@ bytes = { workspace = true } async-stream = { workspace = true } [dev-dependencies] +domain = { workspace = true, features = ["test-helpers"] } tempfile = { workspace = true } diff --git a/crates/adapters/sqlite/src/repositories/persons/tests.rs b/crates/adapters/sqlite/src/repositories/persons/tests.rs index 3dff2026..16623ad1 100644 --- a/crates/adapters/sqlite/src/repositories/persons/tests.rs +++ b/crates/adapters/sqlite/src/repositories/persons/tests.rs @@ -1,6 +1,7 @@ use super::SqlitePersonAdapter; +use domain::testing::test_person_with_tmdb; use domain::{ - models::{ExternalPersonId, Person, PersonId}, + models::{ExternalPersonId, PersonId}, ports::{PersonCommand, PersonQuery}, }; use sqlx::SqlitePool; @@ -47,25 +48,14 @@ async fn pool_with_schema() -> SqlitePool { pool } -fn make_person(tmdb_id: i64, name: &str, dept: Option<&str>) -> Person { - let ext = ExternalPersonId::new(format!("tmdb:{tmdb_id}")); - Person::basic( - PersonId::from_external(&ext), - ext, - name.to_string(), - dept.map(str::to_string), - None, - ) -} - #[tokio::test] async fn upsert_batch_inserts_persons() { let pool = pool_with_schema().await; let adapter = SqlitePersonAdapter::new(pool.clone()); let persons = vec![ - make_person(1, "Alice", Some("Acting")), - make_person(2, "Bob", Some("Directing")), + test_person_with_tmdb(1, "Alice", Some("Acting")), + test_person_with_tmdb(2, "Bob", Some("Directing")), ]; adapter.upsert_batch(&persons).await.unwrap(); @@ -81,7 +71,7 @@ async fn upsert_batch_is_idempotent() { let pool = pool_with_schema().await; let adapter = SqlitePersonAdapter::new(pool.clone()); - let persons = vec![make_person(1, "Alice", Some("Acting"))]; + let persons = vec![test_person_with_tmdb(1, "Alice", Some("Acting"))]; adapter.upsert_batch(&persons).await.unwrap(); adapter.upsert_batch(&persons).await.unwrap(); @@ -97,7 +87,7 @@ async fn get_by_id_returns_person() { let pool = pool_with_schema().await; let adapter = SqlitePersonAdapter::new(pool.clone()); - let p = make_person(42, "Charlie", Some("Acting")); + let p = test_person_with_tmdb(42, "Charlie", Some("Acting")); adapter .upsert_batch(std::slice::from_ref(&p)) .await @@ -122,7 +112,7 @@ async fn get_credits_returns_cast_and_crew() { let pool = pool_with_schema().await; let adapter = SqlitePersonAdapter::new(pool.clone()); - let p = make_person(7, "Diana", Some("Acting")); + let p = test_person_with_tmdb(7, "Diana", Some("Acting")); adapter .upsert_batch(std::slice::from_ref(&p)) .await diff --git a/crates/adapters/sqlite/src/repositories/users/tests.rs b/crates/adapters/sqlite/src/repositories/users/tests.rs index c6fd2faa..bd94d1d6 100644 --- a/crates/adapters/sqlite/src/repositories/users/tests.rs +++ b/crates/adapters/sqlite/src/repositories/users/tests.rs @@ -1,6 +1,5 @@ use super::*; -use domain::models::UserRole; -use domain::value_objects::{Email, PasswordHash, Username}; +use domain::testing::test_user; use sqlx::SqlitePool; async fn setup() -> (SqlitePool, SqliteUserRepository) { @@ -55,12 +54,7 @@ async fn find_by_id_returns_user_when_found() { #[tokio::test] async fn update_profile_persists_bio_and_avatar() { let (_, repo) = setup().await; - let user = domain::models::User::new( - Email::new("test@example.com".to_string()).unwrap(), - Username::new("testuser".to_string()).unwrap(), - PasswordHash::new("hash".to_string()).unwrap(), - UserRole::Standard, - ); + let user = test_user("testuser"); repo.save(&user).await.unwrap(); repo.update_profile( @@ -82,12 +76,7 @@ async fn update_profile_persists_bio_and_avatar() { #[tokio::test] async fn update_profile_clears_fields_with_none() { let (_, repo) = setup().await; - let user = domain::models::User::new( - Email::new("test2@example.com".to_string()).unwrap(), - Username::new("testuser2".to_string()).unwrap(), - PasswordHash::new("hash".to_string()).unwrap(), - UserRole::Standard, - ); + let user = test_user("testuser2"); repo.save(&user).await.unwrap(); repo.update_profile( user.id(), diff --git a/crates/application/src/auth/tests/login.rs b/crates/application/src/auth/tests/login.rs index c60fd40a..b317e2b5 100644 --- a/crates/application/src/auth/tests/login.rs +++ b/crates/application/src/auth/tests/login.rs @@ -1,15 +1,13 @@ +use crate::test_support::{login_command, register_command}; use std::sync::Arc; -use domain::models::UserRole; use domain::testing::InMemoryUserRepository; use crate::{ auth::{ commands::RegisterCommand, deps::{LoginDeps, RegisterDeps}, - login, - queries::LoginCommand, - register, + login, register, }, test_helpers::TestContextBuilder, }; @@ -23,10 +21,8 @@ async fn setup_user(b: &TestContextBuilder, email: &str, password: &str) { register::execute( &deps, RegisterCommand { - email: email.to_string(), - username: "testuser".to_string(), password: password.to_string(), - role: UserRole::Standard, + ..register_command(email, "testuser") }, ) .await @@ -46,15 +42,9 @@ async fn test_login_valid_credentials_returns_token() { refresh_session: b.refresh_session_repo.clone(), config: b.config.clone(), }; - let result = login::execute( - &deps, - LoginCommand { - email: "carol@example.com".into(), - password: "secret123".into(), - }, - ) - .await - .unwrap(); + let result = login::execute(&deps, login_command("carol@example.com", "secret123")) + .await + .unwrap(); assert!(!result.token.is_empty()); assert!(!result.refresh_token.is_empty()); @@ -74,14 +64,7 @@ async fn test_login_wrong_password_fails() { refresh_session: b.refresh_session_repo.clone(), config: b.config.clone(), }; - let result = login::execute( - &deps, - LoginCommand { - email: "dave@example.com".into(), - password: "wrong_password".into(), - }, - ) - .await; + let result = login::execute(&deps, login_command("dave@example.com", "wrong_password")).await; assert!(result.is_err()); } @@ -96,14 +79,7 @@ async fn test_login_unknown_email_fails() { refresh_session: b.refresh_session_repo.clone(), config: b.config.clone(), }; - let result = login::execute( - &deps, - LoginCommand { - email: "nobody@example.com".into(), - password: "anything".into(), - }, - ) - .await; + let result = login::execute(&deps, login_command("nobody@example.com", "anything")).await; assert!(result.is_err()); } diff --git a/crates/application/src/auth/tests/logout.rs b/crates/application/src/auth/tests/logout.rs index ed4a6ff2..ac355548 100644 --- a/crates/application/src/auth/tests/logout.rs +++ b/crates/application/src/auth/tests/logout.rs @@ -1,15 +1,12 @@ +use crate::test_support::{login_command, register_command}; use std::sync::Arc; -use domain::models::UserRole; use domain::testing::InMemoryUserRepository; use crate::{ auth::{ - commands::RegisterCommand, deps::{LoginDeps, LogoutDeps, RefreshDeps, RegisterDeps}, - login, logout, - queries::LoginCommand, - refresh, register, + login, logout, refresh, register, }, test_helpers::TestContextBuilder, }; @@ -24,17 +21,9 @@ async fn logout_revokes_refresh_token() { password_hasher: b.password_hasher.clone(), config: b.config.clone(), }; - register::execute( - ®_deps, - RegisterCommand { - email: "bob@example.com".to_string(), - username: "bob".to_string(), - password: "password123".to_string(), - role: UserRole::Standard, - }, - ) - .await - .unwrap(); + register::execute(®_deps, register_command("bob@example.com", "bob")) + .await + .unwrap(); let login_deps = LoginDeps { user: b.user_repo.clone(), @@ -43,15 +32,9 @@ async fn logout_revokes_refresh_token() { refresh_session: b.refresh_session_repo.clone(), config: b.config.clone(), }; - let login_result = login::execute( - &login_deps, - LoginCommand { - email: "bob@example.com".into(), - password: "password123".into(), - }, - ) - .await - .unwrap(); + let login_result = login::execute(&login_deps, login_command("bob@example.com", "password123")) + .await + .unwrap(); let logout_deps = LogoutDeps { refresh_session: b.refresh_session_repo.clone(), diff --git a/crates/application/src/auth/tests/refresh.rs b/crates/application/src/auth/tests/refresh.rs index 09191959..21493cc4 100644 --- a/crates/application/src/auth/tests/refresh.rs +++ b/crates/application/src/auth/tests/refresh.rs @@ -1,15 +1,12 @@ +use crate::test_support::{login_command, register_command}; use std::sync::Arc; -use domain::models::UserRole; use domain::testing::InMemoryUserRepository; use crate::{ auth::{ - commands::RegisterCommand, deps::{LoginDeps, RefreshDeps, RegisterDeps}, - login, - queries::LoginCommand, - refresh, register, + login, refresh, register, }, test_helpers::TestContextBuilder, }; @@ -20,17 +17,9 @@ async fn login_user(b: &TestContextBuilder) -> login::LoginResult { password_hasher: b.password_hasher.clone(), config: b.config.clone(), }; - register::execute( - ®_deps, - RegisterCommand { - email: "alice@example.com".to_string(), - username: "alice".to_string(), - password: "password123".to_string(), - role: UserRole::Standard, - }, - ) - .await - .unwrap(); + register::execute(®_deps, register_command("alice@example.com", "alice")) + .await + .unwrap(); let login_deps = LoginDeps { user: b.user_repo.clone(), @@ -41,10 +30,7 @@ async fn login_user(b: &TestContextBuilder) -> login::LoginResult { }; login::execute( &login_deps, - LoginCommand { - email: "alice@example.com".into(), - password: "password123".into(), - }, + login_command("alice@example.com", "password123"), ) .await .unwrap() diff --git a/crates/application/src/auth/tests/register.rs b/crates/application/src/auth/tests/register.rs index c67c7f96..5249ddcc 100644 --- a/crates/application/src/auth/tests/register.rs +++ b/crates/application/src/auth/tests/register.rs @@ -1,6 +1,6 @@ +use crate::test_support::register_command; use std::sync::Arc; -use domain::models::UserRole; use domain::ports::UserRepository; use domain::testing::InMemoryUserRepository; use domain::value_objects::Email; @@ -11,12 +11,7 @@ use crate::{ }; fn cmd(email: &str) -> RegisterCommand { - RegisterCommand { - email: email.to_string(), - username: "alice".to_string(), - password: "password123".to_string(), - role: UserRole::Standard, - } + register_command(email, "alice") } #[tokio::test] @@ -67,10 +62,8 @@ async fn test_register_short_password_fails() { let result = register::execute( &deps, RegisterCommand { - email: "x@y.com".to_string(), - username: "testuser".to_string(), password: "short".to_string(), - role: UserRole::Standard, + ..register_command("x@y.com", "testuser") }, ) .await; diff --git a/crates/application/src/diary/tests/delete_review.rs b/crates/application/src/diary/tests/delete_review.rs index 3c36f9f2..a30902ab 100644 --- a/crates/application/src/diary/tests/delete_review.rs +++ b/crates/application/src/diary/tests/delete_review.rs @@ -1,43 +1,18 @@ +use domain::testing::{test_movie_with_year, test_review}; use std::sync::Arc; -use chrono::Utc; - use domain::{ - models::{Movie, Review}, ports::{MovieCommand, MovieQuery, ReviewRepository}, testing::{ FakeDiaryQuery, InMemoryMovieRepository, InMemoryReviewRepository, NoopEventPublisher, }, - value_objects::{MovieId, MovieTitle, Rating, ReleaseYear, UserId}, + value_objects::{MovieId, UserId}, }; use crate::{ diary::commands::DeleteReviewCommand, diary::delete_review, diary::deps::DeleteReviewDeps, }; -fn make_movie() -> Movie { - Movie::new( - None, - MovieTitle::new("Terminator".into()).unwrap(), - ReleaseYear::new(1984).unwrap(), - None, - None, - ) -} - -fn make_review(movie_id: MovieId, user_id: UserId) -> Review { - Review::new( - movie_id, - user_id, - Rating::new(4).unwrap(), - None, - Utc::now().naive_utc(), - None, - false, - ) - .unwrap() -} - #[tokio::test] async fn test_delete_review_removes_it() { let movies = InMemoryMovieRepository::new(); @@ -45,9 +20,9 @@ async fn test_delete_review_removes_it() { let diary = FakeDiaryQuery::new(); let events = NoopEventPublisher::new(); - let movie = make_movie(); + let movie = test_movie_with_year("Terminator", 1984); let user_id = UserId::from_uuid(uuid::Uuid::new_v4()); - let review = make_review(movie.id().clone(), user_id.clone()); + let review = test_review(movie.id(), &user_id, 4); movies.upsert_movie(&movie).await.unwrap(); reviews.save_review(&review).await.unwrap(); @@ -87,7 +62,7 @@ async fn test_delete_review_wrong_user_is_unauthorized() { let movie_id = MovieId::from_uuid(uuid::Uuid::new_v4()); let owner_id = UserId::from_uuid(uuid::Uuid::new_v4()); let other_id = uuid::Uuid::new_v4(); - let review = make_review(movie_id, owner_id); + let review = test_review(&movie_id, &owner_id, 4); reviews.save_review(&review).await.unwrap(); diff --git a/crates/application/src/diary/tests/edit_review.rs b/crates/application/src/diary/tests/edit_review.rs index 37ace682..e5244c8b 100644 --- a/crates/application/src/diary/tests/edit_review.rs +++ b/crates/application/src/diary/tests/edit_review.rs @@ -1,30 +1,15 @@ +use crate::test_support::edit_review_command; use std::sync::Arc; -use chrono::Utc; -use uuid::Uuid; - use domain::{ - models::Review, + models::ReviewEdit, ports::ReviewRepository, - testing::{InMemoryReviewRepository, NoopEventPublisher}, - value_objects::{Comment, MovieId, Rating, ReviewId, UserId, WatchMedium}, + testing::{InMemoryReviewRepository, NoopEventPublisher, test_review}, + value_objects::{Comment, MovieId, ReviewId, UserId, WatchMedium}, }; use crate::diary::{commands::EditReviewCommand, deps::EditReviewDeps, edit_review}; -fn make_review(user_id: UserId) -> Review { - Review::new( - MovieId::generate(), - user_id, - Rating::new(3).unwrap(), - Some(Comment::new("original comment".into()).unwrap()), - Utc::now().naive_utc(), - Some(WatchMedium::Streaming), - false, - ) - .unwrap() -} - async fn setup() -> ( Arc, Arc, @@ -34,7 +19,11 @@ async fn setup() -> ( let reviews = InMemoryReviewRepository::new(); let events = NoopEventPublisher::new(); let user_id = UserId::generate(); - let review = make_review(user_id.clone()); + let review = test_review(&MovieId::generate(), &user_id, 3).apply_edit(ReviewEdit { + comment: Some(Some(Comment::new("original comment".into()).unwrap())), + watch_medium: Some(Some(WatchMedium::Streaming)), + ..Default::default() + }); let review_id = review.id().clone(); reviews.save_review(&review).await.unwrap(); (reviews, events, review_id, user_id) @@ -59,11 +48,7 @@ async fn edit_own_review_updates_rating() { EditReviewCommand { review_id: review_id.value(), requesting_user_id: user_id.value(), - rating: Some(5), - comment: None, - watched_at: None, - watch_medium: None, - spoilers: None, + ..edit_review_command(5) }, ) .await @@ -80,20 +65,9 @@ async fn edit_nonexistent_review_returns_not_found() { let reviews = InMemoryReviewRepository::new(); let events = NoopEventPublisher::new(); - let err = edit_review::execute( - &deps(&reviews, &events), - EditReviewCommand { - review_id: Uuid::new_v4(), - requesting_user_id: Uuid::new_v4(), - rating: Some(5), - comment: None, - watched_at: None, - watch_medium: None, - spoilers: None, - }, - ) - .await - .unwrap_err(); + let err = edit_review::execute(&deps(&reviews, &events), edit_review_command(5)) + .await + .unwrap_err(); assert!(matches!(err, domain::errors::DomainError::NotFound(_))); } @@ -106,12 +80,7 @@ async fn edit_other_users_review_returns_forbidden() { &deps(&reviews, &events), EditReviewCommand { review_id: review_id.value(), - requesting_user_id: Uuid::new_v4(), - rating: Some(5), - comment: None, - watched_at: None, - watch_medium: None, - spoilers: None, + ..edit_review_command(5) }, ) .await @@ -129,11 +98,7 @@ async fn edit_publishes_review_updated_event() { EditReviewCommand { review_id: review_id.value(), requesting_user_id: user_id.value(), - rating: Some(1), - comment: None, - watched_at: None, - watch_medium: None, - spoilers: None, + ..edit_review_command(1) }, ) .await @@ -154,13 +119,11 @@ async fn edit_sets_watch_medium() { edit_review::execute( &deps(&reviews, &events), EditReviewCommand { + rating: None, review_id: review_id.value(), requesting_user_id: user_id.value(), - rating: None, - comment: None, - watched_at: None, watch_medium: Some(Some(WatchMedium::Cinema)), - spoilers: None, + ..edit_review_command(1) }, ) .await diff --git a/crates/application/src/diary/tests/get_movie_social_page.rs b/crates/application/src/diary/tests/get_movie_social_page.rs index c3780679..547b543b 100644 --- a/crates/application/src/diary/tests/get_movie_social_page.rs +++ b/crates/application/src/diary/tests/get_movie_social_page.rs @@ -1,12 +1,11 @@ +use domain::testing::test_movie_with_year; use std::sync::Arc; use uuid::Uuid; use domain::{ - models::Movie, ports::MovieCommand, testing::{FakeDiaryQuery, InMemoryMovieProfileRepository, InMemoryMovieRepository}, - value_objects::{MovieTitle, ReleaseYear}, }; use crate::{ @@ -39,13 +38,7 @@ async fn fails_when_movie_not_found() { async fn returns_movie_social_page() { let movies = InMemoryMovieRepository::new(); - let movie = Movie::new( - None, - MovieTitle::new("Social Movie".into()).unwrap(), - ReleaseYear::new(2024).unwrap(), - None, - None, - ); + let movie = test_movie_with_year("Social Movie", 2024); let movie_uuid = movie.id().value(); movies.upsert_movie(&movie).await.unwrap(); diff --git a/crates/application/src/diary/tests/get_review_history.rs b/crates/application/src/diary/tests/get_review_history.rs index b0131a7c..6306e956 100644 --- a/crates/application/src/diary/tests/get_review_history.rs +++ b/crates/application/src/diary/tests/get_review_history.rs @@ -1,11 +1,7 @@ +use domain::testing::test_movie_with_year; use std::sync::Arc; -use domain::{ - models::Movie, - ports::DiaryQuery, - services::review_history::Trend, - value_objects::{MovieTitle, ReleaseYear}, -}; +use domain::{ports::DiaryQuery, services::review_history::Trend}; use crate::{ diary::deps::GetReviewHistoryDeps, diary::get_review_history, @@ -14,13 +10,7 @@ use crate::{ #[tokio::test] async fn returns_empty_history() { - let movie = Movie::new( - None, - MovieTitle::new("Test".into()).unwrap(), - ReleaseYear::new(2024).unwrap(), - None, - None, - ); + let movie = test_movie_with_year("Test", 2024); let movie_id = movie.id().value(); let diary = domain::testing::FakeDiaryQuery::new(); diff --git a/crates/application/src/diary/tests/get_user_feed.rs b/crates/application/src/diary/tests/get_user_feed.rs index 76e06879..6ada6df8 100644 --- a/crates/application/src/diary/tests/get_user_feed.rs +++ b/crates/application/src/diary/tests/get_user_feed.rs @@ -1,13 +1,14 @@ +use crate::test_support::register_command; +use domain::testing::{test_movie_with_year, test_review}; use std::sync::Arc; use uuid::Uuid; use domain::errors::DomainError; -use domain::models::{DiaryEntry, Movie, Review, UserRole, collections::Paginated}; +use domain::models::{DiaryEntry, collections::Paginated}; use domain::testing::FakeDiaryQuery; -use domain::value_objects::{Email, MovieTitle, Rating, ReleaseYear, UserId}; +use domain::value_objects::{Email, UserId}; -use crate::auth::commands::RegisterCommand; use crate::auth::deps::RegisterDeps; use crate::auth::register; use crate::diary::deps::GetUserFeedDeps; @@ -20,17 +21,9 @@ async fn setup_user(b: &TestContextBuilder, email: &str, username: &str) { password_hasher: b.password_hasher.clone(), config: b.config.clone(), }; - register::execute( - &deps, - RegisterCommand { - email: email.into(), - username: username.into(), - password: "password123".into(), - role: UserRole::Standard, - }, - ) - .await - .unwrap(); + register::execute(&deps, register_command(email, username)) + .await + .unwrap(); } #[tokio::test] @@ -43,23 +36,8 @@ async fn user_feed_carries_author_and_entries() { let uid = user.id().value(); let diary = FakeDiaryQuery::new(); - let movie = Movie::new( - None, - MovieTitle::new("Feed Movie".into()).unwrap(), - ReleaseYear::new(2020).unwrap(), - None, - None, - ); - let review = Review::new( - movie.id().clone(), - UserId::from_uuid(uid), - Rating::new(5).unwrap(), - None, - chrono::Utc::now().naive_utc(), - None, - false, - ) - .unwrap(); + let movie = test_movie_with_year("Feed Movie", 2020); + let review = test_review(movie.id(), &UserId::from_uuid(uid), 5); diary.set_diary_page(Paginated { items: vec![DiaryEntry::new(movie, review)], total_count: 1, diff --git a/crates/application/src/diary/tests/log_review.rs b/crates/application/src/diary/tests/log_review.rs index a4696a17..f511bfdd 100644 --- a/crates/application/src/diary/tests/log_review.rs +++ b/crates/application/src/diary/tests/log_review.rs @@ -1,20 +1,14 @@ +use crate::test_support::{log_review_command, log_review_command_manual}; +use domain::testing::test_movie_with_year; use std::sync::Arc; -use chrono::Utc; - -use domain::{ - models::Movie, - ports::MovieCommand, - value_objects::{MovieTitle, ReleaseYear}, -}; +use domain::ports::MovieCommand; use domain::testing::{InMemoryMovieRepository, InMemoryReviewRepository, NoopEventPublisher}; use crate::{ - diary::commands::{LogReviewCommand, MovieInput}, - diary::log_review, - diary::review_logger::DefaultReviewLogger, - test_helpers::TestContextBuilder, + diary::commands::LogReviewCommand, diary::log_review, + diary::review_logger::DefaultReviewLogger, test_helpers::TestContextBuilder, }; fn build_logger( @@ -32,26 +26,6 @@ fn build_logger( )) } -fn movie_input_manual(title: &str, year: u16) -> MovieInput { - MovieInput { - movie_id: None, - external_metadata_id: None, - manual_title: Some(title.to_string()), - manual_release_year: Some(year), - manual_director: None, - } -} - -fn movie_input_by_id(id: uuid::Uuid) -> MovieInput { - MovieInput { - movie_id: Some(id), - external_metadata_id: None, - manual_title: None, - manual_release_year: None, - manual_director: None, - } -} - #[tokio::test] async fn test_log_review_creates_movie_and_review() { let movies = InMemoryMovieRepository::new(); @@ -60,15 +34,7 @@ async fn test_log_review_creates_movie_and_review() { let logger = build_logger(&movies, &reviews, &events); let user_id = uuid::Uuid::new_v4(); - let cmd = LogReviewCommand { - user_id, - input: movie_input_manual("Blade Runner", 1982), - rating: 4, - comment: None, - watched_at: Utc::now().naive_utc(), - watch_medium: None, - spoilers: false, - }; + let cmd = log_review_command_manual(user_id, "Blade Runner", 1982); log_review::execute(&logger, cmd).await.unwrap(); @@ -81,13 +47,7 @@ async fn test_log_review_reuses_existing_movie() { let movies = InMemoryMovieRepository::new(); let reviews = InMemoryReviewRepository::new(); - let existing_movie = Movie::new( - None, - MovieTitle::new("Alien".into()).unwrap(), - ReleaseYear::new(1979).unwrap(), - None, - None, - ); + let existing_movie = test_movie_with_year("Alien", 1979); let movie_uuid = existing_movie.id().value(); movies.upsert_movie(&existing_movie).await.unwrap(); @@ -95,13 +55,8 @@ async fn test_log_review_reuses_existing_movie() { let logger = build_logger(&movies, &reviews, &events); let cmd = LogReviewCommand { - user_id: uuid::Uuid::new_v4(), - input: movie_input_by_id(movie_uuid), rating: 5, - comment: None, - watched_at: Utc::now().naive_utc(), - watch_medium: None, - spoilers: false, + ..log_review_command(uuid::Uuid::new_v4(), movie_uuid) }; log_review::execute(&logger, cmd).await.unwrap(); @@ -118,13 +73,8 @@ async fn test_log_review_with_invalid_rating_fails() { let logger = build_logger(&movies, &reviews, &events); let cmd = LogReviewCommand { - user_id: uuid::Uuid::new_v4(), - input: movie_input_manual("Some Film", 2000), rating: 6, - comment: None, - watched_at: Utc::now().naive_utc(), - watch_medium: None, - spoilers: false, + ..log_review_command_manual(uuid::Uuid::new_v4(), "Some Film", 2000) }; let result = log_review::execute(&logger, cmd).await; assert!(result.is_err(), "rating > 5 should fail"); diff --git a/crates/application/src/diary/tests/movie_resolver.rs b/crates/application/src/diary/tests/movie_resolver.rs index b9416a83..7387c9ad 100644 --- a/crates/application/src/diary/tests/movie_resolver.rs +++ b/crates/application/src/diary/tests/movie_resolver.rs @@ -1,5 +1,6 @@ use super::*; use crate::diary::commands::MovieInput; +use domain::testing::test_movie_with_year; use domain::{ errors::DomainError, models::{MetadataSearchCriteria, Movie}, @@ -17,16 +18,6 @@ fn make_input(ext_id: Option<&str>, title: Option<&str>, year: Option) -> M } } -fn make_movie() -> Movie { - Movie::new( - None, - MovieTitle::new("Inception".to_string()).unwrap(), - ReleaseYear::new(2010).unwrap(), - None, - None, - ) -} - struct RepoWithExternalMovie(Movie); struct RepoEmpty; struct RepoWithTitleMatch(Movie); @@ -213,7 +204,7 @@ fn external_id_strategy_cannot_handle_cmd_without_id() { #[tokio::test] async fn external_id_strategy_returns_cached_movie() { - let movie = make_movie(); + let movie = test_movie_with_year("Inception", 2010); let repo = RepoWithExternalMovie(movie.clone()); let meta = MetaErrors; let deps = MovieResolverDeps { @@ -227,7 +218,7 @@ async fn external_id_strategy_returns_cached_movie() { #[tokio::test] async fn external_id_strategy_fetches_from_metadata_when_not_cached() { - let movie = make_movie(); + let movie = test_movie_with_year("Inception", 2010); let repo = RepoEmpty; let meta = MetaReturnsMovie(movie); let deps = MovieResolverDeps { @@ -268,7 +259,7 @@ fn title_strategy_cannot_handle_cmd_without_title() { #[tokio::test] async fn title_strategy_fetches_from_metadata() { - let movie = make_movie(); + let movie = test_movie_with_year("Inception", 2010); let repo = RepoEmpty; let meta = MetaReturnsMovie(movie); let deps = MovieResolverDeps { @@ -309,7 +300,7 @@ fn manual_strategy_cannot_handle_cmd_without_title() { #[tokio::test] async fn manual_strategy_returns_existing_movie() { - let movie = make_movie(); + let movie = test_movie_with_year("Inception", 2010); let repo = RepoWithTitleMatch(movie.clone()); let meta = MetaErrors; let deps = MovieResolverDeps { @@ -365,7 +356,7 @@ async fn resolver_returns_error_when_no_strategy_matches() { #[tokio::test] async fn resolver_uses_cached_movie_when_external_id_matches() { - let movie = make_movie(); + let movie = test_movie_with_year("Inception", 2010); let repo = RepoWithExternalMovie(movie.clone()); let meta = MetaErrors; let deps = MovieResolverDeps { diff --git a/crates/application/src/diary/tests/review_logger.rs b/crates/application/src/diary/tests/review_logger.rs index ac2ea4e7..3b8fbaf0 100644 --- a/crates/application/src/diary/tests/review_logger.rs +++ b/crates/application/src/diary/tests/review_logger.rs @@ -1,7 +1,8 @@ +use crate::test_support::{log_review_command, log_review_command_manual}; +use domain::testing::{test_movie_with_external_id, test_movie_with_year}; use std::sync::Arc; use async_trait::async_trait; -use chrono::Utc; use domain::{ errors::DomainError, models::WatchlistEntry, @@ -11,11 +12,11 @@ use domain::{ FakeMetadataClient, InMemoryMovieRepository, InMemoryReviewRepository, InMemoryWatchlistRepository, NoopEventPublisher, }, - value_objects::{ExternalMetadataId, MovieId, MovieTitle, PosterUrl, ReleaseYear, UserId}, + value_objects::{ExternalMetadataId, MovieId, PosterUrl, UserId}, }; use uuid::Uuid; -use crate::diary::commands::{LogReviewCommand, MovieInput}; +use crate::diary::commands::LogReviewCommand; use crate::diary::review_logger::DefaultReviewLogger; use crate::ports::ReviewLogger; @@ -44,21 +45,7 @@ async fn logs_review_with_manual_movie() { let logger = make_logger(&movies, &reviews, &watchlist, &events); let uid = Uuid::new_v4(); - let cmd = LogReviewCommand { - user_id: uid, - input: MovieInput { - movie_id: None, - external_metadata_id: None, - manual_title: Some("Test Film".into()), - manual_release_year: Some(2024), - manual_director: None, - }, - rating: 4, - comment: None, - watched_at: Utc::now().naive_utc(), - watch_medium: None, - spoilers: false, - }; + let cmd = log_review_command_manual(uid, "Test Film", 2024); logger.log_review(cmd).await.unwrap(); @@ -78,13 +65,7 @@ async fn removes_from_watchlist_on_review() { let user_id = UserId::from_uuid(uid); // Create and store movie - let movie = Movie::new( - None, - MovieTitle::new("Watchlisted Film".into()).unwrap(), - ReleaseYear::new(2024).unwrap(), - None, - None, - ); + let movie = test_movie_with_year("Watchlisted Film", 2024); let movie_id = movie.id().value(); movies.upsert_movie(&movie).await.unwrap(); @@ -95,19 +76,8 @@ async fn removes_from_watchlist_on_review() { // Log review for same movie let cmd = LogReviewCommand { - user_id: uid, - input: MovieInput { - movie_id: Some(movie_id), - external_metadata_id: None, - manual_title: None, - manual_release_year: None, - manual_director: None, - }, rating: 5, - comment: None, - watched_at: Utc::now().naive_utc(), - watch_medium: None, - spoilers: false, + ..log_review_command(uid, movie_id) }; logger.log_review(cmd).await.unwrap(); @@ -123,30 +93,13 @@ async fn logs_review_with_existing_movie_by_id() { let events = NoopEventPublisher::new(); let logger = make_logger(&movies, &reviews, &watchlist, &events); - let movie = Movie::new( - None, - MovieTitle::new("Existing Film".into()).unwrap(), - ReleaseYear::new(2020).unwrap(), - None, - None, - ); + let movie = test_movie_with_year("Existing Film", 2020); let movie_uuid = movie.id().value(); movies.upsert_movie(&movie).await.unwrap(); let cmd = LogReviewCommand { - user_id: Uuid::new_v4(), - input: MovieInput { - movie_id: Some(movie_uuid), - external_metadata_id: None, - manual_title: None, - manual_release_year: None, - manual_director: None, - }, rating: 3, - comment: None, - watched_at: Utc::now().naive_utc(), - watch_medium: None, - spoilers: false, + ..log_review_command(Uuid::new_v4(), movie_uuid) }; logger.log_review(cmd).await.unwrap(); @@ -163,21 +116,7 @@ async fn existing_movie_not_found_returns_error() { let events = NoopEventPublisher::new(); let logger = make_logger(&movies, &reviews, &watchlist, &events); - let cmd = LogReviewCommand { - user_id: Uuid::new_v4(), - input: MovieInput { - movie_id: Some(Uuid::new_v4()), - external_metadata_id: None, - manual_title: None, - manual_release_year: None, - manual_director: None, - }, - rating: 4, - comment: None, - watched_at: Utc::now().naive_utc(), - watch_medium: None, - spoilers: false, - }; + let cmd = log_review_command(Uuid::new_v4(), Uuid::new_v4()); assert!(logger.log_review(cmd).await.is_err()); } @@ -191,19 +130,8 @@ async fn invalid_rating_returns_error() { let logger = make_logger(&movies, &reviews, &watchlist, &events); let cmd = LogReviewCommand { - user_id: Uuid::new_v4(), - input: MovieInput { - movie_id: None, - external_metadata_id: None, - manual_title: Some("Film".into()), - manual_release_year: Some(2024), - manual_director: None, - }, rating: 6, - comment: None, - watched_at: Utc::now().naive_utc(), - watch_medium: None, - spoilers: false, + ..log_review_command_manual(Uuid::new_v4(), "Film", 2024) }; let result = logger.log_review(cmd).await; @@ -221,21 +149,7 @@ async fn watchlist_not_present_does_not_publish_removed() { let events = NoopEventPublisher::new(); let logger = make_logger(&movies, &reviews, &watchlist, &events); - let cmd = LogReviewCommand { - user_id: Uuid::new_v4(), - input: MovieInput { - movie_id: None, - external_metadata_id: None, - manual_title: Some("No Watchlist Film".into()), - manual_release_year: Some(2024), - manual_director: None, - }, - rating: 4, - comment: None, - watched_at: Utc::now().naive_utc(), - watch_medium: None, - spoilers: false, - }; + let cmd = log_review_command_manual(Uuid::new_v4(), "No Watchlist Film", 2024); logger.log_review(cmd).await.unwrap(); @@ -256,12 +170,10 @@ impl MetadataClient for MetadataClientWithExternalId { &self, _criteria: &MetadataSearchCriteria, ) -> Result { - Ok(Movie::new( - Some(ExternalMetadataId::new("tmdb:99999".into()).unwrap()), - MovieTitle::new("Discovered Film".into()).unwrap(), - ReleaseYear::new(2024).unwrap(), - None, - None, + Ok(test_movie_with_external_id( + "Discovered Film", + 2024, + ExternalMetadataId::new("tmdb:99999".into()).unwrap(), )) } @@ -290,19 +202,8 @@ async fn publishes_movie_discovered_for_new_movie_with_external_id() { ); let cmd = LogReviewCommand { - user_id: Uuid::new_v4(), - input: MovieInput { - movie_id: None, - external_metadata_id: None, - manual_title: Some("Discovered Film".into()), - manual_release_year: Some(2024), - manual_director: None, - }, rating: 5, - comment: None, - watched_at: Utc::now().naive_utc(), - watch_medium: None, - spoilers: false, + ..log_review_command_manual(Uuid::new_v4(), "Discovered Film", 2024) }; logger.log_review(cmd).await.unwrap(); diff --git a/crates/application/src/goals/tests/create.rs b/crates/application/src/goals/tests/create.rs index 7a9f9783..b3db8e00 100644 --- a/crates/application/src/goals/tests/create.rs +++ b/crates/application/src/goals/tests/create.rs @@ -1,11 +1,12 @@ +use crate::test_support::create_goal_command; use std::sync::Arc; use domain::events::DomainEvent; use domain::testing::{FakeStatsRepository, InMemoryGoalRepository, NoopEventPublisher}; use uuid::Uuid; +use crate::goals::create; use crate::goals::deps::GoalCommandDeps; -use crate::goals::{commands::CreateGoalCommand, create}; use crate::test_helpers::TestContextBuilder; #[tokio::test] @@ -20,16 +21,9 @@ async fn creates_goal_and_returns_progress() { event_publisher: Arc::clone(&events) as _, }; - let result = create::execute( - &deps, - CreateGoalCommand { - user_id: Uuid::nil(), - year: 2025, - target_count: 50, - }, - ) - .await - .unwrap(); + let result = create::execute(&deps, create_goal_command(2025, 50)) + .await + .unwrap(); assert_eq!(result.goal.year(), 2025); assert_eq!(result.goal.target_count(), 50); @@ -50,16 +44,9 @@ async fn creates_goal_with_review_count() { event_publisher: Arc::clone(&events) as _, }; - let result = create::execute( - &deps, - CreateGoalCommand { - user_id: Uuid::nil(), - year: 2025, - target_count: 50, - }, - ) - .await - .unwrap(); + let result = create::execute(&deps, create_goal_command(2025, 50)) + .await + .unwrap(); assert_eq!(result.current_count, 5); assert_eq!(goals.count(), 1); @@ -76,16 +63,9 @@ async fn emits_goal_created_event() { event_publisher: Arc::clone(&events) as _, }; - create::execute( - &deps, - CreateGoalCommand { - user_id: Uuid::nil(), - year: 2025, - target_count: 10, - }, - ) - .await - .unwrap(); + create::execute(&deps, create_goal_command(2025, 10)) + .await + .unwrap(); let published = events.published(); assert!( @@ -104,23 +84,11 @@ async fn rejects_duplicate_year() { stats: b.stats_repo.clone(), event_publisher: b.event_publisher.clone(), }; - let cmd = CreateGoalCommand { - user_id: Uuid::nil(), - year: 2025, - target_count: 10, - }; + let cmd = create_goal_command(2025, 10); create::execute(&deps, cmd).await.unwrap(); - let result = create::execute( - &deps, - CreateGoalCommand { - user_id: Uuid::nil(), - year: 2025, - target_count: 20, - }, - ) - .await; + let result = create::execute(&deps, create_goal_command(2025, 20)).await; assert!(result.is_err()); } @@ -134,15 +102,7 @@ async fn rejects_year_before_2020() { stats: b.stats_repo.clone(), event_publisher: b.event_publisher.clone(), }; - let result = create::execute( - &deps, - CreateGoalCommand { - user_id: Uuid::nil(), - year: 2019, - target_count: 10, - }, - ) - .await; + let result = create::execute(&deps, create_goal_command(2019, 10)).await; assert!(result.is_err()); } @@ -156,15 +116,7 @@ async fn rejects_zero_target() { stats: b.stats_repo.clone(), event_publisher: b.event_publisher.clone(), }; - let result = create::execute( - &deps, - CreateGoalCommand { - user_id: Uuid::nil(), - year: 2025, - target_count: 0, - }, - ) - .await; + let result = create::execute(&deps, create_goal_command(2025, 0)).await; assert!(result.is_err()); } diff --git a/crates/application/src/goals/tests/delete.rs b/crates/application/src/goals/tests/delete.rs index 408cbdf2..07aa2724 100644 --- a/crates/application/src/goals/tests/delete.rs +++ b/crates/application/src/goals/tests/delete.rs @@ -1,13 +1,11 @@ +use crate::test_support::create_goal_command; use std::sync::Arc; use domain::testing::{FakeStatsRepository, InMemoryGoalRepository, NoopEventPublisher}; use uuid::Uuid; use crate::goals::deps::GoalCommandDeps; -use crate::goals::{ - commands::{CreateGoalCommand, DeleteGoalCommand}, - create, delete, -}; +use crate::goals::{commands::DeleteGoalCommand, create, delete}; use crate::test_helpers::TestContextBuilder; #[tokio::test] @@ -22,16 +20,9 @@ async fn deletes_existing_goal() { event_publisher: Arc::clone(&events) as _, }; - create::execute( - &deps, - CreateGoalCommand { - user_id: Uuid::nil(), - year: 2025, - target_count: 10, - }, - ) - .await - .unwrap(); + create::execute(&deps, create_goal_command(2025, 10)) + .await + .unwrap(); assert_eq!(goals.count(), 1); delete::execute( diff --git a/crates/application/src/goals/tests/get.rs b/crates/application/src/goals/tests/get.rs index 8ffb37b9..020b63e9 100644 --- a/crates/application/src/goals/tests/get.rs +++ b/crates/application/src/goals/tests/get.rs @@ -1,7 +1,8 @@ +use crate::test_support::create_goal_command; use uuid::Uuid; use crate::goals::deps::{GoalCommandDeps, GoalQueryDeps}; -use crate::goals::{commands::CreateGoalCommand, create, get, queries::GetGoalQuery}; +use crate::goals::{create, get, queries::GetGoalQuery}; use crate::test_helpers::TestContextBuilder; #[tokio::test] @@ -18,16 +19,9 @@ async fn returns_goal_when_exists() { stats: b.stats_repo.clone(), }; - create::execute( - &cmd_deps, - CreateGoalCommand { - user_id: Uuid::nil(), - year: 2025, - target_count: 50, - }, - ) - .await - .unwrap(); + create::execute(&cmd_deps, create_goal_command(2025, 50)) + .await + .unwrap(); let result = get::execute( &query_deps, diff --git a/crates/application/src/goals/tests/list.rs b/crates/application/src/goals/tests/list.rs index 3edb380f..05d8e688 100644 --- a/crates/application/src/goals/tests/list.rs +++ b/crates/application/src/goals/tests/list.rs @@ -1,7 +1,8 @@ +use crate::test_support::create_goal_command; use uuid::Uuid; use crate::goals::deps::{GoalCommandDeps, GoalQueryDeps}; -use crate::goals::{commands::CreateGoalCommand, create, list, queries::ListGoalsQuery}; +use crate::goals::{create, list, queries::ListGoalsQuery}; use crate::test_helpers::TestContextBuilder; #[tokio::test] @@ -38,16 +39,9 @@ async fn returns_all_goals_for_user() { }; for year in [2023, 2024, 2025] { - create::execute( - &cmd_deps, - CreateGoalCommand { - user_id: Uuid::nil(), - year, - target_count: 10, - }, - ) - .await - .unwrap(); + create::execute(&cmd_deps, create_goal_command(year, 10)) + .await + .unwrap(); } let result = list::execute( diff --git a/crates/application/src/goals/tests/update.rs b/crates/application/src/goals/tests/update.rs index e973ee55..9ec7e650 100644 --- a/crates/application/src/goals/tests/update.rs +++ b/crates/application/src/goals/tests/update.rs @@ -1,10 +1,7 @@ -use uuid::Uuid; +use crate::test_support::{create_goal_command, update_goal_command}; use crate::goals::deps::GoalCommandDeps; -use crate::goals::{ - commands::{CreateGoalCommand, UpdateGoalCommand}, - create, update, -}; +use crate::goals::{create, update}; use crate::test_helpers::TestContextBuilder; #[tokio::test] @@ -17,27 +14,13 @@ async fn updates_target_count() { event_publisher: b.event_publisher.clone(), }; - create::execute( - &deps, - CreateGoalCommand { - user_id: Uuid::nil(), - year: 2025, - target_count: 10, - }, - ) - .await - .unwrap(); + create::execute(&deps, create_goal_command(2025, 10)) + .await + .unwrap(); - let result = update::execute( - &deps, - UpdateGoalCommand { - user_id: Uuid::nil(), - year: 2025, - target_count: 100, - }, - ) - .await - .unwrap(); + let result = update::execute(&deps, update_goal_command(2025, 100)) + .await + .unwrap(); assert_eq!(result.goal.target_count(), 100); } @@ -51,15 +34,7 @@ async fn fails_when_goal_not_found() { stats: b.stats_repo.clone(), event_publisher: b.event_publisher.clone(), }; - let result = update::execute( - &deps, - UpdateGoalCommand { - user_id: Uuid::nil(), - year: 2025, - target_count: 10, - }, - ) - .await; + let result = update::execute(&deps, update_goal_command(2025, 10)).await; assert!(result.is_err()); } @@ -74,26 +49,11 @@ async fn rejects_zero_target() { event_publisher: b.event_publisher.clone(), }; - create::execute( - &deps, - CreateGoalCommand { - user_id: Uuid::nil(), - year: 2025, - target_count: 10, - }, - ) - .await - .unwrap(); + create::execute(&deps, create_goal_command(2025, 10)) + .await + .unwrap(); - let result = update::execute( - &deps, - UpdateGoalCommand { - user_id: Uuid::nil(), - year: 2025, - target_count: 0, - }, - ) - .await; + let result = update::execute(&deps, update_goal_command(2025, 0)).await; assert!(result.is_err()); } diff --git a/crates/application/src/import/tests/apply_mapping.rs b/crates/application/src/import/tests/apply_mapping.rs index 86d3417a..93d7755b 100644 --- a/crates/application/src/import/tests/apply_mapping.rs +++ b/crates/application/src/import/tests/apply_mapping.rs @@ -1,23 +1,21 @@ +use crate::test_support::{apply_import_mapping_command, create_import_session_command}; +use domain::testing::{test_movie_with_external_id, test_movie_with_year}; use std::sync::Arc; use uuid::Uuid; use domain::{ models::{ - AnnotatedRow, Movie, + AnnotatedRow, import::{ImportRow, ParsedFile, RowResult}, }, ports::{DocumentParser, MovieCommand}, testing::{InMemoryImportSessionRepository, InMemoryMovieRepository}, - value_objects::{ExternalMetadataId, MovieTitle, ReleaseYear}, + value_objects::ExternalMetadataId, }; use crate::import::deps::{ApplyMappingDeps, CreateSessionDeps}; -use crate::import::{ - apply_mapping, - commands::{ApplyImportMappingCommand, CreateImportSessionCommand}, - create_session, -}; +use crate::import::{apply_mapping, create_session}; use crate::test_helpers::TestContextBuilder; #[tokio::test] @@ -33,11 +31,7 @@ async fn applies_mapping_to_session() { let session = create_session::execute( &create_deps, - CreateImportSessionCommand { - user_id, - bytes: b"title\nTest".to_vec(), - format: domain::models::FileFormat::Csv, - }, + create_import_session_command(user_id, b"title\nTest"), ) .await .unwrap(); @@ -50,11 +44,7 @@ async fn applies_mapping_to_session() { let rows = apply_mapping::execute( &mapping_deps, - ApplyImportMappingCommand { - user_id, - session_id: session.session_id.value(), - mappings: vec![], - }, + apply_import_mapping_command(user_id, session.session_id.value()), ) .await .unwrap(); @@ -75,11 +65,7 @@ async fn fails_when_session_not_found() { let result = apply_mapping::execute( &deps, - ApplyImportMappingCommand { - user_id: Uuid::new_v4(), - session_id: Uuid::new_v4(), - mappings: vec![], - }, + apply_import_mapping_command(Uuid::new_v4(), Uuid::new_v4()), ) .await; @@ -123,13 +109,7 @@ async fn marks_duplicate_by_external_id() { let sessions = InMemoryImportSessionRepository::new(); let ext_id = ExternalMetadataId::new("tt1234567".into()).unwrap(); - let movie = Movie::new( - Some(ext_id), - MovieTitle::new("Known Movie".into()).unwrap(), - ReleaseYear::new(2020).unwrap(), - None, - None, - ); + let movie = test_movie_with_external_id("Known Movie", 2020, ext_id); movies.upsert_movie(&movie).await.unwrap(); let parser = Arc::new(DuplicateTestParser { @@ -150,11 +130,7 @@ async fn marks_duplicate_by_external_id() { let session = create_session::execute( &create_deps, - CreateImportSessionCommand { - user_id, - bytes: b"title\nKnown Movie".to_vec(), - format: domain::models::FileFormat::Csv, - }, + create_import_session_command(user_id, b"title\nKnown Movie"), ) .await .unwrap(); @@ -167,11 +143,7 @@ async fn marks_duplicate_by_external_id() { let rows = apply_mapping::execute( &mapping_deps, - ApplyImportMappingCommand { - user_id, - session_id: session.session_id.value(), - mappings: vec![], - }, + apply_import_mapping_command(user_id, session.session_id.value()), ) .await .unwrap(); @@ -185,13 +157,7 @@ async fn marks_duplicate_by_title_and_year() { let movies = InMemoryMovieRepository::new(); let sessions = InMemoryImportSessionRepository::new(); - let movie = Movie::new( - None, - MovieTitle::new("Duplicate Film".into()).unwrap(), - ReleaseYear::new(2022).unwrap(), - None, - None, - ); + let movie = test_movie_with_year("Duplicate Film", 2022); movies.upsert_movie(&movie).await.unwrap(); let parser = Arc::new(DuplicateTestParser { @@ -211,11 +177,7 @@ async fn marks_duplicate_by_title_and_year() { let session = create_session::execute( &create_deps, - CreateImportSessionCommand { - user_id, - bytes: b"title\nDuplicate Film".to_vec(), - format: domain::models::FileFormat::Csv, - }, + create_import_session_command(user_id, b"title\nDuplicate Film"), ) .await .unwrap(); @@ -228,11 +190,7 @@ async fn marks_duplicate_by_title_and_year() { let rows = apply_mapping::execute( &mapping_deps, - ApplyImportMappingCommand { - user_id, - session_id: session.session_id.value(), - mappings: vec![], - }, + apply_import_mapping_command(user_id, session.session_id.value()), ) .await .unwrap(); diff --git a/crates/application/src/import/tests/apply_profile.rs b/crates/application/src/import/tests/apply_profile.rs index 59398e7a..c90caaa9 100644 --- a/crates/application/src/import/tests/apply_profile.rs +++ b/crates/application/src/import/tests/apply_profile.rs @@ -1,3 +1,4 @@ +use crate::test_support::apply_import_profile_command; use std::sync::Arc; use chrono::Utc; @@ -7,8 +8,8 @@ use domain::testing::{InMemoryImportProfileRepository, InMemoryImportSessionRepo use domain::value_objects::{ImportProfileId, UserId}; use uuid::Uuid; +use crate::import::apply_profile; use crate::import::deps::ApplyProfileDeps; -use crate::import::{apply_profile, commands::ApplyImportProfileCommand}; #[tokio::test] async fn fails_when_profile_not_found() { @@ -22,11 +23,7 @@ async fn fails_when_profile_not_found() { let result = apply_profile::execute( &deps, - ApplyImportProfileCommand { - user_id: Uuid::new_v4(), - session_id: Uuid::new_v4(), - profile_id: Uuid::new_v4(), - }, + apply_import_profile_command(Uuid::new_v4(), Uuid::new_v4(), Uuid::new_v4()), ) .await; @@ -56,11 +53,7 @@ async fn fails_when_session_not_found() { let result = apply_profile::execute( &deps, - ApplyImportProfileCommand { - user_id, - session_id: Uuid::new_v4(), - profile_id: profile_id.value(), - }, + apply_import_profile_command(user_id, Uuid::new_v4(), profile_id.value()), ) .await; @@ -98,11 +91,7 @@ async fn applies_profile_mappings_to_session() { apply_profile::execute( &deps, - ApplyImportProfileCommand { - user_id, - session_id: session_id.value(), - profile_id: profile_id.value(), - }, + apply_import_profile_command(user_id, session_id.value(), profile_id.value()), ) .await .unwrap(); diff --git a/crates/application/src/import/tests/apply_profile_and_map.rs b/crates/application/src/import/tests/apply_profile_and_map.rs index a5ccb6b1..8906d5d1 100644 --- a/crates/application/src/import/tests/apply_profile_and_map.rs +++ b/crates/application/src/import/tests/apply_profile_and_map.rs @@ -1,19 +1,17 @@ +use crate::test_support::create_import_session_command; use std::sync::Arc; use chrono::Utc; use uuid::Uuid; use domain::models::import::{DomainField, Transform}; -use domain::models::{FieldMapping, FileFormat, ImportProfile}; +use domain::models::{FieldMapping, ImportProfile}; use domain::ports::{ImportProfileRepository, ImportSessionRepository}; use domain::testing::{InMemoryImportProfileRepository, InMemoryImportSessionRepository}; use domain::value_objects::{ImportProfileId, UserId}; use crate::import::deps::{ApplyProfileAndMapDeps, CreateSessionDeps}; -use crate::import::{ - apply_profile_and_map, commands::ApplyProfileAndMapCommand, - commands::CreateImportSessionCommand, create_session, -}; +use crate::import::{apply_profile_and_map, commands::ApplyProfileAndMapCommand, create_session}; use crate::test_helpers::TestContextBuilder; #[tokio::test] @@ -69,11 +67,7 @@ async fn applies_profile_then_regenerates_mapping() { }; let created = create_session::execute( &create_deps, - CreateImportSessionCommand { - user_id, - bytes: b"title\nTest".to_vec(), - format: FileFormat::Csv, - }, + create_import_session_command(user_id, b"title\nTest"), ) .await .unwrap(); diff --git a/crates/application/src/import/tests/create_session.rs b/crates/application/src/import/tests/create_session.rs index bc7f3c66..9ebe974c 100644 --- a/crates/application/src/import/tests/create_session.rs +++ b/crates/application/src/import/tests/create_session.rs @@ -1,11 +1,12 @@ +use crate::test_support::create_import_session_command; use std::sync::Arc; use uuid::Uuid; use domain::testing::InMemoryImportSessionRepository; +use crate::import::create_session; use crate::import::deps::CreateSessionDeps; -use crate::import::{commands::CreateImportSessionCommand, create_session}; use crate::test_helpers::TestContextBuilder; #[tokio::test] @@ -20,11 +21,7 @@ async fn creates_session_with_parsed_file() { let result = create_session::execute( &deps, - CreateImportSessionCommand { - user_id: Uuid::new_v4(), - bytes: b"col1\nval1".to_vec(), - format: domain::models::FileFormat::Csv, - }, + create_import_session_command(Uuid::new_v4(), b"col1\nval1"), ) .await .unwrap(); diff --git a/crates/application/src/import/tests/execute.rs b/crates/application/src/import/tests/execute.rs index 197b7061..cc9fddf0 100644 --- a/crates/application/src/import/tests/execute.rs +++ b/crates/application/src/import/tests/execute.rs @@ -1,3 +1,4 @@ +use crate::test_support::execute_import_command; use std::sync::Arc; use domain::models::{AnnotatedRow, ImportSession, import::RowResult}; @@ -59,9 +60,8 @@ async fn imports_confirmed_rows() { let result = execute::execute( &deps, ExecuteImportCommand { - user_id: uid, - session_id: sid.value(), confirmed_indices: vec![0, 1], + ..execute_import_command(uid, sid.value()) }, ) .await @@ -86,16 +86,9 @@ async fn skips_unconfirmed_rows() { review_logger: Arc::new(NoopReviewLogger), }; - let result = execute::execute( - &deps, - ExecuteImportCommand { - user_id: uid, - session_id: sid.value(), - confirmed_indices: vec![0], - }, - ) - .await - .unwrap(); + let result = execute::execute(&deps, execute_import_command(uid, sid.value())) + .await + .unwrap(); assert_eq!(result.imported, 1); assert_eq!(result.skipped_duplicates, 1); @@ -113,9 +106,8 @@ async fn fails_when_session_not_found() { let result = execute::execute( &deps, ExecuteImportCommand { - user_id: Uuid::new_v4(), - session_id: Uuid::new_v4(), confirmed_indices: vec![], + ..execute_import_command(Uuid::new_v4(), Uuid::new_v4()) }, ) .await; @@ -149,16 +141,9 @@ async fn handles_datetime_format() { review_logger: Arc::new(NoopReviewLogger), }; - let result = execute::execute( - &deps, - ExecuteImportCommand { - user_id: uid, - session_id: sid.value(), - confirmed_indices: vec![0], - }, - ) - .await - .unwrap(); + let result = execute::execute(&deps, execute_import_command(uid, sid.value())) + .await + .unwrap(); assert_eq!(result.imported, 1); assert!(result.failed.is_empty()); @@ -190,16 +175,9 @@ async fn fails_on_invalid_rating() { review_logger: Arc::new(NoopReviewLogger), }; - let result = execute::execute( - &deps, - ExecuteImportCommand { - user_id: uid, - session_id: sid.value(), - confirmed_indices: vec![0], - }, - ) - .await - .unwrap(); + let result = execute::execute(&deps, execute_import_command(uid, sid.value())) + .await + .unwrap(); assert_eq!(result.imported, 0); assert_eq!(result.failed.len(), 1); @@ -231,16 +209,9 @@ async fn fails_on_missing_watched_at() { review_logger: Arc::new(NoopReviewLogger), }; - let result = execute::execute( - &deps, - ExecuteImportCommand { - user_id: uid, - session_id: sid.value(), - confirmed_indices: vec![0], - }, - ) - .await - .unwrap(); + let result = execute::execute(&deps, execute_import_command(uid, sid.value())) + .await + .unwrap(); assert_eq!(result.imported, 0); assert_eq!(result.failed.len(), 1); @@ -272,16 +243,9 @@ async fn imports_row_with_external_metadata_id() { review_logger: Arc::new(NoopReviewLogger), }; - let result = execute::execute( - &deps, - ExecuteImportCommand { - user_id: uid, - session_id: sid.value(), - confirmed_indices: vec![0], - }, - ) - .await - .unwrap(); + let result = execute::execute(&deps, execute_import_command(uid, sid.value())) + .await + .unwrap(); assert_eq!(result.imported, 1); assert!(result.failed.is_empty()); @@ -313,16 +277,9 @@ async fn imports_row_with_director_and_comment() { review_logger: Arc::new(NoopReviewLogger), }; - let result = execute::execute( - &deps, - ExecuteImportCommand { - user_id: uid, - session_id: sid.value(), - confirmed_indices: vec![0], - }, - ) - .await - .unwrap(); + let result = execute::execute(&deps, execute_import_command(uid, sid.value())) + .await + .unwrap(); assert_eq!(result.imported, 1); assert!(result.failed.is_empty()); @@ -354,16 +311,9 @@ async fn handles_space_separated_datetime_format() { review_logger: Arc::new(NoopReviewLogger), }; - let result = execute::execute( - &deps, - ExecuteImportCommand { - user_id: uid, - session_id: sid.value(), - confirmed_indices: vec![0], - }, - ) - .await - .unwrap(); + let result = execute::execute(&deps, execute_import_command(uid, sid.value())) + .await + .unwrap(); assert_eq!(result.imported, 1); assert!(result.failed.is_empty()); @@ -390,16 +340,9 @@ async fn reports_invalid_row_result_errors() { review_logger: Arc::new(NoopReviewLogger), }; - let result = execute::execute( - &deps, - ExecuteImportCommand { - user_id: uid, - session_id: sid.value(), - confirmed_indices: vec![0], - }, - ) - .await - .unwrap(); + let result = execute::execute(&deps, execute_import_command(uid, sid.value())) + .await + .unwrap(); assert_eq!(result.imported, 0); assert_eq!(result.failed.len(), 1); @@ -433,16 +376,9 @@ async fn fails_on_missing_rating() { review_logger: Arc::new(NoopReviewLogger), }; - let result = execute::execute( - &deps, - ExecuteImportCommand { - user_id: uid, - session_id: sid.value(), - confirmed_indices: vec![0], - }, - ) - .await - .unwrap(); + let result = execute::execute(&deps, execute_import_command(uid, sid.value())) + .await + .unwrap(); assert_eq!(result.imported, 0); assert_eq!(result.failed.len(), 1); @@ -475,16 +411,9 @@ async fn fails_on_unparseable_date() { review_logger: Arc::new(NoopReviewLogger), }; - let result = execute::execute( - &deps, - ExecuteImportCommand { - user_id: uid, - session_id: sid.value(), - confirmed_indices: vec![0], - }, - ) - .await - .unwrap(); + let result = execute::execute(&deps, execute_import_command(uid, sid.value())) + .await + .unwrap(); assert_eq!(result.imported, 0); assert_eq!(result.failed.len(), 1); @@ -517,16 +446,9 @@ async fn imports_row_without_release_year() { review_logger: Arc::new(NoopReviewLogger), }; - let result = execute::execute( - &deps, - ExecuteImportCommand { - user_id: uid, - session_id: sid.value(), - confirmed_indices: vec![0], - }, - ) - .await - .unwrap(); + let result = execute::execute(&deps, execute_import_command(uid, sid.value())) + .await + .unwrap(); assert_eq!(result.imported, 1); assert!(result.failed.is_empty()); @@ -547,16 +469,9 @@ async fn deletes_session_after_import() { review_logger: Arc::new(NoopReviewLogger), }; - execute::execute( - &deps, - ExecuteImportCommand { - user_id: uid, - session_id: sid.value(), - confirmed_indices: vec![0], - }, - ) - .await - .unwrap(); + execute::execute(&deps, execute_import_command(uid, sid.value())) + .await + .unwrap(); assert_eq!( sessions.count(), @@ -599,9 +514,8 @@ async fn imports_more_rows_than_concurrency_limit() { let result = execute::execute( &deps, ExecuteImportCommand { - user_id: uid, - session_id: sid.value(), confirmed_indices, + ..execute_import_command(uid, sid.value()) }, ) .await diff --git a/crates/application/src/integrations/tests/confirm.rs b/crates/application/src/integrations/tests/confirm.rs index ce18f3b5..9c7f3a99 100644 --- a/crates/application/src/integrations/tests/confirm.rs +++ b/crates/application/src/integrations/tests/confirm.rs @@ -1,3 +1,4 @@ +use crate::test_support::confirm_watch_events_command; use std::sync::Arc; use domain::models::{WatchEvent, WatchEventSource}; @@ -45,14 +46,7 @@ async fn confirms_watch_event_via_review_logger() { let result = confirm::execute( &deps(&watch_events, noop_logger()), - ConfirmWatchEventsCommand { - user_id: uid, - confirmations: vec![WatchEventConfirmation { - watch_event_id: event_id, - rating: 4, - comment: None, - }], - }, + confirm_watch_events_command(uid, event_id, 4), ) .await .unwrap(); @@ -131,14 +125,7 @@ async fn rejects_other_users_event() { let result = confirm::execute( &deps(&watch_events, noop_logger()), - ConfirmWatchEventsCommand { - user_id: intruder, - confirmations: vec![WatchEventConfirmation { - watch_event_id: event_id, - rating: 3, - comment: None, - }], - }, + confirm_watch_events_command(intruder, event_id, 3), ) .await; @@ -151,14 +138,7 @@ async fn fails_when_event_not_found() { let result = confirm::execute( &deps(&watch_events, noop_logger()), - ConfirmWatchEventsCommand { - user_id: Uuid::new_v4(), - confirmations: vec![WatchEventConfirmation { - watch_event_id: Uuid::new_v4(), - rating: 4, - comment: None, - }], - }, + confirm_watch_events_command(Uuid::new_v4(), Uuid::new_v4(), 4), ) .await; @@ -211,14 +191,7 @@ async fn confirms_event_with_movie_id() { let result = confirm::execute( &deps(&watch_events, review_logger), - ConfirmWatchEventsCommand { - user_id: uid, - confirmations: vec![WatchEventConfirmation { - watch_event_id: event_id, - rating: 4, - comment: None, - }], - }, + confirm_watch_events_command(uid, event_id, 4), ) .await .unwrap(); @@ -293,7 +266,6 @@ async fn confirms_multiple_events() { let result = confirm::execute( &deps(&watch_events, noop_logger()), ConfirmWatchEventsCommand { - user_id: uid, confirmations: vec![ WatchEventConfirmation { watch_event_id: id1, @@ -306,6 +278,7 @@ async fn confirms_multiple_events() { comment: None, }, ], + ..confirm_watch_events_command(uid, id1, 3) }, ) .await @@ -333,14 +306,7 @@ async fn confirms_event_without_year() { let result = confirm::execute( &deps(&watch_events, noop_logger()), - ConfirmWatchEventsCommand { - user_id: uid, - confirmations: vec![WatchEventConfirmation { - watch_event_id: event_id, - rating: 3, - comment: None, - }], - }, + confirm_watch_events_command(uid, event_id, 3), ) .await .unwrap(); diff --git a/crates/application/src/integrations/tests/dismiss.rs b/crates/application/src/integrations/tests/dismiss.rs index 3db9f356..e764efc6 100644 --- a/crates/application/src/integrations/tests/dismiss.rs +++ b/crates/application/src/integrations/tests/dismiss.rs @@ -1,3 +1,4 @@ +use crate::test_support::dismiss_watch_events_command; use std::sync::Arc; use domain::models::{WatchEvent, WatchEventSource}; @@ -7,7 +8,7 @@ use domain::value_objects::UserId; use uuid::Uuid; use crate::integrations::deps::DismissWatchEventsDeps; -use crate::integrations::{commands::DismissWatchEventsCommand, dismiss}; +use crate::integrations::dismiss; fn deps(watch_events: &Arc) -> DismissWatchEventsDeps { DismissWatchEventsDeps { @@ -22,10 +23,7 @@ async fn dismisses_empty_list_returns_zero() { let result = dismiss::execute( &deps(&events), - DismissWatchEventsCommand { - user_id: Uuid::new_v4(), - event_ids: vec![], - }, + dismiss_watch_events_command(Uuid::new_v4(), vec![]), ) .await .unwrap(); @@ -39,10 +37,7 @@ async fn fails_when_event_not_found() { let result = dismiss::execute( &deps(&events), - DismissWatchEventsCommand { - user_id: Uuid::new_v4(), - event_ids: vec![Uuid::new_v4()], - }, + dismiss_watch_events_command(Uuid::new_v4(), vec![Uuid::new_v4()]), ) .await; @@ -80,10 +75,7 @@ async fn dismisses_existing_events() { let result = dismiss::execute( &deps(&watch_events), - DismissWatchEventsCommand { - user_id: uid, - event_ids: vec![id1, id2], - }, + dismiss_watch_events_command(uid, vec![id1, id2]), ) .await .unwrap(); diff --git a/crates/application/src/integrations/tests/generate_token.rs b/crates/application/src/integrations/tests/generate_token.rs index 2b038efc..31227c5e 100644 --- a/crates/application/src/integrations/tests/generate_token.rs +++ b/crates/application/src/integrations/tests/generate_token.rs @@ -1,12 +1,12 @@ +use crate::test_support::generate_webhook_token_command; use std::sync::Arc; -use domain::models::WatchEventSource; use domain::ports::WebhookTokenRepository; use domain::testing::InMemoryWebhookTokenRepository; use uuid::Uuid; use crate::integrations::deps::GenerateWebhookTokenDeps; -use crate::integrations::{commands::GenerateWebhookTokenCommand, generate_token}; +use crate::integrations::generate_token; #[tokio::test] async fn generates_token_and_saves() { @@ -17,11 +17,7 @@ async fn generates_token_and_saves() { &GenerateWebhookTokenDeps { webhook_token: Arc::clone(&tokens), }, - GenerateWebhookTokenCommand { - user_id, - provider: WatchEventSource::Jellyfin, - label: None, - }, + generate_webhook_token_command(user_id), ) .await .unwrap(); diff --git a/crates/application/src/integrations/tests/get_tokens.rs b/crates/application/src/integrations/tests/get_tokens.rs index 43474440..8577eb87 100644 --- a/crates/application/src/integrations/tests/get_tokens.rs +++ b/crates/application/src/integrations/tests/get_tokens.rs @@ -1,3 +1,4 @@ +use crate::test_support::generate_webhook_token_command; use std::sync::Arc; use domain::models::WatchEventSource; @@ -47,11 +48,7 @@ async fn returns_tokens_after_generate() { generate_token::execute( &generate_deps(&tokens), - GenerateWebhookTokenCommand { - user_id, - provider: WatchEventSource::Jellyfin, - label: None, - }, + generate_webhook_token_command(user_id), ) .await .unwrap(); @@ -59,9 +56,9 @@ async fn returns_tokens_after_generate() { generate_token::execute( &generate_deps(&tokens), GenerateWebhookTokenCommand { - user_id, provider: WatchEventSource::Plex, label: Some("living room".into()), + ..generate_webhook_token_command(user_id) }, ) .await diff --git a/crates/application/src/integrations/tests/ingest.rs b/crates/application/src/integrations/tests/ingest.rs index 04099b92..b5d5ed47 100644 --- a/crates/application/src/integrations/tests/ingest.rs +++ b/crates/application/src/integrations/tests/ingest.rs @@ -1,3 +1,4 @@ +use crate::test_support::generate_webhook_token_command; use std::sync::Arc; use domain::models::WatchEventSource; @@ -7,7 +8,7 @@ use domain::testing::{ }; use uuid::Uuid; -use crate::integrations::commands::{GenerateWebhookTokenCommand, IngestWatchEventCommand}; +use crate::integrations::commands::IngestWatchEventCommand; use crate::integrations::deps::{GenerateWebhookTokenDeps, IngestWatchEventDeps}; use crate::integrations::{generate_token, ingest}; @@ -38,11 +39,7 @@ async fn ingests_watch_event() { &GenerateWebhookTokenDeps { webhook_token: Arc::clone(&tokens), }, - GenerateWebhookTokenCommand { - user_id, - provider: WatchEventSource::Jellyfin, - label: None, - }, + generate_webhook_token_command(user_id), ) .await .unwrap(); diff --git a/crates/application/src/integrations/tests/revoke_token.rs b/crates/application/src/integrations/tests/revoke_token.rs index a89d701f..bc7dac40 100644 --- a/crates/application/src/integrations/tests/revoke_token.rs +++ b/crates/application/src/integrations/tests/revoke_token.rs @@ -1,6 +1,6 @@ +use crate::test_support::generate_webhook_token_command; use std::sync::Arc; -use domain::models::WatchEventSource; use domain::ports::WebhookTokenRepository; use domain::testing::InMemoryWebhookTokenRepository; use uuid::Uuid; @@ -9,10 +9,8 @@ use crate::integrations::deps::{ GenerateWebhookTokenDeps, GetWebhookTokensDeps, RevokeWebhookTokenDeps, }; use crate::integrations::{ - commands::{GenerateWebhookTokenCommand, RevokeWebhookTokenCommand}, - generate_token, get_tokens, - queries::GetWebhookTokensQuery, - revoke_token, + commands::RevokeWebhookTokenCommand, generate_token, get_tokens, + queries::GetWebhookTokensQuery, revoke_token, }; #[tokio::test] @@ -25,11 +23,7 @@ async fn revokes_existing_token() { &GenerateWebhookTokenDeps { webhook_token: Arc::clone(&tokens), }, - GenerateWebhookTokenCommand { - user_id, - provider: WatchEventSource::Jellyfin, - label: None, - }, + generate_webhook_token_command(user_id), ) .await .unwrap(); diff --git a/crates/application/src/lib.rs b/crates/application/src/lib.rs index 76345c55..d47b1b82 100644 --- a/crates/application/src/lib.rs +++ b/crates/application/src/lib.rs @@ -22,6 +22,9 @@ pub mod wrapup; #[cfg(test)] pub mod test_helpers; +#[cfg(test)] +mod test_support; + #[cfg(test)] #[path = "tests/services.rs"] mod services_tests; diff --git a/crates/application/src/movies/tests/enrich_movie.rs b/crates/application/src/movies/tests/enrich_movie.rs index 90982582..8fb3a21f 100644 --- a/crates/application/src/movies/tests/enrich_movie.rs +++ b/crates/application/src/movies/tests/enrich_movie.rs @@ -1,14 +1,15 @@ +use domain::testing::test_movie_with_year; use std::sync::Arc; use chrono::Utc; use domain::{ - models::{Movie, MovieProfile}, + models::MovieProfile, ports::MovieCommand, testing::{ FakeSearchCommand, InMemoryMovieProfileRepository, InMemoryMovieRepository, PanicPersonCommand, }, - value_objects::{MovieId, MovieTitle, ReleaseYear}, + value_objects::MovieId, }; use crate::movies::{commands::EnrichMovieCommand, deps::EnrichMovieDeps, enrich_movie}; @@ -19,13 +20,7 @@ async fn stores_profile_and_indexes() { let profile_repo = InMemoryMovieProfileRepository::new(); // PanicPersonCommand is safe here — empty cast/crew means upsert_batch is never called - let movie = Movie::new( - None, - MovieTitle::new("Test".into()).unwrap(), - ReleaseYear::new(2024).unwrap(), - None, - None, - ); + let movie = test_movie_with_year("Test", 2024); let movie_id = MovieId::from_uuid(movie.id().value()); movie_repo.upsert_movie(&movie).await.unwrap(); @@ -99,13 +94,7 @@ async fn extracts_and_indexes_persons() { let movie_repo = InMemoryMovieRepository::new(); let profile_repo = InMemoryMovieProfileRepository::new(); - let movie = Movie::new( - None, - MovieTitle::new("Cast Movie".into()).unwrap(), - ReleaseYear::new(2024).unwrap(), - None, - None, - ); + let movie = test_movie_with_year("Cast Movie", 2024); let movie_id = MovieId::from_uuid(movie.id().value()); movie_repo.upsert_movie(&movie).await.unwrap(); diff --git a/crates/application/src/movies/tests/sync_poster.rs b/crates/application/src/movies/tests/sync_poster.rs index 97245209..40604afd 100644 --- a/crates/application/src/movies/tests/sync_poster.rs +++ b/crates/application/src/movies/tests/sync_poster.rs @@ -1,3 +1,5 @@ +use crate::test_support::sync_poster_command; +use domain::testing::{test_movie_with_external_id, test_movie_with_year}; use std::sync::Arc; use uuid::Uuid; @@ -10,13 +12,10 @@ use domain::{ FakeSearchCommand, InMemoryMovieProfileRepository, InMemoryMovieRepository, NoopEventPublisher, NoopObjectStorage, }, - value_objects::{ExternalMetadataId, MovieTitle, PosterUrl, ReleaseYear}, + value_objects::{ExternalMetadataId, PosterUrl}, }; -use crate::{ - diary::commands::SyncPosterCommand, - movies::{deps::SyncPosterDeps, sync_poster}, -}; +use crate::movies::{deps::SyncPosterDeps, sync_poster}; fn default_deps() -> SyncPosterDeps { SyncPosterDeps { @@ -35,13 +34,7 @@ fn default_deps() -> SyncPosterDeps { async fn fails_when_movie_not_found() { let deps = default_deps(); - let result = sync_poster::execute( - &deps, - SyncPosterCommand { - movie_id: Uuid::new_v4(), - }, - ) - .await; + let result = sync_poster::execute(&deps, sync_poster_command(Uuid::new_v4())).await; assert!(result.is_err()); } @@ -49,13 +42,7 @@ async fn fails_when_movie_not_found() { #[tokio::test] async fn fails_when_no_external_id() { let movies = InMemoryMovieRepository::new(); - let movie = Movie::new( - None, - MovieTitle::new("Test".into()).unwrap(), - ReleaseYear::new(2024).unwrap(), - None, - None, - ); + let movie = test_movie_with_year("Test", 2024); let movie_id = movie.id().value(); movies.upsert_movie(&movie).await.unwrap(); @@ -65,7 +52,7 @@ async fn fails_when_no_external_id() { ..default_deps() }; - let result = sync_poster::execute(&deps, SyncPosterCommand { movie_id }).await; + let result = sync_poster::execute(&deps, sync_poster_command(movie_id)).await; assert!(result.is_err()); } @@ -94,13 +81,7 @@ impl MetadataClient for FakeMetaWithPoster { async fn syncs_poster_for_movie_with_external_id() { let movies = InMemoryMovieRepository::new(); let ext_id = ExternalMetadataId::new("tmdb:999".into()).unwrap(); - let movie = Movie::new( - Some(ext_id), - MovieTitle::new("Poster Movie".into()).unwrap(), - ReleaseYear::new(2024).unwrap(), - None, - None, - ); + let movie = test_movie_with_external_id("Poster Movie", 2024, ext_id); let movie_id = movie.id().value(); movies.upsert_movie(&movie).await.unwrap(); @@ -111,7 +92,7 @@ async fn syncs_poster_for_movie_with_external_id() { ..default_deps() }; - sync_poster::execute(&deps, SyncPosterCommand { movie_id }) + sync_poster::execute(&deps, sync_poster_command(movie_id)) .await .unwrap(); diff --git a/crates/application/src/social/tests/local_service.rs b/crates/application/src/social/tests/local_service.rs index 53615d81..0acc875e 100644 --- a/crates/application/src/social/tests/local_service.rs +++ b/crates/application/src/social/tests/local_service.rs @@ -1,16 +1,16 @@ +use domain::testing::test_user; use std::sync::{Arc, Mutex}; use async_trait::async_trait; use domain::{ errors::DomainError, - models::{User, UserRole}, ports::{ BlockQuery, FollowCommand, FollowGraphQuery, FollowQuery, SocialCommand, UserRepository, }, testing::InMemoryUserRepository, value_objects::{ - Email, FollowRelation, FollowStatus, FollowTarget, InstanceIdentity, PasswordHash, - SocialActor, SocialIdentity, UserId, Username, + FollowRelation, FollowStatus, FollowTarget, InstanceIdentity, SocialActor, SocialIdentity, + UserId, }, }; use uuid::Uuid; @@ -247,12 +247,7 @@ fn instance() -> InstanceIdentity { } async fn register_user(repo: &Arc, username: &str) -> UserId { - let 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, - ); + let user = test_user(username); let id = user.id().clone(); repo.save(&user).await.unwrap(); id diff --git a/crates/application/src/test_support.rs b/crates/application/src/test_support.rs new file mode 100644 index 00000000..ab4862fb --- /dev/null +++ b/crates/application/src/test_support.rs @@ -0,0 +1,240 @@ +//! Command builders for tests. Each builder returns a command with sensible +//! defaults; tests override individual fields with struct-update syntax: +//! `LogReviewCommand { rating: 6, ..log_review_command(user, movie) }`. +//! Moves to `tests/support/mod.rs` when application tests become integration tests. + +use chrono::{NaiveDate, NaiveDateTime}; +use uuid::Uuid; + +use domain::models::{FileFormat, UserRole, WatchEventSource}; + +use crate::auth::commands::RegisterCommand; +use crate::auth::queries::LoginCommand; +use crate::diary::commands::{EditReviewCommand, LogReviewCommand, MovieInput, SyncPosterCommand}; +use crate::goals::commands::{CreateGoalCommand, UpdateGoalCommand}; +use crate::import::commands::{ + ApplyImportMappingCommand, ApplyImportProfileCommand, CreateImportSessionCommand, + ExecuteImportCommand, +}; +use crate::integrations::commands::{ + ConfirmWatchEventsCommand, DismissWatchEventsCommand, GenerateWebhookTokenCommand, + WatchEventConfirmation, +}; +use crate::users::commands::UpdateProfileCommand; +use crate::users::update_settings::UpdateUserSettingsCommand; +use crate::watchlist::commands::AddToWatchlistCommand; +use crate::wrapup::commands::RequestWrapUpCommand; + +fn watched_at() -> NaiveDateTime { + NaiveDate::from_ymd_opt(2024, 6, 15) + .unwrap() + .and_hms_opt(20, 0, 0) + .unwrap() +} + +pub(crate) fn movie_input_by_id(movie_id: Uuid) -> MovieInput { + MovieInput { + movie_id: Some(movie_id), + external_metadata_id: None, + manual_title: None, + manual_release_year: None, + manual_director: None, + } +} + +pub(crate) fn movie_input_manual(title: &str, year: u16) -> MovieInput { + MovieInput { + movie_id: None, + external_metadata_id: None, + manual_title: Some(title.to_string()), + manual_release_year: Some(year), + manual_director: None, + } +} + +pub(crate) fn log_review_command(user_id: Uuid, movie_id: Uuid) -> LogReviewCommand { + LogReviewCommand { + user_id, + input: movie_input_by_id(movie_id), + rating: 4, + comment: None, + watched_at: watched_at(), + watch_medium: None, + spoilers: false, + } +} + +pub(crate) fn log_review_command_manual(user_id: Uuid, title: &str, year: u16) -> LogReviewCommand { + LogReviewCommand { + input: movie_input_manual(title, year), + ..log_review_command(user_id, Uuid::nil()) + } +} + +/// Edits only the rating; `review_id` and `requesting_user_id` are random. +pub(crate) fn edit_review_command(rating: u8) -> EditReviewCommand { + EditReviewCommand { + review_id: Uuid::new_v4(), + requesting_user_id: Uuid::new_v4(), + rating: Some(rating), + comment: None, + watched_at: None, + watch_medium: None, + spoilers: None, + } +} + +pub(crate) fn sync_poster_command(movie_id: Uuid) -> SyncPosterCommand { + SyncPosterCommand { movie_id } +} + +pub(crate) fn register_command( + email: impl Into, + username: impl Into, +) -> RegisterCommand { + RegisterCommand { + email: email.into(), + username: username.into(), + password: "password123".to_string(), + role: UserRole::Standard, + } +} + +pub(crate) fn login_command(email: impl Into, password: impl Into) -> LoginCommand { + LoginCommand { + email: email.into(), + password: password.into(), + } +} + +pub(crate) fn create_goal_command(year: u16, target_count: u32) -> CreateGoalCommand { + CreateGoalCommand { + user_id: Uuid::nil(), + year, + target_count, + } +} + +pub(crate) fn update_goal_command(year: u16, target_count: u32) -> UpdateGoalCommand { + UpdateGoalCommand { + user_id: Uuid::nil(), + year, + target_count, + } +} + +/// Confirms indices `[0]`. +pub(crate) fn execute_import_command(user_id: Uuid, session_id: Uuid) -> ExecuteImportCommand { + ExecuteImportCommand { + user_id, + session_id, + confirmed_indices: vec![0], + } +} + +pub(crate) fn create_import_session_command( + user_id: Uuid, + csv: &[u8], +) -> CreateImportSessionCommand { + CreateImportSessionCommand { + user_id, + bytes: csv.to_vec(), + format: FileFormat::Csv, + } +} + +/// No field mappings. +pub(crate) fn apply_import_mapping_command( + user_id: Uuid, + session_id: Uuid, +) -> ApplyImportMappingCommand { + ApplyImportMappingCommand { + user_id, + session_id, + mappings: vec![], + } +} + +pub(crate) fn apply_import_profile_command( + user_id: Uuid, + session_id: Uuid, + profile_id: Uuid, +) -> ApplyImportProfileCommand { + ApplyImportProfileCommand { + user_id, + session_id, + profile_id, + } +} + +/// One confirmation, rating as given, no comment. +pub(crate) fn confirm_watch_events_command( + user_id: Uuid, + watch_event_id: Uuid, + rating: u8, +) -> ConfirmWatchEventsCommand { + ConfirmWatchEventsCommand { + user_id, + confirmations: vec![WatchEventConfirmation { + watch_event_id, + rating, + comment: None, + }], + } +} + +pub(crate) fn dismiss_watch_events_command( + user_id: Uuid, + event_ids: Vec, +) -> DismissWatchEventsCommand { + DismissWatchEventsCommand { user_id, event_ids } +} + +/// Jellyfin provider, no label. +pub(crate) fn generate_webhook_token_command(user_id: Uuid) -> GenerateWebhookTokenCommand { + GenerateWebhookTokenCommand { + user_id, + provider: WatchEventSource::Jellyfin, + label: None, + } +} + +/// Every field unset; override what the test cares about. +pub(crate) fn update_profile_command(user_id: Uuid) -> UpdateProfileCommand { + UpdateProfileCommand { + user_id, + display_name: None, + bio: None, + avatar_bytes: None, + avatar_content_type: None, + banner_bytes: None, + banner_content_type: None, + also_known_as: None, + } +} + +/// All federation toggles on. +pub(crate) fn update_user_settings_command(user_id: Uuid) -> UpdateUserSettingsCommand { + UpdateUserSettingsCommand { + user_id, + federate_goals: true, + federate_reviews: true, + federate_watchlist: true, + } +} + +pub(crate) fn add_to_watchlist_command(user_id: Uuid, movie_id: Uuid) -> AddToWatchlistCommand { + AddToWatchlistCommand { + user_id, + input: movie_input_by_id(movie_id), + } +} + +/// Window is 1 Jan of `year` to 1 Jan of `year + 1`. +pub(crate) fn request_wrap_up_command(user_id: Option, year: i32) -> RequestWrapUpCommand { + RequestWrapUpCommand { + user_id, + start_date: NaiveDate::from_ymd_opt(year, 1, 1).unwrap(), + end_date: NaiveDate::from_ymd_opt(year + 1, 1, 1).unwrap(), + } +} diff --git a/crates/application/src/users/tests/authorize_admin.rs b/crates/application/src/users/tests/authorize_admin.rs index 7e1810b9..aebf71cb 100644 --- a/crates/application/src/users/tests/authorize_admin.rs +++ b/crates/application/src/users/tests/authorize_admin.rs @@ -1,3 +1,4 @@ +use crate::test_support::register_command; use std::sync::Arc; use domain::models::UserRole; @@ -23,10 +24,8 @@ async fn register_user( register::execute( ®_deps, RegisterCommand { - email: email.into(), - username: username.into(), - password: "password123".into(), role, + ..register_command(email, username) }, ) .await diff --git a/crates/application/src/users/tests/get_current_profile.rs b/crates/application/src/users/tests/get_current_profile.rs index 7aa383cb..3f7b0f5b 100644 --- a/crates/application/src/users/tests/get_current_profile.rs +++ b/crates/application/src/users/tests/get_current_profile.rs @@ -1,3 +1,4 @@ +use crate::test_support::register_command; use std::sync::Arc; use domain::models::{ProfileField, User, UserProfile, UserRole}; @@ -7,7 +8,7 @@ use domain::value_objects::{Email, PasswordHash, UserId, Username}; use uuid::Uuid; use crate::{ - auth::{commands::RegisterCommand, deps::RegisterDeps, register}, + auth::{deps::RegisterDeps, register}, test_helpers::TestContextBuilder, users::{deps::GetCurrentProfileDeps, get_current_profile, queries::GetCurrentProfileQuery}, }; @@ -23,17 +24,9 @@ async fn returns_profile_for_existing_user() { config: b.config.clone(), }; - register::execute( - ®_deps, - RegisterCommand { - email: "alice@example.com".into(), - username: "alice".into(), - password: "password123".into(), - role: UserRole::Standard, - }, - ) - .await - .unwrap(); + register::execute(®_deps, register_command("alice@example.com", "alice")) + .await + .unwrap(); let user = users .find_by_email(&domain::value_objects::Email::new("alice@example.com".into()).unwrap()) diff --git a/crates/application/src/users/tests/get_local_profile.rs b/crates/application/src/users/tests/get_local_profile.rs index 27e4b9b2..6892046f 100644 --- a/crates/application/src/users/tests/get_local_profile.rs +++ b/crates/application/src/users/tests/get_local_profile.rs @@ -1,8 +1,7 @@ +use crate::test_support::{register_command, update_profile_command}; use domain::errors::DomainError; -use domain::models::UserRole; use domain::value_objects::Email; -use crate::auth::commands::RegisterCommand; use crate::auth::deps::RegisterDeps; use crate::auth::register; use crate::test_helpers::TestContextBuilder; @@ -18,17 +17,9 @@ async fn setup_user(b: &TestContextBuilder, email: &str, username: &str) { password_hasher: b.password_hasher.clone(), config: b.config.clone(), }; - register::execute( - &deps, - RegisterCommand { - email: email.into(), - username: username.into(), - password: "password123".into(), - role: UserRole::Standard, - }, - ) - .await - .unwrap(); + register::execute(&deps, register_command(email, username)) + .await + .unwrap(); } fn deps(b: &TestContextBuilder) -> GetLocalProfileDeps { @@ -276,14 +267,9 @@ async fn populates_handle_and_actor_url_for_a_local_user() { update_profile::execute( &update_deps, UpdateProfileCommand { - user_id: uid, display_name: Some("Gabriel K".into()), bio: Some("Movies and diaries.".into()), - avatar_bytes: None, - avatar_content_type: None, - banner_bytes: None, - banner_content_type: None, - also_known_as: None, + ..update_profile_command(uid) }, ) .await diff --git a/crates/application/src/users/tests/get_page_viewer.rs b/crates/application/src/users/tests/get_page_viewer.rs index 7667b4b0..788f5d0e 100644 --- a/crates/application/src/users/tests/get_page_viewer.rs +++ b/crates/application/src/users/tests/get_page_viewer.rs @@ -1,7 +1,6 @@ -use domain::models::UserRole; +use crate::test_support::register_command; use domain::value_objects::{Email, FollowTarget, SocialIdentity, UserId}; -use crate::auth::commands::RegisterCommand; use crate::auth::deps::RegisterDeps; use crate::auth::register; use crate::test_helpers::TestContextBuilder; @@ -14,17 +13,9 @@ async fn setup_user(b: &TestContextBuilder, email: &str, username: &str) -> uuid password_hasher: b.password_hasher.clone(), config: b.config.clone(), }; - register::execute( - &deps, - RegisterCommand { - email: email.into(), - username: username.into(), - password: "password123".into(), - role: UserRole::Standard, - }, - ) - .await - .unwrap(); + register::execute(&deps, register_command(email, username)) + .await + .unwrap(); let user = b .user_repo .find_by_email(&Email::new(email.into()).unwrap()) diff --git a/crates/application/src/users/tests/get_profile_settings.rs b/crates/application/src/users/tests/get_profile_settings.rs index a5c3a98b..defb2a06 100644 --- a/crates/application/src/users/tests/get_profile_settings.rs +++ b/crates/application/src/users/tests/get_profile_settings.rs @@ -1,7 +1,7 @@ -use domain::models::{ProfileField, UserRole}; +use crate::test_support::register_command; +use domain::models::ProfileField; use domain::value_objects::{Email, UserId}; -use crate::auth::commands::RegisterCommand; use crate::auth::deps::RegisterDeps; use crate::auth::register; use crate::test_helpers::TestContextBuilder; @@ -18,12 +18,7 @@ async fn get_profile_settings_includes_profile_fields() { }; register::execute( ®ister_deps, - RegisterCommand { - email: "settings@test.com".into(), - username: "settingsuser".into(), - password: "password123".into(), - role: UserRole::Standard, - }, + register_command("settings@test.com", "settingsuser"), ) .await .unwrap(); diff --git a/crates/application/src/users/tests/update_profile.rs b/crates/application/src/users/tests/update_profile.rs index 157171e3..1145c77d 100644 --- a/crates/application/src/users/tests/update_profile.rs +++ b/crates/application/src/users/tests/update_profile.rs @@ -1,13 +1,13 @@ +use crate::test_support::{register_command, update_profile_command}; use std::sync::Arc; use domain::events::DomainEvent; -use domain::models::UserRole; use domain::ports::UserRepository; use domain::testing::{InMemoryUserRepository, NoopEventPublisher}; use uuid::Uuid; use crate::{ - auth::{commands::RegisterCommand, deps::RegisterDeps, register}, + auth::{deps::RegisterDeps, register}, test_helpers::TestContextBuilder, users::{commands::UpdateProfileCommand, deps::UpdateProfileDeps, update_profile}, }; @@ -18,17 +18,9 @@ async fn register_user(b: &TestContextBuilder, users: &Arc, @@ -30,28 +27,13 @@ async fn test_add_to_watchlist_resolves_and_saves() { let movies = InMemoryMovieRepository::new(); let watchlist = InMemoryWatchlistRepository::new(); - let movie = Movie::new( - None, - MovieTitle::new("The Thing".into()).unwrap(), - ReleaseYear::new(1982).unwrap(), - None, - None, - ); + let movie = test_movie_with_year("The Thing", 1982); let movie_uuid = movie.id().value(); movies.upsert_movie(&movie).await.unwrap(); let deps = make_deps(Arc::clone(&movies), Arc::clone(&watchlist)); - let cmd = AddToWatchlistCommand { - user_id: uuid::Uuid::new_v4(), - input: MovieInput { - movie_id: Some(movie_uuid), - external_metadata_id: None, - manual_title: None, - manual_release_year: None, - manual_director: None, - }, - }; + let cmd = add_to_watchlist_command(uuid::Uuid::new_v4(), movie_uuid); add::execute(&deps, cmd).await.unwrap(); @@ -63,29 +45,14 @@ async fn test_add_to_watchlist_already_present_is_idempotent() { let movies = InMemoryMovieRepository::new(); let watchlist = InMemoryWatchlistRepository::new(); - let movie = Movie::new( - None, - MovieTitle::new("RoboCop".into()).unwrap(), - ReleaseYear::new(1987).unwrap(), - None, - None, - ); + let movie = test_movie_with_year("RoboCop", 1987); let movie_uuid = movie.id().value(); let user_id = uuid::Uuid::new_v4(); movies.upsert_movie(&movie).await.unwrap(); let deps = make_deps(Arc::clone(&movies), Arc::clone(&watchlist)); - let make_cmd = || AddToWatchlistCommand { - user_id, - input: MovieInput { - movie_id: Some(movie_uuid), - external_metadata_id: None, - manual_title: None, - manual_release_year: None, - manual_director: None, - }, - }; + let make_cmd = || add_to_watchlist_command(user_id, movie_uuid); add::execute(&deps, make_cmd()).await.unwrap(); add::execute(&deps, make_cmd()).await.unwrap(); @@ -102,13 +69,7 @@ async fn test_add_to_watchlist_with_manual_movie() { let cmd = AddToWatchlistCommand { user_id: uuid::Uuid::new_v4(), - input: MovieInput { - movie_id: None, - external_metadata_id: None, - manual_title: Some("New Manual Movie".into()), - manual_release_year: Some(2024), - manual_director: None, - }, + input: movie_input_manual("New Manual Movie", 2024), }; add::execute(&deps, cmd).await.unwrap(); @@ -124,16 +85,7 @@ async fn test_add_to_watchlist_movie_not_found_by_id() { let deps = make_deps(Arc::clone(&movies), Arc::clone(&watchlist)); - let cmd = AddToWatchlistCommand { - user_id: uuid::Uuid::new_v4(), - input: MovieInput { - movie_id: Some(uuid::Uuid::new_v4()), - external_metadata_id: None, - manual_title: None, - manual_release_year: None, - manual_director: None, - }, - }; + let cmd = add_to_watchlist_command(uuid::Uuid::new_v4(), uuid::Uuid::new_v4()); assert!(add::execute(&deps, cmd).await.is_err()); } diff --git a/crates/application/src/watchlist/tests/get_watchlist_for_owner.rs b/crates/application/src/watchlist/tests/get_watchlist_for_owner.rs index 72527542..e9788ba3 100644 --- a/crates/application/src/watchlist/tests/get_watchlist_for_owner.rs +++ b/crates/application/src/watchlist/tests/get_watchlist_for_owner.rs @@ -1,15 +1,14 @@ +use crate::test_support::register_command; use std::sync::Arc; use uuid::Uuid; -use domain::models::UserRole; use domain::testing::{ InMemoryRemoteWatchlistRepository, InMemoryUserRepository, InMemoryWatchlistRepository, PanicRemoteWatchlistRepository, PanicWatchlistRepository, }; use domain::value_objects::Email; -use crate::auth::commands::RegisterCommand; use crate::auth::deps::RegisterDeps; use crate::auth::register; use crate::test_helpers::TestContextBuilder; @@ -22,17 +21,9 @@ async fn setup_user(b: &TestContextBuilder, email: &str, username: &str) { password_hasher: b.password_hasher.clone(), config: b.config.clone(), }; - register::execute( - &deps, - RegisterCommand { - email: email.into(), - username: username.into(), - password: "password123".into(), - role: UserRole::Standard, - }, - ) - .await - .unwrap(); + register::execute(&deps, register_command(email, username)) + .await + .unwrap(); } #[tokio::test] diff --git a/crates/application/src/wrapup/tests/generate.rs b/crates/application/src/wrapup/tests/generate.rs index 1212d49a..fceec0bf 100644 --- a/crates/application/src/wrapup/tests/generate.rs +++ b/crates/application/src/wrapup/tests/generate.rs @@ -1,3 +1,4 @@ +use crate::test_support::request_wrap_up_command; use chrono::NaiveDate; use domain::events::DomainEvent; use domain::models::wrapup::{WrapUpRecord, WrapUpStatus}; @@ -21,11 +22,7 @@ fn deps( } fn past_cmd() -> RequestWrapUpCommand { - RequestWrapUpCommand { - user_id: Some(Uuid::nil()), - start_date: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(), - end_date: NaiveDate::from_ymd_opt(2025, 1, 1).unwrap(), - } + request_wrap_up_command(Some(Uuid::nil()), 2024) } #[tokio::test] @@ -105,16 +102,9 @@ async fn replaces_failed_wrapup() { async fn rejects_future_end_date() { let repo = InMemoryWrapUpRepository::new(); let events = NoopEventPublisher::new(); - let err = generate::execute( - &deps(&repo, &events), - RequestWrapUpCommand { - user_id: None, - start_date: NaiveDate::from_ymd_opt(2030, 1, 1).unwrap(), - end_date: NaiveDate::from_ymd_opt(2031, 1, 1).unwrap(), - }, - ) - .await - .unwrap_err(); + let err = generate::execute(&deps(&repo, &events), request_wrap_up_command(None, 2030)) + .await + .unwrap_err(); assert!(err.to_string().contains("future")); } diff --git a/crates/domain/src/lib.rs b/crates/domain/src/lib.rs index 30bb5eb8..42b7ebc1 100644 --- a/crates/domain/src/lib.rs +++ b/crates/domain/src/lib.rs @@ -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; diff --git a/crates/domain/src/models/tests/goal.rs b/crates/domain/src/models/tests/goal.rs index c3575b42..41c2c2b9 100644 --- a/crates/domain/src/models/tests/goal.rs +++ b/crates/domain/src/models/tests/goal.rs @@ -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::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, diff --git a/crates/domain/src/models/tests/mod.rs b/crates/domain/src/models/tests/mod.rs index 57e89c80..f8eb4b7e 100644 --- a/crates/domain/src/models/tests/mod.rs +++ b/crates/domain/src/models/tests/mod.rs @@ -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"))); diff --git a/crates/domain/src/models/tests/review.rs b/crates/domain/src/models/tests/review.rs index c299d344..74c9c9b1 100644 --- a/crates/domain/src/models/tests/review.rs +++ b/crates/domain/src/models/tests/review.rs @@ -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) -> 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) -> 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"); diff --git a/crates/domain/src/services/tests/review_history.rs b/crates/domain/src/services/tests/review_history.rs index 9c99dbe0..85b4d122 100644 --- a/crates/domain/src/services/tests/review_history.rs +++ b/crates/domain/src/services/tests/review_history.rs @@ -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)), diff --git a/crates/domain/src/testing/factories.rs b/crates/domain/src/testing/factories.rs new file mode 100644 index 00000000..4ad9921b --- /dev/null +++ b/crates/domain/src/testing/factories.rs @@ -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, + ) +} diff --git a/crates/domain/src/testing/mod.rs b/crates/domain/src/testing/mod.rs index 21299b3e..8a36fbf8 100644 --- a/crates/domain/src/testing/mod.rs +++ b/crates/domain/src/testing/mod.rs @@ -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::*;