test: move application tests to tests/
test_support/test_helpers -> tests/support; watched_at unified w/ domain test_watched_at (2024-03-15). Kept inline: users::diary_filter tests (feed_sort_to_direction_all_variants, paged_user_filter_builds_correctly) - pub(super) helpers in private module. Dropped stale never-compiled users/tests/get_users.rs (old execute signature).
This commit is contained in:
123
crates/application/tests/movies/enrich_movie_test.rs
Normal file
123
crates/application/tests/movies/enrich_movie_test.rs
Normal file
@@ -0,0 +1,123 @@
|
||||
use domain::testing::{NoopPersonCommand, test_movie_with_year};
|
||||
use std::sync::Arc;
|
||||
|
||||
use chrono::Utc;
|
||||
use domain::{
|
||||
models::MovieProfile,
|
||||
ports::MovieCommand,
|
||||
testing::{FakeSearchCommand, InMemoryStore, PanicPersonCommand},
|
||||
value_objects::MovieId,
|
||||
};
|
||||
|
||||
use application::movies::{commands::EnrichMovieCommand, deps::EnrichMovieDeps, enrich_movie};
|
||||
|
||||
#[tokio::test]
|
||||
async fn stores_profile_and_indexes() {
|
||||
let movie_repo = InMemoryStore::new();
|
||||
let profile_repo = Arc::clone(&movie_repo);
|
||||
// PanicPersonCommand is safe here — empty cast/crew means upsert_batch is never called
|
||||
|
||||
let movie = test_movie_with_year("Test", 2024);
|
||||
let movie_id = MovieId::from_uuid(movie.id().value());
|
||||
movie_repo.upsert_movie(&movie).await.unwrap();
|
||||
|
||||
let profile = MovieProfile {
|
||||
movie_id: movie_id.clone(),
|
||||
tmdb_id: 999,
|
||||
imdb_id: None,
|
||||
overview: Some("A test movie".into()),
|
||||
tagline: None,
|
||||
runtime_minutes: Some(120),
|
||||
budget_usd: None,
|
||||
revenue_usd: None,
|
||||
vote_average: Some(7.5),
|
||||
vote_count: Some(100),
|
||||
original_language: Some("en".into()),
|
||||
collection_name: None,
|
||||
genres: vec![],
|
||||
keywords: vec![],
|
||||
cast: vec![],
|
||||
crew: vec![],
|
||||
enriched_at: Utc::now(),
|
||||
};
|
||||
|
||||
let deps = EnrichMovieDeps {
|
||||
movie_query: movie_repo as Arc<_>,
|
||||
movie_profile: Arc::clone(&profile_repo) as Arc<_>,
|
||||
person_command: Arc::new(PanicPersonCommand),
|
||||
search_command: Arc::new(FakeSearchCommand),
|
||||
};
|
||||
|
||||
enrich_movie::execute(
|
||||
&deps,
|
||||
EnrichMovieCommand {
|
||||
movie_id: movie_id.clone(),
|
||||
profile,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(profile_repo.movie_profile_count(), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn extracts_and_indexes_persons() {
|
||||
let movie_repo = InMemoryStore::new();
|
||||
let profile_repo = Arc::clone(&movie_repo);
|
||||
|
||||
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();
|
||||
|
||||
let profile = MovieProfile {
|
||||
movie_id: movie_id.clone(),
|
||||
tmdb_id: 1001,
|
||||
imdb_id: None,
|
||||
overview: None,
|
||||
tagline: None,
|
||||
runtime_minutes: None,
|
||||
budget_usd: None,
|
||||
revenue_usd: None,
|
||||
vote_average: None,
|
||||
vote_count: None,
|
||||
original_language: None,
|
||||
collection_name: None,
|
||||
genres: vec![],
|
||||
keywords: vec![],
|
||||
cast: vec![domain::models::CastMember {
|
||||
tmdb_person_id: 42,
|
||||
name: "Actor One".into(),
|
||||
character: "Hero".into(),
|
||||
billing_order: 0,
|
||||
profile_path: None,
|
||||
}],
|
||||
crew: vec![domain::models::CrewMember {
|
||||
tmdb_person_id: 99,
|
||||
name: "Director One".into(),
|
||||
job: "Director".into(),
|
||||
department: "Directing".into(),
|
||||
profile_path: None,
|
||||
}],
|
||||
enriched_at: Utc::now(),
|
||||
};
|
||||
|
||||
let deps = EnrichMovieDeps {
|
||||
movie_query: movie_repo as Arc<_>,
|
||||
movie_profile: Arc::clone(&profile_repo) as Arc<_>,
|
||||
person_command: Arc::new(NoopPersonCommand),
|
||||
search_command: Arc::new(FakeSearchCommand),
|
||||
};
|
||||
|
||||
enrich_movie::execute(
|
||||
&deps,
|
||||
EnrichMovieCommand {
|
||||
movie_id: movie_id.clone(),
|
||||
profile,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(profile_repo.movie_profile_count(), 1);
|
||||
}
|
||||
88
crates/application/tests/movies/get_movie_profile_test.rs
Normal file
88
crates/application/tests/movies/get_movie_profile_test.rs
Normal file
@@ -0,0 +1,88 @@
|
||||
use chrono::Utc;
|
||||
use uuid::Uuid;
|
||||
|
||||
use domain::{
|
||||
models::{CastMember, CrewMember, MovieProfile},
|
||||
ports::MovieProfileRepository,
|
||||
testing::InMemoryStore,
|
||||
value_objects::MovieId,
|
||||
};
|
||||
|
||||
use application::movies::deps::GetMovieProfileDeps;
|
||||
use application::movies::get_movie_profile::{self, GetMovieProfileQuery};
|
||||
|
||||
#[tokio::test]
|
||||
async fn returns_none_when_no_profile() {
|
||||
let movie_profile = InMemoryStore::new();
|
||||
let deps = GetMovieProfileDeps { movie_profile };
|
||||
|
||||
let result = get_movie_profile::execute(
|
||||
&deps,
|
||||
GetMovieProfileQuery {
|
||||
movie_id: Uuid::new_v4(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(result.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn returns_profile_with_cast_and_crew() {
|
||||
let profile_repo = InMemoryStore::new();
|
||||
let movie_id = MovieId::generate();
|
||||
|
||||
let profile = MovieProfile {
|
||||
movie_id: movie_id.clone(),
|
||||
tmdb_id: 42,
|
||||
imdb_id: Some("tt1234567".into()),
|
||||
overview: Some("A great movie".into()),
|
||||
tagline: None,
|
||||
runtime_minutes: Some(120),
|
||||
budget_usd: None,
|
||||
revenue_usd: None,
|
||||
vote_average: Some(8.0),
|
||||
vote_count: Some(500),
|
||||
original_language: Some("en".into()),
|
||||
collection_name: None,
|
||||
genres: vec![],
|
||||
keywords: vec![],
|
||||
cast: vec![CastMember {
|
||||
tmdb_person_id: 1,
|
||||
name: "Alice".into(),
|
||||
character: "Hero".into(),
|
||||
billing_order: 0,
|
||||
profile_path: None,
|
||||
}],
|
||||
crew: vec![CrewMember {
|
||||
tmdb_person_id: 2,
|
||||
name: "Bob".into(),
|
||||
job: "Director".into(),
|
||||
department: "Directing".into(),
|
||||
profile_path: None,
|
||||
}],
|
||||
enriched_at: Utc::now(),
|
||||
};
|
||||
profile_repo.upsert(&profile).await.unwrap();
|
||||
|
||||
let deps = GetMovieProfileDeps {
|
||||
movie_profile: profile_repo.clone(),
|
||||
};
|
||||
let result = get_movie_profile::execute(
|
||||
&deps,
|
||||
GetMovieProfileQuery {
|
||||
movie_id: movie_id.value(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let res = result.expect("profile should be present");
|
||||
assert_eq!(res.cast.len(), 1);
|
||||
assert_eq!(res.cast[0].name, "Alice");
|
||||
assert_eq!(res.cast[0].character, "Hero");
|
||||
assert_eq!(res.crew.len(), 1);
|
||||
assert_eq!(res.crew[0].name, "Bob");
|
||||
assert_eq!(res.crew[0].job, "Director");
|
||||
}
|
||||
24
crates/application/tests/movies/get_movies_test.rs
Normal file
24
crates/application/tests/movies/get_movies_test.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
use domain::testing::InMemoryStore;
|
||||
|
||||
use application::movies::{deps::GetMoviesDeps, get_movies, queries::GetMoviesQuery};
|
||||
|
||||
#[tokio::test]
|
||||
async fn returns_empty_when_no_movies() {
|
||||
let movie = InMemoryStore::new();
|
||||
let deps = GetMoviesDeps { movie };
|
||||
|
||||
let result = get_movies::execute(
|
||||
&deps,
|
||||
GetMoviesQuery {
|
||||
limit: None,
|
||||
offset: None,
|
||||
search: None,
|
||||
genre: None,
|
||||
language: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(result.items.is_empty());
|
||||
}
|
||||
103
crates/application/tests/movies/request_enrichment_test.rs
Normal file
103
crates/application/tests/movies/request_enrichment_test.rs
Normal file
@@ -0,0 +1,103 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use chrono::Utc;
|
||||
use domain::{
|
||||
errors::DomainError,
|
||||
models::MovieProfile,
|
||||
ports::{MovieEnrichmentClient, MovieProfileRepository},
|
||||
testing::{FakeMovieEnrichmentClient, InMemoryStore},
|
||||
value_objects::MovieId,
|
||||
};
|
||||
|
||||
use application::movies::request_enrichment;
|
||||
|
||||
#[tokio::test]
|
||||
async fn returns_profile_when_none_cached() {
|
||||
let enrichment = FakeMovieEnrichmentClient;
|
||||
let profile_repo = InMemoryStore::new();
|
||||
let movie_id = MovieId::generate();
|
||||
|
||||
let result = request_enrichment::fetch_if_stale(
|
||||
&enrichment,
|
||||
&(profile_repo as Arc<_>),
|
||||
movie_id.clone(),
|
||||
"tmdb:12345",
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(result.is_some());
|
||||
assert_eq!(result.unwrap().movie_id, movie_id);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn returns_none_when_profile_is_fresh() {
|
||||
let enrichment = FakeMovieEnrichmentClient;
|
||||
let profile_repo = InMemoryStore::new();
|
||||
let movie_id = MovieId::generate();
|
||||
|
||||
// Seed a fresh profile (enriched_at = now)
|
||||
let fresh_profile = MovieProfile {
|
||||
movie_id: movie_id.clone(),
|
||||
tmdb_id: 12345,
|
||||
imdb_id: None,
|
||||
overview: None,
|
||||
tagline: None,
|
||||
runtime_minutes: None,
|
||||
budget_usd: None,
|
||||
revenue_usd: None,
|
||||
vote_average: None,
|
||||
vote_count: None,
|
||||
original_language: None,
|
||||
collection_name: None,
|
||||
genres: vec![],
|
||||
keywords: vec![],
|
||||
cast: vec![],
|
||||
crew: vec![],
|
||||
enriched_at: Utc::now(),
|
||||
};
|
||||
profile_repo.upsert(&fresh_profile).await.unwrap();
|
||||
|
||||
let result = request_enrichment::fetch_if_stale(
|
||||
&enrichment,
|
||||
&(Arc::clone(&profile_repo) as Arc<_>),
|
||||
movie_id,
|
||||
"tmdb:12345",
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(result.is_none(), "fresh profile should be skipped");
|
||||
}
|
||||
|
||||
struct NotFoundEnrichmentClient;
|
||||
|
||||
#[async_trait]
|
||||
impl MovieEnrichmentClient for NotFoundEnrichmentClient {
|
||||
async fn fetch_profile(
|
||||
&self,
|
||||
_movie_id: MovieId,
|
||||
_external_metadata_id: &str,
|
||||
) -> Result<MovieProfile, DomainError> {
|
||||
Err(DomainError::NotFound("not found in TMDb".into()))
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn returns_none_on_not_found_from_client() {
|
||||
let enrichment = NotFoundEnrichmentClient;
|
||||
let profile_repo = InMemoryStore::new();
|
||||
let movie_id = MovieId::generate();
|
||||
|
||||
let result = request_enrichment::fetch_if_stale(
|
||||
&enrichment,
|
||||
&(profile_repo as Arc<_>),
|
||||
movie_id,
|
||||
"tmdb:99999",
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(result.is_none(), "NotFound should return Ok(None), not Err");
|
||||
}
|
||||
102
crates/application/tests/movies/sync_poster_test.rs
Normal file
102
crates/application/tests/movies/sync_poster_test.rs
Normal file
@@ -0,0 +1,102 @@
|
||||
use crate::support::sync_poster_command;
|
||||
use domain::testing::{test_movie_with_external_id, test_movie_with_year};
|
||||
use std::sync::Arc;
|
||||
|
||||
use uuid::Uuid;
|
||||
|
||||
use domain::{
|
||||
errors::DomainError,
|
||||
models::Movie,
|
||||
ports::{MetadataClient, MovieCommand, MovieQuery},
|
||||
testing::{FakeSearchCommand, InMemoryStore, NoopEventPublisher, NoopObjectStorage},
|
||||
value_objects::{ExternalMetadataId, PosterUrl},
|
||||
};
|
||||
|
||||
use application::movies::{deps::SyncPosterDeps, sync_poster};
|
||||
|
||||
fn default_deps() -> SyncPosterDeps {
|
||||
SyncPosterDeps {
|
||||
movie_command: InMemoryStore::new(),
|
||||
movie_query: InMemoryStore::new(),
|
||||
movie_profile: InMemoryStore::new(),
|
||||
metadata: Arc::new(domain::testing::FakeMetadataClient),
|
||||
poster_fetcher: Arc::new(domain::testing::FakePosterFetcher),
|
||||
object_storage: Arc::new(NoopObjectStorage),
|
||||
event_publisher: NoopEventPublisher::new(),
|
||||
search_command: Arc::new(FakeSearchCommand),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fails_when_movie_not_found() {
|
||||
let deps = default_deps();
|
||||
|
||||
let result = sync_poster::execute(&deps, sync_poster_command(Uuid::new_v4())).await;
|
||||
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fails_when_no_external_id() {
|
||||
let movies = InMemoryStore::new();
|
||||
let movie = test_movie_with_year("Test", 2024);
|
||||
let movie_id = movie.id().value();
|
||||
movies.upsert_movie(&movie).await.unwrap();
|
||||
|
||||
let deps = SyncPosterDeps {
|
||||
movie_command: Arc::clone(&movies) as _,
|
||||
movie_query: Arc::clone(&movies) as _,
|
||||
..default_deps()
|
||||
};
|
||||
|
||||
let result = sync_poster::execute(&deps, sync_poster_command(movie_id)).await;
|
||||
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
struct FakeMetaWithPoster;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MetadataClient for FakeMetaWithPoster {
|
||||
async fn fetch_movie_metadata(
|
||||
&self,
|
||||
_: &domain::models::MetadataSearchCriteria,
|
||||
) -> Result<Movie, DomainError> {
|
||||
unimplemented!()
|
||||
}
|
||||
async fn get_poster_url(
|
||||
&self,
|
||||
_: &ExternalMetadataId,
|
||||
) -> Result<Option<PosterUrl>, DomainError> {
|
||||
Ok(Some(
|
||||
PosterUrl::new("https://example.com/poster.jpg".into()).unwrap(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn syncs_poster_for_movie_with_external_id() {
|
||||
let movies = InMemoryStore::new();
|
||||
let ext_id = ExternalMetadataId::new("tmdb:999".into()).unwrap();
|
||||
let movie = test_movie_with_external_id("Poster Movie", 2024, ext_id);
|
||||
let movie_id = movie.id().value();
|
||||
movies.upsert_movie(&movie).await.unwrap();
|
||||
|
||||
let deps = SyncPosterDeps {
|
||||
movie_command: Arc::clone(&movies) as _,
|
||||
movie_query: Arc::clone(&movies) as _,
|
||||
metadata: Arc::new(FakeMetaWithPoster) as _,
|
||||
..default_deps()
|
||||
};
|
||||
|
||||
sync_poster::execute(&deps, sync_poster_command(movie_id))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let updated = movies
|
||||
.get_movie_by_id(&domain::value_objects::MovieId::from_uuid(movie_id))
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert!(updated.poster_path().is_some());
|
||||
}
|
||||
Reference in New Issue
Block a user