#![cfg(feature = "axum")]
use axum::Router;
use axum::body::Body;
use axum::http::header::COOKIE;
use axum::http::{Request, StatusCode};
use axum::response::Response;
use axum::routing::get;
use tower::ServiceExt;
use kekse::{BadCookieHeader, CookieJarBuf};
async fn read_sid(cookies: CookieJarBuf) -> String {
cookies
.jar_strict()
.value
.get_all("SID")
.find(|c| !c.value().is_empty())
.map(|c| c.value().to_owned())
.unwrap_or_else(|| "<none>".to_owned())
}
async fn read_pref(cookies: CookieJarBuf) -> String {
cookies
.jar()
.value
.get("pref")
.map(|c| c.value().to_owned())
.unwrap_or_else(|| "<none>".to_owned())
}
async fn read_sid_or_400(cookies: CookieJarBuf) -> Result<String, BadCookieHeader> {
let jar = cookies.try_jar_strict()?;
Ok(jar
.get("SID")
.map(|c| c.value().to_owned())
.unwrap_or_else(|| "<none>".to_owned()))
}
fn app() -> Router {
Router::new()
.route("/sid", get(read_sid))
.route("/pref", get(read_pref))
.route("/checked", get(read_sid_or_400))
}
async fn body_string(resp: Response) -> String {
let bytes = axum::body::to_bytes(resp.into_body(), usize::MAX)
.await
.unwrap();
String::from_utf8(bytes.to_vec()).unwrap()
}
fn get_request(uri: &str, cookie: Option<&str>) -> Request<Body> {
let mut builder = Request::builder().uri(uri);
if let Some(c) = cookie {
builder = builder.header(COOKIE, c);
}
builder.body(Body::empty()).unwrap()
}
#[tokio::test]
async fn extracts_and_reads_session_strictly() {
let resp = app()
.oneshot(get_request("/sid", Some("a=1; SID=deadbeef; b=2")))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(body_string(resp).await, "deadbeef");
}
#[tokio::test]
async fn strict_view_refuses_a_spaced_session_value() {
let resp = app()
.oneshot(get_request("/sid", Some("SID=dead beef")))
.await
.unwrap();
assert_eq!(body_string(resp).await, "<none>");
}
#[tokio::test]
async fn missing_cookie_header_extracts_infallibly() {
let resp = app().oneshot(get_request("/sid", None)).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(body_string(resp).await, "<none>");
}
#[tokio::test]
async fn lenient_view_reads_a_quoted_preference() {
let resp = app()
.oneshot(get_request("/pref", Some(r#"pref="dark mode""#)))
.await
.unwrap();
assert_eq!(body_string(resp).await, "dark mode");
}
#[tokio::test]
async fn fail_soft_junk_never_hides_the_session_cookie() {
let header = "theme; consent=\"a, b\"; SID=deadbeef; tracking=";
let resp = app()
.oneshot(get_request("/sid", Some(header)))
.await
.unwrap();
assert_eq!(body_string(resp).await, "deadbeef");
}
#[tokio::test]
async fn fail_hard_route_rejects_a_mangled_header_with_400() {
let resp = app()
.oneshot(get_request("/checked", Some("SID=dead beef; =evil")))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let body = body_string(resp).await;
assert_eq!(body, "2 malformed pair(s) in the Cookie header");
assert!(
!body.contains("dead"),
"the body must not echo header bytes"
);
}
#[tokio::test]
async fn fail_hard_route_serves_a_clean_header_normally() {
let resp = app()
.oneshot(get_request("/checked", Some("a=1; SID=deadbeef")))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(body_string(resp).await, "deadbeef");
let resp = app().oneshot(get_request("/checked", None)).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(body_string(resp).await, "<none>");
}
#[tokio::test]
async fn obs_text_in_the_header_never_hides_the_session_cookie() {
let header = axum::http::HeaderValue::from_bytes(b"pref=caf\xE9; SID=deadbeef").unwrap();
let request = Request::builder()
.uri("/sid")
.header(COOKIE, header)
.body(Body::empty())
.unwrap();
let resp = app().oneshot(request).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(body_string(resp).await, "deadbeef");
}