use super::*;
#[tokio::test]
async fn cache_invalidated_by_post_request() {
let hit_count = Arc::new(AtomicU32::new(0));
let hit_count_clone = hit_count.clone();
let (addr, _counter) = h1_server_with(move |req| {
let count = hit_count_clone.clone();
async move {
let n = count.fetch_add(1, Ordering::SeqCst);
let method = req.method().to_string();
let path = req.uri().path().to_string();
if method == "GET" {
Ok::<_, Infallible>(
Response::builder()
.header("cache-control", "max-age=3600")
.body(Full::new(Bytes::from(format!("get-response-{n}"))))
.unwrap(),
)
} else {
Ok(Response::builder()
.body(Full::new(Bytes::from(format!(
"post-response method={method} path={path}"
))))
.unwrap())
}
}
})
.await;
let cache = aioduct::HttpCache::new();
let client = HttpEngineSend::<TokioRuntime, TcpConnector>::builder()
.cache(cache)
.timeout(Duration::from_secs(5))
.build()
.unwrap();
let resp = client
.get(&format!("http://{addr}/resource"))
.unwrap()
.send()
.await
.unwrap();
assert_eq!(resp.text().await.unwrap(), "get-response-0");
assert_eq!(hit_count.load(Ordering::SeqCst), 1);
let resp = client
.get(&format!("http://{addr}/resource"))
.unwrap()
.send()
.await
.unwrap();
assert_eq!(resp.text().await.unwrap(), "get-response-0");
assert_eq!(
hit_count.load(Ordering::SeqCst),
1,
"cache should serve second GET"
);
let resp = client
.post(&format!("http://{addr}/resource"))
.unwrap()
.body("data")
.send()
.await
.unwrap();
let body = resp.text().await.unwrap();
assert!(
body.contains("post-response"),
"POST should succeed, got: {body}"
);
let resp = client
.get(&format!("http://{addr}/resource"))
.unwrap()
.send()
.await
.unwrap();
let body = resp.text().await.unwrap();
assert_eq!(
body, "get-response-2",
"cache should be invalidated after POST"
);
assert_eq!(hit_count.load(Ordering::SeqCst), 3);
}
#[tokio::test]
async fn stale_if_error_serves_stale_on_503() {
let attempt = Arc::new(AtomicU32::new(0));
let attempt_clone = attempt.clone();
let (addr, _counter) = h1_server_with(move |req| {
let attempt = attempt_clone.clone();
async move {
let n = attempt.fetch_add(1, Ordering::SeqCst);
if n == 0 {
Ok::<_, Infallible>(
Response::builder()
.header("cache-control", "max-age=0, stale-if-error=3600")
.header("etag", "\"v1\"")
.body(Full::new(Bytes::from("fresh-data")))
.unwrap(),
)
} else {
let has_inm = req.headers().contains_key("if-none-match");
assert!(has_inm, "revalidation should send If-None-Match");
Ok(Response::builder()
.status(503)
.body(Full::new(Bytes::from("service unavailable")))
.unwrap())
}
}
})
.await;
let cache = aioduct::HttpCache::new();
let client = HttpEngineSend::<TokioRuntime, TcpConnector>::builder()
.cache(cache)
.timeout(Duration::from_secs(5))
.build()
.unwrap();
let resp = client
.get(&format!("http://{addr}/resource"))
.unwrap()
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200);
assert_eq!(resp.text().await.unwrap(), "fresh-data");
let resp = client
.get(&format!("http://{addr}/resource"))
.unwrap()
.send()
.await
.unwrap();
assert_eq!(
resp.status(),
200,
"stale-if-error should serve stale cache on 503"
);
assert_eq!(
resp.text().await.unwrap(),
"fresh-data",
"stale-if-error should serve original cached body"
);
assert_eq!(attempt.load(Ordering::SeqCst), 2);
}
#[tokio::test]
async fn stale_if_error_serves_stale_on_connection_failure() {
let (addr, _counter) = h1_server_with(|_req| async move {
Ok::<_, Infallible>(
Response::builder()
.header("cache-control", "max-age=0, stale-if-error=3600")
.header("etag", "\"conn-v1\"")
.body(Full::new(Bytes::from("originally-cached")))
.unwrap(),
)
})
.await;
let cache = aioduct::HttpCache::new();
let client = HttpEngineSend::<TokioRuntime, TcpConnector>::builder()
.cache(cache.clone())
.timeout(Duration::from_secs(5))
.build()
.unwrap();
let resp = client
.get(&format!("http://{addr}/stale-conn"))
.unwrap()
.send()
.await
.unwrap();
assert_eq!(resp.text().await.unwrap(), "originally-cached");
let dead_port = {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = listener.local_addr().unwrap().port();
drop(listener);
port
};
let client2 = HttpEngineSend::<TokioRuntime, TcpConnector>::builder()
.cache(cache)
.timeout(Duration::from_secs(2))
.resolver(move |_host: &str, _port: u16| {
let addr = std::net::SocketAddr::from(([127, 0, 0, 1], dead_port));
Box::pin(async move { Ok(addr) })
as std::pin::Pin<
Box<dyn std::future::Future<Output = std::io::Result<SocketAddr>> + Send>,
>
})
.build()
.unwrap();
let resp = client2
.get(&format!("http://{addr}/stale-conn"))
.unwrap()
.send()
.await
.unwrap();
assert_eq!(
resp.status(),
200,
"stale-if-error should serve cached data when connection fails"
);
assert_eq!(resp.text().await.unwrap(), "originally-cached");
}
#[tokio::test]
async fn finalize_response_stores_cacheable_response() {
let hit_count = Arc::new(AtomicU32::new(0));
let hit_count_clone = hit_count.clone();
let (addr, _counter) = h1_server_with(move |_req| {
let count = hit_count_clone.clone();
async move {
let n = count.fetch_add(1, Ordering::SeqCst);
Ok::<_, Infallible>(
Response::builder()
.header("cache-control", "max-age=3600")
.body(Full::new(Bytes::from(format!("cacheable-{n}"))))
.unwrap(),
)
}
})
.await;
let cache = aioduct::HttpCache::new();
let client = HttpEngineSend::<TokioRuntime, TcpConnector>::builder()
.cache(cache)
.timeout(Duration::from_secs(5))
.build()
.unwrap();
let resp = client
.get(&format!("http://{addr}/cached"))
.unwrap()
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200);
assert_eq!(resp.text().await.unwrap(), "cacheable-0");
let resp = client
.get(&format!("http://{addr}/cached"))
.unwrap()
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200);
assert_eq!(
resp.text().await.unwrap(),
"cacheable-0",
"second request should serve from cache"
);
assert_eq!(
hit_count.load(Ordering::SeqCst),
1,
"server should only be hit once due to caching"
);
}
#[tokio::test]
async fn cookie_jar_stores_set_cookie_from_response() {
let (addr, _counter) = h1_server_with(|req| async move {
let cookie_header = req
.headers()
.get("cookie")
.map(|v| v.to_str().unwrap_or("").to_string())
.unwrap_or_default();
if cookie_header.is_empty() {
Ok::<_, Infallible>(
Response::builder()
.header("set-cookie", "session=abc123; Path=/")
.body(Full::new(Bytes::from("cookie-set")))
.unwrap(),
)
} else {
Ok(Response::new(Full::new(Bytes::from(format!(
"cookie={cookie_header}"
)))))
}
})
.await;
let jar = aioduct::CookieJar::new();
let client = HttpEngineSend::<TokioRuntime, TcpConnector>::builder()
.cookie_jar(jar)
.timeout(Duration::from_secs(5))
.build()
.unwrap();
let resp = client
.get(&format!("http://{addr}/page"))
.unwrap()
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200);
assert_eq!(resp.text().await.unwrap(), "cookie-set");
let resp = client
.get(&format!("http://{addr}/page"))
.unwrap()
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200);
let body = resp.text().await.unwrap();
assert!(
body.contains("session=abc123"),
"cookie jar should send stored cookie, got: {body}"
);
}