use std::collections::HashSet;
use chrono::{DateTime, FixedOffset, Utc};
use once_cell::sync::Lazy;
use regex::Regex;
use serde::{Deserialize, Serialize};
static REGISTRAR_PATTERNS: Lazy<Vec<Regex>> = Lazy::new(|| {
vec![
Regex::new(r"(?i)Registrar:\s*(.+)").expect("Invalid regex for Registrar"),
Regex::new(r"(?i)Registrar Name:\s*(.+)").expect("Invalid regex for Registrar Name"),
Regex::new(r"(?i)Sponsoring Registrar:\s*(.+)")
.expect("Invalid regex for Sponsoring Registrar"),
]
});
static REGISTRANT_PATTERNS: Lazy<Vec<Regex>> = Lazy::new(|| {
vec![
Regex::new(r"(?i)Registrant Name:\s*(.+)").expect("Invalid regex for Registrant Name"),
Regex::new(r"(?i)Registrant:\s*(.+)").expect("Invalid regex for Registrant"),
]
});
static ORGANIZATION_PATTERNS: Lazy<Vec<Regex>> = Lazy::new(|| {
vec![
Regex::new(r"(?i)Registrant Organization:\s*(.+)")
.expect("Invalid regex for Registrant Organization"),
Regex::new(r"(?i)Organization:\s*(.+)").expect("Invalid regex for Organization"),
Regex::new(r"(?i)org-name:\s*(.+)").expect("Invalid regex for org-name"),
Regex::new(r"(?i)Org Name:\s*(.+)").expect("Invalid regex for Org Name"),
]
});
static CREATION_DATE_PATTERNS: Lazy<Vec<Regex>> = Lazy::new(|| {
vec![
Regex::new(r"(?i)Creation Date:\s*(.+)").expect("Invalid regex for Creation Date"),
Regex::new(r"(?i)Created Date:\s*(.+)").expect("Invalid regex for Created Date"),
Regex::new(r"(?i)Created On:\s*(.+)").expect("Invalid regex for Created On"),
Regex::new(r"(?i)Created:\s*(.+)").expect("Invalid regex for Created"),
Regex::new(r"(?i)Registration Date:\s*(.+)").expect("Invalid regex for Registration Date"),
Regex::new(r"(?i)Domain Registration Date:\s*(.+)")
.expect("Invalid regex for Domain Registration Date"),
]
});
static EXPIRATION_DATE_PATTERNS: Lazy<Vec<Regex>> = Lazy::new(|| {
vec![
Regex::new(r"(?i)(?:Registry )?Expir(?:y|ation) Date:\s*(.+)")
.expect("Invalid regex for Expiry/Expiration Date"),
Regex::new(r"(?i)Expiration Date:\s*(.+)").expect("Invalid regex for Expiration Date"),
Regex::new(r"(?i)Expires On:\s*(.+)").expect("Invalid regex for Expires On"),
Regex::new(r"(?i)Expires:\s*(.+)").expect("Invalid regex for Expires"),
Regex::new(r"(?i)Expiry Date:\s*(.+)").expect("Invalid regex for Expiry Date"),
Regex::new(r"(?i)paid-till:\s*(.+)").expect("Invalid regex for paid-till"),
]
});
static UPDATED_DATE_PATTERNS: Lazy<Vec<Regex>> = Lazy::new(|| {
vec![
Regex::new(r"(?i)Updated Date:\s*(.+)").expect("Invalid regex for Updated Date"),
Regex::new(r"(?i)Last Updated On:\s*(.+)").expect("Invalid regex for Last Updated On"),
Regex::new(r"(?i)Last Modified:\s*(.+)").expect("Invalid regex for Last Modified"),
Regex::new(r"(?i)Last Update:\s*(.+)").expect("Invalid regex for Last Update"),
Regex::new(r"(?i)Modified:\s*(.+)").expect("Invalid regex for Modified"),
]
});
static DNSSEC_PATTERNS: Lazy<Vec<Regex>> = Lazy::new(|| {
vec![
Regex::new(r"(?i)DNSSEC:\s*(.+)").expect("Invalid regex for DNSSEC"),
Regex::new(r"(?i)DNSSEC Status:\s*(.+)").expect("Invalid regex for DNSSEC Status"),
]
});
static NAMESERVER_PATTERNS: Lazy<Vec<Regex>> = Lazy::new(|| {
vec![
Regex::new(r"(?i)Name Server:\s*(.+)").expect("Invalid regex for Name Server"),
Regex::new(r"(?i)Nameserver:\s*(.+)").expect("Invalid regex for Nameserver"),
Regex::new(r"(?i)nserver:\s*(.+)").expect("Invalid regex for nserver"),
Regex::new(r"(?im)^NS:\s+(.+)$").expect("Invalid regex for NS"),
]
});
static REGISTRANT_EMAIL_PATTERNS: Lazy<Vec<Regex>> = Lazy::new(|| {
vec![
Regex::new(r"(?i)Registrant Email:\s*(.+)").expect("Invalid regex for Registrant Email"),
Regex::new(r"(?i)Registrant E-mail:\s*(.+)").expect("Invalid regex for Registrant E-mail"),
]
});
static REGISTRANT_PHONE_PATTERNS: Lazy<Vec<Regex>> = Lazy::new(|| {
vec![
Regex::new(r"(?i)Registrant Phone:\s*(.+)").expect("Invalid regex for Registrant Phone"),
Regex::new(r"(?i)Registrant Tel:\s*(.+)").expect("Invalid regex for Registrant Tel"),
]
});
static REGISTRANT_ADDRESS_PATTERNS: Lazy<Vec<Regex>> = Lazy::new(|| {
vec![
Regex::new(r"(?i)Registrant Street:\s*(.+)").expect("Invalid regex for Registrant Street"),
Regex::new(r"(?i)Registrant Address:\s*(.+)")
.expect("Invalid regex for Registrant Address"),
]
});
static REGISTRANT_COUNTRY_PATTERNS: Lazy<Vec<Regex>> = Lazy::new(|| {
vec![Regex::new(r"(?i)Registrant Country:\s*(.+)")
.expect("Invalid regex for Registrant Country")]
});
static ADMIN_NAME_PATTERNS: Lazy<Vec<Regex>> = Lazy::new(|| {
vec![
Regex::new(r"(?i)Admin Name:\s*(.+)").expect("Invalid regex for Admin Name"),
Regex::new(r"(?i)Administrative Contact Name:\s*(.+)")
.expect("Invalid regex for Administrative Contact Name"),
]
});
static ADMIN_ORG_PATTERNS: Lazy<Vec<Regex>> = Lazy::new(|| {
vec![Regex::new(r"(?i)Admin Organization:\s*(.+)")
.expect("Invalid regex for Admin Organization")]
});
static ADMIN_EMAIL_PATTERNS: Lazy<Vec<Regex>> = Lazy::new(|| {
vec![
Regex::new(r"(?i)Admin Email:\s*(.+)").expect("Invalid regex for Admin Email"),
Regex::new(r"(?i)Admin E-mail:\s*(.+)").expect("Invalid regex for Admin E-mail"),
]
});
static ADMIN_PHONE_PATTERNS: Lazy<Vec<Regex>> = Lazy::new(|| {
vec![
Regex::new(r"(?i)Admin Phone:\s*(.+)").expect("Invalid regex for Admin Phone"),
Regex::new(r"(?i)Admin Tel:\s*(.+)").expect("Invalid regex for Admin Tel"),
]
});
static TECH_NAME_PATTERNS: Lazy<Vec<Regex>> = Lazy::new(|| {
vec![
Regex::new(r"(?i)Tech Name:\s*(.+)").expect("Invalid regex for Tech Name"),
Regex::new(r"(?i)Technical Contact Name:\s*(.+)")
.expect("Invalid regex for Technical Contact Name"),
]
});
static TECH_ORG_PATTERNS: Lazy<Vec<Regex>> = Lazy::new(|| {
vec![Regex::new(r"(?i)Tech Organization:\s*(.+)").expect("Invalid regex for Tech Organization")]
});
static TECH_EMAIL_PATTERNS: Lazy<Vec<Regex>> = Lazy::new(|| {
vec![
Regex::new(r"(?i)Tech Email:\s*(.+)").expect("Invalid regex for Tech Email"),
Regex::new(r"(?i)Tech E-mail:\s*(.+)").expect("Invalid regex for Tech E-mail"),
]
});
static TECH_PHONE_PATTERNS: Lazy<Vec<Regex>> = Lazy::new(|| {
vec![
Regex::new(r"(?i)Tech Phone:\s*(.+)").expect("Invalid regex for Tech Phone"),
Regex::new(r"(?i)Tech Tel:\s*(.+)").expect("Invalid regex for Tech Tel"),
]
});
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WhoisResponse {
pub domain: String,
pub registrar: Option<String>,
pub registrant: Option<String>,
pub organization: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub registrant_email: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub registrant_phone: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub registrant_address: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub registrant_country: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub admin_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub admin_organization: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub admin_email: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub admin_phone: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tech_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tech_organization: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tech_email: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tech_phone: Option<String>,
pub creation_date: Option<DateTime<Utc>>,
pub expiration_date: Option<DateTime<Utc>>,
pub updated_date: Option<DateTime<Utc>>,
pub nameservers: Vec<String>,
pub status: Vec<String>,
pub dnssec: Option<String>,
pub whois_server: String,
#[serde(skip_serializing)]
pub raw_response: String,
}
impl WhoisResponse {
pub fn parse(domain: &str, whois_server: &str, raw: &str) -> Self {
super::parsers::PARSER_REGISTRY.parse(domain, whois_server, raw)
}
pub fn parse_internal(domain: &str, whois_server: &str, raw: &str) -> Self {
let registrar = extract_field_with_patterns(raw, ®ISTRAR_PATTERNS);
let registrant = extract_field_with_patterns(raw, ®ISTRANT_PATTERNS);
let organization = extract_field_with_patterns(raw, &ORGANIZATION_PATTERNS);
let registrant_email = extract_field_with_patterns(raw, ®ISTRANT_EMAIL_PATTERNS);
let registrant_phone = extract_field_with_patterns(raw, ®ISTRANT_PHONE_PATTERNS);
let registrant_address = extract_field_with_patterns(raw, ®ISTRANT_ADDRESS_PATTERNS);
let registrant_country = extract_field_with_patterns(raw, ®ISTRANT_COUNTRY_PATTERNS);
let admin_name = extract_field_with_patterns(raw, &ADMIN_NAME_PATTERNS);
let admin_organization = extract_field_with_patterns(raw, &ADMIN_ORG_PATTERNS);
let admin_email = extract_field_with_patterns(raw, &ADMIN_EMAIL_PATTERNS);
let admin_phone = extract_field_with_patterns(raw, &ADMIN_PHONE_PATTERNS);
let tech_name = extract_field_with_patterns(raw, &TECH_NAME_PATTERNS);
let tech_organization = extract_field_with_patterns(raw, &TECH_ORG_PATTERNS);
let tech_email = extract_field_with_patterns(raw, &TECH_EMAIL_PATTERNS);
let tech_phone = extract_field_with_patterns(raw, &TECH_PHONE_PATTERNS);
let creation_date = extract_date_with_patterns(raw, &CREATION_DATE_PATTERNS);
let expiration_date = extract_date_with_patterns(raw, &EXPIRATION_DATE_PATTERNS);
let updated_date = extract_date_with_patterns(raw, &UPDATED_DATE_PATTERNS);
let nameservers = extract_nameservers(raw);
let status = extract_status_top_level(raw);
let dnssec = extract_field_with_patterns(raw, &DNSSEC_PATTERNS);
WhoisResponse {
domain: domain.to_string(),
registrar,
registrant,
organization,
registrant_email,
registrant_phone,
registrant_address,
registrant_country,
admin_name,
admin_organization,
admin_email,
admin_phone,
tech_name,
tech_organization,
tech_email,
tech_phone,
creation_date,
expiration_date,
updated_date,
nameservers,
status,
dnssec,
whois_server: whois_server.to_string(),
raw_response: raw.to_string(),
}
}
pub fn has_core_data(&self) -> bool {
let has_dates_or_status = self.creation_date.is_some()
|| self.expiration_date.is_some()
|| !self.status.is_empty();
self.registrar.is_some() && has_dates_or_status && !self.nameservers.is_empty()
}
pub fn registry_unavailable(&self) -> bool {
if self.registrar.is_some()
|| self.creation_date.is_some()
|| self.expiration_date.is_some()
|| !self.nameservers.is_empty()
|| !self.status.is_empty()
{
return false;
}
const SERVICE_ERROR_SENTINELS: &[&str] = &[
"tld is not supported",
"this tld is not",
"malformed request",
"invalid query",
"no whois server is known",
"this server does not",
];
let lower = self.raw_response.to_lowercase();
lower.lines().any(|line| {
let t = line.trim_start();
SERVICE_ERROR_SENTINELS.iter().any(|s| t.starts_with(s))
})
}
pub fn is_available(&self) -> bool {
if self.registrar.is_some()
|| self.creation_date.is_some()
|| self.expiration_date.is_some()
|| !self.nameservers.is_empty()
{
return false;
}
for line in self.raw_response.lines() {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with('%') {
continue;
}
let lower = trimmed.to_lowercase();
let words: Vec<&str> = lower.split_whitespace().collect();
if words.len() > MAX_STATUS_LINE_WORDS {
continue;
}
let normalized = words.join(" ");
if AVAILABILITY_PATTERNS.iter().any(|p| normalized.contains(p)) {
return true;
}
}
false
}
pub fn indicates_not_found(&self) -> bool {
let lower = self.raw_response.to_lowercase();
lower.lines().any(|line| {
let t = line.trim_start();
NOT_FOUND_PATTERNS.iter().any(|p| t.starts_with(p))
})
}
}
const MAX_STATUS_LINE_WORDS: usize = 10;
const AVAILABILITY_PATTERNS: &[&str] = &[
"no match for",
"no match",
"not found",
"no data found",
"no entries found",
"domain not found",
"available for registration",
"not registered",
"not been registered",
"status: available",
"status: free",
"no object found",
"does not exist",
"nothing found", "no found", "no record found", "no information was found", "object not found", "not find matchingrecord", ];
const NOT_FOUND_PATTERNS: &[&str] = &[
"no match for",
"domain not found",
"no data found",
"queried object does not exist",
"object does not exist",
"not found:",
"status: free",
"domain is not registered",
];
fn extract_field_with_patterns(text: &str, patterns: &[Regex]) -> Option<String> {
for re in patterns {
if let Some(caps) = re.captures(text) {
if let Some(m) = caps.get(1) {
let value = m.as_str().trim();
if value.is_empty() {
continue;
}
let lower = value.to_lowercase();
let is_redacted = lower.contains("redacted")
|| lower.contains("data protected")
|| lower.contains("privacy")
|| lower.contains("not disclosed")
|| lower.contains("withheld")
|| lower == "n/a"
|| lower == "none";
if !is_redacted {
return Some(value.to_string());
}
}
}
}
None
}
fn extract_date_with_patterns(text: &str, patterns: &[Regex]) -> Option<DateTime<Utc>> {
let date_str = extract_field_with_patterns(text, patterns)?;
parse_date(&date_str)
}
pub(crate) fn parse_date(date_str: &str) -> Option<DateTime<Utc>> {
let cleaned = date_str
.trim()
.replace(" UTC", "Z")
.replace(" (UTC)", "")
.replace(" +0000", "Z");
if let Ok(dt) = DateTime::parse_from_rfc3339(&cleaned) {
return Some(dt.with_timezone(&Utc));
}
if let Ok(dt) = DateTime::<FixedOffset>::parse_from_str(&cleaned, "%Y-%m-%dT%H:%M:%S%z") {
return Some(dt.with_timezone(&Utc));
}
if let Ok(dt) = DateTime::<FixedOffset>::parse_from_str(&cleaned, "%Y-%m-%d %H:%M:%S%z") {
return Some(dt.with_timezone(&Utc));
}
let naive_formats = [
"%Y-%m-%dT%H:%M:%SZ",
"%Y-%m-%dT%H:%M:%S%.fZ",
"%Y-%m-%d %H:%M:%S",
"%d-%b-%Y %H:%M:%S",
"%d-%b-%Y %H:%M:%S%.f",
"%d-%b-%Y %H:%M:%SZ",
"%d-%b-%Y %H:%M:%S UTC",
"%Y-%m-%d",
"%d-%b-%Y",
"%d-%B-%Y",
"%Y.%m.%d",
"%Y/%m/%d",
"%d.%m.%Y",
"%d/%m/%Y",
"%m.%d.%Y",
"%m/%d/%Y",
"%b %d %Y",
];
for fmt in &naive_formats {
if let Ok(dt) = chrono::NaiveDateTime::parse_from_str(&cleaned, fmt) {
return Some(dt.and_utc());
}
if let Ok(d) = chrono::NaiveDate::parse_from_str(&cleaned, fmt) {
return Some(d.and_hms_opt(0, 0, 0)?.and_utc());
}
}
if let Ok(dt) = cleaned.parse::<DateTime<Utc>>() {
return Some(dt);
}
None
}
const MAX_NAMESERVERS: usize = 32;
fn extract_nameservers(text: &str) -> Vec<String> {
let mut seen = HashSet::new();
let mut nameservers = Vec::new();
for re in NAMESERVER_PATTERNS.iter() {
for caps in re.captures_iter(text) {
if nameservers.len() >= MAX_NAMESERVERS {
return nameservers;
}
if let Some(m) = caps.get(1) {
let raw = m.as_str().trim();
let ns = raw.split_whitespace().next().unwrap_or(raw).to_lowercase();
if !ns.is_empty() && seen.insert(ns.clone()) {
nameservers.push(ns);
}
}
}
}
nameservers
}
const MAX_STATUSES: usize = 32;
fn extract_status_top_level(raw: &str) -> Vec<String> {
let mut seen = HashSet::new();
let mut statuses = Vec::new();
for line in raw.lines() {
if statuses.len() >= MAX_STATUSES {
break;
}
let trimmed = line.trim_start();
if trimmed.starts_with('[') && trimmed.contains(']') {
break;
}
if trimmed.is_empty() || trimmed.starts_with('%') || trimmed.starts_with('#') {
continue;
}
let lower = trimmed.to_lowercase();
let value_opt = if lower.starts_with("domain status:") {
Some(&trimmed["domain status:".len()..])
} else if lower.starts_with("status:") {
Some(&trimmed["status:".len()..])
} else if lower.starts_with("state:") {
Some(&trimmed["state:".len()..])
} else {
None
};
if let Some(rest) = value_opt {
let raw_val = rest.trim();
if let Some(first) = raw_val.split_whitespace().next() {
if !first.is_empty() && seen.insert(first.to_string()) {
statuses.push(first.to_string());
}
}
}
}
statuses
}
#[cfg(test)]
mod tests {
use super::*;
fn make_response(raw: &str) -> WhoisResponse {
WhoisResponse::parse_internal("example.jp", "whois.jprs.jp", raw)
}
#[test]
fn is_available_jprs_style_with_notice_preamble() {
let raw = "\
Notice: JPRS database is provided for information purposes only.
Notice: Use of this service is subject to the JPRS terms.
Notice: For more info see https://jprs.jp/
Notice: Copyright (C) JPRS.
No match!!
";
assert!(make_response(raw).is_available());
}
#[test]
fn is_available_nic_br_style() {
let raw = "\
% Copyright (c) Nic.br
% The use of the data below is only permitted as described in
% full by the terms of use at https://registro.br/termo/en.html,
% being prohibited its distribution, commercialization or
% reproduction, in particular, to use it for advertising or
% any similar purpose.
No match for domain exemplo.br.
";
assert!(make_response(raw).is_available());
}
#[test]
fn is_available_twnic_style() {
let raw = "\
TWNIC WHOIS Server. This service is free and is provided as is.
Notice: Use of this service is subject to terms.
Notice: Do not use for spam.
Domain not found.
";
assert!(make_response(raw).is_available());
}
#[test]
fn is_available_false_when_registration_data_present_despite_noise() {
let raw = "\
Domain Name: example.com
Registrar: Example Registrar, Inc.
Creation Date: 2020-01-01T00:00:00Z
Name Server: ns1.example.com
Registrar Abuse Contact: please report if the object not found in directory
";
assert!(!make_response(raw).is_available());
}
#[test]
fn is_available_false_for_registered_domain() {
let raw = "\
Domain Name: example.com
Registrar: Example Registrar, Inc.
Creation Date: 2020-01-01T00:00:00Z
Name Server: ns1.example.com
";
assert!(!make_response(raw).is_available());
}
#[test]
fn registry_unavailable_true_for_tld_not_supported_sentinel() {
let raw = "\
TLD is not supported.
>>> Last update of WHOIS database: 2026-06-03T22:45:28Z <<<
Terms of Use: Access to WHOIS information is provided to assist persons ...
";
assert!(make_response(raw).registry_unavailable());
}
#[test]
fn registry_unavailable_true_for_malformed_request_sentinel() {
let raw = "\
Malformed request.
>>> Last update of WHOIS database: 2026-06-03T22:46:35Z <<<
";
assert!(make_response(raw).registry_unavailable());
}
#[test]
fn registry_unavailable_false_for_registered_domain() {
let raw = "\
Domain Name: example.com
Registrar: Example Registrar, Inc.
Creation Date: 2020-01-01T00:00:00Z
Name Server: ns1.example.com
";
assert!(!make_response(raw).registry_unavailable());
}
#[test]
fn registry_unavailable_false_for_available_domain() {
let raw = "No match for domain EXAMPLE.\n";
assert!(!make_response(raw).registry_unavailable());
}
#[test]
fn extract_status_does_not_panic_on_unicode_lowercasing_expansion() {
let raw = format!("Domain Status: {}\n", "İ".repeat(40));
let r = make_response(&raw); assert!(
!r.status.is_empty(),
"status value should still be extracted"
);
}
#[test]
fn is_available_hkirc_has_not_been_registered() {
let raw = "The domain has not been registered.\n";
assert!(make_response(raw).is_available());
}
#[test]
fn is_available_tab_delimited_status_available() {
let raw = "Domain:\tfoo.be\nStatus:\tAVAILABLE\n";
assert!(make_response(raw).is_available());
}
#[test]
fn is_not_available_tab_delimited_status_not_available() {
let raw = "Domain:\tfoo.be\nStatus:\tNOT AVAILABLE\n";
assert!(!make_response(raw).is_available());
}
#[test]
fn is_available_recognizes_additional_registry_phrasings() {
for raw in [
"*** Nothing found for this query.\n", "No Found\n", "No record found for 'example.ls'.\n", "No information was found matching that query.\n", "Object not found\n", "Not find MatchingRecord\n", ] {
assert!(
make_response(raw).is_available(),
"should detect available from: {raw:?}"
);
}
}
#[test]
fn is_available_does_not_match_registered_or_blocked_phrasings() {
for raw in [
"Domain Status: clientTransferProhibited\nRegistrar: Example, Inc.\n",
"Status: NOT AVAILABLE\n",
"Requests of this client are not permitted. Please use the web form.\n",
"This WHOIS service is free for personal, non-commercial use.\n",
] {
assert!(
!make_response(raw).is_available(),
"must NOT detect available from: {raw:?}"
);
}
}
#[test]
fn is_available_ignores_not_found_phrase_buried_in_prose() {
let raw = "\
This is an informational banner from the registry WHOIS service provider.
Note that if the queried object does not exist in our database we simply return an empty result rather than an error response.
Records not found in this directory may still exist upstream in the thick registry, so please check with the sponsoring registrar.
";
assert!(
!make_response(raw).is_available(),
"long prose sentences must not trigger availability"
);
}
#[test]
fn indicates_not_found_true_on_line_start() {
let raw = "\
Domain Name: example.com
queried object does not exist
";
assert!(make_response(raw).indicates_not_found());
}
#[test]
fn indicates_not_found_false_when_phrase_is_in_tos_footer() {
let raw = "\
Domain Name: example.com
Registrar: Example Registrar, Inc.
Creation Date: 2020-01-01T00:00:00Z
Name Server: ns1.example.com
Terms of Service:
Note that if the queried object does not exist in our database we return NXDOMAIN.
This document does not imply anything about specific domains.
";
assert!(!make_response(raw).indicates_not_found());
}
#[test]
fn status_extracted_only_from_top_level_block() {
let raw = "\
Domain Name: example.jp
Status: Active
Registrar: Example Registrar
[Tech-C]
Status: ok
Name: Technical Contact
";
let parsed = make_response(raw);
assert_eq!(parsed.status, vec!["Active".to_string()]);
}
#[test]
fn status_multiple_top_level_values_deduped() {
let raw = "\
Domain Name: example.com
Domain Status: clientTransferProhibited
Domain Status: clientUpdateProhibited
Domain Status: clientTransferProhibited
";
let parsed = make_response(raw);
assert_eq!(
parsed.status,
vec![
"clientTransferProhibited".to_string(),
"clientUpdateProhibited".to_string(),
]
);
}
#[test]
fn parse_date_handles_d_b_y_with_time() {
let parsed = parse_date("15-Jan-2024 10:30:00").expect("should parse");
use chrono::Datelike;
assert_eq!(parsed.year(), 2024);
assert_eq!(parsed.month(), 1);
assert_eq!(parsed.day(), 15);
}
#[test]
fn parse_date_handles_d_b_y_with_time_utc_suffix() {
let parsed = parse_date("15-Jan-2024 10:30:00 UTC").expect("should parse");
use chrono::Datelike;
assert_eq!(parsed.year(), 2024);
}
#[test]
fn parse_date_still_handles_d_b_y_date_only() {
let parsed = parse_date("15-Jan-2024").expect("should parse");
use chrono::Datelike;
assert_eq!(parsed.year(), 2024);
}
#[test]
fn parse_date_recovers_unambiguous_us_mdy() {
use chrono::Datelike;
let parsed = parse_date("02/13/2024").expect("unambiguous MDY should parse");
assert_eq!(parsed.year(), 2024);
assert_eq!(parsed.month(), 2);
assert_eq!(parsed.day(), 13);
}
#[test]
fn parse_date_ambiguous_numeric_is_day_first() {
use chrono::Datelike;
let parsed = parse_date("06/07/2024").expect("should parse");
assert_eq!(parsed.year(), 2024);
assert_eq!(parsed.month(), 7);
assert_eq!(parsed.day(), 6);
}
#[test]
fn raw_response_is_skipped_from_json_output() {
let raw = "Domain Name: example.com\nRegistrar: Example Registrar\n";
let parsed = make_response(raw);
assert!(
!parsed.raw_response.is_empty(),
"raw_response still populated internally"
);
let json = serde_json::to_string(&parsed).expect("serialize");
assert!(
!json.contains("raw_response"),
"raw_response must not appear in serialized JSON output: {}",
json
);
assert!(
!json.contains("Domain Name: example.com"),
"raw response content must not leak in JSON: {}",
json
);
}
}