use std::sync::Arc;
use std::time::SystemTime;
use axum::{
Router,
body::Body,
routing::{get, post},
};
use http::{Request, StatusCode, header::SET_COOKIE};
use http_body_util::BodyExt;
use serde::{Deserialize, Serialize};
use seshcookie::{Session, SessionConfig, SessionKeys, SessionLayer};
use tokio::sync::Mutex;
use tower::ServiceExt;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
struct AppSession {
user_id: u64,
role: String,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
struct AppSessionB {
page: u32,
}
fn build_keys() -> SessionKeys {
SessionKeys::new(b"fedcba9876543210fedcba9876543210").expect("32-byte key is valid")
}
fn build_layer() -> SessionLayer<AppSession> {
let config = SessionConfig::default().secure(false);
SessionLayer::<AppSession>::new(build_keys(), config).expect("layer construction succeeds")
}
fn build_app() -> Router {
Router::new()
.route("/insert", post(insert_handler))
.route("/get", get(get_handler))
.route("/optional", get(optional_handler))
.route("/required", get(required_handler))
.route("/two-handles", get(two_handles_handler))
.layer(build_layer())
}
async fn insert_handler(session: Session<AppSession>) -> &'static str {
session
.insert(AppSession {
user_id: 1,
role: "admin".into(),
})
.await;
"inserted"
}
async fn get_handler(session: Session<AppSession>) -> String {
match session.get().await {
Some(s) => format!("{}:{}", s.user_id, s.role),
None => "none".into(),
}
}
async fn optional_handler(session: Option<Session<AppSession>>) -> &'static str {
if session.is_some() { "some" } else { "none" }
}
async fn required_handler(_session: Session<AppSession>) -> &'static str {
"required-ok"
}
async fn two_handles_handler(s1: Session<AppSession>, s2: Session<AppSession>) -> String {
s1.insert(AppSession {
user_id: 99,
role: "q".into(),
})
.await;
let seen = s2.get().await.expect("s2 must observe s1's insert");
format!("{}:{}", seen.user_id, seen.role)
}
async fn body_text(response: http::Response<Body>) -> String {
let bytes = response
.into_body()
.collect()
.await
.expect("collecting an in-memory body never fails")
.to_bytes();
String::from_utf8(bytes.to_vec()).expect("test handlers all produce UTF-8")
}
fn first_set_cookie(response: &http::Response<Body>) -> String {
response
.headers()
.get(SET_COOKIE)
.expect("response carries a Set-Cookie header")
.to_str()
.expect("Set-Cookie value is ASCII")
.to_string()
}
fn cookie_name_value(set_cookie: &str) -> String {
set_cookie
.split(';')
.next()
.expect("Set-Cookie always has at least name=value")
.to_string()
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn axum_handler_without_layer_returns_http_500_seshcookie_rs_ac5_4() {
let app: Router = Router::new().route("/required", get(required_handler));
let req = Request::builder()
.method("GET")
.uri("/required")
.body(Body::empty())
.expect("test request is well-formed");
let response = app.oneshot(req).await.expect("oneshot delivers a response");
assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
let body = body_text(response).await;
assert!(
body.contains("not mounted"),
"AC5.4 response body must mention the missing layer; got {body:?}"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn axum_handler_with_layer_extracts_successfully_seshcookie_rs_ac5_1() {
let app = build_app();
let req = Request::builder()
.method("POST")
.uri("/insert")
.body(Body::empty())
.expect("test request is well-formed");
let response = app.clone().oneshot(req).await.expect("first call succeeds");
assert_eq!(response.status(), StatusCode::OK);
let set_cookie = first_set_cookie(&response);
let cookie_pair = cookie_name_value(&set_cookie);
let body = body_text(response).await;
assert_eq!(body, "inserted");
let req = Request::builder()
.method("GET")
.uri("/get")
.header(http::header::COOKIE, &cookie_pair)
.body(Body::empty())
.expect("test request is well-formed");
let response = app.oneshot(req).await.expect("second call succeeds");
assert_eq!(response.status(), StatusCode::OK);
let body = body_text(response).await;
assert_eq!(
body, "1:admin",
"the cookie round-trips the inserted payload"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn axum_handler_with_option_extractor_gets_none_when_layer_missing_seshcookie_rs_ac5_3() {
let app: Router = Router::new().route("/optional", get(optional_handler));
let req = Request::builder()
.method("GET")
.uri("/optional")
.body(Body::empty())
.expect("test request is well-formed");
let response = app.oneshot(req).await.expect("oneshot delivers a response");
assert_eq!(response.status(), StatusCode::OK);
let body = body_text(response).await;
assert_eq!(body, "none");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn axum_handler_with_option_extractor_gets_some_when_layer_present_seshcookie_rs_ac5_3() {
let app = build_app();
let req = Request::builder()
.method("GET")
.uri("/optional")
.body(Body::empty())
.expect("test request is well-formed");
let response = app.oneshot(req).await.expect("oneshot delivers a response");
assert_eq!(response.status(), StatusCode::OK);
let body = body_text(response).await;
assert_eq!(body, "some");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn axum_two_handles_share_underlying_state_seshcookie_rs_ac5_5() {
let app = build_app();
let req = Request::builder()
.method("GET")
.uri("/two-handles")
.body(Body::empty())
.expect("test request is well-formed");
let response = app.oneshot(req).await.expect("oneshot delivers a response");
assert_eq!(response.status(), StatusCode::OK);
let body = body_text(response).await;
assert_eq!(body, "99:q", "AC5.5 demands the two handles share state");
}
async fn insert_a_handler(session: Session<AppSession>) -> &'static str {
session
.insert(AppSession {
user_id: 7,
role: "alpha".into(),
})
.await;
"a"
}
async fn insert_b_handler(session: Session<AppSessionB>) -> &'static str {
session.insert(AppSessionB { page: 42 }).await;
"b"
}
async fn read_a_handler(session: Session<AppSession>) -> String {
match session.get().await {
Some(s) => format!("{}:{}", s.user_id, s.role),
None => "none".into(),
}
}
async fn read_b_handler(session: Session<AppSessionB>) -> String {
match session.get().await {
Some(s) => format!("page={}", s.page),
None => "none".into(),
}
}
fn build_two_layer_app() -> Router {
let config_a = SessionConfig::default()
.secure(false)
.cookie_name("session_a");
let config_b = SessionConfig::default()
.secure(false)
.cookie_name("session_b");
let layer_a = SessionLayer::<AppSession>::new(build_keys(), config_a).expect("layer A");
let layer_b = SessionLayer::<AppSessionB>::new(build_keys(), config_b).expect("layer B");
Router::new()
.route("/insert-a", post(insert_a_handler))
.route("/insert-b", post(insert_b_handler))
.route("/read-a", get(read_a_handler))
.route("/read-b", get(read_b_handler))
.layer(layer_a)
.layer(layer_b)
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn axum_two_layers_with_distinct_types_coexist_seshcookie_rs_ac5_2() {
let app = build_two_layer_app();
let req = Request::builder()
.method("POST")
.uri("/insert-a")
.body(Body::empty())
.expect("test request is well-formed");
let response_a = app
.clone()
.oneshot(req)
.await
.expect("insert-a delivers a response");
assert_eq!(response_a.status(), StatusCode::OK);
let cookie_a = cookie_name_value(&first_set_cookie(&response_a));
assert!(
cookie_a.starts_with("session_a="),
"layer A must emit a cookie under its configured name; got {cookie_a:?}"
);
let req = Request::builder()
.method("POST")
.uri("/insert-b")
.body(Body::empty())
.expect("test request is well-formed");
let response_b = app
.clone()
.oneshot(req)
.await
.expect("insert-b delivers a response");
assert_eq!(response_b.status(), StatusCode::OK);
let cookie_b = cookie_name_value(&first_set_cookie(&response_b));
assert!(
cookie_b.starts_with("session_b="),
"layer B must emit a cookie under its configured name; got {cookie_b:?}"
);
let cookie_header = format!("{cookie_a}; {cookie_b}");
let req = Request::builder()
.method("GET")
.uri("/read-a")
.header(http::header::COOKIE, &cookie_header)
.body(Body::empty())
.expect("test request is well-formed");
let response = app
.clone()
.oneshot(req)
.await
.expect("read-a delivers a response");
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(body_text(response).await, "7:alpha");
let req = Request::builder()
.method("GET")
.uri("/read-b")
.header(http::header::COOKIE, &cookie_header)
.body(Body::empty())
.expect("test request is well-formed");
let response = app.oneshot(req).await.expect("read-b delivers a response");
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(body_text(response).await, "page=42");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn axum_100_concurrent_requests_multi_thread_runtime_seshcookie_rs_ac9_2_ac9_3() {
let layer = build_layer();
let app: Router = Router::new()
.route("/get", get(get_handler))
.layer(layer.clone());
let now = SystemTime::now();
let cookies: Vec<(u64, String)> = (0..100u64)
.map(|i| {
let payload = AppSession {
user_id: i,
role: format!("role-{i}"),
};
let cookie = seshcookie::__testing::encode_cookie_for_layer(&layer, &payload, now);
(i, cookie)
})
.collect();
let mut handles = Vec::with_capacity(cookies.len());
for (id, cookie_value) in cookies {
let app = app.clone();
handles.push(tokio::spawn(async move {
let req = Request::builder()
.method("GET")
.uri("/get")
.header(http::header::COOKIE, format!("session={cookie_value}"))
.body(Body::empty())
.expect("test request is well-formed");
let response = app.oneshot(req).await.expect("oneshot delivers a response");
assert_eq!(
response.status(),
StatusCode::OK,
"all 100 requests must succeed"
);
let body = body_text(response).await;
(id, body)
}));
}
for handle in handles {
let (id, body) = handle.await.expect("task did not panic");
let expected = format!("{id}:role-{id}");
assert_eq!(
body, expected,
"request {id} must observe its own cookie's payload, no cross-request bleed"
);
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn axum_all_integration_tests_run_on_multi_thread_runtime_seshcookie_rs_ac9_3() {
let source = std::fs::read_to_string(file!())
.expect("test binary can read its own source file at test time");
let mut total_attrs = 0usize;
let mut bare_count = 0usize;
for line in source.lines() {
let trimmed = line.trim_start();
if !trimmed.starts_with("#[tokio::test") {
continue;
}
total_attrs += 1;
let after = &trimmed["#[tokio::test".len()..];
if !after.starts_with('(') {
bare_count += 1;
}
}
assert!(
total_attrs > 0,
"AC9.3 self-audit must observe at least one #[tokio::test] attribute; \
found {total_attrs} (the regex above is broken if this fires)"
);
assert_eq!(
bare_count, 0,
"AC9.3: every #[tokio::test] in tests/axum_integration.rs must use \
flavor = \"multi_thread\", worker_threads = 4 — found {bare_count} bare attribute(s)"
);
}
#[allow(dead_code)]
fn _async_discipline_anchor() -> Arc<Mutex<u8>> {
Arc::new(Mutex::new(0))
}