From 42154d56f4c973353781f3b7259fd0b04e91ce68 Mon Sep 17 00:00:00 2001 From: Oguz Kocer Date: Sun, 19 Apr 2026 00:29:31 -0400 Subject: [PATCH 01/10] Add `/domains/{name}/is-available/` endpoint Adds `RestV1_3` namespace, `DomainName` newtype, and `DomainAvailability` response type covering all API fields. Includes anonymized test fixtures for 5 status categories (available, blacklisted, transferrable, tld_not_supported, hsts_required) and e2e tests. --- wp_api/src/wp_com/domains.rs | 211 ++++++++++++++++++ wp_api/src/wp_com/endpoint.rs | 4 + .../src/wp_com/endpoint/domains_endpoint.rs | 25 ++- wp_api/src/wp_com/mod.rs | 2 + .../wpcom/domains/is_available/available.json | 22 ++ .../domains/is_available/hsts-required.json | 22 ++ .../domains/is_available/not-available.json | 8 + .../is_available/tld-not-supported.json | 9 + .../domains/is_available/transferrable.json | 14 ++ wp_com_e2e/src/domains_tests.rs | 66 +++++- 10 files changed, 378 insertions(+), 5 deletions(-) create mode 100644 wp_api/tests/wpcom/domains/is_available/available.json create mode 100644 wp_api/tests/wpcom/domains/is_available/hsts-required.json create mode 100644 wp_api/tests/wpcom/domains/is_available/not-available.json create mode 100644 wp_api/tests/wpcom/domains/is_available/tld-not-supported.json create mode 100644 wp_api/tests/wpcom/domains/is_available/transferrable.json diff --git a/wp_api/src/wp_com/domains.rs b/wp_api/src/wp_com/domains.rs index 8432f9a1e..98b5ea068 100644 --- a/wp_api/src/wp_com/domains.rs +++ b/wp_api/src/wp_com/domains.rs @@ -151,6 +151,80 @@ pub struct DomainPolicyNotice { pub message: String, } +/// Response from `GET /domains/{name}/is-available/` (v1.3). +/// +/// Reports whether a domain name is available for registration or +/// mapping, along with pricing and product details when applicable. +/// +/// The set of fields present varies by `status`: available domains +/// include full pricing, transferrable domains include partial +/// pricing, and blacklisted/restricted domains have only the core +/// fields. +#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)] +pub struct DomainAvailability { + /// The domain name that was checked. + pub domain_name: String, + /// The TLD portion of the domain (e.g. `"com"`, `"io"`, `"dev"`). + pub tld: String, + /// Availability status (e.g. `"available"`, `"transferrable"`, + /// `"blacklisted_domain"`, `"tld_not_supported"`, + /// `"restricted_domain"`, + /// `"recent_registration_lock_not_transferrable"`). + pub status: String, + /// Whether the domain can be mapped to a WordPress.com site + /// (e.g. `"mappable"`, `"blacklisted_domain"`, + /// `"restricted_domain"`). + pub mappable: String, + /// Whether the domain supports WHOIS privacy protection. + #[serde(default)] + #[uniffi(default = false)] + pub supports_privacy: bool, + /// Provider of the root domain (e.g. `"unknown"`). + pub root_domain_provider: String, + /// WordPress.com product ID for purchasing this domain. + pub product_id: Option, + /// WordPress.com product slug (e.g. `"domain_reg"`, + /// `"domain_transfer"`, `"dotnet_domain"`). + pub product_slug: Option, + /// Formatted registration cost (e.g. `"TL 426"`, `"$18.00"`). + pub cost: Option, + /// Formatted renewal cost (e.g. `"TL 426"`). + pub renew_cost: Option, + /// Raw numeric renewal price in `currency_code`. + pub renew_raw_price: Option, + /// Raw numeric registration price in `currency_code`. + pub raw_price: Option, + /// ISO 4217 currency code (e.g. `"USD"`, `"TRY"`). + pub currency_code: Option, + /// Reasons the domain matched (e.g. `"exact-match"`, + /// `"tld-exact"`, `"tld-common"`). + pub match_reasons: Option>, + /// The registry vendor (e.g. `"availability"`). + pub vendor: Option, + /// Type of ownership verification required (e.g. + /// `"no_verification_required"`). + pub ownership_verification_type: Option, + /// `true` if the TLD requires HSTS (e.g. `.dev`). + pub hsts_required: Option, + /// Policy notices attached to the domain (e.g. HSTS warnings). + #[serde(default)] + #[uniffi(default = [])] + pub policy_notices: Vec, +} + +impl_as_query_value_for_new_type!(DomainName); +uniffi::custom_newtype!(DomainName, String); +/// A domain name (e.g. `"example.com"`, `"myblog.org"`). +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(transparent)] +pub struct DomainName(pub String); + +impl std::fmt::Display for DomainName { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + impl_as_query_value_for_new_type!(CountryCode); uniffi::custom_newtype!(CountryCode, String); /// ISO 3166-1 alpha-2 country code (e.g. `"US"`, `"CA"`, `"GB"`). @@ -509,4 +583,141 @@ mod tests { assert_eq!(only.domain_name, "testsite.home.blog"); assert_eq!(only.vendor, "dotblogsubdomains"); } + + #[rstest] + #[case::available( + "tests/wpcom/domains/is_available/available.json", + "freshsite2025.com", + "com", + "available", + "mappable", + true + )] + #[case::blacklisted( + "tests/wpcom/domains/is_available/not-available.json", + "example.com", + "com", + "blacklisted_domain", + "blacklisted_domain", + true + )] + #[case::transferrable( + "tests/wpcom/domains/is_available/transferrable.json", + "taken-domain.io", + "io", + "transferrable", + "mappable", + true + )] + #[case::tld_not_supported( + "tests/wpcom/domains/is_available/tld-not-supported.json", + "mysite.ai", + "ai", + "tld_not_supported", + "mappable", + false + )] + #[case::hsts_required( + "tests/wpcom/domains/is_available/hsts-required.json", + "myproject.dev", + "dev", + "recent_registration_lock_not_transferrable", + "mappable", + false + )] + fn test_domain_availability_deserialization( + #[case] json_file_path: &str, + #[case] expected_domain: &str, + #[case] expected_tld: &str, + #[case] expected_status: &str, + #[case] expected_mappable: &str, + #[case] expected_privacy: bool, + ) { + let file = File::open(json_file_path).expect("Failed to open file"); + let availability: DomainAvailability = + serde_json::from_reader(file).expect("Unable to parse JSON"); + assert_eq!(availability.domain_name, expected_domain); + assert_eq!(availability.tld, expected_tld); + assert_eq!(availability.status, expected_status); + assert_eq!(availability.mappable, expected_mappable); + assert_eq!(availability.supports_privacy, expected_privacy); + } + + #[test] + fn test_domain_availability_available_details() { + let file = File::open("tests/wpcom/domains/is_available/available.json") + .expect("Failed to open file"); + let availability: DomainAvailability = + serde_json::from_reader(file).expect("Unable to parse JSON"); + assert_eq!(availability.product_id, Some(6)); + assert_eq!(availability.product_slug.as_deref(), Some("domain_reg")); + assert_eq!(availability.cost.as_deref(), Some("$18.00")); + assert_eq!(availability.renew_cost.as_deref(), Some("$18.00")); + assert_eq!(availability.raw_price, Some(1800)); + assert_eq!(availability.renew_raw_price, Some(1800)); + assert_eq!(availability.currency_code.as_deref(), Some("USD")); + assert_eq!( + availability.match_reasons.as_deref(), + Some( + ["exact-match", "tld-exact", "tld-common"] + .map(String::from) + .as_slice() + ) + ); + assert_eq!(availability.vendor.as_deref(), Some("availability")); + assert_eq!( + availability.ownership_verification_type.as_deref(), + Some("no_verification_required") + ); + } + + #[test] + fn test_domain_availability_blacklisted_has_no_pricing() { + let file = File::open("tests/wpcom/domains/is_available/not-available.json") + .expect("Failed to open file"); + let availability: DomainAvailability = + serde_json::from_reader(file).expect("Unable to parse JSON"); + assert_eq!(availability.product_id, None); + assert_eq!(availability.product_slug, None); + assert_eq!(availability.cost, None); + assert_eq!(availability.raw_price, None); + assert_eq!(availability.currency_code, None); + assert_eq!(availability.match_reasons, None); + assert!(availability.policy_notices.is_empty()); + } + + #[test] + fn test_domain_availability_transferrable_partial_pricing() { + let file = File::open("tests/wpcom/domains/is_available/transferrable.json") + .expect("Failed to open file"); + let availability: DomainAvailability = + serde_json::from_reader(file).expect("Unable to parse JSON"); + assert_eq!(availability.product_id, Some(1337)); + assert_eq!( + availability.product_slug.as_deref(), + Some("domain_transfer") + ); + assert_eq!(availability.cost.as_deref(), Some("$48.00")); + assert_eq!(availability.raw_price, Some(4800)); + // Transferrable domains don't include renewal pricing. + assert_eq!(availability.renew_cost, None); + assert_eq!(availability.renew_raw_price, None); + } + + #[test] + fn test_domain_availability_hsts_policy_notices() { + let file = File::open("tests/wpcom/domains/is_available/hsts-required.json") + .expect("Failed to open file"); + let availability: DomainAvailability = + serde_json::from_reader(file).expect("Unable to parse JSON"); + assert_eq!(availability.hsts_required, Some(true)); + assert_eq!(availability.policy_notices.len(), 1); + assert_eq!(availability.policy_notices[0].notice_type, "hsts"); + assert_eq!(availability.policy_notices[0].label, "HSTS required"); + assert!( + availability.policy_notices[0] + .message + .contains("SSL certificate") + ); + } } diff --git a/wp_api/src/wp_com/endpoint.rs b/wp_api/src/wp_com/endpoint.rs index 89496f7a1..20d072095 100644 --- a/wp_api/src/wp_com/endpoint.rs +++ b/wp_api/src/wp_com/endpoint.rs @@ -169,6 +169,10 @@ pub(crate) mod tests { validate_endpoint(WpComNamespace::RestV1_1, endpoint_url, path); } + pub fn validate_wp_com_rest_v1_3_endpoint(endpoint_url: ApiEndpointUrl, path: &str) { + validate_endpoint(WpComNamespace::RestV1_3, endpoint_url, path); + } + pub fn validate_wp_com_v2_endpoint(endpoint_url: ApiEndpointUrl, path: &str) { validate_endpoint(WpComNamespace::V2, endpoint_url, path); } diff --git a/wp_api/src/wp_com/endpoint/domains_endpoint.rs b/wp_api/src/wp_com/endpoint/domains_endpoint.rs index a9b215c2a..e47a4f550 100644 --- a/wp_api/src/wp_com/endpoint/domains_endpoint.rs +++ b/wp_api/src/wp_com/endpoint/domains_endpoint.rs @@ -3,8 +3,8 @@ use crate::{ wp_com::{ WpComNamespace, domains::{ - CountryCode, DomainSuggestion, DomainSuggestionsParams, SupportedCountries, - SupportedState, + CountryCode, DomainAvailability, DomainName, DomainSuggestion, + DomainSuggestionsParams, SupportedCountries, SupportedState, }, }, }; @@ -18,11 +18,16 @@ enum DomainsRequest { SupportedCountries, #[get(url = "/domains/supported-states/", output = Vec)] SupportedStates, + #[get(url = "/domains//is-available", output = DomainAvailability)] + IsAvailable, } impl DerivedRequest for DomainsRequest { fn namespace(&self) -> impl AsNamespace { - WpComNamespace::RestV1_1 + match self { + Self::IsAvailable => WpComNamespace::RestV1_3, + _ => WpComNamespace::RestV1_1, + } } } @@ -32,9 +37,10 @@ mod tests { use crate::{ request::endpoint::ApiUrlResolver, wp_com::{ - domains::CountryCode, + domains::{CountryCode, DomainName}, endpoint::tests::{ fixture_wp_com_api_url_resolver, validate_wp_com_rest_v1_1_endpoint, + validate_wp_com_rest_v1_3_endpoint, }, segments::SegmentId, }, @@ -106,6 +112,17 @@ mod tests { validate_wp_com_rest_v1_1_endpoint(endpoint.supported_states(&country_code), expected_path); } + #[rstest] + #[case::com(DomainName("example.com".to_string()), "/domains/example.com/is-available")] + #[case::org(DomainName("myblog.org".to_string()), "/domains/myblog.org/is-available")] + fn is_available( + endpoint: DomainsRequestEndpoint, + #[case] domain_name: DomainName, + #[case] expected_path: &str, + ) { + validate_wp_com_rest_v1_3_endpoint(endpoint.is_available(&domain_name), expected_path); + } + fn base_domain_suggestions_params() -> DomainSuggestionsParams { DomainSuggestionsParams { query: "coolsite".to_string(), diff --git a/wp_api/src/wp_com/mod.rs b/wp_api/src/wp_com/mod.rs index 56c7535eb..112febc16 100644 --- a/wp_api/src/wp_com/mod.rs +++ b/wp_api/src/wp_com/mod.rs @@ -90,6 +90,7 @@ pub(crate) enum WpComNamespace { Oauth2, RestV1_1, RestV1_2, + RestV1_3, V2, } @@ -99,6 +100,7 @@ impl AsNamespace for WpComNamespace { WpComNamespace::Oauth2 => "/oauth2", WpComNamespace::RestV1_1 => "/rest/v1.1", WpComNamespace::RestV1_2 => "/rest/v1.2", + WpComNamespace::RestV1_3 => "/rest/v1.3", WpComNamespace::V2 => "/wpcom/v2", } } diff --git a/wp_api/tests/wpcom/domains/is_available/available.json b/wp_api/tests/wpcom/domains/is_available/available.json new file mode 100644 index 000000000..0360f47e6 --- /dev/null +++ b/wp_api/tests/wpcom/domains/is_available/available.json @@ -0,0 +1,22 @@ +{ + "domain_name": "freshsite2025.com", + "tld": "com", + "status": "available", + "mappable": "mappable", + "supports_privacy": true, + "root_domain_provider": "unknown", + "product_id": 6, + "product_slug": "domain_reg", + "cost": "$18.00", + "renew_cost": "$18.00", + "renew_raw_price": 1800, + "raw_price": 1800, + "currency_code": "USD", + "match_reasons": [ + "exact-match", + "tld-exact", + "tld-common" + ], + "vendor": "availability", + "ownership_verification_type": "no_verification_required" +} diff --git a/wp_api/tests/wpcom/domains/is_available/hsts-required.json b/wp_api/tests/wpcom/domains/is_available/hsts-required.json new file mode 100644 index 000000000..9eddb565e --- /dev/null +++ b/wp_api/tests/wpcom/domains/is_available/hsts-required.json @@ -0,0 +1,22 @@ +{ + "domain_name": "myproject.dev", + "tld": "dev", + "status": "recent_registration_lock_not_transferrable", + "mappable": "mappable", + "supports_privacy": false, + "root_domain_provider": "unknown", + "hsts_required": true, + "product_id": 1337, + "product_slug": "domain_transfer", + "cost": "$120.00", + "raw_price": 12000, + "currency_code": "USD", + "ownership_verification_type": "no_verification_required", + "policy_notices": [ + { + "type": "hsts", + "label": "HSTS required", + "message": "All domains with this ending require an SSL certificate to host a website." + } + ] +} diff --git a/wp_api/tests/wpcom/domains/is_available/not-available.json b/wp_api/tests/wpcom/domains/is_available/not-available.json new file mode 100644 index 000000000..d6a2e4bbc --- /dev/null +++ b/wp_api/tests/wpcom/domains/is_available/not-available.json @@ -0,0 +1,8 @@ +{ + "domain_name": "example.com", + "tld": "com", + "status": "blacklisted_domain", + "mappable": "blacklisted_domain", + "supports_privacy": true, + "root_domain_provider": "unknown" +} diff --git a/wp_api/tests/wpcom/domains/is_available/tld-not-supported.json b/wp_api/tests/wpcom/domains/is_available/tld-not-supported.json new file mode 100644 index 000000000..556fd8408 --- /dev/null +++ b/wp_api/tests/wpcom/domains/is_available/tld-not-supported.json @@ -0,0 +1,9 @@ +{ + "domain_name": "mysite.ai", + "tld": "ai", + "status": "tld_not_supported", + "mappable": "mappable", + "supports_privacy": false, + "root_domain_provider": "unknown", + "ownership_verification_type": "no_verification_required" +} diff --git a/wp_api/tests/wpcom/domains/is_available/transferrable.json b/wp_api/tests/wpcom/domains/is_available/transferrable.json new file mode 100644 index 000000000..a484c4e76 --- /dev/null +++ b/wp_api/tests/wpcom/domains/is_available/transferrable.json @@ -0,0 +1,14 @@ +{ + "domain_name": "taken-domain.io", + "tld": "io", + "status": "transferrable", + "mappable": "mappable", + "supports_privacy": true, + "root_domain_provider": "unknown", + "product_id": 1337, + "product_slug": "domain_transfer", + "cost": "$48.00", + "raw_price": 4800, + "currency_code": "USD", + "ownership_verification_type": "no_verification_required" +} diff --git a/wp_com_e2e/src/domains_tests.rs b/wp_com_e2e/src/domains_tests.rs index 5adc0b529..3b8f51157 100644 --- a/wp_com_e2e/src/domains_tests.rs +++ b/wp_com_e2e/src/domains_tests.rs @@ -1,7 +1,7 @@ use crate::context::TestContext; use libtest_mimic::Trial; use std::sync::Arc; -use wp_api::wp_com::domains::CountryCode; +use wp_api::wp_com::domains::{CountryCode, DomainName}; pub fn tests(ctx: Arc) -> Vec { let mut trials = vec![]; @@ -83,5 +83,69 @@ pub fn tests(ctx: Arc) -> Vec { } })); + trials.push(Trial::test("domains::is_available_taken", { + let ctx = Arc::clone(&ctx); + move || { + ctx.runtime.block_on(async { + let availability = ctx + .client + .domains() + .is_available(&DomainName("google.com".to_string())) + .await + .map_err(|e| e.to_string())? + .data; + + if availability.domain_name != "google.com" { + return Err(format!( + "expected domain_name 'google.com', got '{}'", + availability.domain_name + ) + .into()); + } + + if availability.status == "available" { + return Err("expected google.com to not be available".into()); + } + + Ok(()) + }) + } + })); + + trials.push(Trial::test("domains::is_available_likely_available", { + let ctx = Arc::clone(&ctx); + move || { + ctx.runtime.block_on(async { + let availability = ctx + .client + .domains() + .is_available(&DomainName( + "xyzzy-test-unlikely-taken-2025.com".to_string(), + )) + .await + .map_err(|e| e.to_string())? + .data; + + if availability.status != "available" { + return Err(format!( + "expected status 'available', got '{}'", + availability.status + ) + .into()); + } + + if !availability.supports_privacy { + return Err("expected .com domain to support privacy".into()); + } + + if availability.product_id.is_none() { + return Err("expected product_id for available domain".into()); + } + + Ok(()) + }) + } + })); + trials } From 9ab20dae997e06c851bfa9e3877e5a0988a3ebc7 Mon Sep 17 00:00:00 2001 From: Oguz Kocer Date: Tue, 21 Apr 2026 23:02:52 -0400 Subject: [PATCH 02/10] Change DomainAvailability raw_price/renew_raw_price to Decimal2 The backend's Store_Price::amount() returns a float, not cents. Consistent with how domain suggestions already use Decimal2 for these same fields. Also normalize None assertions to use is_none(). --- wp_api/src/wp_com/domains.rs | 37 +++++++++++-------- .../wpcom/domains/is_available/available.json | 4 +- .../domains/is_available/hsts-required.json | 2 +- .../domains/is_available/transferrable.json | 2 +- 4 files changed, 26 insertions(+), 19 deletions(-) diff --git a/wp_api/src/wp_com/domains.rs b/wp_api/src/wp_com/domains.rs index 98b5ea068..43f303a5a 100644 --- a/wp_api/src/wp_com/domains.rs +++ b/wp_api/src/wp_com/domains.rs @@ -191,9 +191,13 @@ pub struct DomainAvailability { /// Formatted renewal cost (e.g. `"TL 426"`). pub renew_cost: Option, /// Raw numeric renewal price in `currency_code`. - pub renew_raw_price: Option, + #[serde(default)] + #[uniffi(default = None)] + pub renew_raw_price: Option, /// Raw numeric registration price in `currency_code`. - pub raw_price: Option, + #[serde(default)] + #[uniffi(default = None)] + pub raw_price: Option, /// ISO 4217 currency code (e.g. `"USD"`, `"TRY"`). pub currency_code: Option, /// Reasons the domain matched (e.g. `"exact-match"`, @@ -411,8 +415,8 @@ mod tests { assert_eq!(first.renew_raw_price.hundredths(), 1800); assert_eq!(first.raw_price.hundredths(), 1800); assert_eq!(first.currency_code, "USD"); - assert_eq!(first.sale_cost, None); - assert_eq!(first.hsts_required, None); + assert!(first.sale_cost.is_none()); + assert!(first.hsts_required.is_none()); assert!(first.policy_notices.is_empty()); // `freshpage.art` has no `match_reasons` field in the JSON. @@ -653,8 +657,11 @@ mod tests { assert_eq!(availability.product_slug.as_deref(), Some("domain_reg")); assert_eq!(availability.cost.as_deref(), Some("$18.00")); assert_eq!(availability.renew_cost.as_deref(), Some("$18.00")); - assert_eq!(availability.raw_price, Some(1800)); - assert_eq!(availability.renew_raw_price, Some(1800)); + assert_eq!(availability.raw_price, Some(Decimal2::from_hundredths(1800))); + assert_eq!( + availability.renew_raw_price, + Some(Decimal2::from_hundredths(1800)) + ); assert_eq!(availability.currency_code.as_deref(), Some("USD")); assert_eq!( availability.match_reasons.as_deref(), @@ -677,12 +684,12 @@ mod tests { .expect("Failed to open file"); let availability: DomainAvailability = serde_json::from_reader(file).expect("Unable to parse JSON"); - assert_eq!(availability.product_id, None); - assert_eq!(availability.product_slug, None); - assert_eq!(availability.cost, None); - assert_eq!(availability.raw_price, None); - assert_eq!(availability.currency_code, None); - assert_eq!(availability.match_reasons, None); + assert!(availability.product_id.is_none()); + assert!(availability.product_slug.is_none()); + assert!(availability.cost.is_none()); + assert!(availability.raw_price.is_none()); + assert!(availability.currency_code.is_none()); + assert!(availability.match_reasons.is_none()); assert!(availability.policy_notices.is_empty()); } @@ -698,10 +705,10 @@ mod tests { Some("domain_transfer") ); assert_eq!(availability.cost.as_deref(), Some("$48.00")); - assert_eq!(availability.raw_price, Some(4800)); + assert_eq!(availability.raw_price, Some(Decimal2::from_hundredths(4800))); // Transferrable domains don't include renewal pricing. - assert_eq!(availability.renew_cost, None); - assert_eq!(availability.renew_raw_price, None); + assert!(availability.renew_cost.is_none()); + assert!(availability.renew_raw_price.is_none()); } #[test] diff --git a/wp_api/tests/wpcom/domains/is_available/available.json b/wp_api/tests/wpcom/domains/is_available/available.json index 0360f47e6..c13284622 100644 --- a/wp_api/tests/wpcom/domains/is_available/available.json +++ b/wp_api/tests/wpcom/domains/is_available/available.json @@ -9,8 +9,8 @@ "product_slug": "domain_reg", "cost": "$18.00", "renew_cost": "$18.00", - "renew_raw_price": 1800, - "raw_price": 1800, + "renew_raw_price": 18, + "raw_price": 18, "currency_code": "USD", "match_reasons": [ "exact-match", diff --git a/wp_api/tests/wpcom/domains/is_available/hsts-required.json b/wp_api/tests/wpcom/domains/is_available/hsts-required.json index 9eddb565e..17033496f 100644 --- a/wp_api/tests/wpcom/domains/is_available/hsts-required.json +++ b/wp_api/tests/wpcom/domains/is_available/hsts-required.json @@ -9,7 +9,7 @@ "product_id": 1337, "product_slug": "domain_transfer", "cost": "$120.00", - "raw_price": 12000, + "raw_price": 120, "currency_code": "USD", "ownership_verification_type": "no_verification_required", "policy_notices": [ diff --git a/wp_api/tests/wpcom/domains/is_available/transferrable.json b/wp_api/tests/wpcom/domains/is_available/transferrable.json index a484c4e76..1206935ed 100644 --- a/wp_api/tests/wpcom/domains/is_available/transferrable.json +++ b/wp_api/tests/wpcom/domains/is_available/transferrable.json @@ -8,7 +8,7 @@ "product_id": 1337, "product_slug": "domain_transfer", "cost": "$48.00", - "raw_price": 4800, + "raw_price": 48, "currency_code": "USD", "ownership_verification_type": "no_verification_required" } From 5ba8aa51aa3db24dee436b403f116f8cb0c93844 Mon Sep 17 00:00:00 2001 From: Oguz Kocer Date: Tue, 21 Apr 2026 23:08:54 -0400 Subject: [PATCH 03/10] Add missing fields to DomainAvailability from backend API Fields added: sale_cost, is_price_limit_exceeded, is_supported_premium_domain, other_site_domain, transferrability, maintenance_end_time, is_dot_gay_notice_required, cannot_transfer_due_to_unsupported_premium_tld, and trademark_claims_notice_info. Also updates status doc comment with comprehensive list from the backend implementation. --- wp_api/src/wp_com/domains.rs | 161 ++++++++++++++++-- .../is_available/available-premium.json | 20 +++ .../domains/is_available/maintenance.json | 9 + .../is_available/mapped-same-user.json | 11 ++ .../domains/is_available/sale-coupon.json | 19 +++ 5 files changed, 207 insertions(+), 13 deletions(-) create mode 100644 wp_api/tests/wpcom/domains/is_available/available-premium.json create mode 100644 wp_api/tests/wpcom/domains/is_available/maintenance.json create mode 100644 wp_api/tests/wpcom/domains/is_available/mapped-same-user.json create mode 100644 wp_api/tests/wpcom/domains/is_available/sale-coupon.json diff --git a/wp_api/src/wp_com/domains.rs b/wp_api/src/wp_com/domains.rs index 43f303a5a..54774e7be 100644 --- a/wp_api/src/wp_com/domains.rs +++ b/wp_api/src/wp_com/domains.rs @@ -158,62 +158,117 @@ pub struct DomainPolicyNotice { /// /// The set of fields present varies by `status`: available domains /// include full pricing, transferrable domains include partial -/// pricing, and blacklisted/restricted domains have only the core -/// fields. +/// pricing, and unavailable domains have only the core fields. +/// +/// Common `status` values: `"available"`, `"available_premium"`, +/// `"transferrable"`, `"transferrable_premium"`, +/// `"tld_not_supported"`, `"tld_in_maintenance"`, +/// `"blacklisted_domain"`, `"mapped_domain"`, +/// `"registered_domain"`, `"registered_on_other_site_same_user"`, +/// `"mapped_to_other_site_same_user"`, +/// `"recent_registration_lock_not_transferrable"`, +/// `"dotblog_subdomain"`, `"unknown"`. #[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)] pub struct DomainAvailability { /// The domain name that was checked. pub domain_name: String, /// The TLD portion of the domain (e.g. `"com"`, `"io"`, `"dev"`). pub tld: String, - /// Availability status (e.g. `"available"`, `"transferrable"`, - /// `"blacklisted_domain"`, `"tld_not_supported"`, - /// `"restricted_domain"`, - /// `"recent_registration_lock_not_transferrable"`). + /// Availability status — see struct-level docs for known values. pub status: String, /// Whether the domain can be mapped to a WordPress.com site - /// (e.g. `"mappable"`, `"blacklisted_domain"`, - /// `"restricted_domain"`). + /// (e.g. `"mappable"`, `"blacklisted_domain"`). pub mappable: String, /// Whether the domain supports WHOIS privacy protection. #[serde(default)] #[uniffi(default = false)] pub supports_privacy: bool, - /// Provider of the root domain (e.g. `"unknown"`). + /// Provider of the root domain (`"wpcom"` or `"unknown"`). pub root_domain_provider: String, + // -- Product/pricing fields (conditional on status) -- /// WordPress.com product ID for purchasing this domain. pub product_id: Option, /// WordPress.com product slug (e.g. `"domain_reg"`, - /// `"domain_transfer"`, `"dotnet_domain"`). + /// `"domain_transfer"`). pub product_slug: Option, - /// Formatted registration cost (e.g. `"TL 426"`, `"$18.00"`). + /// Formatted registration/transfer cost (e.g. `"$18.00"`). pub cost: Option, - /// Formatted renewal cost (e.g. `"TL 426"`). + /// Formatted renewal cost. pub renew_cost: Option, /// Raw numeric renewal price in `currency_code`. #[serde(default)] #[uniffi(default = None)] pub renew_raw_price: Option, - /// Raw numeric registration price in `currency_code`. + /// Raw numeric registration/transfer price in `currency_code`. #[serde(default)] #[uniffi(default = None)] pub raw_price: Option, /// ISO 4217 currency code (e.g. `"USD"`, `"TRY"`). pub currency_code: Option, + /// Discounted sale price when a coupon applies. + #[serde(default)] + #[uniffi(default = None)] + pub sale_cost: Option, + /// `true` if a premium domain exceeds the price limit. + #[serde(default)] + #[uniffi(default = None)] + pub is_price_limit_exceeded: Option, + // -- Match/vendor fields (available domains only) -- /// Reasons the domain matched (e.g. `"exact-match"`, /// `"tld-exact"`, `"tld-common"`). pub match_reasons: Option>, /// The registry vendor (e.g. `"availability"`). pub vendor: Option, + /// `true` for status `"available_premium"`. + #[serde(default)] + #[uniffi(default = None)] + pub is_supported_premium_domain: Option, + // -- Mapping/transfer fields -- /// Type of ownership verification required (e.g. /// `"no_verification_required"`). pub ownership_verification_type: Option, + /// Transfer status for mapped domains (e.g. `"transferrable"`, + /// `"recent_registration_lock_not_transferrable"`). + pub transferrability: Option, + /// Primary domain of the other site where this domain is + /// registered or mapped (same user, different site). + pub other_site_domain: Option, + // -- TLD-specific fields -- /// `true` if the TLD requires HSTS (e.g. `.dev`). pub hsts_required: Option, + /// `true` if the `.gay` TLD policy notice is required. + #[serde(default)] + #[uniffi(default = None)] + pub is_dot_gay_notice_required: Option, + /// `true` if premium domain transfers are unsupported for this TLD. + #[serde(default)] + #[uniffi(default = None)] + pub cannot_transfer_due_to_unsupported_premium_tld: Option, /// Policy notices attached to the domain (e.g. HSTS warnings). #[serde(default)] #[uniffi(default = [])] pub policy_notices: Vec, + // -- Maintenance -- + /// When domain registration or TLD is in maintenance, the end time. + pub maintenance_end_time: Option, + /// TMCH (Trademark Claims) notice info as a raw JSON string, + /// if applicable. Complex XML-derived structure that varies by TLD. + #[serde( + default, + deserialize_with = "crate::wp_com::domains::deserialize_json_value_as_string", + skip_serializing_if = "Option::is_none" + )] + #[uniffi(default = None)] + pub trademark_claims_notice_info: Option, +} + +/// Deserializes an arbitrary JSON value into its stringified form. +fn deserialize_json_value_as_string<'de, D>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + let value: Option = Option::deserialize(deserializer)?; + Ok(value.map(|v| v.to_string())) } impl_as_query_value_for_new_type!(DomainName); @@ -629,6 +684,38 @@ mod tests { "mappable", false )] + #[case::available_premium( + "tests/wpcom/domains/is_available/available-premium.json", + "luxury.com", + "com", + "available_premium", + "mappable", + true + )] + #[case::mapped_same_user( + "tests/wpcom/domains/is_available/mapped-same-user.json", + "myblog.com", + "com", + "mapped_to_other_site_same_user", + "mappable", + true + )] + #[case::sale_coupon( + "tests/wpcom/domains/is_available/sale-coupon.json", + "newsite.info", + "info", + "available", + "mappable", + true + )] + #[case::maintenance( + "tests/wpcom/domains/is_available/maintenance.json", + "mysite.example", + "example", + "tld_in_maintenance", + "mappable", + false + )] fn test_domain_availability_deserialization( #[case] json_file_path: &str, #[case] expected_domain: &str, @@ -711,6 +798,54 @@ mod tests { assert!(availability.renew_raw_price.is_none()); } + #[test] + fn test_domain_availability_premium_fields() { + let file = File::open("tests/wpcom/domains/is_available/available-premium.json") + .expect("Failed to open file"); + let availability: DomainAvailability = + serde_json::from_reader(file).expect("Unable to parse JSON"); + assert_eq!(availability.is_supported_premium_domain, Some(true)); + assert_eq!(availability.is_price_limit_exceeded, Some(false)); + assert_eq!(availability.raw_price, Some(Decimal2::from_hundredths(500000))); + } + + #[test] + fn test_domain_availability_mapped_same_user() { + let file = File::open("tests/wpcom/domains/is_available/mapped-same-user.json") + .expect("Failed to open file"); + let availability: DomainAvailability = + serde_json::from_reader(file).expect("Unable to parse JSON"); + assert_eq!( + availability.other_site_domain.as_deref(), + Some("myothersite.wordpress.com") + ); + assert_eq!(availability.transferrability.as_deref(), Some("transferrable")); + assert!(availability.product_id.is_none()); + } + + #[test] + fn test_domain_availability_sale_coupon() { + let file = File::open("tests/wpcom/domains/is_available/sale-coupon.json") + .expect("Failed to open file"); + let availability: DomainAvailability = + serde_json::from_reader(file).expect("Unable to parse JSON"); + assert_eq!(availability.sale_cost, Some(Decimal2::from_hundredths(700))); + assert_eq!(availability.raw_price, Some(Decimal2::from_hundredths(2000))); + } + + #[test] + fn test_domain_availability_maintenance() { + let file = File::open("tests/wpcom/domains/is_available/maintenance.json") + .expect("Failed to open file"); + let availability: DomainAvailability = + serde_json::from_reader(file).expect("Unable to parse JSON"); + assert_eq!( + availability.maintenance_end_time.as_deref(), + Some("2026-05-01 12:00:00") + ); + assert!(availability.product_id.is_none()); + } + #[test] fn test_domain_availability_hsts_policy_notices() { let file = File::open("tests/wpcom/domains/is_available/hsts-required.json") diff --git a/wp_api/tests/wpcom/domains/is_available/available-premium.json b/wp_api/tests/wpcom/domains/is_available/available-premium.json new file mode 100644 index 000000000..c3391df87 --- /dev/null +++ b/wp_api/tests/wpcom/domains/is_available/available-premium.json @@ -0,0 +1,20 @@ +{ + "domain_name": "luxury.com", + "tld": "com", + "status": "available_premium", + "mappable": "mappable", + "supports_privacy": true, + "root_domain_provider": "unknown", + "product_id": 6, + "product_slug": "domain_reg", + "cost": "$5,000.00", + "renew_cost": "$5,000.00", + "renew_raw_price": 5000, + "raw_price": 5000, + "currency_code": "USD", + "is_supported_premium_domain": true, + "is_price_limit_exceeded": false, + "match_reasons": ["exact-match", "tld-exact", "tld-common"], + "vendor": "availability", + "ownership_verification_type": "no_verification_required" +} diff --git a/wp_api/tests/wpcom/domains/is_available/maintenance.json b/wp_api/tests/wpcom/domains/is_available/maintenance.json new file mode 100644 index 000000000..499e6dd19 --- /dev/null +++ b/wp_api/tests/wpcom/domains/is_available/maintenance.json @@ -0,0 +1,9 @@ +{ + "domain_name": "mysite.example", + "tld": "example", + "status": "tld_in_maintenance", + "mappable": "mappable", + "supports_privacy": false, + "root_domain_provider": "unknown", + "maintenance_end_time": "2026-05-01 12:00:00" +} diff --git a/wp_api/tests/wpcom/domains/is_available/mapped-same-user.json b/wp_api/tests/wpcom/domains/is_available/mapped-same-user.json new file mode 100644 index 000000000..20b0bb72f --- /dev/null +++ b/wp_api/tests/wpcom/domains/is_available/mapped-same-user.json @@ -0,0 +1,11 @@ +{ + "domain_name": "myblog.com", + "tld": "com", + "status": "mapped_to_other_site_same_user", + "mappable": "mappable", + "supports_privacy": true, + "root_domain_provider": "unknown", + "other_site_domain": "myothersite.wordpress.com", + "transferrability": "transferrable", + "ownership_verification_type": "no_verification_required" +} diff --git a/wp_api/tests/wpcom/domains/is_available/sale-coupon.json b/wp_api/tests/wpcom/domains/is_available/sale-coupon.json new file mode 100644 index 000000000..2c0e94ed5 --- /dev/null +++ b/wp_api/tests/wpcom/domains/is_available/sale-coupon.json @@ -0,0 +1,19 @@ +{ + "domain_name": "newsite.info", + "tld": "info", + "status": "available", + "mappable": "mappable", + "supports_privacy": true, + "root_domain_provider": "unknown", + "product_id": 18, + "product_slug": "dotinfo_domain", + "cost": "$20.00", + "renew_cost": "$20.00", + "renew_raw_price": 20, + "raw_price": 20, + "currency_code": "USD", + "sale_cost": 7.00, + "match_reasons": ["exact-match", "tld-exact"], + "vendor": "availability", + "ownership_verification_type": "no_verification_required" +} From 6aceb93aae31ebdbeda4b48d2e04140ad750af3d Mon Sep 17 00:00:00 2001 From: Oguz Kocer Date: Wed, 22 Apr 2026 00:03:23 -0400 Subject: [PATCH 04/10] Add query params to domains is-available endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add DomainAvailabilityParams with blog_id (WpComSiteId), is_cart_pre_check, and vendor — matching the backend's accepted query parameters for the v1.3 is-available endpoint. --- wp_api/src/wp_com/domains.rs | 25 +++++++++++++- .../src/wp_com/endpoint/domains_endpoint.rs | 33 +++++++++++++++---- wp_com_e2e/src/domains_tests.rs | 14 +++++--- 3 files changed, 59 insertions(+), 13 deletions(-) diff --git a/wp_api/src/wp_com/domains.rs b/wp_api/src/wp_com/domains.rs index 54774e7be..b0e1c4f1c 100644 --- a/wp_api/src/wp_com/domains.rs +++ b/wp_api/src/wp_com/domains.rs @@ -2,7 +2,7 @@ use crate::{ decimal2::Decimal2, impl_as_query_value_for_new_type, url_query::{AppendUrlQueryPairs, QueryPairs, QueryPairsExtension}, - wp_com::segments::SegmentId, + wp_com::{WpComSiteId, segments::SegmentId}, }; use serde::{Deserialize, Serialize}; @@ -151,6 +151,29 @@ pub struct DomainPolicyNotice { pub message: String, } +/// Optional query parameters for `GET /domains/{name}/is-available/`. +#[derive(Debug, Default, Clone, PartialEq, Eq, uniffi::Record)] +pub struct DomainAvailabilityParams { + /// Site ID to check domain availability against. + #[uniffi(default = None)] + pub blog_id: Option, + /// Whether this is a pre-check before adding to cart. + #[uniffi(default = None)] + pub is_cart_pre_check: Option, + /// Vendor for the availability check (e.g. `"100-year-domains"`). + #[uniffi(default = None)] + pub vendor: Option, +} + +impl AppendUrlQueryPairs for DomainAvailabilityParams { + fn append_query_pairs(&self, query_pairs_mut: &mut QueryPairs) { + query_pairs_mut + .append_option_query_value_pair("blog_id", self.blog_id.as_ref()) + .append_option_query_value_pair("is_cart_pre_check", self.is_cart_pre_check.as_ref()) + .append_option_query_value_pair("vendor", self.vendor.as_ref()); + } +} + /// Response from `GET /domains/{name}/is-available/` (v1.3). /// /// Reports whether a domain name is available for registration or diff --git a/wp_api/src/wp_com/endpoint/domains_endpoint.rs b/wp_api/src/wp_com/endpoint/domains_endpoint.rs index e47a4f550..878407351 100644 --- a/wp_api/src/wp_com/endpoint/domains_endpoint.rs +++ b/wp_api/src/wp_com/endpoint/domains_endpoint.rs @@ -3,8 +3,8 @@ use crate::{ wp_com::{ WpComNamespace, domains::{ - CountryCode, DomainAvailability, DomainName, DomainSuggestion, - DomainSuggestionsParams, SupportedCountries, SupportedState, + CountryCode, DomainAvailability, DomainAvailabilityParams, DomainName, + DomainSuggestion, DomainSuggestionsParams, SupportedCountries, SupportedState, }, }, }; @@ -18,7 +18,7 @@ enum DomainsRequest { SupportedCountries, #[get(url = "/domains/supported-states/", output = Vec)] SupportedStates, - #[get(url = "/domains//is-available", output = DomainAvailability)] + #[get(url = "/domains//is-available", params = &DomainAvailabilityParams, output = DomainAvailability)] IsAvailable, } @@ -37,7 +37,8 @@ mod tests { use crate::{ request::endpoint::ApiUrlResolver, wp_com::{ - domains::{CountryCode, DomainName}, + domains::{CountryCode, DomainAvailabilityParams, DomainName}, + WpComSiteId, endpoint::tests::{ fixture_wp_com_api_url_resolver, validate_wp_com_rest_v1_1_endpoint, validate_wp_com_rest_v1_3_endpoint, @@ -113,14 +114,32 @@ mod tests { } #[rstest] - #[case::com(DomainName("example.com".to_string()), "/domains/example.com/is-available")] - #[case::org(DomainName("myblog.org".to_string()), "/domains/myblog.org/is-available")] + #[case::com(DomainName("example.com".to_string()), "/domains/example.com/is-available?")] + #[case::org(DomainName("myblog.org".to_string()), "/domains/myblog.org/is-available?")] fn is_available( endpoint: DomainsRequestEndpoint, #[case] domain_name: DomainName, #[case] expected_path: &str, ) { - validate_wp_com_rest_v1_3_endpoint(endpoint.is_available(&domain_name), expected_path); + validate_wp_com_rest_v1_3_endpoint( + endpoint.is_available(&domain_name, &DomainAvailabilityParams::default()), + expected_path, + ); + } + + #[rstest] + fn is_available_with_params(endpoint: DomainsRequestEndpoint) { + validate_wp_com_rest_v1_3_endpoint( + endpoint.is_available( + &DomainName("test.com".to_string()), + &DomainAvailabilityParams { + blog_id: Some(WpComSiteId(12345)), + is_cart_pre_check: Some(true), + vendor: Some("100-year-domains".to_string()), + }, + ), + "/domains/test.com/is-available?blog_id=12345&is_cart_pre_check=true&vendor=100-year-domains", + ); } fn base_domain_suggestions_params() -> DomainSuggestionsParams { diff --git a/wp_com_e2e/src/domains_tests.rs b/wp_com_e2e/src/domains_tests.rs index 3b8f51157..64437d9dd 100644 --- a/wp_com_e2e/src/domains_tests.rs +++ b/wp_com_e2e/src/domains_tests.rs @@ -1,7 +1,7 @@ use crate::context::TestContext; use libtest_mimic::Trial; use std::sync::Arc; -use wp_api::wp_com::domains::{CountryCode, DomainName}; +use wp_api::wp_com::domains::{CountryCode, DomainAvailabilityParams, DomainName}; pub fn tests(ctx: Arc) -> Vec { let mut trials = vec![]; @@ -90,7 +90,10 @@ pub fn tests(ctx: Arc) -> Vec { let availability = ctx .client .domains() - .is_available(&DomainName("google.com".to_string())) + .is_available( + &DomainName("google.com".to_string()), + &DomainAvailabilityParams::default(), + ) .await .map_err(|e| e.to_string())? .data; @@ -119,9 +122,10 @@ pub fn tests(ctx: Arc) -> Vec { let availability = ctx .client .domains() - .is_available(&DomainName( - "xyzzy-test-unlikely-taken-2025.com".to_string(), - )) + .is_available( + &DomainName("xyzzy-test-unlikely-taken-2025.com".to_string()), + &DomainAvailabilityParams::default(), + ) .await .map_err(|e| e.to_string())? .data; From ac0381c1dcb8f7247f54172e33f7151eecd1ef6f Mon Sep 17 00:00:00 2001 From: Oguz Kocer Date: Wed, 22 Apr 2026 00:57:17 -0400 Subject: [PATCH 05/10] Replace hand-crafted sale-coupon fixture with anonymized real data Also add dot-gay-notice fixture from real API response with is_dot_gay_notice_required and gay_accept_requirements policy notice. --- wp_api/src/wp_com/domains.rs | 30 ++++++++++++++++--- .../domains/is_available/dot-gay-notice.json | 17 +++++++++++ .../domains/is_available/sale-coupon.json | 23 +++++++------- 3 files changed, 56 insertions(+), 14 deletions(-) create mode 100644 wp_api/tests/wpcom/domains/is_available/dot-gay-notice.json diff --git a/wp_api/src/wp_com/domains.rs b/wp_api/src/wp_com/domains.rs index b0e1c4f1c..716f6fff0 100644 --- a/wp_api/src/wp_com/domains.rs +++ b/wp_api/src/wp_com/domains.rs @@ -725,8 +725,8 @@ mod tests { )] #[case::sale_coupon( "tests/wpcom/domains/is_available/sale-coupon.json", - "newsite.info", - "info", + "freshblog2025.online", + "online", "available", "mappable", true @@ -739,6 +739,14 @@ mod tests { "mappable", false )] + #[case::dot_gay_notice( + "tests/wpcom/domains/is_available/dot-gay-notice.json", + "testsite2025.gay", + "gay", + "mappable", + "mappable", + true + )] fn test_domain_availability_deserialization( #[case] json_file_path: &str, #[case] expected_domain: &str, @@ -852,8 +860,22 @@ mod tests { .expect("Failed to open file"); let availability: DomainAvailability = serde_json::from_reader(file).expect("Unable to parse JSON"); - assert_eq!(availability.sale_cost, Some(Decimal2::from_hundredths(700))); - assert_eq!(availability.raw_price, Some(Decimal2::from_hundredths(2000))); + assert_eq!(availability.sale_cost, Some(Decimal2::from_hundredths(1000))); + assert_eq!(availability.raw_price, Some(Decimal2::from_hundredths(2500))); + } + + #[test] + fn test_domain_availability_dot_gay_notice() { + let file = File::open("tests/wpcom/domains/is_available/dot-gay-notice.json") + .expect("Failed to open file"); + let availability: DomainAvailability = + serde_json::from_reader(file).expect("Unable to parse JSON"); + assert_eq!(availability.is_dot_gay_notice_required, Some(true)); + assert_eq!(availability.policy_notices.len(), 1); + assert_eq!( + availability.policy_notices[0].notice_type, + "gay_accept_requirements" + ); } #[test] diff --git a/wp_api/tests/wpcom/domains/is_available/dot-gay-notice.json b/wp_api/tests/wpcom/domains/is_available/dot-gay-notice.json new file mode 100644 index 000000000..ba91ab8ac --- /dev/null +++ b/wp_api/tests/wpcom/domains/is_available/dot-gay-notice.json @@ -0,0 +1,17 @@ +{ + "domain_name": "testsite2025.gay", + "tld": "gay", + "status": "mappable", + "mappable": "mappable", + "supports_privacy": true, + "root_domain_provider": "unknown", + "is_dot_gay_notice_required": true, + "ownership_verification_type": "no_verification_required", + "policy_notices": [ + { + "type": "gay_accept_requirements", + "label": "Special requirements", + "message": "Any anti-LGBTQ content is prohibited and can result in registration termination. The registry will donate 20% of all registration revenue to LGBTQ non-profit organizations." + } + ] +} diff --git a/wp_api/tests/wpcom/domains/is_available/sale-coupon.json b/wp_api/tests/wpcom/domains/is_available/sale-coupon.json index 2c0e94ed5..505ae92c1 100644 --- a/wp_api/tests/wpcom/domains/is_available/sale-coupon.json +++ b/wp_api/tests/wpcom/domains/is_available/sale-coupon.json @@ -1,19 +1,22 @@ { - "domain_name": "newsite.info", - "tld": "info", + "domain_name": "freshblog2025.online", + "tld": "online", "status": "available", "mappable": "mappable", "supports_privacy": true, "root_domain_provider": "unknown", - "product_id": 18, - "product_slug": "dotinfo_domain", - "cost": "$20.00", - "renew_cost": "$20.00", - "renew_raw_price": 20, - "raw_price": 20, + "product_id": 271, + "product_slug": "dotonline_domain", + "cost": "$25.00", + "renew_cost": "$25.00", + "renew_raw_price": 25, + "raw_price": 25, "currency_code": "USD", - "sale_cost": 7.00, - "match_reasons": ["exact-match", "tld-exact"], + "sale_cost": 10.00, + "match_reasons": [ + "exact-match", + "tld-exact" + ], "vendor": "availability", "ownership_verification_type": "no_verification_required" } From a6e3d3230ec845706ba16cb17d0b296a5783a07f Mon Sep 17 00:00:00 2001 From: Oguz Kocer Date: Wed, 22 Apr 2026 18:04:53 -0400 Subject: [PATCH 06/10] Cargo fmt --- wp_api/src/wp_com/domains.rs | 30 +++++++++++++++---- .../src/wp_com/endpoint/domains_endpoint.rs | 2 +- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/wp_api/src/wp_com/domains.rs b/wp_api/src/wp_com/domains.rs index 716f6fff0..51b15a14b 100644 --- a/wp_api/src/wp_com/domains.rs +++ b/wp_api/src/wp_com/domains.rs @@ -775,7 +775,10 @@ mod tests { assert_eq!(availability.product_slug.as_deref(), Some("domain_reg")); assert_eq!(availability.cost.as_deref(), Some("$18.00")); assert_eq!(availability.renew_cost.as_deref(), Some("$18.00")); - assert_eq!(availability.raw_price, Some(Decimal2::from_hundredths(1800))); + assert_eq!( + availability.raw_price, + Some(Decimal2::from_hundredths(1800)) + ); assert_eq!( availability.renew_raw_price, Some(Decimal2::from_hundredths(1800)) @@ -823,7 +826,10 @@ mod tests { Some("domain_transfer") ); assert_eq!(availability.cost.as_deref(), Some("$48.00")); - assert_eq!(availability.raw_price, Some(Decimal2::from_hundredths(4800))); + assert_eq!( + availability.raw_price, + Some(Decimal2::from_hundredths(4800)) + ); // Transferrable domains don't include renewal pricing. assert!(availability.renew_cost.is_none()); assert!(availability.renew_raw_price.is_none()); @@ -837,7 +843,10 @@ mod tests { serde_json::from_reader(file).expect("Unable to parse JSON"); assert_eq!(availability.is_supported_premium_domain, Some(true)); assert_eq!(availability.is_price_limit_exceeded, Some(false)); - assert_eq!(availability.raw_price, Some(Decimal2::from_hundredths(500000))); + assert_eq!( + availability.raw_price, + Some(Decimal2::from_hundredths(500000)) + ); } #[test] @@ -850,7 +859,10 @@ mod tests { availability.other_site_domain.as_deref(), Some("myothersite.wordpress.com") ); - assert_eq!(availability.transferrability.as_deref(), Some("transferrable")); + assert_eq!( + availability.transferrability.as_deref(), + Some("transferrable") + ); assert!(availability.product_id.is_none()); } @@ -860,8 +872,14 @@ mod tests { .expect("Failed to open file"); let availability: DomainAvailability = serde_json::from_reader(file).expect("Unable to parse JSON"); - assert_eq!(availability.sale_cost, Some(Decimal2::from_hundredths(1000))); - assert_eq!(availability.raw_price, Some(Decimal2::from_hundredths(2500))); + assert_eq!( + availability.sale_cost, + Some(Decimal2::from_hundredths(1000)) + ); + assert_eq!( + availability.raw_price, + Some(Decimal2::from_hundredths(2500)) + ); } #[test] diff --git a/wp_api/src/wp_com/endpoint/domains_endpoint.rs b/wp_api/src/wp_com/endpoint/domains_endpoint.rs index 878407351..f69f404db 100644 --- a/wp_api/src/wp_com/endpoint/domains_endpoint.rs +++ b/wp_api/src/wp_com/endpoint/domains_endpoint.rs @@ -37,8 +37,8 @@ mod tests { use crate::{ request::endpoint::ApiUrlResolver, wp_com::{ - domains::{CountryCode, DomainAvailabilityParams, DomainName}, WpComSiteId, + domains::{CountryCode, DomainAvailabilityParams, DomainName}, endpoint::tests::{ fixture_wp_com_api_url_resolver, validate_wp_com_rest_v1_1_endpoint, validate_wp_com_rest_v1_3_endpoint, From 0ab2e47a0c06cfeae4bdc615aefa3cd37c428f87 Mon Sep 17 00:00:00 2001 From: Oguz Kocer Date: Wed, 22 Apr 2026 21:00:22 -0400 Subject: [PATCH 07/10] Add DomainAvailabilityStatus enum and clean up DomainAvailability - Replace stringly-typed status field with DomainAvailabilityStatus enum covering all known backend values, with Other(String) fallback. - Remove unnecessary deserialize_json_value_as_string helper and trademark_claims_notice_info field (rare TMCH case, complex opaque structure not worth modeling). - Remove redundant #[serde(default)] on Option fields. --- wp_api/src/wp_com/domains.rs | 160 ++++++++++++++++---------------- wp_com_e2e/src/domains_tests.rs | 10 +- 2 files changed, 88 insertions(+), 82 deletions(-) diff --git a/wp_api/src/wp_com/domains.rs b/wp_api/src/wp_com/domains.rs index 51b15a14b..adf2465e2 100644 --- a/wp_api/src/wp_com/domains.rs +++ b/wp_api/src/wp_com/domains.rs @@ -174,6 +174,75 @@ impl AppendUrlQueryPairs for DomainAvailabilityParams { } } +/// Availability status for a domain checked via `GET /domains/{name}/is-available/`. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, uniffi::Enum)] +#[serde(rename_all = "snake_case")] +pub enum DomainAvailabilityStatus { + /// Domain is available for registration. + Available, + /// Premium domain available at a higher price. + AvailablePremium, + /// Available but reserved. + AvailableReserved, + /// Available but not registrable by current user. + AvailableButNotRegistrable, + /// Domain can be transferred in. + Transferrable, + /// Premium domain can be transferred in. + TransferrablePremium, + /// Registered on the same site being checked. + RegisteredOnSameSite, + /// Registered by the same user on a different site. + RegisteredOnOtherSiteSameUser, + /// Registered by a different user. + RegisteredDomain, + /// Mapped to same site, can still be registered. + MappedToSameSiteRegistrable, + /// Mapped to same site, can be transferred. + MappedToSameSiteTransferrable, + /// Mapped to same site, cannot be transferred. + MappedToSameSiteNotTransferrable, + /// Mapped to another site by the same user, registrable. + MappedToOtherSiteSameUserRegistrable, + /// Mapped to another site by the same user. + MappedToOtherSiteSameUser, + /// Mapped by a different user. + MappedDomain, + /// Transfer in progress by the same user. + TransferPendingSameUser, + /// Transfer in progress by a different user. + TransferPending, + /// Domain in redemption period. + InRedemption, + /// TLD is not supported for registration. + TldNotSupported, + /// TLD is currently in maintenance. + TldInMaintenance, + /// Domain registration is temporarily disabled. + DomainRegistrationUnavailable, + /// 60-day transfer lock active. + RecentRegistrationLockNotTransferrable, + /// EPP status prevents transfer. + ServerTransferProhibitedNotTransferrable, + /// Domain is blacklisted. + BlacklistedDomain, + /// Domain is restricted. + RestrictedDomain, + /// WordPress.com managed subdomain (.blog, .link, etc.). + DotblogSubdomain, + /// 100-year domain vendor but TLD not supported. + HundredYearDomainTldRestriction, + /// CNAME conflict prevents mapping. + ConflictingCnameExists, + /// Domain can be mapped (mappable-only status). + Mappable, + /// Unknown status. + Unknown, + /// A status not covered by the known variants. + #[serde(untagged)] + Other(String), +} + /// Response from `GET /domains/{name}/is-available/` (v1.3). /// /// Reports whether a domain name is available for registration or @@ -182,23 +251,14 @@ impl AppendUrlQueryPairs for DomainAvailabilityParams { /// The set of fields present varies by `status`: available domains /// include full pricing, transferrable domains include partial /// pricing, and unavailable domains have only the core fields. -/// -/// Common `status` values: `"available"`, `"available_premium"`, -/// `"transferrable"`, `"transferrable_premium"`, -/// `"tld_not_supported"`, `"tld_in_maintenance"`, -/// `"blacklisted_domain"`, `"mapped_domain"`, -/// `"registered_domain"`, `"registered_on_other_site_same_user"`, -/// `"mapped_to_other_site_same_user"`, -/// `"recent_registration_lock_not_transferrable"`, -/// `"dotblog_subdomain"`, `"unknown"`. #[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)] pub struct DomainAvailability { /// The domain name that was checked. pub domain_name: String, /// The TLD portion of the domain (e.g. `"com"`, `"io"`, `"dev"`). pub tld: String, - /// Availability status — see struct-level docs for known values. - pub status: String, + /// Availability status. + pub status: DomainAvailabilityStatus, /// Whether the domain can be mapped to a WordPress.com site /// (e.g. `"mappable"`, `"blacklisted_domain"`). pub mappable: String, @@ -219,22 +279,14 @@ pub struct DomainAvailability { /// Formatted renewal cost. pub renew_cost: Option, /// Raw numeric renewal price in `currency_code`. - #[serde(default)] - #[uniffi(default = None)] pub renew_raw_price: Option, /// Raw numeric registration/transfer price in `currency_code`. - #[serde(default)] - #[uniffi(default = None)] pub raw_price: Option, /// ISO 4217 currency code (e.g. `"USD"`, `"TRY"`). pub currency_code: Option, /// Discounted sale price when a coupon applies. - #[serde(default)] - #[uniffi(default = None)] pub sale_cost: Option, /// `true` if a premium domain exceeds the price limit. - #[serde(default)] - #[uniffi(default = None)] pub is_price_limit_exceeded: Option, // -- Match/vendor fields (available domains only) -- /// Reasons the domain matched (e.g. `"exact-match"`, @@ -243,8 +295,6 @@ pub struct DomainAvailability { /// The registry vendor (e.g. `"availability"`). pub vendor: Option, /// `true` for status `"available_premium"`. - #[serde(default)] - #[uniffi(default = None)] pub is_supported_premium_domain: Option, // -- Mapping/transfer fields -- /// Type of ownership verification required (e.g. @@ -260,12 +310,8 @@ pub struct DomainAvailability { /// `true` if the TLD requires HSTS (e.g. `.dev`). pub hsts_required: Option, /// `true` if the `.gay` TLD policy notice is required. - #[serde(default)] - #[uniffi(default = None)] pub is_dot_gay_notice_required: Option, /// `true` if premium domain transfers are unsupported for this TLD. - #[serde(default)] - #[uniffi(default = None)] pub cannot_transfer_due_to_unsupported_premium_tld: Option, /// Policy notices attached to the domain (e.g. HSTS warnings). #[serde(default)] @@ -274,24 +320,6 @@ pub struct DomainAvailability { // -- Maintenance -- /// When domain registration or TLD is in maintenance, the end time. pub maintenance_end_time: Option, - /// TMCH (Trademark Claims) notice info as a raw JSON string, - /// if applicable. Complex XML-derived structure that varies by TLD. - #[serde( - default, - deserialize_with = "crate::wp_com::domains::deserialize_json_value_as_string", - skip_serializing_if = "Option::is_none" - )] - #[uniffi(default = None)] - pub trademark_claims_notice_info: Option, -} - -/// Deserializes an arbitrary JSON value into its stringified form. -fn deserialize_json_value_as_string<'de, D>(deserializer: D) -> Result, D::Error> -where - D: serde::Deserializer<'de>, -{ - let value: Option = Option::deserialize(deserializer)?; - Ok(value.map(|v| v.to_string())) } impl_as_query_value_for_new_type!(DomainName); @@ -670,98 +698,74 @@ mod tests { #[case::available( "tests/wpcom/domains/is_available/available.json", "freshsite2025.com", - "com", - "available", - "mappable", + DomainAvailabilityStatus::Available, true )] #[case::blacklisted( "tests/wpcom/domains/is_available/not-available.json", "example.com", - "com", - "blacklisted_domain", - "blacklisted_domain", + DomainAvailabilityStatus::BlacklistedDomain, true )] #[case::transferrable( "tests/wpcom/domains/is_available/transferrable.json", "taken-domain.io", - "io", - "transferrable", - "mappable", + DomainAvailabilityStatus::Transferrable, true )] #[case::tld_not_supported( "tests/wpcom/domains/is_available/tld-not-supported.json", "mysite.ai", - "ai", - "tld_not_supported", - "mappable", + DomainAvailabilityStatus::TldNotSupported, false )] #[case::hsts_required( "tests/wpcom/domains/is_available/hsts-required.json", "myproject.dev", - "dev", - "recent_registration_lock_not_transferrable", - "mappable", + DomainAvailabilityStatus::RecentRegistrationLockNotTransferrable, false )] #[case::available_premium( "tests/wpcom/domains/is_available/available-premium.json", "luxury.com", - "com", - "available_premium", - "mappable", + DomainAvailabilityStatus::AvailablePremium, true )] #[case::mapped_same_user( "tests/wpcom/domains/is_available/mapped-same-user.json", "myblog.com", - "com", - "mapped_to_other_site_same_user", - "mappable", + DomainAvailabilityStatus::MappedToOtherSiteSameUser, true )] #[case::sale_coupon( "tests/wpcom/domains/is_available/sale-coupon.json", "freshblog2025.online", - "online", - "available", - "mappable", + DomainAvailabilityStatus::Available, true )] #[case::maintenance( "tests/wpcom/domains/is_available/maintenance.json", "mysite.example", - "example", - "tld_in_maintenance", - "mappable", + DomainAvailabilityStatus::TldInMaintenance, false )] #[case::dot_gay_notice( "tests/wpcom/domains/is_available/dot-gay-notice.json", "testsite2025.gay", - "gay", - "mappable", - "mappable", + DomainAvailabilityStatus::Mappable, true )] fn test_domain_availability_deserialization( #[case] json_file_path: &str, #[case] expected_domain: &str, - #[case] expected_tld: &str, - #[case] expected_status: &str, - #[case] expected_mappable: &str, + #[case] expected_status: DomainAvailabilityStatus, #[case] expected_privacy: bool, ) { let file = File::open(json_file_path).expect("Failed to open file"); let availability: DomainAvailability = serde_json::from_reader(file).expect("Unable to parse JSON"); assert_eq!(availability.domain_name, expected_domain); - assert_eq!(availability.tld, expected_tld); assert_eq!(availability.status, expected_status); - assert_eq!(availability.mappable, expected_mappable); assert_eq!(availability.supports_privacy, expected_privacy); } diff --git a/wp_com_e2e/src/domains_tests.rs b/wp_com_e2e/src/domains_tests.rs index 64437d9dd..991dd290a 100644 --- a/wp_com_e2e/src/domains_tests.rs +++ b/wp_com_e2e/src/domains_tests.rs @@ -1,7 +1,9 @@ use crate::context::TestContext; use libtest_mimic::Trial; use std::sync::Arc; -use wp_api::wp_com::domains::{CountryCode, DomainAvailabilityParams, DomainName}; +use wp_api::wp_com::domains::{ + CountryCode, DomainAvailabilityParams, DomainAvailabilityStatus, DomainName, +}; pub fn tests(ctx: Arc) -> Vec { let mut trials = vec![]; @@ -106,7 +108,7 @@ pub fn tests(ctx: Arc) -> Vec { .into()); } - if availability.status == "available" { + if availability.status == DomainAvailabilityStatus::Available { return Err("expected google.com to not be available".into()); } @@ -130,9 +132,9 @@ pub fn tests(ctx: Arc) -> Vec { .map_err(|e| e.to_string())? .data; - if availability.status != "available" { + if availability.status != DomainAvailabilityStatus::Available { return Err(format!( - "expected status 'available', got '{}'", + "expected status Available, got {:?}", availability.status ) .into()); From 08db11858c1b2028614e1d12aeb278b92d450f11 Mon Sep 17 00:00:00 2001 From: Oguz Kocer Date: Wed, 22 Apr 2026 21:30:33 -0400 Subject: [PATCH 08/10] Add CurrencyCode newtype for ISO 4217 currency codes Define CurrencyCode in wp_com module (alongside WpComSiteId) and use it for currency_code fields in PaidDomainSuggestion and DomainAvailability. --- wp_api/src/wp_com/domains.rs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/wp_api/src/wp_com/domains.rs b/wp_api/src/wp_com/domains.rs index adf2465e2..a95dd72fd 100644 --- a/wp_api/src/wp_com/domains.rs +++ b/wp_api/src/wp_com/domains.rs @@ -2,7 +2,7 @@ use crate::{ decimal2::Decimal2, impl_as_query_value_for_new_type, url_query::{AppendUrlQueryPairs, QueryPairs, QueryPairsExtension}, - wp_com::{WpComSiteId, segments::SegmentId}, + wp_com::{CurrencyCode, WpComSiteId, segments::SegmentId}, }; use serde::{Deserialize, Serialize}; @@ -127,8 +127,8 @@ pub struct PaidDomainSuggestion { pub renew_raw_price: Decimal2, /// Raw numeric registration price in `currency_code`. pub raw_price: Decimal2, - /// ISO 4217 currency code for `raw_price`/`renew_raw_price` (e.g. `"USD"`). - pub currency_code: String, + /// ISO 4217 currency code for `raw_price`/`renew_raw_price`. + pub currency_code: CurrencyCode, /// Promotional sale price in `currency_code`, if the domain is on sale. pub sale_cost: Option, /// `true` if the TLD requires HSTS (e.g. `.dev`). @@ -282,8 +282,8 @@ pub struct DomainAvailability { pub renew_raw_price: Option, /// Raw numeric registration/transfer price in `currency_code`. pub raw_price: Option, - /// ISO 4217 currency code (e.g. `"USD"`, `"TRY"`). - pub currency_code: Option, + /// ISO 4217 currency code. + pub currency_code: Option, /// Discounted sale price when a coupon applies. pub sale_cost: Option, /// `true` if a premium domain exceeds the price limit. @@ -520,7 +520,7 @@ mod tests { assert_eq!(first.renew_cost, "$18.00"); assert_eq!(first.renew_raw_price.hundredths(), 1800); assert_eq!(first.raw_price.hundredths(), 1800); - assert_eq!(first.currency_code, "USD"); + assert_eq!(first.currency_code, CurrencyCode("USD".to_string())); assert!(first.sale_cost.is_none()); assert!(first.hsts_required.is_none()); assert!(first.policy_notices.is_empty()); @@ -787,7 +787,10 @@ mod tests { availability.renew_raw_price, Some(Decimal2::from_hundredths(1800)) ); - assert_eq!(availability.currency_code.as_deref(), Some("USD")); + assert_eq!( + availability.currency_code, + Some(CurrencyCode("USD".to_string())) + ); assert_eq!( availability.match_reasons.as_deref(), Some( From 365e9084ac54cd74af593b07a88fde1f8b59c3b3 Mon Sep 17 00:00:00 2001 From: Oguz Kocer Date: Thu, 23 Apr 2026 00:51:14 -0400 Subject: [PATCH 09/10] Trim DomainAvailabilityStatus to client-relevant variants --- wp_api/src/wp_com/domains.rs | 50 +++--------------------------------- 1 file changed, 4 insertions(+), 46 deletions(-) diff --git a/wp_api/src/wp_com/domains.rs b/wp_api/src/wp_com/domains.rs index a95dd72fd..fffff8dab 100644 --- a/wp_api/src/wp_com/domains.rs +++ b/wp_api/src/wp_com/domains.rs @@ -182,62 +182,20 @@ pub enum DomainAvailabilityStatus { Available, /// Premium domain available at a higher price. AvailablePremium, - /// Available but reserved. - AvailableReserved, - /// Available but not registrable by current user. - AvailableButNotRegistrable, /// Domain can be transferred in. Transferrable, /// Premium domain can be transferred in. TransferrablePremium, - /// Registered on the same site being checked. - RegisteredOnSameSite, - /// Registered by the same user on a different site. + /// Already registered by the same user on a different site. RegisteredOnOtherSiteSameUser, - /// Registered by a different user. - RegisteredDomain, - /// Mapped to same site, can still be registered. - MappedToSameSiteRegistrable, - /// Mapped to same site, can be transferred. - MappedToSameSiteTransferrable, - /// Mapped to same site, cannot be transferred. - MappedToSameSiteNotTransferrable, - /// Mapped to another site by the same user, registrable. - MappedToOtherSiteSameUserRegistrable, - /// Mapped to another site by the same user. + /// Already mapped to another site by the same user. MappedToOtherSiteSameUser, - /// Mapped by a different user. - MappedDomain, - /// Transfer in progress by the same user. - TransferPendingSameUser, - /// Transfer in progress by a different user. - TransferPending, - /// Domain in redemption period. - InRedemption, /// TLD is not supported for registration. TldNotSupported, /// TLD is currently in maintenance. TldInMaintenance, - /// Domain registration is temporarily disabled. - DomainRegistrationUnavailable, - /// 60-day transfer lock active. - RecentRegistrationLockNotTransferrable, - /// EPP status prevents transfer. - ServerTransferProhibitedNotTransferrable, /// Domain is blacklisted. BlacklistedDomain, - /// Domain is restricted. - RestrictedDomain, - /// WordPress.com managed subdomain (.blog, .link, etc.). - DotblogSubdomain, - /// 100-year domain vendor but TLD not supported. - HundredYearDomainTldRestriction, - /// CNAME conflict prevents mapping. - ConflictingCnameExists, - /// Domain can be mapped (mappable-only status). - Mappable, - /// Unknown status. - Unknown, /// A status not covered by the known variants. #[serde(untagged)] Other(String), @@ -722,7 +680,7 @@ mod tests { #[case::hsts_required( "tests/wpcom/domains/is_available/hsts-required.json", "myproject.dev", - DomainAvailabilityStatus::RecentRegistrationLockNotTransferrable, + DomainAvailabilityStatus::Other("recent_registration_lock_not_transferrable".to_string()), false )] #[case::available_premium( @@ -752,7 +710,7 @@ mod tests { #[case::dot_gay_notice( "tests/wpcom/domains/is_available/dot-gay-notice.json", "testsite2025.gay", - DomainAvailabilityStatus::Mappable, + DomainAvailabilityStatus::Other("mappable".to_string()), true )] fn test_domain_availability_deserialization( From 52feb0f8d8cb33c88155744208485dc952725821 Mon Sep 17 00:00:00 2001 From: Oguz Kocer Date: Fri, 8 May 2026 01:27:10 -0400 Subject: [PATCH 10/10] Use `ProductId` newtype for domain `product_id` fields and add changelog --- CHANGELOG.md | 4 ++++ wp_api/src/wp_com/domains.rs | 12 ++++++------ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index caec18c75..4ca590e47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- WordPress.com `/domains/{name}/is-available` endpoint for checking domain availability, pricing, and transfer status + ## [0.2.0] - 2026-05-05 ### Added diff --git a/wp_api/src/wp_com/domains.rs b/wp_api/src/wp_com/domains.rs index fffff8dab..0238ad334 100644 --- a/wp_api/src/wp_com/domains.rs +++ b/wp_api/src/wp_com/domains.rs @@ -2,7 +2,7 @@ use crate::{ decimal2::Decimal2, impl_as_query_value_for_new_type, url_query::{AppendUrlQueryPairs, QueryPairs, QueryPairsExtension}, - wp_com::{CurrencyCode, WpComSiteId, segments::SegmentId}, + wp_com::{CurrencyCode, WpComSiteId, products::ProductId, segments::SegmentId}, }; use serde::{Deserialize, Serialize}; @@ -116,7 +116,7 @@ pub struct PaidDomainSuggestion { /// Whether the domain supports multi-year registrations. pub multi_year_reg_allowed: bool, /// WordPress.com product ID used to purchase this domain. - pub product_id: u64, + pub product_id: ProductId, /// WordPress.com product slug used to purchase this domain. pub product_slug: String, /// Formatted registration cost (e.g. `"$18.00"`). @@ -228,7 +228,7 @@ pub struct DomainAvailability { pub root_domain_provider: String, // -- Product/pricing fields (conditional on status) -- /// WordPress.com product ID for purchasing this domain. - pub product_id: Option, + pub product_id: Option, /// WordPress.com product slug (e.g. `"domain_reg"`, /// `"domain_transfer"`). pub product_slug: Option, @@ -472,7 +472,7 @@ mod tests { ); assert_eq!(first.max_reg_years, 10); assert!(first.multi_year_reg_allowed); - assert_eq!(first.product_id, 6); + assert_eq!(first.product_id, ProductId(6)); assert_eq!(first.product_slug, "domain_reg"); assert_eq!(first.cost, "$18.00"); assert_eq!(first.renew_cost, "$18.00"); @@ -733,7 +733,7 @@ mod tests { .expect("Failed to open file"); let availability: DomainAvailability = serde_json::from_reader(file).expect("Unable to parse JSON"); - assert_eq!(availability.product_id, Some(6)); + assert_eq!(availability.product_id, Some(ProductId(6))); assert_eq!(availability.product_slug.as_deref(), Some("domain_reg")); assert_eq!(availability.cost.as_deref(), Some("$18.00")); assert_eq!(availability.renew_cost.as_deref(), Some("$18.00")); @@ -785,7 +785,7 @@ mod tests { .expect("Failed to open file"); let availability: DomainAvailability = serde_json::from_reader(file).expect("Unable to parse JSON"); - assert_eq!(availability.product_id, Some(1337)); + assert_eq!(availability.product_id, Some(ProductId(1337))); assert_eq!( availability.product_slug.as_deref(), Some("domain_transfer")