hcaptcha_no_wasm/response.rs
1//! Structure to capture the response from the hcaptcha api
2//!
3//! ## Example
4//!
5//! ```no_run
6//! # use hcaptcha_no_wasm::{Request, Client};
7//! # #[tokio::main]
8//! # async fn main() -> Result<(), hcaptcha_no_wasm::Error> {
9//! # let request = Request::new(
10//! # "0x123456789abcedf0123456789abcdef012345678",
11//! # get_captcha(),
12//! # )?;
13//! # let client = Client::new();
14//! let response = client.verify(request).await?;
15//!
16//! if let Some(timestamp) = response.timestamp() {
17//! println!("Timestamp: {}", timestamp);
18//! };
19//! if let Some(hostname) = response.hostname() {
20//! println!("Timestamp: {}", hostname);
21//! };
22//! if let Some(credit) = response.credit() {
23//! println!("Timestamp: {}", credit);
24//! };
25//! // Only available with an Enterprise subscription to Hcaptcha
26//! # #[cfg(feature = "enterprise")]
27//! if let Some(score) = response.score() {
28//! println!("Score: {}", score);
29//! };
30//! # #[cfg(feature = "enterprise")]
31//! if let Some(score_reason) = response.score_reason() {
32//! println!("Score reasons: {:?}", score_reason);
33//! };
34//!
35//! # Ok(())
36//! # }
37//! # use hcaptcha_no_wasm::Captcha;
38//! # use rand::distributions::Alphanumeric;
39//! # use rand::{thread_rng, Rng};
40//! # use std::iter;
41//! # fn random_response() -> String {
42//! # let mut rng = thread_rng();
43//! # iter::repeat(())
44//! # .map(|()| rng.sample(Alphanumeric))
45//! # .map(char::from)
46//! # .take(100)
47//! # .collect()
48//! # }
49//! # fn get_captcha() -> Captcha {
50//! # Captcha::new(&random_response())
51//! # .unwrap()
52//! # .set_remoteip(&mockd::internet::ipv4_address())
53//! # .unwrap()
54//! # .set_sitekey(&mockd::unique::uuid_v4())
55//! # .unwrap()
56//! # }
57
58//! ```
59use crate::Code;
60use crate::Error;
61use serde::Deserialize;
62use std::collections::HashSet;
63use std::fmt;
64
65type Score = f32;
66
67/// Result from call to verify the client's response
68#[cfg_attr(docsrs, allow(rustdoc::missing_doc_code_examples))]
69#[derive(Debug, Default, Deserialize, Clone)]
70pub struct Response {
71 /// verification status: true or false.
72 ///
73 /// Successful verification may have additional information.
74 /// Unsuccessful verification will return a set of error codes.
75 success: bool,
76 /// timestamp of the captcha (ISO format yyyy-MM-dd'T'HH:mm:ssZZ)
77 challenge_ts: Option<String>, //yyyy-MM-dd'T'HH:mm:ssZZ
78 /// the hostname of the site where the captcha was solved
79 hostname: Option<String>,
80 /// optional: whether the response will be credited
81 credit: Option<bool>,
82 /// optional: any error codes
83 #[serde(rename = "error-codes")]
84 error_codes: Option<HashSet<Code>>,
85 /// `enterprise` feature: a score denoting malicious activity.
86 #[allow(dead_code)]
87 #[cfg_attr(docsrs, doc(cfg(feature = "enterprise")))]
88 score: Option<Score>,
89 /// `enterprise` feature: reason(s) for score. See [BotStop.com] for details
90 ///
91 /// [BotStop.com]: https://BotStop.com
92 #[allow(dead_code)]
93 #[cfg_attr(docsrs, doc(cfg(feature = "enterprise")))]
94 score_reason: Option<HashSet<String>>,
95}
96
97#[cfg_attr(docsrs, allow(rustdoc::missing_doc_code_examples))]
98#[cfg(feature = "enterprise")]
99impl fmt::Display for Response {
100 #[cfg_attr(docsrs, allow(rustdoc::missing_doc_code_examples))]
101 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102 write!(
103 f,
104 r#"
105 Status: {}
106 Timestamp: {}
107 Hostname: {}
108 Credit: {}
109 Error Codes: {}
110 Score: {}
111 Score Reason: {}
112 "#,
113 self.success,
114 match self.timestamp() {
115 Some(v) => v,
116 None => "".to_owned(),
117 },
118 match self.hostname() {
119 Some(v) => v,
120 None => "".to_owned(),
121 },
122 match self.credit() {
123 Some(v) => format!("{v}"),
124 None => "".to_owned(),
125 },
126 match self.error_codes() {
127 Some(v) => format!("{v:?}"),
128 None => "".to_owned(),
129 },
130 match self.score() {
131 Some(v) => format!("{v}"),
132 None => "".to_owned(),
133 },
134 match self.score_reason() {
135 Some(v) => format!("{v:?}"),
136 None => "".to_owned(),
137 },
138 )
139 }
140}
141
142#[cfg(not(feature = "enterprise"))]
143impl fmt::Display for Response {
144 #[cfg_attr(docsrs, allow(rustdoc::missing_doc_code_examples))]
145 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
146 write!(
147 f,
148 r#"
149 Status: {}
150 Timestamp: {}
151 Hostname: {}
152 Credit: {}
153 Error Codes: {}
154 "#,
155 self.success,
156 match self.timestamp() {
157 Some(v) => v,
158 None => "".to_owned(),
159 },
160 match self.hostname() {
161 Some(v) => v,
162 None => "".to_owned(),
163 },
164 match self.credit() {
165 Some(v) => format!("{}", v),
166 None => "".to_owned(),
167 },
168 match self.error_codes() {
169 Some(v) => format!("{:?}", v),
170 None => "".to_owned(),
171 },
172 )
173 }
174}
175
176#[cfg_attr(docsrs, allow(rustdoc::missing_doc_code_examples))]
177impl Response {
178 /// Check success of API call and return Error
179 /// with the error codes if not successful.
180 pub(crate) fn check_error(&self) -> Result<(), Error> {
181 if !self.success() {
182 match &self.error_codes {
183 Some(codes) => Err(Error::Codes(codes.clone())),
184 None => {
185 let mut codes = HashSet::new();
186 codes.insert(Code::Unknown("No error codes returned".to_owned()));
187 Err(Error::Codes(codes))
188 }
189 }
190 } else {
191 Ok(())
192 }
193 }
194
195 /// Get the value of the success field
196 ///
197 /// # Example
198 /// ```no_run
199 /// # use hcaptcha_no_wasm::{Request, Client};
200 /// # #[tokio::main]
201 /// # async fn main() -> Result<(), hcaptcha_no_wasm::Error> {
202 /// # let request = Request::new(
203 /// # "0x123456789abcedf0123456789abcdef012345678",
204 /// # get_captcha(),
205 /// # )?;
206 /// # let client = Client::new();
207 /// let response = client.verify(request).await?;
208 /// println!("Success returns true: {}", response.success());
209 /// # Ok(())
210 /// # }
211 /// # use hcaptcha_no_wasm::Captcha;
212 /// # use rand::distributions::Alphanumeric;
213 /// # use rand::{thread_rng, Rng};
214 /// # use std::iter;
215 /// # fn random_response() -> String {
216 /// # let mut rng = thread_rng();
217 /// # iter::repeat(())
218 /// # .map(|()| rng.sample(Alphanumeric))
219 /// # .map(char::from)
220 /// # .take(100)
221 /// # .collect()
222 /// # }
223 /// # fn get_captcha() -> Captcha {
224 /// # Captcha::new(&random_response())
225 /// # .unwrap()
226 /// # .set_remoteip(&mockd::internet::ipv4_address())
227 /// # .unwrap()
228 /// # .set_sitekey(&mockd::unique::uuid_v4())
229 /// # .unwrap()
230 /// # }
231 #[allow(dead_code)]
232 pub fn success(&self) -> bool {
233 self.success
234 }
235
236 /// Get the value of the hostname field
237 ///
238 /// # Example
239 /// ```no_run
240 /// # use hcaptcha_no_wasm::{Request, Client};
241 /// # #[tokio::main]
242 /// # async fn main() -> Result<(), hcaptcha_no_wasm::Error> {
243 /// # let request = Request::new(
244 /// # "0x123456789abcedf0123456789abcdef012345678",
245 /// # get_captcha(),
246 /// # )?;
247 /// # let client = Client::new();
248 /// let response = client.verify(request).await?;
249 ///
250 /// if let Some(hostname) = response.hostname() {
251 /// println!("Timestamp: {}", hostname);
252 /// };
253 /// # Ok(())
254 /// # }
255 /// # use hcaptcha_no_wasm::Captcha;
256 /// # use rand::distributions::Alphanumeric;
257 /// # use rand::{thread_rng, Rng};
258 /// # use std::iter;
259 /// # fn random_response() -> String {
260 /// # let mut rng = thread_rng();
261 /// # iter::repeat(())
262 /// # .map(|()| rng.sample(Alphanumeric))
263 /// # .map(char::from)
264 /// # .take(100)
265 /// # .collect()
266 /// # }
267 /// # fn get_captcha() -> Captcha {
268 /// # Captcha::new(&random_response())
269 /// # .unwrap()
270 /// # .set_remoteip(&mockd::internet::ipv4_address())
271 /// # .unwrap()
272 /// # .set_sitekey(&mockd::unique::uuid_v4())
273 /// # .unwrap()
274 /// # }
275 #[allow(dead_code)]
276 pub fn hostname(&self) -> Option<String> {
277 self.hostname.clone()
278 }
279
280 /// Get the value of the timestamp field
281 ///
282 /// # Example
283 /// ```no_run
284 /// # use hcaptcha_no_wasm::{Request, Client};
285 /// # #[tokio::main]
286 /// # async fn main() -> Result<(), hcaptcha_no_wasm::Error> {
287 /// # let request = Request::new(
288 /// # "0x123456789abcedf0123456789abcdef012345678",
289 /// # get_captcha(),
290 /// # )?;
291 /// # let client = Client::new();
292 /// let response = client.verify(request).await?;
293 ///
294 /// if let Some(timestamp) = response.timestamp() {
295 /// println!("Timestamp: {}", timestamp);
296 /// };
297 /// # Ok(())
298 /// # }
299 /// # use hcaptcha_no_wasm::Captcha;
300 /// # use rand::distributions::Alphanumeric;
301 /// # use rand::{thread_rng, Rng};
302 /// # use std::iter;
303 /// # fn random_response() -> String {
304 /// # let mut rng = thread_rng();
305 /// # iter::repeat(())
306 /// # .map(|()| rng.sample(Alphanumeric))
307 /// # .map(char::from)
308 /// # .take(100)
309 /// # .collect()
310 /// # }
311 /// # fn get_captcha() -> Captcha {
312 /// # Captcha::new(&random_response())
313 /// # .unwrap()
314 /// # .set_remoteip(&mockd::internet::ipv4_address())
315 /// # .unwrap()
316 /// # .set_sitekey(&mockd::unique::uuid_v4())
317 /// # .unwrap()
318 /// # }
319 #[allow(dead_code)]
320 pub fn timestamp(&self) -> Option<String> {
321 self.challenge_ts.clone()
322 }
323
324 /// Get the value of the credit field
325 ///
326 /// # Example
327 /// ```no_run
328 /// # use hcaptcha_no_wasm::{Request, Client};
329 /// # #[tokio::main]
330 /// # async fn main() -> Result<(), hcaptcha_no_wasm::Error> {
331 /// # let request = Request::new(
332 /// # "0x123456789abcedf0123456789abcdef012345678",
333 /// # get_captcha(),
334 /// # )?;
335 /// # let client = Client::new();
336 /// let response = client.verify(request).await?;
337 ///
338 /// if let Some(credit) = response.credit() {
339 /// println!("Timestamp: {}", credit);
340 /// };
341 ///
342 /// # Ok(())
343 /// # }
344 /// # use hcaptcha_no_wasm::Captcha;
345 /// # use rand::distributions::Alphanumeric;
346 /// # use rand::{thread_rng, Rng};
347 /// # use std::iter;
348 /// # fn random_response() -> String {
349 /// # let mut rng = thread_rng();
350 /// # iter::repeat(())
351 /// # .map(|()| rng.sample(Alphanumeric))
352 /// # .map(char::from)
353 /// # .take(100)
354 /// # .collect()
355 /// # }
356 /// # fn get_captcha() -> Captcha {
357 /// # Captcha::new(&random_response())
358 /// # .unwrap()
359 /// # .set_remoteip(&mockd::internet::ipv4_address())
360 /// # .unwrap()
361 /// # .set_sitekey(&mockd::unique::uuid_v4())
362 /// # .unwrap()
363 /// # }
364 #[allow(dead_code)]
365 pub fn credit(&self) -> Option<bool> {
366 self.credit
367 }
368
369 /// Get the value of the error_codes field
370 ///
371 /// # Example
372 /// ```no_run
373 /// # use hcaptcha_no_wasm::{Request, Client};
374 /// # #[tokio::main]
375 /// # async fn main() -> Result<(), hcaptcha_no_wasm::Error> {
376 /// # let request = Request::new(
377 /// # "0x123456789abcedf0123456789abcdef012345678",
378 /// # get_captcha(),
379 /// # )?;
380 /// # let client = Client::new();
381 /// let response = client.verify(request).await?;
382 ///
383 /// if let Some(error_codes) = response.error_codes() {
384 /// println!("Error Codes: {:?}", error_codes);
385 /// };
386 ///
387 /// # Ok(())
388 /// # }
389 /// # use hcaptcha_no_wasm::Captcha;
390 /// # use rand::distributions::Alphanumeric;
391 /// # use rand::{thread_rng, Rng};
392 /// # use std::iter;
393 /// # fn random_response() -> String {
394 /// # let mut rng = thread_rng();
395 /// # iter::repeat(())
396 /// # .map(|()| rng.sample(Alphanumeric))
397 /// # .map(char::from)
398 /// # .take(100)
399 /// # .collect()
400 /// # }
401 /// # fn get_captcha() -> Captcha {
402 /// # Captcha::new(&random_response())
403 /// # .unwrap()
404 /// # .set_remoteip(&mockd::internet::ipv4_address())
405 /// # .unwrap()
406 /// # .set_sitekey(&mockd::unique::uuid_v4())
407 /// # .unwrap()
408 /// # }
409 #[allow(dead_code)]
410 pub fn error_codes(&self) -> Option<HashSet<Code>> {
411 self.error_codes.clone()
412 }
413
414 /// Get the value of the score field
415 ///
416 /// # Example
417 /// ```no_run
418 /// # use hcaptcha_no_wasm::{Request, Client};
419 /// # #[tokio::main]
420 /// # async fn main() -> Result<(), hcaptcha_no_wasm::Error> {
421 /// # let request = Request::new(
422 /// # "0x123456789abcedf0123456789abcdef012345678",
423 /// # get_captcha(),
424 /// # )?;
425 /// # let client = Client::new();
426 /// let response = client.verify(request).await?;
427 ///
428 /// if let Some(score) = response.score() {
429 /// println!("Score: {}", score);
430 /// };
431 ///
432 /// # Ok(())
433 /// # }
434 /// # use hcaptcha_no_wasm::Captcha;
435 /// # use rand::distributions::Alphanumeric;
436 /// # use rand::{thread_rng, Rng};
437 /// # use std::iter;
438 /// # fn random_response() -> String {
439 /// # let mut rng = thread_rng();
440 /// # iter::repeat(())
441 /// # .map(|()| rng.sample(Alphanumeric))
442 /// # .map(char::from)
443 /// # .take(100)
444 /// # .collect()
445 /// # }
446 /// # fn get_captcha() -> Captcha {
447 /// # Captcha::new(&random_response())
448 /// # .unwrap()
449 /// # .set_remoteip(&mockd::internet::ipv4_address())
450 /// # .unwrap()
451 /// # .set_sitekey(&mockd::unique::uuid_v4())
452 /// # .unwrap()
453 /// # }
454 #[cfg(feature = "enterprise")]
455 #[cfg_attr(docsrs, doc(cfg(feature = "enterprise")))]
456 #[allow(dead_code)]
457 pub fn score(&self) -> Option<Score> {
458 self.score
459 }
460
461 /// Get the value of the score_reason field
462 ///
463 /// # Example
464 /// ```no_run
465 /// # use hcaptcha_no_wasm::{Request, Client};
466 /// # #[tokio::main]
467 /// # async fn main() -> Result<(), hcaptcha_no_wasm::Error> {
468 /// # let request = Request::new(
469 /// # "0x123456789abcedf0123456789abcdef012345678",
470 /// # get_captcha(),
471 /// # )?;
472 /// # let client = Client::new();
473 /// let response = client.verify(request).await?;
474 ///
475 /// if let Some(score_reason) = response.score_reason() {
476 /// println!("Score reasons: {:?}", score_reason);
477 /// };
478 ///
479 /// # Ok(())
480 /// # }
481 /// # use hcaptcha_no_wasm::Captcha;
482 /// # use rand::distributions::Alphanumeric;
483 /// # use rand::{thread_rng, Rng};
484 /// # use std::iter;
485 /// # fn random_response() -> String {
486 /// # let mut rng = thread_rng();
487 /// # iter::repeat(())
488 /// # .map(|()| rng.sample(Alphanumeric))
489 /// # .map(char::from)
490 /// # .take(100)
491 /// # .collect()
492 /// # }
493 /// # fn get_captcha() -> Captcha {
494 /// # Captcha::new(&random_response())
495 /// # .unwrap()
496 /// # .set_remoteip(&mockd::internet::ipv4_address())
497 /// # .unwrap()
498 /// # .set_sitekey(&mockd::unique::uuid_v4())
499 /// # .unwrap()
500 /// # }
501 #[allow(dead_code)]
502 #[cfg(feature = "enterprise")]
503 #[cfg_attr(docsrs, doc(cfg(feature = "enterprise")))]
504 pub fn score_reason(&self) -> Option<HashSet<String>> {
505 self.score_reason.clone()
506 }
507}
508
509#[cfg(test)]
510mod tests {
511 use std::collections::HashSet;
512
513 use crate::{Code, Error, Response};
514 use serde_json::json;
515
516 #[test]
517 fn decoding_test() {
518 use crate::Code::*;
519
520 let response = json!({
521 "success": true,
522 "error-codes": ["missing-input-secret", "foo"],
523 "hostname": "hostname"
524 });
525 let response: Response = serde_json::from_value(response).unwrap();
526
527 assert!(response.success);
528 assert!(response.error_codes.is_some());
529
530 let errors = response.error_codes.unwrap();
531 assert!(errors.len() == 2);
532 assert!(errors.contains(&MissingSecret));
533 assert!(errors.contains(&Unknown("foo".to_string())));
534 }
535
536 fn test_response() -> Response {
537 let response = json!({
538 "success": true,
539 "challenge_ts": "2020-11-11T23:27:00Z",
540 "hostname": "my-host.ie",
541 "credit": false,
542 "error-codes": ["missing-input-secret", "foo"],
543 "score": null,
544 "score_reason": ["first-reason", "second-reason"],
545 });
546 serde_json::from_value(response).unwrap()
547 }
548
549 #[test]
550 fn success_test() {
551 let response = test_response();
552
553 assert!(response.success());
554 }
555
556 #[test]
557 fn timestamp_test() {
558 let response = test_response();
559
560 assert_eq!(
561 response.timestamp(),
562 Some("2020-11-11T23:27:00Z".to_owned())
563 );
564 }
565
566 #[test]
567 fn hostname_test() {
568 let response = test_response();
569
570 assert_eq!(response.hostname(), Some("my-host.ie".to_owned()));
571 }
572
573 #[test]
574 fn credit_test() {
575 let response = test_response();
576
577 assert_eq!(response.credit(), Some(false));
578 }
579
580 #[test]
581 fn error_codes_test() {
582 let response = test_response();
583
584 assert!(response.error_codes().is_some());
585 if let Some(hash_set) = response.error_codes() {
586 assert_eq!(hash_set.len(), 2)
587 }
588 }
589
590 #[cfg(feature = "enterprise")]
591 #[test]
592 fn score_test() {
593 let response = test_response();
594
595 assert!(response.score().is_none());
596 }
597
598 #[cfg(feature = "enterprise")]
599 #[test]
600 fn score_reason_test() {
601 let response = test_response();
602
603 assert!(response.score_reason().is_some());
604 if let Some(hash_set) = response.score_reason() {
605 assert!(!hash_set.is_empty());
606 assert!(hash_set.contains("first-reason"));
607 assert!(hash_set.contains("second-reason"));
608 }
609 }
610
611 #[test]
612 fn test_successful_decoding_with_error_codes() {
613 use crate::Code::*;
614
615 let response = json!({
616 "success": true,
617 "error-codes": ["missing-input-secret", "foo"],
618 "hostname": "hostname"
619 });
620 let response: Response = serde_json::from_value(response).unwrap();
621
622 assert!(response.success);
623 assert!(response.error_codes.is_some());
624
625 let errors = response.error_codes.unwrap();
626 assert!(errors.len() == 2);
627 assert!(errors.contains(&MissingSecret));
628 assert!(errors.contains(&Unknown("foo".to_string())));
629 }
630
631 #[test]
632 fn test_error_codes_handling() {
633 use crate::Code::*;
634
635 let response = json!({
636 "success": true,
637 "error-codes": ["missing-input-secret", "foo"],
638 "hostname": "hostname"
639 });
640 let response: Response = serde_json::from_value(response).unwrap();
641
642 let errors = response.error_codes.unwrap();
643 assert!(errors.contains(&MissingSecret));
644 assert!(errors.contains(&Unknown("foo".to_string())));
645 }
646
647 #[test]
648 fn test_success_field_decoding() {
649 let response = json!({
650 "success": true,
651 "error-codes": ["missing-input-secret", "foo"],
652 "hostname": "hostname"
653 });
654 let response: Response = serde_json::from_value(response).unwrap();
655
656 assert!(response.success);
657 }
658
659 #[cfg(feature = "enterprise")]
660 #[test]
661 fn test_display_format_enterprise() {
662 {
663 let mut codes = HashSet::new();
664 codes.insert(Code::MissingSecret);
665 let mut reasons = HashSet::new();
666 reasons.insert("reason1".to_string());
667
668 let response = Response {
669 success: true,
670 challenge_ts: Some("2023-01-01T00:00:00Z".to_string()),
671 hostname: Some("test.com".to_string()),
672 credit: Some(true),
673 error_codes: Some(codes),
674 score: Some(0.9),
675 score_reason: Some(reasons),
676 };
677
678 let formatted = format!("{}", response);
679 println!("{}", formatted);
680 assert!(formatted.contains("Status: true"));
681 assert!(formatted.contains("Timestamp: 2023-01-01T00:00:00Z"));
682 assert!(formatted.contains("Hostname: test.com"));
683 assert!(formatted.contains("Credit: true"));
684 assert!(formatted.contains(r#"Error Codes: {MissingSecret}"#));
685 assert!(formatted.contains("Score: 0.9"));
686 assert!(formatted.contains(r#"Score Reason: {"reason1"}"#));
687 }
688 }
689
690 #[cfg(not(feature = "enterprise"))]
691 #[test]
692 fn test_display_format_non_enterprise() {
693 {
694 let mut codes = HashSet::new();
695 codes.insert(Code::MissingSecret);
696
697 let response = Response {
698 success: false,
699 challenge_ts: Some("2023-01-01T00:00:00Z".to_string()),
700 hostname: Some("test.com".to_string()),
701 credit: Some(false),
702 error_codes: Some(codes),
703 score: None,
704 score_reason: None,
705 };
706
707 let formatted = format!("{}", response);
708 assert!(formatted.contains("Status: false"));
709 assert!(formatted.contains("Timestamp: 2023-01-01T00:00:00Z"));
710 assert!(formatted.contains("Hostname: test.com"));
711 assert!(formatted.contains("Credit: false"));
712 assert!(formatted.contains(r#"Error Codes: {MissingSecret}"#));
713 }
714 }
715
716 #[test]
717 fn test_display_format_empty_fields() {
718 let response = Response {
719 success: true,
720 challenge_ts: None,
721 hostname: None,
722 credit: None,
723 error_codes: None,
724 score: None,
725 score_reason: None,
726 };
727
728 let formatted = format!("{}", response);
729 assert!(formatted.contains("Status: true"));
730 assert!(formatted.contains("Timestamp: "));
731 assert!(formatted.contains("Hostname: "));
732 assert!(formatted.contains("Credit: "));
733 assert!(formatted.contains("Error Codes: "));
734 }
735
736 #[test]
737 fn test_check_error_success() {
738 let response = Response {
739 success: true,
740 challenge_ts: None,
741 hostname: None,
742 credit: None,
743 error_codes: None,
744 score: None,
745 score_reason: None,
746 };
747 assert!(response.check_error().is_ok());
748 }
749
750 #[test]
751 fn test_check_error_with_codes() {
752 let mut error_codes = HashSet::new();
753 error_codes.insert(Code::MissingResponse);
754 let response = Response {
755 success: false,
756 challenge_ts: None,
757 hostname: None,
758 credit: None,
759 error_codes: Some(error_codes.clone()),
760 score: None,
761 score_reason: None,
762 };
763 match response.check_error() {
764 Err(Error::Codes(codes)) => {
765 assert_eq!(codes, error_codes);
766 }
767 _ => unreachable!(),
768 }
769 }
770
771 #[test]
772 fn test_check_error_without_codes() {
773 let response = Response {
774 success: false,
775 challenge_ts: None,
776 hostname: None,
777 credit: None,
778 error_codes: None,
779 score: None,
780 score_reason: None,
781 };
782
783 match response.check_error() {
784 Err(Error::Codes(codes)) => {
785 assert_eq!(codes.len(), 1);
786 assert!(codes.iter().any(
787 |code| matches!(code, Code::Unknown(msg) if msg == "No error codes returned")
788 ));
789 }
790 _ => unreachable!(),
791 }
792 }
793}