use http::StatusCode;
use rapina::cache::{CACHE_STATUS_HEADER, CacheConfig};
use rapina::prelude::*;
use rapina::testing::TestClient;
#[tokio::test]
async fn test_cache_miss_then_hit() {
let app = Rapina::new()
.with_introspection(false)
.with_cache(CacheConfig::in_memory(100))
.await
.unwrap()
.router(
Router::new().route(http::Method::GET, "/data", |_, _, _| async {
let mut response = http::Response::builder()
.status(StatusCode::OK)
.header("content-type", "application/json")
.body(http_body_util::Full::new(bytes::Bytes::from(
r#"{"value":42}"#,
)))
.unwrap();
response
.headers_mut()
.insert("x-rapina-cache-ttl", http::HeaderValue::from_static("60"));
response
}),
);
let client = TestClient::new(app).await;
let response = client.get("/data").send().await;
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(response.headers().get(CACHE_STATUS_HEADER).unwrap(), "MISS");
assert_eq!(response.text(), r#"{"value":42}"#);
let response = client.get("/data").send().await;
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(response.headers().get(CACHE_STATUS_HEADER).unwrap(), "HIT");
assert_eq!(response.text(), r#"{"value":42}"#);
}
#[tokio::test]
async fn test_cache_strips_internal_ttl_header() {
let app = Rapina::new()
.with_introspection(false)
.with_cache(CacheConfig::in_memory(100))
.await
.unwrap()
.router(
Router::new().route(http::Method::GET, "/data", |_, _, _| async {
let mut response = http::Response::builder()
.status(StatusCode::OK)
.body(http_body_util::Full::new(bytes::Bytes::from("ok")))
.unwrap();
response
.headers_mut()
.insert("x-rapina-cache-ttl", http::HeaderValue::from_static("30"));
response
}),
);
let client = TestClient::new(app).await;
let response = client.get("/data").send().await;
assert!(response.headers().get("x-rapina-cache-ttl").is_none());
}
#[tokio::test]
async fn test_cache_does_not_cache_without_ttl_header() {
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
let counter = Arc::new(AtomicU32::new(0));
let counter_clone = counter.clone();
let app = Rapina::new()
.with_introspection(false)
.with_cache(CacheConfig::in_memory(100))
.await
.unwrap()
.router(
Router::new().route(http::Method::GET, "/data", move |_, _, _| {
let counter = counter_clone.clone();
async move {
let n = counter.fetch_add(1, Ordering::Relaxed);
format!("call {}", n)
}
}),
);
let client = TestClient::new(app).await;
let r1 = client.get("/data").send().await;
let r2 = client.get("/data").send().await;
assert!(r1.headers().get(CACHE_STATUS_HEADER).is_none());
assert!(r2.headers().get(CACHE_STATUS_HEADER).is_none());
assert_ne!(r1.text(), r2.text());
}
#[tokio::test]
async fn test_mutation_invalidates_cache() {
let app = Rapina::new()
.with_introspection(false)
.with_cache(CacheConfig::in_memory(100))
.await
.unwrap()
.router(
Router::new()
.route(http::Method::GET, "/items", |_, _, _| async {
let mut response = http::Response::builder()
.status(StatusCode::OK)
.body(http_body_util::Full::new(bytes::Bytes::from("items")))
.unwrap();
response
.headers_mut()
.insert("x-rapina-cache-ttl", http::HeaderValue::from_static("60"));
response
})
.route(http::Method::POST, "/items", |_, _, _| async {
StatusCode::CREATED
}),
);
let client = TestClient::new(app).await;
let response = client.get("/items").send().await;
assert_eq!(response.headers().get(CACHE_STATUS_HEADER).unwrap(), "MISS");
let response = client.get("/items").send().await;
assert_eq!(response.headers().get(CACHE_STATUS_HEADER).unwrap(), "HIT");
let response = client.post("/items").send().await;
assert_eq!(response.status(), StatusCode::CREATED);
let response = client.get("/items").send().await;
assert_eq!(response.headers().get(CACHE_STATUS_HEADER).unwrap(), "MISS");
}
#[tokio::test]
async fn test_cache_preserves_response_headers() {
let app = Rapina::new()
.with_introspection(false)
.with_cache(CacheConfig::in_memory(100))
.await
.unwrap()
.router(
Router::new().route(http::Method::GET, "/data", |_, _, _| async {
let mut response = http::Response::builder()
.status(StatusCode::OK)
.header("content-type", "application/json")
.header("x-custom", "preserved")
.body(http_body_util::Full::new(bytes::Bytes::from("{}")))
.unwrap();
response
.headers_mut()
.insert("x-rapina-cache-ttl", http::HeaderValue::from_static("60"));
response
}),
);
let client = TestClient::new(app).await;
client.get("/data").send().await;
let response = client.get("/data").send().await;
assert_eq!(response.headers().get(CACHE_STATUS_HEADER).unwrap(), "HIT");
assert_eq!(
response.headers().get("content-type").unwrap(),
"application/json"
);
assert_eq!(response.headers().get("x-custom").unwrap(), "preserved");
}
#[tokio::test]
async fn test_cache_only_caches_get() {
let app = Rapina::new()
.with_introspection(false)
.with_cache(CacheConfig::in_memory(100))
.await
.unwrap()
.router(
Router::new().route(http::Method::POST, "/data", |_, _, _| async {
let mut response = http::Response::builder()
.status(StatusCode::CREATED)
.body(http_body_util::Full::new(bytes::Bytes::from("created")))
.unwrap();
response
.headers_mut()
.insert("x-rapina-cache-ttl", http::HeaderValue::from_static("60"));
response
}),
);
let client = TestClient::new(app).await;
let response = client.post("/data").send().await;
assert_eq!(response.status(), StatusCode::CREATED);
assert!(response.headers().get(CACHE_STATUS_HEADER).is_none());
}
#[tokio::test]
async fn test_cache_query_params_affect_key() {
let app = Rapina::new()
.with_introspection(false)
.with_cache(CacheConfig::in_memory(100))
.await
.unwrap()
.router(
Router::new().route(http::Method::GET, "/search", |req, _, _| async move {
let query = req.uri().query().unwrap_or("none").to_string();
let mut response = http::Response::builder()
.status(StatusCode::OK)
.body(http_body_util::Full::new(bytes::Bytes::from(query)))
.unwrap();
response
.headers_mut()
.insert("x-rapina-cache-ttl", http::HeaderValue::from_static("60"));
response
}),
);
let client = TestClient::new(app).await;
let r1 = client.get("/search?q=rust").send().await;
assert_eq!(r1.headers().get(CACHE_STATUS_HEADER).unwrap(), "MISS");
let r2 = client.get("/search?q=python").send().await;
assert_eq!(r2.headers().get(CACHE_STATUS_HEADER).unwrap(), "MISS");
let r3 = client.get("/search?q=rust").send().await;
assert_eq!(r3.headers().get(CACHE_STATUS_HEADER).unwrap(), "HIT");
assert_eq!(r3.text(), "q=rust");
}