You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
136 lines
3.6 KiB
Rust
136 lines
3.6 KiB
Rust
//! 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"
|
|
);
|
|
}
|