capmonstercloud_client/requests/
recaptcha_v3.rs1use serde::Serialize;
2
3use super::{require_non_empty, CaptchaRequest};
4use crate::error::{Error, Result};
5use crate::timeouts::GetResultTimeouts;
6
7pub(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#[derive(Debug, Clone, Serialize)]
19pub struct RecaptchaV3ProxylessRequest {
20 #[serde(rename = "websiteURL")]
22 pub website_url: String,
23 #[serde(rename = "websiteKey")]
25 pub website_key: String,
26 #[serde(rename = "minScore", skip_serializing_if = "Option::is_none")]
28 pub min_score: Option<f64>,
29 #[serde(rename = "pageAction", skip_serializing_if = "Option::is_none")]
31 pub page_action: Option<String>,
32 #[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 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}