160 lines
6.0 KiB
Rust
160 lines
6.0 KiB
Rust
mod tests {
|
|
use super::*;
|
|
|
|
fn entry() -> LogEntry {
|
|
LogEntry {
|
|
vhost: Some("app".into()),
|
|
route: Some("root".into()),
|
|
cache_effective_enabled: Some(true),
|
|
transform_effective_enabled: Some(false),
|
|
backend: Some("origin".into()),
|
|
id: 7,
|
|
time_ms: 1_700_000_000_000,
|
|
method: "GET".into(),
|
|
host: "app.example".into(),
|
|
path: "/health".into(),
|
|
http_version: "3",
|
|
transport: "h3",
|
|
websocket_bootstrap: Some("h3_extended_connect"),
|
|
websocket_phase: Some("tunnel_closed"),
|
|
websocket_error: None,
|
|
h3_reset_code: None,
|
|
upstream_status: Some(101),
|
|
upstream_error: None,
|
|
client_ip: Some("192.0.2.10".into()),
|
|
user_agent: Some("RustProxy-Test/1.0".into()),
|
|
country: Some("PL".into()),
|
|
status: 200,
|
|
duration_ms: 12,
|
|
request_bytes: 10,
|
|
response_bytes: 42,
|
|
outcome: "ok",
|
|
cache: "HIT",
|
|
cache_reason: "cache_hit",
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn json_format_can_map_and_add_static_fields() {
|
|
let mut format = JsonFormat::default();
|
|
format.fields.insert("http_status".into(), "status".into());
|
|
format.fields.insert("site".into(), "vhost".into());
|
|
format.fields.insert("route".into(), "route".into());
|
|
format.fields.insert("ua".into(), "user_agent".into());
|
|
format.fields.insert("geo_country".into(), "country".into());
|
|
format
|
|
.fields
|
|
.insert("cache_enabled".into(), "cache_effective_enabled".into());
|
|
format.fields.insert(
|
|
"transform_enabled".into(),
|
|
"transform_effective_enabled".into(),
|
|
);
|
|
format.static_fields.insert("service".into(), "edge".into());
|
|
let mut settings = Settings::default();
|
|
settings.format = format.clone();
|
|
settings.validate().unwrap();
|
|
let value = format_entry(&entry(), &format);
|
|
assert_eq!(value["http_status"], 200);
|
|
assert_eq!(value["site"], "app");
|
|
assert_eq!(value["route"], "root");
|
|
assert_eq!(value["ua"], "RustProxy-Test/1.0");
|
|
assert_eq!(value["geo_country"], "PL");
|
|
assert_eq!(value["cache_enabled"], true);
|
|
assert_eq!(value["transform_enabled"], false);
|
|
assert_eq!(value["service"], "edge");
|
|
assert!(value.get("method").is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn logging_validation_rejects_credentials_unknown_fields_and_bad_endpoints() {
|
|
let mut settings = Settings::default();
|
|
settings.loki.enabled = true;
|
|
settings.loki.url = "https://user:secret@example.invalid/loki/api/v1/push".into();
|
|
assert!(settings.validate().is_err());
|
|
|
|
let mut settings = Settings::default();
|
|
settings.logstash.enabled = true;
|
|
settings.logstash.address = "missing-port.example".into();
|
|
assert!(settings.validate().is_err());
|
|
|
|
let mut settings = Settings::default();
|
|
settings
|
|
.format
|
|
.fields
|
|
.insert("status".into(), "does_not_exist".into());
|
|
assert!(settings.validate().is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn per_vhost_file_mode_has_safe_names_and_requires_directory() {
|
|
assert_eq!(vhost_log_name(Some("app-1")), "app-1");
|
|
assert_eq!(vhost_log_name(None), "_unmatched");
|
|
assert_eq!(vhost_log_name(Some("bad/name")), "bad_name");
|
|
let mut settings = Settings::default();
|
|
settings.file.enabled = true;
|
|
settings.file.per_vhost = true;
|
|
settings.file.directory.clear();
|
|
assert!(settings.validate().is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn capture_defaults_keep_geoip_off_request_path() {
|
|
let settings = Settings::default();
|
|
assert!(settings.capture_user_agent);
|
|
assert!(!settings.capture_country);
|
|
}
|
|
|
|
#[test]
|
|
fn default_json_format_keeps_standard_entry() {
|
|
let value = format_entry(&entry(), &JsonFormat::default());
|
|
assert_eq!(value["method"], "GET");
|
|
assert_eq!(value["cache"], "HIT");
|
|
assert_eq!(value["response_bytes"], 42);
|
|
}
|
|
|
|
#[test]
|
|
fn error_log_defaults_off_and_redacts_common_secret_shapes() {
|
|
let settings = ErrorLogSettings::default();
|
|
assert!(!settings.enabled);
|
|
assert_eq!(settings.format, ErrorLogFormat::Json);
|
|
let text = redact_text(
|
|
r#"Authorization: Bearer topsecret url=https://x.invalid/?api_token=abc password=hunter2 {\"private_key\":\"pem-data\"}"#,
|
|
);
|
|
assert!(!text.contains("topsecret"));
|
|
assert!(!text.contains("api_token=abc"));
|
|
assert!(!text.contains("hunter2"));
|
|
assert!(!text.contains("pem-data"));
|
|
}
|
|
|
|
#[test]
|
|
fn error_json_is_one_record_and_reserved_fields_cannot_be_overwritten() {
|
|
let event = ErrorEvent {
|
|
timestamp: "2026-10-03T12:34:56.123Z".into(),
|
|
target: "rust_proxy::http".into(),
|
|
message: "upstream failed\nsecond line".into(),
|
|
fields: BTreeMap::from([
|
|
("message".into(), "attacker override".into()),
|
|
("request_id".into(), "abc".into()),
|
|
]),
|
|
};
|
|
let line = format_error_event(&event, ErrorLogFormat::Json);
|
|
assert_eq!(line.iter().filter(|byte| **byte == b'\n').count(), 1);
|
|
let value: Value = serde_json::from_slice(&line).unwrap();
|
|
assert_eq!(value["message"], "upstream failed\nsecond line");
|
|
assert_eq!(value["request_id"], "abc");
|
|
}
|
|
|
|
#[test]
|
|
fn retained_history_is_larger_than_the_live_snapshot_but_still_bounded() {
|
|
let logs = Logs::default();
|
|
for id in 0..(LIVE_WINDOW + 80) {
|
|
let mut item = entry();
|
|
item.id = id as u64;
|
|
logs.push(item);
|
|
}
|
|
assert_eq!(logs.recent_limit(LIVE_WINDOW).len(), LIVE_WINDOW);
|
|
assert_eq!(logs.recent().len(), LIVE_WINDOW + 80);
|
|
assert_eq!(logs.recent_limit(HISTORY_CAPACITY + 1).len(), LIVE_WINDOW + 80);
|
|
}
|
|
}
|