Skip to main content

eoka_protocol/
turnstile.rs

1use serde::{Deserialize, Serialize};
2
3pub const TURNSTILE_DEFAULT_TIMEOUT_MS: u64 = 30_000;
4pub const TURNSTILE_MAX_TIMEOUT_MS: u64 = 120_000;
5
6#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
7#[serde(default, deny_unknown_fields)]
8pub struct CaptchaTurnstileArgs {
9    #[serde(deserialize_with = "deserialize_timeout")]
10    #[schemars(range(min = 1, max = TURNSTILE_MAX_TIMEOUT_MS))]
11    pub timeout_ms: u64,
12}
13
14impl Default for CaptchaTurnstileArgs {
15    fn default() -> Self {
16        Self {
17            timeout_ms: TURNSTILE_DEFAULT_TIMEOUT_MS,
18        }
19    }
20}
21
22fn deserialize_timeout<'de, D: serde::Deserializer<'de>>(d: D) -> Result<u64, D::Error> {
23    let value = u64::deserialize(d)?;
24    if !(1..=TURNSTILE_MAX_TIMEOUT_MS).contains(&value) {
25        return Err(serde::de::Error::custom(
26            "timeout_ms must be between 1 and 120000",
27        ));
28    }
29    Ok(value)
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
33pub struct CaptchaTurnstileResult {
34    pub tab_id: String,
35    pub clicks: u32,
36    pub elapsed_ms: u64,
37}
38
39#[cfg(test)]
40mod tests {
41    use super::*;
42
43    #[test]
44    fn timeout_defaults_and_is_bounded() {
45        let args: CaptchaTurnstileArgs = serde_json::from_value(serde_json::json!({})).unwrap();
46        assert_eq!(args.timeout_ms, TURNSTILE_DEFAULT_TIMEOUT_MS);
47        for timeout in [0, TURNSTILE_MAX_TIMEOUT_MS + 1] {
48            assert!(serde_json::from_value::<CaptchaTurnstileArgs>(
49                serde_json::json!({ "timeout_ms": timeout })
50            )
51            .is_err());
52        }
53    }
54}