use std::collections::BTreeMap;
use proptest::prelude::*;
use serde_json::{json, Value};
use provide_telemetry::testing::{acquire_test_state_lock, reset_telemetry_state};
use provide_telemetry::{
compute_error_fingerprint, event, extract_w3c_context, get_sampling_policy, parse_baggage,
sanitize_payload, set_sampling_policy, set_strict_schema, should_sample, SamplingPolicy,
Signal,
};
fn any_signal() -> impl Strategy<Value = Signal> {
prop_oneof![
Just(Signal::Logs),
Just(Signal::Traces),
Just(Signal::Metrics),
]
}
fn valid_traceparent() -> impl Strategy<Value = String> {
(
"[0-9a-f]{2}".prop_filter("version must not be ff", |v| v != "ff"),
"[0-9a-f]{32}".prop_filter("trace_id must not be all zeros", |t| {
t != "00000000000000000000000000000000"
}),
"[0-9a-f]{16}".prop_filter("span_id must not be all zeros", |s| s != "0000000000000000"),
"[0-9a-f]{2}",
)
.prop_map(|(v, t, s, f)| format!("{v}-{t}-{s}-{f}"))
}
proptest! {
#![proptest_config(ProptestConfig {
cases: 50,
failure_persistence: Some(Box::new(
proptest::test_runner::FileFailurePersistence::Direct(
"tests/property_test.proptest-regressions",
),
)),
..ProptestConfig::default()
})]
#[test]
fn sampling_rate_zero_never_samples(signal in any_signal()) {
let _guard = acquire_test_state_lock();
reset_telemetry_state();
set_sampling_policy(signal, SamplingPolicy {
default_rate: 0.0,
overrides: BTreeMap::new(),
}).unwrap();
for _ in 0..100 {
prop_assert!(!should_sample(signal, Some("anything")).unwrap());
prop_assert!(!should_sample(signal, None).unwrap());
}
}
#[test]
fn sampling_rate_one_always_samples(signal in any_signal()) {
let _guard = acquire_test_state_lock();
reset_telemetry_state();
set_sampling_policy(signal, SamplingPolicy {
default_rate: 1.0,
overrides: BTreeMap::new(),
}).unwrap();
for _ in 0..100 {
prop_assert!(should_sample(signal, Some("anything")).unwrap());
prop_assert!(should_sample(signal, None).unwrap());
}
}
#[test]
fn sampling_roundtrip_preserves_clamped_rate(
signal in any_signal(),
rate in -1.0f64..=2.0,
) {
let _guard = acquire_test_state_lock();
reset_telemetry_state();
let returned = set_sampling_policy(signal, SamplingPolicy {
default_rate: rate,
overrides: BTreeMap::new(),
}).unwrap();
let fetched = get_sampling_policy(signal).unwrap();
prop_assert_eq!(returned, fetched.clone());
prop_assert!(fetched.default_rate >= 0.0);
prop_assert!(fetched.default_rate <= 1.0);
}
}
proptest! {
#![proptest_config(ProptestConfig {
cases: 50,
failure_persistence: Some(Box::new(
proptest::test_runner::FileFailurePersistence::Direct(
"tests/property_test.proptest-regressions",
),
)),
..ProptestConfig::default()
})]
#[test]
fn sanitize_always_redacts_default_sensitive_keys(
value in "[a-zA-Z0-9]{1,40}",
) {
let _guard = acquire_test_state_lock();
reset_telemetry_state();
let payload = json!({
"password": value,
"token": value,
"api_key": value,
"safe_field": "visible",
});
let result = sanitize_payload(&payload, true, 10);
let obj = result.as_object().unwrap();
prop_assert_eq!(obj.get("password").unwrap(), &Value::String("***".into()));
prop_assert_eq!(obj.get("token").unwrap(), &Value::String("***".into()));
prop_assert_eq!(obj.get("api_key").unwrap(), &Value::String("***".into()));
prop_assert_eq!(
obj.get("safe_field").unwrap(),
&Value::String("visible".into())
);
}
#[test]
fn sanitize_disabled_returns_original(
key in "[a-z]{1,20}",
value in "[a-zA-Z0-9]{1,40}",
) {
let payload = json!({ key: value });
let result = sanitize_payload(&payload, false, 10);
prop_assert_eq!(result, payload);
}
#[test]
fn sanitize_detects_aws_like_keys(
suffix in "[A-Z0-9]{16}",
) {
let _guard = acquire_test_state_lock();
reset_telemetry_state();
let fake_key = format!("AKIA{suffix}");
let payload = json!({ "data": fake_key });
let result = sanitize_payload(&payload, true, 10);
let obj = result.as_object().unwrap();
prop_assert_eq!(
obj.get("data").unwrap(),
&Value::String("***".into()),
"AWS-like key '{}' should be redacted",
fake_key
);
}
}
proptest! {
#![proptest_config(ProptestConfig {
cases: 100,
failure_persistence: Some(Box::new(
proptest::test_runner::FileFailurePersistence::Direct(
"tests/property_test.proptest-regressions",
),
)),
..ProptestConfig::default()
})]
#[test]
fn event_three_valid_segments_succeeds(
a in "[a-z][a-z0-9_]{0,10}",
b in "[a-z][a-z0-9_]{0,10}",
c in "[a-z][a-z0-9_]{0,10}",
) {
let _guard = acquire_test_state_lock();
set_strict_schema(false);
let result = event(&[&a, &b, &c]);
prop_assert!(result.is_ok(), "3-segment event should succeed: {:?}", result);
let ev = result.unwrap();
prop_assert_eq!(ev.domain, a);
prop_assert_eq!(ev.action, b);
prop_assert_eq!(ev.status, c);
prop_assert!(ev.resource.is_none());
}
#[test]
fn event_four_valid_segments_succeeds(
a in "[a-z][a-z0-9_]{0,10}",
b in "[a-z][a-z0-9_]{0,10}",
c in "[a-z][a-z0-9_]{0,10}",
d in "[a-z][a-z0-9_]{0,10}",
) {
let _guard = acquire_test_state_lock();
set_strict_schema(false);
let result = event(&[&a, &b, &c, &d]);
prop_assert!(result.is_ok(), "4-segment event should succeed: {:?}", result);
let ev = result.unwrap();
prop_assert_eq!(ev.domain, a);
prop_assert_eq!(ev.action, b);
prop_assert_eq!(ev.resource, Some(c));
prop_assert_eq!(ev.status, d);
}
#[test]
fn event_wrong_segment_count_fails(count in (0usize..=10).prop_filter(
"exclude 3 and 4",
|c| *c < 3 || *c > 4,
)) {
let _guard = acquire_test_state_lock();
set_strict_schema(false);
let segments: Vec<&str> = (0..count).map(|_| "seg").collect();
let result = event(&segments);
prop_assert!(result.is_err(), "segment count {} should fail", count);
}
#[test]
fn event_strict_rejects_hyphens(
a in "[a-z][a-z0-9_]{0,10}",
) {
let _guard = acquire_test_state_lock();
set_strict_schema(true);
let result = event(&[&a, "has-hyphen", "ok"]);
prop_assert!(result.is_err(), "strict should reject hyphens");
set_strict_schema(false);
}
}
proptest! {
#![proptest_config(ProptestConfig {
cases: 200,
failure_persistence: Some(Box::new(
proptest::test_runner::FileFailurePersistence::Direct(
"tests/property_test.proptest-regressions",
),
)),
..ProptestConfig::default()
})]
#[test]
fn parse_baggage_never_panics(input in "\\PC{0,500}") {
let _ = parse_baggage(&input);
}
#[test]
fn parse_baggage_no_empty_keys(input in "\\PC{0,500}") {
let result = parse_baggage(&input);
for key in result.keys() {
prop_assert!(!key.is_empty(), "empty key in parse_baggage result");
}
}
#[test]
fn parse_baggage_roundtrip_simple_pairs(
pairs in prop::collection::vec(
("[a-z]{1,10}", "[a-z0-9]{1,20}"),
1..=5,
),
) {
let header = pairs
.iter()
.map(|(k, v)| format!("{k}={v}"))
.collect::<Vec<_>>()
.join(",");
let result = parse_baggage(&header);
for key in result.keys() {
prop_assert!(
pairs.iter().any(|(k, _)| k == key),
"unexpected key: {}",
key
);
}
for (k, v) in &result {
let expected = pairs.iter().rev().find(|(pk, _)| pk == k).unwrap();
prop_assert_eq!(v, &expected.1);
}
}
#[test]
fn extract_w3c_context_valid_traceparent_always_parses(tp in valid_traceparent()) {
let ctx = extract_w3c_context(Some(&tp), None, None);
prop_assert!(ctx.trace_id.is_some(), "trace_id should be set for valid traceparent");
prop_assert!(ctx.span_id.is_some(), "span_id should be set for valid traceparent");
prop_assert!(ctx.traceparent.is_some(), "traceparent should be preserved");
}
#[test]
fn extract_w3c_context_arbitrary_never_panics(
tp in proptest::option::of("\\PC{0,200}"),
ts in proptest::option::of("\\PC{0,200}"),
bg in proptest::option::of("\\PC{0,200}"),
) {
let _ = extract_w3c_context(
tp.as_deref(),
ts.as_deref(),
bg.as_deref(),
);
}
}
proptest! {
#![proptest_config(ProptestConfig {
cases: 200,
failure_persistence: Some(Box::new(
proptest::test_runner::FileFailurePersistence::Direct(
"tests/property_test.proptest-regressions",
),
)),
..ProptestConfig::default()
})]
#[test]
fn fingerprint_always_12_hex(
error_name in "[a-zA-Z]{1,30}",
stack in proptest::option::of("[a-zA-Z0-9 /\n]{0,200}"),
) {
let fp = compute_error_fingerprint(&error_name, stack.as_deref());
prop_assert_eq!(fp.len(), 12, "fingerprint length should be 12, got {}", fp.len());
prop_assert!(
fp.chars().all(|c| c.is_ascii_hexdigit()),
"non-hex char in fingerprint: {}",
fp
);
}
#[test]
fn fingerprint_deterministic(
error_name in "[a-zA-Z]{1,30}",
stack in proptest::option::of("[a-zA-Z0-9 /\n]{0,200}"),
) {
let fp1 = compute_error_fingerprint(&error_name, stack.as_deref());
let fp2 = compute_error_fingerprint(&error_name, stack.as_deref());
prop_assert_eq!(fp1, fp2, "same input must produce same fingerprint");
}
#[test]
fn fingerprint_different_inputs_different_outputs(
a in "[a-zA-Z]{1,20}",
b in "[a-zA-Z]{1,20}",
) {
prop_assume!(!a.eq_ignore_ascii_case(&b));
let fp_a = compute_error_fingerprint(&a, None);
let fp_b = compute_error_fingerprint(&b, None);
prop_assert_ne!(fp_a, fp_b, "distinct names should (almost always) differ");
}
}
#[test]
fn parse_baggage_rejects_non_token_keys() {
for raw in [
"ev\nil=x,ok=1",
"ev\ril=x,ok=1",
"bad key=x,ok=1",
"ev\u{0}il=x,ok=1",
] {
let got = provide_telemetry::parse_baggage(raw);
assert_eq!(got.len(), 1, "raw={raw:?} produced {got:?}");
assert_eq!(got.get("ok").map(String::as_str), Some("1"));
}
}
#[test]
fn parse_baggage_strips_control_chars_from_values() {
let got = provide_telemetry::parse_baggage("k=a\u{0}b\nc");
assert_eq!(got.get("k").map(String::as_str), Some("abc"));
}
#[test]
fn parse_baggage_keeps_legitimate_members() {
let got = provide_telemetry::parse_baggage("tenant=acme;role=admin,region=eu");
assert_eq!(got.get("tenant").map(String::as_str), Some("acme"));
assert_eq!(got.get("region").map(String::as_str), Some("eu"));
assert_eq!(got.len(), 2);
}