1use serde::{Deserialize, Serialize};
7use tracing::{debug, instrument};
8
9use crate::dns::{DnsPresence, DnsResolver};
10use crate::error::Result;
11use crate::rdap::{rdap_error_is_404, RdapClient};
12use crate::whois::WhoisClient;
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct AvailabilityResult {
17 pub domain: String,
19 pub available: bool,
21 pub confidence: String,
23 pub method: String,
25 pub details: Option<String>,
27}
28
29impl AvailabilityResult {
30 pub fn verdict(&self) -> &'static str {
34 match (self.available, self.confidence.as_str()) {
35 (true, "high") => "available",
36 (true, "medium") => "likely_available",
37 (false, "high") => "registered",
38 (false, "medium") => "likely_registered",
39 _ => "unknown",
40 }
41 }
42}
43
44pub(crate) enum PriorRdap {
48 Response(Box<crate::rdap::RdapResponse>),
51 Failed(crate::error::SeerError),
53 Missing,
55}
56
57pub(crate) enum PriorWhois {
63 Failed(crate::error::SeerError),
65 Missing,
67}
68
69#[derive(Debug, Clone)]
71pub struct AvailabilityChecker {
72 rdap_client: RdapClient,
73 whois_client: WhoisClient,
74 dns_resolver: DnsResolver,
75}
76
77impl Default for AvailabilityChecker {
78 fn default() -> Self {
79 Self::new()
80 }
81}
82
83impl AvailabilityChecker {
84 pub fn new() -> Self {
85 Self {
86 rdap_client: RdapClient::new(),
87 whois_client: WhoisClient::new(),
88 dns_resolver: DnsResolver::new(),
89 }
90 }
91
92 pub fn from_config(config: &crate::config::SeerConfig) -> Self {
94 Self {
95 rdap_client: RdapClient::new().with_timeout(config.rdap_timeout()),
96 whois_client: WhoisClient::new().with_timeout(config.whois_timeout()),
97 dns_resolver: DnsResolver::new().with_timeout(config.dns_timeout()),
98 }
99 }
100
101 #[instrument(skip(self), fields(domain = %domain))]
103 pub async fn check(&self, domain: &str) -> Result<AvailabilityResult> {
104 let domain = crate::validation::normalize_domain(domain)?;
105 debug!(domain = %domain, "Checking domain availability");
106
107 match self.rdap_client.lookup_domain(&domain).await {
109 Ok(response) => Ok(decide_from_rdap(&domain, response)),
110 Err(rdap_err) => {
111 debug!(error = %rdap_err, "RDAP lookup failed, falling back to WHOIS + DNS");
112 let (whois_result, dns_presence) = tokio::join!(
118 self.whois_client.lookup(&domain),
119 self.dns_resolver.presence(&domain),
120 );
121 Ok(decide_fallback(
122 &domain,
123 &rdap_err,
124 whois_result,
125 dns_presence,
126 ))
127 }
128 }
129 }
130
131 #[instrument(skip(self, prior_rdap, prior_whois), fields(domain = %domain))]
141 pub(crate) async fn check_with_prior(
142 &self,
143 domain: &str,
144 prior_rdap: PriorRdap,
145 prior_whois: PriorWhois,
146 ) -> Result<AvailabilityResult> {
147 let domain = crate::validation::normalize_domain(domain)?;
148 debug!(domain = %domain, "Checking availability (reusing prior protocol outcomes)");
149
150 let rdap = match prior_rdap {
153 PriorRdap::Response(r) => Ok(*r),
154 PriorRdap::Failed(e) => Err(e),
155 PriorRdap::Missing => self.rdap_client.lookup_domain(&domain).await,
156 };
157
158 match rdap {
159 Ok(response) => Ok(decide_from_rdap(&domain, response)),
160 Err(rdap_err) => {
161 let (whois_result, dns_presence) = match prior_whois {
166 PriorWhois::Failed(e) => (Err(e), self.dns_resolver.presence(&domain).await),
167 PriorWhois::Missing => tokio::join!(
168 self.whois_client.lookup(&domain),
169 self.dns_resolver.presence(&domain),
170 ),
171 };
172 Ok(decide_fallback(
173 &domain,
174 &rdap_err,
175 whois_result,
176 dns_presence,
177 ))
178 }
179 }
180 }
181}
182
183fn decide_from_rdap(domain: &str, response: crate::rdap::RdapResponse) -> AvailabilityResult {
187 let statuses: Vec<String> = response.status.clone();
188 let is_redemption = statuses.iter().any(|s| {
189 let norm: String = s
196 .chars()
197 .filter(|c| !c.is_whitespace())
198 .collect::<String>()
199 .to_lowercase();
200 matches!(norm.as_str(), "redemptionperiod" | "pendingdelete")
201 });
202
203 if is_redemption {
204 return AvailabilityResult {
205 domain: domain.to_string(),
206 available: false,
207 confidence: "medium".to_string(),
208 method: "rdap".to_string(),
209 details: Some("Domain is in redemption/pending delete period".to_string()),
210 };
211 }
212
213 AvailabilityResult {
214 domain: domain.to_string(),
215 available: false,
216 confidence: "high".to_string(),
217 method: "rdap".to_string(),
218 details: Some(format!(
219 "Domain is registered (status: {})",
220 statuses.join(", ")
221 )),
222 }
223}
224
225fn decide_fallback(
234 domain: &str,
235 rdap_err: &crate::error::SeerError,
236 whois_result: Result<crate::whois::WhoisResponse>,
237 dns_presence: DnsPresence,
238) -> AvailabilityResult {
239 match whois_result {
240 Ok(whois_response) => {
241 let thin = whois_response.registrar.is_none()
245 && whois_response.creation_date.is_none()
246 && whois_response.expiration_date.is_none();
247
248 if whois_response.is_available() {
249 AvailabilityResult {
250 domain: domain.to_string(),
251 available: true,
252 confidence: "high".to_string(),
253 method: "whois".to_string(),
254 details: Some("WHOIS indicates domain is not registered".to_string()),
255 }
256 } else if !thin {
257 AvailabilityResult {
260 domain: domain.to_string(),
261 available: false,
262 confidence: "high".to_string(),
263 method: "whois".to_string(),
264 details: whois_response
265 .registrar
266 .map(|r| format!("Registered with {}", r)),
267 }
268 } else if rdap_error_is_404(rdap_err) {
269 AvailabilityResult {
272 domain: domain.to_string(),
273 available: true,
274 confidence: "high".to_string(),
275 method: "rdap".to_string(),
276 details: Some("Registry RDAP reports no such domain (HTTP 404)".to_string()),
277 }
278 } else if whois_response.indicates_registry_refusal() {
279 AvailabilityResult {
286 domain: domain.to_string(),
287 available: false,
288 confidence: "none".to_string(),
289 method: "inconclusive".to_string(),
290 details: Some(
291 "Registry refused or throttled the query; availability is inconclusive"
292 .to_string(),
293 ),
294 }
295 } else if dns_presence == DnsPresence::Absent {
296 AvailabilityResult {
299 domain: domain.to_string(),
300 available: true,
301 confidence: "medium".to_string(),
302 method: "dns_nxdomain".to_string(),
303 details: Some(
304 "No registry data available; domain has no DNS presence (NXDOMAIN)"
305 .to_string(),
306 ),
307 }
308 } else {
309 AvailabilityResult {
312 domain: domain.to_string(),
313 available: false,
314 confidence: "high".to_string(),
315 method: "whois".to_string(),
316 details: None,
317 }
318 }
319 }
320 Err(whois_err) => {
321 if rdap_error_is_404(rdap_err) {
325 return AvailabilityResult {
326 domain: domain.to_string(),
327 available: true,
328 confidence: "high".to_string(),
329 method: "rdap".to_string(),
330 details: Some("Registry RDAP reports no such domain (HTTP 404)".to_string()),
331 };
332 }
333 let likely_available = matches!(whois_err, crate::error::SeerError::WhoisError(_)) && {
339 let whois_msg = whois_err.to_string().to_lowercase();
340 whois_msg.contains("no match")
341 || whois_msg.contains("not found")
342 || whois_msg.contains("no data found")
343 || whois_msg.contains("no entries found")
344 };
345
346 if likely_available {
347 AvailabilityResult {
348 domain: domain.to_string(),
349 available: true,
350 confidence: "medium".to_string(),
351 method: "whois_error".to_string(),
352 details: Some("WHOIS server indicates no matching records".to_string()),
353 }
354 } else if dns_presence == DnsPresence::Absent {
355 AvailabilityResult {
358 domain: domain.to_string(),
359 available: true,
360 confidence: "medium".to_string(),
361 method: "dns_nxdomain".to_string(),
362 details: Some(
363 "Registry lookups failed; domain has no DNS presence (NXDOMAIN)"
364 .to_string(),
365 ),
366 }
367 } else if dns_presence == DnsPresence::Present {
368 AvailabilityResult {
378 domain: domain.to_string(),
379 available: false,
380 confidence: "medium".to_string(),
381 method: "dns_present".to_string(),
382 details: Some(
383 "Registry lookups failed, but the apex is delegated in DNS \
384 (NS records present) — the domain is almost certainly registered"
385 .to_string(),
386 ),
387 }
388 } else {
389 AvailabilityResult {
394 domain: domain.to_string(),
395 available: false,
396 confidence: "none".to_string(),
397 method: "inconclusive".to_string(),
398 details: Some(format!(
403 "Could not determine availability. RDAP: {}. WHOIS: {}",
404 rdap_err.sanitized_message(),
405 whois_err.sanitized_message()
406 )),
407 }
408 }
409 }
410 }
411}
412
413#[cfg(test)]
414mod tests {
415 use super::*;
416 use crate::error::SeerError;
417 use crate::rdap::RdapResponse;
418 use crate::whois::WhoisResponse;
419
420 #[test]
421 fn verdict_matrix() {
422 let make = |available, confidence: &str| AvailabilityResult {
423 domain: "example.test".to_string(),
424 available,
425 confidence: confidence.to_string(),
426 method: "whois".to_string(),
427 details: None,
428 };
429 assert_eq!(make(true, "high").verdict(), "available");
430 assert_eq!(make(true, "medium").verdict(), "likely_available");
431 assert_eq!(make(false, "high").verdict(), "registered");
432 assert_eq!(make(false, "medium").verdict(), "likely_registered");
433 assert_eq!(make(false, "none").verdict(), "unknown");
434 assert_eq!(make(true, "low").verdict(), "unknown");
435 }
436
437 #[test]
438 fn test_availability_result_serialization() {
439 let result = AvailabilityResult {
440 domain: "example.com".to_string(),
441 available: false,
442 confidence: "high".to_string(),
443 method: "rdap".to_string(),
444 details: Some("Domain is registered".to_string()),
445 };
446 let json = serde_json::to_string(&result).unwrap();
447 assert!(json.contains("\"available\":false"));
448 assert!(json.contains("\"confidence\":\"high\""));
449 }
450
451 fn whois_with(raw: &str, registrar: Option<&str>) -> WhoisResponse {
463 WhoisResponse {
464 domain: "example.test".to_string(),
465 registrar: registrar.map(str::to_string),
466 registrant: None,
467 organization: None,
468 registrant_email: None,
469 registrant_phone: None,
470 registrant_address: None,
471 registrant_country: None,
472 admin_name: None,
473 admin_organization: None,
474 admin_email: None,
475 admin_phone: None,
476 tech_name: None,
477 tech_organization: None,
478 tech_email: None,
479 tech_phone: None,
480 creation_date: None,
481 expiration_date: None,
482 updated_date: None,
483 nameservers: vec![],
484 status: vec![],
485 dnssec: None,
486 whois_server: "whois.test".to_string(),
487 raw_response: raw.to_string(),
488 }
489 }
490
491 fn rdap_with(statuses: &[&str]) -> RdapResponse {
492 RdapResponse {
493 status: statuses.iter().map(|s| s.to_string()).collect(),
494 ldh_name: Some("example.test".to_string()),
495 ..Default::default()
496 }
497 }
498
499 #[test]
502 fn rdap_success_registered_marks_taken_high_confidence() {
503 let rdap = rdap_with(&["active"]);
504 let r = decide_from_rdap("example.test", rdap);
505 assert!(!r.available, "registered domain must be marked taken");
506 assert_eq!(r.confidence, "high");
507 assert_eq!(r.method, "rdap");
508 assert!(
509 r.details.as_deref().unwrap().contains("active"),
510 "details should include status list"
511 );
512 }
513
514 #[test]
515 fn rdap_success_empty_status_marks_taken_high_confidence() {
516 let rdap = rdap_with(&[]);
519 let r = decide_from_rdap("example.test", rdap);
520 assert!(!r.available);
521 assert_eq!(r.confidence, "high");
522 assert_eq!(r.method, "rdap");
523 }
524
525 #[test]
526 fn rdap_success_redemption_period_marks_taken_medium_confidence() {
527 let rdap = rdap_with(&["redemption period"]);
528 let r = decide_from_rdap("example.test", rdap);
529 assert!(!r.available, "redemption period still means taken");
530 assert_eq!(r.confidence, "medium", "redemption drops confidence");
531 assert_eq!(r.method, "rdap");
532 assert!(r.details.as_deref().unwrap().contains("redemption"));
533 }
534
535 #[test]
536 fn rdap_success_pending_delete_marks_taken_medium_confidence() {
537 let rdap = rdap_with(&["pending delete"]);
538 let r = decide_from_rdap("example.test", rdap);
539 assert!(!r.available);
540 assert_eq!(r.confidence, "medium");
541 assert!(r.details.as_deref().unwrap().contains("redemption"));
542 }
543
544 #[test]
545 fn rdap_status_substring_redemption_not_misclassified() {
546 let rdap = rdap_with(&["clientHold (no redemption requested)"]);
550 let r = decide_from_rdap("example.test", rdap);
551 assert!(!r.available, "still registered");
552 assert_eq!(
553 r.confidence, "high",
554 "verbose status must not be downgraded to redemption/medium"
555 );
556 }
557
558 #[test]
559 fn rdap_status_redemption_detected_case_insensitively() {
560 let rdap = rdap_with(&["Redemption Period"]);
563 let r = decide_from_rdap("example.test", rdap);
564 assert_eq!(
565 r.confidence, "medium",
566 "standard token detected regardless of case"
567 );
568 }
569
570 #[test]
573 fn rdap_fail_whois_says_available_high_confidence() {
574 let whois = whois_with("No match for \"example.test\".\n", None);
577 let rdap_err = SeerError::RdapError("404 not found".to_string());
578 let r = decide_fallback("example.test", &rdap_err, Ok(whois), DnsPresence::Unknown);
579 assert!(r.available, "WHOIS 'no match' must mark available");
580 assert_eq!(r.confidence, "high");
581 assert_eq!(r.method, "whois");
582 }
583
584 #[test]
585 fn rdap_fail_whois_says_registered_high_confidence() {
586 let whois = whois_with("Domain Name: example.test\n", Some("Test Registrar"));
587 let rdap_err = SeerError::RdapError("404 not found".to_string());
588 let r = decide_fallback("example.test", &rdap_err, Ok(whois), DnsPresence::Unknown);
589 assert!(!r.available);
590 assert_eq!(r.confidence, "high");
591 assert_eq!(r.method, "whois");
592 assert!(r.details.as_deref().unwrap().contains("Test Registrar"));
593 }
594
595 #[test]
596 fn rdap_fail_whois_registered_without_registrar_no_detail() {
597 let whois = whois_with("Domain Name: example.test\n", None);
600 let rdap_err = SeerError::RdapError("404".to_string());
601 let r = decide_fallback("example.test", &rdap_err, Ok(whois), DnsPresence::Unknown);
602 assert!(!r.available);
603 assert_eq!(r.confidence, "high");
604 assert!(
605 r.details.is_none(),
606 "no registrar means no details string, got: {:?}",
607 r.details
608 );
609 }
610
611 #[test]
614 fn rdap_fail_whois_error_contains_no_match_marks_available_medium() {
615 let rdap_err = SeerError::RdapError("500".to_string());
616 let whois_err =
617 SeerError::WhoisError("whois server returned 'No match for this domain'".to_string());
618 let r = decide_fallback(
619 "example.test",
620 &rdap_err,
621 Err(whois_err),
622 DnsPresence::Unknown,
623 );
624 assert!(
625 r.available,
626 "whois error containing 'no match' is available"
627 );
628 assert_eq!(r.confidence, "medium");
629 assert_eq!(r.method, "whois_error");
630 }
631
632 #[test]
633 fn rdap_fail_whois_error_not_found_marks_available_medium() {
634 let rdap_err = SeerError::RdapError("500".to_string());
635 let whois_err = SeerError::WhoisError("Domain not found".to_string());
636 let r = decide_fallback(
637 "example.test",
638 &rdap_err,
639 Err(whois_err),
640 DnsPresence::Unknown,
641 );
642 assert!(r.available);
643 assert_eq!(r.confidence, "medium");
644 assert_eq!(r.method, "whois_error");
645 }
646
647 #[test]
648 fn rdap_fail_whois_error_no_data_found_marks_available_medium() {
649 let rdap_err = SeerError::RdapError("no".to_string());
650 let whois_err = SeerError::WhoisError("No Data Found for query".to_string());
651 let r = decide_fallback(
652 "example.test",
653 &rdap_err,
654 Err(whois_err),
655 DnsPresence::Unknown,
656 );
657 assert!(r.available);
658 assert_eq!(r.confidence, "medium");
659 }
660
661 #[test]
662 fn rdap_fail_whois_error_no_entries_marks_available_medium() {
663 let rdap_err = SeerError::RdapError("no".to_string());
664 let whois_err =
665 SeerError::WhoisError("No entries found for the selected source".to_string());
666 let r = decide_fallback(
667 "example.test",
668 &rdap_err,
669 Err(whois_err),
670 DnsPresence::Unknown,
671 );
672 assert!(r.available);
673 assert_eq!(r.confidence, "medium");
674 }
675
676 #[test]
677 fn rdap_fail_whois_timeout_marks_inconclusive_none_confidence() {
678 let rdap_err = SeerError::Timeout("rdap timed out".to_string());
679 let whois_err = SeerError::Timeout("whois timed out".to_string());
680 let r = decide_fallback(
681 "example.test",
682 &rdap_err,
683 Err(whois_err),
684 DnsPresence::Unknown,
685 );
686 assert!(
687 !r.available,
688 "inconclusive means NOT available (fail-safe default)"
689 );
690 assert_eq!(r.confidence, "none");
691 assert_eq!(r.method, "inconclusive");
692 assert!(r.details.as_deref().unwrap().contains("RDAP:"));
693 assert!(r.details.as_deref().unwrap().contains("WHOIS:"));
694 }
695
696 #[test]
697 fn rdap_fail_whois_transport_error_with_phrase_not_available() {
698 let rdap_err = SeerError::RdapError("503 service unavailable".to_string());
704 let whois_err =
705 SeerError::Timeout("no match within deadline querying whois.nic.test".to_string());
706 let r = decide_fallback(
707 "example.test",
708 &rdap_err,
709 Err(whois_err),
710 DnsPresence::Present,
711 );
712 assert!(
713 !r.available,
714 "transport error text must not infer availability"
715 );
716 }
717
718 #[test]
719 fn rdap_fail_whois_connection_error_marks_inconclusive_none_confidence() {
720 let rdap_err = SeerError::RdapError("connection refused".to_string());
721 let whois_err = SeerError::WhoisError(
722 "failed to connect to whois.example: connection refused".to_string(),
723 );
724 let r = decide_fallback(
725 "example.test",
726 &rdap_err,
727 Err(whois_err),
728 DnsPresence::Unknown,
729 );
730 assert!(!r.available);
731 assert_eq!(r.confidence, "none");
732 assert_eq!(r.method, "inconclusive");
733 }
734
735 #[test]
736 fn rdap_fail_whois_error_case_insensitive_not_found() {
737 let rdap_err = SeerError::RdapError("500".to_string());
740 let whois_err = SeerError::WhoisError("NOT FOUND in registry".to_string());
741 let r = decide_fallback(
742 "example.test",
743 &rdap_err,
744 Err(whois_err),
745 DnsPresence::Unknown,
746 );
747 assert!(r.available, "'NOT FOUND' should classify as available");
748 assert_eq!(r.confidence, "medium");
749 }
750
751 #[test]
754 fn rdap_404_with_blocked_whois_marks_available() {
755 let whois = whois_with("Requests of this client are not permitted.\n", None);
760 let rdap_err = SeerError::RdapError("query failed with status 404 Not Found".to_string());
761 let r = decide_fallback("example.ch", &rdap_err, Ok(whois), DnsPresence::Unknown);
762 assert!(
763 r.available,
764 "RDAP 404 must mark available even with blocked WHOIS"
765 );
766 assert_eq!(r.confidence, "high");
767 assert_eq!(r.method, "rdap");
768 }
769
770 #[test]
771 fn rdap_404_with_whois_error_marks_available() {
772 let rdap_err = SeerError::RdapError("query failed with status 404".to_string());
774 let whois_err = SeerError::WhoisError("connection refused".to_string());
775 let r = decide_fallback(
776 "example.test",
777 &rdap_err,
778 Err(whois_err),
779 DnsPresence::Unknown,
780 );
781 assert!(r.available);
782 assert_eq!(r.confidence, "high");
783 assert_eq!(r.method, "rdap");
784 }
785
786 #[test]
787 fn rdap_404_but_whois_has_full_registration_marks_registered() {
788 let mut whois = whois_with("Domain Name: example.test\n", Some("Real Registrar"));
792 whois.creation_date = Some(chrono::Utc::now());
793 whois.nameservers = vec!["ns1.example.net".to_string()];
794 let rdap_err = SeerError::RdapError("query failed with status 404 Not Found".to_string());
795 let r = decide_fallback("example.test", &rdap_err, Ok(whois), DnsPresence::Unknown);
796 assert!(
797 !r.available,
798 "concrete WHOIS registration must win over RDAP 404"
799 );
800 assert_eq!(r.confidence, "high");
801 assert_eq!(r.method, "whois");
802 }
803
804 #[test]
807 fn thin_whois_non404_dns_absent_marks_likely_available() {
808 let whois = whois_with(
812 "Conditions of use for the whois service via port 43\n",
813 None,
814 );
815 let rdap_err = SeerError::RdapBootstrapError("no RDAP server for example.es".to_string());
816 let r = decide_fallback("example.es", &rdap_err, Ok(whois), DnsPresence::Absent);
817 assert!(r.available);
818 assert_eq!(r.confidence, "medium");
819 assert_eq!(r.method, "dns_nxdomain");
820 }
821
822 #[test]
823 fn thin_whois_non404_dns_present_stays_unavailable() {
824 let whois = whois_with(
827 "Conditions of use for the whois service via port 43\n",
828 None,
829 );
830 let rdap_err = SeerError::RdapBootstrapError("no RDAP server for example.es".to_string());
831 let r = decide_fallback("example.es", &rdap_err, Ok(whois), DnsPresence::Present);
832 assert!(!r.available);
833 assert_ne!(r.method, "dns_nxdomain");
834 }
835
836 #[test]
837 fn thin_whois_non404_dns_unknown_stays_unavailable_failsafe() {
838 let whois = whois_with(
842 "Conditions of use for the whois service via port 43\n",
843 None,
844 );
845 let rdap_err = SeerError::RdapBootstrapError("no RDAP server".to_string());
846 let r = decide_fallback("example.es", &rdap_err, Ok(whois), DnsPresence::Unknown);
847 assert!(!r.available);
848 }
849
850 #[test]
851 fn both_legs_failed_dns_absent_marks_likely_available() {
852 let rdap_err = SeerError::Timeout("rdap timed out".to_string());
855 let whois_err = SeerError::WhoisError("connection refused".to_string());
856 let r = decide_fallback(
857 "example.test",
858 &rdap_err,
859 Err(whois_err),
860 DnsPresence::Absent,
861 );
862 assert!(r.available);
863 assert_eq!(r.confidence, "medium");
864 assert_eq!(r.method, "dns_nxdomain");
865 }
866
867 #[test]
868 fn both_legs_failed_dns_present_marks_likely_registered() {
869 let rdap_err = SeerError::RdapBootstrapError("no RDAP server for example.ru".to_string());
875 let whois_err = SeerError::Timeout(
876 "Operation failed after 3 attempts: Operation timed out".to_string(),
877 );
878 let r = decide_fallback(
879 "example.ru",
880 &rdap_err,
881 Err(whois_err),
882 DnsPresence::Present,
883 );
884 assert!(
885 !r.available,
886 "a delegated apex must never read as available"
887 );
888 assert_eq!(r.confidence, "medium");
889 assert_eq!(r.method, "dns_present");
890 assert_eq!(r.verdict(), "likely_registered");
891 assert!(
892 r.details.as_deref().unwrap().contains("delegated"),
893 "details should explain the DNS-delegation evidence"
894 );
895 }
896
897 #[test]
900 fn rdap_fail_thin_whois_refusal_marks_inconclusive() {
901 let whois = whois_with(
906 "Access rate limited; no data found for unauthenticated clients\n",
907 None,
908 );
909 let rdap_err = SeerError::RdapError("503 service unavailable".to_string());
910 let r = decide_fallback("example.test", &rdap_err, Ok(whois), DnsPresence::Absent);
911 assert!(
912 !r.available,
913 "refusal/throttle must not be called available"
914 );
915 assert_eq!(r.confidence, "none");
916 assert_eq!(r.method, "inconclusive");
917 }
918
919 #[test]
920 fn rdap_fail_thin_whois_not_available_for_registration_marks_inconclusive() {
921 let whois = whois_with(
925 "This domain name is not available for registration.\n",
926 None,
927 );
928 let rdap_err = SeerError::RdapBootstrapError("no RDAP server".to_string());
929 let r = decide_fallback("example.test", &rdap_err, Ok(whois), DnsPresence::Absent);
930 assert!(!r.available);
931 assert_eq!(r.confidence, "none");
932 assert_eq!(r.method, "inconclusive");
933 }
934
935 #[test]
936 fn rdap_404_with_refusal_whois_still_marks_available() {
937 let whois = whois_with("Access rate limited; please try again later.\n", None);
940 let rdap_err = SeerError::RdapError("query failed with status 404 Not Found".to_string());
941 let r = decide_fallback("example.test", &rdap_err, Ok(whois), DnsPresence::Unknown);
942 assert!(r.available, "RDAP 404 is authoritative over a refusal body");
943 assert_eq!(r.confidence, "high");
944 assert_eq!(r.method, "rdap");
945 }
946
947 #[tokio::test]
950 async fn check_with_prior_reuses_rdap_response_without_network() {
951 let checker = AvailabilityChecker::new();
956 let rdap = rdap_with(&["active"]);
957 let r = checker
958 .check_with_prior(
959 "example.test",
960 PriorRdap::Response(Box::new(rdap)),
961 PriorWhois::Missing,
962 )
963 .await
964 .expect("prior-response path must not error");
965 assert!(!r.available, "an existing RDAP object means registered");
966 assert_eq!(r.confidence, "high");
967 assert_eq!(r.method, "rdap");
968 }
969
970 #[tokio::test]
971 async fn check_with_prior_reuses_redemption_status_verdict() {
972 let checker = AvailabilityChecker::new();
975 let rdap = rdap_with(&["redemption period"]);
976 let r = checker
977 .check_with_prior(
978 "example.test",
979 PriorRdap::Response(Box::new(rdap)),
980 PriorWhois::Missing,
981 )
982 .await
983 .expect("prior-response path must not error");
984 assert!(!r.available);
985 assert_eq!(r.confidence, "medium");
986 assert_eq!(r.method, "rdap");
987 }
988}