use std::time::Duration;
use hitbox::policy::PolicyConfig;
use hitbox::{Config, Neutral};
use hitbox_http::extractors::Method as MethodExtractor;
use hitbox_http::extractors::path::PathExtractor;
use hitbox_http::predicates::request::Method as MethodPredicate;
use hitbox_moka::MokaBackend;
use hitbox_reqwest::{CacheMiddleware, NoopConcurrencyManager};
use reqwest::Client;
use reqwest_middleware::ClientBuilder;
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
#[tokio::test]
async fn test_cache_miss_then_hit() {
let mock_server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/data"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"message": "Hello from server"
})))
.expect(1) .mount(&mock_server)
.await;
let backend = MokaBackend::builder().max_entries(100).build();
let config = Config::builder()
.request_predicate(MethodPredicate::new(http::Method::GET).unwrap())
.response_predicate(Neutral::new())
.extractor(MethodExtractor::new().path("/{path}*"))
.policy(PolicyConfig::builder().ttl(Duration::from_secs(60)).build())
.build();
let middleware = CacheMiddleware::builder()
.backend(backend)
.config(config)
.concurrency_manager(NoopConcurrencyManager)
.build();
let client = ClientBuilder::new(Client::new()).with(middleware).build();
let url = format!("{}/data", mock_server.uri());
let response1 = client.get(&url).send().await.unwrap();
assert_eq!(response1.status(), 200);
assert_eq!(response1.headers().get("X-Cache-Status").unwrap(), "MISS");
let body1: serde_json::Value = serde_json::from_str(&response1.text().await.unwrap()).unwrap();
assert_eq!(body1["message"], "Hello from server");
let response2 = client.get(&url).send().await.unwrap();
assert_eq!(response2.status(), 200);
assert_eq!(response2.headers().get("X-Cache-Status").unwrap(), "HIT");
let body2: serde_json::Value = serde_json::from_str(&response2.text().await.unwrap()).unwrap();
assert_eq!(body2["message"], "Hello from server");
}
#[tokio::test]
async fn test_response_integrity() {
let mock_server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/headers"))
.respond_with(
ResponseTemplate::new(200)
.set_body_string("response body content")
.insert_header("X-Custom-Header", "custom-value")
.insert_header("X-Another-Header", "another-value"),
)
.expect(1)
.mount(&mock_server)
.await;
let backend = MokaBackend::builder().max_entries(100).build();
let config = Config::builder()
.request_predicate(MethodPredicate::new(http::Method::GET).unwrap())
.response_predicate(Neutral::new())
.extractor(MethodExtractor::new().path("/{path}*"))
.policy(PolicyConfig::builder().ttl(Duration::from_secs(60)).build())
.build();
let middleware = CacheMiddleware::builder()
.backend(backend)
.config(config)
.build();
let client = ClientBuilder::new(Client::new()).with(middleware).build();
let url = format!("{}/headers", mock_server.uri());
let response1 = client.get(&url).send().await.unwrap();
assert_eq!(response1.status(), 200);
assert_eq!(
response1.headers().get("X-Custom-Header").unwrap(),
"custom-value"
);
assert_eq!(
response1.headers().get("X-Another-Header").unwrap(),
"another-value"
);
assert_eq!(response1.text().await.unwrap(), "response body content");
let response2 = client.get(&url).send().await.unwrap();
assert_eq!(response2.headers().get("X-Cache-Status").unwrap(), "HIT");
assert_eq!(response2.status(), 200);
assert_eq!(
response2.headers().get("X-Custom-Header").unwrap(),
"custom-value"
);
assert_eq!(
response2.headers().get("X-Another-Header").unwrap(),
"another-value"
);
assert_eq!(response2.text().await.unwrap(), "response body content");
}
#[tokio::test]
async fn test_body_limit_exceeded_returns_full_body() {
use hitbox_http::predicates::body::{Body as BodyPredicate, Operation as BodyOperation};
let mock_server = MockServer::start().await;
let large_body = "x".repeat(200);
Mock::given(method("GET"))
.and(path("/large"))
.respond_with(ResponseTemplate::new(200).set_body_string(large_body.clone()))
.expect(2) .mount(&mock_server)
.await;
let backend = MokaBackend::builder().max_entries(100).build();
let config = Config::builder()
.request_predicate(MethodPredicate::new(http::Method::GET).unwrap())
.response_predicate(BodyPredicate::new(BodyOperation::Limit { bytes: 100 }))
.extractor(MethodExtractor::new().path("/{path}*"))
.policy(PolicyConfig::builder().ttl(Duration::from_secs(60)).build())
.build();
let middleware = CacheMiddleware::builder()
.backend(backend)
.config(config)
.build();
let client = ClientBuilder::new(Client::new()).with(middleware).build();
let url = format!("{}/large", mock_server.uri());
let response1 = client.get(&url).send().await.unwrap();
assert_eq!(response1.status(), 200);
assert_eq!(response1.headers().get("X-Cache-Status").unwrap(), "MISS");
let body1 = response1.text().await.unwrap();
assert_eq!(body1.len(), 200, "Full body should be returned");
assert_eq!(body1, large_body);
let response2 = client.get(&url).send().await.unwrap();
assert_eq!(response2.status(), 200);
assert_eq!(
response2.headers().get("X-Cache-Status").unwrap(),
"MISS",
"Should be MISS because body exceeded limit"
);
let body2 = response2.text().await.unwrap();
assert_eq!(body2.len(), 200, "Full body should still be returned");
assert_eq!(body2, large_body);
}