fix: use docker-compose.test.yml for integration test DB

Rewrite run-integration-tests.sh to use the e2e docker-compose config
(Postgres on port 5433). Add --db-check flag for connectivity debugging.
Remove build_test_router (reverted to build_router). Keep minimal_test
for oneshot debugging.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
master
oabrivard 3 months ago
parent 5fa060fadc
commit aee70b37d4

@ -137,62 +137,6 @@ 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_test_router;
use ai_synth_backend::router::build_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;
@ -119,7 +119,14 @@ impl TestApp {
let http_client = reqwest::Client::new();
let state = AppState::new(config.clone(), pool.clone(), http_client);
let router = build_test_router(state, &config);
// Create the static dir so ServeDir/ServeFile don't error
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 router = build_router(state, &config);
Self {
router,

@ -0,0 +1,26 @@
/// Minimal test to debug oneshot() hanging issue.
use axum::body::Body;
use axum::http::{Request, StatusCode};
use axum::{routing::get, Router};
use http_body_util::BodyExt;
use tower::ServiceExt;
async fn hello() -> &'static str {
"ok"
}
#[tokio::test]
async fn minimal_oneshot_works() {
let app: Router = Router::new().route("/test", get(hello));
let req: Request<Body> = Request::builder()
.uri("/test")
.body(Body::empty())
.unwrap();
let response: axum::http::Response<Body> = app.oneshot(req).await.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = response.into_body().collect().await.unwrap().to_bytes();
assert_eq!(&body[..], b"ok");
}

@ -1,66 +1,87 @@
#!/usr/bin/env bash
#
# Run backend integration tests (Rust).
# Starts a Postgres container, runs tests, then cleans up.
# Uses the e2e/docker-compose.test.yml for Postgres (port 5433).
#
# Usage:
# ./scripts/run-integration-tests.sh # all tests
# ./scripts/run-integration-tests.sh --test pipeline_test # one test file
# ./scripts/run-integration-tests.sh --test api_admin_test config_providers # one test by name
# ./scripts/run-integration-tests.sh --test api_admin_test -- --test-threads=1 # extra cargo args
# ./scripts/run-integration-tests.sh --lib # unit tests only
# ./scripts/run-integration-tests.sh --db-check # just check DB connectivity
#
# All arguments are passed directly to `cargo test`.
# All arguments (except --db-check) are passed directly to `cargo test`.
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
E2E_DIR="$PROJECT_DIR/e2e"
CONTAINER_NAME="ai-synth-integration-db"
PG_PORT=5434
PG_USER="test_user"
PG_PASS="test_password"
PG_DB="postgres"
PG_HOST="127.0.0.1"
PG_PORT=5433
PG_USER="ai_synth_test"
PG_PASS="testpassword"
PG_DB="ai_synth_test"
cleanup() {
echo "Cleaning up..."
docker rm -f "$CONTAINER_NAME" 2>/dev/null || true
}
trap cleanup EXIT
export TEST_DATABASE_URL="postgres://${PG_USER}:${PG_PASS}@${PG_HOST}:${PG_PORT}/${PG_DB}"
echo "=== Starting Postgres for integration tests ==="
docker rm -f "$CONTAINER_NAME" 2>/dev/null || true
docker run -d \
--name "$CONTAINER_NAME" \
-e POSTGRES_USER="$PG_USER" \
-e POSTGRES_PASSWORD="$PG_PASS" \
-e POSTGRES_DB="$PG_DB" \
-p "127.0.0.1:${PG_PORT}:5432" \
--health-cmd "pg_isready -U $PG_USER -d $PG_DB" \
--health-interval 5s \
--health-timeout 3s \
--health-retries 10 \
postgres:17-alpine
# ── DB check mode ──────────────────────────────────────────────────
if [ "${1:-}" = "--db-check" ]; then
echo "Checking DB connectivity at ${PG_HOST}:${PG_PORT}..."
if docker exec ai-synth-test-db pg_isready -U "$PG_USER" -d "$PG_DB" 2>/dev/null; then
echo "DB is ready."
echo "Testing SQL query..."
docker exec ai-synth-test-db psql -U "$PG_USER" -d "$PG_DB" -c "SELECT 1 AS ok" 2>&1
echo ""
echo "Testing connection from host..."
if command -v psql >/dev/null 2>&1; then
PGPASSWORD="$PG_PASS" psql -h "$PG_HOST" -p "$PG_PORT" -U "$PG_USER" -d "$PG_DB" -c "SELECT 1 AS ok" 2>&1 || echo "Host psql connection failed"
else
echo "(psql not installed on host, skipping host connection test)"
fi
else
echo "DB container not running. Start it with:"
echo " cd $E2E_DIR && docker compose -f docker-compose.test.yml up -d db"
fi
exit 0
fi
# ── Start DB if not running ────────────────────────────────────────
echo "=== Ensuring test Postgres is running ==="
cd "$E2E_DIR"
# Start only the DB service (not the app)
docker compose -f docker-compose.test.yml up -d db
echo "Waiting for Postgres to be ready..."
for i in $(seq 1 30); do
if docker exec "$CONTAINER_NAME" pg_isready -U "$PG_USER" -d "$PG_DB" >/dev/null 2>&1; then
echo "Postgres is ready."
if docker exec ai-synth-test-db pg_isready -U "$PG_USER" -d "$PG_DB" >/dev/null 2>&1; then
echo "Postgres is ready on port ${PG_PORT}."
break
fi
if [ "$i" -eq 30 ]; then
echo "ERROR: Postgres did not become ready in time."
docker compose -f docker-compose.test.yml logs db | tail -10
exit 1
fi
sleep 1
done
export TEST_DATABASE_URL="postgres://${PG_USER}:${PG_PASS}@127.0.0.1:${PG_PORT}/${PG_DB}"
# Verify connectivity from host
echo "Verifying host connectivity..."
cd "$PROJECT_DIR/backend"
if ! cargo sqlx database exists "$TEST_DATABASE_URL" 2>/dev/null; then
# sqlx-cli not installed, try a simple Rust connection test
echo "(sqlx-cli not available, will rely on cargo test to verify connectivity)"
fi
# ── Run tests ──────────────────────────────────────────────────────
echo ""
echo "=== Running integration tests ==="
cd "$PROJECT_DIR/backend"
echo "TEST_DATABASE_URL=$TEST_DATABASE_URL"
echo ""
if [ $# -gt 0 ]; then
echo "Running: cargo test $*"
@ -70,4 +91,8 @@ else
cargo test
fi
echo "=== All tests passed ==="
echo ""
echo "=== Tests complete ==="
echo ""
echo "Note: DB container is still running. To stop it:"
echo " cd $E2E_DIR && docker compose -f docker-compose.test.yml down"

Loading…
Cancel
Save