test: shared factories and command builders
This commit is contained in:
@@ -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 }
|
||||
|
||||
@@ -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,
|
||||
))
|
||||
|
||||
@@ -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 _,
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -22,4 +22,5 @@ bytes = { workspace = true }
|
||||
async-stream = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
domain = { workspace = true, features = ["test-helpers"] }
|
||||
tempfile = { workspace = true }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user