use axum::body::Body;
use axum::extract::Request;
use axum::http::{header, StatusCode};
use axum::response::Response;
use larust_http::{responsecache, Route};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use tower::ServiceExt;
async fn connect_test_db() {
let dir = tempfile::tempdir().unwrap().keep();
let database_url = format!("sqlite://{}/test.sqlite", dir.display());
larust_orm::connect(&database_url).await.unwrap();
}
fn cookie_from(response: &Response) -> String {
response
.headers()
.get(header::SET_COOKIE)
.expect("a session-backed router should set a session cookie")
.to_str()
.unwrap()
.split(';')
.next()
.unwrap()
.to_string()
}
#[tokio::test]
async fn caches_a_200_get_response_bypasses_writes_and_forgets_on_demand() {
connect_test_db().await;
let hits = Arc::new(AtomicUsize::new(0));
let ok_hits = hits.clone();
let not_found_hits = Arc::new(AtomicUsize::new(0));
let nf_hits = not_found_hits.clone();
let router = Route::get("/ok", move || {
let hits = ok_hits.clone();
async move {
let count = hits.fetch_add(1, Ordering::SeqCst) + 1;
format!("hit {count}")
}
})
.post("/ok", || async move { "posted" })
.get("/missing", move || {
let hits = nf_hits.clone();
async move {
hits.fetch_add(1, Ordering::SeqCst);
StatusCode::NOT_FOUND
}
})
.middleware(responsecache::for_minutes(5))
.into_axum_router();
let response = router
.clone()
.oneshot(Request::get("/ok").body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap();
assert_eq!(&body[..], b"hit 1");
assert_eq!(hits.load(Ordering::SeqCst), 1);
let response = router
.clone()
.oneshot(Request::get("/ok").body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap();
assert_eq!(&body[..], b"hit 1");
assert_eq!(hits.load(Ordering::SeqCst), 1);
let response = router
.clone()
.oneshot(Request::post("/ok").body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap();
assert_eq!(&body[..], b"posted");
let response = router
.clone()
.oneshot(Request::get("/ok").body(Body::empty()).unwrap())
.await
.unwrap();
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap();
assert_eq!(&body[..], b"hit 1");
assert_eq!(hits.load(Ordering::SeqCst), 1);
router
.clone()
.oneshot(Request::get("/missing").body(Body::empty()).unwrap())
.await
.unwrap();
router
.clone()
.oneshot(Request::get("/missing").body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(not_found_hits.load(Ordering::SeqCst), 2);
responsecache::forget("/ok").await.unwrap();
let response = router
.clone()
.oneshot(Request::get("/ok").body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap();
assert_eq!(&body[..], b"hit 2");
assert_eq!(hits.load(Ordering::SeqCst), 2);
let session_hits = Arc::new(AtomicUsize::new(0));
let counted_hits = session_hits.clone();
let pool = larust_orm::pool().unwrap();
let session_router = Route::get(
"/dashboard",
move |session: larust_http::session::Session| {
let hits = counted_hits.clone();
async move {
session.insert("touched", true).await.unwrap();
let count = hits.fetch_add(1, Ordering::SeqCst) + 1;
format!("render {count}")
}
},
)
.middleware(responsecache::for_minutes_per_session(5))
.with_sessions(pool, true)
.await
.unwrap()
.into_axum_router();
let a_first = session_router
.clone()
.oneshot(Request::get("/dashboard").body(Body::empty()).unwrap())
.await
.unwrap();
let a_cookie = cookie_from(&a_first);
let a_first_body = axum::body::to_bytes(a_first.into_body(), usize::MAX)
.await
.unwrap();
assert_eq!(&a_first_body[..], b"render 1");
let b_first = session_router
.clone()
.oneshot(Request::get("/dashboard").body(Body::empty()).unwrap())
.await
.unwrap();
let b_cookie = cookie_from(&b_first);
assert_ne!(
a_cookie, b_cookie,
"two fresh requests must get distinct sessions"
);
let b_first_body = axum::body::to_bytes(b_first.into_body(), usize::MAX)
.await
.unwrap();
assert_eq!(
&b_first_body[..],
b"render 2",
"a different session must not reuse session A's response"
);
assert_eq!(session_hits.load(Ordering::SeqCst), 2);
let a_second = session_router
.clone()
.oneshot(
Request::get("/dashboard")
.header(header::COOKIE, &a_cookie)
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
let a_second_body = axum::body::to_bytes(a_second.into_body(), usize::MAX)
.await
.unwrap();
assert_eq!(
&a_second_body[..],
b"render 3",
"A's second request is a cache miss (nothing was stored on its first) and runs for real"
);
assert_eq!(session_hits.load(Ordering::SeqCst), 3);
let a_third = session_router
.oneshot(
Request::get("/dashboard")
.header(header::COOKIE, &a_cookie)
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
let a_third_body = axum::body::to_bytes(a_third.into_body(), usize::MAX)
.await
.unwrap();
assert_eq!(
&a_third_body[..],
b"render 3",
"A's third request must hit the entry stored on its second"
);
assert_eq!(
session_hits.load(Ordering::SeqCst),
3,
"the handler must not run again for a real per-session cache hit"
);
}