use super::*;
#[test]
fn test_minimal_config_deserialize() {
let yaml = r#"
upstream:
default: "grpc://localhost:4180"
"#;
let config: ProxyConfig = parse_yaml(yaml).unwrap();
assert_eq!(config.upstream.unwrap().default, "grpc://localhost:4180");
assert_eq!(config.listen.http, "0.0.0.0:8080");
assert_eq!(config.service.name, "structured-proxy");
assert_eq!(config.streaming.sse_keep_alive_secs, 15);
assert!(config.descriptors.is_empty());
assert!(config.auth.is_none());
assert!(config.shield.is_none());
}
#[test]
fn cors_defaults_cover_grpc_web_and_nothing_extra() {
for yaml in ["service:\n name: demo\n", "cors: {}\n"] {
let cors = parse_yaml::<ProxyConfig>(yaml).unwrap().cors;
assert!(cors.origins.is_empty(), "{yaml}");
assert!(cors.expose_headers.is_empty(), "{yaml}");
assert_eq!(cors.max_age_secs, None, "{yaml}");
assert!(cors.grpc_web, "{yaml}");
}
}
#[test]
fn cors_settings_are_read() {
let yaml = "cors:\n origins: [\"https://app.example\"]\n expose_headers: [\"x-request-id\"]\n max_age_secs: 600\n grpc_web: false\n";
let cors = parse_yaml::<ProxyConfig>(yaml).unwrap().cors;
assert_eq!(cors.origins, ["https://app.example"]);
assert_eq!(cors.expose_headers, ["x-request-id"]);
assert_eq!(cors.max_age_secs, Some(600));
assert!(!cors.grpc_web);
}
#[test]
fn upstream_is_optional() {
let config: ProxyConfig = parse_yaml("service:\n name: demo\n").unwrap();
assert!(config.upstream.is_none());
}
#[test]
fn upstream_without_its_address_is_rejected() {
let err = parse_yaml::<ProxyConfig>("upstream: {}\n").unwrap_err();
assert!(err.to_string().contains("default"), "{err}");
}
#[test]
fn jwks_max_age_defaults_and_overrides() {
let yaml = "upstream:\n default: \"grpc://x:1\"\nauth:\n mode: jwt\n jwt:\n jwks_uri: \"https://idp/jwks\"\n";
let cfg: ProxyConfig = parse_yaml(yaml).unwrap();
assert_eq!(cfg.auth.unwrap().jwt.unwrap().jwks_max_age_secs, 300);
let yaml = "upstream:\n default: \"grpc://x:1\"\nauth:\n mode: jwt\n jwt:\n jwks_uri: \"https://idp/jwks\"\n jwks_max_age_secs: 120\n";
let cfg: ProxyConfig = parse_yaml(yaml).unwrap();
assert_eq!(cfg.auth.unwrap().jwt.unwrap().jwks_max_age_secs, 120);
}
#[test]
fn jwt_cache_defaults_and_overrides() {
let yaml = "upstream:\n default: \"grpc://x:1\"\nauth:\n mode: jwt\n jwt:\n jwks_uri: \"https://idp/jwks\"\n";
let cfg: ProxyConfig = parse_yaml(yaml).unwrap();
let cache = &cfg.auth.unwrap().jwt.unwrap().cache;
assert!(cache.enabled);
assert_eq!(cache.max_entries, 10_000);
assert_eq!(cache.max_ttl_secs, 60);
assert_eq!(cache.max_token_bytes, 4096);
let yaml = "upstream:\n default: \"grpc://x:1\"\nauth:\n mode: jwt\n jwt:\n jwks_uri: \"https://idp/jwks\"\n cache:\n max_ttl_secs: 15\n max_token_bytes: 2048\n";
let cfg: ProxyConfig = parse_yaml(yaml).unwrap();
let cache = &cfg.auth.unwrap().jwt.unwrap().cache;
assert!(cache.enabled);
assert_eq!(cache.max_entries, 10_000);
assert_eq!(cache.max_ttl_secs, 15);
assert_eq!(cache.max_token_bytes, 2048);
}
#[test]
fn jwt_cache_rejects_unknown_keys() {
let yaml = "upstream:\n default: \"grpc://x:1\"\nauth:\n mode: jwt\n jwt:\n cache:\n ttl: 15\n";
let err = parse_yaml::<ProxyConfig>(yaml).unwrap_err();
assert!(err.to_string().contains("ttl"), "{err}");
}
#[test]
fn health_and_metrics_defaults_and_overrides() {
let min: ProxyConfig = parse_yaml("upstream:\n default: \"grpc://x:1\"\n").unwrap();
assert!(min.health.enabled);
assert_eq!(min.health.path, "/health");
assert_eq!(min.health.ready_path, "/health/ready");
assert!(min.metrics.enabled);
assert_eq!(min.metrics.path, "/metrics");
let yaml = r#"
upstream:
default: "grpc://x:1"
health:
path: "/internal/health"
metrics:
enabled: false
path: "/internal/metrics"
"#;
let cfg: ProxyConfig = parse_yaml(yaml).unwrap();
assert_eq!(cfg.health.path, "/internal/health");
assert_eq!(cfg.health.live_path, "/health/live");
assert!(!cfg.metrics.enabled);
assert_eq!(cfg.metrics.path, "/internal/metrics");
}
#[test]
fn duplicate_probe_paths_are_rejected() {
let yaml = r#"
upstream:
default: "grpc://x:1"
health:
path: "/health/live"
"#;
let err = ProxyConfig::from_yaml_str(yaml).unwrap_err();
assert!(err.to_string().contains("duplicate endpoint path"));
let yaml2 = r#"
upstream:
default: "grpc://x:1"
metrics:
path: "/health"
"#;
let err2 = ProxyConfig::from_yaml_str(yaml2).unwrap_err();
assert!(err2.to_string().contains("duplicate endpoint path"));
let yaml3 = r#"
upstream:
default: "grpc://x:1"
health:
enabled: false
path: "/metrics"
"#;
assert!(ProxyConfig::from_yaml_str(yaml3).is_ok());
}
#[test]
fn malformed_edge_path_is_rejected() {
let yaml = r#"
upstream:
default: "grpc://x:1"
health:
path: "health"
"#;
let err = ProxyConfig::from_yaml_str(yaml).unwrap_err();
assert!(err.to_string().contains("must start with '/'"));
}
#[test]
fn test_zero_sse_keep_alive_is_rejected() {
let yaml = r#"
upstream:
default: "grpc://localhost:4180"
streaming:
sse_keep_alive_secs: 0
"#;
let err = ProxyConfig::from_yaml_str(yaml).unwrap_err();
assert!(err.to_string().contains("sse_keep_alive_secs"));
}
#[test]
fn test_full_config_deserialize() {
let yaml = r#"
upstream:
default: "grpc://sid-identity:4180"
descriptors:
- file: "/etc/proxy/sid.descriptor.bin"
listen:
http: "0.0.0.0:9090"
service:
name: "sid-proxy"
aliases:
- from: "/oauth2/{path}"
to: "/v1/oauth2/{path}"
auth:
mode: "jwt"
jwt:
issuer: "https://auth.example.com"
public_key_pem_file: "/etc/proxy/signing.pub"
claims_headers:
sub: "x-forwarded-user"
acr: "x-sid-auth-level"
forward_auth:
enabled: true
path: "/auth/verify"
policies:
- path: "/v1/admin/**"
require_auth: true
required_roles: ["admin"]
- path: "/v1/public/**"
require_auth: false
authz:
enabled: true
endpoint: "http://opa:9191" # Envoy ext_authz server (gRPC)
timeout_ms: 200
failure_mode_allow: false # fail closed: deny if authz is unreachable
shield:
enabled: true
profiles:
auth: { rate: "20/min", burst: 5 }
default: { rate: "100/min" }
premium: { rate: "1000/min", burst: 50 }
default_profile: "default"
jwt_limits:
tier_claim: "ratelimit_tier"
rules:
- pattern: "/v1/auth/**"
key: { type: ip }
profile: "auth"
- pattern: "/v1/**"
key: { type: jwt_claim, claim: "sub" }
client_address:
trusted_proxies: ["10.0.0.0/8"]
oidc_discovery:
enabled: true
issuer: "https://auth.example.com"
maintenance:
enabled: false
exempt_paths:
- "/health/**"
- "/.well-known/**"
cors:
origins:
- "https://app.example.com"
metrics_classes:
- pattern: "/v1/auth/**"
class: "auth"
- pattern: "/v1/admin/**"
class: "admin"
forwarded_headers:
- "authorization"
- "dpop"
- "x-request-id"
"#;
let config: ProxyConfig = parse_yaml(yaml).unwrap();
assert_eq!(
config.upstream.as_ref().unwrap().default,
"grpc://sid-identity:4180"
);
assert_eq!(config.listen.http, "0.0.0.0:9090");
assert_eq!(config.service.name, "sid-proxy");
assert_eq!(config.aliases.len(), 1);
assert!(config.auth.is_some());
let authz = config.auth.as_ref().unwrap().authz.as_ref().unwrap();
assert!(authz.enabled);
assert_eq!(authz.endpoint, "http://opa:9191");
assert_eq!(authz.timeout_ms, 200);
assert!(!authz.failure_mode_allow);
assert!(config.shield.is_some());
assert_eq!(config.client_address.trusted_proxies, ["10.0.0.0/8"]);
assert!(config.oidc_discovery.is_some());
assert_eq!(config.cors.origins.len(), 1);
assert_eq!(config.metrics_classes.len(), 2);
assert_eq!(config.forwarded_headers.len(), 3);
}
#[test]
fn authz_disabled_without_endpoint_parses() {
let yaml = r#"
upstream:
default: "grpc://localhost:4180"
descriptors:
- file: "/x.bin"
auth:
mode: "jwt"
authz:
enabled: false
"#;
let config: ProxyConfig = parse_yaml(yaml).unwrap();
let authz = config.auth.unwrap().authz.unwrap();
assert!(!authz.enabled);
assert_eq!(authz.endpoint, "");
}
#[test]
fn test_descriptor_source_file() {
let yaml = r#"
upstream:
default: "grpc://localhost:4180"
descriptors:
- file: "/etc/proxy/service.descriptor.bin"
"#;
let config: ProxyConfig = parse_yaml(yaml).unwrap();
assert_eq!(config.descriptors.len(), 1);
match &config.descriptors[0] {
DescriptorSource::File { file } => {
assert_eq!(file.to_str().unwrap(), "/etc/proxy/service.descriptor.bin");
}
_ => panic!("expected File descriptor source"),
}
}
#[test]
fn test_descriptor_source_reflection() {
let yaml = r#"
upstream:
default: "grpc://localhost:4180"
descriptors:
- reflection: "grpc://localhost:4180"
"#;
let config: ProxyConfig = parse_yaml(yaml).unwrap();
match &config.descriptors[0] {
DescriptorSource::Reflection { reflection } => {
assert_eq!(reflection, "grpc://localhost:4180");
}
_ => panic!("expected Reflection descriptor source"),
}
}
#[test]
fn test_parse_rate() {
assert_eq!(ProxyConfig::parse_rate("20/min"), Some(20));
assert_eq!(ProxyConfig::parse_rate("100/min"), Some(100));
assert_eq!(ProxyConfig::parse_rate("5/min"), Some(5));
assert_eq!(ProxyConfig::parse_rate("invalid"), None);
}
#[test]
fn shield_rejects_unknown_field() {
let yaml = r#"
upstream:
default: "grpc://localhost:4180"
shield:
enabled: true
profiles:
auth: { rate: "20/min", burst: 5 }
rules:
- pattern: "/v1/**"
key: { type: ip }
profil: "auth"
"#;
let err = parse_yaml::<ProxyConfig>(yaml);
assert!(err.is_err(), "unknown shield field must be rejected");
}
#[test]
fn shield_rejects_unknown_field_in_rule_key() {
let yaml = r#"
upstream:
default: "grpc://localhost:4180"
shield:
enabled: true
profiles:
auth: { rate: "20/min", burst: 5 }
rules:
- pattern: "/v1/**"
key: { type: ip, name: x-api-key }
profile: "auth"
"#;
let err = parse_yaml::<ProxyConfig>(yaml);
assert!(err.is_err(), "unknown field in a rule key must be rejected");
}
#[test]
fn test_openapi_config_deserialize() {
let yaml = r#"
upstream:
default: "grpc://localhost:4180"
openapi:
enabled: true
path: "/api/openapi.json"
docs_path: "/api/docs"
title: "Test API"
version: "2.0.0"
"#;
let config: ProxyConfig = parse_yaml(yaml).unwrap();
let openapi = config.openapi.unwrap();
assert!(openapi.enabled);
assert_eq!(openapi.path, "/api/openapi.json");
assert_eq!(openapi.docs_path, "/api/docs");
assert_eq!(openapi.title.unwrap(), "Test API");
assert_eq!(openapi.version.unwrap(), "2.0.0");
}
#[test]
fn test_openapi_config_defaults() {
let yaml = r#"
upstream:
default: "grpc://localhost:4180"
openapi:
enabled: true
"#;
let config: ProxyConfig = parse_yaml(yaml).unwrap();
let openapi = config.openapi.unwrap();
assert!(openapi.enabled);
assert_eq!(openapi.path, "/openapi.json");
assert_eq!(openapi.docs_path, "/docs");
assert!(openapi.title.is_none());
assert!(openapi.version.is_none());
}
#[test]
fn misspelled_top_level_key_is_reported() {
let yaml = r#"
upstream:
default: "grpc://x:1"
streaming:
ndjson_envelope: true
error_details:
enabled: false
error_detail:
enabled: false
maintenence:
enabled: true
"#;
assert_eq!(
unknown_config_keys(yaml),
vec!["error_detail".to_string(), "maintenence".to_string()]
);
}
#[test]
fn misspelled_streaming_key_is_reported() {
let yaml = r#"
upstream:
default: "grpc://x:1"
streaming:
sse_keep_alive_secs: 5
ndjson_envelope: true
ndjson_envelop: true
"#;
assert_eq!(
unknown_config_keys(yaml),
vec!["streaming.ndjson_envelop".to_string()]
);
}
#[test]
fn known_top_level_keys_cover_every_proxy_config_field() {
let config: ProxyConfig = parse_yaml("upstream:\n default: \"grpc://x:1\"\n").unwrap();
let ProxyConfig {
upstream: _,
descriptors: _,
listen: _,
service: _,
aliases: _,
openapi: _,
auth: _,
shield: _,
oidc_discovery: _,
health: _,
metrics: _,
maintenance: _,
cors: _,
logging: _,
metrics_classes: _,
forwarded_headers: _,
streaming: _,
concurrency: _,
grpc_web: _,
transcode: _,
client_address: _,
} = config;
for key in [
"upstream",
"descriptors",
"listen",
"service",
"aliases",
"openapi",
"auth",
"shield",
"oidc_discovery",
"health",
"metrics",
"maintenance",
"cors",
"logging",
"metrics_classes",
"forwarded_headers",
"streaming",
"error_details",
"response_headers",
"runtime",
"concurrency",
"grpc_web",
"transcode",
"client_address",
] {
assert!(KNOWN_TOP_LEVEL_KEYS.contains(&key), "{key}");
}
assert_eq!(KNOWN_TOP_LEVEL_KEYS.len(), 24);
}
#[test]
fn client_address_reads_its_section() {
let config = ProxyConfig::from_yaml_str(
"client_address:\n trusted_proxies: [\"10.0.0.0/8\", \"192.0.2.10\"]\n header: x_real_ip\n required: true\n",
)
.unwrap();
assert_eq!(
config.client_address.trusted_proxies,
["10.0.0.0/8", "192.0.2.10"]
);
assert_eq!(config.client_address.header, ForwardingHeader::XRealIp);
assert!(config.client_address.required);
let config = ProxyConfig::from_yaml_str("").unwrap();
assert!(config.client_address.trusted_proxies.is_empty());
assert_eq!(
config.client_address.header,
ForwardingHeader::XForwardedFor
);
assert!(!config.client_address.required);
}
#[test]
fn client_address_forward_reads_its_section() {
let config = ProxyConfig::from_yaml_str(
"client_address:\n forward:\n x_forwarded_for: append\n client_header: cf-connecting-ip\n audit_header: x-original-forwarded-for\n",
)
.unwrap();
let forward = &config.client_address.forward;
assert_eq!(forward.x_forwarded_for, XForwardedFor::Append);
assert_eq!(forward.client_header.as_deref(), Some("cf-connecting-ip"));
assert_eq!(
forward.audit_header.as_deref(),
Some("x-original-forwarded-for")
);
let config =
ProxyConfig::from_yaml_str("client_address:\n forward:\n client_header: null\n")
.unwrap();
assert_eq!(config.client_address.forward.client_header, None);
let forward = ProxyConfig::from_yaml_str("")
.unwrap()
.client_address
.forward;
assert_eq!(forward.x_forwarded_for, XForwardedFor::Verified);
assert_eq!(forward.client_header.as_deref(), Some("x-real-ip"));
assert_eq!(forward.audit_header, None);
for mode in ["verified", "resolved", "append", "preserve", "remove"] {
let yaml = format!("client_address:\n forward:\n x_forwarded_for: {mode}\n");
assert!(ProxyConfig::from_yaml_str(&yaml).is_ok(), "{mode}");
}
}
#[test]
fn an_invalid_client_address_section_fails_to_load() {
for yaml in [
"client_address:\n trusted_proxies: [\"10.0.0.0/33\"]\n",
"client_address:\n header: forwarded\n",
"client_address:\n trusted_proxy: [\"10.0.0.0/8\"]\n",
"client_address:\n forward:\n x_forwarded_for: rewrite\n",
"client_address:\n forward:\n client_headers: x-real-ip\n",
"client_address:\n forward:\n client_header: x-forwarded-for\n",
"client_address:\n forward:\n client_header: \"not a header\"\n",
"client_address:\n forward:\n audit_header: x-real-ip\n",
] {
assert!(ProxyConfig::from_yaml_str(yaml).is_err(), "{yaml}");
}
}
#[test]
fn shield_trusted_proxies_names_its_new_place() {
let err = ProxyConfig::from_yaml_str(
"shield:\n enabled: true\n trusted_proxies: [\"10.0.0.0/8\"]\n",
)
.unwrap_err();
assert!(
err.to_string().contains("client_address.trusted_proxies"),
"{err}"
);
let err = ProxyConfig::from_yaml_str("shield:\n enabld: true\n").unwrap_err();
assert!(err.to_string().contains("enabld"), "{err}");
}
#[test]
fn the_default_forwarded_headers_carry_no_client_address() {
let config = ProxyConfig::default();
for owned in ["x-forwarded-for", "x-real-ip", "forwarded"] {
assert!(
!config.forwarded_headers.iter().any(|h| h == owned),
"{owned}"
);
}
}
#[test]
fn runtime_key_is_known() {
let yaml = r#"
upstream:
default: "grpc://x:1"
runtime:
worker_threads: 2
runtimes:
worker_threads: 2
"#;
assert_eq!(unknown_config_keys(yaml), vec!["runtimes".to_string()]);
}
#[test]
fn response_headers_key_is_known() {
let yaml = r#"
upstream:
default: "grpc://x:1"
response_headers:
deny: ["x-debug-trace"]
response_header:
deny: ["x-debug-trace"]
"#;
assert_eq!(
unknown_config_keys(yaml),
vec!["response_header".to_string()]
);
}
#[test]
fn transcode_settings_read_the_response_header_deny_list() {
let options = transcode_options(
"upstream:\n default: \"grpc://x:1\"\nresponse_headers:\n deny: [\"X-Debug-Trace\", \"x-backend\"]\n",
)
.unwrap();
assert_eq!(
options
.denied_response_headers
.iter()
.map(|name| name.as_str())
.collect::<Vec<_>>(),
["x-debug-trace", "x-backend"]
);
}
#[test]
fn transcode_settings_without_response_headers_deny_nothing() {
let options = transcode_options("upstream:\n default: \"grpc://x:1\"\n").unwrap();
assert!(options.denied_response_headers.is_empty());
}
#[test]
fn transcode_settings_reject_an_invalid_deny_entry() {
let err = transcode_options(
"upstream:\n default: \"grpc://x:1\"\nresponse_headers:\n deny: [\"x debug\"]\n",
)
.unwrap_err();
assert!(err.contains("not a header name"), "{err}");
}
#[test]
fn transcode_settings_reject_unknown_response_headers_key() {
let err = transcode_options(
"upstream:\n default: \"grpc://x:1\"\nresponse_headers:\n denied: [\"x-debug-trace\"]\n",
)
.unwrap_err();
assert!(err.contains("denied"), "{err}");
}
#[test]
fn known_streaming_keys_cover_both_streaming_structs() {
let StreamingConfig {
sse_keep_alive_secs: _,
} = StreamingConfig::default();
let StreamingFileConfig { ndjson_envelope: _ } = StreamingFileConfig::default();
assert_eq!(
KNOWN_STREAMING_KEYS,
&["sse_keep_alive_secs", "ndjson_envelope"]
);
}
fn transcode_options(yaml: &str) -> Result<crate::transcode::TranscodeOptions, String> {
parse_yaml::<TranscodeFileConfig>(yaml)
.map_err(|e| e.to_string())?
.options()
}
#[test]
fn transcode_settings_default_to_details_everywhere_and_no_envelope() {
let options = transcode_options("upstream:\n default: \"grpc://x:1\"\n").unwrap();
assert!(options.error_details.enabled_for("/v1/users/{id}"));
assert!(!options.ndjson_envelope);
}
#[test]
fn transcode_settings_read_error_details_and_the_ndjson_envelope() {
let yaml = r#"
upstream:
default: "grpc://x:1"
streaming:
sse_keep_alive_secs: 30
ndjson_envelope: true
error_details:
enabled: false
routes:
- pattern: "/v1/public/internal/*"
enabled: false
- pattern: "/v1/public/**"
enabled: true
"#;
let options = transcode_options(yaml).unwrap();
assert!(options.ndjson_envelope);
assert!(options.error_details.enabled_for("/v1/public/items"));
assert!(!options
.error_details
.enabled_for("/v1/public/internal/{id}"));
assert!(!options.error_details.enabled_for("/v1/admin/items"));
let config: ProxyConfig = parse_yaml(yaml).unwrap();
assert_eq!(config.streaming.sse_keep_alive_secs, 30);
}
#[test]
fn transcode_settings_reject_unknown_error_details_key() {
let err = transcode_options(
"upstream:\n default: \"grpc://x:1\"\nerror_details:\n enable: false\n",
)
.unwrap_err();
assert!(err.contains("enable"), "{err}");
}
#[test]
fn transcode_settings_require_enabled_on_a_route_rule() {
let err = transcode_options(
"upstream:\n default: \"grpc://x:1\"\nerror_details:\n routes:\n - pattern: \"/v1/**\"\n",
)
.unwrap_err();
assert!(err.contains("enabled"), "{err}");
}
#[test]
fn transcode_settings_reject_a_relative_route_pattern() {
let err = transcode_options(
"upstream:\n default: \"grpc://x:1\"\nerror_details:\n routes:\n - pattern: \"v1/admin/**\"\n enabled: false\n",
)
.unwrap_err();
assert!(err.contains("must start with '/'"), "{err}");
}
#[test]
fn json_text_reads_as_a_config() {
let json = r#"{"upstream": {"default": "grpc://x:1"}, "service": {"name": "demo"},
"cors": {"origins": ["https://app.example"], "grpc_web": false},
"streaming": {"sse_keep_alive_secs": 5, "ndjson_envelope": true}}"#;
let config = ProxyConfig::from_yaml_str(json).unwrap();
assert_eq!(config.upstream.unwrap().default, "grpc://x:1");
assert_eq!(config.service.name, "demo");
assert_eq!(config.cors.origins, ["https://app.example"]);
assert!(!config.cors.grpc_web);
assert_eq!(config.streaming.sse_keep_alive_secs, 5);
assert!(transcode_options(json).unwrap().ndjson_envelope);
assert!(unknown_config_keys(json).is_empty());
}
#[test]
fn only_yaml_1_2_booleans_are_booleans() {
for value in ["yes", "no", "on", "off", "Y", "n"] {
let err = ProxyConfig::from_yaml_str(&format!("cors:\n grpc_web: {value}\n")).unwrap_err();
assert!(err.to_string().contains("cors.grpc_web"), "{value}: {err}");
}
for (value, expected) in [
("true", true),
("True", true),
("TRUE", true),
("false", false),
("False", false),
("FALSE", false),
] {
let config = ProxyConfig::from_yaml_str(&format!("cors:\n grpc_web: {value}\n")).unwrap();
assert_eq!(config.cors.grpc_web, expected, "{value}");
}
}
#[test]
fn the_packaged_config_template_loads() {
let yaml = include_str!("../../packaging/config.yaml");
let config = ProxyConfig::from_yaml_str(yaml).unwrap();
assert_eq!(config.service.name, "structured-proxy");
assert!(unknown_config_keys(yaml).is_empty());
}
#[test]
fn a_number_or_boolean_is_not_taken_for_text() {
for yaml in [
"service:\n name: true\n",
"service:\n name: 8080\n",
"service:\n name: 1.5\n",
"forwarded_headers: [123]\n",
"cors:\n origins: [false]\n",
] {
assert!(ProxyConfig::from_yaml_str(yaml).is_err(), "{yaml}");
}
let config = ProxyConfig::from_yaml_str("service:\n name: \"8080\"\n").unwrap();
assert_eq!(config.service.name, "8080");
let config = ProxyConfig::from_yaml_str("forwarded_headers: ['123', x-request-id]\n").unwrap();
assert_eq!(config.forwarded_headers, ["123", "x-request-id"]);
}
#[test]
fn a_parse_error_names_the_key_path_on_one_line() {
let err = ProxyConfig::from_yaml_str("cors:\n max_age_secs: soon\n").unwrap_err();
let message = err.to_string();
assert!(message.starts_with("cors.max_age_secs: "), "{message}");
assert!(!message.contains('\n'), "{message}");
let yaml =
"shield:\n rules:\n - pattern: \"/**\"\n key: { type: ip }\n profil: anon\n";
let message = ProxyConfig::from_yaml_str(yaml).unwrap_err().to_string();
assert!(message.contains("shield.rules[0]"), "{message}");
assert!(message.contains("profil"), "{message}");
}
#[test]
fn a_document_that_is_not_a_config_map_is_refused_without_a_path() {
for yaml in ["just text", "[1, 2]", "upstream: [unclosed"] {
let message = parse_yaml::<ProxyConfig>(yaml).unwrap_err().to_string();
assert!(!message.is_empty(), "{yaml}");
assert!(!message.starts_with(':'), "{yaml}: {message}");
assert!(!message.contains('\n'), "{yaml}: {message}");
}
}
#[test]
fn an_empty_document_is_the_default_config() {
for yaml in ["", "# nothing set yet\n", "---\n"] {
let config = ProxyConfig::from_yaml_str(yaml).unwrap();
assert!(config.upstream.is_none(), "{yaml:?}");
assert_eq!(config.listen.http, "0.0.0.0:8080", "{yaml:?}");
assert!(unknown_config_keys(yaml).is_empty(), "{yaml:?}");
}
}
#[test]
fn a_repeated_key_is_refused() {
let err = ProxyConfig::from_yaml_str("service:\n name: a\nservice:\n name: b\n").unwrap_err();
assert!(err.to_string().contains("service"), "{err}");
}
#[test]
fn more_than_one_document_is_refused() {
assert!(ProxyConfig::from_yaml_str("service:\n name: a\n---\nservice:\n name: b\n").is_err());
}
#[test]
fn unknown_keys_keep_the_document_order() {
let yaml =
"zeta: 1\nupstream:\n default: \"grpc://x:1\"\nalpha: 2\nstreaming:\n zz: 1\n aa: 2\n";
assert_eq!(
unknown_config_keys(yaml),
["zeta", "alpha", "streaming.zz", "streaming.aa"]
);
}
#[test]
fn unknown_keys_of_an_unreadable_document_are_none() {
for yaml in [
"just text",
"[1, 2]",
"upstream: [unclosed",
"streaming: 5\n",
] {
assert!(unknown_config_keys(yaml).is_empty(), "{yaml}");
}
}
#[test]
fn an_empty_streaming_section_has_no_unknown_keys() {
assert!(unknown_config_keys("streaming:\nstreamin:\n") == ["streamin"]);
}
#[test]
fn a_number_or_boolean_key_leaves_the_other_keys_reported() {
assert_eq!(
unknown_config_keys(
"8080: listen\ntrue: 1\nmaintenence: {}\nstreaming:\n 5: x\n ndjson_envelop: true\n"
),
["maintenence", "streaming.ndjson_envelop"]
);
}
#[test]
fn shield_trusted_proxies_names_its_new_place_even_when_empty() {
let err = ProxyConfig::from_yaml_str("shield:\n trusted_proxies:\n").unwrap_err();
assert!(
err.to_string().contains("client_address.trusted_proxies"),
"{err}"
);
let err = ProxyConfig::from_yaml_str("shield: 5\n").unwrap_err();
assert!(
!err.to_string().contains("client_address.trusted_proxies"),
"{err}"
);
}