use std::collections::HashMap;
use provide_telemetry::{redact_config, setup_telemetry, ConfigurationError, TelemetryConfig};
fn config_from(entries: &[(&str, &str)]) -> Result<TelemetryConfig, ConfigurationError> {
let env: HashMap<String, String> = entries
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect();
TelemetryConfig::from_map(&env)
}
#[test]
fn test_telemetry_config_default() {
let config = TelemetryConfig::default();
assert!(!config.service_name.is_empty());
}
#[test]
fn test_telemetry_config_from_env() {
let _config = TelemetryConfig::from_env();
}
#[test]
fn test_setup_telemetry() {
let _ = setup_telemetry();
let _ = setup_telemetry();
}
fn config_with_header(header: &str) -> Result<TelemetryConfig, String> {
let mut env = HashMap::new();
env.insert("OTEL_EXPORTER_OTLP_HEADERS".to_string(), header.to_string());
TelemetryConfig::from_map(&env).map_err(|e| e.to_string())
}
#[test]
fn test_percent_encoding_valid_is_accepted() {
let cfg = config_with_header("x-custom=hello%41world").expect("valid encoding must not fail");
let logs_headers = &cfg.logging.otlp_headers;
assert_eq!(
logs_headers.get("x-custom").map(|s| s.as_str()),
Some("helloAworld")
);
}
#[test]
fn test_percent_encoding_percent_at_end_is_rejected() {
let cfg = config_with_header("x-bad=value%").expect("config-level parse must succeed");
assert!(
!cfg.logging.otlp_headers.contains_key("x-bad"),
"header with bare % at end should be silently skipped"
);
}
#[test]
fn test_percent_encoding_one_hex_digit_is_rejected() {
let cfg = config_with_header("x-bad=value%4").expect("config-level parse must succeed");
assert!(
!cfg.logging.otlp_headers.contains_key("x-bad"),
"header with %<one-digit> should be silently skipped"
);
}
#[test]
fn test_percent_encoding_non_hex_first_char_is_rejected() {
let cfg = config_with_header("x-bad=value%GF").expect("config-level parse must succeed");
assert!(
!cfg.logging.otlp_headers.contains_key("x-bad"),
"header with %G (non-hex first digit) should be silently skipped"
);
}
#[test]
fn test_percent_encoding_non_hex_second_char_is_rejected() {
let cfg = config_with_header("x-bad=value%4Z").expect("config-level parse must succeed");
assert!(
!cfg.logging.otlp_headers.contains_key("x-bad"),
"header with %4Z (non-hex second digit) should be silently skipped"
);
}
#[test]
fn test_percent_encoding_boundary_exactly_two_chars_after_percent_is_valid() {
let cfg = config_with_header("x-ok=%4F").expect("config-level parse must succeed");
assert_eq!(
cfg.logging.otlp_headers.get("x-ok").map(|s| s.as_str()),
Some("O"),
"%4F (exactly idx+2 == len-1) must be treated as valid encoding"
);
}
#[test]
fn config_test_parse_bool_falsy_values_are_accepted() {
for val in &["false", "False", "FALSE", "0", "no", "NO", "off", "OFF"] {
let cfg = config_from(&[("PROVIDE_TRACE_ENABLED", val)])
.unwrap_or_else(|e| panic!("{val:?} should parse as false, got error: {e}"));
assert!(!cfg.tracing.enabled, "{val:?} must parse as false");
}
}
#[test]
fn config_test_non_negative_float_rejects_infinity() {
let err = config_from(&[("PROVIDE_EXPORTER_LOGS_TIMEOUT_SECONDS", "inf")])
.expect_err("infinity must be rejected");
assert!(err
.message
.contains("PROVIDE_EXPORTER_LOGS_TIMEOUT_SECONDS"));
}
#[test]
fn config_test_non_negative_float_rejects_negative() {
let err = config_from(&[("PROVIDE_EXPORTER_LOGS_BACKOFF_SECONDS", "-1")])
.expect_err("negative float must be rejected");
assert!(err
.message
.contains("PROVIDE_EXPORTER_LOGS_BACKOFF_SECONDS"));
}
#[test]
fn config_test_non_negative_float_accepts_zero() {
let cfg = config_from(&[("PROVIDE_EXPORTER_LOGS_BACKOFF_SECONDS", "0.0")])
.expect("zero must be a valid non-negative float");
assert_eq!(cfg.exporter.logs_backoff_seconds, 0.0);
}
#[test]
fn redact_config_masks_otlp_header_values() {
let cfg = config_from(&[(
"OTEL_EXPORTER_OTLP_HEADERS",
"authorization=Bearer secret123",
)])
.unwrap();
let redacted = redact_config(&cfg);
assert!(
redacted.logging.otlp_headers.contains_key("authorization"),
"key must be preserved"
);
assert_eq!(
redacted
.logging
.otlp_headers
.get("authorization")
.map(String::as_str),
Some("***REDACTED***"),
"value must be masked"
);
}
#[test]
fn redact_config_preserves_non_header_fields() {
let cfg = config_from(&[
("PROVIDE_TELEMETRY_SERVICE_NAME", "my-service"),
("PROVIDE_TELEMETRY_ENV", "prod"),
])
.unwrap();
let redacted = redact_config(&cfg);
assert_eq!(redacted.service_name, "my-service");
assert_eq!(redacted.environment, "prod");
}
#[test]
fn redact_config_empty_headers_unchanged() {
let cfg = TelemetryConfig::default();
let redacted = redact_config(&cfg);
assert!(
redacted.logging.otlp_headers.is_empty(),
"empty headers must stay empty"
);
}
#[test]
fn redact_config_does_not_mutate_original() {
let cfg = config_from(&[("OTEL_EXPORTER_OTLP_HEADERS", "x-token=realvalue")]).unwrap();
let original_value = cfg.logging.otlp_headers.get("x-token").cloned();
let _ = redact_config(&cfg);
assert_eq!(
cfg.logging.otlp_headers.get("x-token").cloned(),
original_value,
"original config must not be mutated"
);
}