Skip to main content

capmonstercloud_client/requests/
recaptcha_v3.rs

1use serde::Serialize;
2
3use super::{require_non_empty, CaptchaRequest};
4use crate::error::{Error, Result};
5use crate::timeouts::GetResultTimeouts;
6
7/// Checks `minScore`: it must be within `0.1..=0.9`.
8pub(crate) fn check_min_score(min_score: Option<f64>) -> Result<()> {
9    match min_score {
10        Some(v) if !(0.1..=0.9).contains(&v) => Err(Error::Validation(format!(
11            "min_score must be between 0.1 and 0.9, got {v}"
12        ))),
13        _ => Ok(()),
14    }
15}
16
17/// reCAPTCHA v3 (`RecaptchaV3TaskProxyless`). Proxies are not supported.
18#[derive(Debug, Clone, Serialize)]
19pub struct RecaptchaV3ProxylessRequest {
20    /// Address of a webpage with Google reCAPTCHA.
21    #[serde(rename = "websiteURL")]
22    pub website_url: String,
23    /// The reCAPTCHA v3 site key (`...api.js?render=THIS_ONE`).
24    #[serde(rename = "websiteKey")]
25    pub website_key: String,
26    /// Minimum score of the token, `0.1..=0.9`.
27    #[serde(rename = "minScore", skip_serializing_if = "Option::is_none")]
28    pub min_score: Option<f64>,
29    /// Widget action value, default `verify`.
30    #[serde(rename = "pageAction", skip_serializing_if = "Option::is_none")]
31    pub page_action: Option<String>,
32    /// Solve as reCAPTCHA v3 Enterprise (same as `RecaptchaV3EnterpriseRequest`).
33    #[serde(rename = "isEnterprise", skip_serializing_if = "Option::is_none")]
34    pub is_enterprise: Option<bool>,
35}
36
37impl RecaptchaV3ProxylessRequest {
38    pub fn new(website_url: impl Into<String>, website_key: impl Into<String>) -> Self {
39        Self {
40            website_url: website_url.into(),
41            website_key: website_key.into(),
42            min_score: None,
43            page_action: None,
44            is_enterprise: None,
45        }
46    }
47
48    /// Minimum score, must be in `0.1..=0.9` (checked by `validate()`).
49    pub fn with_min_score(mut self, value: f64) -> Self {
50        self.min_score = Some(value);
51        self
52    }
53
54    pub fn with_page_action(mut self, value: impl Into<String>) -> Self {
55        self.page_action = Some(value.into());
56        self
57    }
58
59    pub fn with_is_enterprise(mut self, value: bool) -> Self {
60        self.is_enterprise = Some(value);
61        self
62    }
63}
64
65impl CaptchaRequest for RecaptchaV3ProxylessRequest {
66    const TASK_TYPE: &'static str = "RecaptchaV3TaskProxyless";
67
68    fn timeouts(&self) -> GetResultTimeouts {
69        GetResultTimeouts::secs(1.0, 10.0, 3.0, 180.0)
70    }
71
72    fn validate(&self) -> Result<()> {
73        require_non_empty("websiteURL", &self.website_url)?;
74        require_non_empty("websiteKey", &self.website_key)?;
75        check_min_score(self.min_score)
76    }
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82    use serde_json::json;
83
84    #[test]
85    fn minimal_task() {
86        let task = RecaptchaV3ProxylessRequest::new("https://example.com", "KEY")
87            .to_task()
88            .unwrap();
89        assert_eq!(
90            task,
91            json!({"type": "RecaptchaV3TaskProxyless", "websiteURL": "https://example.com", "websiteKey": "KEY"})
92        );
93    }
94
95    #[test]
96    fn full_task() {
97        let task = RecaptchaV3ProxylessRequest::new("https://example.com", "KEY")
98            .with_min_score(0.7)
99            .with_page_action("login")
100            .with_is_enterprise(true)
101            .to_task()
102            .unwrap();
103        assert_eq!(
104            task,
105            json!({"type": "RecaptchaV3TaskProxyless", "websiteURL": "https://example.com",
106                   "websiteKey": "KEY", "minScore": 0.7, "pageAction": "login", "isEnterprise": true})
107        );
108    }
109
110    #[test]
111    fn min_score_bounds() {
112        for bad in [0.0, 0.05, 0.95, 1.0, f64::NAN] {
113            let err = RecaptchaV3ProxylessRequest::new("u", "k")
114                .with_min_score(bad)
115                .to_task()
116                .unwrap_err();
117            assert!(matches!(err, Error::Validation(_)), "{bad}");
118        }
119        for ok in [0.1, 0.5, 0.9] {
120            assert!(RecaptchaV3ProxylessRequest::new("u", "k")
121                .with_min_score(ok)
122                .to_task()
123                .is_ok());
124        }
125    }
126
127    #[test]
128    fn empty_required_strings_are_rejected() {
129        for (url, key, field) in [
130            ("", "KEY", "websiteURL"),
131            ("https://example.com", "", "websiteKey"),
132        ] {
133            let err = RecaptchaV3ProxylessRequest::new(url, key)
134                .to_task()
135                .unwrap_err();
136            assert!(matches!(err, crate::error::Error::Validation(ref m) if m.contains(field)));
137        }
138    }
139}