use crate::policy::{parse_policy_quiet, ParsedPolicy};
use proptest::prelude::*;
fn is_meaningful(line: &str) -> bool {
let t = line.split('#').next().unwrap_or("").trim();
!t.is_empty()
}
fn honoured_count(p: &ParsedPolicy) -> usize {
p.rules.len() + p.allow_rules.len() + p.layer_rules.len() + p.only_rules.len()
}
fn token() -> impl Strategy<Value = String> {
prop_oneof![
Just("Net".to_string()), Just("Fs".to_string()), Just("Exec".to_string()),
Just("Env".to_string()), Just("Clock".to_string()), Just("Unknown".to_string()),
Just("deny".to_string()), Just("allow".to_string()), Just("pure".to_string()),
Just("forbid".to_string()), Just("in".to_string()), Just("->".to_string()),
Just("net".to_string()), Just("NET".to_string()), Just("Nett".to_string()),
Just("Deny".to_string()), Just("denyy".to_string()), Just("fobid".to_string()),
Just("Unknown[dispatch]".to_string()), Just("Unknown[dispatch,native]".to_string()),
Just("Unknown[dispatch,nativ]".to_string()), Just("Unknown[corp]".to_string()),
Just("Unknown[]".to_string()), Just("Unknown[".to_string()),
Just("app".to_string()), Just("billing".to_string()), Just("api.stripe.com".to_string()),
Just("a.b.c".to_string()), Just("*".to_string()),
"[a-zA-Z0-9_.:*\\[\\],-]{1,12}".prop_map(|s| s),
]
}
fn line() -> impl Strategy<Value = String> {
prop::collection::vec(token(), 1..5).prop_map(|ts| ts.join(" "))
}
proptest! {
#[test]
fn every_meaningful_line_is_honoured_or_disclosed(l in line()) {
prop_assume!(is_meaningful(&l));
let p = parse_policy_quiet(&l);
prop_assert!(
honoured_count(&p) > 0 || !p.errors.is_empty(),
"line was neither honoured nor disclosed — silently dropped: {l:?}"
);
}
#[test]
fn lines_do_not_interfere(ls in prop::collection::vec(line(), 1..6),
sep in prop_oneof![Just("\n"), Just("\r\n"), Just("\r")]) {
let ls: Vec<String> = ls.into_iter().filter(|l| is_meaningful(l)).collect();
prop_assume!(!ls.is_empty());
let together = parse_policy_quiet(&ls.join(sep));
let apart: (usize, usize) = ls.iter().map(|l| {
let p = parse_policy_quiet(l);
(honoured_count(&p), p.errors.len())
}).fold((0, 0), |a, b| (a.0 + b.0, a.1 + b.1));
prop_assert_eq!(
(honoured_count(&together), together.errors.len()), apart,
"parsing {} lines together differs from parsing them apart: {:?}", ls.len(), ls
);
}
#[test]
fn a_typo_in_an_unknown_filter_is_always_fatal(
valid in prop::collection::vec(
prop_oneof![Just("reflect"), Just("dispatch"), Just("indirect"),
Just("native"), Just("unresolved"), Just("setup")], 0..3),
typo in prop_oneof![Just("nativ"), Just("corp"), Just("dispatchh"), Just("Reflect"), Just("zz")],
at in 0usize..4,
) {
let mut toks: Vec<&str> = valid.clone();
let at = at.min(toks.len());
toks.insert(at, typo);
let l = format!("deny Unknown[{}] app", toks.join(","));
let p = parse_policy_quiet(&l);
prop_assert!(
p.errors.iter().any(|e| e.fatal),
"an unrecognised reason-class token must make the policy unhonourable — it was instead \
absorbed, leaving a rule that gates less than it says: {l:?} -> {} rule(s), {} error(s)",
honoured_count(&p), p.errors.len()
);
}
}