//! Integration tests for the tidal-server middleware stack. //! //! Each test spins up an in-process router using `tower::ServiceExt::oneshot` //! — no TCP port binding required — and verifies middleware behaviors: //! body limits, authentication, and request-ID propagation. use std::sync::Arc; use axum::body::Body; use axum::http::{Method, Request, StatusCode}; use tidal_server::{router::build_router, state::ServerState}; use tidaldb::TidalDb; use tower::ServiceExt; // ── Test helpers ───────────────────────────────────────────────────────────── fn make_state() -> Arc { // Use the default schema so ranking profiles are available — same setup as // `run_standalone` in main.rs. let schema = tidal_server::config::load_schema(None).unwrap(); let db = TidalDb::builder() .ephemeral() .with_schema(schema) .open() .unwrap(); Arc::new(ServerState::new(db)) } fn make_app(api_key: Option<&str>) -> axum::Router { let key = api_key.map(|k| Arc::from(k)); build_router(make_state(), key) } // ── Auth tests ──────────────────────────────────────────────────────────────── /// /health is public — no Bearer token required. #[tokio::test] async fn health_no_auth_required() { let app = make_app(Some("secret")); let response = app .oneshot( Request::builder() .method(Method::GET) .uri("/health") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); } /// Protected routes (e.g. /feed) require a Bearer token when a key is set. #[tokio::test] async fn protected_route_requires_auth() { let app = make_app(Some("secret")); let response = app .oneshot( Request::builder() .method(Method::GET) .uri("/feed") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } /// A valid Bearer token grants access to protected routes. #[tokio::test] async fn protected_route_accepts_valid_key() { let app = make_app(Some("secret")); let response = app .oneshot( Request::builder() .method(Method::GET) .uri("/feed") .header("Authorization", "Bearer secret") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); } /// RFC 7235: auth scheme is case-insensitive — `bearer` should work like `Bearer`. #[tokio::test] async fn lowercase_bearer_accepted() { let app = make_app(Some("secret")); let response = app .oneshot( Request::builder() .method(Method::GET) .uri("/feed") .header("Authorization", "bearer secret") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); } /// An incorrect Bearer token is rejected with 401. #[tokio::test] async fn wrong_key_rejected() { let app = make_app(Some("secret")); let response = app .oneshot( Request::builder() .method(Method::GET) .uri("/feed") .header("Authorization", "Bearer wrong") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } /// When no API key is configured, all routes are accessible without auth. #[tokio::test] async fn no_auth_mode_allows_all_routes() { let app = make_app(None); let response = app .oneshot( Request::builder() .method(Method::GET) .uri("/feed") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); } // ── Middleware behavior tests ───────────────────────────────────────────────── /// Bodies exceeding 2MB on protected routes are rejected with 413. #[tokio::test] async fn body_too_large_returns_413() { let app = make_app(Some("secret")); // 3 MB exceeds the 2 MB BODY_LIMIT_BYTES constant in router.rs. let big_body = vec![0u8; 3 * 1024 * 1024]; let response = app .oneshot( Request::builder() .method(Method::POST) .uri("/embeddings") .header("Authorization", "Bearer secret") .header("Content-Type", "application/json") .body(Body::from(big_body)) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::PAYLOAD_TOO_LARGE); } /// x-request-id is present in every response and increments per request. #[tokio::test] async fn x_request_id_increments() { // Both calls share the same App (and therefore the same AtomicU64 counter). let app = make_app(None); let r1 = app .clone() .oneshot( Request::builder() .method(Method::GET) .uri("/health") .body(Body::empty()) .unwrap(), ) .await .unwrap(); let r2 = app .oneshot( Request::builder() .method(Method::GET) .uri("/health") .body(Body::empty()) .unwrap(), ) .await .unwrap(); let id1: u64 = r1 .headers() .get("x-request-id") .expect("x-request-id missing from first response") .to_str() .unwrap() .parse() .unwrap(); let id2: u64 = r2 .headers() .get("x-request-id") .expect("x-request-id missing from second response") .to_str() .unwrap() .parse() .unwrap(); assert!(id2 > id1, "x-request-id should increase: {id1} then {id2}"); }