use std::fmt;
use crate::detect::{DetectionReport, Verdict};
use crate::domain::{Availability, DomainName, Tld};
use crate::registry::Endpoint;
use crate::transport::RawResponse;
#[cfg(feature = "parser")]
use crate::parser::WhoisRecord;
#[derive(Debug, Clone)]
pub struct Lookup {
pub queried: DomainName,
pub domain: DomainName,
pub tld: Tld,
pub verdict: Verdict,
pub responses: Vec<RawResponse>,
#[cfg(feature = "parser")]
pub record: Option<WhoisRecord>,
}
impl Lookup {
pub fn availability(&self) -> Availability {
self.verdict.availability
}
pub fn is_available(&self) -> bool {
self.verdict.availability.is_available()
}
pub fn is_registered(&self) -> bool {
self.verdict.availability.is_registered()
}
pub fn primary_response(&self) -> Option<&RawResponse> {
self.responses.first()
}
pub fn final_response(&self) -> Option<&RawResponse> {
self.responses.last()
}
pub fn raw_text(&self) -> String {
if self.responses.len() == 1 {
return self.responses[0].text().to_string();
}
self.responses
.iter()
.map(|response| {
format!(
">>> {} <<<\n{}",
response.endpoint().address(),
response.text()
)
})
.collect::<Vec<_>>()
.join("\n\n")
}
pub fn consulted(&self) -> Vec<Endpoint> {
self.responses
.iter()
.map(|response| response.endpoint().clone())
.collect()
}
pub fn followed_referral(&self) -> bool {
self.responses.len() > 1
}
pub fn was_cached(&self) -> bool {
!self.responses.is_empty() && self.responses.iter().all(RawResponse::is_cached)
}
#[cfg(feature = "parser")]
pub fn record_or_empty(&self) -> WhoisRecord {
self.record.clone().unwrap_or_default()
}
}
impl fmt::Display for Lookup {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} {}", self.domain, self.verdict.availability)
}
}
#[derive(Debug, Clone)]
pub struct Explanation {
pub lookup: Lookup,
pub report: DetectionReport,
}
impl fmt::Display for Explanation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "{} — {}", self.lookup.domain, self.lookup.verdict)?;
writeln!(
f,
"consulted: {}",
self.lookup
.consulted()
.iter()
.map(Endpoint::address)
.collect::<Vec<_>>()
.join(", ")
)?;
write!(f, "{}", self.report)
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::*;
use crate::detect::Confidence;
use crate::transport::ResponseKind;
fn response(host: &str, text: &str) -> RawResponse {
RawResponse::new(
Endpoint::whois(host),
ResponseKind::WhoisText,
text,
Duration::from_millis(5),
)
}
fn lookup(responses: Vec<RawResponse>) -> Lookup {
Lookup {
queried: DomainName::parse("www.example.com").unwrap(),
domain: DomainName::parse("example.com").unwrap(),
tld: Tld::parse("com").unwrap(),
verdict: Verdict {
availability: Availability::Registered,
confidence: Confidence::High,
rule: "registered",
because: "status field".into(),
},
responses,
#[cfg(feature = "parser")]
record: None,
}
}
#[test]
fn exposes_the_verdict_directly() {
let result = lookup(vec![response("whois.example", "record")]);
assert_eq!(result.availability(), Availability::Registered);
assert!(result.is_registered());
assert!(!result.is_available());
assert_eq!(result.to_string(), "example.com registered");
}
#[test]
fn a_single_response_is_returned_verbatim() {
let result = lookup(vec![response("whois.example", "Domain Name: EXAMPLE.COM")]);
assert_eq!(result.raw_text(), "Domain Name: EXAMPLE.COM");
assert!(!result.followed_referral());
}
#[test]
fn several_responses_are_labelled_by_server() {
let result = lookup(vec![
response("whois.verisign-grs.com", "thin record"),
response("whois.registrar.example", "thick record"),
]);
let text = result.raw_text();
assert!(text.contains(">>> whois.verisign-grs.com <<<"), "{text}");
assert!(text.contains(">>> whois.registrar.example <<<"), "{text}");
assert!(text.contains("thin record") && text.contains("thick record"));
assert!(result.followed_referral());
}
#[test]
fn primary_and_final_responses_are_the_ends_of_the_chain() {
let result = lookup(vec![
response("registry.example", "first"),
response("registrar.example", "second"),
]);
assert_eq!(result.primary_response().unwrap().text(), "first");
assert_eq!(result.final_response().unwrap().text(), "second");
assert_eq!(
result
.consulted()
.iter()
.map(Endpoint::address)
.collect::<Vec<_>>(),
["registry.example", "registrar.example"]
);
}
#[test]
fn an_empty_chain_is_handled_without_panicking() {
let result = lookup(Vec::new());
assert!(result.primary_response().is_none());
assert!(result.final_response().is_none());
assert_eq!(result.raw_text(), "");
assert!(!result.was_cached());
assert!(result.consulted().is_empty());
}
#[test]
fn cached_is_only_true_when_every_response_was() {
let fresh = response("a.example", "x");
let cached = response("b.example", "y").mark_cached();
assert!(!lookup(vec![fresh.clone(), cached.clone()]).was_cached());
assert!(lookup(vec![cached]).was_cached());
assert!(!lookup(vec![fresh]).was_cached());
}
#[test]
fn the_queried_name_and_the_registrable_name_are_both_kept() {
let result = lookup(vec![response("a.example", "x")]);
assert_eq!(result.queried.as_ascii(), "www.example.com");
assert_eq!(result.domain.as_ascii(), "example.com");
}
}