mod default_listener_tests { use super::*; use serde_json::json; #[test] fn tls_defaults_to_h3_and_explicit_null_survives_a_roundtrip() { let auto: Listener = serde_json::from_value(json!({"address":"127.0.0.1:8443","tls":true})).unwrap(); assert_eq!(auto.http3, Some(crate::http3::Settings::default())); assert!(auto.http2); let disabled: Listener = serde_json::from_value(json!({"address":"127.0.0.1:8443","tls":true,"http3":null})) .unwrap(); let wire = serde_json::to_value(disabled).unwrap(); assert!(wire["http3"].is_null()); assert!(serde_json::from_value::(wire) .unwrap() .http3 .is_none()); let plain: Listener = serde_json::from_value(json!({"address":"127.0.0.1:8080"})).unwrap(); assert!(plain.http3.is_none()); } #[test] fn listener_plan_inherits_legacy_h2_limits_and_prefers_listener_override() { let mut config = Config::default(); config.http2 = crate::settings::Http2Connections { idle_timeout_seconds: 111, max_connection_lifetime_seconds: 222, drain_timeout_seconds: 33, }; config.listeners.endpoints = serde_json::from_value(json!([ { "id": "tls", "name": "TLS", "scope": "public", "enabled": true, "binding": { "address": "127.0.0.1:8443", "tls": true, "http2": true, "http3": null } }, { "id": "h2c", "name": "H2C", "scope": "internal", "enabled": true, "binding": { "address": "127.0.0.1:8081", "tls": false, "http2": false, "h2c": true, "http2_settings": { "idle_timeout_seconds": 444, "max_connection_lifetime_seconds": 555, "drain_timeout_seconds": 66 }, "http3": null } } ])) .unwrap(); assert!(config.listeners.endpoints[0].binding.http2_settings.is_none()); let policy = ValidationPolicy { proxy_addr: "127.0.0.1:18080".parse().unwrap(), admin_addr: "127.0.0.1:19090".parse().unwrap(), tls_addr: None, cert_dir: None, data_dir: None, }; let plan = listener_plan(&config, &policy).unwrap(); let tls = plan.iter().find(|item| item.address.port() == 8443).unwrap(); assert_eq!(tls.http2_settings, Some(config.http2)); let h2c = plan.iter().find(|item| item.address.port() == 8081).unwrap(); assert_eq!( h2c.http2_settings, Some(crate::settings::Http2Connections { idle_timeout_seconds: 444, max_connection_lifetime_seconds: 555, drain_timeout_seconds: 66, }) ); } #[test] fn method_policy_normalizes_and_enforces_allow_and_block_modes() { let mut allow: MethodPolicy = serde_json::from_value(json!({ "mode": "allow", "methods": ["get", "HEAD", "post"] })) .unwrap(); allow.validate().unwrap(); assert_eq!(allow.methods, vec!["GET", "HEAD", "POST"]); assert!(allow.allows(&http::Method::GET)); assert!(!allow.allows(&http::Method::DELETE)); assert_eq!(allow.allow_header().unwrap().to_str().unwrap(), "GET, HEAD, POST"); let mut block: MethodPolicy = serde_json::from_value(json!({ "mode": "block", "methods": ["delete"] })) .unwrap(); block.validate().unwrap(); assert!(!block.allows(&http::Method::DELETE)); assert!(block.allows(&http::Method::GET)); assert!(block.allow_header().is_none()); } #[test] fn h2c_is_cleartext_only_and_defaults_off() { let plain: Listener = serde_json::from_value(json!({"address":"127.0.0.1:8080","tls":false})).unwrap(); assert!(!plain.h2c); let h2c: Listener = serde_json::from_value( json!({"address":"127.0.0.1:8080","tls":false,"h2c":true}), ) .unwrap(); assert!(validate_listener(h2c).is_ok()); let invalid: Listener = serde_json::from_value( json!({"address":"127.0.0.1:8443","tls":true,"h2c":true}), ) .unwrap(); assert!(validate_listener(invalid).is_err()); } #[test] fn proxy_timeouts_have_explicit_safe_defaults_but_keep_null_compatibility() { let defaults: ProxyTimeouts = serde_json::from_value(json!({})).unwrap(); assert_eq!(defaults.connect_timeout_seconds, Some(10)); assert_eq!(defaults.response_header_timeout_seconds, Some(120)); assert_eq!(defaults.read_timeout_seconds, Some(60)); assert_eq!(defaults.send_timeout_seconds, Some(60)); let inherited: ProxyTimeouts = serde_json::from_value(json!({"read_timeout_seconds":null})).unwrap(); assert_eq!(inherited.read_timeout_seconds, None); assert_eq!(inherited.connect_timeout_seconds, Some(10)); } #[test] fn vhost_hostname_count_is_not_artificially_capped() { let hosts = (0..40) .map(|index| format!("host-{index}.example.test")) .collect::>(); let mut vhost: Vhost = serde_json::from_value(json!({ "id": "many-hosts", "frontend": {"hosts": hosts, "listener_ids": ["https"]}, "backends": [{"id": "origin", "url": "http://127.0.0.1:9001", "allow_private": true}] })) .unwrap(); assert!(vhost.validate().is_ok()); } #[test] fn error_fallback_requires_internal_target_and_rejects_cycles() { let base = json!({ "id": "fallback-test", "frontend": {"hosts": ["example.test"], "listener_ids": ["http"]}, "backends": [{"id": "origin", "url": "http://127.0.0.1:9001", "allow_private": true}], "error_fallback": {"target": "redirect-check", "statuses": [503, 404, 404]}, "routes": [{ "id": "redirect-check", "internal": true, "path_prefix": "/", "source": {"type": "reverse_proxy"} }] }); let mut valid: Vhost = serde_json::from_value(base.clone()).unwrap(); valid.validate().unwrap(); assert_eq!(valid.error_fallback.as_ref().unwrap().statuses, vec![404, 503]); let mut public_target: Vhost = serde_json::from_value(base.clone()).unwrap(); public_target.routes[0].internal = false; assert!(public_target .validate() .unwrap_err() .to_string() .contains("internal location")); let mut cyclic: Vhost = serde_json::from_value(base).unwrap(); cyclic.routes[0].error_fallback = Some(ErrorFallback { target: "redirect-check".into(), statuses: vec![404], }); assert!(cyclic .validate() .unwrap_err() .to_string() .contains("recursive error fallback")); } #[test] fn collision_explains_addresses_owners_and_protocol_limits() { let mut config = Config::default(); config.listeners.endpoints = serde_json::from_value(json!([ {"id":"public","name":"Public TLS","binding":{"address":"0.0.0.0:8443","tls":true}}, {"id":"private","name":"Private TLS","binding":{"address":"127.0.0.1:8443","tls":true,"http3":{"max_concurrent_connections":12}}} ])).unwrap(); let policy = ValidationPolicy { proxy_addr: "127.0.0.1:8080".parse().unwrap(), admin_addr: "127.0.0.1:9090".parse().unwrap(), tls_addr: None, cert_dir: None, data_dir: None, }; let message = listener_plan(&config, &policy).unwrap_err().to_string(); for expected in [ "0.0.0.0:8443", "127.0.0.1:8443", "Public TLS", "Private TLS", "H3 limits", "max_concurrent_connections", ] { assert!(message.contains(expected), "{message}"); } } }