mod tests { use super::*; fn policy() -> ValidationPolicy { ValidationPolicy { tls_addr: None, cert_dir: None, data_dir: None, proxy_addr: "127.0.0.1:8080".parse().unwrap(), admin_addr: "127.0.0.1:9090".parse().unwrap(), } } #[test] fn route_handles_reuse_snapshot_ownership_without_cloning_compiled_routes() { let snapshot = compile(demo_config("127.0.0.1:9000".parse().unwrap()), 1, &policy()).unwrap(); let local = "127.0.0.1:8080".parse().unwrap(); let first = snapshot.select_on_arc("localhost", "/", local, false).unwrap(); let second = snapshot.select_on_arc("localhost", "/child", local, false).unwrap(); // Both selectors point into one immutable compiled snapshot. assert!(std::sync::Arc::ptr_eq(first, second)); let legacy = snapshot.select_on("localhost", "/", local, false).unwrap(); assert!(std::ptr::eq(first.as_ref(), legacy)); assert!(snapshot.select_on_arc("not-this-host", "/", local, false).is_none()); } #[test] fn missing_http2_settings_use_release_defaults() { let config: Config = serde_json::from_value(serde_json::json!({})).unwrap(); assert_eq!(config.http2, crate::settings::Http2Connections::default()); config.http2.validate().unwrap(); } #[test] fn normalized_unicode_prefix_survives_serialization_and_recompile() { let mut config = demo_config("127.0.0.1:9000".parse().unwrap()); config.vhosts[0].routes[0].path_prefix = "/caf%C3%A9".into(); let first = compile(config, 0, &policy()).unwrap(); let saved = serde_json::to_string(&first.envelope.config).unwrap(); let second = compile(serde_json::from_str(&saved).unwrap(), 1, &policy()).unwrap(); assert!(second.select("localhost", "/caf\u{e9}/hello").is_some()); } #[test] fn web_server_main_source_needs_no_backend_pool() { let tmp = tempfile::TempDir::new().unwrap(); std::fs::write(tmp.path().join("index.html"), b"hello").unwrap(); let mut config = demo_config("127.0.0.1:9000".parse().unwrap()); config.web_services.push(crate::web_server::Service { id: "site".into(), name: "Site".into(), document_root: tmp.path().to_path_buf(), index: vec!["index.html".into()], directory_listing: false, ..Default::default() }); config.vhosts[0].source = crate::vhost::Source::WebServer { web_service_id: "site".into(), }; config.vhosts[0].backends.clear(); config.vhosts[0].routes.clear(); let snapshot = compile(config, 1, &policy()).unwrap(); let route = snapshot.select("localhost", "/").unwrap(); assert!(matches!(&route.source, CompiledSource::WebServer(_))); assert!(route.pool.is_none()); assert!(route.backend.is_none()); assert!(snapshot.pools.is_empty()); } #[test] fn web_server_and_reverse_proxy_can_share_one_vhost() { let tmp = tempfile::TempDir::new().unwrap(); std::fs::write(tmp.path().join("index.html"), b"hello").unwrap(); let mut config = demo_config("127.0.0.1:9000".parse().unwrap()); config.web_services.push(crate::web_server::Service { id: "site".into(), name: "Site".into(), document_root: tmp.path().to_path_buf(), index: vec!["index.html".into()], directory_listing: false, ..Default::default() }); let host = &mut config.vhosts[0]; host.source = crate::vhost::Source::WebServer { web_service_id: "site".into(), }; host.routes = vec![crate::vhost::PathRoute { id: "api".into(), path_prefix: "/api".into(), source: crate::vhost::Source::ReverseProxy, ..Default::default() }]; let snapshot = compile(config, 1, &policy()).unwrap(); let root = snapshot.select("localhost", "/").unwrap(); let api = snapshot.select("localhost", "/api/test").unwrap(); assert!(matches!(&root.source, CompiledSource::WebServer(_))); assert!(matches!(&api.source, CompiledSource::ReverseProxy)); assert!(api.pool.is_some()); assert!(api.backend.is_some()); } #[test] fn one_web_service_can_be_reused_by_multiple_vhosts() { let tmp = tempfile::TempDir::new().unwrap(); std::fs::write(tmp.path().join("index.html"), b"shared").unwrap(); let mut config = demo_config("127.0.0.1:9000".parse().unwrap()); config.web_services.push(crate::web_server::Service { id: "shared-site".into(), name: "Shared site".into(), document_root: tmp.path().to_path_buf(), index: vec!["index.html".into()], directory_listing: false, ..Default::default() }); let first = &mut config.vhosts[0]; first.source = crate::vhost::Source::WebServer { web_service_id: "shared-site".into(), }; first.backends.clear(); first.routes.clear(); let mut second = first.clone(); second.id = "shared-second".into(); second.frontend.hosts = vec!["second.localhost".into()]; config.vhosts.push(second); let snapshot = compile(config, 1, &policy()).unwrap(); let first_route = snapshot.select("localhost", "/").unwrap(); let second_route = snapshot.select("second.localhost", "/").unwrap(); assert!( matches!(&first_route.source, CompiledSource::WebServer(service) if service.id == "shared-site") ); assert!( matches!(&second_route.source, CompiledSource::WebServer(service) if service.id == "shared-site") ); } #[test] fn unknown_web_service_reference_is_rejected() { let mut config = demo_config("127.0.0.1:9000".parse().unwrap()); config.vhosts[0].source = crate::vhost::Source::WebServer { web_service_id: "missing".into(), }; let error = compile(config, 1, &policy()).unwrap_err().to_string(); assert!(error.contains("unknown Web Service"), "{error}"); } #[test] fn missing_source_fields_remain_reverse_proxy_for_old_configs() { let source: crate::vhost::Source = serde_json::from_value(serde_json::json!({"type":"reverse_proxy"})).unwrap(); assert_eq!(source, crate::vhost::Source::ReverseProxy); let route: crate::vhost::PathRoute = serde_json::from_value(serde_json::json!({"id":"legacy"})).unwrap(); assert_eq!(route.source, crate::vhost::Source::ReverseProxy); } #[test] fn host_normalization() { assert_eq!(request_host("EXAMPLE.com.:8080").unwrap(), "example.com"); assert_eq!(request_host("example.com:65535").unwrap(), "example.com"); assert_eq!(request_host("[::1]").unwrap(), "::1"); assert_eq!(request_host("[::1]:8080").unwrap(), "::1"); assert_eq!(request_host("a:").unwrap(), "a"); assert_eq!(request_host("[::1]:").unwrap(), "::1"); for invalid in [ "", "a..b", "*.test", "a/b", "user@host", "-bad.test", "a:99999", "a:0", "a:abc", "[::1]:99999", "[::1]:0", "[::1]suffix", "[::1]suffix:80", ] { assert!(request_host(invalid).is_err(), "{invalid}"); } } #[test] fn wildcard_host_patterns_are_normalized_and_exact_hosts_win() { assert_eq!( normalize_host_pattern("*.Example.TEST.").unwrap(), "*.example.test" ); for invalid in ["*", "foo.*.test", "*.com", "*.*.example.test"] { assert!(normalize_host_pattern(invalid).is_err(), "{invalid}"); } let mut c = demo_config("127.0.0.1:9000".parse().unwrap()); c.vhosts[0].id = "wildcard".into(); c.vhosts[0].frontend.hosts = vec!["*.example.test".into()]; let mut exact = c.vhosts[0].clone(); exact.id = "exact".into(); exact.frontend.hosts = vec!["api.example.test".into()]; c.vhosts.push(exact); let snapshot = compile(c, 1, &policy()).unwrap(); let local: SocketAddr = "127.0.0.1:8080".parse().unwrap(); assert_eq!( snapshot .select_on("api.example.test", "/", local, false) .unwrap() .id, "exact/main" ); assert_eq!( snapshot .select_on("www.example.test", "/", local, false) .unwrap() .id, "wildcard/main" ); } #[test] fn logging_vhost_resolution_reuses_router_host_precedence() { let mut config = demo_config("127.0.0.1:9000".parse().unwrap()); config.vhosts[0].id = "wildcard".into(); config.vhosts[0].frontend.hosts = vec!["*.example.test".into()]; let mut exact = config.vhosts[0].clone(); exact.id = "exact".into(); exact.frontend.hosts = vec!["api.example.test".into()]; config.vhosts.push(exact); let snapshot = compile(config, 1, &policy()).unwrap(); let local: SocketAddr = "127.0.0.1:8080".parse().unwrap(); let routed = snapshot .resolve_logging_vhost( local, false, Some("wildcard"), Some("api.example.test"), None, None, ) .unwrap(); assert_eq!(routed.vhost, "wildcard"); assert_eq!(routed.matched_by, "router"); let exact = snapshot .resolve_logging_vhost(local, false, None, Some("API.EXAMPLE.TEST:8080"), None, None) .unwrap(); assert_eq!(exact.vhost, "exact"); assert_eq!(exact.matched_by, "authority"); let wildcard = snapshot .resolve_logging_vhost(local, false, None, None, Some("www.example.test:8080"), None) .unwrap(); assert_eq!(wildcard.vhost, "wildcard"); assert_eq!(wildcard.matched_by, "host"); let sni = snapshot .resolve_logging_vhost(local, false, None, None, None, Some("api.example.test")) .unwrap(); assert_eq!(sni.vhost, "exact"); assert_eq!(sni.matched_by, "sni"); assert!(snapshot .resolve_logging_vhost(local, false, None, None, None, Some("unknown.test")) .is_none()); assert!(snapshot .resolve_logging_vhost(local, false, None, None, None, None) .is_none()); } #[test] fn reusable_acl_profile_reference_is_compiled_and_unknown_reference_fails() { let mut c = demo_config("127.0.0.1:9000".parse().unwrap()); c.acl_profiles = serde_json::from_value(serde_json::json!([{ "id":"office", "name":"Office", "acl":{"default":"deny","rules":[{"network":"10.0.0.0/8","action":"allow"}]}, "country_access":{"mode":"block","countries":["RU"]} }])) .unwrap(); c.vhosts[0].acl_profile_id = Some("office".into()); let snapshot = compile(c.clone(), 1, &policy()).unwrap(); let local: SocketAddr = "127.0.0.1:8080".parse().unwrap(); let frontend = snapshot.frontend("localhost", local, false).unwrap(); assert_eq!(frontend.acl_profile_id.as_deref(), Some("office")); assert!(frontend .profile_acl .as_ref() .unwrap() .allows("10.2.3.4".parse().unwrap())); assert!(!frontend .profile_acl .as_ref() .unwrap() .allows("192.0.2.1".parse().unwrap())); assert!(!frontend.profile_country_access.allows(Some("RU"))); c.vhosts[0].acl_profile_id = Some("missing".into()); assert!(compile(c, 2, &policy()).is_err()); } #[test] fn rfc8441_is_advertised_only_on_opted_in_h2_capable_listener() { let mut c = demo_config("127.0.0.1:9000".parse().unwrap()); c.vhosts[0].websockets = true; c.vhosts[0].websocket_rfc8441 = true; let error = compile(c.clone(), 1, &policy()).unwrap_err().to_string(); assert!(error.contains("HTTP/2-capable listener"), "{error}"); c.listeners.endpoints[0].binding.h2c = true; let snapshot = compile(c.clone(), 2, &policy()).unwrap(); let local: SocketAddr = "127.0.0.1:8080".parse().unwrap(); assert!(snapshot.websocket_rfc8441_on(local, false)); assert!(!snapshot.websocket_rfc8441_on(local, true)); c.vhosts[0].websocket_rfc8441 = false; let snapshot = compile(c, 3, &policy()).unwrap(); assert!(!snapshot.websocket_rfc8441_on(local, false)); } #[test] fn force_ssl_requires_plain_http_and_chooses_https_target() { let mut c = demo_config("127.0.0.1:9000".parse().unwrap()); c.vhosts[0].force_ssl = true; let error = compile(c.clone(), 1, &policy()).unwrap_err().to_string(); assert!( error.contains("at least one selected TLS listener"), "{error}" ); c.listeners.endpoints.push(crate::settings::NamedListener { id: "https-a".into(), name: "HTTPS A".into(), scope: crate::settings::ListenerScope::Internal, enabled: true, binding: crate::vhost::Listener { address: "127.0.0.1:8443".parse().unwrap(), tls: true, http2: true, http2_settings: Some(crate::settings::Http2Connections::default()), h2c: false, http3: None, }, additional_addresses: vec![], }); c.listeners.endpoints.push(crate::settings::NamedListener { id: "https-b".into(), name: "HTTPS B".into(), scope: crate::settings::ListenerScope::Internal, enabled: true, binding: crate::vhost::Listener { address: "127.0.0.1:9443".parse().unwrap(), tls: true, http2: true, http2_settings: Some(crate::settings::Http2Connections::default()), h2c: false, http3: None, }, additional_addresses: vec![], }); c.vhosts[0].frontend.listener_ids = vec!["http".into(), "https-a".into(), "https-b".into()]; let error = compile(c.clone(), 2, &policy()).unwrap_err().to_string(); assert!(error.contains("cannot choose an HTTPS target"), "{error}"); c.listeners .endpoints .iter_mut() .find(|item| item.id == "https-b") .unwrap() .binding .address = "127.0.0.1:443".parse().unwrap(); let snapshot = compile(c.clone(), 3, &policy()).unwrap(); let frontend = snapshot .frontend("localhost", "127.0.0.1:8080".parse().unwrap(), false) .unwrap(); assert_eq!(frontend.https_redirect_port, Some(443)); c.vhosts[0].frontend.listener_ids = vec!["https-a".into(), "https-b".into()]; let error = compile(c, 4, &policy()).unwrap_err().to_string(); assert!(error.contains("plain HTTP listener"), "{error}"); } #[test] fn preserve_host_is_valid_with_every_upstream_protocol() { for protocol in [ UpstreamProtocol::Http1, UpstreamProtocol::Auto, UpstreamProtocol::Http2, UpstreamProtocol::H2, ] { let mut config = demo_config("127.0.0.1:9000".parse().unwrap()); let member = &mut config.vhosts[0].backends[0]; member.url = "https://10.87.20.2:443".into(); member.allow_private = true; member.preserve_host = true; member.protocol = protocol; let snapshot = compile(config, 1, &policy()).unwrap(); assert!(snapshot.pools[0].members[0].backend.preserve_host); } } #[test] fn backend_tls_server_name_keeps_network_origin_and_changes_verified_request_name() { let mut c = demo_config("127.0.0.1:9000".parse().unwrap()); c.vhosts[0].backends[0].url = "https://10.87.20.2:443".into(); c.vhosts[0].backends[0].allow_private = true; c.vhosts[0].backends[0].tls_server_name = Some("Backend.Unitra.Internal.".into()); let s = compile(c, 1, &policy()).unwrap(); let backend = &s.pools[0].members[0].backend; assert_eq!(backend.origin, "https://10.87.20.2:443"); assert_eq!( backend.request_origin, "https://backend.unitra.internal:443" ); } #[test] fn tls_server_name_is_https_dns_only() { let mut c = demo_config("127.0.0.1:9000".parse().unwrap()); c.vhosts[0].backends[0].tls_server_name = Some("backend.internal".into()); assert!(compile(c.clone(), 1, &policy()).is_err()); c.vhosts[0].backends[0].url = "https://10.87.20.2:443".into(); c.vhosts[0].backends[0].tls_server_name = Some("10.87.20.2".into()); assert!(compile(c, 1, &policy()).is_err()); } #[test] fn backend_tls_verification_defaults_off_and_can_be_enabled_explicitly() { let mut c = demo_config("127.0.0.1:9000".parse().unwrap()); assert!(!c.vhosts[0].backends[0].tls_verify); assert!(compile(c.clone(), 1, &policy()).is_ok()); c.vhosts[0].backends[0].url = "https://10.87.20.2:443".into(); c.vhosts[0].backends[0].allow_private = true; c.vhosts[0].backends[0].tls_ca_pem = Some("not a certificate".into()); assert!(compile(c.clone(), 1, &policy()).is_ok()); c.vhosts[0].backends[0].tls_verify = true; assert!(compile(c, 1, &policy()).is_err()); } #[test] fn redirect_hosts_share_listener_routing_without_opening_vhost_routes() { let mut c = demo_config("127.0.0.1:9000".parse().unwrap()); c.redirects = serde_json::from_value(serde_json::json!([{ "id":"legacy","name":"Legacy","frontend":{"hosts":["old.localhost"],"listener_ids":["http"]}, "rules":[{"id":"root","target":"https://new.localhost/","status_code":308}] }])).unwrap(); let snapshot = compile(c, 1, &policy()).unwrap(); let local: SocketAddr = "127.0.0.1:8080".parse().unwrap(); let (_host, rule) = snapshot .redirect_on("old.localhost", "/a", local, false) .unwrap(); assert_eq!(rule.id, "root"); assert!(snapshot .select_on("old.localhost", "/a", local, false) .is_none()); } #[test] fn custom_locations_override_but_main_vhost_remains_fallback() { let mut c = demo_config("127.0.0.1:9000".parse().unwrap()); // The demo document still contains the pre-0.16 synthetic root route. // Validation must normalize it away and keep the main backend internally. let mut custom = c.vhosts[0].routes[0].clone(); custom.id = "test".into(); custom.path_prefix = "/test".into(); custom.response = Some(crate::vhost::LocationResponse { status: 200, content_type: "text/plain".into(), body: "ok".into(), }); c.vhosts[0].routes.push(custom); let snapshot = compile(c, 1, &policy()).unwrap(); assert_eq!( snapshot.select("localhost", "/test").unwrap().id, "demo/test" ); assert_eq!( snapshot.select("localhost", "/test/a").unwrap().id, "demo/test" ); assert_eq!(snapshot.select("localhost", "/").unwrap().id, "demo/main"); assert_eq!( snapshot.select("localhost", "/abc").unwrap().id, "demo/main" ); assert_eq!( snapshot.select("localhost", "/static/a.js").unwrap().id, "demo/main" ); assert!(snapshot.envelope.config.vhosts[0] .routes .iter() .all(|route| !route.is_legacy_main_fallback())); } #[test] fn deliberate_root_custom_location_can_override_main_fallback() { let mut c = demo_config("127.0.0.1:9000".parse().unwrap()); let root = &mut c.vhosts[0].routes[0]; root.response = Some(crate::vhost::LocationResponse { status: 204, content_type: "text/plain".into(), body: String::new(), }); let snapshot = compile(c, 1, &policy()).unwrap(); assert_eq!( snapshot.select("localhost", "/anything").unwrap().id, "demo/root" ); } #[test] fn direct_admin_listener_backend_is_marked_for_guarded_control_plane_use() { let mut c = demo_config("127.0.0.1:9000".parse().unwrap()); c.administration.listen = Some("0.0.0.0:9090".parse().unwrap()); c.administration.tls = true; c.vhosts[0].backends[0].url = "https://127.0.0.1:9090".into(); c.vhosts[0].backends[0].allow_private = true; c.vhosts[0].backends[0].accept_local_listener_risk = true; let snapshot = compile(c, 1, &policy()).unwrap(); assert!(snapshot.pools[0].members[0].backend.targets_admin_listener); } #[test] fn backend_diagnostic_flags_compile_to_frontend() { let mut c = demo_config("127.0.0.1:9000".parse().unwrap()); c.vhosts[0].backend_header = true; c.vhosts[0].backend_timing_header = true; let snapshot = compile(c, 1, &policy()).unwrap(); let local: SocketAddr = "127.0.0.1:8080".parse().unwrap(); let frontend = snapshot.frontend("localhost", local, false).unwrap(); assert!(frontend.backend_header); assert!(frontend.backend_timing_header); } #[test] fn path_boundaries() { assert!(prefix_matches("/api", "/api")); assert!(prefix_matches("/api", "/api/users")); assert!(!prefix_matches("/api", "/apix")); assert!(prefix_matches("/", "/anything")); } #[test] fn invalid_per_backend_health_policy_rejected_without_global_override() { let mut c = demo_config("127.0.0.1:9000".parse().unwrap()); c.vhosts[0].backends[0].health_check = Some(crate::balance::HealthCheck { mode: crate::balance::HealthMode::Http, interval_ms: 300, timeout_ms: 400, slow_response_ms: 0, ..Default::default() }); assert!(compile(c.clone(), 1, &policy()).is_err()); c.vhosts[0].backends[0].health_check.as_mut().unwrap().timeout_ms = 200; assert!(compile(c, 1, &policy()).is_ok()); } #[test] fn private_requires_opt_in_and_metadata_always_denied() { let mut c = demo_config("127.0.0.1:9000".parse().unwrap()); assert!(compile(c.clone(), 1, &policy()).is_ok()); c.vhosts[0].backends[0].allow_private = false; assert!(compile(c.clone(), 1, &policy()).is_err()); c.vhosts[0].backends[0].allow_private = true; for target in [ "http://169.254.169.254", "http://100.100.100.200", "http://0.0.0.0", "http://127.0.0.1:9090", "http://[::ffff:169.254.169.254]", "http://[fe80::1]", "http://user:pass@127.0.0.1:9000", ] { c.vhosts[0].backends[0].url = target.into(); assert!(compile(c.clone(), 1, &policy()).is_err(), "{target}"); } c.vhosts[0].backends[0].url = "http://127.0.0.1:9090".into(); c.vhosts[0].backends[0].accept_local_listener_risk = true; assert!(compile(c.clone(), 1, &policy()).is_ok()); c.vhosts[0].backends[0].allow_private = false; assert!(compile(c, 1, &policy()).is_err()); } #[test] fn longest_prefix_and_aliases() { let mut c = demo_config("127.0.0.1:9000".parse().unwrap()); let mut specific = c.vhosts[0].routes[0].clone(); specific.id = "api".into(); specific.path_prefix = "/api".into(); c.vhosts[0].routes.push(specific); let s = compile(c, 7, &policy()).unwrap(); assert_eq!(s.select("localhost", "/api/x").unwrap().id, "demo/api"); assert_eq!(s.select("demo.localhost", "/apix").unwrap().id, "demo/main"); assert!(s.select("unknown", "/").is_none()); assert_eq!(s.envelope.revision, 7); } #[test] fn nginx_style_location_precedence_exact_priority_regex_prefix() { let mut c = demo_config("127.0.0.1:9000".parse().unwrap()); c.vhosts[0].routes[0].id = "root".into(); let mut prefix = c.vhosts[0].routes[0].clone(); prefix.id = "api-prefix".into(); prefix.path_prefix = "/api".into(); let mut regex = c.vhosts[0].routes[0].clone(); regex.id = "api-regex".into(); regex.match_type = crate::vhost::PathMatch::Regex; regex.path_prefix = r"^/api/v[0-9]+/".into(); let mut exact = c.vhosts[0].routes[0].clone(); exact.id = "api-exact".into(); exact.match_type = crate::vhost::PathMatch::Exact; exact.path_prefix = "/api/v1/fixed".into(); let mut trailing_exact = c.vhosts[0].routes[0].clone(); trailing_exact.id = "trailing-exact".into(); trailing_exact.match_type = crate::vhost::PathMatch::Exact; trailing_exact.path_prefix = "/trailing/".into(); c.vhosts[0] .routes .extend([prefix.clone(), regex, exact, trailing_exact]); let snapshot = compile(c.clone(), 1, &policy()).unwrap(); assert_eq!( snapshot.select("localhost", "/api/v1/fixed").unwrap().id, "demo/api-exact" ); assert_eq!( snapshot.select("localhost", "/api/v2/users").unwrap().id, "demo/api-regex" ); assert_eq!( snapshot.select("localhost", "/api/plain").unwrap().id, "demo/api-prefix" ); assert_eq!( snapshot.select("localhost", "/trailing/").unwrap().id, "demo/trailing-exact" ); assert_eq!( snapshot.select("localhost", "/trailing").unwrap().id, "demo/main" ); c.vhosts[0].routes.retain(|route| route.id != "api-prefix"); prefix.match_type = crate::vhost::PathMatch::PrefixPriority; prefix.id = "api-priority".into(); c.vhosts[0].routes.push(prefix); let snapshot = compile(c, 2, &policy()).unwrap(); assert_eq!( snapshot.select("localhost", "/api/v2/users").unwrap().id, "demo/api-priority" ); } #[test] fn location_rewrites_are_normalized_and_regex_captures_expand() { let mut c = demo_config("127.0.0.1:9000".parse().unwrap()); let route = &mut c.vhosts[0].routes[0]; route.match_type = crate::vhost::PathMatch::Regex; route.path_prefix = r"^/api/v([0-9]+)(/.*)$".into(); route.rewrite = crate::vhost::PathRewrite { mode: crate::vhost::RewriteMode::Regex, value: "/service/${1}${2}".into(), }; let snapshot = compile(c, 1, &policy()).unwrap(); let selected = snapshot.select("localhost", "/api/v42/items").unwrap(); assert_eq!( selected .rewritten_path("/api/v42/items") .unwrap() .as_deref(), Some("/service/42/items") ); assert_eq!( selected .rewritten_path("/api/v42/Ubuntu 14.04 LTS") .unwrap() .as_deref(), Some("/service/42/Ubuntu 14.04 LTS") ); } #[test] fn custom_location_response_is_validated() { let mut c = demo_config("127.0.0.1:9000".parse().unwrap()); c.vhosts[0].routes[0].response = Some(crate::vhost::LocationResponse { status: 200, content_type: "application/json".into(), body: r#"{"ok":true}"#.into(), }); assert!(compile(c.clone(), 1, &policy()).is_ok()); c.vhosts[0].routes[0].response.as_mut().unwrap().status = 199; assert!(compile(c.clone(), 1, &policy()).is_err()); c.vhosts[0].routes[0].response.as_mut().unwrap().status = 200; c.vhosts[0].routes[0] .response .as_mut() .unwrap() .content_type = "text/plain\r\nx-bad: 1".into(); assert!(compile(c, 1, &policy()).is_err()); } #[test] fn local_response_hosts_reserve_frontends_and_do_not_require_backends() { let mut c = demo_config("127.0.0.1:9000".parse().unwrap()); c.not_found_hosts = serde_json::from_value(serde_json::json!([{ "id":"missing", "name":"Missing site", "frontend":{"hosts":["missing.localhost"],"listener_ids":["http"]}, "page":{"kind":"html","status":404,"html":"