use captchaforge::detect::{CaptchaInfo, DetectedCaptcha};
use captchaforge::solver::{
CaptchaSolver, CloudflareInterstitialSolver, DataDomeSolver, ImpervaSolver, KasadaSolver,
};
mod common;
fn info(kind: DetectedCaptcha, url: &str) -> CaptchaInfo {
CaptchaInfo {
kind,
site_key: None,
page_url: url.to_string(),
container_selector: None,
}
}
async fn page_with(body_html: &str) -> (common::TestBrowser, String) {
let html = format!(
"<!doctype html><html><head><title>t</title></head><body>{body_html}</body></html>"
);
let addr = common::serve_once(html).await;
let url = format!("http://{addr}/");
let browser = common::launch_browser().await;
browser.new_page(&url).await.expect("navigate to mock origin");
(browser, url)
}
#[tokio::test]
async fn datadome_valid_cookie_solves() {
let (browser, url) = page_with(
r#"<script>document.cookie = 'datadome=' + 'A1b2C3d4'.repeat(16);</script>Loaded"#,
)
.await;
let solver = DataDomeSolver::new().with_max_wait_ms(5_000);
let res = solver
.solve(&browser, &info(DetectedCaptcha::Custom("datadome".into()), &url))
.await
.unwrap();
assert!(res.success, "valid datadome cookie should solve");
assert_eq!(res.solution, "datadome:cookie");
browser.close().await.unwrap();
}
#[tokio::test]
async fn datadome_slider_iframe_yields() {
let (browser, url) = page_with(
r#"<iframe src="https://geo.captcha-delivery.com/captcha/"></iframe>"#,
)
.await;
let solver = DataDomeSolver::new().with_max_wait_ms(3_000);
let res = solver
.solve(&browser, &info(DetectedCaptcha::Custom("datadome".into()), &url))
.await
.unwrap();
assert!(!res.success, "slider variant must yield, not falsely succeed");
browser.close().await.unwrap();
}
#[tokio::test]
async fn imperva_reese84_cookie_solves() {
let (browser, url) = page_with(
r#"<script>document.cookie = 'reese84=' + 'r'.repeat(90);</script>Loaded"#,
)
.await;
let solver = ImpervaSolver::new().with_max_wait_ms(5_000);
let res = solver
.solve(
&browser,
&info(DetectedCaptcha::Custom("imperva_incapsula".into()), &url),
)
.await
.unwrap();
assert!(res.success, "valid reese84 token should solve");
assert_eq!(res.solution, "imperva:cookie");
browser.close().await.unwrap();
}
#[tokio::test]
async fn imperva_incident_page_is_blocked() {
let (browser, url) = page_with(
r#"Request unsuccessful. Incapsula incident ID: 1234-5678901234567890"#,
)
.await;
let solver = ImpervaSolver::new().with_max_wait_ms(2_000);
let res = solver
.solve(
&browser,
&info(DetectedCaptcha::Custom("imperva_incapsula".into()), &url),
)
.await
.unwrap();
assert!(!res.success, "incident page must not be reported as solved");
assert!(res.screenshot.is_none(), "a hard block is a failure(), not a yield");
browser.close().await.unwrap();
}
#[tokio::test]
async fn kasada_token_and_kpsdk_solves() {
let (browser, url) = page_with(
r#"<script>window.KPSDK = { configured: true };
document.cookie = 'KP_UIDz=' + 'k'.repeat(40);</script>Loaded"#,
)
.await;
let solver = KasadaSolver::new().with_max_wait_ms(5_000);
let res = solver
.solve(&browser, &info(DetectedCaptcha::Custom("kasada".into()), &url))
.await
.unwrap();
assert!(res.success, "kasada clearance should solve");
assert_eq!(res.solution, "kasada:cookie");
browser.close().await.unwrap();
}
#[tokio::test]
async fn kasada_interruption_present_does_not_falsely_solve() {
let (browser, url) = page_with(
r#"<script src="/ips.js"></script>
<script>document.cookie = 'KP_UIDz=' + 'k'.repeat(40);</script>"#,
)
.await;
let solver = KasadaSolver::new().with_max_wait_ms(1_500);
let res = solver
.solve(&browser, &info(DetectedCaptcha::Custom("kasada".into()), &url))
.await
.unwrap();
assert!(!res.success, "active interruption must not be reported as solved");
browser.close().await.unwrap();
}
#[tokio::test]
async fn cloudflare_clearance_cookie_solves() {
let (browser, url) = page_with(
r#"<script>document.cookie = 'cf_clearance=' + 'c'.repeat(48);</script>Loaded"#,
)
.await;
let solver = CloudflareInterstitialSolver::new().with_max_wait_ms(5_000);
let res = solver
.solve(&browser, &info(DetectedCaptcha::Turnstile, &url))
.await
.unwrap();
assert!(res.success, "valid cf_clearance should solve");
assert_eq!(res.solution, "cf_clearance");
browser.close().await.unwrap();
}
#[tokio::test]
async fn cloudflare_error_1020_is_blocked() {
let (browser, url) = page_with(r#"<h1>Sorry, you have been blocked</h1><p>Error 1020</p>"#).await;
let solver = CloudflareInterstitialSolver::new().with_max_wait_ms(2_000);
let res = solver
.solve(&browser, &info(DetectedCaptcha::Turnstile, &url))
.await
.unwrap();
assert!(!res.success, "error 1020 block must fail");
assert!(res.screenshot.is_none(), "a hard block is a failure(), not a yield");
browser.close().await.unwrap();
}
#[tokio::test]
async fn cloudflare_managed_turnstile_yields_interactive() {
let (browser, url) = page_with(
r#"<script>window._cf_chl_opt = { cType: 'managed' };</script>
<iframe src="https://challenges.cloudflare.com/cdn-cgi/challenge-platform/x"></iframe>"#,
)
.await;
let solver = CloudflareInterstitialSolver::new().with_max_wait_ms(2_000);
let res = solver
.solve(&browser, &info(DetectedCaptcha::Turnstile, &url))
.await
.unwrap();
assert!(!res.success, "interactive managed challenge must yield, not auto-pass");
assert!(
res.screenshot.is_some(),
"interactive yield must capture a screenshot for the hand-off"
);
browser.close().await.unwrap();
}