use autumn_web::config::AutumnConfig;
use autumn_web::test::TestApp;
use autumn_web::{get, routes};
fn test_config_with_server_timing(enabled: Option<bool>) -> AutumnConfig {
let mut config = AutumnConfig {
profile: Some("test".into()),
..Default::default()
};
config.security.csrf.enabled = false;
config.observability.server_timing = enabled;
config
}
#[get("/plain")]
async fn plain() -> String {
"hi".to_owned()
}
#[get("/html")]
async fn html_handler() -> axum::response::Html<&'static str> {
axum::response::Html("<p>hi</p>")
}
#[get("/json")]
async fn json_handler() -> axum::Json<serde_json::Value> {
axum::Json(serde_json::json!({ "ok": true }))
}
#[get("/slow")]
async fn slow_handler() -> String {
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
"done".to_owned()
}
#[get("/sse")]
async fn sse_handler() -> axum::response::Response<axum::body::Body> {
use axum::http::header;
axum::response::Response::builder()
.status(200)
.header(header::CONTENT_TYPE, "text/event-stream")
.body(axum::body::Body::from("data: hi\n\n"))
.expect("failed to build SSE response")
}
#[tokio::test]
async fn header_present_when_enabled_html() {
let client = TestApp::new()
.config(test_config_with_server_timing(Some(true)))
.routes(routes![html_handler])
.build();
let response = client.get("/html").send().await;
response.assert_ok();
let header = response
.header("server-timing")
.expect("server-timing header should be present when enabled");
assert!(
header.starts_with("total;dur="),
"expected header to start with `total;dur=`, got {header:?}"
);
let dur_str = header
.strip_prefix("total;dur=")
.and_then(|s| s.split(',').next())
.expect("could not extract total duration");
let dur: f64 = dur_str.parse().expect("total dur should be numeric");
assert!(dur >= 0.0, "expected non-negative total dur, got {dur}");
}
#[tokio::test]
async fn header_present_when_enabled_json() {
let client = TestApp::new()
.config(test_config_with_server_timing(Some(true)))
.routes(routes![json_handler])
.build();
let response = client.get("/json").send().await;
response.assert_ok();
let header = response
.header("server-timing")
.expect("server-timing header should be present on JSON responses too");
assert!(header.starts_with("total;dur="), "got {header:?}");
}
#[tokio::test]
async fn header_absent_when_disabled() {
let client = TestApp::new()
.config(test_config_with_server_timing(Some(false)))
.routes(routes![plain])
.build();
let response = client.get("/plain").send().await;
response.assert_ok();
assert!(
response.header("server-timing").is_none(),
"server-timing header must not appear when disabled: {:?}",
response.header("server-timing")
);
}
#[tokio::test]
async fn total_reflects_elapsed_time() {
let client = TestApp::new()
.config(test_config_with_server_timing(Some(true)))
.routes(routes![slow_handler])
.build();
let response = client.get("/slow").send().await;
response.assert_ok();
let header = response
.header("server-timing")
.expect("server-timing header should be present");
let dur_str = header
.strip_prefix("total;dur=")
.and_then(|s| s.split(',').next())
.expect("could not extract total duration");
let dur: f64 = dur_str.parse().expect("total dur should be numeric");
assert!(
dur >= 15.0,
"total dur should reflect the 20ms handler sleep, got {dur}"
);
}
#[tokio::test]
async fn sse_response_total_only() {
let client = TestApp::new()
.config(test_config_with_server_timing(Some(true)))
.routes(routes![sse_handler])
.build();
let response = client.get("/sse").send().await;
response.assert_ok();
let header = response
.header("server-timing")
.expect("SSE responses should still carry `total`");
assert!(header.contains("total;dur="), "got {header:?}");
assert!(
!header.contains("db;"),
"SSE responses must not carry `db` metric: {header:?}"
);
}
#[tokio::test]
async fn primary_and_fallback_do_not_duplicate_total() {
let client = TestApp::new()
.config(test_config_with_server_timing(Some(true)))
.routes(routes![html_handler])
.build();
let response = client.get("/html").send().await;
response.assert_ok();
let header = response
.header("server-timing")
.expect("server-timing header should be present");
assert_eq!(
header.matches("total;dur=").count(),
1,
"primary+fallback must not double-emit `total`: {header:?}"
);
}
#[tokio::test]
async fn zero_queries_omits_db() {
let client = TestApp::new()
.config(test_config_with_server_timing(Some(true)))
.routes(routes![html_handler])
.build();
let response = client.get("/html").send().await;
response.assert_ok();
let header = response
.header("server-timing")
.expect("server-timing header should be present");
assert!(header.starts_with("total;dur="), "got {header:?}");
assert!(
!header.contains(", db;"),
"requests with no DB queries must not include `db`: {header:?}"
);
}