use super::*;
use crate::cookies::CapturedCookie;
use guise::fingerprint::default_profile_facts;
#[test]
fn clean_transcript_strips_punctuation_and_lowercases() {
assert_eq!(clean_transcript("Five Seven, Nine!"), "five seven nine");
}
#[test]
fn clean_transcript_collapses_whitespace() {
assert_eq!(clean_transcript("a b\t\tc\nd"), "a b c d");
}
#[test]
fn clean_transcript_keeps_numbers() {
assert_eq!(clean_transcript("5 7 9 2 6"), "5 7 9 2 6");
}
#[test]
fn clean_transcript_drops_unicode_punctuation() {
assert_eq!(clean_transcript("\u{201C}hello\u{201D}"), "hello");
}
#[test]
fn clean_transcript_empty_input_is_empty() {
assert_eq!(clean_transcript(""), "");
assert_eq!(clean_transcript(" "), "");
assert_eq!(clean_transcript(",,,..."), "");
}
#[test]
fn rate_limit_matches_known_phrases() {
for p in RATE_LIMIT_PHRASES {
assert!(is_rate_limited(p), "should match: {p}");
assert!(
is_rate_limited(&p.to_uppercase()),
"case-insensitive match should hold for: {p}",
);
}
}
#[test]
fn rate_limit_substring_match_works() {
assert!(is_rate_limited(
"Sorry (please try again later. We have detected unusual traffic from your network)."
));
}
#[test]
fn rate_limit_does_not_match_normal_prompts() {
assert!(!is_rate_limited(
"Press PLAY to listen and type what you hear"
));
assert!(!is_rate_limited(""));
}
#[test]
fn audio_url_accepts_recaptcha_host() {
assert!(looks_like_audio_url(
"https://www.google.com/recaptcha/api2/payload?p=ASDF"
));
}
#[test]
fn audio_url_accepts_mp3_suffix_on_other_hosts() {
assert!(looks_like_audio_url("https://cdn.example.com/clip.mp3"));
assert!(looks_like_audio_url("http://x.test/y.mp3"));
}
#[test]
fn audio_url_rejects_data_blob_relative_empty() {
assert!(!looks_like_audio_url(""));
assert!(!looks_like_audio_url("data:audio/mpeg;base64,abc"));
assert!(!looks_like_audio_url("blob:https://x.test/abc"));
assert!(!looks_like_audio_url("//google.com/recaptcha/x.mp3"));
assert!(!looks_like_audio_url("/relative/clip.mp3"));
}
#[test]
fn solver_constructs_with_default_endpoint() {
let s = RecaptchaAudioSolver::new();
assert_eq!(s.stt_endpoint, "http://localhost:9000/asr");
assert_eq!(s.name(), "RecaptchaAudioSolver");
assert_eq!(s.method(), SolveMethod::AudioBypass);
}
fn captured_header<'a>(raw: &'a str, name: &str) -> Option<&'a str> {
raw.lines().find_map(|line| {
let (key, value) = line.split_once(':')?;
key.eq_ignore_ascii_case(name).then(|| value.trim())
})
}
#[tokio::test]
async fn recaptcha_audio_client_uses_shared_audio_subresource_headers() {
use tokio::io::{AsyncReadExt, AsyncWriteExt};
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let url = format!("http://{}/clip.mp3", listener.local_addr().unwrap());
let server = tokio::spawn(async move {
let (mut socket, _) = listener.accept().await.unwrap();
let mut request = Vec::new();
let mut buf = [0_u8; 1024];
loop {
let n = socket.read(&mut buf).await.unwrap();
if n == 0 {
break;
}
request.extend_from_slice(&buf[..n]);
if request.windows(4).any(|window| window == b"\r\n\r\n") {
break;
}
}
socket
.write_all(b"HTTP/1.1 204 No Content\r\nContent-Length: 0\r\nConnection: close\r\n\r\n")
.await
.unwrap();
String::from_utf8(request).unwrap()
});
let response = RecaptchaAudioSolver::new()
.client
.get(&url)
.send()
.await
.expect("reCAPTCHA audio request");
assert_eq!(response.status(), reqwest::StatusCode::NO_CONTENT);
let raw_request = server.await.unwrap();
let facts = default_profile_facts();
assert_eq!(
captured_header(&raw_request, "User-Agent"),
Some(facts.user_agent)
);
assert_eq!(captured_header(&raw_request, "Accept"), Some("*/*"));
assert_eq!(
captured_header(&raw_request, "Accept-Language"),
Some(facts.accept_language)
);
assert_eq!(
captured_header(&raw_request, "Sec-Fetch-Dest"),
Some("audio")
);
assert_eq!(
captured_header(&raw_request, "Sec-Fetch-Mode"),
Some("no-cors")
);
assert_eq!(
captured_header(&raw_request, "Sec-Fetch-Site"),
Some("cross-site")
);
assert!(captured_header(&raw_request, "Accept-Encoding").is_none());
assert!(captured_header(&raw_request, "Upgrade-Insecure-Requests").is_none());
assert!(captured_header(&raw_request, "Sec-Fetch-User").is_none());
}
fn ck(name: &str, value: &str, domain: &str, path: &str, secure: bool) -> CapturedCookie {
CapturedCookie {
name: name.into(),
value: value.into(),
domain: domain.into(),
path: path.into(),
expires: None,
secure,
http_only: false,
same_site: None,
}
}
#[test]
fn recaptcha_audio_cookie_header_replays_only_url_scoped_safe_cookies() {
let url = reqwest::Url::parse("https://www.google.com/recaptcha/api2/payload?p=clip").unwrap();
let mut expired = ck("expired", "old", ".google.com", "/", false);
expired.expires = Some(1);
let cookies = vec![
ck("NID", "abc", ".google.com", "/", false),
ck(
"__Secure-3PSID",
"secure",
".google.com",
"/recaptcha",
true,
),
ck(
"host_only",
"one",
"www.google.com",
"/recaptcha/api2",
false,
),
ck("wrong_host", "skip", ".example.com", "/", false),
ck("wrong_path", "skip", ".google.com", "/accounts", false),
ck("bad;name", "skip", ".google.com", "/", false),
ck("bad_value", "skip;me", ".google.com", "/", false),
expired,
];
let header = recaptcha_audio_cookie_header(&cookies, &url)
.expect("matching cookies should produce a header");
assert_eq!(
header.to_str().unwrap(),
"NID=abc; __Secure-3PSID=secure; host_only=one"
);
}
#[test]
fn solver_with_custom_endpoint() {
let s = RecaptchaAudioSolver::new().with_stt_endpoint("http://stt.test:5000/v1");
assert_eq!(s.stt_endpoint, "http://stt.test:5000/v1");
}
#[test]
fn solver_supports_recaptcha_v2_and_audio_kinds() {
use crate::captcha_detect::DetectedCaptcha;
let s = RecaptchaAudioSolver::new();
assert!(s.supports(&DetectedCaptcha::RecaptchaV2));
assert!(s.supports(&DetectedCaptcha::AudioCaptcha));
assert!(!s.supports(&DetectedCaptcha::HCaptcha));
assert!(!s.supports(&DetectedCaptcha::Turnstile));
assert!(!s.supports(&DetectedCaptcha::None));
}
#[test]
fn selectors_are_stable_strings() {
assert_eq!(AUDIO_BUTTON_SELECTOR, "#recaptcha-audio-button");
assert_eq!(RESPONSE_INPUT_SELECTOR, "#audio-response");
assert_eq!(VERIFY_BUTTON_SELECTOR, "#recaptcha-verify-button");
assert_eq!(TOKEN_INPUT_NAME, "g-recaptcha-response");
assert!(RECAPTCHA_BFRAME_SELECTOR.contains("api2/bframe"));
}
#[test]
fn max_audio_retries_is_bounded_small() {
assert_eq!(MAX_AUDIO_RETRIES, 2);
}