use captchaforge::detect::rules::built_in_rules;
#[test]
fn schema_file_is_well_formed_json() {
let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("schemas")
.join("community-rules.schema.json");
let body = std::fs::read_to_string(&path).expect("read schema");
let v: serde_json::Value = serde_json::from_str(&body).expect("schema is valid JSON");
assert_eq!(v["$schema"], "https://json-schema.org/draft-07/schema#");
assert_eq!(v["type"], "object");
let provider_def = &v["definitions"]["provider"];
assert!(provider_def.is_object(), "definitions.provider missing");
let required = &provider_def["required"];
assert!(
required
.as_array()
.map(|a| a.iter().any(|v| v == "name") && a.iter().any(|v| v == "priority"))
.unwrap_or(false),
"schema provider definition must require name + priority"
);
}
#[test]
fn every_bundled_rule_passes_lightweight_schema_invariants() {
let pack = built_in_rules().expect("bundled rules parse");
let mut errors: Vec<String> = Vec::new();
let mut seen_names = std::collections::HashSet::new();
for rule in &pack.providers {
if !rule
.name
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
{
errors.push(format!(
"rule {:?} name violates pattern ^[a-z][a-z0-9_]+$",
rule.name
));
}
if rule.name.is_empty()
|| rule.name.len() > 64
|| !rule.name.chars().next().is_some_and(|c| c.is_ascii_lowercase())
{
errors.push(format!("rule name length / leading-char invalid: {:?}", rule.name));
}
if !seen_names.insert(rule.name.clone()) {
errors.push(format!("rule name {:?} duplicated in pack", rule.name));
}
if !(1..=999).contains(&rule.priority) {
errors.push(format!(
"rule {} priority {} outside [1, 999]",
rule.name, rule.priority
));
}
let t = &rule.triggers;
if t.selectors.is_empty()
&& t.window_globals.is_empty()
&& t.script_src_contains.is_empty()
&& t.title_contains.is_empty()
&& t.cookie_names.is_empty()
{
errors.push(format!("rule {} has no triggers at all", rule.name));
}
}
assert!(
errors.is_empty(),
"bundled rules fail schema invariants:\n {}",
errors.join("\n ")
);
}
#[test]
fn schema_solver_enum_matches_chain_solver_names() {
let chain = captchaforge::solver::CaptchaSolverChain::default_chain();
let chain_names: std::collections::HashSet<&str> =
chain.solver_names().into_iter().collect();
let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("schemas")
.join("community-rules.schema.json");
let body = std::fs::read_to_string(&path).expect("read schema");
let v: serde_json::Value = serde_json::from_str(&body).expect("schema parses");
let schema_enum: Vec<&str> = v["definitions"]["provider"]["properties"]["solver_names"]
["items"]["enum"]
.as_array()
.expect("solver_names enum is an array")
.iter()
.map(|s| s.as_str().expect("enum entry is string"))
.collect();
for name in chain_names {
assert!(
schema_enum.contains(&name),
"solver {:?} from default_chain is missing from the schema's solver_names enum — \
external contributors who reference it in TOML will fail validation",
name
);
}
}