This commit is contained in:
Mateusz Gruszczyński committed 2026-10-10 00:18:06 +02:00
1 parent ad95bfb050
commit 1431dd47f0
62 files changed
+1360 -198

No files matched your search

+100
View File
@@ -161,4 +161,104 @@ mod tests {
assert_eq!(page.serve(&Method::POST, StatusCode::OK).status(), 405);
assert_eq!(page.serve(&Method::GET, StatusCode::OK).status(), 200);
}
#[tokio::test]
async fn origin_error_interception_is_opt_in_and_keeps_http_status() {
let mk_origin = || {
let mut response = Response::new(Body::from("<html>secret application error</html>"));
*response.status_mut() = StatusCode::NOT_FOUND;
response.headers_mut().insert("content-type", HeaderValue::from_static("text/html"));
response
};
let defaults: VhostPages = serde_json::from_str("{}").unwrap();
assert!(defaults.intercept_statuses.is_empty());
let unchanged = intercept_application_error(mk_origin(), &defaults, &GlobalPages::default());
assert_eq!(unchanged.status(), StatusCode::NOT_FOUND);
assert_eq!(unchanged.into_body().collect().await.unwrap().to_bytes(),
"<html>secret application error</html>");
let configured = VhostPages { intercept_statuses: vec![404], ..VhostPages::default() };
configured.validate().unwrap();
let replaced = intercept_application_error(mk_origin(), &configured, &GlobalPages::default());
assert_eq!(replaced.status(), StatusCode::NOT_FOUND);
assert_eq!(replaced.headers().get("cache-control").unwrap(), "no-store");
assert!(replaced.extensions().get::<PageServed>().is_some());
let bytes = replaced.into_body().collect().await.unwrap().to_bytes();
assert!(String::from_utf8_lossy(&bytes).contains("404"));
assert!(!String::from_utf8_lossy(&bytes).contains("secret application error"));
}
#[tokio::test]
async fn interception_replaces_only_the_body_even_when_page_has_a_status_override() {
let mut vhost = VhostPages { intercept_statuses: vec![404], ..VhostPages::default() };
vhost.errors.insert(404, Page {
kind: Kind::Html,
// Overrides are valid for standalone error pages, not an origin
// response whose HTTP status must remain 404.
status: Some(503),
html: "custom 404".into(),
});
let mut origin = Response::new(Body::from("application 404"));
*origin.status_mut() = StatusCode::NOT_FOUND;
let intercepted = intercept_application_error(origin, &vhost, &GlobalPages::default());
assert_eq!(intercepted.status(), StatusCode::NOT_FOUND);
assert_eq!(intercepted.headers().get("cache-control").unwrap(), "no-store");
assert_eq!(intercepted.into_body().collect().await.unwrap().to_bytes(), "custom 404");
// Outside interception the configured status override still applies.
assert_eq!(vhost.errors.get(&404).unwrap().render(StatusCode::NOT_FOUND).status(), StatusCode::SERVICE_UNAVAILABLE);
}
#[tokio::test]
async fn interception_uses_vhost_then_global_page_and_preserves_challenge() {
let mut global = GlobalPages::default();
global.errors.insert(401, Page { kind: Kind::Html,
html: "global denied".into(), status: None });
let mut vhost = VhostPages { intercept_statuses: vec![401], ..VhostPages::default() };
let mut response = Response::new(Body::from("application denied"));
*response.status_mut() = StatusCode::UNAUTHORIZED;
response.headers_mut().insert("www-authenticate", HeaderValue::from_static("Basic realm=\"app\""));
response.headers_mut().insert("set-cookie", HeaderValue::from_static("secret=1"));
response.headers_mut().insert("content-encoding", HeaderValue::from_static("gzip"));
let global_response = intercept_application_error(response, &vhost, &global);
assert_eq!(global_response.headers().get("www-authenticate").unwrap(), "Basic realm=\"app\"");
assert!(!global_response.headers().contains_key("set-cookie"));
assert!(!global_response.headers().contains_key("content-encoding"));
assert_eq!(global_response.into_body().collect().await.unwrap().to_bytes(), "global denied");
vhost.errors.insert(401, Page { kind: Kind::Html,
html: "local denied".into(), status: None });
let mut response = Response::new(Body::from("application denied"));
*response.status_mut() = StatusCode::UNAUTHORIZED;
let local = intercept_application_error(response, &vhost, &global);
assert_eq!(local.into_body().collect().await.unwrap().to_bytes(), "local denied");
}
#[tokio::test]
async fn interception_skips_nonmatching_and_already_served_responses() {
let vhost = VhostPages { intercept_statuses: vec![404], ..VhostPages::default() };
let mut response = Response::new(Body::from("ordinary ok"));
*response.status_mut() = StatusCode::OK;
let untouched = intercept_application_error(response, &vhost, &GlobalPages::default());
assert_eq!(untouched.into_body().collect().await.unwrap().to_bytes(), "ordinary ok");
let served = Page { kind: Kind::Html, status: Some(404), html: "internal fallback".into() }
.render(StatusCode::NOT_FOUND);
let preserved = intercept_application_error(served, &vhost, &GlobalPages::default());
assert_eq!(preserved.into_body().collect().await.unwrap().to_bytes(), "internal fallback");
}
#[test]
fn intercepted_error_code_configuration_is_validated_and_backwards_compatible() {
let mut pages: VhostPages = serde_json::from_str("{\"response\": null}").unwrap();
assert!(pages.intercept_statuses.is_empty());
pages.intercept_statuses = vec![404, 503];
pages.validate().unwrap();
let json = serde_json::to_string(&pages).unwrap();
assert!(json.contains("\"intercept_statuses\":[404,503]"));
pages.intercept_statuses = vec![404, 404];
assert!(pages.validate().is_err());
pages.intercept_statuses = vec![302];
assert!(pages.validate().is_err());
pages.intercept_statuses.clear();
pages.validate().unwrap();
assert!(!serde_json::to_string(&pages).unwrap().contains("intercept_statuses"));
}
}