use proptest::prelude::*;
use serde_json::Value;
use waf_core::LimitsConfig;
use waf_normalizer::body::flatten_value;
use waf_normalizer::NormalizationError;
fn limits_depth(max_json_depth: usize) -> LimitsConfig {
let mut l = LimitsConfig::default();
l.max_json_depth = max_json_depth;
l
}
fn nest(depth: usize) -> Value {
let mut v = Value::Null;
for _ in 0..depth {
v = Value::Array(vec![v]);
}
v
}
fn count_leaves(v: &Value) -> usize {
match v {
Value::Object(m) => m.values().map(count_leaves).sum(),
Value::Array(a) => a.iter().map(count_leaves).sum(),
_ => 1,
}
}
#[test]
fn depth_limit_fires_at_the_right_point() {
for m in 1..=8 {
let lim = limits_depth(m);
for d in 0..=(m + 3) {
let res = flatten_value(&nest(d), &lim);
if d > m {
assert!(
matches!(res, Err(NormalizationError::JsonDepthExceeded { .. })),
"depth {d} > max {m} must error, got {res:?}"
);
} else {
assert!(res.is_ok(), "depth {d} <= max {m} must be ok, got {res:?}");
}
}
}
}
fn arb_json() -> impl Strategy<Value = Value> {
let leaf = prop_oneof![
Just(Value::Null),
any::<bool>().prop_map(Value::Bool),
any::<i64>().prop_map(|n| Value::Number(n.into())),
"[a-z0-9]{0,8}".prop_map(Value::String),
];
leaf.prop_recursive(6, 64, 8, |inner| {
prop_oneof![
prop::collection::vec(inner.clone(), 0..6).prop_map(Value::Array),
prop::collection::hash_map("[a-z]{1,4}", inner, 0..6)
.prop_map(|m| Value::Object(m.into_iter().collect())),
]
})
}
proptest! {
#[test]
fn prop_flatten_non_panic_and_leaf_count(v in arb_json()) {
let pairs = flatten_value(&v, &LimitsConfig::default()).expect("within depth");
prop_assert_eq!(pairs.len(), count_leaves(&v));
}
}
#[test]
fn flat_giant_array_is_bounded_by_leaf_count() {
let arr = Value::Array((0..10_000).map(Value::from).collect());
let pairs = flatten_value(&arr, &LimitsConfig::default()).unwrap();
assert_eq!(pairs.len(), 10_000);
}