use captchaforge::solver::{
CaptchaPattern, CaptchaSolveResult, CaptchaType, PatternStore, SolveConfig, SolveMethod,
};
#[test]
fn solve_config_with_max_values() {
let config = SolveConfig {
checkbox_poll_interval_ms: u64::MAX,
checkbox_max_attempts: u32::MAX,
token_poll_interval_ms: u64::MAX,
token_max_attempts: u32::MAX,
audio_button_delay_ms: u64::MAX,
audio_submit_delay_ms: u64::MAX,
vlm_http_timeout_ms: u64::MAX,
client_http_timeout_ms: u64::MAX,
};
assert_eq!(config.checkbox_poll_interval_ms, u64::MAX);
assert_eq!(config.checkbox_max_attempts, u32::MAX);
}
#[test]
fn pattern_record_with_u64_max_time() {
let mut pattern = CaptchaPattern::new(
"example.com",
CaptchaType::CloudflareTurnstile,
SolveMethod::BehavioralBypass,
);
pattern.record(true, u64::MAX, SolveMethod::BehavioralBypass);
assert_eq!(pattern.avg_solve_time, u64::MAX);
}
#[test]
fn pattern_with_1million_records() {
let mut pattern = CaptchaPattern::new(
"example.com",
CaptchaType::CloudflareTurnstile,
SolveMethod::BehavioralBypass,
);
for i in 0..1_000_000 {
pattern.record(i % 2 == 0, 100, SolveMethod::BehavioralBypass);
}
assert_eq!(pattern.sample_count, 1_000_000);
assert!(
pattern.success_rate >= 0.4 && pattern.success_rate <= 0.6,
"EMA in alternating steady state should bounce in [0.4, 0.6]; got {}",
pattern.success_rate
);
}
#[test]
fn store_with_1000_domains() {
let store = PatternStore::default();
for i in 0..1000 {
let domain = format!("domain{}.com", i);
store.record(
&domain,
&CaptchaType::RecaptchaV2,
true,
100,
SolveMethod::BehavioralBypass,
);
}
assert_eq!(store.all_patterns().len(), 1000);
}
#[test]
fn store_same_domain_different_types() {
let store = PatternStore::default();
store.record(
"example.com",
&CaptchaType::RecaptchaV2,
true,
100,
SolveMethod::BehavioralBypass,
);
store.record(
"example.com",
&CaptchaType::RecaptchaV3,
true,
200,
SolveMethod::VisionLLM,
);
store.record(
"example.com",
&CaptchaType::HCaptcha,
false,
500,
SolveMethod::AudioBypass,
);
assert_eq!(
store.best_method("example.com", &CaptchaType::RecaptchaV2),
Some(SolveMethod::BehavioralBypass)
);
assert_eq!(
store.best_method("example.com", &CaptchaType::RecaptchaV3),
Some(SolveMethod::VisionLLM)
);
assert_eq!(
store.best_method("example.com", &CaptchaType::HCaptcha),
Some(SolveMethod::AudioBypass)
);
}
#[test]
fn result_serde_with_1mb_solution() {
let big = "x".repeat(1_000_000);
let result = CaptchaSolveResult {
solution: big.clone(),
confidence: 0.5,
method: SolveMethod::BehavioralBypass,
time_ms: 1,
success: true,
screenshot: None,
cookies: Vec::new(),
verified_outcome: None,
};
let json = serde_json::to_string(&result).unwrap();
let back: CaptchaSolveResult = serde_json::from_str(&json).unwrap();
assert_eq!(back.solution.len(), 1_000_000);
}
#[test]
fn result_serde_with_unicode_solution() {
let result = CaptchaSolveResult {
solution: "日本語テスト🎉".into(),
confidence: 0.99,
method: SolveMethod::BehavioralBypass,
time_ms: 100,
success: true,
screenshot: None,
cookies: Vec::new(),
verified_outcome: None,
};
let json = serde_json::to_string(&result).unwrap();
let back: CaptchaSolveResult = serde_json::from_str(&json).unwrap();
assert_eq!(back.solution, "日本語テスト🎉");
}
#[test]
fn pattern_store_concurrent_high_contention() {
use std::thread;
let store = std::sync::Arc::new(PatternStore::default());
let mut handles = vec![];
for t in 0..50 {
let s = store.clone();
handles.push(thread::spawn(move || {
for i in 0..1000 {
let domain = format!("thread{}-domain{}.com", t, i % 10);
let ct = if i % 2 == 0 {
CaptchaType::RecaptchaV2
} else {
CaptchaType::CloudflareTurnstile
};
s.record(
&domain,
&ct,
i % 3 == 0,
i as u64 * 10,
SolveMethod::BehavioralBypass,
);
}
}));
}
for h in handles {
h.join().unwrap();
}
assert!(!store.all_patterns().is_empty());
}