test: move http-axum tests to tests/
Drop bootstrap dev-dep. Extractor + api handler + app-context tests (need full AppState via build_deps) -> bootstrap/tests/http_handlers/. forms tests -> http-axum/tests/log_review_form_test.rs. Federation collection tests now go through public federation::html_routes(). Kept inline (private fns): api/social social_actor_to_dto (4), federation/blocklist_api social_actor_to_blocked_dto (1), html/social social_actor_to_template (1), router slow_down/api_governor (2).
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -2421,7 +2421,6 @@ dependencies = [
|
|||||||
"async-trait",
|
"async-trait",
|
||||||
"axum",
|
"axum",
|
||||||
"axum-governor",
|
"axum-governor",
|
||||||
"bootstrap",
|
|
||||||
"bytes",
|
"bytes",
|
||||||
"chrono",
|
"chrono",
|
||||||
"chrono-tz",
|
"chrono-tz",
|
||||||
|
|||||||
@@ -43,4 +43,3 @@ bytes = { workspace = true }
|
|||||||
futures = { workspace = true }
|
futures = { workspace = true }
|
||||||
tower = { version = "0.5", features = ["util"] }
|
tower = { version = "0.5", features = ["util"] }
|
||||||
domain = { workspace = true, features = ["test-helpers"] }
|
domain = { workspace = true, features = ["test-helpers"] }
|
||||||
bootstrap = { workspace = true }
|
|
||||||
|
|||||||
@@ -156,6 +156,3 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) mod tests;
|
|
||||||
|
|||||||
@@ -310,7 +310,3 @@ pub struct DismissWatchForm {
|
|||||||
#[serde(rename = "_csrf", default)]
|
#[serde(rename = "_csrf", default)]
|
||||||
pub csrf_token: String,
|
pub csrf_token: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[path = "forms_tests.rs"]
|
|
||||||
mod tests;
|
|
||||||
|
|||||||
@@ -8,6 +8,3 @@ pub mod openapi;
|
|||||||
pub mod router;
|
pub mod router;
|
||||||
pub mod state;
|
pub mod state;
|
||||||
mod support;
|
mod support;
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests;
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use super::*;
|
use api_types::{DiaryQueryParams, LogReviewRequest};
|
||||||
|
use http_axum::html::forms::{LogReviewData, LogReviewForm, to_diary_query};
|
||||||
|
|
||||||
fn make_form(watched_at: &str) -> LogReviewForm {
|
fn make_form(watched_at: &str) -> LogReviewForm {
|
||||||
LogReviewForm {
|
LogReviewForm {
|
||||||
|
|||||||
@@ -46,3 +46,4 @@ chrono = { workspace = true }
|
|||||||
uuid = { workspace = true }
|
uuid = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
tower = { version = "0.5", features = ["util"] }
|
tower = { version = "0.5", features = ["util"] }
|
||||||
|
domain = { workspace = true, features = ["test-helpers"] }
|
||||||
|
|||||||
9
crates/bootstrap/tests/http_handlers.rs
Normal file
9
crates/bootstrap/tests/http_handlers.rs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
#[path = "http_handlers/support.rs"]
|
||||||
|
mod support;
|
||||||
|
|
||||||
|
#[path = "http_handlers/api_handlers_test.rs"]
|
||||||
|
mod api_handlers_test;
|
||||||
|
#[path = "http_handlers/app_context_test.rs"]
|
||||||
|
mod app_context_test;
|
||||||
|
#[path = "http_handlers/extractors_test.rs"]
|
||||||
|
mod extractors_test;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::extractors::tests::{AcceptingAuth, test_app_state, test_app_state_with_follow_graph};
|
use crate::support::{AcceptingAuth, test_app_state, test_app_state_with_follow_graph};
|
||||||
use axum::{
|
use axum::{
|
||||||
Router,
|
Router,
|
||||||
body::Body,
|
body::Body,
|
||||||
@@ -73,9 +73,9 @@ impl domain::ports::PersonQuery for PersonQueryStub {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn search_endpoint_returns_200_with_empty_results() {
|
async fn search_endpoint_returns_200_with_empty_results() {
|
||||||
let state = crate::extractors::tests::test_app_state_with_search_port(Arc::new(SearchPortStub));
|
let state = crate::support::test_app_state_with_search_port(Arc::new(SearchPortStub));
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.route("/api/v1/search", get(crate::api::search::get_search))
|
.route("/api/v1/search", get(http_axum::api::search::get_search))
|
||||||
.with_state(state);
|
.with_state(state);
|
||||||
|
|
||||||
let resp = app
|
let resp = app
|
||||||
@@ -93,9 +93,9 @@ async fn search_endpoint_returns_200_with_empty_results() {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn search_endpoint_with_no_query_returns_200() {
|
async fn search_endpoint_with_no_query_returns_200() {
|
||||||
let state = crate::extractors::tests::test_app_state_with_search_port(Arc::new(SearchPortStub));
|
let state = crate::support::test_app_state_with_search_port(Arc::new(SearchPortStub));
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.route("/api/v1/search", get(crate::api::search::get_search))
|
.route("/api/v1/search", get(http_axum::api::search::get_search))
|
||||||
.with_state(state);
|
.with_state(state);
|
||||||
|
|
||||||
let resp = app
|
let resp = app
|
||||||
@@ -115,12 +115,11 @@ async fn search_endpoint_with_no_query_returns_200() {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn person_endpoint_returns_404_for_unknown_id() {
|
async fn person_endpoint_returns_404_for_unknown_id() {
|
||||||
let state =
|
let state = crate::support::test_app_state_with_person_query(Arc::new(PersonQueryStub));
|
||||||
crate::extractors::tests::test_app_state_with_person_query(Arc::new(PersonQueryStub));
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.route(
|
.route(
|
||||||
"/api/v1/people/{id}",
|
"/api/v1/people/{id}",
|
||||||
get(crate::api::search::get_person_handler),
|
get(http_axum::api::search::get_person_handler),
|
||||||
)
|
)
|
||||||
.with_state(state);
|
.with_state(state);
|
||||||
|
|
||||||
@@ -140,12 +139,11 @@ async fn person_endpoint_returns_404_for_unknown_id() {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn person_credits_endpoint_returns_404_for_unknown_id() {
|
async fn person_credits_endpoint_returns_404_for_unknown_id() {
|
||||||
let state =
|
let state = crate::support::test_app_state_with_person_query(Arc::new(PersonQueryStub));
|
||||||
crate::extractors::tests::test_app_state_with_person_query(Arc::new(PersonQueryStub));
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.route(
|
.route(
|
||||||
"/api/v1/people/{id}/credits",
|
"/api/v1/people/{id}/credits",
|
||||||
get(crate::api::search::get_person_credits_handler),
|
get(http_axum::api::search::get_person_credits_handler),
|
||||||
)
|
)
|
||||||
.with_state(state);
|
.with_state(state);
|
||||||
|
|
||||||
@@ -166,7 +164,7 @@ async fn person_credits_endpoint_returns_404_for_unknown_id() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn injected_stub_reaches_prebuilt_deps() {
|
fn injected_stub_reaches_prebuilt_deps() {
|
||||||
let stub: Arc<dyn domain::ports::PersonQuery> = Arc::new(PersonQueryStub);
|
let stub: Arc<dyn domain::ports::PersonQuery> = Arc::new(PersonQueryStub);
|
||||||
let state = crate::extractors::tests::test_app_state_with_person_query(Arc::clone(&stub));
|
let state = crate::support::test_app_state_with_person_query(Arc::clone(&stub));
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
Arc::ptr_eq(&state.app_ctx.deps.person.get_person.person_query, &stub),
|
Arc::ptr_eq(&state.app_ctx.deps.person.get_person.person_query, &stub),
|
||||||
@@ -183,7 +181,7 @@ async fn get_watchlist_requires_auth() {
|
|||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.route(
|
.route(
|
||||||
"/api/v1/watchlist",
|
"/api/v1/watchlist",
|
||||||
get(crate::api::watchlist::get_watchlist_handler),
|
get(http_axum::api::watchlist::get_watchlist_handler),
|
||||||
)
|
)
|
||||||
.with_state(state);
|
.with_state(state);
|
||||||
|
|
||||||
@@ -266,7 +264,7 @@ async fn pending_count_endpoint_reports_the_use_case_result() {
|
|||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.route(
|
.route(
|
||||||
"/api/v1/social/followers/pending/count",
|
"/api/v1/social/followers/pending/count",
|
||||||
get(crate::api::social::get_pending_follower_count),
|
get(http_axum::api::social::get_pending_follower_count),
|
||||||
)
|
)
|
||||||
.with_state(state);
|
.with_state(state);
|
||||||
|
|
||||||
@@ -295,7 +293,7 @@ async fn get_watchlist_status_requires_auth() {
|
|||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.route(
|
.route(
|
||||||
"/api/v1/watchlist/{movie_id}",
|
"/api/v1/watchlist/{movie_id}",
|
||||||
get(crate::api::watchlist::get_watchlist_status),
|
get(http_axum::api::watchlist::get_watchlist_status),
|
||||||
)
|
)
|
||||||
.with_state(state);
|
.with_state(state);
|
||||||
|
|
||||||
@@ -340,7 +338,7 @@ impl domain::ports::ApDocumentPort for FailingApDocument {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "federation")]
|
#[cfg(feature = "federation")]
|
||||||
fn state_with_failing_ap_document() -> crate::state::AppState {
|
fn state_with_failing_ap_document() -> http_axum::state::AppState {
|
||||||
let mut state = test_app_state();
|
let mut state = test_app_state();
|
||||||
state.app_ctx.ap_document = Arc::new(FailingApDocument);
|
state.app_ctx.ap_document = Arc::new(FailingApDocument);
|
||||||
state
|
state
|
||||||
@@ -349,12 +347,7 @@ fn state_with_failing_ap_document() -> crate::state::AppState {
|
|||||||
#[cfg(feature = "federation")]
|
#[cfg(feature = "federation")]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn followers_collection_returns_500_on_federation_error() {
|
async fn followers_collection_returns_500_on_federation_error() {
|
||||||
let app = Router::new()
|
let app = http_axum::federation::html_routes().with_state(state_with_failing_ap_document());
|
||||||
.route(
|
|
||||||
"/users/{id}/followers",
|
|
||||||
get(crate::federation::collections::get_followers_collection),
|
|
||||||
)
|
|
||||||
.with_state(state_with_failing_ap_document());
|
|
||||||
|
|
||||||
let resp = app
|
let resp = app
|
||||||
.oneshot(
|
.oneshot(
|
||||||
@@ -373,12 +366,7 @@ async fn followers_collection_returns_500_on_federation_error() {
|
|||||||
#[cfg(feature = "federation")]
|
#[cfg(feature = "federation")]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn following_collection_returns_500_on_federation_error() {
|
async fn following_collection_returns_500_on_federation_error() {
|
||||||
let app = Router::new()
|
let app = http_axum::federation::html_routes().with_state(state_with_failing_ap_document());
|
||||||
.route(
|
|
||||||
"/users/{id}/following",
|
|
||||||
get(crate::federation::collections::get_following_collection),
|
|
||||||
)
|
|
||||||
.with_state(state_with_failing_ap_document());
|
|
||||||
|
|
||||||
let resp = app
|
let resp = app
|
||||||
.oneshot(
|
.oneshot(
|
||||||
@@ -405,12 +393,11 @@ async fn refusal_body(resp: axum::response::Response) -> serde_json::Value {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn a_missing_resource_is_refused_with_not_found_code() {
|
async fn a_missing_resource_is_refused_with_not_found_code() {
|
||||||
let state =
|
let state = crate::support::test_app_state_with_person_query(Arc::new(PersonQueryStub));
|
||||||
crate::extractors::tests::test_app_state_with_person_query(Arc::new(PersonQueryStub));
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.route(
|
.route(
|
||||||
"/api/v1/people/{id}",
|
"/api/v1/people/{id}",
|
||||||
get(crate::api::search::get_person_handler),
|
get(http_axum::api::search::get_person_handler),
|
||||||
)
|
)
|
||||||
.with_state(state);
|
.with_state(state);
|
||||||
|
|
||||||
@@ -435,7 +422,7 @@ async fn a_missing_credential_is_refused_with_unauthorized_code() {
|
|||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.route(
|
.route(
|
||||||
"/api/v1/watchlist",
|
"/api/v1/watchlist",
|
||||||
get(crate::api::watchlist::get_watchlist_handler),
|
get(http_axum::api::watchlist::get_watchlist_handler),
|
||||||
)
|
)
|
||||||
.with_state(test_app_state());
|
.with_state(test_app_state());
|
||||||
|
|
||||||
@@ -455,12 +442,11 @@ async fn a_missing_credential_is_refused_with_unauthorized_code() {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn a_malformed_path_parameter_is_refused_with_invalid_input_code() {
|
async fn a_malformed_path_parameter_is_refused_with_invalid_input_code() {
|
||||||
let state =
|
let state = crate::support::test_app_state_with_person_query(Arc::new(PersonQueryStub));
|
||||||
crate::extractors::tests::test_app_state_with_person_query(Arc::new(PersonQueryStub));
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.route(
|
.route(
|
||||||
"/api/v1/people/{id}",
|
"/api/v1/people/{id}",
|
||||||
get(crate::api::search::get_person_handler),
|
get(http_axum::api::search::get_person_handler),
|
||||||
)
|
)
|
||||||
.with_state(state);
|
.with_state(state);
|
||||||
|
|
||||||
@@ -483,7 +469,7 @@ async fn a_malformed_json_body_is_refused_with_invalid_input_code() {
|
|||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.route(
|
.route(
|
||||||
"/api/v1/auth/login",
|
"/api/v1/auth/login",
|
||||||
axum::routing::post(crate::api::auth::login),
|
axum::routing::post(http_axum::api::auth::login),
|
||||||
)
|
)
|
||||||
.with_state(test_app_state());
|
.with_state(test_app_state());
|
||||||
|
|
||||||
@@ -510,7 +496,7 @@ async fn a_json_body_of_the_wrong_shape_is_refused_as_unprocessable() {
|
|||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.route(
|
.route(
|
||||||
"/api/v1/auth/login",
|
"/api/v1/auth/login",
|
||||||
axum::routing::post(crate::api::auth::login),
|
axum::routing::post(http_axum::api::auth::login),
|
||||||
)
|
)
|
||||||
.with_state(test_app_state());
|
.with_state(test_app_state());
|
||||||
|
|
||||||
@@ -535,7 +521,7 @@ async fn a_non_multipart_upload_is_refused_with_invalid_input_code() {
|
|||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.route(
|
.route(
|
||||||
"/api/v1/webhooks/plex",
|
"/api/v1/webhooks/plex",
|
||||||
axum::routing::post(crate::api::webhook::post_plex_webhook),
|
axum::routing::post(http_axum::api::webhook::post_plex_webhook),
|
||||||
)
|
)
|
||||||
.with_state(test_app_state());
|
.with_state(test_app_state());
|
||||||
|
|
||||||
|
|||||||
@@ -2,14 +2,12 @@
|
|||||||
fn prebuilt_deps_share_port_instances_with_repos() {
|
fn prebuilt_deps_share_port_instances_with_repos() {
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
let repos = crate::extractors::tests::test_repositories();
|
let repos = crate::support::test_repositories();
|
||||||
let user = Arc::clone(&repos.user);
|
let user = Arc::clone(&repos.user);
|
||||||
let follow_graph = Arc::clone(&repos.follow_graph);
|
let follow_graph = Arc::clone(&repos.follow_graph);
|
||||||
|
|
||||||
let state = crate::extractors::tests::build_test_state_from(
|
let state =
|
||||||
repos,
|
crate::support::build_test_state_from(repos, Arc::new(domain::testing::PanicAuthService));
|
||||||
Arc::new(domain::testing::PanicAuthService),
|
|
||||||
);
|
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
Arc::ptr_eq(&state.app_ctx.deps.users.get_local_profile.user, &user),
|
Arc::ptr_eq(&state.app_ctx.deps.users.get_local_profile.user, &user),
|
||||||
|
|||||||
@@ -1,37 +1,23 @@
|
|||||||
use super::*;
|
use std::sync::Arc;
|
||||||
use crate::context::AppContext;
|
|
||||||
use application::config::AppConfig;
|
|
||||||
use axum::{
|
use axum::{
|
||||||
Router,
|
Router,
|
||||||
body::Body,
|
body::Body,
|
||||||
http::{Request, StatusCode},
|
http::{Request, StatusCode},
|
||||||
routing::get,
|
routing::get,
|
||||||
};
|
};
|
||||||
use bootstrap::Repositories;
|
|
||||||
use domain::{
|
use domain::{
|
||||||
errors::DomainError,
|
errors::DomainError, models::GeneratedToken, ports::AuthService, testing::InMemoryStore,
|
||||||
models::GeneratedToken,
|
|
||||||
ports::{AuthService, FollowGraphQuery, PersonQuery, SearchPort, UserRepository},
|
|
||||||
testing::{
|
|
||||||
InMemoryStore, PanicAuthService, PanicDiaryExporter, PanicDiaryQuery, PanicDocumentParser,
|
|
||||||
PanicEventPublisher, PanicGoalCommand, PanicGoalQuery, PanicImportProfileRepository,
|
|
||||||
PanicImportSessionRepository, PanicInsightsQuery, PanicMetadataClient, PanicMovieCommand,
|
|
||||||
PanicMovieProfileRepository, PanicMovieQuery, PanicObjectStorage, PanicPasswordHasher,
|
|
||||||
PanicPersonCommand, PanicPersonQuery, PanicPosterFetcher, PanicProfileFieldsRepo,
|
|
||||||
PanicRefreshSessionRepository, PanicRemoteGoalRepository, PanicRemoteWatchlistRepository,
|
|
||||||
PanicReviewRepository, PanicRssFeedRenderer, PanicSearchCommand, PanicSearchPort,
|
|
||||||
PanicStatsRepository, PanicUserRepository, PanicUserSettingsRepository,
|
|
||||||
PanicWatchEventCommand, PanicWatchEventQuery, PanicWatchlistRepository,
|
|
||||||
PanicWebhookTokenRepository, PanicWrapUpRepository, PanicWrapUpStatsQuery,
|
|
||||||
},
|
|
||||||
value_objects::UserId,
|
value_objects::UserId,
|
||||||
};
|
};
|
||||||
use std::sync::Arc;
|
use http_axum::extractors::{
|
||||||
|
AdminApiUser, AdminUser, AuthenticatedUser, OptionalCookieUser, RequiredCookieUser,
|
||||||
|
};
|
||||||
use tower::ServiceExt;
|
use tower::ServiceExt;
|
||||||
|
|
||||||
// --- Local stubs ---
|
use crate::support::{
|
||||||
|
AcceptingAuth, ErroringUserRepo, make_test_state, test_app_state, test_app_state_with_user,
|
||||||
struct PanicReviewLogger;
|
};
|
||||||
|
|
||||||
struct RejectingAuth;
|
struct RejectingAuth;
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
@@ -44,219 +30,6 @@ impl AuthService for RejectingAuth {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
|
||||||
impl application::ports::ReviewLogger for PanicReviewLogger {
|
|
||||||
async fn log_review(
|
|
||||||
&self,
|
|
||||||
_: application::diary::commands::LogReviewCommand,
|
|
||||||
) -> Result<(), DomainError> {
|
|
||||||
panic!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Repositories factory — `Panic*` stubs everywhere ---
|
|
||||||
|
|
||||||
pub(crate) fn test_repositories() -> Repositories {
|
|
||||||
Repositories {
|
|
||||||
movie_command: Arc::new(PanicMovieCommand) as _,
|
|
||||||
movie_query: Arc::new(PanicMovieQuery) as _,
|
|
||||||
review: Arc::new(PanicReviewRepository) as _,
|
|
||||||
diary: Arc::new(PanicDiaryQuery) as _,
|
|
||||||
stats: Arc::new(PanicStatsRepository) as _,
|
|
||||||
user: Arc::new(PanicUserRepository) as _,
|
|
||||||
import_session: Arc::new(PanicImportSessionRepository) as _,
|
|
||||||
import_profile: Arc::new(PanicImportProfileRepository) as _,
|
|
||||||
movie_profile: Arc::new(PanicMovieProfileRepository) as _,
|
|
||||||
watchlist: Arc::new(PanicWatchlistRepository) as _,
|
|
||||||
watch_event_command: Arc::new(PanicWatchEventCommand) as _,
|
|
||||||
watch_event_query: Arc::new(PanicWatchEventQuery) as _,
|
|
||||||
webhook_token: Arc::new(PanicWebhookTokenRepository) as _,
|
|
||||||
profile_fields: Arc::new(PanicProfileFieldsRepo) as _,
|
|
||||||
person_command: Arc::new(PanicPersonCommand) as _,
|
|
||||||
person_query: Arc::new(PanicPersonQuery) as _,
|
|
||||||
search_port: Arc::new(PanicSearchPort) as _,
|
|
||||||
search_command: Arc::new(PanicSearchCommand) as _,
|
|
||||||
remote_watchlist: Arc::new(PanicRemoteWatchlistRepository) as _,
|
|
||||||
social_command: Arc::new(domain::ports::noop::NoopSocialCommand) as _,
|
|
||||||
follow_graph: Arc::new(domain::ports::noop::NoopSocialQuery) as _,
|
|
||||||
block_query: Arc::new(domain::ports::noop::NoopSocialQuery) as _,
|
|
||||||
federation_admin: Arc::new(domain::ports::noop::NoopFederationAdminQuery) as _,
|
|
||||||
wrapup_stats: Arc::new(PanicWrapUpStatsQuery) as _,
|
|
||||||
wrapup_repo: Arc::new(PanicWrapUpRepository) as _,
|
|
||||||
insights: Arc::new(PanicInsightsQuery) as _,
|
|
||||||
goal_command: Arc::new(PanicGoalCommand) as _,
|
|
||||||
goal_query: Arc::new(PanicGoalQuery) as _,
|
|
||||||
user_settings: Arc::new(PanicUserSettingsRepository) as _,
|
|
||||||
remote_goal: Arc::new(PanicRemoteGoalRepository) as _,
|
|
||||||
refresh_session: Arc::new(PanicRefreshSessionRepository) as _,
|
|
||||||
federated_profile: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Single state factory — builds `deps` from whatever `repos` it is given ---
|
|
||||||
|
|
||||||
/// Builds an `AppState` from caller-supplied `repos`, running `bootstrap::build_deps`
|
|
||||||
/// AFTER any overrides the caller already applied to `repos`. This is the only place
|
|
||||||
/// `deps` gets built for tests, so `repos` and `deps` can never disagree.
|
|
||||||
pub(crate) fn build_test_state_from(
|
|
||||||
repos: Repositories,
|
|
||||||
auth_service: Arc<dyn AuthService>,
|
|
||||||
) -> crate::state::AppState {
|
|
||||||
let services = application::Services {
|
|
||||||
auth: auth_service,
|
|
||||||
password_hasher: Arc::new(PanicPasswordHasher) as _,
|
|
||||||
metadata: Arc::new(PanicMetadataClient) as _,
|
|
||||||
poster_fetcher: Arc::new(PanicPosterFetcher) as _,
|
|
||||||
object_storage: Arc::new(PanicObjectStorage) as _,
|
|
||||||
event_publisher: Arc::new(PanicEventPublisher) as _,
|
|
||||||
diary_exporter: Arc::new(PanicDiaryExporter) as _,
|
|
||||||
document_parser: Arc::new(PanicDocumentParser) as _,
|
|
||||||
review_logger: Arc::new(PanicReviewLogger) as _,
|
|
||||||
person_enrichment: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let config = AppConfig {
|
|
||||||
allow_registration: false,
|
|
||||||
base_url: "http://localhost:3000".to_string(),
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
let instance = domain::value_objects::InstanceIdentity::new("http://localhost:3000");
|
|
||||||
|
|
||||||
let deps = Arc::new(bootstrap::build_deps(&repos, &services, &config, &instance));
|
|
||||||
|
|
||||||
crate::state::AppState {
|
|
||||||
app_ctx: AppContext {
|
|
||||||
deps,
|
|
||||||
services,
|
|
||||||
config,
|
|
||||||
instance,
|
|
||||||
#[cfg(feature = "federation")]
|
|
||||||
ap_document: Arc::new(domain::ports::noop::NoopApDocument),
|
|
||||||
#[cfg(feature = "federation")]
|
|
||||||
ap_blocklist: Arc::new(domain::ports::noop::NoopInstanceBlocklist),
|
|
||||||
},
|
|
||||||
rss_renderer: Arc::new(PanicRssFeedRenderer),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn make_test_state(auth_service: Arc<dyn AuthService>) -> crate::state::AppState {
|
|
||||||
build_test_state_from(test_repositories(), auth_service)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reusable default test `AppState`: `Panic*` stubs everywhere, real prebuilt `deps`.
|
|
||||||
/// Callers that need a specific stub swapped in should use one of the
|
|
||||||
/// `test_app_state_with_*` constructors below, which inject BEFORE `build_deps`
|
|
||||||
/// runs — mutating the returned `AppState` afterwards would only reach `repos`,
|
|
||||||
/// not the prebuilt `deps`.
|
|
||||||
pub(crate) fn test_app_state() -> crate::state::AppState {
|
|
||||||
make_test_state(Arc::new(PanicAuthService))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Test state with `person_query` overridden BEFORE `build_deps` runs, so the
|
|
||||||
/// stub is visible from both `app_ctx.repos.person_query` and every prebuilt
|
|
||||||
/// deps struct that carries a `person_query` (e.g. `deps.person.get_person`).
|
|
||||||
pub(crate) fn test_app_state_with_person_query(
|
|
||||||
person_query: Arc<dyn PersonQuery>,
|
|
||||||
) -> crate::state::AppState {
|
|
||||||
let mut repos = test_repositories();
|
|
||||||
repos.person_query = person_query;
|
|
||||||
build_test_state_from(repos, Arc::new(PanicAuthService))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Test state with `search_port` overridden BEFORE `build_deps` runs, so the
|
|
||||||
/// stub is visible from both `app_ctx.repos.search_port` and any prebuilt deps
|
|
||||||
/// struct that carries a `search_port`.
|
|
||||||
pub(crate) fn test_app_state_with_search_port(
|
|
||||||
search_port: Arc<dyn SearchPort>,
|
|
||||||
) -> crate::state::AppState {
|
|
||||||
let mut repos = test_repositories();
|
|
||||||
repos.search_port = search_port;
|
|
||||||
build_test_state_from(repos, Arc::new(PanicAuthService))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Test state with `follow_graph` overridden BEFORE `build_deps` runs, so the
|
|
||||||
/// stub is visible from both `app_ctx.repos.follow_graph` and the prebuilt
|
|
||||||
/// `deps.social.query` that the social handlers actually read from.
|
|
||||||
pub(crate) fn test_app_state_with_follow_graph(
|
|
||||||
follow_graph: Arc<dyn FollowGraphQuery>,
|
|
||||||
auth_service: Arc<dyn AuthService>,
|
|
||||||
) -> crate::state::AppState {
|
|
||||||
let mut repos = test_repositories();
|
|
||||||
repos.follow_graph = follow_graph;
|
|
||||||
build_test_state_from(repos, auth_service)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Test state with `user` overridden BEFORE `build_deps` runs, so the stub is
|
|
||||||
/// visible from both `app_ctx.repos.user` and `deps.users.authorize_admin.user`
|
|
||||||
/// — the field the `AdminApiUser`/`AdminUser` extractors now read through.
|
|
||||||
pub(crate) fn test_app_state_with_user(
|
|
||||||
user: Arc<dyn UserRepository>,
|
|
||||||
auth_service: Arc<dyn AuthService>,
|
|
||||||
) -> crate::state::AppState {
|
|
||||||
let mut repos = test_repositories();
|
|
||||||
repos.user = user;
|
|
||||||
build_test_state_from(repos, auth_service)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A `UserRepository` whose `find_by_id` always fails, for exercising the
|
|
||||||
/// repository-error rejection path of the admin extractors. Every other
|
|
||||||
/// method panics — the admin extractors only ever call `find_by_id`.
|
|
||||||
pub(crate) struct ErroringUserRepo;
|
|
||||||
#[async_trait::async_trait]
|
|
||||||
impl UserRepository for ErroringUserRepo {
|
|
||||||
async fn find_by_email(
|
|
||||||
&self,
|
|
||||||
_: &domain::value_objects::Email,
|
|
||||||
) -> Result<Option<domain::models::User>, DomainError> {
|
|
||||||
panic!()
|
|
||||||
}
|
|
||||||
async fn save(&self, _: &domain::models::User) -> Result<(), DomainError> {
|
|
||||||
panic!()
|
|
||||||
}
|
|
||||||
async fn find_by_id(&self, _: &UserId) -> Result<Option<domain::models::User>, DomainError> {
|
|
||||||
Err(DomainError::InfrastructureError("db down".into()))
|
|
||||||
}
|
|
||||||
async fn find_by_username(
|
|
||||||
&self,
|
|
||||||
_: &domain::value_objects::Username,
|
|
||||||
) -> Result<Option<domain::models::User>, DomainError> {
|
|
||||||
panic!()
|
|
||||||
}
|
|
||||||
async fn list_with_stats(&self) -> Result<Vec<domain::models::UserSummary>, DomainError> {
|
|
||||||
panic!()
|
|
||||||
}
|
|
||||||
async fn update_profile(
|
|
||||||
&self,
|
|
||||||
_: &UserId,
|
|
||||||
_: &domain::models::UserProfile,
|
|
||||||
) -> Result<(), DomainError> {
|
|
||||||
panic!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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.
|
|
||||||
pub(crate) struct AcceptingAuth(pub UserId);
|
|
||||||
#[async_trait::async_trait]
|
|
||||||
impl AuthService for AcceptingAuth {
|
|
||||||
async fn generate_token(&self, _: &UserId) -> Result<GeneratedToken, DomainError> {
|
|
||||||
panic!()
|
|
||||||
}
|
|
||||||
async fn validate_token(&self, _: &str) -> Result<UserId, DomainError> {
|
|
||||||
Ok(self.0.clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Routers ---
|
// --- Routers ---
|
||||||
|
|
||||||
async fn protected_handler(user: AuthenticatedUser) -> String {
|
async fn protected_handler(user: AuthenticatedUser) -> String {
|
||||||
@@ -278,27 +51,27 @@ async fn admin_cookie_handler(user: AdminUser) -> String {
|
|||||||
user.0.value().to_string()
|
user.0.value().to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn router_protected(state: crate::state::AppState) -> Router {
|
fn router_protected(state: http_axum::state::AppState) -> Router {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/protected", get(protected_handler))
|
.route("/protected", get(protected_handler))
|
||||||
.with_state(state)
|
.with_state(state)
|
||||||
}
|
}
|
||||||
fn router_optional(state: crate::state::AppState) -> Router {
|
fn router_optional(state: http_axum::state::AppState) -> Router {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/optional", get(optional_cookie_handler))
|
.route("/optional", get(optional_cookie_handler))
|
||||||
.with_state(state)
|
.with_state(state)
|
||||||
}
|
}
|
||||||
fn router_admin_api(state: crate::state::AppState) -> Router {
|
fn router_admin_api(state: http_axum::state::AppState) -> Router {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/admin-api", get(admin_api_handler))
|
.route("/admin-api", get(admin_api_handler))
|
||||||
.with_state(state)
|
.with_state(state)
|
||||||
}
|
}
|
||||||
fn router_admin_cookie(state: crate::state::AppState) -> Router {
|
fn router_admin_cookie(state: http_axum::state::AppState) -> Router {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/admin-cookie", get(admin_cookie_handler))
|
.route("/admin-cookie", get(admin_cookie_handler))
|
||||||
.with_state(state)
|
.with_state(state)
|
||||||
}
|
}
|
||||||
fn router_required(state: crate::state::AppState) -> Router {
|
fn router_required(state: http_axum::state::AppState) -> Router {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/required", get(required_cookie_handler))
|
.route("/required", get(required_cookie_handler))
|
||||||
.with_state(state)
|
.with_state(state)
|
||||||
|
|||||||
243
crates/bootstrap/tests/http_handlers/support.rs
Normal file
243
crates/bootstrap/tests/http_handlers/support.rs
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
//! `AppState` fixtures: `Panic*` stubs everywhere, real prebuilt `deps` built through
|
||||||
|
//! `bootstrap::build_deps`, with per-port overrides injected BEFORE the deps are built.
|
||||||
|
#![allow(dead_code)]
|
||||||
|
|
||||||
|
use application::config::AppConfig;
|
||||||
|
use bootstrap::Repositories;
|
||||||
|
use domain::{
|
||||||
|
errors::DomainError,
|
||||||
|
models::GeneratedToken,
|
||||||
|
ports::{AuthService, FollowGraphQuery, PersonQuery, SearchPort, UserRepository},
|
||||||
|
testing::{
|
||||||
|
PanicAuthService, PanicDiaryExporter, PanicDiaryQuery, PanicDocumentParser,
|
||||||
|
PanicEventPublisher, PanicGoalCommand, PanicGoalQuery, PanicImportProfileRepository,
|
||||||
|
PanicImportSessionRepository, PanicInsightsQuery, PanicMetadataClient, PanicMovieCommand,
|
||||||
|
PanicMovieProfileRepository, PanicMovieQuery, PanicObjectStorage, PanicPasswordHasher,
|
||||||
|
PanicPersonCommand, PanicPersonQuery, PanicPosterFetcher, PanicProfileFieldsRepo,
|
||||||
|
PanicRefreshSessionRepository, PanicRemoteGoalRepository, PanicRemoteWatchlistRepository,
|
||||||
|
PanicReviewRepository, PanicRssFeedRenderer, PanicSearchCommand, PanicSearchPort,
|
||||||
|
PanicStatsRepository, PanicUserRepository, PanicUserSettingsRepository,
|
||||||
|
PanicWatchEventCommand, PanicWatchEventQuery, PanicWatchlistRepository,
|
||||||
|
PanicWebhookTokenRepository, PanicWrapUpRepository, PanicWrapUpStatsQuery,
|
||||||
|
},
|
||||||
|
value_objects::UserId,
|
||||||
|
};
|
||||||
|
use http_axum::context::AppContext;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
// --- Local stubs ---
|
||||||
|
|
||||||
|
struct PanicReviewLogger;
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl application::ports::ReviewLogger for PanicReviewLogger {
|
||||||
|
async fn log_review(
|
||||||
|
&self,
|
||||||
|
_: application::diary::commands::LogReviewCommand,
|
||||||
|
) -> Result<(), DomainError> {
|
||||||
|
panic!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Repositories factory — `Panic*` stubs everywhere ---
|
||||||
|
|
||||||
|
pub fn test_repositories() -> Repositories {
|
||||||
|
Repositories {
|
||||||
|
movie_command: Arc::new(PanicMovieCommand) as _,
|
||||||
|
movie_query: Arc::new(PanicMovieQuery) as _,
|
||||||
|
review: Arc::new(PanicReviewRepository) as _,
|
||||||
|
diary: Arc::new(PanicDiaryQuery) as _,
|
||||||
|
stats: Arc::new(PanicStatsRepository) as _,
|
||||||
|
user: Arc::new(PanicUserRepository) as _,
|
||||||
|
import_session: Arc::new(PanicImportSessionRepository) as _,
|
||||||
|
import_profile: Arc::new(PanicImportProfileRepository) as _,
|
||||||
|
movie_profile: Arc::new(PanicMovieProfileRepository) as _,
|
||||||
|
watchlist: Arc::new(PanicWatchlistRepository) as _,
|
||||||
|
watch_event_command: Arc::new(PanicWatchEventCommand) as _,
|
||||||
|
watch_event_query: Arc::new(PanicWatchEventQuery) as _,
|
||||||
|
webhook_token: Arc::new(PanicWebhookTokenRepository) as _,
|
||||||
|
profile_fields: Arc::new(PanicProfileFieldsRepo) as _,
|
||||||
|
person_command: Arc::new(PanicPersonCommand) as _,
|
||||||
|
person_query: Arc::new(PanicPersonQuery) as _,
|
||||||
|
search_port: Arc::new(PanicSearchPort) as _,
|
||||||
|
search_command: Arc::new(PanicSearchCommand) as _,
|
||||||
|
remote_watchlist: Arc::new(PanicRemoteWatchlistRepository) as _,
|
||||||
|
social_command: Arc::new(domain::ports::noop::NoopSocialCommand) as _,
|
||||||
|
follow_graph: Arc::new(domain::ports::noop::NoopSocialQuery) as _,
|
||||||
|
block_query: Arc::new(domain::ports::noop::NoopSocialQuery) as _,
|
||||||
|
federation_admin: Arc::new(domain::ports::noop::NoopFederationAdminQuery) as _,
|
||||||
|
wrapup_stats: Arc::new(PanicWrapUpStatsQuery) as _,
|
||||||
|
wrapup_repo: Arc::new(PanicWrapUpRepository) as _,
|
||||||
|
insights: Arc::new(PanicInsightsQuery) as _,
|
||||||
|
goal_command: Arc::new(PanicGoalCommand) as _,
|
||||||
|
goal_query: Arc::new(PanicGoalQuery) as _,
|
||||||
|
user_settings: Arc::new(PanicUserSettingsRepository) as _,
|
||||||
|
remote_goal: Arc::new(PanicRemoteGoalRepository) as _,
|
||||||
|
refresh_session: Arc::new(PanicRefreshSessionRepository) as _,
|
||||||
|
federated_profile: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Single state factory — builds `deps` from whatever `repos` it is given ---
|
||||||
|
|
||||||
|
/// Builds an `AppState` from caller-supplied `repos`, running `bootstrap::build_deps`
|
||||||
|
/// AFTER any overrides the caller already applied to `repos`. This is the only place
|
||||||
|
/// `deps` gets built for tests, so `repos` and `deps` can never disagree.
|
||||||
|
pub fn build_test_state_from(
|
||||||
|
repos: Repositories,
|
||||||
|
auth_service: Arc<dyn AuthService>,
|
||||||
|
) -> http_axum::state::AppState {
|
||||||
|
let services = application::Services {
|
||||||
|
auth: auth_service,
|
||||||
|
password_hasher: Arc::new(PanicPasswordHasher) as _,
|
||||||
|
metadata: Arc::new(PanicMetadataClient) as _,
|
||||||
|
poster_fetcher: Arc::new(PanicPosterFetcher) as _,
|
||||||
|
object_storage: Arc::new(PanicObjectStorage) as _,
|
||||||
|
event_publisher: Arc::new(PanicEventPublisher) as _,
|
||||||
|
diary_exporter: Arc::new(PanicDiaryExporter) as _,
|
||||||
|
document_parser: Arc::new(PanicDocumentParser) as _,
|
||||||
|
review_logger: Arc::new(PanicReviewLogger) as _,
|
||||||
|
person_enrichment: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let config = AppConfig {
|
||||||
|
allow_registration: false,
|
||||||
|
base_url: "http://localhost:3000".to_string(),
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let instance = domain::value_objects::InstanceIdentity::new("http://localhost:3000");
|
||||||
|
|
||||||
|
let deps = Arc::new(bootstrap::build_deps(&repos, &services, &config, &instance));
|
||||||
|
|
||||||
|
http_axum::state::AppState {
|
||||||
|
app_ctx: AppContext {
|
||||||
|
deps,
|
||||||
|
services,
|
||||||
|
config,
|
||||||
|
instance,
|
||||||
|
#[cfg(feature = "federation")]
|
||||||
|
ap_document: Arc::new(domain::ports::noop::NoopApDocument),
|
||||||
|
#[cfg(feature = "federation")]
|
||||||
|
ap_blocklist: Arc::new(domain::ports::noop::NoopInstanceBlocklist),
|
||||||
|
},
|
||||||
|
rss_renderer: Arc::new(PanicRssFeedRenderer),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn make_test_state(auth_service: Arc<dyn AuthService>) -> http_axum::state::AppState {
|
||||||
|
build_test_state_from(test_repositories(), auth_service)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reusable default test `AppState`: `Panic*` stubs everywhere, real prebuilt `deps`.
|
||||||
|
/// Callers that need a specific stub swapped in should use one of the
|
||||||
|
/// `test_app_state_with_*` constructors below, which inject BEFORE `build_deps`
|
||||||
|
/// runs — mutating the returned `AppState` afterwards would only reach `repos`,
|
||||||
|
/// not the prebuilt `deps`.
|
||||||
|
pub fn test_app_state() -> http_axum::state::AppState {
|
||||||
|
make_test_state(Arc::new(PanicAuthService))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test state with `person_query` overridden BEFORE `build_deps` runs, so the
|
||||||
|
/// stub is visible from both `app_ctx.repos.person_query` and every prebuilt
|
||||||
|
/// deps struct that carries a `person_query` (e.g. `deps.person.get_person`).
|
||||||
|
pub fn test_app_state_with_person_query(
|
||||||
|
person_query: Arc<dyn PersonQuery>,
|
||||||
|
) -> http_axum::state::AppState {
|
||||||
|
let mut repos = test_repositories();
|
||||||
|
repos.person_query = person_query;
|
||||||
|
build_test_state_from(repos, Arc::new(PanicAuthService))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test state with `search_port` overridden BEFORE `build_deps` runs, so the
|
||||||
|
/// stub is visible from both `app_ctx.repos.search_port` and any prebuilt deps
|
||||||
|
/// struct that carries a `search_port`.
|
||||||
|
pub fn test_app_state_with_search_port(
|
||||||
|
search_port: Arc<dyn SearchPort>,
|
||||||
|
) -> http_axum::state::AppState {
|
||||||
|
let mut repos = test_repositories();
|
||||||
|
repos.search_port = search_port;
|
||||||
|
build_test_state_from(repos, Arc::new(PanicAuthService))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test state with `follow_graph` overridden BEFORE `build_deps` runs, so the
|
||||||
|
/// stub is visible from both `app_ctx.repos.follow_graph` and the prebuilt
|
||||||
|
/// `deps.social.query` that the social handlers actually read from.
|
||||||
|
pub fn test_app_state_with_follow_graph(
|
||||||
|
follow_graph: Arc<dyn FollowGraphQuery>,
|
||||||
|
auth_service: Arc<dyn AuthService>,
|
||||||
|
) -> http_axum::state::AppState {
|
||||||
|
let mut repos = test_repositories();
|
||||||
|
repos.follow_graph = follow_graph;
|
||||||
|
build_test_state_from(repos, auth_service)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test state with `user` overridden BEFORE `build_deps` runs, so the stub is
|
||||||
|
/// visible from both `app_ctx.repos.user` and `deps.users.authorize_admin.user`
|
||||||
|
/// — the field the `AdminApiUser`/`AdminUser` extractors now read through.
|
||||||
|
pub fn test_app_state_with_user(
|
||||||
|
user: Arc<dyn UserRepository>,
|
||||||
|
auth_service: Arc<dyn AuthService>,
|
||||||
|
) -> http_axum::state::AppState {
|
||||||
|
let mut repos = test_repositories();
|
||||||
|
repos.user = user;
|
||||||
|
build_test_state_from(repos, auth_service)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A `UserRepository` whose `find_by_id` always fails, for exercising the
|
||||||
|
/// repository-error rejection path of the admin extractors. Every other
|
||||||
|
/// method panics — the admin extractors only ever call `find_by_id`.
|
||||||
|
pub struct ErroringUserRepo;
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl UserRepository for ErroringUserRepo {
|
||||||
|
async fn find_by_email(
|
||||||
|
&self,
|
||||||
|
_: &domain::value_objects::Email,
|
||||||
|
) -> Result<Option<domain::models::User>, DomainError> {
|
||||||
|
panic!()
|
||||||
|
}
|
||||||
|
async fn save(&self, _: &domain::models::User) -> Result<(), DomainError> {
|
||||||
|
panic!()
|
||||||
|
}
|
||||||
|
async fn find_by_id(&self, _: &UserId) -> Result<Option<domain::models::User>, DomainError> {
|
||||||
|
Err(DomainError::InfrastructureError("db down".into()))
|
||||||
|
}
|
||||||
|
async fn find_by_username(
|
||||||
|
&self,
|
||||||
|
_: &domain::value_objects::Username,
|
||||||
|
) -> Result<Option<domain::models::User>, DomainError> {
|
||||||
|
panic!()
|
||||||
|
}
|
||||||
|
async fn list_with_stats(&self) -> Result<Vec<domain::models::UserSummary>, DomainError> {
|
||||||
|
panic!()
|
||||||
|
}
|
||||||
|
async fn update_profile(
|
||||||
|
&self,
|
||||||
|
_: &UserId,
|
||||||
|
_: &domain::models::UserProfile,
|
||||||
|
) -> Result<(), DomainError> {
|
||||||
|
panic!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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.
|
||||||
|
pub struct AcceptingAuth(pub UserId);
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl AuthService for AcceptingAuth {
|
||||||
|
async fn generate_token(&self, _: &UserId) -> Result<GeneratedToken, DomainError> {
|
||||||
|
panic!()
|
||||||
|
}
|
||||||
|
async fn validate_token(&self, _: &str) -> Result<UserId, DomainError> {
|
||||||
|
Ok(self.0.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user