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`] (vendor token/clearance scraping).

use super::*;

#[test]
fn extract_probes_finds_navigator_webdriver() {
    let js = "if(navigator.webdriver){return false;}";
    let probes = extract_probes(js);
    assert!(probes.contains(&ProbeSurface::new("navigator", "webdriver")));
}

#[test]
fn extract_probes_finds_multi_namespace_probes() {
    let js = r#"
            const a = navigator.userAgent;
            const b = window.chrome;
            const c = document.referrer;
            const d = screen.width;
            const e = WebGLRenderingContext.UNMASKED_VENDOR_WEBGL;
        "#;
    let probes = extract_probes(js);
    assert!(probes.contains(&ProbeSurface::new("navigator", "userAgent")));
    assert!(probes.contains(&ProbeSurface::new("window", "chrome")));
    assert!(probes.contains(&ProbeSurface::new("document", "referrer")));
    assert!(probes.contains(&ProbeSurface::new("screen", "width")));
    assert!(probes.contains(&ProbeSurface::new(
        "WebGLRenderingContext",
        "UNMASKED_VENDOR_WEBGL"
    )));
}

#[test]
fn extract_probes_deduplicates_repeats() {
    let js = r#"
            navigator.webdriver;
            navigator.webdriver;
            navigator.webdriver;
        "#;
    let probes = extract_probes(js);
    assert_eq!(
        probes.iter().filter(|p| p.property == "webdriver").count(),
        1
    );
}

#[test]
fn extract_probes_filters_js_reserved_words() {
    // `navigator.then` is legal syntax (Promise interop) but
    // never a real probe (must be filtered).
    let js = "navigator.then(()=>{});navigator.toString();navigator.length;";
    let probes = extract_probes(js);
    assert!(!probes
        .iter()
        .any(|p| matches!(p.property.as_str(), "then" | "toString" | "length")));
}

#[test]
fn extract_probes_respects_left_word_boundary() {
    // `xnavigator.foo` shouldn't match `navigator.foo`.
    let js = "xnavigator.foo;navigator.bar;";
    let probes = extract_probes(js);
    assert!(probes.contains(&ProbeSurface::new("navigator", "bar")));
    // Important: `foo` only appears as part of `xnavigator.foo`,
    // which our boundary check should reject.
    assert!(!probes.contains(&ProbeSurface::new("navigator", "foo")));
}

#[test]
fn extract_probes_handles_empty_input() {
    assert!(extract_probes("").is_empty());
    assert!(extract_probes("// just a comment").is_empty());
}

#[test]
fn extract_probes_skips_minified_short_identifiers_correctly() {
    // Realistic minified pattern: `e=navigator,t=window`.
    // `e.foo` patterns become `navigator.foo` after the
    // assignment, but our regex-free scanner sees only the
    // LITERAL `<receiver>.<prop>` access. We pick up the
    // literal probe; the indirect alias `t.chrome` (where t
    // = window) is a documented false-negative class, wiring
    // alias resolution would require a real JS parser, which
    // we deliberately don't pull in.
    let js = "var e=navigator;var t=window;t.chrome;navigator.platform;";
    let probes = extract_probes(js);
    // Literal access (picked up).
    assert!(probes.contains(&ProbeSurface::new("navigator", "platform")));
    // Aliased access via `t.chrome` is NOT picked up because
    // the scanner doesn't follow assignments. This is the
    // contract.
    assert!(
        !probes.contains(&ProbeSurface::new("window", "chrome")),
        "aliased access via `t.chrome` should NOT resolve to window.chrome"
    );
}

#[test]
fn scrape_diff_returns_new_probes_when_baseline_is_empty() {
    let mut s = VendorJsScraper::new();
    // Manually populate a baseline via set_baseline + then
    // exercise `extract_probes` against a JS body to compute
    // the diff via the same logic the scraper runs internally.
    s.set_baseline("test_vendor", HashSet::new());
    let probes_now: HashSet<ProbeSurface> =
        extract_probes("navigator.webdriver;navigator.platform;")
            .into_iter()
            .collect();
    let baseline = s.baseline("test_vendor").unwrap().clone();
    let new: HashSet<_> = probes_now.difference(&baseline).cloned().collect();
    assert_eq!(new.len(), 2);
}

#[test]
fn scrape_diff_detects_added_probes_between_runs() {
    let mut s = VendorJsScraper::new();
    let v1: HashSet<ProbeSurface> = extract_probes("navigator.webdriver;").into_iter().collect();
    s.set_baseline("v", v1.clone());
    let v2: HashSet<ProbeSurface> = extract_probes("navigator.webdriver;navigator.platform;")
        .into_iter()
        .collect();
    let added: Vec<_> = v2.difference(&v1).cloned().collect();
    assert_eq!(added.len(), 1);
    assert_eq!(added[0].property, "platform");
}

#[test]
fn scrape_diff_detects_removed_probes_between_runs() {
    let v1: HashSet<ProbeSurface> =
        extract_probes("navigator.webdriver;navigator.userAgent;navigator.platform;")
            .into_iter()
            .collect();
    let v2: HashSet<ProbeSurface> = extract_probes("navigator.webdriver;navigator.platform;")
        .into_iter()
        .collect();
    let removed: Vec<_> = v1.difference(&v2).cloned().collect();
    assert_eq!(removed.len(), 1);
    assert_eq!(removed[0].property, "userAgent");
}

#[test]
fn scrape_report_summary_lists_new_probes() {
    let r = ScrapeReport {
        vendor: "demo".into(),
        urls_fetched: vec!["https://example.com/api.js".into()],
        bytes_total: 1234,
        probes_total: 1,
        probes_new: vec![ProbeSurface::new("navigator", "newProbe")],
        probes_removed: vec![],
        probes_stable: vec![],
    };
    let s = r.render_summary();
    assert!(s.contains("vendor: demo"));
    assert!(s.contains("new probes: 1"));
    assert!(s.contains("navigator.newProbe"));
}

#[test]
fn scrape_report_summary_lists_removed_probes() {
    let r = ScrapeReport {
        vendor: "demo".into(),
        urls_fetched: vec![],
        bytes_total: 0,
        probes_total: 0,
        probes_new: vec![],
        probes_removed: vec![ProbeSurface::new("window", "deprecated")],
        probes_stable: vec![],
    };
    let s = r.render_summary();
    assert!(s.contains("window.deprecated"));
    assert!(s.contains("removed:"));
}

#[test]
fn bundled_targets_cover_the_four_most_deployed_vendors() {
    let targets = bundled_targets();
    let names: Vec<&str> = targets.iter().map(|t| t.name.as_str()).collect();
    assert!(names.contains(&"cloudflare-turnstile"));
    assert!(names.contains(&"hcaptcha"));
    assert!(names.contains(&"recaptcha-v2"));
    assert!(names.contains(&"datadome"));
}

#[test]
fn every_bundled_target_has_at_least_one_url() {
    for t in bundled_targets() {
        assert!(!t.urls.is_empty(), "{} has no URLs", t.name);
    }
}

#[test]
fn probe_surface_displays_as_dotted_path() {
    let p = ProbeSurface::new("navigator", "webdriver");
    assert_eq!(p.to_string(), "navigator.webdriver");
}