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

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

View File

@@ -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::Value> = 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::Value> = 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,
))

View File

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

View File

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

View File

@@ -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)
}

View File

@@ -22,4 +22,5 @@ bytes = { workspace = true }
async-stream = { workspace = true }
[dev-dependencies]
domain = { workspace = true, features = ["test-helpers"] }
tempfile = { workspace = true }

View File

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

View File

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

View File

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

View File

@@ -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(
&reg_deps,
RegisterCommand {
email: "bob@example.com".to_string(),
username: "bob".to_string(),
password: "password123".to_string(),
role: UserRole::Standard,
},
)
.await
.unwrap();
register::execute(&reg_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(),

View File

@@ -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(
&reg_deps,
RegisterCommand {
email: "alice@example.com".to_string(),
username: "alice".to_string(),
password: "password123".to_string(),
role: UserRole::Standard,
},
)
.await
.unwrap();
register::execute(&reg_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()

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<String>,
username: impl Into<String>,
) -> RegisterCommand {
RegisterCommand {
email: email.into(),
username: username.into(),
password: "password123".to_string(),
role: UserRole::Standard,
}
}
pub(crate) fn login_command(email: impl Into<String>, password: impl Into<String>) -> 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<Uuid>,
) -> 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<Uuid>, 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(),
}
}

View File

@@ -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(
&reg_deps,
RegisterCommand {
email: email.into(),
username: username.into(),
password: "password123".into(),
role,
..register_command(email, username)
},
)
.await

View File

@@ -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(
&reg_deps,
RegisterCommand {
email: "alice@example.com".into(),
username: "alice".into(),
password: "password123".into(),
role: UserRole::Standard,
},
)
.await
.unwrap();
register::execute(&reg_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())

View File

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

View File

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

View File

@@ -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(
&register_deps,
RegisterCommand {
email: "settings@test.com".into(),
username: "settingsuser".into(),
password: "password123".into(),
role: UserRole::Standard,
},
register_command("settings@test.com", "settingsuser"),
)
.await
.unwrap();

View File

@@ -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<InMemoryUserRepositor
password_hasher: b.password_hasher.clone(),
config: b.config.clone(),
};
register::execute(
&reg_deps,
RegisterCommand {
email: "alice@example.com".into(),
username: "alice".into(),
password: "password123".into(),
role: UserRole::Standard,
},
)
.await
.unwrap();
register::execute(&reg_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())
@@ -55,14 +47,8 @@ async fn updates_display_name() {
update_profile::execute(
&deps,
UpdateProfileCommand {
user_id: uid,
display_name: Some("Alice W.".into()),
bio: None,
avatar_bytes: None,
avatar_content_type: None,
banner_bytes: None,
banner_content_type: None,
also_known_as: None,
..update_profile_command(uid)
},
)
.await
@@ -90,14 +76,9 @@ async fn rejects_invalid_avatar_content_type() {
let result = update_profile::execute(
&deps,
UpdateProfileCommand {
user_id: uid,
display_name: None,
bio: None,
avatar_bytes: Some(vec![0u8; 10]),
avatar_content_type: Some("image/gif".into()),
banner_bytes: None,
banner_content_type: None,
also_known_as: None,
..update_profile_command(uid)
},
)
.await;
@@ -122,14 +103,9 @@ async fn uploads_avatar() {
update_profile::execute(
&deps,
UpdateProfileCommand {
user_id: uid,
display_name: None,
bio: None,
avatar_bytes: Some(vec![0xFFu8, 0xD8, 0xFF]),
avatar_content_type: Some("image/jpeg".into()),
banner_bytes: None,
banner_content_type: None,
also_known_as: None,
..update_profile_command(uid)
},
)
.await
@@ -165,14 +141,9 @@ async fn uploads_banner() {
update_profile::execute(
&deps,
UpdateProfileCommand {
user_id: uid,
display_name: None,
bio: None,
avatar_bytes: None,
avatar_content_type: None,
banner_bytes: Some(vec![0x89, 0x50, 0x4E]),
banner_content_type: Some("image/png".into()),
also_known_as: None,
..update_profile_command(uid)
},
)
.await
@@ -203,14 +174,8 @@ async fn fails_for_nonexistent_user() {
let result = update_profile::execute(
&deps,
UpdateProfileCommand {
user_id: Uuid::new_v4(),
display_name: Some("Ghost".into()),
bio: None,
avatar_bytes: None,
avatar_content_type: None,
banner_bytes: None,
banner_content_type: None,
also_known_as: None,
..update_profile_command(Uuid::new_v4())
},
)
.await;
@@ -232,14 +197,9 @@ async fn rejects_invalid_banner_content_type() {
let result = update_profile::execute(
&deps,
UpdateProfileCommand {
user_id: uid,
display_name: None,
bio: None,
avatar_bytes: None,
avatar_content_type: None,
banner_bytes: Some(vec![0u8; 10]),
banner_content_type: Some("text/plain".into()),
also_known_as: None,
..update_profile_command(uid)
},
)
.await;
@@ -264,14 +224,9 @@ async fn text_only_update_emits_user_updated_no_image_stored() {
update_profile::execute(
&deps,
UpdateProfileCommand {
user_id: uid,
display_name: Some("Alice Updated".into()),
bio: Some("Hello world".into()),
avatar_bytes: None,
avatar_content_type: None,
banner_bytes: None,
banner_content_type: None,
also_known_as: None,
..update_profile_command(uid)
},
)
.await

View File

@@ -1,3 +1,4 @@
use crate::test_support::update_user_settings_command;
use std::sync::Arc;
use domain::testing::InMemoryUserSettingsRepository;
@@ -24,10 +25,8 @@ async fn updates_federate_goals() {
user_settings: user_settings.clone(),
},
UpdateUserSettingsCommand {
user_id: uid,
federate_goals: false,
federate_reviews: true,
federate_watchlist: true,
..update_user_settings_command(uid)
},
)
.await
@@ -53,10 +52,8 @@ async fn updates_federate_reviews() {
user_settings: user_settings.clone(),
},
UpdateUserSettingsCommand {
user_id: uid,
federate_goals: true,
federate_reviews: false,
federate_watchlist: true,
..update_user_settings_command(uid)
},
)
.await
@@ -82,10 +79,8 @@ async fn updates_federate_watchlist() {
user_settings: user_settings.clone(),
},
UpdateUserSettingsCommand {
user_id: uid,
federate_goals: true,
federate_reviews: true,
federate_watchlist: false,
..update_user_settings_command(uid)
},
)
.await

View File

@@ -1,16 +1,13 @@
use crate::test_support::{add_to_watchlist_command, movie_input_manual};
use domain::testing::test_movie_with_year;
use std::sync::Arc;
use domain::{
models::Movie,
ports::MovieCommand,
testing::{InMemoryMovieRepository, InMemoryWatchlistRepository, NoopEventPublisher},
value_objects::{MovieTitle, ReleaseYear},
};
use crate::{
diary::commands::MovieInput,
watchlist::{add, commands::AddToWatchlistCommand, deps::WatchlistAddDeps},
};
use crate::watchlist::{add, commands::AddToWatchlistCommand, deps::WatchlistAddDeps};
fn make_deps(
movies: Arc<InMemoryMovieRepository>,
@@ -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());
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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