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) <noreply@anthropic.com>
master
oabrivard 3 months ago
parent 7cbafdfb31
commit 53813007c6

@ -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).

@ -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, "<html><body>test</body></html>");
}
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,

Loading…
Cancel
Save