This commit is contained in:
Mateusz Gruszczyński committed 2026-10-25 17:45:57 +01:00
1 parent bd077754a9
commit ed7e3377c1
40 files changed
+1764 -365

No files matched your search

+162 -3
View File
@@ -651,6 +651,99 @@ mod tests {
store.close().await;
}
#[test]
fn config_section_registry_read_and_replace_are_symmetric() {
let names = [
"listeners", "docker-agents", "http2", "pages", "cache", "transform",
"forwarding", "headers", "prometheus", "geoip", "administration",
"tls", "vhosts", "web-services", "php-fpm-profiles", "redirects",
"not-found-hosts", "streams", "acl-profiles", "host-policies",
"response", "logging", "global-security",
];
let mut original = Config::default();
original.global_security.compile().unwrap();
let expected = serde_json::to_value(&original).unwrap();
for name in names {
let section = ConfigSection::from_name(name).expect(name);
let value = section.read(&original);
let mut updated = original.clone();
section.replace(&mut updated, name, value.clone()).unwrap();
assert_eq!(serde_json::to_value(&updated).unwrap(), expected, "{name}");
assert_eq!(section.read(&updated), value, "{name}");
}
for name in ["user-agent-blocking", "error_log", "history", "storage", "unknown", "tls/nested"] {
assert!(ConfigSection::from_name(name).is_none(), "{name}");
}
}
#[tokio::test]
async fn section_get_matches_put_payload_and_rejects_unknown_names() {
let (router, store, _dir, secret, _csrf) = fixture().await;
let cookie = format!("{}={secret}", auth::COOKIE_NAME);
for name in [
"listeners", "docker-agents", "http2", "pages", "cache", "transform",
"forwarding", "headers", "prometheus", "geoip", "administration", "tls",
"vhosts", "web-services", "php-fpm-profiles", "redirects", "not-found-hosts",
"streams", "acl-profiles", "host-policies", "response", "logging",
"global-security",
] {
let response = router.clone().oneshot(
request(&format!("/api/config/{name}"))
.header("cookie", &cookie)
.body(Body::empty()).unwrap(),
).await.unwrap();
assert_eq!(response.status(), StatusCode::OK, "{name}");
let data = json_body(response).await;
assert_eq!(data["section"], name);
assert_eq!(data["revision"], 0);
assert!(!data["value"].is_null(), "{name}");
if name == "logging" {
assert!(data["value"].get("logging").is_some());
assert!(data["value"].get("error_log").is_some());
}
if name == "response" {
assert_eq!(data["value"].as_object().unwrap().len(), 3);
}
if name == "global-security" {
assert!(data["value"]["user_agent_blocking"].is_object());
}
}
let unknown = router.clone().oneshot(
request("/api/config/user-agent-blocking")
.header("cookie", &cookie)
.body(Body::empty()).unwrap(),
).await.unwrap();
assert_eq!(unknown.status(), StatusCode::NOT_FOUND);
store.close().await;
}
#[test]
fn geoip_update_is_scoped_to_settings_for_general_api_tokens() {
assert!(general_api_token_allowed(&Method::GET, "/api/config/geoip", "ro"));
assert!(general_api_token_allowed(&Method::PUT, "/api/config/geoip", "rw"));
assert!(!general_api_token_allowed(&Method::PUT, "/api/config/geoip", "ro"));
assert!(general_api_token_allowed(&Method::POST, "/api/config/geoip/update", "rw"));
assert!(!general_api_token_allowed(&Method::POST, "/api/config/geoip/update", "ro"));
assert!(!general_api_token_allowed(&Method::POST, "/api/geoip/update", "rw"));
assert!(!general_api_token_allowed(&Method::GET, "/api/config/geoip/update", "rw"));
}
#[tokio::test]
async fn geoip_action_new_path_and_old_path_route_contract() {
let (router, store, _dir, secret, _csrf) = fixture().await;
let cookie = format!("{}={secret}", auth::COOKIE_NAME);
for (path, expected) in [
("/api/config/geoip/update", StatusCode::METHOD_NOT_ALLOWED),
("/api/geoip/update", StatusCode::NOT_FOUND),
] {
let response = router.clone().oneshot(
request(path).header("cookie", &cookie).body(Body::empty()).unwrap(),
).await.unwrap();
assert_eq!(response.status(), expected, "{path}");
}
store.close().await;
}
#[tokio::test]
async fn section_updates_are_revisioned_and_do_not_require_full_config_payloads() {
let (router, store, _dir, secret, csrf) = fixture().await;
@@ -658,7 +751,7 @@ mod tests {
let logging = Config::default().logging;
let body = serde_json::to_vec(&json!({
"expected_revision": 0,
"value": logging,
"value": {"logging": logging, "error_log": Config::default().error_log},
}))
.unwrap();
let response = router
@@ -721,7 +814,10 @@ mod tests {
.header("x-csrf-token", &csrf)
.body(Body::from(
serde_json::to_vec(
&json!({"expected_revision":0,"value":Config::default().logging}),
&json!({"expected_revision":0,"value":{
"logging":Config::default().logging,
"error_log":Config::default().error_log
}}),
)
.unwrap(),
))
@@ -750,6 +846,69 @@ mod tests {
store.close().await;
}
#[tokio::test]
async fn global_security_section_saves_rules_and_existing_ua_atomically() {
let (router, _store, _dir, secret, csrf) = fixture().await;
let cookie = format!("{}={secret}", auth::COOKIE_NAME);
let value = serde_json::json!({
"country_access": {"mode":"block", "countries":["ru"]},
"method_policy": {"mode":"block", "methods":["TRACE"]},
"security_headers": {"nosniff":true},
"user_agent_blocking": {"enabled":true, "patterns":["bad-bot"]}
});
let body = serde_json::to_vec(&json!({"expected_revision":0, "value":value})).unwrap();
let response = router.clone().oneshot(
request("/api/config/global-security")
.method("PUT")
.header("content-type", "application/json")
.header("cookie", &cookie)
.header("origin", "http://127.0.0.1:9090")
.header("x-rustproxy-request", "1")
.header("x-csrf-token", &csrf)
.body(Body::from(body)).unwrap(),
).await.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let json = json_body(response).await;
assert_eq!(json["revision"], 1);
assert_eq!(json["config"]["global_security"]["country_access"]["countries"][0], "RU");
assert_eq!(json["config"]["global_security"]["security_headers"]["nosniff"], true);
assert_eq!(json["config"]["global_security"]["user_agent_blocking"]["patterns"][0], "bad-bot");
assert!(json["config"].get("user_agent_blocking").is_none());
// A partial section update must not silently reset the existing UA policy.
let body = serde_json::to_vec(&json!({
"expected_revision":1,
"value": {"method_policy":{"mode":"off","methods":[]}}
})).unwrap();
let response = router.clone().oneshot(
request("/api/config/global-security")
.method("PUT")
.header("content-type", "application/json")
.header("cookie", &cookie)
.header("origin", "http://127.0.0.1:9090")
.header("x-rustproxy-request", "1")
.header("x-csrf-token", &csrf)
.body(Body::from(body)).unwrap(),
).await.unwrap();
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
// GET returns the same standalone section shape accepted by PUT, at the
// matching revision and without unrequested configuration fields.
let read = router.clone().oneshot(
request("/api/config/global-security")
.header("cookie", &cookie)
.body(Body::empty()).unwrap(),
).await.unwrap();
assert_eq!(read.status(), StatusCode::OK);
let view = json_body(read).await;
assert_eq!(view["revision"], 1);
assert_eq!(view["section"], "global-security");
assert_eq!(view["value"]["user_agent_blocking"]["patterns"][0], "bad-bot");
assert!(view.get("config").is_none());
assert!(view["value"].get("global_security").is_none());
}
#[tokio::test]
async fn logging_section_persists_error_log_settings_atomically() {
let (router, store, dir, secret, csrf) = fixture().await;
@@ -2295,7 +2454,7 @@ mod tests {
("/api/acme/issue", "POST"),
("/api/acme/renew", "POST"),
("/api/docker/refresh", "POST"),
("/api/geoip/update", "POST"),
("/api/config/geoip/update", "POST"),
("/api/cache/purge", "POST"),
("/api/cache/invalidate", "POST"),
("/api/cache/bans", "GET"),