1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
//! Property tests for `solver::token_shapes`. These guarantee the
//! oracle never exhibits surprising classification behaviour under
//! random input — a regression here looks like decoy tokens silently
//! passing as Plausible, which would let a fooled solver report
//! success on a hostile vendor response.
use captchaforge::solver::token_shapes::{for_vendor, TokenShape};
use proptest::prelude::*;
const VENDORS: &[&str] = &[
"cloudflare-turnstile",
"recaptcha-v2",
"recaptcha-v3",
"recaptcha-enterprise",
"hcaptcha",
];
proptest! {
/// The empty string is never Plausible — a successful solver
/// would never have produced it. Failing this check means the
/// chain might silently accept "" as a valid token from a buggy
/// solver implementation.
#[test]
fn empty_string_is_never_plausible(vendor in proptest::sample::select(VENDORS)) {
let oracle = for_vendor(vendor).unwrap();
prop_assert_ne!(oracle.classify(""), TokenShape::Plausible);
}
/// Whitespace-only strings are never Plausible.
#[test]
fn whitespace_only_is_never_plausible(
vendor in proptest::sample::select(VENDORS),
ws in "[ \t\n]{1,40}",
) {
let oracle = for_vendor(vendor).unwrap();
prop_assert_ne!(oracle.classify(&ws), TokenShape::Plausible);
}
/// The literal sentinel "FAILED" is always Decoy across all
/// vendors — common solver-fallback string we've seen in stub
/// code. Must never appear as Plausible.
#[test]
fn failed_sentinel_is_always_decoy(vendor in proptest::sample::select(VENDORS)) {
let oracle = for_vendor(vendor).unwrap();
prop_assert_eq!(oracle.classify("FAILED"), TokenShape::Decoy);
}
/// Single-byte tokens are always at most Suspect — far too short
/// to be a real captcha token. (Most vendors emit 200+ chars.)
#[test]
fn single_char_tokens_are_not_plausible(
vendor in proptest::sample::select(VENDORS),
c in "[a-zA-Z0-9]",
) {
let oracle = for_vendor(vendor).unwrap();
prop_assert_ne!(oracle.classify(&c), TokenShape::Plausible);
}
/// Classifications are deterministic. Calling classify twice
/// with the same input returns the same answer — no hidden
/// global state.
#[test]
fn classify_is_deterministic(
vendor in proptest::sample::select(VENDORS),
token in "[A-Za-z0-9_\\-=\\.]{0,400}",
) {
let oracle = for_vendor(vendor).unwrap();
let first = oracle.classify(&token);
let second = oracle.classify(&token);
prop_assert_eq!(first, second);
}
/// classify never panics on arbitrary unicode bytes — defensive
/// invariant since hostile vendor responses might contain
/// non-ASCII or even invalid sequences (we receive `&str` so
/// they'll be valid UTF-8 by typing, but exotic codepoints
/// shouldn't cause an oracle panic).
#[test]
fn classify_does_not_panic_on_unicode(
vendor in proptest::sample::select(VENDORS),
token in ".{0,200}",
) {
let oracle = for_vendor(vendor).unwrap();
let _ = oracle.classify(&token);
}
/// for_vendor is case-stable — exact-string match is the
/// contract, so unrecognised vendors return None instead of
/// silently aliasing to a different oracle.
#[test]
fn unknown_vendors_return_none(name in "[a-z][a-z0-9]{0,20}") {
prop_assume!(!matches!(
name.as_str(),
"cloudflare-turnstile"
| "turnstile"
| "recaptcha-v2"
| "recaptcha"
| "recaptcha-v3"
| "recaptcha-enterprise"
| "hcaptcha"
));
prop_assert!(for_vendor(&name).is_none());
}
}
#[test]
fn vendor_names_are_stable() {
// for_vendor matches via exact string equality. If a refactor
// changes the canonical names without updating consumers, the
// chain silently stops calling the oracle. Lock the canonical
// set so a renaming attempt fails CI.
for v in VENDORS {
let oracle = for_vendor(v).unwrap_or_else(|| panic!("oracle missing for {v}"));
// vendor() returns the canonical name; for the short alias
// forms ("turnstile", "recaptcha") it returns the canonical
// form, not the alias.
assert!(!oracle.vendor().is_empty());
}
}