use std::sync::Arc; use application::config::AppConfig; use domain::testing::{ FakeAuthService, FakeDocumentParser, FakeMetadataClient, FakePasswordHasher, FakePersonQuery, FakePosterFetcher, FakeSearchCommand, FakeSearchPort, FakeStatsRepository, InMemoryInsightsQuery, InMemoryStore, InMemoryWrapUpRepository, InMemoryWrapUpStatsQuery, NoopEventPublisher, NoopFederationAdminQuery, NoopObjectStorage, NoopRemoteGoalRepository, NoopRemoteWatchlistRepository, PanicDiaryExporter, PanicPersonCommand, }; use crate::{DatabaseOutput, Repositories, build::build_deps, build::build_worker_deps}; struct NoopReviewLogger; #[async_trait::async_trait] impl application::ports::ReviewLogger for NoopReviewLogger { async fn log_review( &self, _cmd: application::diary::commands::LogReviewCommand, ) -> Result<(), domain::errors::DomainError> { Ok(()) } } struct NoopMovieDeduplicator; #[async_trait::async_trait] impl domain::ports::MovieDeduplicator for NoopMovieDeduplicator { async fn merge_into_canonical( &self, _old_id: &domain::value_objects::MovieId, _canonical: &domain::models::Movie, ) -> Result { Ok(0) } } struct NoopLocalApContentQuery; #[async_trait::async_trait] impl domain::ports::LocalApContentQuery for NoopLocalApContentQuery { async fn get_local_watchlist_for_user( &self, _user_id: &domain::value_objects::UserId, ) -> Result, domain::errors::DomainError> { Ok(vec![]) } async fn get_local_reviews_for_movie( &self, _movie_id: &domain::value_objects::MovieId, ) -> Result, domain::errors::DomainError> { Ok(vec![]) } async fn get_local_reviews_page( &self, _user_id: &domain::value_objects::UserId, _before: Option, _limit: usize, ) -> Result, domain::errors::DomainError> { Ok(vec![]) } } struct NoopImageRefCommand; #[async_trait::async_trait] impl domain::ports::ImageRefCommand for NoopImageRefCommand { async fn swap( &self, _old_key: &str, _new_key: &str, ) -> Result<(), domain::errors::DomainError> { Ok(()) } } struct NoopImageRefQuery; #[async_trait::async_trait] impl domain::ports::ImageRefQuery for NoopImageRefQuery { async fn list_keys(&self) -> Result, domain::errors::DomainError> { Ok(vec![]) } } /// `DatabaseOutput::follow_command`/`follow_query` need a fake independent of /// `InMemoryStore` (domain::testing), which implements /// `SocialCommand`/`FollowGraphQuery`/`BlockQuery` — the composite ports these /// two feed, one level up — so it cannot stand in as their dependency. These /// fixture fields are never exercised by `build_deps`/`build_worker_deps` /// (neither wires them further), so the bodies just need to type-check. struct NoopFollowCommand; #[async_trait::async_trait] impl domain::ports::FollowCommand for NoopFollowCommand { async fn add_follow( &self, _follower_id: uuid::Uuid, _target_actor_url: &str, _status: domain::value_objects::FollowStatus, ) -> Result<(), domain::errors::DomainError> { Ok(()) } async fn update_follow_status( &self, _follower_id: uuid::Uuid, _target_actor_url: &str, _status: domain::value_objects::FollowStatus, ) -> Result<(), domain::errors::DomainError> { Ok(()) } async fn remove_follow( &self, _follower_id: uuid::Uuid, _target_actor_url: &str, ) -> Result<(), domain::errors::DomainError> { Ok(()) } async fn add_follower( &self, _local_user_id: uuid::Uuid, _follower_actor_url: &str, _status: domain::value_objects::FollowStatus, ) -> Result<(), domain::errors::DomainError> { Ok(()) } async fn update_follower_status( &self, _local_user_id: uuid::Uuid, _follower_actor_url: &str, _status: domain::value_objects::FollowStatus, ) -> Result<(), domain::errors::DomainError> { Ok(()) } async fn remove_follower_record( &self, _local_user_id: uuid::Uuid, _follower_actor_url: &str, ) -> Result<(), domain::errors::DomainError> { Ok(()) } } struct NoopFollowQuery; #[async_trait::async_trait] impl domain::ports::FollowQuery for NoopFollowQuery { async fn get_following( &self, _user_id: uuid::Uuid, ) -> Result, domain::errors::DomainError> { Ok(vec![]) } async fn get_followers( &self, _user_id: uuid::Uuid, ) -> Result, domain::errors::DomainError> { Ok(vec![]) } async fn get_pending_followers( &self, _user_id: uuid::Uuid, ) -> Result, domain::errors::DomainError> { Ok(vec![]) } async fn get_pending_following( &self, _user_id: uuid::Uuid, ) -> Result, domain::errors::DomainError> { Ok(vec![]) } async fn count_following( &self, _user_id: uuid::Uuid, ) -> Result { Ok(0) } async fn count_followers( &self, _user_id: uuid::Uuid, ) -> Result { Ok(0) } async fn count_pending_followers( &self, _user_id: uuid::Uuid, ) -> Result { Ok(0) } async fn get_relation( &self, _viewer_id: uuid::Uuid, _target_actor_url: &str, ) -> Result { // Empty/no-edge, matching every sibling method's empty-success shape above — // unlike them this isn't exercised by any test, so there's no behavior to pin, // just consistency for a future reader. Ok(domain::value_objects::FollowRelation { following: None, followed_by: None, }) } } fn test_repositories() -> Repositories { let movies = InMemoryStore::new(); let watch_events = Arc::clone(&movies); let goals = Arc::clone(&movies); let social = Arc::clone(&movies); Repositories { movie_command: Arc::clone(&movies) as _, movie_query: movies as _, review: InMemoryStore::new(), diary: domain::testing::FakeDiaryQuery::new(), stats: FakeStatsRepository::new(), user: InMemoryStore::new(), import_session: InMemoryStore::new(), import_profile: InMemoryStore::new(), movie_profile: InMemoryStore::new(), watchlist: InMemoryStore::new(), watch_event_command: Arc::clone(&watch_events) as _, watch_event_query: watch_events as _, webhook_token: InMemoryStore::new(), person_command: Arc::new(PanicPersonCommand), person_query: Arc::new(FakePersonQuery), search_port: Arc::new(FakeSearchPort), search_command: Arc::new(FakeSearchCommand), profile_fields: InMemoryStore::new(), remote_watchlist: Arc::new(NoopRemoteWatchlistRepository), social_command: Arc::clone(&social) as _, follow_graph: Arc::clone(&social) as _, block_query: Arc::clone(&social) as _, federation_admin: Arc::new(NoopFederationAdminQuery), wrapup_stats: InMemoryWrapUpStatsQuery::new(), wrapup_repo: InMemoryWrapUpRepository::new(), insights: InMemoryInsightsQuery::new(), goal_command: Arc::clone(&goals) as _, goal_query: goals as _, user_settings: InMemoryStore::new(), remote_goal: Arc::new(NoopRemoteGoalRepository), refresh_session: InMemoryStore::new(), federated_profile: None, } } /// A lazily-connected in-memory sqlite pool — never actually opened in this test, /// just a value to satisfy `DatabaseOutput::db_pool`. fn test_db_pool() -> crate::DbPool { let pool = sqlx::sqlite::SqlitePoolOptions::new() .connect_lazy("sqlite::memory:") .expect("lazy sqlite pool"); crate::DbPool::Sqlite(pool) } fn test_database_output() -> DatabaseOutput { let movies = InMemoryStore::new(); let watch_events = Arc::clone(&movies); let goals = Arc::clone(&movies); let user_settings = Arc::clone(&movies); DatabaseOutput { movie_command: Arc::clone(&movies) as _, movie_query: movies as _, review: InMemoryStore::new(), diary: domain::testing::FakeDiaryQuery::new(), stats: FakeStatsRepository::new(), user: InMemoryStore::new(), import_session: InMemoryStore::new(), import_profile: InMemoryStore::new(), movie_profile: InMemoryStore::new(), watchlist: InMemoryStore::new(), watch_event_command: Arc::clone(&watch_events) as _, watch_event_query: watch_events as _, webhook_token: InMemoryStore::new(), person_command: Arc::new(PanicPersonCommand), person_query: Arc::new(FakePersonQuery), search_port: Arc::new(FakeSearchPort), search_command: Arc::new(FakeSearchCommand), profile_fields: InMemoryStore::new(), ap_content: Arc::new(NoopLocalApContentQuery), wrapup_stats: InMemoryWrapUpStatsQuery::new(), wrapup_repo: InMemoryWrapUpRepository::new(), insights: InMemoryInsightsQuery::new(), goal_command: Arc::clone(&goals) as _, goal_query: goals as _, user_settings: Arc::clone(&user_settings) as _, federation_settings: user_settings as _, remote_goal: Arc::new(NoopRemoteGoalRepository), refresh_session: InMemoryStore::new(), deduplicator: Arc::new(NoopMovieDeduplicator), image_ref_command: Arc::new(NoopImageRefCommand), image_ref_query: Arc::new(NoopImageRefQuery), follow_command: Arc::new(NoopFollowCommand), follow_query: Arc::new(NoopFollowQuery), db_pool: test_db_pool(), } } fn test_services() -> application::Services { application::Services { auth: Arc::new(FakeAuthService), password_hasher: Arc::new(FakePasswordHasher), metadata: Arc::new(FakeMetadataClient), poster_fetcher: Arc::new(FakePosterFetcher), object_storage: Arc::new(NoopObjectStorage), event_publisher: NoopEventPublisher::new(), diary_exporter: Arc::new(PanicDiaryExporter), document_parser: Arc::new(FakeDocumentParser), review_logger: Arc::new(NoopReviewLogger), person_enrichment: None, } } fn test_worker_services() -> application::WorkerServices { application::WorkerServices { object_storage: Arc::new(NoopObjectStorage), event_publisher: NoopEventPublisher::new(), person_enrichment: None, } } fn test_config() -> AppConfig { AppConfig { allow_registration: true, base_url: "http://localhost:3000".into(), rate_limit: 20, refresh_ttl_seconds: 2_592_000, secure_cookies: false, cors_origins: String::new(), wrapup: application::config::WrapUpConfig { font_path: None, logo_path: None, bg_dir: None, }, } } /// The composition root must produce a Deps whose narrow structs point at the /// same Arcs it was given. Proven by pointer identity on one representative /// field per group boundary — if wiring is crossed, these differ. #[test] fn build_deps_wires_the_given_arcs() { let repos = test_repositories(); let services = test_services(); let config = test_config(); let instance = domain::value_objects::InstanceIdentity::new("https://md.test"); let deps = build_deps(&repos, &services, &config, &instance); assert!( std::sync::Arc::ptr_eq(&deps.users.get_local_profile.user, &repos.user), "get_local_profile.user must be the repositories' user repo" ); assert!( std::sync::Arc::ptr_eq(&deps.social.query.follow_graph, &repos.follow_graph), "social.query.follow_graph must be the repositories' follow_graph" ); assert_eq!(deps.users.get_local_profile.instance, instance); assert!( std::sync::Arc::ptr_eq( &deps.social.command.event_publisher, &services.event_publisher ), "social.command.event_publisher must be the services' event_publisher" ); } // The test above covers one representative field per group boundary. The tests below // give every one of the 11 groups its own coverage: at least one `Arc::ptr_eq` per // struct in the group, favoring fields most likely to get silently crossed in a future // edit — same-prefixed fields reused across sibling structs in the group (e.g. // `wrapup_repo`/`wrapup_stats`, `watch_event_command`/`watch_event_query`), and the two // name-mismatched `social_query` fields that read like they should come from a // `social_query` repo but must come from `repos.follow_graph`. #[test] fn auth_group_wiring() { let repos = test_repositories(); let services = test_services(); let config = test_config(); let instance = domain::value_objects::InstanceIdentity::new("https://md.test"); let deps = build_deps(&repos, &services, &config, &instance); assert!(Arc::ptr_eq(&deps.auth.login.user, &repos.user)); assert!(Arc::ptr_eq(&deps.auth.login.auth, &services.auth)); assert!(Arc::ptr_eq( &deps.auth.login.refresh_session, &repos.refresh_session )); assert!(Arc::ptr_eq( &deps.auth.register.password_hasher, &services.password_hasher )); // login and refresh both carry refresh_session + auth — confirm the second struct // gets the same repo/service Arcs independently, not a stray clone of something else. assert!(Arc::ptr_eq( &deps.auth.refresh.refresh_session, &repos.refresh_session )); assert!(Arc::ptr_eq(&deps.auth.refresh.auth, &services.auth)); assert!(Arc::ptr_eq(&deps.auth.register_and_login.user, &repos.user)); assert_eq!(deps.auth.login.config.base_url, config.base_url); // logout shares refresh_session with login/refresh — confirm it's wired // independently to the same repo Arc. assert!(Arc::ptr_eq( &deps.auth.logout.refresh_session, &repos.refresh_session )); } #[test] fn diary_group_wiring() { let repos = test_repositories(); let services = test_services(); let config = test_config(); let instance = domain::value_objects::InstanceIdentity::new("https://md.test"); let deps = build_deps(&repos, &services, &config, &instance); assert!(Arc::ptr_eq(&deps.diary.delete_review.review, &repos.review)); assert!(Arc::ptr_eq(&deps.diary.delete_review.diary, &repos.diary)); assert!(Arc::ptr_eq( &deps.diary.delete_review.movie_command, &repos.movie_command )); assert!(Arc::ptr_eq( &deps.diary.delete_review.event_publisher, &services.event_publisher )); // edit_review also carries `review` — confirm it's independently wired, not left // pointing at delete_review's clone by accident. assert!(Arc::ptr_eq(&deps.diary.edit_review.review, &repos.review)); assert!(Arc::ptr_eq( &deps.diary.get_movie_social_page.movie_query, &repos.movie_query )); assert!(Arc::ptr_eq( &deps.diary.get_movie_social_page.movie_profile, &repos.movie_profile )); // Name mismatch: the field is called `social_query` but there is no // `repos.social_query` — it must come from `repos.follow_graph`. assert!(Arc::ptr_eq( &deps.diary.get_activity_feed.social_query, &repos.follow_graph )); assert_eq!( deps.diary.get_activity_feed.config.base_url, config.base_url ); assert!(Arc::ptr_eq(&deps.diary.get_user_feed.user, &repos.user)); assert!(Arc::ptr_eq(&deps.diary.get_user_feed.diary, &repos.diary)); assert!(Arc::ptr_eq(&deps.diary.get_diary.diary, &repos.diary)); assert!(Arc::ptr_eq( &deps.diary.get_review_history.diary, &repos.diary )); assert!(Arc::ptr_eq(&deps.diary.export_diary.diary, &repos.diary)); assert!(Arc::ptr_eq( &deps.diary.export_diary.diary_exporter, &services.diary_exporter )); } #[test] fn goals_group_wiring() { let repos = test_repositories(); let services = test_services(); let config = test_config(); let instance = domain::value_objects::InstanceIdentity::new("https://md.test"); let deps = build_deps(&repos, &services, &config, &instance); assert!(Arc::ptr_eq( &deps.goals.command.goal_command, &repos.goal_command )); assert!(Arc::ptr_eq( &deps.goals.command.event_publisher, &services.event_publisher )); // goal_query and stats each appear in both command and query — confirm both // structs land on the same underlying repo Arc rather than diverging. assert!(Arc::ptr_eq( &deps.goals.command.goal_query, &repos.goal_query )); assert!(Arc::ptr_eq(&deps.goals.query.goal_query, &repos.goal_query)); assert!(Arc::ptr_eq(&deps.goals.command.stats, &repos.stats)); assert!(Arc::ptr_eq(&deps.goals.query.stats, &repos.stats)); } #[test] fn import_group_wiring() { let repos = test_repositories(); let services = test_services(); let config = test_config(); let instance = domain::value_objects::InstanceIdentity::new("https://md.test"); let deps = build_deps(&repos, &services, &config, &instance); // document_parser (a service) is shared by create_session and apply_mapping — // confirm both point at the same Arc, not two independently-sourced ones. assert!(Arc::ptr_eq( &deps.import.create_session.document_parser, &services.document_parser )); assert!(Arc::ptr_eq( &deps.import.apply_mapping.document_parser, &services.document_parser )); assert!(Arc::ptr_eq( &deps.import.apply_mapping.movie_query, &repos.movie_query )); // import_session/import_profile recur across apply_profile and save_profile — // confirm both fields land correctly in both structs. assert!(Arc::ptr_eq( &deps.import.apply_profile.import_profile, &repos.import_profile )); assert!(Arc::ptr_eq( &deps.import.apply_profile.import_session, &repos.import_session )); assert!(Arc::ptr_eq( &deps.import.save_profile.import_session, &repos.import_session )); assert!(Arc::ptr_eq( &deps.import.save_profile.import_profile, &repos.import_profile )); assert!(Arc::ptr_eq( &deps.import.execute_import.review_logger, &services.review_logger )); // The four stage-gating/orchestration use cases added alongside the // handler-bypass removal — each field asserted individually since several // recur across siblings above (import_session, movie_query, document_parser). assert!(Arc::ptr_eq( &deps.import.get_mapping_stage.import_session, &repos.import_session )); assert!(Arc::ptr_eq( &deps.import.get_preview_stage.import_session, &repos.import_session )); assert!(Arc::ptr_eq( &deps.import.get_session_state.import_session, &repos.import_session )); assert!(Arc::ptr_eq( &deps.import.apply_profile_and_map.import_profile, &repos.import_profile )); assert!(Arc::ptr_eq( &deps.import.apply_profile_and_map.import_session, &repos.import_session )); assert!(Arc::ptr_eq( &deps.import.apply_profile_and_map.document_parser, &services.document_parser )); assert!(Arc::ptr_eq( &deps.import.apply_profile_and_map.movie_query, &repos.movie_query )); // import_profile recurs again here — confirm both new use cases are wired // independently rather than sharing apply_profile's/save_profile's clone. assert!(Arc::ptr_eq( &deps.import.delete_profile.import_profile, &repos.import_profile )); assert!(Arc::ptr_eq( &deps.import.list_profiles.import_profile, &repos.import_profile )); } #[test] fn integrations_group_wiring() { let repos = test_repositories(); let services = test_services(); let config = test_config(); let instance = domain::value_objects::InstanceIdentity::new("https://md.test"); let deps = build_deps(&repos, &services, &config, &instance); // watch_event_command and watch_event_query are the classic same-prefix pair most // at risk of being swapped by a future reader. assert!(Arc::ptr_eq( &deps.integrations.ingest_watch_event.watch_event_command, &repos.watch_event_command )); assert!(Arc::ptr_eq( &deps.integrations.ingest_watch_event.watch_event_query, &repos.watch_event_query )); assert!(Arc::ptr_eq( &deps.integrations.ingest_watch_event.webhook_token, &repos.webhook_token )); assert!(Arc::ptr_eq( &deps.integrations.ingest_watch_event.event_publisher, &services.event_publisher )); assert!(Arc::ptr_eq( &deps.integrations.confirm_watch_events.watch_event_command, &repos.watch_event_command )); assert!(Arc::ptr_eq( &deps.integrations.confirm_watch_events.watch_event_query, &repos.watch_event_query )); assert!(Arc::ptr_eq( &deps.integrations.confirm_watch_events.review_logger, &services.review_logger )); assert!(Arc::ptr_eq( &deps.integrations.dismiss_watch_events.watch_event_command, &repos.watch_event_command )); assert!(Arc::ptr_eq( &deps.integrations.dismiss_watch_events.watch_event_query, &repos.watch_event_query )); assert!(Arc::ptr_eq( &deps.integrations.generate_webhook_token.webhook_token, &repos.webhook_token )); assert!(Arc::ptr_eq( &deps.integrations.get_watch_queue.watch_event_query, &repos.watch_event_query )); assert!(Arc::ptr_eq( &deps.integrations.get_webhook_tokens.webhook_token, &repos.webhook_token )); assert!(Arc::ptr_eq( &deps.integrations.revoke_webhook_token.webhook_token, &repos.webhook_token )); } /// MoviesGroup was the reviewer's worked example of the risk this suite closes: 8 /// same-shaped `.clone()` calls on one struct where a crossed pair would still compile. /// Every field of `sync_poster` gets its own assertion for that reason. #[test] fn movies_group_wiring() { let repos = test_repositories(); let services = test_services(); let config = test_config(); let instance = domain::value_objects::InstanceIdentity::new("https://md.test"); let deps = build_deps(&repos, &services, &config, &instance); assert!(Arc::ptr_eq( &deps.movies.sync_poster.movie_command, &repos.movie_command )); assert!(Arc::ptr_eq( &deps.movies.sync_poster.movie_query, &repos.movie_query )); assert!(Arc::ptr_eq( &deps.movies.sync_poster.movie_profile, &repos.movie_profile )); assert!(Arc::ptr_eq( &deps.movies.sync_poster.metadata, &services.metadata )); assert!(Arc::ptr_eq( &deps.movies.sync_poster.poster_fetcher, &services.poster_fetcher )); assert!(Arc::ptr_eq( &deps.movies.sync_poster.object_storage, &services.object_storage )); assert!(Arc::ptr_eq( &deps.movies.sync_poster.event_publisher, &services.event_publisher )); assert!(Arc::ptr_eq( &deps.movies.sync_poster.search_command, &repos.search_command )); // movie_profile/movie_query also recur in sync_poster above — confirm these two // use cases are wired independently rather than reusing sync_poster's clone. assert!(Arc::ptr_eq( &deps.movies.get_movie_profile.movie_profile, &repos.movie_profile )); assert!(Arc::ptr_eq( &deps.movies.get_movies.movie, &repos.movie_query )); } #[test] fn person_group_wiring() { let repos = test_repositories(); let services = test_services(); let config = test_config(); let instance = domain::value_objects::InstanceIdentity::new("https://md.test"); let deps = build_deps(&repos, &services, &config, &instance); assert!(Arc::ptr_eq( &deps.person.get_person.person_query, &repos.person_query )); assert!(Arc::ptr_eq( &deps.person.get_person.event_publisher, &services.event_publisher )); } #[test] fn search_group_wiring() { let repos = test_repositories(); let services = test_services(); let config = test_config(); let instance = domain::value_objects::InstanceIdentity::new("https://md.test"); let deps = build_deps(&repos, &services, &config, &instance); assert!(Arc::ptr_eq( &deps.search.execute.search_port, &repos.search_port )); } #[test] fn social_group_wiring() { let repos = test_repositories(); let services = test_services(); let config = test_config(); let instance = domain::value_objects::InstanceIdentity::new("https://md.test"); let deps = build_deps(&repos, &services, &config, &instance); assert!(Arc::ptr_eq( &deps.social.command.social_command, &repos.social_command )); assert!(Arc::ptr_eq( &deps.social.command.event_publisher, &services.event_publisher )); assert!(Arc::ptr_eq( &deps.social.query.follow_graph, &repos.follow_graph )); assert!(Arc::ptr_eq( &deps.social.query.block_query, &repos.block_query )); } #[test] fn users_group_wiring() { let repos = test_repositories(); let services = test_services(); let config = test_config(); let instance = domain::value_objects::InstanceIdentity::new("https://md.test"); let deps = build_deps(&repos, &services, &config, &instance); assert!(Arc::ptr_eq( &deps.users.get_local_profile.stats, &repos.stats )); assert!(Arc::ptr_eq( &deps.users.get_local_profile.diary, &repos.diary )); // Name mismatch: the field is called `social_query` but must come from // `repos.follow_graph`, same as diary::get_activity_feed above. assert!(Arc::ptr_eq( &deps.users.get_local_profile.social_query, &repos.follow_graph )); assert_eq!(deps.users.get_local_profile.instance, instance); assert!(Arc::ptr_eq( &deps.users.get_federated_profile_stats.stats, &repos.stats )); assert!(Arc::ptr_eq( &deps.users.get_federated_profile_stats.diary, &repos.diary )); assert!(Arc::ptr_eq( &deps.users.get_federated_profile_stats.social_query, &repos.follow_graph )); assert!(Arc::ptr_eq(&deps.users.get_page_viewer.user, &repos.user)); assert!(Arc::ptr_eq( &deps.users.get_page_viewer.follow_graph, &repos.follow_graph )); assert!(Arc::ptr_eq(&deps.users.resolve_username.user, &repos.user)); assert!(Arc::ptr_eq( &deps.users.get_profile_settings.user, &repos.user )); assert!(Arc::ptr_eq( &deps.users.get_profile_settings.profile_fields, &repos.profile_fields )); assert_eq!(deps.users.get_profile_settings.instance, instance); // `user` recurs in several structs of this group — confirm each is wired // independently rather than one struct silently sharing another's clone call. assert!(Arc::ptr_eq(&deps.users.get_local_profile.user, &repos.user)); assert!(Arc::ptr_eq(&deps.users.get_users_list.user, &repos.user)); assert!(Arc::ptr_eq(&deps.users.update_profile.user, &repos.user)); assert!(Arc::ptr_eq(&deps.users.delete_account.user, &repos.user)); assert!(Arc::ptr_eq( &deps.users.get_users_list.federation_admin, &repos.federation_admin )); assert!(Arc::ptr_eq( &deps.users.update_profile.object_storage, &services.object_storage )); // event_publisher recurs in update_profile and delete_account — confirm both. assert!(Arc::ptr_eq( &deps.users.update_profile.event_publisher, &services.event_publisher )); assert!(Arc::ptr_eq( &deps.users.delete_account.event_publisher, &services.event_publisher )); // `user` recurs again here — confirm get_current_profile is independently wired. assert!(Arc::ptr_eq( &deps.users.get_current_profile.user, &repos.user )); assert!(Arc::ptr_eq( &deps.users.update_profile_fields.profile_fields, &repos.profile_fields )); assert!(Arc::ptr_eq( &deps.users.update_profile_fields.event_publisher, &services.event_publisher )); // user_settings recurs across get_settings and update_settings — confirm both // land on the same repo Arc independently. assert!(Arc::ptr_eq( &deps.users.get_settings.user_settings, &repos.user_settings )); assert!(Arc::ptr_eq( &deps.users.update_settings.user_settings, &repos.user_settings )); assert!(Arc::ptr_eq(&deps.users.authorize_admin.user, &repos.user)); // federated_profile is genuinely optional configuration (federation on/off), // the same exemption as WorkerServices::person_enrichment (see // worker_deps_wiring below) — the fixture leaves it None, so pointer // identity doesn't apply; confirm it passes through unchanged instead. assert_eq!( deps.users.get_federated_profile.federated_profile.is_none(), repos.federated_profile.is_none(), "the optional federated_profile port must pass through as given" ); } #[test] fn watchlist_group_wiring() { let repos = test_repositories(); let services = test_services(); let config = test_config(); let instance = domain::value_objects::InstanceIdentity::new("https://md.test"); let deps = build_deps(&repos, &services, &config, &instance); assert!(Arc::ptr_eq( &deps.watchlist.add.movie_command, &repos.movie_command )); assert!(Arc::ptr_eq( &deps.watchlist.add.movie_query, &repos.movie_query )); assert!(Arc::ptr_eq( &deps.watchlist.add.metadata, &services.metadata )); assert!(Arc::ptr_eq(&deps.watchlist.add.watchlist, &repos.watchlist)); assert!(Arc::ptr_eq( &deps.watchlist.add.event_publisher, &services.event_publisher )); assert!(Arc::ptr_eq( &deps.watchlist.get_watchlist_for_owner.user, &repos.user )); // `watchlist` also appears in `add` — confirm `get_watchlist_for_owner` is // independently wired to the same repo Arc, not a stray clone. assert!(Arc::ptr_eq( &deps.watchlist.get_watchlist_for_owner.watchlist, &repos.watchlist )); assert!(Arc::ptr_eq( &deps.watchlist.get_watchlist_for_owner.remote_watchlist, &repos.remote_watchlist )); assert!(Arc::ptr_eq( &deps.watchlist.get_watchlist.watchlist, &repos.watchlist )); assert!(Arc::ptr_eq( &deps.watchlist.is_on_watchlist.watchlist, &repos.watchlist )); assert!(Arc::ptr_eq( &deps.watchlist.remove_from_watchlist.watchlist, &repos.watchlist )); assert!(Arc::ptr_eq( &deps.watchlist.remove_from_watchlist.event_publisher, &services.event_publisher )); } #[test] fn wrapup_group_wiring() { let repos = test_repositories(); let services = test_services(); let config = test_config(); let instance = domain::value_objects::InstanceIdentity::new("https://md.test"); let deps = build_deps(&repos, &services, &config, &instance); assert!(Arc::ptr_eq( &deps.wrapup.get_ready_report.wrapup_repo, &repos.wrapup_repo )); assert!(Arc::ptr_eq( &deps.wrapup.delete_wrapup.wrapup_repo, &repos.wrapup_repo )); assert!(Arc::ptr_eq( &deps.wrapup.generate.wrapup_repo, &repos.wrapup_repo )); assert!(Arc::ptr_eq( &deps.wrapup.generate.event_publisher, &services.event_publisher )); assert!(Arc::ptr_eq( &deps.wrapup.get_wrapup.wrapup_repo, &repos.wrapup_repo )); assert!(Arc::ptr_eq( &deps.wrapup.list_wrapups.wrapup_repo, &repos.wrapup_repo )); } /// Covers the five groups that moved to `WorkerDeps` during worker unification: /// `enrich_movie`, `reindex_search`, `merge_duplicates`, `enrich_person`, /// `handle_requested`. Assertions here are the ones that used to live in /// `movies_group_wiring`, `person_group_wiring`, and `wrapup_group_wiring` against /// `build_deps` — they now drive `build_worker_deps` against `WorkerServices` instead. #[tokio::test] async fn worker_deps_wiring() { let db = test_database_output(); let services = test_worker_services(); let worker_deps = build_worker_deps(&db, &services); // enrich_movie and reindex_search share movie_query/movie_profile/search_command // with each other — confirm each struct is wired independently rather than // reusing a stray clone from a sibling struct. assert!(Arc::ptr_eq( &worker_deps.enrich_movie.movie_query, &db.movie_query )); assert!(Arc::ptr_eq( &worker_deps.enrich_movie.movie_profile, &db.movie_profile )); assert!(Arc::ptr_eq( &worker_deps.enrich_movie.person_command, &db.person_command )); assert!(Arc::ptr_eq( &worker_deps.enrich_movie.search_command, &db.search_command )); assert!(Arc::ptr_eq( &worker_deps.reindex_search.movie_query, &db.movie_query )); assert!(Arc::ptr_eq( &worker_deps.reindex_search.movie_profile, &db.movie_profile )); assert!(Arc::ptr_eq( &worker_deps.reindex_search.search_command, &db.search_command )); assert!(Arc::ptr_eq( &worker_deps.reindex_search.person_command, &db.person_command )); assert!(Arc::ptr_eq( &worker_deps.reindex_search.person_query, &db.person_query )); assert!(Arc::ptr_eq( &worker_deps.merge_duplicates.movie_query, &db.movie_query )); assert!(Arc::ptr_eq( &worker_deps.merge_duplicates.object_storage, &services.object_storage )); assert!( Arc::ptr_eq(&worker_deps.merge_duplicates.deduplicator, &db.deduplicator), "merge_duplicates must carry the DatabaseOutput deduplicator — this field was \ unconstructible before worker unification" ); assert!(Arc::ptr_eq( &worker_deps.enrich_person.person_query, &db.person_query )); assert!(Arc::ptr_eq( &worker_deps.enrich_person.person_command, &db.person_command )); assert_eq!( worker_deps.enrich_person.person_enrichment.is_none(), services.person_enrichment.is_none(), "the optional person_enrichment port must pass through as given" ); // wrapup_repo and wrapup_stats are the same-prefix pair most at risk of a swap; // both are different trait types, but a future reader "correcting" a name could // still reach for the wrong one. assert!(Arc::ptr_eq( &worker_deps.handle_requested.wrapup_repo, &db.wrapup_repo )); assert!(Arc::ptr_eq( &worker_deps.handle_requested.wrapup_stats, &db.wrapup_stats )); assert!(Arc::ptr_eq( &worker_deps.handle_requested.event_publisher, &services.event_publisher )); }