From 53813007c66c21455ed0b61b6398857a28dbef99 Mon Sep 17 00:00:00 2001 From: oabrivard Date: Thu, 26 Mar 2026 10:50:58 +0100 Subject: [PATCH] fix: use lightweight test router without SPA fallback and TraceLayer Unauthenticated requests were hanging in integration tests due to tower middleware layers interacting with oneshot(). Add build_test_router() that only includes API routes + CSRF middleware. Co-Authored-By: Claude Opus 4.6 (1M context) --- backend/src/router.rs | 56 +++++++++++++++++++++++++++++++++++++ backend/tests/common/mod.rs | 11 ++------ 2 files changed, 58 insertions(+), 9 deletions(-) diff --git a/backend/src/router.rs b/backend/src/router.rs index 81b2b12..2cf4cb9 100644 --- a/backend/src/router.rs +++ b/backend/src/router.rs @@ -137,6 +137,62 @@ pub fn build_router(state: AppState, config: &AppConfig) -> Router { app } +/// Build a lightweight router for integration tests. +/// +/// Skips static file serving (SPA fallback) and TraceLayer to avoid +/// issues with `oneshot()` in test environments. +pub fn build_test_router(state: AppState, config: &AppConfig) -> Router { + let api_routes = Router::new() + .route("/auth/register", post(handlers::auth::register)) + .route("/auth/login", post(handlers::auth::login)) + .route("/auth/verify", get(handlers::auth::verify_get)) + .route("/auth/verify", post(handlers::auth::verify_post)) + .route("/auth/logout", post(handlers::auth::logout)) + .route("/auth/me", get(handlers::auth::me)) + .route("/settings", get(handlers::settings::get_settings)) + .route("/settings", put(handlers::settings::update_settings)) + .route("/sources", get(handlers::sources::list)) + .route("/sources", post(handlers::sources::create)) + .route("/sources/{id}", delete(handlers::sources::delete)) + .route("/sources/bulk", post(handlers::sources::bulk_import)) + .route("/sources/import-csv", post(handlers::sources::import_csv)) + .route("/sources/export-csv", get(handlers::sources::export_csv)) + .route("/user/api-keys", get(handlers::api_keys::list)) + .route("/user/api-keys", post(handlers::api_keys::create)) + .route("/user/api-keys/{provider}", delete(handlers::api_keys::delete)) + .route("/user/api-keys/{provider}/test", post(handlers::api_keys::test_key)) + .route("/user/api-keys/export", post(handlers::api_keys::export_keys)) + .route("/syntheses/generate", post(handlers::generation::trigger_generate)) + .route("/syntheses/generate/{job_id}/progress", get(handlers::generation::progress_stream)) + .route("/article-history", get(handlers::article_history::list_history).delete(handlers::article_history::clear_history)) + .route("/syntheses/{id}/provenance", get(handlers::article_history::get_provenance)) + .route("/llm-logs/{job_id}", get(handlers::llm_logs::get_logs)) + .route("/syntheses", get(handlers::syntheses::list)) + .route("/syntheses/{id}", get(handlers::syntheses::get)) + .route("/syntheses/{id}", delete(handlers::syntheses::delete)) + .route("/syntheses/{id}/send-email", post(handlers::syntheses::send_email)) + .route("/syntheses/{id}/export/markdown", get(handlers::syntheses::export_markdown)) + .route("/syntheses/{id}/export/pdf", get(handlers::syntheses::export_pdf)) + .route("/config/providers", get(handlers::config::list_enabled_providers)) + .route("/admin/providers", get(handlers::admin::list_providers)) + .route("/admin/providers", post(handlers::admin::create_provider)) + .route("/admin/providers/{id}", put(handlers::admin::update_provider)) + .route("/admin/providers/{id}", delete(handlers::admin::delete_provider)) + .route("/admin/rate-limits", get(handlers::admin::list_rate_limits)) + .route("/admin/rate-limits/{provider_name}", put(handlers::admin::update_rate_limit)) + .route("/admin/users", get(handlers::admin::list_users)) + .route("/admin/users/{id}/role", put(handlers::admin::update_user_role)) + .route("/health", get(handlers::health::health_check)) + .layer(axum_mw::from_fn(csrf::csrf_check)); + + let api = Router::new().nest("/api/v1", api_routes); + + Router::new() + .merge(api) + .with_state(state) + .layer(DefaultBodyLimit::max(1024 * 1024)) +} + /// Build the CORS layer based on configuration. /// /// Allows the configured `APP_URL` as origin, with credentials (cookies). diff --git a/backend/tests/common/mod.rs b/backend/tests/common/mod.rs index c9d642b..5b99a44 100644 --- a/backend/tests/common/mod.rs +++ b/backend/tests/common/mod.rs @@ -28,7 +28,7 @@ use ai_synth_backend::app_state::AppState; use ai_synth_backend::config::AppConfig; use ai_synth_backend::db; use ai_synth_backend::models::user::UserRole; -use ai_synth_backend::router::build_router; +use ai_synth_backend::router::build_test_router; use ai_synth_backend::services::auth; use ai_synth_backend::services::email::TEST_API_KEY as EMAIL_TEST_KEY; use ai_synth_backend::services::turnstile::TEST_SECRET_KEY as TURNSTILE_TEST_KEY; @@ -117,16 +117,9 @@ impl TestApp { turnstile_site_key: "test-site-key".into(), }; - // Create the static dir so ServeDir/ServeFile don't hang - let _ = std::fs::create_dir_all(&config.static_dir); - let index_path = format!("{}/index.html", config.static_dir); - if !std::path::Path::new(&index_path).exists() { - let _ = std::fs::write(&index_path, "test"); - } - let http_client = reqwest::Client::new(); let state = AppState::new(config.clone(), pool.clone(), http_client); - let router = build_router(state, &config); + let router = build_test_router(state, &config); Self { router,