test: move domain tests to tests/

Self dev-dep w/ test-helpers for domain::testing. Also moved social blanket-impl + InMemoryStore tests (public API only). No private-item exceptions.
This commit is contained in:
2026-10-25 22:34:08 +01:00
parent 1e6e9ddf6b
commit a2c713c1fa
39 changed files with 257 additions and 274 deletions

1
Cargo.lock generated
View File

@@ -1663,6 +1663,7 @@ dependencies = [
"async-trait", "async-trait",
"bytes", "bytes",
"chrono", "chrono",
"domain",
"email_address", "email_address",
"futures", "futures",
"serde", "serde",

View File

@@ -16,6 +16,7 @@ serde_json = { workspace = true }
email_address = "0.2.9" email_address = "0.2.9"
[dev-dependencies] [dev-dependencies]
domain = { path = ".", features = ["test-helpers"] }
tokio = { workspace = true } tokio = { workspace = true }
[features] [features]

View File

@@ -26,7 +26,3 @@ impl DomainError {
matches!(self, DomainError::InfrastructureError(_)) matches!(self, DomainError::InfrastructureError(_))
} }
} }
#[cfg(test)]
#[path = "tests.rs"]
mod tests;

View File

@@ -168,7 +168,3 @@ impl EventEnvelope {
self.ack.nack().await self.ack.nack().await
} }
} }
#[cfg(test)]
#[path = "tests/events.rs"]
mod tests;

View File

@@ -109,7 +109,3 @@ impl GoalWithProgress {
self.current_count >= self.goal.target_count self.current_count >= self.goal.target_count
} }
} }
#[cfg(test)]
#[path = "tests/goal.rs"]
mod tests;

View File

@@ -103,7 +103,3 @@ pub enum ExportFormat {
Csv, Csv,
Json, Json,
} }
#[cfg(test)]
#[path = "tests/mod.rs"]
mod tests;

View File

@@ -181,7 +181,3 @@ pub struct CrewCredit {
pub department: String, pub department: String,
pub poster_path: Option<String>, pub poster_path: Option<String>,
} }
#[cfg(test)]
#[path = "tests/person.rs"]
mod tests;

View File

@@ -202,7 +202,3 @@ impl ReviewHistory {
self.viewings.sort_by_key(|r| *r.watched_at()); self.viewings.sort_by_key(|r| *r.watched_at());
} }
} }
#[cfg(test)]
#[path = "tests/review.rs"]
mod tests;

View File

@@ -138,7 +138,3 @@ pub struct MovieStats {
pub federated_count: u64, pub federated_count: u64,
pub rating_histogram: [u64; 5], // index 0 = 1★, index 4 = 5★ pub rating_histogram: [u64; 5], // index 0 = 1★, index 4 = 5★
} }
#[cfg(test)]
#[path = "tests/stats.rs"]
mod tests;

View File

@@ -234,7 +234,3 @@ pub struct ParsedPlaybackEvent {
pub tmdb_id: Option<String>, pub tmdb_id: Option<String>,
pub imdb_id: Option<String>, pub imdb_id: Option<String>,
} }
#[cfg(test)]
#[path = "tests/watch_event.rs"]
mod tests;

View File

@@ -49,7 +49,3 @@ pub trait ApBackfillPort: Send + Sync {
follower_inbox_url: String, follower_inbox_url: String,
) -> Result<(), DomainError>; ) -> Result<(), DomainError>;
} }
#[cfg(test)]
#[path = "tests/federation.rs"]
mod tests;

View File

@@ -150,164 +150,3 @@ impl<T: SocialCommand + FollowGraphQuery + BlockQuery + FollowTargetResolver + R
LocalSocial for T LocalSocial for T
{ {
} }
#[cfg(test)]
mod local_social_tests {
use super::*;
use std::sync::Arc;
struct Stub;
#[async_trait]
impl SocialCommand for Stub {
async fn follow(
&self,
_: &crate::value_objects::UserId,
_: &crate::value_objects::FollowTarget,
) -> Result<(), DomainError> {
Ok(())
}
async fn unfollow(
&self,
_: &crate::value_objects::UserId,
_: &crate::value_objects::SocialIdentity,
) -> Result<(), DomainError> {
Ok(())
}
async fn accept_follow(
&self,
_: &crate::value_objects::UserId,
_: &crate::value_objects::SocialIdentity,
) -> Result<(), DomainError> {
Ok(())
}
async fn reject_follow(
&self,
_: &crate::value_objects::UserId,
_: &crate::value_objects::SocialIdentity,
) -> Result<(), DomainError> {
Ok(())
}
async fn remove_follower(
&self,
_: &crate::value_objects::UserId,
_: &crate::value_objects::SocialIdentity,
) -> Result<(), DomainError> {
Ok(())
}
async fn block(
&self,
_: &crate::value_objects::UserId,
_: &crate::value_objects::SocialIdentity,
) -> Result<(), DomainError> {
Ok(())
}
async fn unblock(
&self,
_: &crate::value_objects::UserId,
_: &crate::value_objects::SocialIdentity,
) -> Result<(), DomainError> {
Ok(())
}
}
#[async_trait]
impl FollowGraphQuery for Stub {
async fn get_following(
&self,
_: &crate::value_objects::UserId,
) -> Result<Vec<crate::value_objects::SocialActor>, DomainError> {
Ok(vec![])
}
async fn get_followers(
&self,
_: &crate::value_objects::UserId,
) -> Result<Vec<crate::value_objects::SocialActor>, DomainError> {
Ok(vec![])
}
async fn get_pending_followers(
&self,
_: &crate::value_objects::UserId,
) -> Result<Vec<crate::value_objects::SocialActor>, DomainError> {
Ok(vec![])
}
async fn get_pending_following(
&self,
_: &crate::value_objects::UserId,
) -> Result<Vec<crate::value_objects::SocialActor>, DomainError> {
Ok(vec![])
}
async fn count_following(
&self,
_: &crate::value_objects::UserId,
) -> Result<usize, DomainError> {
Ok(0)
}
async fn count_followers(
&self,
_: &crate::value_objects::UserId,
) -> Result<usize, DomainError> {
Ok(0)
}
async fn count_pending_followers(
&self,
_: &crate::value_objects::UserId,
) -> Result<usize, DomainError> {
Ok(0)
}
async fn get_relation(
&self,
_: &crate::value_objects::UserId,
_: &crate::value_objects::SocialIdentity,
) -> Result<crate::value_objects::FollowRelation, DomainError> {
Err(DomainError::NotFound("stub".into()))
}
}
#[async_trait]
impl BlockQuery for Stub {
async fn get_blocked(
&self,
_: &crate::value_objects::UserId,
) -> Result<Vec<crate::value_objects::SocialActor>, DomainError> {
Ok(vec![])
}
}
#[async_trait]
impl FollowTargetResolver for Stub {
async fn resolve_target(
&self,
target: &crate::value_objects::FollowTarget,
) -> Result<SocialIdentity, DomainError> {
match target {
crate::value_objects::FollowTarget::Identity(id) => Ok(id.clone()),
crate::value_objects::FollowTarget::Handle(handle) => Ok(SocialIdentity::Remote {
actor_url: handle.clone(),
}),
}
}
}
#[async_trait]
impl ResolvedFollow for Stub {
async fn follow_resolved(
&self,
_: &crate::value_objects::UserId,
_: &SocialIdentity,
) -> Result<(), DomainError> {
Ok(())
}
}
#[test]
fn blanket_impl_yields_a_trait_object() {
let local: Arc<dyn LocalSocial> = Arc::new(Stub);
// Reachable through each supertrait without a separate Arc.
let _: &dyn SocialCommand = local.as_ref();
let _: &dyn FollowGraphQuery = local.as_ref();
let _: &dyn BlockQuery = local.as_ref();
let _: &dyn FollowTargetResolver = local.as_ref();
let _: &dyn ResolvedFollow = local.as_ref();
}
}

View File

@@ -342,7 +342,3 @@ fn label_of(insight: &Insight) -> &str {
fn round2(v: f64) -> f64 { fn round2(v: f64) -> f64 {
(v * 100.0).round() / 100.0 (v * 100.0).round() / 100.0
} }
#[cfg(test)]
#[path = "tests/insights_analyzer.rs"]
mod tests;

View File

@@ -51,7 +51,3 @@ impl ReviewHistoryAnalyzer {
} }
} }
} }
#[cfg(test)]
#[path = "tests/review_history.rs"]
mod tests;

View File

@@ -464,7 +464,3 @@ fn compute_global_stats(
(most_active_user, most_watched, total_users_active) (most_active_user, most_watched, total_users_active)
} }
#[cfg(test)]
#[path = "tests/wrapup_analyzer.rs"]
mod tests;

View File

@@ -13,8 +13,6 @@ mod store_user;
mod store_watch_event; mod store_watch_event;
mod store_watchlist; mod store_watchlist;
mod stubs; mod stubs;
#[cfg(test)]
mod tests;
mod wrapup; mod wrapup;
pub use insights::*; pub use insights::*;

View File

@@ -11,7 +11,3 @@ pub use movie::*;
pub use review::*; pub use review::*;
pub use social::*; pub use social::*;
pub use user::*; pub use user::*;
#[cfg(test)]
#[path = "../tests/value_objects.rs"]
mod tests;

View File

@@ -0,0 +1,2 @@
#[path = "errors/errors_test.rs"]
mod errors_test;

View File

@@ -1,4 +1,4 @@
use super::*; use domain::errors::DomainError;
#[test] #[test]
fn infrastructure_error_is_transient() { fn infrastructure_error_is_transient() {

View File

@@ -0,0 +1,2 @@
#[path = "events/events_test.rs"]
mod events_test;

View File

@@ -1,5 +1,5 @@
use super::*; use domain::events::DomainEvent;
use crate::value_objects::{FollowTarget, SocialIdentity, UserId}; use domain::value_objects::{FollowTarget, SocialIdentity, UserId};
#[test] #[test]
fn follow_accepted_matches() { fn follow_accepted_matches() {

View File

@@ -0,0 +1,12 @@
#[path = "models/constructors_test.rs"]
mod constructors_test;
#[path = "models/goal_test.rs"]
mod goal_test;
#[path = "models/person_test.rs"]
mod person_test;
#[path = "models/review_test.rs"]
mod review_test;
#[path = "models/stats_test.rs"]
mod stats_test;
#[path = "models/watch_event_test.rs"]
mod watch_event_test;

View File

@@ -1,6 +1,6 @@
use super::*; use domain::models::*;
use crate::testing::{test_movie_with_director, test_movie_with_year, test_review, test_user}; use domain::testing::{test_movie_with_director, test_movie_with_year, test_review, test_user};
use crate::value_objects::{Email, PasswordHash, UserId, Username}; use domain::value_objects::{Email, PasswordHash, UserId, Username};
#[test] #[test]
fn update_profile_sets_fields() { fn update_profile_sets_fields() {
@@ -32,11 +32,11 @@ fn update_profile_clears_with_none() {
#[test] #[test]
fn movie_new_sets_fields() { fn movie_new_sets_fields() {
let m = Movie::new( let m = Movie::new(
Some(crate::value_objects::ExternalMetadataId::new("tt1234567".into()).unwrap()), Some(domain::value_objects::ExternalMetadataId::new("tt1234567".into()).unwrap()),
crate::value_objects::MovieTitle::new("Blade Runner".into()).unwrap(), domain::value_objects::MovieTitle::new("Blade Runner".into()).unwrap(),
crate::value_objects::ReleaseYear::new(1982).unwrap(), domain::value_objects::ReleaseYear::new(1982).unwrap(),
Some("Ridley Scott".into()), Some("Ridley Scott".into()),
Some(crate::value_objects::PosterPath::new("/poster.jpg".into()).unwrap()), Some(domain::value_objects::PosterPath::new("/poster.jpg".into()).unwrap()),
); );
assert_eq!(m.title().value(), "Blade Runner"); assert_eq!(m.title().value(), "Blade Runner");
assert_eq!(m.release_year().value(), 1982); assert_eq!(m.release_year().value(), 1982);
@@ -48,14 +48,14 @@ fn movie_new_sets_fields() {
#[test] #[test]
fn movie_update_poster() { fn movie_update_poster() {
let mut m = test_movie_with_year("Blade Runner", 1982); let mut m = test_movie_with_year("Blade Runner", 1982);
let new_poster = crate::value_objects::PosterPath::new("/new.jpg".into()).unwrap(); let new_poster = domain::value_objects::PosterPath::new("/new.jpg".into()).unwrap();
m.update_poster(new_poster); m.update_poster(new_poster);
assert_eq!(m.poster_path().unwrap().value(), "/new.jpg"); assert_eq!(m.poster_path().unwrap().value(), "/new.jpg");
} }
// ── Review ─────────────────────────────────────────────────────────────────── // ── Review ───────────────────────────────────────────────────────────────────
use crate::value_objects::{MovieId, Rating, ReviewId}; use domain::value_objects::{MovieId, Rating, ReviewId};
#[test] #[test]
fn review_new_sets_local_source() { fn review_new_sets_local_source() {
@@ -199,16 +199,16 @@ fn month_activity_construction() {
#[test] #[test]
fn movie_is_manual_match_same_title_year() { fn movie_is_manual_match_same_title_year() {
let m = test_movie_with_director("Blade Runner", 1982, "Ridley Scott"); let m = test_movie_with_director("Blade Runner", 1982, "Ridley Scott");
let title = crate::value_objects::MovieTitle::new("Blade Runner".into()).unwrap(); let title = domain::value_objects::MovieTitle::new("Blade Runner".into()).unwrap();
let year = crate::value_objects::ReleaseYear::new(1982).unwrap(); let year = domain::value_objects::ReleaseYear::new(1982).unwrap();
assert!(m.is_manual_match(&title, &year, Some("ridley scott"))); assert!(m.is_manual_match(&title, &year, Some("ridley scott")));
} }
#[test] #[test]
fn movie_is_manual_match_different_director_fails() { fn movie_is_manual_match_different_director_fails() {
let m = test_movie_with_director("Blade Runner", 1982, "Ridley Scott"); let m = test_movie_with_director("Blade Runner", 1982, "Ridley Scott");
let title = crate::value_objects::MovieTitle::new("Blade Runner".into()).unwrap(); let title = domain::value_objects::MovieTitle::new("Blade Runner".into()).unwrap();
let year = crate::value_objects::ReleaseYear::new(1982).unwrap(); let year = domain::value_objects::ReleaseYear::new(1982).unwrap();
assert!(!m.is_manual_match(&title, &year, Some("Denis Villeneuve"))); assert!(!m.is_manual_match(&title, &year, Some("Denis Villeneuve")));
} }

View File

@@ -1,6 +1,6 @@
use super::*; use domain::models::*;
use crate::testing::test_goal; use domain::testing::test_goal;
use crate::value_objects::UserId; use domain::value_objects::{GoalId, UserId};
#[test] #[test]
fn new_goal_valid() { fn new_goal_valid() {

View File

@@ -1,4 +1,4 @@
use super::*; use domain::models::*;
#[test] #[test]
fn person_new() { fn person_new() {

View File

@@ -1,7 +1,7 @@
use crate::testing::{test_review, test_watched_at}; use domain::testing::{test_review, test_watched_at};
use crate::value_objects::{Comment, MovieId, Rating, UserId, WatchMedium}; use domain::value_objects::{Comment, MovieId, Rating, UserId, WatchMedium};
use super::*; use domain::models::*;
fn review_with_medium(watch_medium: Option<WatchMedium>) -> Review { fn review_with_medium(watch_medium: Option<WatchMedium>) -> Review {
test_review(&MovieId::generate(), &UserId::generate(), 4).apply_edit(ReviewEdit { test_review(&MovieId::generate(), &UserId::generate(), 4).apply_edit(ReviewEdit {

View File

@@ -1,4 +1,4 @@
use super::*; use domain::models::*;
fn appearance(person_id: u64, name: &str, billing: u32, movie: &str) -> ActorAppearance { fn appearance(person_id: u64, name: &str, billing: u32, movie: &str) -> ActorAppearance {
ActorAppearance { ActorAppearance {

View File

@@ -1,4 +1,6 @@
use super::*; use chrono::NaiveDateTime;
use domain::models::*;
use domain::value_objects::{MovieId, UserId, WebhookTokenId};
fn ts() -> NaiveDateTime { fn ts() -> NaiveDateTime {
chrono::NaiveDate::from_ymd_opt(2024, 6, 1) chrono::NaiveDate::from_ymd_opt(2024, 6, 1)

View File

@@ -0,0 +1,4 @@
#[path = "ports/federation_test.rs"]
mod federation_test;
#[path = "ports/social_test.rs"]
mod social_test;

View File

@@ -1,5 +1,5 @@
use super::*; use domain::ports::noop::{NoopApBackfill, NoopApDocument, NoopInstanceBlocklist};
use crate::ports::noop::{NoopApBackfill, NoopApDocument, NoopInstanceBlocklist}; use domain::ports::{ApBackfillPort, ApDocumentPort, InstanceBlocklistPort};
#[tokio::test] #[tokio::test]
async fn noop_document_returns_empty_strings() { async fn noop_document_returns_empty_strings() {

View File

@@ -0,0 +1,162 @@
use async_trait::async_trait;
use domain::errors::DomainError;
use domain::ports::{
BlockQuery, FollowGraphQuery, FollowTargetResolver, LocalSocial, ResolvedFollow, SocialCommand,
};
use domain::value_objects::SocialIdentity;
use std::sync::Arc;
struct Stub;
#[async_trait]
impl SocialCommand for Stub {
async fn follow(
&self,
_: &domain::value_objects::UserId,
_: &domain::value_objects::FollowTarget,
) -> Result<(), DomainError> {
Ok(())
}
async fn unfollow(
&self,
_: &domain::value_objects::UserId,
_: &domain::value_objects::SocialIdentity,
) -> Result<(), DomainError> {
Ok(())
}
async fn accept_follow(
&self,
_: &domain::value_objects::UserId,
_: &domain::value_objects::SocialIdentity,
) -> Result<(), DomainError> {
Ok(())
}
async fn reject_follow(
&self,
_: &domain::value_objects::UserId,
_: &domain::value_objects::SocialIdentity,
) -> Result<(), DomainError> {
Ok(())
}
async fn remove_follower(
&self,
_: &domain::value_objects::UserId,
_: &domain::value_objects::SocialIdentity,
) -> Result<(), DomainError> {
Ok(())
}
async fn block(
&self,
_: &domain::value_objects::UserId,
_: &domain::value_objects::SocialIdentity,
) -> Result<(), DomainError> {
Ok(())
}
async fn unblock(
&self,
_: &domain::value_objects::UserId,
_: &domain::value_objects::SocialIdentity,
) -> Result<(), DomainError> {
Ok(())
}
}
#[async_trait]
impl FollowGraphQuery for Stub {
async fn get_following(
&self,
_: &domain::value_objects::UserId,
) -> Result<Vec<domain::value_objects::SocialActor>, DomainError> {
Ok(vec![])
}
async fn get_followers(
&self,
_: &domain::value_objects::UserId,
) -> Result<Vec<domain::value_objects::SocialActor>, DomainError> {
Ok(vec![])
}
async fn get_pending_followers(
&self,
_: &domain::value_objects::UserId,
) -> Result<Vec<domain::value_objects::SocialActor>, DomainError> {
Ok(vec![])
}
async fn get_pending_following(
&self,
_: &domain::value_objects::UserId,
) -> Result<Vec<domain::value_objects::SocialActor>, DomainError> {
Ok(vec![])
}
async fn count_following(
&self,
_: &domain::value_objects::UserId,
) -> Result<usize, DomainError> {
Ok(0)
}
async fn count_followers(
&self,
_: &domain::value_objects::UserId,
) -> Result<usize, DomainError> {
Ok(0)
}
async fn count_pending_followers(
&self,
_: &domain::value_objects::UserId,
) -> Result<usize, DomainError> {
Ok(0)
}
async fn get_relation(
&self,
_: &domain::value_objects::UserId,
_: &domain::value_objects::SocialIdentity,
) -> Result<domain::value_objects::FollowRelation, DomainError> {
Err(DomainError::NotFound("stub".into()))
}
}
#[async_trait]
impl BlockQuery for Stub {
async fn get_blocked(
&self,
_: &domain::value_objects::UserId,
) -> Result<Vec<domain::value_objects::SocialActor>, DomainError> {
Ok(vec![])
}
}
#[async_trait]
impl FollowTargetResolver for Stub {
async fn resolve_target(
&self,
target: &domain::value_objects::FollowTarget,
) -> Result<SocialIdentity, DomainError> {
match target {
domain::value_objects::FollowTarget::Identity(id) => Ok(id.clone()),
domain::value_objects::FollowTarget::Handle(handle) => Ok(SocialIdentity::Remote {
actor_url: handle.clone(),
}),
}
}
}
#[async_trait]
impl ResolvedFollow for Stub {
async fn follow_resolved(
&self,
_: &domain::value_objects::UserId,
_: &SocialIdentity,
) -> Result<(), DomainError> {
Ok(())
}
}
#[test]
fn blanket_impl_yields_a_trait_object() {
let local: Arc<dyn LocalSocial> = Arc::new(Stub);
// Reachable through each supertrait without a separate Arc.
let _: &dyn SocialCommand = local.as_ref();
let _: &dyn FollowGraphQuery = local.as_ref();
let _: &dyn BlockQuery = local.as_ref();
let _: &dyn FollowTargetResolver = local.as_ref();
let _: &dyn ResolvedFollow = local.as_ref();
}

View File

@@ -0,0 +1,6 @@
#[path = "services/insights_analyzer_test.rs"]
mod insights_analyzer_test;
#[path = "services/review_history_test.rs"]
mod review_history_test;
#[path = "services/wrapup_analyzer_test.rs"]
mod wrapup_analyzer_test;

View File

@@ -1,9 +1,9 @@
use chrono::{Datelike, FixedOffset, NaiveDate, Utc, Weekday}; use chrono::{Datelike, FixedOffset, NaiveDate, Utc, Weekday};
use uuid::Uuid; use uuid::Uuid;
use crate::models::{Insight, WatchRow}; use domain::models::{Insight, WatchRow};
use super::super::insights_analyzer::build_insights; use domain::services::insights_analyzer::build_insights;
fn watch(genres: &[&str], rating: u8, date: NaiveDate, hour: u32) -> WatchRow { fn watch(genres: &[&str], rating: u8, date: NaiveDate, hour: u32) -> WatchRow {
WatchRow { WatchRow {

View File

@@ -1,8 +1,8 @@
use super::*;
use crate::models::{Review, ReviewHistory};
use crate::testing::test_movie_with_year;
use crate::value_objects::{MovieId, Rating, UserId};
use chrono::{NaiveDate, NaiveDateTime, NaiveTime}; use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
use domain::models::{Review, ReviewHistory};
use domain::services::review_history::{ReviewHistoryAnalyzer, Trend};
use domain::testing::test_movie_with_year;
use domain::value_objects::{MovieId, Rating, UserId};
fn dt(year: i32, month: u32, day: u32) -> NaiveDateTime { fn dt(year: i32, month: u32, day: u32) -> NaiveDateTime {
NaiveDateTime::new( NaiveDateTime::new(

View File

@@ -1,10 +1,10 @@
use chrono::NaiveDate; use chrono::NaiveDate;
use uuid::Uuid; use uuid::Uuid;
use crate::models::WrapUpMovieRow; use domain::models::WrapUpMovieRow;
use crate::models::wrapup::{DateRange, WrapUpScope}; use domain::models::wrapup::{DateRange, WrapUpScope};
use super::super::wrapup_analyzer::build_report; use domain::services::wrapup_analyzer::build_report;
fn row(title: &str, rating: u8, ym: &str) -> WrapUpMovieRow { fn row(title: &str, rating: u8, ym: &str) -> WrapUpMovieRow {
WrapUpMovieRow { WrapUpMovieRow {

View File

@@ -0,0 +1,2 @@
#[path = "testing/in_memory_store_test.rs"]
mod in_memory_store_test;

View File

@@ -3,21 +3,21 @@ use std::sync::Arc;
use chrono::Utc; use chrono::Utc;
use uuid::Uuid; use uuid::Uuid;
use super::{InMemoryStore, MOVIE_CASCADE_TABLES, USER_CASCADE_TABLES}; use domain::errors::DomainError;
use crate::errors::DomainError; use domain::models::{
use crate::models::{
CastMember, CrewMember, Genre, Keyword, Movie, MovieProfile, ProfileField, RefreshSession, CastMember, CrewMember, Genre, Keyword, Movie, MovieProfile, ProfileField, RefreshSession,
UserSettings, WatchEvent, WatchEventSource, WatchlistEntry, WebhookToken, UserSettings, WatchEvent, WatchEventSource, WatchlistEntry, WebhookToken,
}; };
use crate::ports::{ use domain::ports::{
GoalCommand, MovieCommand, MovieProfileRepository, RefreshSessionRepository, ReviewRepository, GoalCommand, MovieCommand, MovieProfileRepository, RefreshSessionRepository, ReviewRepository,
SocialCommand, UserProfileFieldsRepository, UserRepository, UserSettingsRepository, SocialCommand, UserProfileFieldsRepository, UserRepository, UserSettingsRepository,
WatchEventCommand, WatchEventQuery, WatchlistRepository, WebhookTokenRepository, WatchEventCommand, WatchEventQuery, WatchlistRepository, WebhookTokenRepository,
}; };
use crate::testing::{ use domain::testing::{InMemoryStore, MOVIE_CASCADE_TABLES, USER_CASCADE_TABLES};
use domain::testing::{
test_goal, test_movie, test_movie_with_external_id, test_review, test_user, test_watched_at, test_goal, test_movie, test_movie_with_external_id, test_review, test_user, test_watched_at,
}; };
use crate::value_objects::{ExternalMetadataId, FollowTarget, MovieId, SocialIdentity, UserId}; use domain::value_objects::{ExternalMetadataId, FollowTarget, MovieId, SocialIdentity, UserId};
const REMOTE_ACTOR: &str = "https://remote.example/users/ann"; const REMOTE_ACTOR: &str = "https://remote.example/users/ann";
@@ -210,7 +210,10 @@ async fn deleting_a_user_leaves_other_users_rows_alone() {
assert!(store.find_by_id(bystander.id()).await.unwrap().is_some()); assert!(store.find_by_id(bystander.id()).await.unwrap().is_some());
} }
async fn seed_movie_with_dependents(store: &Arc<InMemoryStore>, with_review: bool) -> Movie { async fn seed_movie_with_dependents(
store: &Arc<InMemoryStore>,
with_review: bool,
) -> (Movie, UserId) {
let user = test_user("alice"); let user = test_user("alice");
UserRepository::save(&**store, &user).await.unwrap(); UserRepository::save(&**store, &user).await.unwrap();
let movie = test_movie("Heat"); let movie = test_movie("Heat");
@@ -240,13 +243,13 @@ async fn seed_movie_with_dependents(store: &Arc<InMemoryStore>, with_review: boo
.await .await
.unwrap(); .unwrap();
} }
movie (movie, user.id().clone())
} }
#[tokio::test] #[tokio::test]
async fn deleting_a_movie_removes_watchlist_entries_and_enrichment_profile() { async fn deleting_a_movie_removes_watchlist_entries_and_enrichment_profile() {
let store = InMemoryStore::new(); let store = InMemoryStore::new();
let movie = seed_movie_with_dependents(&store, false).await; let (movie, _) = seed_movie_with_dependents(&store, false).await;
for table in MOVIE_CASCADE_TABLES { for table in MOVIE_CASCADE_TABLES {
assert!(store.rows_in(table) > 0, "{table} should be seeded"); assert!(store.rows_in(table) > 0, "{table} should be seeded");
} }
@@ -262,8 +265,7 @@ async fn deleting_a_movie_removes_watchlist_entries_and_enrichment_profile() {
#[tokio::test] #[tokio::test]
async fn deleting_a_movie_detaches_watch_events_instead_of_removing_them() { async fn deleting_a_movie_detaches_watch_events_instead_of_removing_them() {
let store = InMemoryStore::new(); let store = InMemoryStore::new();
let movie = seed_movie_with_dependents(&store, false).await; let (movie, user_id) = seed_movie_with_dependents(&store, false).await;
let user_id = store.watch_events.read().unwrap()[0].user_id().clone();
store.delete_movie(movie.id()).await.unwrap(); store.delete_movie(movie.id()).await.unwrap();
@@ -275,7 +277,7 @@ async fn deleting_a_movie_detaches_watch_events_instead_of_removing_them() {
#[tokio::test] #[tokio::test]
async fn deleting_a_movie_that_has_reviews_is_refused_because_reviews_do_not_cascade() { async fn deleting_a_movie_that_has_reviews_is_refused_because_reviews_do_not_cascade() {
let store = InMemoryStore::new(); let store = InMemoryStore::new();
let movie = seed_movie_with_dependents(&store, true).await; let (movie, _) = seed_movie_with_dependents(&store, true).await;
let result = store.delete_movie(movie.id()).await; let result = store.delete_movie(movie.id()).await;
@@ -291,11 +293,11 @@ async fn saving_a_user_with_a_taken_username_is_a_validation_error() {
UserRepository::save(&*store, &test_user("alice")) UserRepository::save(&*store, &test_user("alice"))
.await .await
.unwrap(); .unwrap();
let clash = crate::models::User::new( let clash = domain::models::User::new(
crate::value_objects::Email::new("other@example.com".into()).unwrap(), domain::value_objects::Email::new("other@example.com".into()).unwrap(),
crate::value_objects::Username::new("alice".into()).unwrap(), domain::value_objects::Username::new("alice".into()).unwrap(),
crate::value_objects::PasswordHash::new("hashed".into()).unwrap(), domain::value_objects::PasswordHash::new("hashed".into()).unwrap(),
crate::models::UserRole::Standard, domain::models::UserRole::Standard,
); );
let result = UserRepository::save(&*store, &clash).await; let result = UserRepository::save(&*store, &clash).await;
@@ -309,11 +311,11 @@ async fn saving_a_user_with_a_taken_email_is_a_validation_error() {
UserRepository::save(&*store, &test_user("alice")) UserRepository::save(&*store, &test_user("alice"))
.await .await
.unwrap(); .unwrap();
let clash = crate::models::User::new( let clash = domain::models::User::new(
crate::value_objects::Email::new("alice@example.com".into()).unwrap(), domain::value_objects::Email::new("alice@example.com".into()).unwrap(),
crate::value_objects::Username::new("alice2".into()).unwrap(), domain::value_objects::Username::new("alice2".into()).unwrap(),
crate::value_objects::PasswordHash::new("hashed".into()).unwrap(), domain::value_objects::PasswordHash::new("hashed".into()).unwrap(),
crate::models::UserRole::Standard, domain::models::UserRole::Standard,
); );
let result = UserRepository::save(&*store, &clash).await; let result = UserRepository::save(&*store, &clash).await;

View File

@@ -0,0 +1,2 @@
#[path = "value_objects/value_objects_test.rs"]
mod value_objects_test;

View File

@@ -1,4 +1,4 @@
use super::*; use domain::value_objects::*;
#[test] #[test]
fn movie_id_generate_unique() { fn movie_id_generate_unique() {