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, request_id: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".into(), 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 ); } }