use captchaforge::solver::{CaptchaPattern, CaptchaType, PatternStore, SolveMethod};
use proptest::prelude::*;
use proptest::test_runner::TestRunner;
#[test]
fn pattern_avg_solve_time_stays_within_observed_range() {
let mut runner = TestRunner::default();
runner
.run(
&proptest::collection::vec(100u64..1000u64, 1..100),
|times| {
let mut pattern = CaptchaPattern::new(
"example.com".to_string(),
CaptchaType::CloudflareTurnstile,
SolveMethod::BehavioralBypass,
);
for &t in × {
pattern.record(true, t, SolveMethod::BehavioralBypass);
}
let min_t = *times.iter().min().unwrap();
let max_t = *times.iter().max().unwrap();
prop_assert!(
pattern.avg_solve_time >= min_t && pattern.avg_solve_time <= max_t,
"EMA avg_solve_time {} should be in [{min_t}, {max_t}]",
pattern.avg_solve_time,
);
Ok(())
},
)
.unwrap();
}
#[test]
fn pattern_success_rate_exact_for_all_success() {
let mut pattern = CaptchaPattern::new(
"example.com".to_string(),
CaptchaType::CloudflareTurnstile,
SolveMethod::BehavioralBypass,
);
for _ in 0..100 {
pattern.record(true, 100, SolveMethod::BehavioralBypass);
}
assert_eq!(pattern.success_rate, 1.0);
}
#[test]
fn pattern_success_rate_exact_for_all_failure() {
let mut pattern = CaptchaPattern::new(
"example.com".to_string(),
CaptchaType::CloudflareTurnstile,
SolveMethod::BehavioralBypass,
);
for _ in 0..100 {
pattern.record(false, 100, SolveMethod::BehavioralBypass);
}
assert_eq!(pattern.success_rate, 0.0);
}
#[test]
fn pattern_store_isolation_per_domain() {
let store = PatternStore::default();
store.record(
"a.com",
&CaptchaType::RecaptchaV2,
true,
100,
SolveMethod::BehavioralBypass,
);
store.record(
"b.com",
&CaptchaType::RecaptchaV2,
false,
5000,
SolveMethod::VisionLLM,
);
let best_a = store.best_method("a.com", &CaptchaType::RecaptchaV2);
let best_b = store.best_method("b.com", &CaptchaType::RecaptchaV2);
assert_eq!(best_a, Some(SolveMethod::BehavioralBypass));
assert_eq!(best_b, Some(SolveMethod::VisionLLM));
}
#[test]
fn pattern_store_isolation_per_type() {
let store = PatternStore::default();
store.record(
"example.com",
&CaptchaType::RecaptchaV2,
true,
100,
SolveMethod::BehavioralBypass,
);
store.record(
"example.com",
&CaptchaType::RecaptchaV3,
false,
5000,
SolveMethod::VisionLLM,
);
let best_v2 = store.best_method("example.com", &CaptchaType::RecaptchaV2);
let best_v3 = store.best_method("example.com", &CaptchaType::RecaptchaV3);
assert_eq!(best_v2, Some(SolveMethod::BehavioralBypass));
assert_eq!(best_v3, Some(SolveMethod::VisionLLM));
}
#[test]
fn pattern_store_ties_break_to_faster_method() {
let store = PatternStore::default();
store.record(
"example.com",
&CaptchaType::CloudflareTurnstile,
true,
100,
SolveMethod::BehavioralBypass,
);
store.record(
"example.com",
&CaptchaType::CloudflareTurnstile,
true,
200,
SolveMethod::VisionLLM,
);
let best = store.best_method("example.com", &CaptchaType::CloudflareTurnstile);
assert_eq!(best, Some(SolveMethod::BehavioralBypass));
}
#[test]
fn pattern_store_best_method_not_updated_on_failure() {
let store = PatternStore::default();
store.record(
"example.com",
&CaptchaType::CloudflareTurnstile,
true,
100,
SolveMethod::BehavioralBypass,
);
store.record(
"example.com",
&CaptchaType::CloudflareTurnstile,
false,
200,
SolveMethod::VisionLLM,
);
let best = store.best_method("example.com", &CaptchaType::CloudflareTurnstile);
assert_eq!(best, Some(SolveMethod::BehavioralBypass));
}
#[test]
fn pattern_store_many_domains_no_crosstalk() {
let mut runner = TestRunner::default();
let domains: Vec<String> = (0..50).map(|i| format!("domain{}.com", i)).collect();
runner
.run(&(0..50usize), |_idx| {
let store = PatternStore::default();
for (i, domain) in domains.iter().enumerate() {
store.record(
domain,
&CaptchaType::RecaptchaV2,
i % 2 == 0,
100,
SolveMethod::BehavioralBypass,
);
}
let all = store.all_patterns();
prop_assert_eq!(all.len(), 50);
Ok(())
})
.unwrap();
}
#[test]
fn pattern_store_record_after_best_method_doesnt_panic() {
let store = PatternStore::default();
let _ = store.best_method("example.com", &CaptchaType::CloudflareTurnstile);
store.record(
"example.com",
&CaptchaType::CloudflareTurnstile,
true,
100,
SolveMethod::BehavioralBypass,
);
let best = store.best_method("example.com", &CaptchaType::CloudflareTurnstile);
assert_eq!(best, Some(SolveMethod::BehavioralBypass));
}