use super::*;
#[test]
fn miss_returns_none() {
let c = TokenCache::new();
assert!(c
.get("example.com", &CaptchaType::CloudflareTurnstile)
.is_none());
}
#[test]
fn hit_returns_token_within_ttl() {
let c = TokenCache::new();
c.put(
"example.com",
&CaptchaType::CloudflareTurnstile,
"tok123".into(),
"BehavioralCaptchaSolver",
);
let got = c.get_token("example.com", &CaptchaType::CloudflareTurnstile);
assert_eq!(got.as_deref(), Some("tok123"));
}
#[test]
fn expired_entry_returns_none() {
let c = TokenCache::with_ttl(Duration::from_millis(1));
c.put(
"example.com",
&CaptchaType::HCaptcha,
"tok".into(),
"VlmCaptchaSolver",
);
std::thread::sleep(Duration::from_millis(5));
assert!(c.get("example.com", &CaptchaType::HCaptcha).is_none());
}
#[test]
fn put_replaces_existing_entry() {
let c = TokenCache::new();
c.put(
"x.test",
&CaptchaType::HCaptcha,
"old".into(),
"VlmCaptchaSolver",
);
c.put(
"x.test",
&CaptchaType::HCaptcha,
"new".into(),
"VlmCaptchaSolver",
);
assert_eq!(
c.get_token("x.test", &CaptchaType::HCaptcha).as_deref(),
Some("new")
);
assert_eq!(c.len(), 1, "replace must not grow the map");
}
#[test]
fn invalidate_drops_only_the_targeted_entry() {
let c = TokenCache::new();
c.put("a.test", &CaptchaType::HCaptcha, "ta".into(), "Vlm");
c.put("b.test", &CaptchaType::HCaptcha, "tb".into(), "Vlm");
c.invalidate("a.test", &CaptchaType::HCaptcha);
assert!(c.get("a.test", &CaptchaType::HCaptcha).is_none());
assert!(c.get("b.test", &CaptchaType::HCaptcha).is_some());
}
#[test]
fn different_captcha_types_on_same_domain_are_isolated() {
let c = TokenCache::new();
c.put("d.test", &CaptchaType::HCaptcha, "h".into(), "Vlm");
c.put(
"d.test",
&CaptchaType::CloudflareTurnstile,
"t".into(),
"Beh",
);
assert_eq!(
c.get_token("d.test", &CaptchaType::HCaptcha).as_deref(),
Some("h")
);
assert_eq!(
c.get_token("d.test", &CaptchaType::CloudflareTurnstile)
.as_deref(),
Some("t")
);
}
#[test]
fn put_with_ttl_overrides_default() {
let c = TokenCache::with_ttl(Duration::from_secs(10));
c.put_with_ttl(
"x.test",
&CaptchaType::HCaptcha,
"tok".into(),
"Vlm",
Duration::from_millis(1),
);
std::thread::sleep(Duration::from_millis(5));
assert!(
c.get("x.test", &CaptchaType::HCaptcha).is_none(),
"explicit short TTL wins"
);
}
#[test]
fn put_with_cookies_round_trips_on_hit() {
let c = TokenCache::new();
let cookie = CapturedCookie {
name: "datadome".into(),
value: "abc".into(),
domain: "x.test".into(),
path: "/".into(),
expires: None,
secure: true,
http_only: true,
same_site: None,
};
c.put_full(
"x.test",
&CaptchaType::CloudflareTurnstile,
"tok".into(),
"Behavioral",
Duration::from_secs(60),
vec![cookie.clone()],
);
let entry = c.get("x.test", &CaptchaType::CloudflareTurnstile).unwrap();
assert_eq!(entry.cookies().len(), 1);
assert_eq!(entry.cookies()[0].name, "datadome");
}
#[test]
fn put_without_cookies_returns_empty_cookies_on_hit() {
let c = TokenCache::new();
c.put("x.test", &CaptchaType::HCaptcha, "t".into(), "Vlm");
let entry = c.get("x.test", &CaptchaType::HCaptcha).unwrap();
assert!(entry.cookies().is_empty());
}
#[test]
fn stats_initialise_to_zero() {
let c = TokenCache::new();
let s = c.stats();
assert_eq!(s.hits, 0);
assert_eq!(s.misses, 0);
assert_eq!(s.expired_misses, 0);
assert_eq!(s.puts, 0);
assert_eq!(s.invalidations, 0);
assert!(s.hit_rate().is_none(), "no lookups → no rate");
}
#[test]
fn stats_count_each_event_class_separately() {
let c = TokenCache::new();
c.put("a.test", &CaptchaType::HCaptcha, "t".into(), "Vlm");
assert_eq!(c.stats().puts, 1);
let _ = c.get("a.test", &CaptchaType::HCaptcha);
let _ = c.get("a.test", &CaptchaType::HCaptcha);
assert_eq!(c.stats().hits, 2);
let _ = c.get("never.test", &CaptchaType::HCaptcha);
assert_eq!(c.stats().misses, 1);
c.invalidate("a.test", &CaptchaType::HCaptcha);
assert_eq!(c.stats().invalidations, 1);
let s = c.stats();
let r = s.hit_rate().unwrap();
assert!(
(r - 2.0 / 3.0).abs() < 0.001,
"expected 0.667 hit rate; got {r}"
);
}
#[test]
fn stats_track_expired_misses_distinct_from_misses() {
let c = TokenCache::with_ttl(Duration::from_millis(1));
c.put("x.test", &CaptchaType::HCaptcha, "t".into(), "Vlm");
std::thread::sleep(Duration::from_millis(5));
let _ = c.get("x.test", &CaptchaType::HCaptcha);
let s = c.stats();
assert_eq!(s.misses, 0, "key existed → not a plain miss");
assert_eq!(s.expired_misses, 1, "key existed but expired");
}
#[test]
fn invalidate_on_missing_key_does_not_count() {
let c = TokenCache::new();
c.invalidate("missing.test", &CaptchaType::HCaptcha);
assert_eq!(c.stats().invalidations, 0);
}
#[test]
fn reset_stats_zeroes_counters_but_keeps_entries() {
let c = TokenCache::new();
c.put("a.test", &CaptchaType::HCaptcha, "t".into(), "Vlm");
let _ = c.get("a.test", &CaptchaType::HCaptcha);
assert_eq!(c.stats().hits, 1);
c.reset_stats();
let s = c.stats();
assert_eq!(s.hits, 0);
assert_eq!(s.puts, 0);
assert!(c.get("a.test", &CaptchaType::HCaptcha).is_some());
}
#[test]
fn clear_removes_all_entries() {
let c = TokenCache::new();
c.put("a.test", &CaptchaType::HCaptcha, "ta".into(), "Vlm");
c.put("b.test", &CaptchaType::HCaptcha, "tb".into(), "Vlm");
assert_eq!(c.len(), 2);
c.clear();
assert!(c.is_empty());
}