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!(
117 self.whois_client.lookup(&domain),
118 self.dns_resolver.presence(&domain),
119 );
120 Ok(decide_fallback(
121 &domain,
122 &rdap_err,
123 whois_result,
124 dns_presence,
125 ))
126 }
127 }
128 }
129
130 #[instrument(skip(self, prior_rdap, prior_whois), fields(domain = %domain))]
140 pub(crate) async fn check_with_prior(
141 &self,
142 domain: &str,
143 prior_rdap: PriorRdap,
144 prior_whois: PriorWhois,
145 ) -> Result<AvailabilityResult> {
146 let domain = crate::validation::normalize_domain(domain)?;
147 debug!(domain = %domain, "Checking availability (reusing prior protocol outcomes)");
148
149 let rdap = match prior_rdap {
152 PriorRdap::Response(r) => Ok(*r),
153 PriorRdap::Failed(e) => Err(e),
154 PriorRdap::Missing => self.rdap_client.lookup_domain(&domain).await,
155 };
156
157 match rdap {
158 Ok(response) => Ok(decide_from_rdap(&domain, response)),
159 Err(rdap_err) => {
160 let (whois_result, dns_presence) = match prior_whois {
165 PriorWhois::Failed(e) => (Err(e), self.dns_resolver.presence(&domain).await),
166 PriorWhois::Missing => tokio::join!(
167 self.whois_client.lookup(&domain),
168 self.dns_resolver.presence(&domain),
169 ),
170 };
171 Ok(decide_fallback(
172 &domain,
173 &rdap_err,
174 whois_result,
175 dns_presence,
176 ))
177 }
178 }
179 }
180}
181
182fn decide_from_rdap(domain: &str, response: crate::rdap::RdapResponse) -> AvailabilityResult {
186 let statuses: Vec<String> = response.status.clone();
187 let is_redemption = statuses.iter().any(|s| {
188 let norm: String = s
195 .chars()
196 .filter(|c| !c.is_whitespace())
197 .collect::<String>()
198 .to_lowercase();
199 matches!(norm.as_str(), "redemptionperiod" | "pendingdelete")
200 });
201
202 if is_redemption {
203 return AvailabilityResult {
204 domain: domain.to_string(),
205 available: false,
206 confidence: "medium".to_string(),
207 method: "rdap".to_string(),
208 details: Some("Domain is in redemption/pending delete period".to_string()),
209 };
210 }
211
212 AvailabilityResult {
213 domain: domain.to_string(),
214 available: false,
215 confidence: "high".to_string(),
216 method: "rdap".to_string(),
217 details: Some(format!(
218 "Domain is registered (status: {})",
219 statuses.join(", ")
220 )),
221 }
222}
223
224fn decide_fallback(
231 domain: &str,
232 rdap_err: &crate::error::SeerError,
233 whois_result: Result<crate::whois::WhoisResponse>,
234 dns_presence: DnsPresence,
235) -> AvailabilityResult {
236 match whois_result {
237 Ok(whois_response) => {
238 let thin = whois_response.registrar.is_none()
242 && whois_response.creation_date.is_none()
243 && whois_response.expiration_date.is_none();
244
245 if whois_response.is_available() {
246 AvailabilityResult {
247 domain: domain.to_string(),
248 available: true,
249 confidence: "high".to_string(),
250 method: "whois".to_string(),
251 details: Some("WHOIS indicates domain is not registered".to_string()),
252 }
253 } else if !thin {
254 AvailabilityResult {
257 domain: domain.to_string(),
258 available: false,
259 confidence: "high".to_string(),
260 method: "whois".to_string(),
261 details: whois_response
262 .registrar
263 .map(|r| format!("Registered with {}", r)),
264 }
265 } else if rdap_error_is_404(rdap_err) {
266 AvailabilityResult {
269 domain: domain.to_string(),
270 available: true,
271 confidence: "high".to_string(),
272 method: "rdap".to_string(),
273 details: Some("Registry RDAP reports no such domain (HTTP 404)".to_string()),
274 }
275 } else if whois_response.indicates_registry_refusal() {
276 AvailabilityResult {
283 domain: domain.to_string(),
284 available: false,
285 confidence: "none".to_string(),
286 method: "inconclusive".to_string(),
287 details: Some(
288 "Registry refused or throttled the query; availability is inconclusive"
289 .to_string(),
290 ),
291 }
292 } else if dns_presence == DnsPresence::Absent {
293 AvailabilityResult {
296 domain: domain.to_string(),
297 available: true,
298 confidence: "medium".to_string(),
299 method: "dns_nxdomain".to_string(),
300 details: Some(
301 "No registry data available; domain has no DNS presence (NXDOMAIN)"
302 .to_string(),
303 ),
304 }
305 } else {
306 AvailabilityResult {
309 domain: domain.to_string(),
310 available: false,
311 confidence: "high".to_string(),
312 method: "whois".to_string(),
313 details: None,
314 }
315 }
316 }
317 Err(whois_err) => {
318 if rdap_error_is_404(rdap_err) {
322 return AvailabilityResult {
323 domain: domain.to_string(),
324 available: true,
325 confidence: "high".to_string(),
326 method: "rdap".to_string(),
327 details: Some("Registry RDAP reports no such domain (HTTP 404)".to_string()),
328 };
329 }
330 let likely_available = matches!(whois_err, crate::error::SeerError::WhoisError(_)) && {
336 let whois_msg = whois_err.to_string().to_lowercase();
337 whois_msg.contains("no match")
338 || whois_msg.contains("not found")
339 || whois_msg.contains("no data found")
340 || whois_msg.contains("no entries found")
341 };
342
343 if likely_available {
344 AvailabilityResult {
345 domain: domain.to_string(),
346 available: true,
347 confidence: "medium".to_string(),
348 method: "whois_error".to_string(),
349 details: Some("WHOIS server indicates no matching records".to_string()),
350 }
351 } else if dns_presence == DnsPresence::Absent {
352 AvailabilityResult {
355 domain: domain.to_string(),
356 available: true,
357 confidence: "medium".to_string(),
358 method: "dns_nxdomain".to_string(),
359 details: Some(
360 "Registry lookups failed; domain has no DNS presence (NXDOMAIN)"
361 .to_string(),
362 ),
363 }
364 } else {
365 AvailabilityResult {
371 domain: domain.to_string(),
372 available: false,
373 confidence: "none".to_string(),
374 method: "inconclusive".to_string(),
375 details: Some(format!(
380 "Could not determine availability. RDAP: {}. WHOIS: {}",
381 rdap_err.sanitized_message(),
382 whois_err.sanitized_message()
383 )),
384 }
385 }
386 }
387 }
388}
389
390#[cfg(test)]
391mod tests {
392 use super::*;
393 use crate::error::SeerError;
394 use crate::rdap::RdapResponse;
395 use crate::whois::WhoisResponse;
396
397 #[test]
398 fn verdict_matrix() {
399 let make = |available, confidence: &str| AvailabilityResult {
400 domain: "example.test".to_string(),
401 available,
402 confidence: confidence.to_string(),
403 method: "whois".to_string(),
404 details: None,
405 };
406 assert_eq!(make(true, "high").verdict(), "available");
407 assert_eq!(make(true, "medium").verdict(), "likely_available");
408 assert_eq!(make(false, "high").verdict(), "registered");
409 assert_eq!(make(false, "medium").verdict(), "likely_registered");
410 assert_eq!(make(false, "none").verdict(), "unknown");
411 assert_eq!(make(true, "low").verdict(), "unknown");
412 }
413
414 #[test]
415 fn test_availability_result_serialization() {
416 let result = AvailabilityResult {
417 domain: "example.com".to_string(),
418 available: false,
419 confidence: "high".to_string(),
420 method: "rdap".to_string(),
421 details: Some("Domain is registered".to_string()),
422 };
423 let json = serde_json::to_string(&result).unwrap();
424 assert!(json.contains("\"available\":false"));
425 assert!(json.contains("\"confidence\":\"high\""));
426 }
427
428 fn whois_with(raw: &str, registrar: Option<&str>) -> WhoisResponse {
440 WhoisResponse {
441 domain: "example.test".to_string(),
442 registrar: registrar.map(str::to_string),
443 registrant: None,
444 organization: None,
445 registrant_email: None,
446 registrant_phone: None,
447 registrant_address: None,
448 registrant_country: None,
449 admin_name: None,
450 admin_organization: None,
451 admin_email: None,
452 admin_phone: None,
453 tech_name: None,
454 tech_organization: None,
455 tech_email: None,
456 tech_phone: None,
457 creation_date: None,
458 expiration_date: None,
459 updated_date: None,
460 nameservers: vec![],
461 status: vec![],
462 dnssec: None,
463 whois_server: "whois.test".to_string(),
464 raw_response: raw.to_string(),
465 }
466 }
467
468 fn rdap_with(statuses: &[&str]) -> RdapResponse {
469 RdapResponse {
470 status: statuses.iter().map(|s| s.to_string()).collect(),
471 ldh_name: Some("example.test".to_string()),
472 ..Default::default()
473 }
474 }
475
476 #[test]
479 fn rdap_success_registered_marks_taken_high_confidence() {
480 let rdap = rdap_with(&["active"]);
481 let r = decide_from_rdap("example.test", rdap);
482 assert!(!r.available, "registered domain must be marked taken");
483 assert_eq!(r.confidence, "high");
484 assert_eq!(r.method, "rdap");
485 assert!(
486 r.details.as_deref().unwrap().contains("active"),
487 "details should include status list"
488 );
489 }
490
491 #[test]
492 fn rdap_success_empty_status_marks_taken_high_confidence() {
493 let rdap = rdap_with(&[]);
496 let r = decide_from_rdap("example.test", rdap);
497 assert!(!r.available);
498 assert_eq!(r.confidence, "high");
499 assert_eq!(r.method, "rdap");
500 }
501
502 #[test]
503 fn rdap_success_redemption_period_marks_taken_medium_confidence() {
504 let rdap = rdap_with(&["redemption period"]);
505 let r = decide_from_rdap("example.test", rdap);
506 assert!(!r.available, "redemption period still means taken");
507 assert_eq!(r.confidence, "medium", "redemption drops confidence");
508 assert_eq!(r.method, "rdap");
509 assert!(r.details.as_deref().unwrap().contains("redemption"));
510 }
511
512 #[test]
513 fn rdap_success_pending_delete_marks_taken_medium_confidence() {
514 let rdap = rdap_with(&["pending delete"]);
515 let r = decide_from_rdap("example.test", rdap);
516 assert!(!r.available);
517 assert_eq!(r.confidence, "medium");
518 assert!(r.details.as_deref().unwrap().contains("redemption"));
519 }
520
521 #[test]
522 fn rdap_status_substring_redemption_not_misclassified() {
523 let rdap = rdap_with(&["clientHold (no redemption requested)"]);
527 let r = decide_from_rdap("example.test", rdap);
528 assert!(!r.available, "still registered");
529 assert_eq!(
530 r.confidence, "high",
531 "verbose status must not be downgraded to redemption/medium"
532 );
533 }
534
535 #[test]
536 fn rdap_status_redemption_detected_case_insensitively() {
537 let rdap = rdap_with(&["Redemption Period"]);
540 let r = decide_from_rdap("example.test", rdap);
541 assert_eq!(
542 r.confidence, "medium",
543 "standard token detected regardless of case"
544 );
545 }
546
547 #[test]
550 fn rdap_fail_whois_says_available_high_confidence() {
551 let whois = whois_with("No match for \"example.test\".\n", None);
554 let rdap_err = SeerError::RdapError("404 not found".to_string());
555 let r = decide_fallback("example.test", &rdap_err, Ok(whois), DnsPresence::Unknown);
556 assert!(r.available, "WHOIS 'no match' must mark available");
557 assert_eq!(r.confidence, "high");
558 assert_eq!(r.method, "whois");
559 }
560
561 #[test]
562 fn rdap_fail_whois_says_registered_high_confidence() {
563 let whois = whois_with("Domain Name: example.test\n", Some("Test Registrar"));
564 let rdap_err = SeerError::RdapError("404 not found".to_string());
565 let r = decide_fallback("example.test", &rdap_err, Ok(whois), DnsPresence::Unknown);
566 assert!(!r.available);
567 assert_eq!(r.confidence, "high");
568 assert_eq!(r.method, "whois");
569 assert!(r.details.as_deref().unwrap().contains("Test Registrar"));
570 }
571
572 #[test]
573 fn rdap_fail_whois_registered_without_registrar_no_detail() {
574 let whois = whois_with("Domain Name: example.test\n", None);
577 let rdap_err = SeerError::RdapError("404".to_string());
578 let r = decide_fallback("example.test", &rdap_err, Ok(whois), DnsPresence::Unknown);
579 assert!(!r.available);
580 assert_eq!(r.confidence, "high");
581 assert!(
582 r.details.is_none(),
583 "no registrar means no details string, got: {:?}",
584 r.details
585 );
586 }
587
588 #[test]
591 fn rdap_fail_whois_error_contains_no_match_marks_available_medium() {
592 let rdap_err = SeerError::RdapError("500".to_string());
593 let whois_err =
594 SeerError::WhoisError("whois server returned 'No match for this domain'".to_string());
595 let r = decide_fallback(
596 "example.test",
597 &rdap_err,
598 Err(whois_err),
599 DnsPresence::Unknown,
600 );
601 assert!(
602 r.available,
603 "whois error containing 'no match' is available"
604 );
605 assert_eq!(r.confidence, "medium");
606 assert_eq!(r.method, "whois_error");
607 }
608
609 #[test]
610 fn rdap_fail_whois_error_not_found_marks_available_medium() {
611 let rdap_err = SeerError::RdapError("500".to_string());
612 let whois_err = SeerError::WhoisError("Domain not found".to_string());
613 let r = decide_fallback(
614 "example.test",
615 &rdap_err,
616 Err(whois_err),
617 DnsPresence::Unknown,
618 );
619 assert!(r.available);
620 assert_eq!(r.confidence, "medium");
621 assert_eq!(r.method, "whois_error");
622 }
623
624 #[test]
625 fn rdap_fail_whois_error_no_data_found_marks_available_medium() {
626 let rdap_err = SeerError::RdapError("no".to_string());
627 let whois_err = SeerError::WhoisError("No Data Found for query".to_string());
628 let r = decide_fallback(
629 "example.test",
630 &rdap_err,
631 Err(whois_err),
632 DnsPresence::Unknown,
633 );
634 assert!(r.available);
635 assert_eq!(r.confidence, "medium");
636 }
637
638 #[test]
639 fn rdap_fail_whois_error_no_entries_marks_available_medium() {
640 let rdap_err = SeerError::RdapError("no".to_string());
641 let whois_err =
642 SeerError::WhoisError("No entries found for the selected source".to_string());
643 let r = decide_fallback(
644 "example.test",
645 &rdap_err,
646 Err(whois_err),
647 DnsPresence::Unknown,
648 );
649 assert!(r.available);
650 assert_eq!(r.confidence, "medium");
651 }
652
653 #[test]
654 fn rdap_fail_whois_timeout_marks_inconclusive_none_confidence() {
655 let rdap_err = SeerError::Timeout("rdap timed out".to_string());
656 let whois_err = SeerError::Timeout("whois timed out".to_string());
657 let r = decide_fallback(
658 "example.test",
659 &rdap_err,
660 Err(whois_err),
661 DnsPresence::Unknown,
662 );
663 assert!(
664 !r.available,
665 "inconclusive means NOT available (fail-safe default)"
666 );
667 assert_eq!(r.confidence, "none");
668 assert_eq!(r.method, "inconclusive");
669 assert!(r.details.as_deref().unwrap().contains("RDAP:"));
670 assert!(r.details.as_deref().unwrap().contains("WHOIS:"));
671 }
672
673 #[test]
674 fn rdap_fail_whois_transport_error_with_phrase_not_available() {
675 let rdap_err = SeerError::RdapError("503 service unavailable".to_string());
681 let whois_err =
682 SeerError::Timeout("no match within deadline querying whois.nic.test".to_string());
683 let r = decide_fallback(
684 "example.test",
685 &rdap_err,
686 Err(whois_err),
687 DnsPresence::Present,
688 );
689 assert!(
690 !r.available,
691 "transport error text must not infer availability"
692 );
693 }
694
695 #[test]
696 fn rdap_fail_whois_connection_error_marks_inconclusive_none_confidence() {
697 let rdap_err = SeerError::RdapError("connection refused".to_string());
698 let whois_err = SeerError::WhoisError(
699 "failed to connect to whois.example: connection refused".to_string(),
700 );
701 let r = decide_fallback(
702 "example.test",
703 &rdap_err,
704 Err(whois_err),
705 DnsPresence::Unknown,
706 );
707 assert!(!r.available);
708 assert_eq!(r.confidence, "none");
709 assert_eq!(r.method, "inconclusive");
710 }
711
712 #[test]
713 fn rdap_fail_whois_error_case_insensitive_not_found() {
714 let rdap_err = SeerError::RdapError("500".to_string());
717 let whois_err = SeerError::WhoisError("NOT FOUND in registry".to_string());
718 let r = decide_fallback(
719 "example.test",
720 &rdap_err,
721 Err(whois_err),
722 DnsPresence::Unknown,
723 );
724 assert!(r.available, "'NOT FOUND' should classify as available");
725 assert_eq!(r.confidence, "medium");
726 }
727
728 #[test]
731 fn rdap_404_with_blocked_whois_marks_available() {
732 let whois = whois_with("Requests of this client are not permitted.\n", None);
737 let rdap_err = SeerError::RdapError("query failed with status 404 Not Found".to_string());
738 let r = decide_fallback("example.ch", &rdap_err, Ok(whois), DnsPresence::Unknown);
739 assert!(
740 r.available,
741 "RDAP 404 must mark available even with blocked WHOIS"
742 );
743 assert_eq!(r.confidence, "high");
744 assert_eq!(r.method, "rdap");
745 }
746
747 #[test]
748 fn rdap_404_with_whois_error_marks_available() {
749 let rdap_err = SeerError::RdapError("query failed with status 404".to_string());
751 let whois_err = SeerError::WhoisError("connection refused".to_string());
752 let r = decide_fallback(
753 "example.test",
754 &rdap_err,
755 Err(whois_err),
756 DnsPresence::Unknown,
757 );
758 assert!(r.available);
759 assert_eq!(r.confidence, "high");
760 assert_eq!(r.method, "rdap");
761 }
762
763 #[test]
764 fn rdap_404_but_whois_has_full_registration_marks_registered() {
765 let mut whois = whois_with("Domain Name: example.test\n", Some("Real Registrar"));
769 whois.creation_date = Some(chrono::Utc::now());
770 whois.nameservers = vec!["ns1.example.net".to_string()];
771 let rdap_err = SeerError::RdapError("query failed with status 404 Not Found".to_string());
772 let r = decide_fallback("example.test", &rdap_err, Ok(whois), DnsPresence::Unknown);
773 assert!(
774 !r.available,
775 "concrete WHOIS registration must win over RDAP 404"
776 );
777 assert_eq!(r.confidence, "high");
778 assert_eq!(r.method, "whois");
779 }
780
781 #[test]
784 fn thin_whois_non404_dns_absent_marks_likely_available() {
785 let whois = whois_with(
789 "Conditions of use for the whois service via port 43\n",
790 None,
791 );
792 let rdap_err = SeerError::RdapBootstrapError("no RDAP server for example.es".to_string());
793 let r = decide_fallback("example.es", &rdap_err, Ok(whois), DnsPresence::Absent);
794 assert!(r.available);
795 assert_eq!(r.confidence, "medium");
796 assert_eq!(r.method, "dns_nxdomain");
797 }
798
799 #[test]
800 fn thin_whois_non404_dns_present_stays_unavailable() {
801 let whois = whois_with(
804 "Conditions of use for the whois service via port 43\n",
805 None,
806 );
807 let rdap_err = SeerError::RdapBootstrapError("no RDAP server for example.es".to_string());
808 let r = decide_fallback("example.es", &rdap_err, Ok(whois), DnsPresence::Present);
809 assert!(!r.available);
810 assert_ne!(r.method, "dns_nxdomain");
811 }
812
813 #[test]
814 fn thin_whois_non404_dns_unknown_stays_unavailable_failsafe() {
815 let whois = whois_with(
819 "Conditions of use for the whois service via port 43\n",
820 None,
821 );
822 let rdap_err = SeerError::RdapBootstrapError("no RDAP server".to_string());
823 let r = decide_fallback("example.es", &rdap_err, Ok(whois), DnsPresence::Unknown);
824 assert!(!r.available);
825 }
826
827 #[test]
828 fn both_legs_failed_dns_absent_marks_likely_available() {
829 let rdap_err = SeerError::Timeout("rdap timed out".to_string());
832 let whois_err = SeerError::WhoisError("connection refused".to_string());
833 let r = decide_fallback(
834 "example.test",
835 &rdap_err,
836 Err(whois_err),
837 DnsPresence::Absent,
838 );
839 assert!(r.available);
840 assert_eq!(r.confidence, "medium");
841 assert_eq!(r.method, "dns_nxdomain");
842 }
843
844 #[test]
847 fn rdap_fail_thin_whois_refusal_marks_inconclusive() {
848 let whois = whois_with(
853 "Access rate limited; no data found for unauthenticated clients\n",
854 None,
855 );
856 let rdap_err = SeerError::RdapError("503 service unavailable".to_string());
857 let r = decide_fallback("example.test", &rdap_err, Ok(whois), DnsPresence::Absent);
858 assert!(
859 !r.available,
860 "refusal/throttle must not be called available"
861 );
862 assert_eq!(r.confidence, "none");
863 assert_eq!(r.method, "inconclusive");
864 }
865
866 #[test]
867 fn rdap_fail_thin_whois_not_available_for_registration_marks_inconclusive() {
868 let whois = whois_with(
872 "This domain name is not available for registration.\n",
873 None,
874 );
875 let rdap_err = SeerError::RdapBootstrapError("no RDAP server".to_string());
876 let r = decide_fallback("example.test", &rdap_err, Ok(whois), DnsPresence::Absent);
877 assert!(!r.available);
878 assert_eq!(r.confidence, "none");
879 assert_eq!(r.method, "inconclusive");
880 }
881
882 #[test]
883 fn rdap_404_with_refusal_whois_still_marks_available() {
884 let whois = whois_with("Access rate limited; please try again later.\n", None);
887 let rdap_err = SeerError::RdapError("query failed with status 404 Not Found".to_string());
888 let r = decide_fallback("example.test", &rdap_err, Ok(whois), DnsPresence::Unknown);
889 assert!(r.available, "RDAP 404 is authoritative over a refusal body");
890 assert_eq!(r.confidence, "high");
891 assert_eq!(r.method, "rdap");
892 }
893
894 #[tokio::test]
897 async fn check_with_prior_reuses_rdap_response_without_network() {
898 let checker = AvailabilityChecker::new();
903 let rdap = rdap_with(&["active"]);
904 let r = checker
905 .check_with_prior(
906 "example.test",
907 PriorRdap::Response(Box::new(rdap)),
908 PriorWhois::Missing,
909 )
910 .await
911 .expect("prior-response path must not error");
912 assert!(!r.available, "an existing RDAP object means registered");
913 assert_eq!(r.confidence, "high");
914 assert_eq!(r.method, "rdap");
915 }
916
917 #[tokio::test]
918 async fn check_with_prior_reuses_redemption_status_verdict() {
919 let checker = AvailabilityChecker::new();
922 let rdap = rdap_with(&["redemption period"]);
923 let r = checker
924 .check_with_prior(
925 "example.test",
926 PriorRdap::Response(Box::new(rdap)),
927 PriorWhois::Missing,
928 )
929 .await
930 .expect("prior-response path must not error");
931 assert!(!r.available);
932 assert_eq!(r.confidence, "medium");
933 assert_eq!(r.method, "rdap");
934 }
935}