use captchaforge::detect::{CaptchaInfo, DetectedCaptcha};
use captchaforge::solver::{CaptchaPattern, CaptchaSolveResult, CaptchaType, SolveMethod};
fn roundtrip<T>(v: &T)
where
T: serde::Serialize + serde::de::DeserializeOwned + std::fmt::Debug + PartialEq,
{
let json = serde_json::to_string(v).expect("serialize");
let back: T = serde_json::from_str(&json).expect("deserialize");
assert_eq!(v, &back, "roundtrip mismatch via JSON: {json}");
}
#[test]
fn detected_captcha_tag_strings_are_stable() {
let pairs = [
(DetectedCaptcha::Turnstile, "\"turnstile\""),
(DetectedCaptcha::RecaptchaV2, "\"recaptcha_v2\""),
(DetectedCaptcha::RecaptchaV3, "\"recaptcha_v3\""),
(DetectedCaptcha::HCaptcha, "\"hcaptcha\""),
(DetectedCaptcha::ImageCaptcha, "\"image_captcha\""),
(DetectedCaptcha::AudioCaptcha, "\"audio_captcha\""),
(DetectedCaptcha::PowCaptcha, "\"pow_captcha\""),
(DetectedCaptcha::SliderCaptcha, "\"slider_captcha\""),
(DetectedCaptcha::CanvasCaptcha, "\"canvas_captcha\""),
(DetectedCaptcha::ShadowDomCaptcha, "\"shadow_dom_captcha\""),
(DetectedCaptcha::MultiStepCaptcha, "\"multi_step_captcha\""),
(DetectedCaptcha::None, "\"none\""),
];
for (variant, expected) in pairs {
let got = serde_json::to_string(&variant).unwrap();
assert_eq!(
got, expected,
"DetectedCaptcha::{variant:?} serialised as {got}, expected {expected}"
);
}
}
#[test]
fn detected_captcha_roundtrips_every_variant() {
for variant in [
DetectedCaptcha::Turnstile,
DetectedCaptcha::RecaptchaV2,
DetectedCaptcha::RecaptchaV3,
DetectedCaptcha::HCaptcha,
DetectedCaptcha::ImageCaptcha,
DetectedCaptcha::AudioCaptcha,
DetectedCaptcha::PowCaptcha,
DetectedCaptcha::SliderCaptcha,
DetectedCaptcha::CanvasCaptcha,
DetectedCaptcha::ShadowDomCaptcha,
DetectedCaptcha::MultiStepCaptcha,
DetectedCaptcha::None,
] {
roundtrip(&variant);
}
}
#[test]
fn captcha_info_roundtrips_with_and_without_optionals() {
let full = CaptchaInfo {
kind: DetectedCaptcha::Turnstile,
site_key: Some("0x4AAAAAAA_abc123".into()),
page_url: "https://example.com/login".into(),
container_selector: Some(".cf-turnstile".into()),
};
let json = serde_json::to_string(&full).unwrap();
let back: CaptchaInfo = serde_json::from_str(&json).unwrap();
assert_eq!(back.kind, full.kind);
assert_eq!(back.site_key, full.site_key);
assert_eq!(back.page_url, full.page_url);
assert_eq!(back.container_selector, full.container_selector);
let empty = CaptchaInfo {
kind: DetectedCaptcha::None,
site_key: None,
page_url: String::new(),
container_selector: None,
};
let json = serde_json::to_string(&empty).unwrap();
assert!(json.contains("\"site_key\":null"));
assert!(json.contains("\"container_selector\":null"));
}
#[test]
fn captcha_info_roundtrips_for_new_variants() {
for variant in [
DetectedCaptcha::PowCaptcha,
DetectedCaptcha::SliderCaptcha,
DetectedCaptcha::CanvasCaptcha,
DetectedCaptcha::ShadowDomCaptcha,
DetectedCaptcha::MultiStepCaptcha,
] {
let info = CaptchaInfo {
kind: variant,
site_key: Some("test-key".into()),
page_url: "https://example.com/captcha".into(),
container_selector: Some(".captcha".into()),
};
roundtrip(&info);
}
}
#[test]
fn solve_method_tag_strings_are_stable() {
let pairs = [
(SolveMethod::VisionLLM, "\"vision_llm\""),
(SolveMethod::AudioBypass, "\"audio_bypass\""),
(SolveMethod::BehavioralBypass, "\"behavioral_bypass\""),
(SolveMethod::ThirdPartyService, "\"third_party_service\""),
(SolveMethod::CrowdSourced, "\"crowd_sourced\""),
];
for (variant, expected) in pairs {
let got = serde_json::to_string(&variant).unwrap();
assert_eq!(got, expected);
}
}
#[test]
fn captcha_type_tag_strings_are_stable() {
let pairs = [
(CaptchaType::RecaptchaV2, "\"recaptcha_v2\""),
(CaptchaType::RecaptchaV3, "\"recaptcha_v3\""),
(CaptchaType::HCaptcha, "\"hcaptcha\""),
(CaptchaType::CloudflareTurnstile, "\"cloudflare_turnstile\""),
(CaptchaType::ImageGrid, "\"image_grid\""),
(CaptchaType::TextCaptcha, "\"text_captcha\""),
(CaptchaType::AudioCaptcha, "\"audio_captcha\""),
(CaptchaType::Slider, "\"slider\""),
];
for (variant, expected) in pairs {
let got = serde_json::to_string(&variant).unwrap();
assert_eq!(
got, expected,
"CaptchaType::{variant:?} serialised as {got}, expected {expected}"
);
}
}
#[test]
fn captcha_type_custom_serialises_with_inner_string() {
let v = CaptchaType::Custom("turnstile_v2".into());
let json = serde_json::to_string(&v).unwrap();
let back: CaptchaType = serde_json::from_str(&json).unwrap();
match back {
CaptchaType::Custom(s) => assert_eq!(s, "turnstile_v2"),
other => panic!("Custom variant lost in roundtrip: {other:?}"),
}
}
#[test]
fn captcha_pattern_roundtrips() {
let mut p = CaptchaPattern::new(
"example.com",
CaptchaType::CloudflareTurnstile,
SolveMethod::BehavioralBypass,
);
for i in 0..50 {
p.record(i % 3 != 0, 100 + i * 5, SolveMethod::BehavioralBypass);
}
let json = serde_json::to_string(&p).unwrap();
let back: CaptchaPattern = serde_json::from_str(&json).unwrap();
assert_eq!(back.domain, p.domain);
assert_eq!(back.captcha_type, p.captcha_type);
assert_eq!(back.best_method, p.best_method);
assert_eq!(back.sample_count, p.sample_count);
assert!((back.success_rate - p.success_rate).abs() < 1e-6);
assert_eq!(back.avg_solve_time, p.avg_solve_time);
}
#[test]
fn captcha_solve_result_roundtrips() {
let v = CaptchaSolveResult {
solution: "0x_token_payload".into(),
confidence: 0.93,
method: SolveMethod::VisionLLM,
time_ms: 4321,
success: true,
screenshot: Some("iVBORw0KGgo=".into()),
cookies: Vec::new(),
verified_outcome: None,
};
let json = serde_json::to_string(&v).unwrap();
let back: CaptchaSolveResult = serde_json::from_str(&json).unwrap();
assert_eq!(back.solution, v.solution);
assert!((back.confidence - v.confidence).abs() < 1e-6);
assert_eq!(back.method, v.method);
assert_eq!(back.time_ms, v.time_ms);
assert_eq!(back.success, v.success);
}
#[test]
fn unknown_solve_method_string_is_rejected() {
let bad = "\"telepathy\"";
let res: Result<SolveMethod, _> = serde_json::from_str(bad);
assert!(res.is_err(), "unknown SolveMethod tag must not deserialize");
}
#[test]
fn unknown_detected_captcha_string_is_rejected() {
let bad = "\"funcaptcha_v9\"";
let res: Result<DetectedCaptcha, _> = serde_json::from_str(bad);
assert!(
res.is_err(),
"unknown DetectedCaptcha tag must not deserialize"
);
}