Files
movies-diary/crates/bootstrap/src/tests/build.rs

1075 lines
35 KiB
Rust

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<u64, domain::errors::DomainError> {
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<Vec<domain::models::WatchlistWithMovie>, domain::errors::DomainError> {
Ok(vec![])
}
async fn get_local_reviews_for_movie(
&self,
_movie_id: &domain::value_objects::MovieId,
) -> Result<Vec<domain::models::DiaryEntry>, domain::errors::DomainError> {
Ok(vec![])
}
async fn get_local_reviews_page(
&self,
_user_id: &domain::value_objects::UserId,
_before: Option<chrono::NaiveDateTime>,
_limit: usize,
) -> Result<Vec<domain::models::DiaryEntry>, 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<Vec<String>, 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<Vec<domain::value_objects::SocialActor>, domain::errors::DomainError> {
Ok(vec![])
}
async fn get_followers(
&self,
_user_id: uuid::Uuid,
) -> Result<Vec<domain::value_objects::SocialActor>, domain::errors::DomainError> {
Ok(vec![])
}
async fn get_pending_followers(
&self,
_user_id: uuid::Uuid,
) -> Result<Vec<domain::value_objects::SocialActor>, domain::errors::DomainError> {
Ok(vec![])
}
async fn get_pending_following(
&self,
_user_id: uuid::Uuid,
) -> Result<Vec<domain::value_objects::SocialActor>, domain::errors::DomainError> {
Ok(vec![])
}
async fn count_following(
&self,
_user_id: uuid::Uuid,
) -> Result<usize, domain::errors::DomainError> {
Ok(0)
}
async fn count_followers(
&self,
_user_id: uuid::Uuid,
) -> Result<usize, domain::errors::DomainError> {
Ok(0)
}
async fn count_pending_followers(
&self,
_user_id: uuid::Uuid,
) -> Result<usize, domain::errors::DomainError> {
Ok(0)
}
async fn get_relation(
&self,
_viewer_id: uuid::Uuid,
_target_actor_url: &str,
) -> Result<domain::value_objects::FollowRelation, domain::errors::DomainError> {
// 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
));
}