use std::path::PathBuf;
use captchaforge::detect::rules::{built_in_rules, ProviderRule};
use scraper::{Html, Selector};
fn fixture_path(vendor: &str, kind: &str) -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("rule_fixtures")
.join(vendor)
.join(format!("{kind}.html"))
}
fn read_fixture(vendor: &str, kind: &str) -> String {
let path = fixture_path(vendor, kind);
std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("read fixture {}: {e}", path.display()))
}
fn rule_matches(rule: &ProviderRule, html: &str) -> bool {
let doc = Html::parse_document(html);
for sel_str in &rule.triggers.selectors {
let Ok(sel) = Selector::parse(sel_str) else {
panic!(
"rule `{}` ships invalid CSS selector `{}` — fix the rule",
rule.name, sel_str,
);
};
if doc.select(&sel).next().is_some() {
return true;
}
}
if !rule.triggers.script_src_contains.is_empty() {
let script_sel = Selector::parse("script[src]").unwrap();
for script in doc.select(&script_sel) {
let Some(src) = script.value().attr("src") else {
continue;
};
for needle in &rule.triggers.script_src_contains {
if src.contains(needle) {
return true;
}
}
}
}
false
}
#[test]
fn every_community_rule_has_positive_and_negative_fixtures() {
let set = built_in_rules().expect("bundled rules parse");
for rule in &set.providers {
let pos = fixture_path(&rule.name, "positive");
let neg = fixture_path(&rule.name, "negative");
assert!(
pos.exists(),
"missing positive fixture for `{}` at {}",
rule.name,
pos.display()
);
assert!(
neg.exists(),
"missing negative fixture for `{}` at {}",
rule.name,
neg.display()
);
}
}
#[test]
fn every_rule_fires_on_its_positive_fixture() {
let set = built_in_rules().expect("bundled rules parse");
let mut misses: Vec<String> = Vec::new();
for rule in &set.providers {
let html = read_fixture(&rule.name, "positive");
if !rule_matches(rule, &html) {
misses.push(format!(
"rule `{}` did NOT match its positive fixture — selectors {:?}, script_src {:?}",
rule.name, rule.triggers.selectors, rule.triggers.script_src_contains,
));
}
}
assert!(
misses.is_empty(),
"rules failing positive case:\n {}",
misses.join("\n "),
);
}
#[test]
fn every_rule_rejects_its_negative_fixture() {
let set = built_in_rules().expect("bundled rules parse");
let mut spurious: Vec<String> = Vec::new();
for rule in &set.providers {
let html = read_fixture(&rule.name, "negative");
if rule_matches(rule, &html) {
spurious.push(format!(
"rule `{}` FIRED on its negative fixture — selectors or script_src too broad",
rule.name,
));
}
}
assert!(
spurious.is_empty(),
"rules with false-positive negatives:\n {}",
spurious.join("\n "),
);
}
#[test]
fn no_rule_cross_fires_on_another_rules_negative() {
let set = built_in_rules().expect("bundled rules parse");
let mut violations: Vec<String> = Vec::new();
for rule in &set.providers {
for other in &set.providers {
if rule.name == other.name {
continue;
}
let html = read_fixture(&other.name, "negative");
if rule_matches(rule, &html) {
violations.push(format!(
"rule `{}` matched `{}`'s negative fixture",
rule.name, other.name,
));
}
}
}
assert!(
violations.is_empty(),
"cross-rule precision violations:\n {}",
violations.join("\n "),
);
}