use std::sync::Arc;
use axum::body::Body;
use axum::extract::Extension;
use axum::http::{HeaderName, HeaderValue, Method, Request, StatusCode};
use axum::middleware::from_fn;
use axum::routing::get;
use axum::{Json, Router};
use http_body_util::BodyExt;
use serde_json::{json, Value};
use tensor_wasm_api::middleware::{bearer_auth, tenant_scope, MAX_AUTH_HEADER_BYTES};
use tensor_wasm_api::{AuthConfig, TenantConfig, HEADER_TENANT};
use tensor_wasm_core::types::TenantId;
use tower::ServiceExt;
async fn body_json(body: Body) -> Value {
let bytes = body.collect().await.expect("body").to_bytes().to_vec();
serde_json::from_slice(&bytes).expect("body is JSON")
}
fn error_kind(body: &Value) -> Option<&str> {
body.pointer("/error/kind").and_then(Value::as_str)
}
async fn echo_tenant(tenant: Option<Extension<TenantId>>) -> Json<Value> {
Json(match tenant {
Some(Extension(TenantId(v))) => json!({ "tenant": v }),
None => json!({ "tenant": null }),
})
}
fn tenant_router(cfg: TenantConfig) -> Router {
let stack = tower::ServiceBuilder::new()
.layer(axum::Extension(cfg))
.layer(from_fn(tenant_scope));
Router::new().route("/probe", get(echo_tenant)).layer(stack)
}
async fn ok_handler() -> &'static str {
"ok"
}
fn auth_router(cfg: AuthConfig) -> Router {
let stack = tower::ServiceBuilder::new()
.layer(axum::Extension(cfg))
.layer(from_fn(bearer_auth));
Router::new().route("/probe", get(ok_handler)).layer(stack)
}
#[tokio::test]
async fn duplicate_tenant_header_rejected() {
let router = tenant_router(TenantConfig::default());
let req = Request::builder()
.method(Method::GET)
.uri("/probe")
.header(HEADER_TENANT, "1")
.header(HEADER_TENANT, "999")
.body(Body::empty())
.unwrap();
assert_eq!(
req.headers().get_all(HEADER_TENANT).iter().count(),
2,
"test setup must produce two X-TensorWasm-Tenant headers",
);
let resp = router.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let body = body_json(resp.into_body()).await;
assert_eq!(
error_kind(&body),
Some("duplicate_tenant_header"),
"got {body}"
);
}
#[tokio::test]
async fn invalid_tenant_header_distinct_from_missing() {
let router = tenant_router(TenantConfig {
require_header: true,
});
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);
let body = body_json(resp.into_body()).await;
assert_eq!(
error_kind(&body),
Some("missing_tenant"),
"absent + required must surface as missing_tenant: got {body}"
);
let router = tenant_router(TenantConfig::default());
let req = Request::builder()
.method(Method::GET)
.uri("/probe")
.header(HEADER_TENANT, "not-a-number")
.body(Body::empty())
.unwrap();
let resp = router.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let body = body_json(resp.into_body()).await;
assert_eq!(
error_kind(&body),
Some("invalid_tenant"),
"present-but-unparseable must surface as invalid_tenant: got {body}"
);
let router = tenant_router(TenantConfig::default());
let req = Request::builder()
.method(Method::GET)
.uri("/probe")
.header(HEADER_TENANT, "42")
.body(Body::empty())
.unwrap();
let resp = router.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let body = body_json(resp.into_body()).await;
assert_eq!(
body.get("tenant").and_then(Value::as_u64),
Some(42),
"numeric tenant must thread to handler: got {body}"
);
}
#[tokio::test]
async fn oversized_authorization_header_rejected() {
let auth = AuthConfig::from_tokens(["legit-token"]);
let router = auth_router(auth);
let oversized_token = "x".repeat(8 * 1024);
let header_value_str = format!("Bearer {}", oversized_token);
assert!(
header_value_str.len() > MAX_AUTH_HEADER_BYTES,
"test setup must produce a value above MAX_AUTH_HEADER_BYTES \
({} bytes); got {} bytes",
MAX_AUTH_HEADER_BYTES,
header_value_str.len(),
);
let req = Request::builder()
.method(Method::GET)
.uri("/probe")
.header(
axum::http::header::AUTHORIZATION,
HeaderValue::from_bytes(header_value_str.as_bytes())
.expect("ASCII bytes parse into HeaderValue"),
)
.body(Body::empty())
.unwrap();
let resp = router.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
let body = body_json(resp.into_body()).await;
assert_eq!(
error_kind(&body),
Some("invalid_auth"),
"oversized Authorization must surface as invalid_auth: got {body}"
);
}
#[tokio::test]
async fn authorization_with_control_bytes_rejected() {
for attack in [b"Bearer \x00malicious".as_slice(), b"Bearer \r\nevil:"] {
let result = HeaderValue::from_bytes(attack);
assert!(
result.is_err(),
"HeaderValue::from_bytes must reject control bytes; \
value {:?} unexpectedly parsed",
attack,
);
}
let auth = AuthConfig::from_tokens(["legit-token"]);
let router = auth_router(auth);
let value = HeaderValue::from_bytes(b"Bearer\tlegit-token")
.expect("tab separator is a permitted header byte");
let req = Request::builder()
.method(Method::GET)
.uri("/probe")
.header(axum::http::header::AUTHORIZATION, value)
.body(Body::empty())
.unwrap();
let resp = router.oneshot(req).await.unwrap();
assert_eq!(
resp.status(),
StatusCode::OK,
"tab-separated Bearer value must still be admitted",
);
let auth = AuthConfig::from_tokens(["legit-token"]);
let router = auth_router(auth);
let req = Request::builder()
.method(Method::GET)
.uri("/probe")
.header(
axum::http::header::AUTHORIZATION,
HeaderValue::from_static("Bearer wrong-token"),
)
.body(Body::empty())
.unwrap();
let resp = router.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
let body = body_json(resp.into_body()).await;
assert_eq!(
error_kind(&body),
Some("unauthorized"),
"non-allowlisted token must still surface as unauthorized: got {body}"
);
let _ = Arc::new(());
let _: HeaderName = axum::http::header::AUTHORIZATION;
}
#[test]
fn missing_tenant_when_required() {
temp_env::with_var("TENSOR_WASM_API_REQUIRE_TENANT", Some("1"), || {
let cfg = TenantConfig::from_env();
assert!(
cfg.require_header,
"TENSOR_WASM_API_REQUIRE_TENANT=1 must yield require_header=true",
);
let router = tenant_router(cfg);
let req = Request::builder()
.method(Method::GET)
.uri("/probe")
.body(Body::empty())
.unwrap();
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("current-thread runtime");
let (status, body) = rt.block_on(async move {
let resp = router.oneshot(req).await.unwrap();
let status = resp.status();
let body = body_json(resp.into_body()).await;
(status, body)
});
assert_eq!(status, StatusCode::BAD_REQUEST);
assert_eq!(
error_kind(&body),
Some("missing_tenant"),
"TENSOR_WASM_API_REQUIRE_TENANT=1 + absent header must surface as \
missing_tenant: got {body}"
);
});
}
#[test]
fn authorization_with_embedded_nul_rejected() {
let attack: &[u8] = b"Bearer abc\0def";
let result = HeaderValue::from_bytes(attack);
assert!(
result.is_err(),
"HeaderValue::from_bytes must reject embedded NUL; \
value {:?} unexpectedly parsed",
attack,
);
let trailing_nul: &[u8] = b"Bearer\tlegit\0";
assert!(
HeaderValue::from_bytes(trailing_nul).is_err(),
"HeaderValue::from_bytes must reject trailing NUL too; \
value {:?} unexpectedly parsed",
trailing_nul,
);
let bare_nul: &[u8] = b"Bearer \0";
assert!(
HeaderValue::from_bytes(bare_nul).is_err(),
"HeaderValue::from_bytes must reject a bare NUL payload; \
value {:?} unexpectedly parsed",
bare_nul,
);
}