use axum::Router;
use axum::body::Body;
use axum::routing::get;
use http::header::{AUTHORIZATION, CACHE_CONTROL, COOKIE};
use http::{Request, StatusCode};
use tower::ServiceExt;
use umbral::prelude::Plugin;
use umbral_security::{SecurityConfig, SecurityPlugin};
fn app() -> Router {
SecurityPlugin::new().wrap_router(routes())
}
fn routes() -> Router {
Router::new()
.route("/", get(|| async { "public" }))
.route("/admin/", get(|| async { "admin" }))
.route(
"/assets/app.css",
get(|| async { ([(CACHE_CONTROL, "public, max-age=86400")], "css") }),
)
}
async fn cache_control(app: Router, req: Request<Body>) -> Option<String> {
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
resp.headers()
.get(CACHE_CONTROL)
.map(|v| v.to_str().unwrap().to_string())
}
fn get_with(uri: &str, header: Option<(http::HeaderName, &str)>) -> Request<Body> {
let mut b = Request::builder().method("GET").uri(uri);
if let Some((name, value)) = header {
b = b.header(name, value);
}
b.body(Body::empty()).unwrap()
}
#[tokio::test]
async fn session_cookie_request_is_no_store_and_private() {
let req = get_with(
"/admin/",
Some((COOKIE, "umbral_session=abc123; umbral_csrf_token=deadbeef")),
);
let cc = cache_control(app(), req)
.await
.expect("an authenticated response must carry Cache-Control");
assert!(cc.contains("no-store"), "expected no-store, got `{cc}`");
assert!(cc.contains("private"), "expected private, got `{cc}`");
}
#[tokio::test]
async fn authorization_header_request_is_no_store() {
let req = get_with("/admin/", Some((AUTHORIZATION, "Bearer sometoken")));
let cc = cache_control(app(), req)
.await
.expect("a bearer-authenticated response must carry Cache-Control");
assert!(cc.contains("no-store"), "expected no-store, got `{cc}`");
}
#[tokio::test]
async fn anonymous_request_keeps_no_cache_control() {
let req = get_with("/", Some((COOKIE, "umbral_csrf_token=deadbeef")));
assert_eq!(
cache_control(app(), req).await,
None,
"an anonymous page must stay cacheable",
);
}
#[tokio::test]
async fn handler_set_cache_control_wins() {
let req = get_with("/assets/app.css", Some((COOKIE, "umbral_session=abc123")));
assert_eq!(
cache_control(app(), req).await.as_deref(),
Some("public, max-age=86400"),
"the handler's own Cache-Control must not be clobbered",
);
}
#[tokio::test]
async fn private_cache_can_be_disabled() {
let plugin = SecurityPlugin::with_config(SecurityConfig {
private_cache: false,
..SecurityConfig::default()
});
let req = get_with("/admin/", Some((COOKIE, "umbral_session=abc123")));
assert_eq!(
cache_control(plugin.wrap_router(routes()), req).await,
None,
"private_cache = false disables the header",
);
}
#[tokio::test]
async fn lookalike_cookie_is_not_a_session() {
let req = get_with(
"/",
Some((COOKIE, "not_umbral_session=abc; x=umbral_session")),
);
assert_eq!(
cache_control(app(), req).await,
None,
"only a real `umbral_session=` cookie marks the request personalised",
);
}