captchaforge 0.2.40

Captcha detection and solving for Firefox and BiDi-driven browsers. Detection, vendor solver scaffolding, trusted cross-origin click delivery into nested OOPIFs, and stealth personas are implemented and tested; broad live-vendor solve rates are not yet benchmarked.
Documentation
//! Unit tests for [`super`] (arithmetic-captcha parse + answer injection).

use super::*;

#[test]
fn solves_digit_addition() {
    assert_eq!(solve_expression("What is 3 + 5?"), Some(8));
}

#[test]
fn solves_digit_subtraction() {
    assert_eq!(solve_expression("12 - 4 = ?"), Some(8));
}

#[test]
fn solves_digit_multiplication() {
    assert_eq!(solve_expression("7 * 2 = ?"), Some(14));
    assert_eq!(solve_expression("7 × 2 = ?"), Some(14));
    assert_eq!(solve_expression("7 x 2 = ?"), Some(14));
}

#[test]
fn solves_digit_division() {
    assert_eq!(solve_expression("20 / 4 = ?"), Some(5));
    assert_eq!(solve_expression("20 ÷ 4 = ?"), Some(5));
}

#[test]
fn solves_word_numbers() {
    assert_eq!(solve_expression("What is two plus four?"), Some(6));
    assert_eq!(solve_expression("six minus three"), Some(3));
    assert_eq!(solve_expression("five times three"), Some(15));
}

#[test]
fn solves_word_operators() {
    assert_eq!(solve_expression("4 plus 5"), Some(9));
    assert_eq!(solve_expression("10 minus 3"), Some(7));
    assert_eq!(solve_expression("3 times 4"), Some(12));
    assert_eq!(solve_expression("12 divided by 3"), Some(4));
    assert_eq!(solve_expression("5 multiplied by 6"), Some(30));
}

#[test]
fn ignores_surrounding_text() {
    assert_eq!(
        solve_expression("Solve this anti-spam check: 8 + 7 = ? then submit"),
        Some(15)
    );
}

#[test]
fn handles_no_match() {
    assert!(solve_expression("How are you today?").is_none());
    assert!(solve_expression("").is_none());
    assert!(solve_expression("just one number 5").is_none());
}

#[test]
fn handles_div_by_zero() {
    assert!(solve_expression("5 / 0").is_none());
}

#[test]
fn handles_negative_results() {
    assert_eq!(solve_expression("3 - 10"), Some(-7));
}

#[test]
fn solves_unicode_minus_sign() {
    // Typeset math captchas (e.g. multi-step wizards using a CSS
    // font-stack with proper math glyphs) emit U+2212 instead of
    // ASCII '-'. Solver must handle both, without this, the
    // multi_step bench fixture (8 − 3 = ?) fails detection.
    assert_eq!(solve_expression("8 \u{2212} 3 = ?"), Some(5));
    assert_eq!(solve_expression("8 \u{2013} 3"), Some(5));
    assert_eq!(solve_expression("8 \u{2014} 3"), Some(5));
}

#[test]
fn first_match_wins_when_multiple_expressions() {
    // First expression in source order is what we return.
    assert_eq!(
        solve_expression("Question 1: 2 + 2 = ? Question 2: 3 + 3 = ?"),
        Some(4)
    );
}

#[test]
fn solves_zero_through_twenty_word_numbers() {
    for (i, word) in [
        "zero",
        "one",
        "two",
        "three",
        "four",
        "five",
        "six",
        "seven",
        "eight",
        "nine",
        "ten",
        "eleven",
        "twelve",
        "thirteen",
        "fourteen",
        "fifteen",
        "sixteen",
        "seventeen",
        "eighteen",
        "nineteen",
        "twenty",
    ]
    .iter()
    .enumerate()
    {
        assert_eq!(parse_number(word), Some(i as i64), "word: {word}");
    }
}

#[test]
fn name_and_method_stable() {
    let s = MathCaptchaSolver::new();
    assert_eq!(s.name(), "MathCaptchaSolver");
    assert_eq!(s.method(), SolveMethod::BehavioralBypass);
}

#[test]
fn supports_relevant_kinds_only() {
    let s = MathCaptchaSolver::new();
    assert!(s.supports(&DetectedCaptcha::Custom("wp_math_captcha".into())));
    assert!(s.supports(&DetectedCaptcha::Custom("math_captcha".into())));
    assert!(s.supports(&DetectedCaptcha::PowCaptcha));
    assert!(s.supports(&DetectedCaptcha::ImageCaptcha));
    assert!(s.supports(&DetectedCaptcha::CanvasCaptcha));
    // Slider/non-math Custom names must NOT match, math should
    // not claim DataDome or PerimeterX, which are interactive
    // slider/click captchas.
    assert!(!s.supports(&DetectedCaptcha::Custom("datadome".into())));
    assert!(!s.supports(&DetectedCaptcha::Custom("perimeterx_human".into())));
    assert!(!s.supports(&DetectedCaptcha::Custom("akamai_bot_manager".into())));
    assert!(!s.supports(&DetectedCaptcha::Turnstile));
    assert!(!s.supports(&DetectedCaptcha::None));
}

const QUESTION_SELECTORS: &[&str] = &[
    "[id*='math-captcha'] label",
    "[class*='math-captcha'] label",
    "[id*='math_captcha'] label",
    "[class*='math_captcha'] label",
    ".captcha-question",
    ".captcha-prompt",
    "label[for*='captcha']",
    "label[for*='math']",
    "[data-captcha-question]",
    ".g-recaptcha-question",
];

const ANSWER_SELECTORS: &[&str] = &[
    "input[name*='math-captcha']",
    "input[name*='math_captcha']",
    "input[name='captcha']",
    "input[name='captcha_answer']",
    "input[id*='captcha-answer']",
    "input[name*='quiz']",
    "input[type='text'][placeholder*='answer']",
    "input[type='number'][name*='captcha']",
];

#[test]
fn probe_js_includes_documented_selectors() {
    for sel in QUESTION_SELECTORS {
        assert!(
            PROBE_JS.contains(sel),
            "probe must contain question sel: {sel}"
        );
    }
    for sel in ANSWER_SELECTORS {
        assert!(
            PROBE_JS.contains(sel),
            "probe must contain input sel: {sel}"
        );
    }
}