This commit is contained in:
Mateusz Gruszczyński committed 2026-10-13 01:34:42 +02:00
1 parent 101df02a5f
commit bb025dcf44
27 files changed
+541 -85

No files matched your search

+166
View File
@@ -0,0 +1,166 @@
use super::*;
use crate::{cache, web_server};
use http_body_util::BodyExt;
use tempfile::TempDir;
use std::sync::Arc;
fn cache_settings() -> Settings {
Settings {
enabled: true,
expose_status: true,
expose_hits: true,
expose_cache_status: true,
..Default::default()
}
}
async fn fixture() -> (TempDir, web_server::CompiledService, Arc<Cache>, Ticket, Settings) {
let dir = TempDir::new().unwrap();
std::fs::write(dir.path().join("shCore.js"), b"before").unwrap();
let service = web_server::Service {
id: "wordpress".into(),
name: "WordPress".into(),
document_root: dir.path().to_path_buf(),
cache_control: Some("public, max-age=604800".into()),
..Default::default()
}.compile().unwrap();
let cache = Cache::new(1);
let settings = cache_settings();
let ticket = cache.ticket(
1, "https", "blog.example.test", "wordpress",
"/shCore.js?ver=3.0.9b", &HeaderMap::new(),
).unwrap();
let initial = serve_file(&service, Method::GET, HeaderMap::new()).await;
assert_eq!(initial.status(), StatusCode::OK);
let plan = cache::plan(initial.status(), initial.headers(), &settings, Duration::ZERO)
.expect("static origin response is cacheable");
cache.wrap(initial, ticket.clone(), plan, &settings)
.into_body().collect().await.unwrap();
(dir, service, cache, ticket, settings)
}
async fn serve_file(service: &web_server::CompiledService, method: Method, headers: HeaderMap) -> Response {
web_server::serve(
service, method, headers,
"/shCore.js?ver=3.0.9b".parse().unwrap(), "/shCore.js", Body::empty(),
web_server::RequestContext {
client_ip: "127.0.0.1".parse().unwrap(),
client_port: Some(12345),
server_name: "blog.example.test".into(),
server_port: 443,
tls: true,
version: http::Version::HTTP_2,
},
).await
}
async fn validate(
service: &web_server::CompiledService, cache: &Cache, ticket: &Ticket,
settings: &Settings, method: Method, client: HeaderMap,
) -> Option<WebRevalidated> {
let cached = cache.revalidation_tiered(ticket).await.unwrap();
let (headers, sent) = web_origin_request_headers(&client, Some(&cached));
assert!(sent, "stored validator must be used for local source revalidation");
let origin = serve_file(service, method.clone(), headers).await;
if origin.status() != StatusCode::NOT_MODIFIED {
return None;
}
finish_web_revalidation(cache, ticket, &cached, origin.headers(), settings,
Duration::ZERO, &client, &method, false, false, |_| {})
}
#[tokio::test]
async fn browser_refresh_304_preserves_web_server_hit_counter() {
let (_dir, service, cache, ticket, settings) = fixture().await;
assert_eq!(cache.get_tiered(&ticket, &settings).await.unwrap().hits, 1);
for directive in ["max-age=0", "no-cache"] {
let mut client = HeaderMap::new();
client.insert("cache-control", directive.parse().unwrap());
let response = validate(&service, &cache, &ticket, &settings, Method::GET, client)
.await.unwrap().response;
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(response.headers()["x-cache"], "HIT");
let hits = response.headers()["x-cache-hits"].to_str().unwrap().parse::<u64>().unwrap();
assert!(hits >= 2);
assert_eq!(response.headers()["x-cache-reason"], "cache_revalidated");
assert_eq!(response.into_body().collect().await.unwrap().to_bytes(), "before");
}
assert_eq!(cache.get_tiered(&ticket, &settings).await.unwrap().hits, 4);
}
#[tokio::test]
async fn browser_conditional_refresh_returns_304_without_resetting_hits() {
let (_dir, service, cache, ticket, settings) = fixture().await;
let etag = cache.revalidation_tiered(&ticket).await.unwrap().headers()["etag"].clone();
let mut client = HeaderMap::new();
client.insert("cache-control", HeaderValue::from_static("no-cache"));
client.insert("if-none-match", etag);
let response = validate(&service, &cache, &ticket, &settings, Method::GET, client)
.await.unwrap().response;
assert_eq!(response.status(), StatusCode::NOT_MODIFIED);
assert!(!response.headers().contains_key("content-length"));
assert_eq!(response.headers()["x-cache-hits"], "1");
assert!(response.into_body().collect().await.unwrap().to_bytes().is_empty());
assert_eq!(cache.get_tiered(&ticket, &settings).await.unwrap().hits, 2);
}
#[tokio::test]
async fn mismatched_browser_validator_cannot_validate_different_cached_object() {
let (_dir, service, cache, ticket, settings) = fixture().await;
let mut client = HeaderMap::new();
client.insert("cache-control", HeaderValue::from_static("no-cache"));
client.insert("if-none-match", HeaderValue::from_static("\"unrelated\""));
let response = validate(&service, &cache, &ticket, &settings, Method::GET, client)
.await.unwrap().response;
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(response.into_body().collect().await.unwrap().to_bytes(), "before");
}
#[tokio::test]
async fn range_and_head_requests_use_validated_cached_body() {
let (_dir, service, cache, ticket, settings) = fixture().await;
let mut range = HeaderMap::new();
range.insert("cache-control", HeaderValue::from_static("max-age=0"));
range.insert("range", HeaderValue::from_static("bytes=0-2"));
let partial = validate(&service, &cache, &ticket, &settings, Method::GET, range)
.await.unwrap().response;
assert_eq!(partial.status(), StatusCode::PARTIAL_CONTENT);
assert_eq!(partial.into_body().collect().await.unwrap().to_bytes(), "bef");
let head = validate(&service, &cache, &ticket, &settings, Method::HEAD, HeaderMap::new())
.await.unwrap().response;
assert_eq!(head.status(), StatusCode::OK);
assert!(head.into_body().collect().await.unwrap().to_bytes().is_empty());
}
#[tokio::test]
async fn changed_local_file_is_not_falsely_revalidated() {
let (dir, service, cache, ticket, settings) = fixture().await;
// A size change guarantees different ETag even on filesystems with
// coarse modification-time resolution.
std::fs::write(dir.path().join("shCore.js"), b"new file content").unwrap();
let cached = cache.revalidation_tiered(&ticket).await.unwrap();
let (headers, sent) = web_origin_request_headers(&HeaderMap::new(), Some(&cached));
assert!(sent);
let origin = serve_file(&service, Method::GET, headers).await;
assert_eq!(origin.status(), StatusCode::OK);
let plan = cache::plan(origin.status(), origin.headers(), &settings, Duration::ZERO).unwrap();
cache.discard(&ticket);
cache.wrap(origin, ticket.clone(), plan, &settings)
.into_body().collect().await.unwrap();
assert_eq!(cache.get_tiered(&ticket, &settings).await.unwrap().hits, 1);
}
#[tokio::test]
async fn 304_new_private_policy_discards_cached_representation() {
let (_dir, _service, cache, ticket, settings) = fixture().await;
let cached = cache.revalidation_tiered(&ticket).await.unwrap();
let mut headers = HeaderMap::new();
headers.insert("cache-control", HeaderValue::from_static("private, max-age=300"));
headers.insert("etag", cached.headers()["etag"].clone());
let outcome = finish_web_revalidation(&cache, &ticket, &cached, &headers,
&settings, Duration::ZERO, &HeaderMap::new(), &Method::GET,
false, false, |_| {}).unwrap();
assert_eq!(outcome.reason, "cache_revalidated_uncached");
assert_eq!(outcome.response.into_body().collect().await.unwrap().to_bytes(), "before");
assert!(cache.revalidation_tiered(&ticket).await.is_none());
}