//! WP-41: security headers and cookie hardening. mod common; use axum::http::StatusCode; use common::{get, post, test_app_with_admin}; use serde_json::json; #[tokio::test] async fn responses_carry_security_headers() { let app = test_app_with_admin().await; let res = get(&app, "/healthz", None).await; let h = |k: &str| { res.headers .get(k) .and_then(|v| v.to_str().ok()) .unwrap_or("") .to_string() }; assert_eq!(h("x-content-type-options"), "nosniff"); assert_eq!(h("x-frame-options"), "DENY"); assert_eq!(h("referrer-policy"), "same-origin"); let csp = h("content-security-policy"); assert!(csp.contains("default-src 'self'"), "{csp}"); assert!(csp.contains("frame-ancestors 'none'"), "{csp}"); assert_eq!(h("cache-control"), "no-store"); assert!(res.headers.get("server").is_none()); // API errors are JSON, not HTML, and still carry the headers let res = get(&app, "/api/users", None).await; assert_eq!(res.status, StatusCode::UNAUTHORIZED); assert_eq!(res.json["error"], "unauthorized"); assert_eq!( res.headers.get("x-content-type-options").unwrap(), "nosniff" ); } #[tokio::test] async fn refresh_cookie_is_strict_and_scoped() { let app = test_app_with_admin().await; let res = post( &app, "/api/auth/login", json!({"email": "admin@example.com", "password": "admin-password-123"}), None, ) .await; let cookie = res.headers.get("set-cookie").unwrap().to_str().unwrap(); assert!( cookie.contains("HttpOnly") && cookie.contains("SameSite=Strict") && cookie.contains("Path=/api/auth") ); // cross-site style request without the cookie cannot refresh assert_eq!( post(&app, "/api/auth/refresh", json!({}), None) .await .status, StatusCode::UNAUTHORIZED ); } #[tokio::test] async fn oversized_json_bodies_are_rejected() { let app = test_app_with_admin().await; let token = common::login(&app, "admin@example.com", "admin-password-123") .await .access; let big = "x".repeat(2 * 1024 * 1024); let res = post(&app, "/api/users", json!({"email": "a@b.de", "display_name": big, "password": "user-password-123", "role": "user"}), Some(&token)).await; assert_eq!(res.status, StatusCode::PAYLOAD_TOO_LARGE); }