//! Integration tests for CSRF protection middleware. //! //! Verifies that: //! - POST/PUT/DELETE requests without X-Requested-With header are rejected (403) //! - POST/PUT/DELETE requests with X-Requested-With: XMLHttpRequest pass through //! - GET requests are exempt from CSRF checks //! //! Requires a running Postgres instance. Set `TEST_DATABASE_URL` to run. mod common; use axum::http::StatusCode; fn require_test_db() -> bool { std::env::var("TEST_DATABASE_URL").is_ok() } #[tokio::test] async fn post_without_csrf_header_returns_403() { if !require_test_db() { eprintln!("SKIPPED: TEST_DATABASE_URL not set"); return; } let app = common::TestApp::new().await; let body = serde_json::json!({ "email": "test@example.com", "turnstile_token": "test-token" }); let (status, resp_body) = app.post_without_csrf("/api/v1/auth/register", &body).await; assert_eq!( status, StatusCode::FORBIDDEN, "POST without X-Requested-With should return 403" ); assert_eq!( resp_body["error"], "forbidden", "Error type should be 'forbidden'" ); assert!( resp_body["message"] .as_str() .unwrap_or("") .contains("X-Requested-With"), "Error message should mention X-Requested-With header" ); } #[tokio::test] async fn post_with_csrf_header_passes_csrf_check() { if !require_test_db() { eprintln!("SKIPPED: TEST_DATABASE_URL not set"); return; } let app = common::TestApp::new().await; let body = serde_json::json!({ "email": "csrf-test@example.com", "turnstile_token": "test-token" }); // This should pass the CSRF check (may return 200 from the handler) let (status, _) = app.post("/api/v1/auth/register", &body).await; assert_ne!( status, StatusCode::FORBIDDEN, "POST with X-Requested-With should NOT return 403 (CSRF passed)" ); // Should be 200 since registration with bypassed turnstile works assert_eq!( status, StatusCode::OK, "Registration should succeed with valid CSRF header" ); } #[tokio::test] async fn get_request_is_exempt_from_csrf_check() { if !require_test_db() { eprintln!("SKIPPED: TEST_DATABASE_URL not set"); return; } let app = common::TestApp::new().await; // GET /api/v1/health does not need X-Requested-With let (status, _) = app.get("/api/v1/health").await; assert_eq!( status, StatusCode::OK, "GET request should be exempt from CSRF check" ); } #[tokio::test] async fn put_without_csrf_header_returns_403() { if !require_test_db() { eprintln!("SKIPPED: TEST_DATABASE_URL not set"); return; } let app = common::TestApp::new().await; // Build a PUT request without X-Requested-With let body = serde_json::json!({ "theme": "Test", "max_age_days": 7, "categories": ["Cat"], "max_items_per_category": 4, "search_agent_behavior": "" }); let body_bytes = serde_json::to_vec(&body).unwrap(); let req = axum::http::Request::builder() .method(axum::http::Method::PUT) .uri("/api/v1/settings") .header("Content-Type", "application/json") // Deliberately omit X-Requested-With .body(axum::body::Body::from(body_bytes)) .unwrap(); let response = app.raw_request(req).await; let status = response.status(); assert_eq!( status, StatusCode::FORBIDDEN, "PUT without X-Requested-With should return 403" ); }