use captchaforge::detect::{is_captcha, solver_kind_str, CaptchaInfo, DetectedCaptcha};
use proptest::prelude::*;
use proptest::test_runner::TestRunner;
#[test]
fn is_captcha_true_for_all_non_none() {
let mut runner = TestRunner::default();
runner
.run(&detected_captcha_non_none(), |variant| {
let info = CaptchaInfo {
kind: variant,
site_key: None,
page_url: String::new(),
container_selector: None,
};
prop_assert!(is_captcha(&info));
Ok(())
})
.unwrap();
}
#[test]
fn is_captcha_false_for_none() {
let info = CaptchaInfo {
kind: DetectedCaptcha::None,
site_key: None,
page_url: String::new(),
container_selector: None,
};
assert!(!is_captcha(&info));
}
#[test]
fn solver_kind_str_matches_variant() {
let mut runner = TestRunner::default();
runner
.run(&any_detected_captcha(), |variant| {
let result = solver_kind_str(&variant);
match variant {
DetectedCaptcha::Turnstile => prop_assert_eq!(result, Some("turnstile")),
DetectedCaptcha::RecaptchaV2 => prop_assert_eq!(result, Some("recaptcha_v2")),
DetectedCaptcha::RecaptchaV3 => prop_assert_eq!(result, Some("recaptcha_v3")),
DetectedCaptcha::HCaptcha => prop_assert_eq!(result, Some("hcaptcha")),
DetectedCaptcha::ImageCaptcha => prop_assert_eq!(result, Some("image_captcha")),
DetectedCaptcha::AudioCaptcha => prop_assert_eq!(result, Some("audio_captcha")),
DetectedCaptcha::PowCaptcha => prop_assert_eq!(result, Some("pow_captcha")),
DetectedCaptcha::SliderCaptcha => prop_assert_eq!(result, Some("slider_captcha")),
DetectedCaptcha::CanvasCaptcha => prop_assert_eq!(result, Some("canvas_captcha")),
DetectedCaptcha::ShadowDomCaptcha => {
prop_assert_eq!(result, Some("shadow_dom_captcha"))
}
DetectedCaptcha::MultiStepCaptcha => {
prop_assert_eq!(result, Some("multi_step_captcha"))
}
DetectedCaptcha::Custom(_) => prop_assert_eq!(result, None),
DetectedCaptcha::None => prop_assert_eq!(result, None),
_ => prop_assert_eq!(result, None),
}
Ok(())
})
.unwrap();
}
#[test]
fn detected_captcha_serde_roundtrip() {
let mut runner = TestRunner::default();
runner
.run(&any_detected_captcha(), |variant| {
let json = serde_json::to_string(&variant).unwrap();
let back: DetectedCaptcha = serde_json::from_str(&json).unwrap();
prop_assert_eq!(variant, back);
Ok(())
})
.unwrap();
}
#[test]
fn captcha_info_serde_roundtrip() {
let mut runner = TestRunner::default();
let strategy = (
any_detected_captcha(),
proptest::option::of("[a-zA-Z0-9_-]{0,64}"),
"https?://[a-z0-9.-]{1,64}/[a-zA-Z0-9/_-]{0,128}",
proptest::option::of("[.#][a-zA-Z_-][a-zA-Z0-9_-]{0,32}"),
);
runner
.run(&strategy, |(kind, site_key, page_url, container)| {
let info = CaptchaInfo {
kind,
site_key,
page_url,
container_selector: container,
};
let json = serde_json::to_string(&info).unwrap();
let back: CaptchaInfo = serde_json::from_str(&json).unwrap();
prop_assert_eq!(info.kind, back.kind);
prop_assert_eq!(info.site_key, back.site_key);
prop_assert_eq!(info.page_url, back.page_url);
prop_assert_eq!(info.container_selector, back.container_selector);
Ok(())
})
.unwrap();
}
#[test]
fn captcha_info_serde_never_panics() {
let mut runner = TestRunner::default();
let strategy = (
any_detected_captcha(),
proptest::option::of("(?s).*"),
"(?s).*",
proptest::option::of("(?s).*"),
);
runner
.run(&strategy, |(kind, site_key, page_url, container)| {
let info = CaptchaInfo {
kind,
site_key,
page_url,
container_selector: container,
};
let json = serde_json::to_string(&info);
prop_assert!(json.is_ok());
let back: Result<CaptchaInfo, _> = serde_json::from_str(&json.unwrap());
prop_assert!(back.is_ok());
Ok(())
})
.unwrap();
}
#[test]
fn new_detected_captcha_variants_serialize_to_snake_case() {
static CASES: &[(DetectedCaptcha, &str)] = &[
(DetectedCaptcha::PowCaptcha, "pow_captcha"),
(DetectedCaptcha::SliderCaptcha, "slider_captcha"),
(DetectedCaptcha::CanvasCaptcha, "canvas_captcha"),
(DetectedCaptcha::ShadowDomCaptcha, "shadow_dom_captcha"),
(DetectedCaptcha::MultiStepCaptcha, "multi_step_captcha"),
];
let mut runner = TestRunner::default();
runner
.run(&proptest::sample::select(CASES), |case| {
let variant = &case.0;
let expected = case.1;
let json = serde_json::to_string(variant).unwrap();
prop_assert_eq!(
json,
format!("\"{}\"", expected),
"serialization mismatch for {:?}",
variant
);
Ok(())
})
.unwrap();
}
#[test]
fn solver_kind_str_returns_correct_values_for_new_variants() {
let cases = [
(DetectedCaptcha::PowCaptcha, Some("pow_captcha")),
(DetectedCaptcha::SliderCaptcha, Some("slider_captcha")),
(DetectedCaptcha::CanvasCaptcha, Some("canvas_captcha")),
(
DetectedCaptcha::ShadowDomCaptcha,
Some("shadow_dom_captcha"),
),
(
DetectedCaptcha::MultiStepCaptcha,
Some("multi_step_captcha"),
),
];
for (variant, expected) in cases {
assert_eq!(
solver_kind_str(&variant),
expected,
"solver_kind_str mismatch for {:?}",
variant
);
}
}
#[test]
fn detect_js_contains_expected_fragments() {
let js = captchaforge::detect::DETECT_JS;
let known = [
"turnstile",
"recaptcha",
"hcaptcha",
"image",
"audio",
"pow",
"slider",
"canvas",
"shadow_dom",
"multi_step",
"challenge",
];
assert!(known.iter().any(|&k| js.contains(k)));
}
fn any_detected_captcha() -> impl Strategy<Value = DetectedCaptcha> {
prop_oneof![
Just(DetectedCaptcha::Turnstile),
Just(DetectedCaptcha::RecaptchaV2),
Just(DetectedCaptcha::RecaptchaV3),
Just(DetectedCaptcha::HCaptcha),
Just(DetectedCaptcha::ImageCaptcha),
Just(DetectedCaptcha::AudioCaptcha),
Just(DetectedCaptcha::PowCaptcha),
Just(DetectedCaptcha::SliderCaptcha),
Just(DetectedCaptcha::CanvasCaptcha),
Just(DetectedCaptcha::ShadowDomCaptcha),
Just(DetectedCaptcha::MultiStepCaptcha),
Just(DetectedCaptcha::None),
]
}
fn detected_captcha_non_none() -> impl Strategy<Value = DetectedCaptcha> {
prop_oneof![
Just(DetectedCaptcha::Turnstile),
Just(DetectedCaptcha::RecaptchaV2),
Just(DetectedCaptcha::RecaptchaV3),
Just(DetectedCaptcha::HCaptcha),
Just(DetectedCaptcha::ImageCaptcha),
Just(DetectedCaptcha::AudioCaptcha),
Just(DetectedCaptcha::PowCaptcha),
Just(DetectedCaptcha::SliderCaptcha),
Just(DetectedCaptcha::CanvasCaptcha),
Just(DetectedCaptcha::ShadowDomCaptcha),
Just(DetectedCaptcha::MultiStepCaptcha),
]
}