1use serde::Deserialize;
5
6use crate::client::Client;
7use crate::error::Result;
8use crate::response::{de_bool, de_opt_bool, de_opt_from_str};
9
10#[derive(Debug, Clone, Copy)]
13pub struct Domains<'a> {
14 client: &'a Client,
15}
16
17impl<'a> Domains<'a> {
18 pub(crate) fn new(client: &'a Client) -> Self {
19 Self { client }
20 }
21
22 pub async fn check<I, S>(&self, domains: I) -> Result<Vec<DomainCheckResult>>
45 where
46 I: IntoIterator<Item = S>,
47 S: AsRef<str>,
48 {
49 let list = domains
50 .into_iter()
51 .map(|domain| domain.as_ref().to_owned())
52 .collect::<Vec<_>>()
53 .join(",");
54 let params = vec![("DomainList".to_owned(), list)];
55 let payload: CheckPayload = self.client.send("namecheap.domains.check", params).await?;
56 Ok(payload.results)
57 }
58
59 pub async fn create(&self, request: &DomainCreateRequest) -> Result<DomainCreateResult> {
76 let payload: CreatePayload = self
77 .client
78 .send("namecheap.domains.create", request.to_params())
79 .await?;
80 Ok(payload.result)
81 }
82
83 pub async fn list(&self) -> Result<DomainListResult> {
96 let params = vec![("PageSize".to_owned(), "100".to_owned())];
97 let payload: GetListPayload = self
98 .client
99 .send("namecheap.domains.getList", params)
100 .await?;
101 Ok(DomainListResult {
102 domains: payload.result.domains,
103 total_items: payload.paging.total_items,
104 current_page: payload.paging.current_page,
105 page_size: payload.paging.page_size,
106 })
107 }
108
109 pub async fn set_auto_renew(&self, domain: &str, enabled: bool) -> Result<SetAutoRenewResult> {
122 let params = vec![
123 ("DomainName".to_owned(), domain.to_owned()),
124 (
125 "AutoRenew".to_owned(),
126 if enabled { "true" } else { "false" }.to_owned(),
127 ),
128 ];
129 let payload: SetAutoRenewPayload = self
130 .client
131 .send("namecheap.domains.setAutoRenew", params)
132 .await?;
133 payload
134 .into_result()
135 .ok_or(crate::error::Error::EmptyResponse)
136 }
137
138 #[must_use]
140 pub fn dns(&self) -> Dns<'a> {
141 Dns {
142 client: self.client,
143 }
144 }
145}
146
147#[derive(Debug, Clone, Copy)]
150pub struct Dns<'a> {
151 client: &'a Client,
152}
153
154impl Dns<'_> {
155 pub async fn get_hosts(&self, sld: &str, tld: &str) -> Result<GetHostsResult> {
172 let params = vec![
173 ("SLD".to_owned(), sld.to_owned()),
174 ("TLD".to_owned(), tld.to_owned()),
175 ];
176 let payload: GetHostsPayload = self
177 .client
178 .send("namecheap.domains.dns.getHosts", params)
179 .await?;
180 Ok(payload.result)
181 }
182
183 pub async fn set_hosts(&self, request: &SetHostsRequest) -> Result<SetHostsResult> {
203 let payload: SetHostsPayload = self
204 .client
205 .send("namecheap.domains.dns.setHosts", request.to_params())
206 .await?;
207 Ok(payload.result)
208 }
209}
210
211#[derive(Debug, Deserialize)]
214struct CheckPayload {
215 #[serde(rename = "DomainCheckResult", default)]
216 results: Vec<DomainCheckResult>,
217}
218
219#[derive(Debug, Clone, Deserialize)]
222#[non_exhaustive]
223pub struct DomainCheckResult {
224 #[serde(rename = "@Domain")]
226 pub domain: String,
227 #[serde(rename = "@Available", deserialize_with = "de_bool")]
229 pub available: bool,
230 #[serde(rename = "@IsPremiumName", default, deserialize_with = "de_bool")]
232 pub is_premium_name: bool,
233 #[serde(
235 rename = "@PremiumRegistrationPrice",
236 default,
237 deserialize_with = "de_opt_from_str"
238 )]
239 pub premium_registration_price: Option<f64>,
240 #[serde(rename = "@ErrorNo", default)]
243 pub error_no: Option<String>,
244 #[serde(rename = "@Description", default)]
246 pub description: Option<String>,
247}
248
249#[derive(Debug, Clone)]
261pub struct Contact {
262 pub first_name: String,
264 pub last_name: String,
266 pub address1: String,
268 pub address2: Option<String>,
270 pub city: String,
272 pub state_province: String,
274 pub postal_code: String,
276 pub country: String,
278 pub phone: String,
280 pub email_address: String,
282 pub organization: Option<String>,
284}
285
286impl Contact {
287 #[allow(clippy::too_many_arguments)]
290 pub fn new(
291 first_name: impl Into<String>,
292 last_name: impl Into<String>,
293 address1: impl Into<String>,
294 city: impl Into<String>,
295 state_province: impl Into<String>,
296 postal_code: impl Into<String>,
297 country: impl Into<String>,
298 phone: impl Into<String>,
299 email_address: impl Into<String>,
300 ) -> Self {
301 Self {
302 first_name: first_name.into(),
303 last_name: last_name.into(),
304 address1: address1.into(),
305 address2: None,
306 city: city.into(),
307 state_province: state_province.into(),
308 postal_code: postal_code.into(),
309 country: country.into(),
310 phone: phone.into(),
311 email_address: email_address.into(),
312 organization: None,
313 }
314 }
315
316 #[must_use]
318 pub fn with_address2(mut self, address2: impl Into<String>) -> Self {
319 self.address2 = Some(address2.into());
320 self
321 }
322
323 #[must_use]
325 pub fn with_organization(mut self, organization: impl Into<String>) -> Self {
326 self.organization = Some(organization.into());
327 self
328 }
329}
330
331#[derive(Debug, Clone)]
333pub struct DomainCreateRequest {
334 pub domain: String,
336 pub years: u32,
338 pub registrant: Contact,
340 pub tech: Contact,
342 pub admin: Contact,
344 pub aux_billing: Contact,
346 pub nameservers: Vec<String>,
348 pub add_free_whoisguard: bool,
350 pub enable_whoisguard: bool,
352}
353
354impl DomainCreateRequest {
355 pub fn new(domain: impl Into<String>, years: u32, contact: Contact) -> Self {
358 Self {
359 domain: domain.into(),
360 years,
361 registrant: contact.clone(),
362 tech: contact.clone(),
363 admin: contact.clone(),
364 aux_billing: contact,
365 nameservers: Vec::new(),
366 add_free_whoisguard: true,
367 enable_whoisguard: true,
368 }
369 }
370
371 #[must_use]
373 pub fn with_nameservers<I, S>(mut self, nameservers: I) -> Self
374 where
375 I: IntoIterator<Item = S>,
376 S: Into<String>,
377 {
378 self.nameservers = nameservers.into_iter().map(Into::into).collect();
379 self
380 }
381
382 #[must_use]
384 pub fn with_whois_privacy(mut self, enabled: bool) -> Self {
385 self.add_free_whoisguard = enabled;
386 self.enable_whoisguard = enabled;
387 self
388 }
389
390 fn to_params(&self) -> Vec<(String, String)> {
391 let mut params = Vec::new();
392 params.push(("DomainName".to_owned(), self.domain.clone()));
393 params.push(("Years".to_owned(), self.years.to_string()));
394 push_contact(&mut params, "Registrant", &self.registrant);
395 push_contact(&mut params, "Tech", &self.tech);
396 push_contact(&mut params, "Admin", &self.admin);
397 push_contact(&mut params, "AuxBilling", &self.aux_billing);
398 if !self.nameservers.is_empty() {
399 params.push(("Nameservers".to_owned(), self.nameservers.join(",")));
400 }
401 params.push((
402 "AddFreeWhoisguard".to_owned(),
403 yes_no(self.add_free_whoisguard),
404 ));
405 params.push(("WGEnabled".to_owned(), yes_no(self.enable_whoisguard)));
406 params
407 }
408}
409
410fn push_contact(params: &mut Vec<(String, String)>, role: &str, contact: &Contact) {
411 let key = |suffix: &str| format!("{role}{suffix}");
412 params.push((key("FirstName"), contact.first_name.clone()));
413 params.push((key("LastName"), contact.last_name.clone()));
414 params.push((key("Address1"), contact.address1.clone()));
415 if let Some(address2) = &contact.address2 {
416 params.push((key("Address2"), address2.clone()));
417 }
418 params.push((key("City"), contact.city.clone()));
419 params.push((key("StateProvince"), contact.state_province.clone()));
420 params.push((key("PostalCode"), contact.postal_code.clone()));
421 params.push((key("Country"), contact.country.clone()));
422 params.push((key("Phone"), contact.phone.clone()));
423 params.push((key("EmailAddress"), contact.email_address.clone()));
424 if let Some(organization) = &contact.organization {
425 params.push((key("OrganizationName"), organization.clone()));
426 }
427}
428
429fn yes_no(value: bool) -> String {
430 if value { "yes" } else { "no" }.to_owned()
431}
432
433#[derive(Debug, Deserialize)]
434struct CreatePayload {
435 #[serde(rename = "DomainCreateResult")]
436 result: DomainCreateResult,
437}
438
439#[derive(Debug, Clone, Deserialize)]
441#[non_exhaustive]
442pub struct DomainCreateResult {
443 #[serde(rename = "@Domain")]
445 pub domain: String,
446 #[serde(rename = "@Registered", deserialize_with = "de_bool")]
448 pub registered: bool,
449 #[serde(
451 rename = "@ChargedAmount",
452 default,
453 deserialize_with = "de_opt_from_str"
454 )]
455 pub charged_amount: Option<f64>,
456 #[serde(rename = "@DomainID", default, deserialize_with = "de_opt_from_str")]
458 pub domain_id: Option<u64>,
459 #[serde(rename = "@OrderID", default, deserialize_with = "de_opt_from_str")]
461 pub order_id: Option<u64>,
462 #[serde(
464 rename = "@TransactionID",
465 default,
466 deserialize_with = "de_opt_from_str"
467 )]
468 pub transaction_id: Option<u64>,
469 #[serde(
471 rename = "@WhoisguardEnable",
472 default,
473 deserialize_with = "de_opt_bool"
474 )]
475 pub whoisguard_enabled: Option<bool>,
476 #[serde(
479 rename = "@NonRealTimeDomain",
480 default,
481 deserialize_with = "de_opt_bool"
482 )]
483 pub non_real_time_domain: Option<bool>,
484}
485
486#[derive(Debug, Clone, Copy, PartialEq, Eq)]
490#[non_exhaustive]
491pub enum RecordType {
492 A,
494 Aaaa,
496 Cname,
498 Mx,
500 Mxe,
502 Txt,
504 Ns,
506 Url,
508 Url301,
510 Frame,
512 Caa,
514}
515
516impl RecordType {
517 #[must_use]
520 pub fn as_str(self) -> &'static str {
521 match self {
522 RecordType::A => "A",
523 RecordType::Aaaa => "AAAA",
524 RecordType::Cname => "CNAME",
525 RecordType::Mx => "MX",
526 RecordType::Mxe => "MXE",
527 RecordType::Txt => "TXT",
528 RecordType::Ns => "NS",
529 RecordType::Url => "URL",
530 RecordType::Url301 => "URL301",
531 RecordType::Frame => "FRAME",
532 RecordType::Caa => "CAA",
533 }
534 }
535
536 #[must_use]
540 pub fn from_api_str(value: &str) -> Option<Self> {
541 Some(match value.trim().to_ascii_uppercase().as_str() {
542 "A" => RecordType::A,
543 "AAAA" => RecordType::Aaaa,
544 "CNAME" => RecordType::Cname,
545 "MX" => RecordType::Mx,
546 "MXE" => RecordType::Mxe,
547 "TXT" => RecordType::Txt,
548 "NS" => RecordType::Ns,
549 "URL" => RecordType::Url,
550 "URL301" => RecordType::Url301,
551 "FRAME" => RecordType::Frame,
552 "CAA" => RecordType::Caa,
553 _ => return None,
554 })
555 }
556}
557
558#[derive(Debug, Clone, Copy, PartialEq, Eq)]
564#[non_exhaustive]
565pub enum EmailType {
566 Mx,
568 Mxe,
570 Forward,
572 PrivateEmail,
574 Gmail,
576}
577
578impl EmailType {
579 #[must_use]
582 pub fn as_str(self) -> &'static str {
583 match self {
584 EmailType::Mx => "MX",
585 EmailType::Mxe => "MXE",
586 EmailType::Forward => "FWD",
587 EmailType::PrivateEmail => "OX",
588 EmailType::Gmail => "GMAIL",
589 }
590 }
591
592 #[must_use]
596 pub fn from_api_str(value: &str) -> Option<Self> {
597 Some(match value.trim().to_ascii_uppercase().as_str() {
598 "MX" => EmailType::Mx,
599 "MXE" => EmailType::Mxe,
600 "FWD" => EmailType::Forward,
601 "OX" => EmailType::PrivateEmail,
602 "GMAIL" => EmailType::Gmail,
603 _ => return None,
604 })
605 }
606}
607
608#[derive(Debug, Clone)]
610pub struct HostRecord {
611 pub host_name: String,
613 pub record_type: RecordType,
615 pub address: String,
617 pub mx_pref: Option<u32>,
619 pub ttl: Option<u32>,
621}
622
623impl HostRecord {
624 pub fn new(
626 host_name: impl Into<String>,
627 record_type: RecordType,
628 address: impl Into<String>,
629 ) -> Self {
630 Self {
631 host_name: host_name.into(),
632 record_type,
633 address: address.into(),
634 mx_pref: None,
635 ttl: None,
636 }
637 }
638
639 pub fn a(host_name: impl Into<String>, ipv4: impl Into<String>) -> Self {
641 Self::new(host_name, RecordType::A, ipv4)
642 }
643
644 pub fn aaaa(host_name: impl Into<String>, ipv6: impl Into<String>) -> Self {
646 Self::new(host_name, RecordType::Aaaa, ipv6)
647 }
648
649 pub fn cname(host_name: impl Into<String>, target: impl Into<String>) -> Self {
651 Self::new(host_name, RecordType::Cname, target)
652 }
653
654 pub fn txt(host_name: impl Into<String>, value: impl Into<String>) -> Self {
656 Self::new(host_name, RecordType::Txt, value)
657 }
658
659 pub fn mx(
661 host_name: impl Into<String>,
662 mail_server: impl Into<String>,
663 preference: u32,
664 ) -> Self {
665 Self {
666 host_name: host_name.into(),
667 record_type: RecordType::Mx,
668 address: mail_server.into(),
669 mx_pref: Some(preference),
670 ttl: None,
671 }
672 }
673
674 #[must_use]
676 pub fn with_ttl(mut self, ttl: u32) -> Self {
677 self.ttl = Some(ttl);
678 self
679 }
680
681 #[must_use]
683 pub fn with_mx_pref(mut self, preference: u32) -> Self {
684 self.mx_pref = Some(preference);
685 self
686 }
687}
688
689#[derive(Debug, Clone)]
696pub struct SetHostsRequest {
697 pub sld: String,
699 pub tld: String,
701 pub records: Vec<HostRecord>,
703 pub email_type: Option<EmailType>,
705}
706
707impl SetHostsRequest {
708 pub fn new(sld: impl Into<String>, tld: impl Into<String>, records: Vec<HostRecord>) -> Self {
710 Self {
711 sld: sld.into(),
712 tld: tld.into(),
713 records,
714 email_type: None,
715 }
716 }
717
718 pub fn from_domain(domain: &str, records: Vec<HostRecord>) -> Option<Self> {
722 let (sld, tld) = domain.split_once('.')?;
723 if sld.is_empty() || tld.is_empty() {
724 return None;
725 }
726 Some(Self::new(sld, tld, records))
727 }
728
729 #[must_use]
731 pub fn with_email_type(mut self, email_type: EmailType) -> Self {
732 self.email_type = Some(email_type);
733 self
734 }
735
736 fn to_params(&self) -> Vec<(String, String)> {
737 let mut params = Vec::new();
738 params.push(("SLD".to_owned(), self.sld.clone()));
739 params.push(("TLD".to_owned(), self.tld.clone()));
740 for (index, record) in self.records.iter().enumerate() {
741 let n = index + 1;
742 params.push((format!("HostName{n}"), record.host_name.clone()));
743 params.push((
744 format!("RecordType{n}"),
745 record.record_type.as_str().to_owned(),
746 ));
747 params.push((format!("Address{n}"), record.address.clone()));
748 let mx_pref = record.mx_pref.or(match record.record_type {
749 RecordType::Mx => Some(10),
750 _ => None,
751 });
752 if let Some(pref) = mx_pref {
753 params.push((format!("MXPref{n}"), pref.to_string()));
754 }
755 if let Some(ttl) = record.ttl {
756 params.push((format!("TTL{n}"), ttl.to_string()));
757 }
758 }
759 if let Some(email_type) = self.email_type {
760 params.push(("EmailType".to_owned(), email_type.as_str().to_owned()));
761 }
762 params
763 }
764}
765
766#[derive(Debug, Deserialize)]
767struct SetHostsPayload {
768 #[serde(rename = "DomainDNSSetHostsResult")]
769 result: SetHostsResult,
770}
771
772#[derive(Debug, Clone, Deserialize)]
774#[non_exhaustive]
775pub struct SetHostsResult {
776 #[serde(rename = "@Domain")]
778 pub domain: String,
779 #[serde(rename = "@IsSuccess", deserialize_with = "de_bool")]
781 pub is_success: bool,
782}
783
784#[derive(Debug, Deserialize)]
787struct GetHostsPayload {
788 #[serde(rename = "DomainDNSGetHostsResult")]
789 result: GetHostsResult,
790}
791
792#[derive(Debug, Clone, Deserialize)]
794#[non_exhaustive]
795pub struct GetHostsResult {
796 #[serde(rename = "@Domain")]
798 pub domain: String,
799 #[serde(rename = "@IsUsingOurDNS", default, deserialize_with = "de_bool")]
803 pub is_using_our_dns: bool,
804 #[serde(rename = "@EmailType", default)]
808 pub email_type: Option<String>,
809 #[serde(rename = "host", default)]
811 pub records: Vec<HostInfo>,
812}
813
814impl GetHostsResult {
815 #[must_use]
822 pub fn to_host_records(&self) -> Vec<HostRecord> {
823 self.records
824 .iter()
825 .filter_map(HostInfo::to_host_record)
826 .collect()
827 }
828}
829
830#[derive(Debug, Clone, Deserialize)]
836#[non_exhaustive]
837pub struct HostInfo {
838 #[serde(rename = "@HostId", default, deserialize_with = "de_opt_from_str")]
840 pub host_id: Option<u64>,
841 #[serde(rename = "@Name")]
843 pub name: String,
844 #[serde(rename = "@Type")]
846 pub record_type: String,
847 #[serde(rename = "@Address")]
849 pub address: String,
850 #[serde(rename = "@MXPref", default, deserialize_with = "de_opt_from_str")]
852 pub mx_pref: Option<u32>,
853 #[serde(rename = "@TTL", default, deserialize_with = "de_opt_from_str")]
855 pub ttl: Option<u32>,
856 #[serde(rename = "@IsActive", default, deserialize_with = "de_opt_bool")]
858 pub is_active: Option<bool>,
859}
860
861impl HostInfo {
862 #[must_use]
868 pub fn to_host_record(&self) -> Option<HostRecord> {
869 let record_type = RecordType::from_api_str(&self.record_type)?;
870 Some(HostRecord {
871 host_name: self.name.clone(),
872 record_type,
873 address: self.address.clone(),
874 mx_pref: self.mx_pref,
875 ttl: self.ttl,
876 })
877 }
878}
879
880#[derive(Debug, Deserialize)]
883struct GetListPayload {
884 #[serde(rename = "DomainGetListResult", default)]
885 result: GetListInner,
886 #[serde(rename = "Paging", default)]
887 paging: Paging,
888}
889
890#[derive(Debug, Default, Deserialize)]
891struct GetListInner {
892 #[serde(rename = "Domain", default)]
893 domains: Vec<DomainListItem>,
894}
895
896#[derive(Debug, Default, Deserialize)]
897struct Paging {
898 #[serde(rename = "TotalItems", default)]
899 total_items: u32,
900 #[serde(rename = "CurrentPage", default)]
901 current_page: u32,
902 #[serde(rename = "PageSize", default)]
903 page_size: u32,
904}
905
906#[derive(Debug, Clone)]
908#[non_exhaustive]
909pub struct DomainListResult {
910 pub domains: Vec<DomainListItem>,
912 pub total_items: u32,
914 pub current_page: u32,
916 pub page_size: u32,
918}
919
920#[derive(Debug, Clone, Deserialize)]
922#[non_exhaustive]
923pub struct DomainListItem {
924 #[serde(rename = "@ID", default, deserialize_with = "de_opt_from_str")]
926 pub id: Option<u64>,
927 #[serde(rename = "@Name")]
929 pub name: String,
930 #[serde(rename = "@Created", default)]
932 pub created: Option<String>,
933 #[serde(rename = "@Expires", default)]
935 pub expires: Option<String>,
936 #[serde(rename = "@IsExpired", default, deserialize_with = "de_opt_bool")]
938 pub is_expired: Option<bool>,
939 #[serde(rename = "@IsLocked", default, deserialize_with = "de_opt_bool")]
941 pub is_locked: Option<bool>,
942 #[serde(rename = "@AutoRenew", default, deserialize_with = "de_bool")]
944 pub auto_renew: bool,
945 #[serde(rename = "@WhoisGuard", default)]
947 pub whois_guard: Option<String>,
948 #[serde(rename = "@IsOurDNS", default, deserialize_with = "de_opt_bool")]
950 pub is_our_dns: Option<bool>,
951}
952
953#[derive(Debug, Deserialize)]
956struct SetAutoRenewPayload {
957 #[serde(rename = "SetAutoRenewResult", default)]
960 result: Option<SetAutoRenewResult>,
961 #[serde(rename = "DomainSetAutoRenewResult", default)]
962 alt_result: Option<SetAutoRenewResult>,
963}
964
965impl SetAutoRenewPayload {
966 fn into_result(self) -> Option<SetAutoRenewResult> {
967 self.result.or(self.alt_result)
968 }
969}
970
971#[derive(Debug, Clone, Deserialize)]
973#[non_exhaustive]
974pub struct SetAutoRenewResult {
975 #[serde(rename = "@Domain")]
977 pub domain: String,
978 #[serde(rename = "@IsSuccess", deserialize_with = "de_bool")]
980 pub is_success: bool,
981}
982
983#[cfg(test)]
984mod tests {
985 use super::*;
986 use reqwest::StatusCode;
987
988 fn value<'a>(params: &'a [(String, String)], key: &str) -> Option<&'a str> {
989 params
990 .iter()
991 .find(|(name, _)| name == key)
992 .map(|(_, value)| value.as_str())
993 }
994
995 fn sample_contact() -> Contact {
996 Contact::new(
997 "John",
998 "Doe",
999 "123 Main St",
1000 "Los Angeles",
1001 "CA",
1002 "90001",
1003 "US",
1004 "+1.5555551234",
1005 "john@example.com",
1006 )
1007 }
1008
1009 #[test]
1010 fn parses_check_response() {
1011 let body = r#"<?xml version="1.0" encoding="utf-8"?>
1012 <ApiResponse Status="OK" xmlns="http://api.namecheap.com/xml.response">
1013 <Errors />
1014 <CommandResponse Type="namecheap.domains.check">
1015 <DomainCheckResult Domain="taken.com" Available="false" ErrorNo="0" Description="" IsPremiumName="false" PremiumRegistrationPrice="0.0000" />
1016 <DomainCheckResult Domain="free-domain-9876.com" Available="true" ErrorNo="0" Description="" IsPremiumName="false" />
1017 <DomainCheckResult Domain="fancy.io" Available="true" IsPremiumName="true" PremiumRegistrationPrice="2999.9900" />
1018 </CommandResponse>
1019 </ApiResponse>"#;
1020
1021 let payload: CheckPayload = crate::response::parse(StatusCode::OK, body).unwrap();
1022 assert_eq!(payload.results.len(), 3);
1023
1024 assert_eq!(payload.results[0].domain, "taken.com");
1025 assert!(!payload.results[0].available);
1026
1027 assert!(payload.results[1].available);
1028 assert!(!payload.results[1].is_premium_name);
1029
1030 assert!(payload.results[2].is_premium_name);
1031 assert_eq!(payload.results[2].premium_registration_price, Some(2999.99));
1032 }
1033
1034 #[test]
1035 fn parses_create_response() {
1036 let body = r#"<ApiResponse Status="OK" xmlns="http://api.namecheap.com/xml.response">
1037 <Errors />
1038 <CommandResponse Type="namecheap.domains.create">
1039 <DomainCreateResult Domain="example.com" Registered="true" ChargedAmount="10.8700" DomainID="123456" OrderID="654321" TransactionID="111222" WhoisguardEnable="true" NonRealTimeDomain="false" />
1040 </CommandResponse>
1041 </ApiResponse>"#;
1042
1043 let payload: CreatePayload = crate::response::parse(StatusCode::OK, body).unwrap();
1044 let result = payload.result;
1045 assert_eq!(result.domain, "example.com");
1046 assert!(result.registered);
1047 assert_eq!(result.charged_amount, Some(10.87));
1048 assert_eq!(result.domain_id, Some(123456));
1049 assert_eq!(result.order_id, Some(654321));
1050 assert_eq!(result.whoisguard_enabled, Some(true));
1051 assert_eq!(result.non_real_time_domain, Some(false));
1052 }
1053
1054 #[test]
1055 fn parses_set_hosts_response() {
1056 let body = r#"<ApiResponse Status="OK" xmlns="http://api.namecheap.com/xml.response">
1057 <Errors />
1058 <CommandResponse Type="namecheap.domains.dns.setHosts">
1059 <DomainDNSSetHostsResult Domain="example.com" IsSuccess="true" />
1060 </CommandResponse>
1061 </ApiResponse>"#;
1062
1063 let payload: SetHostsPayload = crate::response::parse(StatusCode::OK, body).unwrap();
1064 assert_eq!(payload.result.domain, "example.com");
1065 assert!(payload.result.is_success);
1066 }
1067
1068 #[test]
1069 fn create_request_builds_all_contact_roles() {
1070 let request = DomainCreateRequest::new("example.com", 2, sample_contact())
1071 .with_nameservers(["ns1.example.com", "ns2.example.com"]);
1072 let params = request.to_params();
1073
1074 assert_eq!(value(¶ms, "DomainName"), Some("example.com"));
1075 assert_eq!(value(¶ms, "Years"), Some("2"));
1076 assert_eq!(
1077 value(¶ms, "Nameservers"),
1078 Some("ns1.example.com,ns2.example.com")
1079 );
1080 assert_eq!(value(¶ms, "AddFreeWhoisguard"), Some("yes"));
1081 assert_eq!(value(¶ms, "WGEnabled"), Some("yes"));
1082
1083 for role in ["Registrant", "Tech", "Admin", "AuxBilling"] {
1084 assert_eq!(value(¶ms, &format!("{role}FirstName")), Some("John"));
1085 assert_eq!(value(¶ms, &format!("{role}Country")), Some("US"));
1086 assert_eq!(
1087 value(¶ms, &format!("{role}EmailAddress")),
1088 Some("john@example.com")
1089 );
1090 }
1091 }
1092
1093 #[test]
1094 fn create_request_can_disable_privacy() {
1095 let params = DomainCreateRequest::new("example.com", 1, sample_contact())
1096 .with_whois_privacy(false)
1097 .to_params();
1098 assert_eq!(value(¶ms, "AddFreeWhoisguard"), Some("no"));
1099 assert_eq!(value(¶ms, "WGEnabled"), Some("no"));
1100 }
1101
1102 #[test]
1103 fn set_hosts_request_indexes_records_and_defaults_mx_pref() {
1104 let request = SetHostsRequest::new(
1105 "example",
1106 "com",
1107 vec![
1108 HostRecord::mx("@", "mx1.example.com", 10),
1109 HostRecord::txt("@", "v=spf1 include:_spf.example.com ~all"),
1110 HostRecord::a("www", "203.0.113.10").with_ttl(300),
1111 ],
1112 )
1113 .with_email_type(EmailType::Mx);
1114 let params = request.to_params();
1115
1116 assert_eq!(value(¶ms, "SLD"), Some("example"));
1117 assert_eq!(value(¶ms, "TLD"), Some("com"));
1118
1119 assert_eq!(value(¶ms, "HostName1"), Some("@"));
1120 assert_eq!(value(¶ms, "RecordType1"), Some("MX"));
1121 assert_eq!(value(¶ms, "Address1"), Some("mx1.example.com"));
1122 assert_eq!(value(¶ms, "MXPref1"), Some("10"));
1123
1124 assert_eq!(value(¶ms, "RecordType2"), Some("TXT"));
1125 assert_eq!(value(¶ms, "MXPref2"), None);
1127
1128 assert_eq!(value(¶ms, "RecordType3"), Some("A"));
1129 assert_eq!(value(¶ms, "TTL3"), Some("300"));
1130
1131 assert_eq!(value(¶ms, "EmailType"), Some("MX"));
1132 }
1133
1134 #[test]
1135 fn from_domain_splits_at_first_dot() {
1136 let records = vec![HostRecord::a("@", "203.0.113.10")];
1137
1138 let simple = SetHostsRequest::from_domain("example.com", records.clone()).unwrap();
1139 assert_eq!(simple.sld, "example");
1140 assert_eq!(simple.tld, "com");
1141
1142 let multi = SetHostsRequest::from_domain("example.co.uk", records.clone()).unwrap();
1143 assert_eq!(multi.sld, "example");
1144 assert_eq!(multi.tld, "co.uk");
1145
1146 assert!(SetHostsRequest::from_domain("nodot", records.clone()).is_none());
1147 assert!(SetHostsRequest::from_domain(".com", records.clone()).is_none());
1148 assert!(SetHostsRequest::from_domain("example.", records).is_none());
1149 }
1150
1151 #[test]
1152 fn parses_get_hosts_response_and_round_trips() {
1153 let body = r#"<ApiResponse Status="OK" xmlns="http://api.namecheap.com/xml.response">
1154 <Errors />
1155 <CommandResponse Type="namecheap.domains.dns.getHosts">
1156 <DomainDNSGetHostsResult Domain="example.com" IsUsingOurDNS="true" EmailType="MX">
1157 <host HostId="12" Name="@" Type="A" Address="203.0.113.10" MXPref="10" TTL="1800" IsActive="true" IsDDNSEnabled="false" />
1158 <host HostId="13" Name="@" Type="MX" Address="mx.example.com" MXPref="10" TTL="1800" IsActive="true" />
1159 <host HostId="14" Name="www" Type="CNAME" Address="example.com." MXPref="10" TTL="60" IsActive="true" />
1160 </DomainDNSGetHostsResult>
1161 </CommandResponse>
1162 </ApiResponse>"#;
1163
1164 let payload: GetHostsPayload = crate::response::parse(StatusCode::OK, body).unwrap();
1165 let result = payload.result;
1166 assert_eq!(result.domain, "example.com");
1167 assert!(result.is_using_our_dns);
1168 assert_eq!(result.email_type.as_deref(), Some("MX"));
1169 assert_eq!(result.records.len(), 3);
1170 assert_eq!(result.records[0].host_id, Some(12));
1171 assert_eq!(result.records[0].name, "@");
1172 assert_eq!(result.records[0].record_type, "A");
1173 assert_eq!(result.records[0].ttl, Some(1800));
1174 assert_eq!(result.records[0].is_active, Some(true));
1175
1176 let writable = result.to_host_records();
1178 assert_eq!(writable.len(), 3);
1179 assert_eq!(writable[1].record_type, RecordType::Mx);
1180 assert_eq!(writable[1].mx_pref, Some(10));
1181 assert_eq!(writable[2].host_name, "www");
1182 assert_eq!(writable[2].record_type, RecordType::Cname);
1183 }
1184
1185 #[test]
1186 fn record_and_email_types_parse_from_api_strings() {
1187 for record_type in [
1188 RecordType::A,
1189 RecordType::Mx,
1190 RecordType::Txt,
1191 RecordType::Caa,
1192 ] {
1193 assert_eq!(
1194 RecordType::from_api_str(record_type.as_str()),
1195 Some(record_type)
1196 );
1197 }
1198 assert_eq!(RecordType::from_api_str("cname"), Some(RecordType::Cname));
1199 assert_eq!(RecordType::from_api_str("bogus"), None);
1200
1201 assert_eq!(EmailType::from_api_str("MX"), Some(EmailType::Mx));
1202 assert_eq!(EmailType::from_api_str("fwd"), Some(EmailType::Forward));
1203 assert_eq!(EmailType::from_api_str("NONE"), None);
1204 }
1205
1206 #[test]
1207 fn parses_get_list_response() {
1208 let body = r#"<ApiResponse Status="OK" xmlns="http://api.namecheap.com/xml.response">
1209 <Errors />
1210 <CommandResponse Type="namecheap.domains.getList">
1211 <DomainGetListResult>
1212 <Domain ID="1120966" Name="alpha.com" User="someone" Created="06/19/2026" Expires="06/20/2027" IsExpired="false" IsLocked="false" AutoRenew="false" WhoisGuard="NOTPRESENT" IsPremium="false" IsOurDNS="true" />
1213 <Domain ID="1120967" Name="beta.com" User="someone" Created="06/19/2026" Expires="06/20/2027" IsExpired="false" IsLocked="false" AutoRenew="true" WhoisGuard="ENABLED" IsPremium="false" IsOurDNS="true" />
1214 </DomainGetListResult>
1215 <Paging>
1216 <TotalItems>2</TotalItems>
1217 <CurrentPage>1</CurrentPage>
1218 <PageSize>100</PageSize>
1219 </Paging>
1220 </CommandResponse>
1221 </ApiResponse>"#;
1222
1223 let payload: GetListPayload = crate::response::parse(StatusCode::OK, body).unwrap();
1224 assert_eq!(payload.paging.total_items, 2);
1225 assert_eq!(payload.paging.page_size, 100);
1226 assert_eq!(payload.result.domains.len(), 2);
1227
1228 let alpha = &payload.result.domains[0];
1229 assert_eq!(alpha.name, "alpha.com");
1230 assert_eq!(alpha.id, Some(1120966));
1231 assert!(!alpha.auto_renew);
1232 assert_eq!(alpha.expires.as_deref(), Some("06/20/2027"));
1233 assert_eq!(alpha.is_our_dns, Some(true));
1234
1235 assert!(payload.result.domains[1].auto_renew);
1236 assert_eq!(
1237 payload.result.domains[1].whois_guard.as_deref(),
1238 Some("ENABLED")
1239 );
1240 }
1241
1242 #[test]
1243 fn parses_set_auto_renew_response_both_spellings() {
1244 let observed = r#"<ApiResponse Status="OK" xmlns="http://api.namecheap.com/xml.response">
1246 <Errors />
1247 <CommandResponse Type="namecheap.domains.setAutoRenew">
1248 <SetAutoRenewResult Domain="example.com" IsSuccess="true" />
1249 </CommandResponse>
1250 </ApiResponse>"#;
1251 let payload: SetAutoRenewPayload =
1252 crate::response::parse(StatusCode::OK, observed).unwrap();
1253 let result = payload.into_result().unwrap();
1254 assert_eq!(result.domain, "example.com");
1255 assert!(result.is_success);
1256
1257 let alternative = r#"<ApiResponse Status="OK" xmlns="http://api.namecheap.com/xml.response">
1259 <Errors />
1260 <CommandResponse Type="namecheap.domains.setAutoRenew">
1261 <DomainSetAutoRenewResult Domain="example.com" IsSuccess="true" />
1262 </CommandResponse>
1263 </ApiResponse>"#;
1264 let payload: SetAutoRenewPayload =
1265 crate::response::parse(StatusCode::OK, alternative).unwrap();
1266 assert!(payload.into_result().unwrap().is_success);
1267 }
1268}