use axum::body::Body;
use axum::http::{Method, Request, StatusCode};
use axum::middleware::from_fn;
use axum::routing::get;
use axum::Router;
use tensor_wasm_api::middleware::host_validate;
use tensor_wasm_api::TrustedHosts;
use tower::ServiceExt;
async fn ok_handler() -> &'static str {
"ok"
}
fn router(allow: TrustedHosts) -> Router {
let stack = tower::ServiceBuilder::new()
.layer(axum::Extension(allow))
.layer(from_fn(host_validate));
Router::new().route("/probe", get(ok_handler)).layer(stack)
}
#[tokio::test]
async fn host_validate_passes_through_when_allowlist_empty() {
let router = router(TrustedHosts::allow_all());
let req = Request::builder()
.method(Method::GET)
.uri("/probe")
.header("host", "anything.example.com")
.body(Body::empty())
.unwrap();
let resp = router.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
}
#[tokio::test]
async fn host_validate_rejects_unlisted_host() {
let router = router(TrustedHosts::from_hosts(["api.example.com"]));
let req = Request::builder()
.method(Method::GET)
.uri("/probe")
.header("host", "evil.com")
.body(Body::empty())
.unwrap();
let resp = router.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}
#[tokio::test]
async fn host_validate_accepts_listed_host() {
let router = router(TrustedHosts::from_hosts(["api.example.com"]));
let req = Request::builder()
.method(Method::GET)
.uri("/probe")
.header("host", "api.example.com")
.body(Body::empty())
.unwrap();
let resp = router.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
}
#[tokio::test]
async fn host_validate_accepts_listed_host_case_insensitive() {
let router = router(TrustedHosts::from_hosts(["api.example.com"]));
let req = Request::builder()
.method(Method::GET)
.uri("/probe")
.header("host", "API.EXAMPLE.COM")
.body(Body::empty())
.unwrap();
let resp = router.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
}
#[tokio::test]
async fn host_validate_rejects_missing_host_when_configured() {
let router = router(TrustedHosts::from_hosts(["api.example.com"]));
let req = Request::builder()
.method(Method::GET)
.uri("/probe")
.body(Body::empty())
.unwrap();
let resp = router.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}
#[tokio::test]
async fn host_validate_uses_http2_authority_fallback() {
let router = router(TrustedHosts::from_hosts(["api.example.com"]));
let req = Request::builder()
.method(Method::GET)
.uri("http://api.example.com/probe")
.body(Body::empty())
.unwrap();
assert_eq!(
req.uri().authority().map(|a| a.as_str()),
Some("api.example.com"),
);
assert!(req.headers().get(axum::http::header::HOST).is_none());
let resp = router.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
}
#[tokio::test]
async fn host_validate_rejects_http2_authority_when_unlisted() {
let router = router(TrustedHosts::from_hosts(["api.example.com"]));
let req = Request::builder()
.method(Method::GET)
.uri("http://evil.com/probe")
.body(Body::empty())
.unwrap();
let resp = router.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}
#[tokio::test]
async fn host_validate_default_port_strip_443() {
let router = router(TrustedHosts::from_hosts(["api.example.com"]));
let req = Request::builder()
.method(Method::GET)
.uri("/probe")
.header("host", "api.example.com:443")
.body(Body::empty())
.unwrap();
let resp = router.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
}
#[tokio::test]
async fn host_validate_default_port_strip_80() {
let router = router(TrustedHosts::from_hosts(["api.example.com"]));
let req = Request::builder()
.method(Method::GET)
.uri("/probe")
.header("host", "api.example.com:80")
.body(Body::empty())
.unwrap();
let resp = router.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
}
#[tokio::test]
async fn host_validate_port_bound_entry_requires_exact_match() {
let router = router(TrustedHosts::from_hosts(["api.example.com:8443"]));
let req = Request::builder()
.method(Method::GET)
.uri("/probe")
.header("host", "api.example.com:8443")
.body(Body::empty())
.unwrap();
let resp = router.clone().oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let req = Request::builder()
.method(Method::GET)
.uri("/probe")
.header("host", "api.example.com")
.body(Body::empty())
.unwrap();
let resp = router.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}
#[test]
fn trusted_hosts_from_raw_parses_comma_separated_list() {
let parsed = TrustedHosts::from_raw("api.example.com, api2.example.com ,");
assert!(parsed.contains("api.example.com"));
assert!(parsed.contains("api2.example.com"));
assert!(!parsed.contains("evil.com"));
}
#[test]
fn trusted_hosts_from_raw_empty_is_allow_all() {
let parsed = TrustedHosts::from_raw("");
assert!(parsed.is_empty());
assert!(parsed.contains("anything.example.com"));
}
#[test]
fn trusted_hosts_contains_is_case_insensitive() {
let parsed = TrustedHosts::from_hosts(["api.example.com"]);
assert!(parsed.contains("API.EXAMPLE.COM"));
assert!(parsed.contains("Api.Example.Com"));
}