use hyper::body::Bytes;
use hyper::{Response, StatusCode};
use mini_serve::{body, handler, RouteBuilder, ServeError};
#[tokio::test]
async fn nosniff_is_sent_on_every_response_shape() {
let app = RouteBuilder::stateless()
.get(
"/exists",
handler(|_, _| async {
Ok::<_, ServeError>(Response::new(body(Bytes::from("ok"))))
}),
)
.get(
"/boom",
handler(|_, _| async {
Err::<Response<mini_serve::ResponseBody>, _>(ServeError::new(500, "detail"))
}),
)
.seal();
let port = app.bind_ephemeral().await.unwrap();
let client = reqwest::Client::new();
for path in ["/exists", "/boom", "/missing"] {
let resp = client
.get(format!("http://127.0.0.1:{port}{path}"))
.send()
.await
.unwrap();
assert_eq!(
resp.headers()
.get("x-content-type-options")
.map(|v| v.to_str().unwrap()),
Some("nosniff"),
"{path} is missing nosniff"
);
}
let resp = client
.post(format!("http://127.0.0.1:{port}/exists"))
.send()
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED);
assert!(resp.headers().contains_key("x-content-type-options"));
let long = "/".to_string() + &"a".repeat(9000);
let resp = client
.get(format!("http://127.0.0.1:{port}{long}"))
.send()
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
assert!(resp.headers().contains_key("x-content-type-options"));
}
#[tokio::test]
async fn configured_headers_are_sent_on_every_response() {
let app = RouteBuilder::stateless()
.with_response_header("Strict-Transport-Security", "max-age=63072000")
.unwrap()
.with_response_header("Referrer-Policy", "no-referrer")
.unwrap()
.get(
"/exists",
handler(|_, _| async {
Ok::<_, ServeError>(Response::new(body(Bytes::from("ok"))))
}),
)
.seal();
let port = app.bind_ephemeral().await.unwrap();
let client = reqwest::Client::new();
for path in ["/exists", "/missing"] {
let resp = client
.get(format!("http://127.0.0.1:{port}{path}"))
.send()
.await
.unwrap();
assert_eq!(
resp.headers()
.get("strict-transport-security")
.map(|v| v.to_str().unwrap()),
Some("max-age=63072000"),
"{path}"
);
assert_eq!(
resp.headers()
.get("referrer-policy")
.map(|v| v.to_str().unwrap()),
Some("no-referrer"),
"{path}"
);
}
}
#[tokio::test]
async fn a_handler_set_header_is_not_clobbered() {
let app = RouteBuilder::stateless()
.with_response_header("Content-Security-Policy", "default-src 'self'")
.unwrap()
.get(
"/relaxed",
handler(|_, _| async {
let mut resp = Response::new(body(Bytes::from("ok")));
resp.headers_mut().insert(
"content-security-policy",
"default-src *".parse().unwrap(),
);
Ok::<_, ServeError>(resp)
}),
)
.seal();
let port = app.bind_ephemeral().await.unwrap();
let resp = reqwest::get(format!("http://127.0.0.1:{port}/relaxed"))
.await
.unwrap();
assert_eq!(
resp.headers()
.get("content-security-policy")
.map(|v| v.to_str().unwrap()),
Some("default-src *"),
"the route's own policy must survive the app-wide default"
);
}
#[tokio::test]
async fn nothing_extra_is_sent_without_configuration() {
let app = RouteBuilder::stateless()
.get(
"/exists",
handler(|_, _| async {
Ok::<_, ServeError>(Response::new(body(Bytes::from("ok"))))
}),
)
.seal();
let port = app.bind_ephemeral().await.unwrap();
let resp = reqwest::get(format!("http://127.0.0.1:{port}/exists"))
.await
.unwrap();
assert!(!resp.headers().contains_key("strict-transport-security"));
}
#[test]
fn invalid_names_and_values_are_rejected_at_configuration_time() {
assert!(RouteBuilder::stateless()
.with_response_header("Not A Header", "x")
.is_err());
assert!(RouteBuilder::stateless()
.with_response_header("X-Fine", "bad\nvalue")
.is_err());
}
#[test]
fn connection_owned_headers_are_refused() {
for name in ["Content-Length", "connection", "Transfer-Encoding"] {
assert!(
RouteBuilder::stateless()
.with_response_header(name, "1")
.is_err(),
"{name} should be refused"
);
}
}