test: shared factories and command builders

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

View File

@@ -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();

View File

@@ -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<InMemoryReviewRepository>,
Arc<NoopEventPublisher>,
@@ -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

View File

@@ -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();

View File

@@ -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();

View File

@@ -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,

View File

@@ -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");

View File

@@ -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<u16>) -> 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 {

View File

@@ -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<Movie, DomainError> {
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();