diff --git a/src/dnsimple.rs b/src/dnsimple.rs index 2420ede..f962851 100644 --- a/src/dnsimple.rs +++ b/src/dnsimple.rs @@ -33,6 +33,7 @@ pub mod oauth; pub mod registrar; pub mod registrar_auto_renewal; pub mod registrar_name_servers; +pub mod registrar_registrant_changes; pub mod registrar_whois_privacy; pub mod services; pub mod templates; @@ -442,6 +443,19 @@ impl Client { let status = resp.status(); + // if the response is empty, we return empty data + if resp.status() == 204 { + return Ok(DNSimpleResponse { + rate_limit, + rate_limit_remaining, + rate_limit_reset, + status, + data: None, + pagination: None, + body: None, + }); + } + let json = resp .into_json::() .map_err(|e| DNSimpleError::Deserialization(e.to_string()))?; diff --git a/src/dnsimple/registrar_registrant_changes.rs b/src/dnsimple/registrar_registrant_changes.rs new file mode 100644 index 0000000..04c8c0c --- /dev/null +++ b/src/dnsimple/registrar_registrant_changes.rs @@ -0,0 +1,193 @@ +use crate::dnsimple::registrar::Registrar; +use crate::dnsimple::tlds::TldExtendedAttribute; +use crate::dnsimple::{DNSimpleResponse, Endpoint, RequestOptions}; +use crate::errors::DNSimpleError; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +/// Represents the contact change data +#[derive(Debug, Deserialize)] +pub struct RegistrantChange { + /// The contact change id in DNSimple + pub id: u64, + /// The associated account ID. + pub account_id: u64, + /// The associated contact ID. + pub contact_id: u64, + /// The associated domain ID. + pub domain_id: u64, + /// The registrant change state. + pub state: String, + /// The extended attributes. + pub extended_attributes: Option>, + /// True if the registrant change is a registry owner change. + pub registry_owner_change: bool, + /// When the Inter-Registrar Transfer lock (60 days) is going to be lifted. + pub irt_lock_lifted_by: Option, + /// When the registrant change was created in DNSimple. + pub created_at: String, + /// When the registrant change was last updated in DNSimple. + pub updated_at: String, +} + +/// Represents the contact change check data +#[derive(Debug, Deserialize)] +pub struct RegistrantChangeCheck { + /// The associated contact ID. + pub contact_id: u64, + /// The associated domain ID. + pub domain_id: u64, + /// The extended attributes. + pub extended_attributes: Option>, + /// True if the registrant change is a registry owner change. + pub registry_owner_change: bool, +} + +/// Payload used to check the requirements for a contact change +#[derive(Debug, Deserialize, Serialize)] +pub struct RegistrantChangePayload { + /// The associated domain ID. + pub domain_id: u64, + /// The associated registrant (contact) ID. + pub contact_id: u64, + // The extended attributes. + #[serde(skip_serializing_if = "Option::is_none")] + pub extended_attributes: Option>, +} + +/// Payload used to check the requirements for a contact change +#[derive(Debug, Deserialize, Serialize)] +pub struct RegistrantChangeCheckPayload { + /// The associated domain ID. + pub domain_id: u64, + /// The associated registrant (contact) ID. + pub contact_id: u64, +} + +struct RegistrantChangeEndpoint; + +impl Endpoint for RegistrantChangeEndpoint { + type Output = RegistrantChange; +} + +struct DeleteRegistrantChangeEndpoint; + +impl Endpoint for DeleteRegistrantChangeEndpoint { + type Output = Option; +} + +struct RegistrantChangesEndpoint; + +impl Endpoint for RegistrantChangesEndpoint { + type Output = Vec; +} + +struct RegistrantChangeCheckEndpoint; + +impl Endpoint for RegistrantChangeCheckEndpoint { + type Output = RegistrantChangeCheck; +} + +impl Registrar<'_> { + /// Retrieve the domain contact change + /// + /// # Arguments + /// + /// `account_id`: The account ID + /// `registrant_change_id`: The contact change ID + pub fn get_registrant_change( + &self, + account_id: u64, + registrant_change_id: u64, + ) -> Result, DNSimpleError> { + let path = format!( + "/{}/registrar/registrant_changes/{}", + account_id, registrant_change_id + ); + + self.client.get::(&path, None) + } + + /// Retrieves the requirements of a registrant change + /// + /// # Arguments + /// + /// `account_id`: The account ID + /// `payload`: The `RegistrantChangeCheckPayload` with the information needed to check the + /// requirements for a registrant change + pub fn check_registrant_change( + &self, + account_id: u64, + payload: RegistrantChangeCheckPayload, + ) -> Result, DNSimpleError> { + let path = format!("/{}/registrar/registrant_changes/check", account_id); + + match serde_json::to_value(payload) { + Ok(json) => self + .client + .post::(&path, json), + Err(_) => Err(DNSimpleError::Deserialization(String::from( + "Cannot deserialize json payload", + ))), + } + } + + /// Start registrant change. + /// + /// # Arguments + /// + /// `account_id`: The account ID + /// `payload`: The `RegistrantChangePayload` with the information needed to start a registrant change + pub fn create_registrant_change( + &self, + account_id: u64, + payload: RegistrantChangePayload, + ) -> Result, DNSimpleError> { + let path = format!("/{}/registrar/registrant_changes", account_id); + + match serde_json::to_value(payload) { + Ok(json) => self.client.post::(&path, json), + Err(_) => Err(DNSimpleError::Deserialization(String::from( + "Cannot deserialize json payload", + ))), + } + } + + /// List registrant changes in the account. + /// + /// # Arguments + /// + /// `account_id`: The account ID + /// `options`: The `RequestOptions` + /// - Filters: `domain_id`, `state`, `contact_id` + /// - Sorting: `id` + pub fn list_registrant_changes( + &self, + account_id: u64, + options: Option, + ) -> Result>, DNSimpleError> { + let path = format!("/{}/registrar/registrant_changes", account_id); + + self.client.get::(&path, options) + } + + /// Cancel a registrant change. + /// + /// # Arguments + /// + /// `account_id`: The account ID + /// `registrant_change_id`: The contact change ID + pub fn delete_registrant_change( + &self, + account_id: u64, + registrant_change_id: u64, + ) -> Result>, DNSimpleError> { + let path = format!( + "/{}/registrar/registrant_changes/{}", + account_id, registrant_change_id + ); + + self.client + .delete_with_response::(&path) + } +} diff --git a/tests/fixtures/v2/api/checkRegistrantChange/error-contactnotfound.http b/tests/fixtures/v2/api/checkRegistrantChange/error-contactnotfound.http new file mode 100644 index 0000000..4d82ae2 --- /dev/null +++ b/tests/fixtures/v2/api/checkRegistrantChange/error-contactnotfound.http @@ -0,0 +1,14 @@ +HTTP/1.1 404 +server: nginx +date: Tue, 22 Aug 2023 13:59:02 GMT +content-type: application/json; charset=utf-8 +x-ratelimit-limit: 2400 +x-ratelimit-remaining: 2398 +x-ratelimit-reset: 1692716201 +x-work-with-us: Love automation? So do we! https://dnsimple.com/jobs +cache-control: no-cache +x-request-id: b1dd3f42-ebb9-42fd-a121-d595de96f667 +x-runtime: 0.019122 +strict-transport-security: max-age=63072000 + +{"message":"Contact `21` not found"} \ No newline at end of file diff --git a/tests/fixtures/v2/api/checkRegistrantChange/error-domainnotfound.http b/tests/fixtures/v2/api/checkRegistrantChange/error-domainnotfound.http new file mode 100644 index 0000000..6dcc238 --- /dev/null +++ b/tests/fixtures/v2/api/checkRegistrantChange/error-domainnotfound.http @@ -0,0 +1,15 @@ +HTTP/1.1 404 +server: nginx +date: Tue, 22 Aug 2023 11:09:40 GMT +content-type: application/json; charset=utf-8 +x-ratelimit-limit: 2400 +x-ratelimit-remaining: 2395 +x-ratelimit-reset: 1692705338 +x-work-with-us: Love automation? So do we! https://dnsimple.com/jobs +etag: W/"cef1e7d85d0b9bfd25e81b812891d34f" +cache-control: max-age=0, private, must-revalidate +x-request-id: 5b0d8bfb-7b6a-40b5-a079-b640fd817e34 +x-runtime: 3.066249 +strict-transport-security: max-age=63072000 + +{"message":"Domain `dnsimple-rraform.bio` not found"} \ No newline at end of file diff --git a/tests/fixtures/v2/api/checkRegistrantChange/success.http b/tests/fixtures/v2/api/checkRegistrantChange/success.http new file mode 100644 index 0000000..3055e57 --- /dev/null +++ b/tests/fixtures/v2/api/checkRegistrantChange/success.http @@ -0,0 +1,15 @@ +HTTP/1.1 200 +server: nginx +date: Tue, 22 Aug 2023 11:09:40 GMT +content-type: application/json; charset=utf-8 +x-ratelimit-limit: 2400 +x-ratelimit-remaining: 2395 +x-ratelimit-reset: 1692705338 +x-work-with-us: Love automation? So do we! https://dnsimple.com/jobs +etag: W/"cef1e7d85d0b9bfd25e81b812891d34f" +cache-control: max-age=0, private, must-revalidate +x-request-id: 5b0d8bfb-7b6a-40b5-a079-b640fd817e34 +x-runtime: 3.066249 +strict-transport-security: max-age=63072000 + +{"data":{"domain_id":101,"contact_id":101,"extended_attributes":[],"registry_owner_change":true}} \ No newline at end of file diff --git a/tests/fixtures/v2/api/createRegistrantChange/success.http b/tests/fixtures/v2/api/createRegistrantChange/success.http new file mode 100644 index 0000000..1010e26 --- /dev/null +++ b/tests/fixtures/v2/api/createRegistrantChange/success.http @@ -0,0 +1,14 @@ +HTTP/1.1 202 +server: nginx +date: Tue, 22 Aug 2023 11:11:00 GMT +content-type: application/json; charset=utf-8 +x-ratelimit-limit: 2400 +x-ratelimit-remaining: 2394 +x-ratelimit-reset: 1692705339 +x-work-with-us: Love automation? So do we! https://dnsimple.com/jobs +cache-control: no-cache +x-request-id: 26bf7ff9-2075-42b0-9431-1778c825b6b0 +x-runtime: 3.408950 +strict-transport-security: max-age=63072000 + +{"data":{"id":101,"account_id":101,"domain_id":101,"contact_id":101,"state":"new","extended_attributes":{},"registry_owner_change":true,"irt_lock_lifted_by":null,"created_at":"2017-02-03T17:43:22Z","updated_at":"2017-02-03T17:43:22Z"}} \ No newline at end of file diff --git a/tests/fixtures/v2/api/deleteRegistrantChange/success.http b/tests/fixtures/v2/api/deleteRegistrantChange/success.http new file mode 100644 index 0000000..bc40aa5 --- /dev/null +++ b/tests/fixtures/v2/api/deleteRegistrantChange/success.http @@ -0,0 +1,12 @@ +HTTP/1.1 204 No Content +server: nginx +date: Tue, 22 Aug 2023 11:14:44 GMT +content-type: application/json; charset=utf-8 +x-ratelimit-limit: 2400 +x-ratelimit-remaining: 2391 +x-ratelimit-reset: 1692705338 +x-work-with-us: Love automation? So do we! https://dnsimple.com/jobs +cache-control: no-cache +x-request-id: b123e1f0-aa70-4abb-95cf-34f377c83ef4 +x-runtime: 0.114839 +strict-transport-security: max-age=63072000 diff --git a/tests/fixtures/v2/api/deleteRegistrantChange/success_async.http b/tests/fixtures/v2/api/deleteRegistrantChange/success_async.http new file mode 100644 index 0000000..885fd38 --- /dev/null +++ b/tests/fixtures/v2/api/deleteRegistrantChange/success_async.http @@ -0,0 +1,14 @@ +HTTP/1.1 202 +server: nginx +date: Tue, 22 Aug 2023 11:11:00 GMT +content-type: application/json; charset=utf-8 +x-ratelimit-limit: 2400 +x-ratelimit-remaining: 2394 +x-ratelimit-reset: 1692705339 +x-work-with-us: Love automation? So do we! https://dnsimple.com/jobs +cache-control: no-cache +x-request-id: 26bf7ff9-2075-42b0-9431-1778c825b6b0 +x-runtime: 3.408950 +strict-transport-security: max-age=63072000 + +{"data":{"id":101,"account_id":101,"domain_id":101,"contact_id":101,"state":"cancelling","extended_attributes":{},"registry_owner_change":true,"irt_lock_lifted_by":null,"created_at":"2017-02-03T17:43:22Z","updated_at":"2017-02-03T17:43:22Z"}} diff --git a/tests/fixtures/v2/api/getRegistrantChange/success.http b/tests/fixtures/v2/api/getRegistrantChange/success.http new file mode 100644 index 0000000..0707fe3 --- /dev/null +++ b/tests/fixtures/v2/api/getRegistrantChange/success.http @@ -0,0 +1,15 @@ +HTTP/1.1 200 +server: nginx +date: Tue, 22 Aug 2023 11:13:58 GMT +content-type: application/json; charset=utf-8 +X-RateLimit-Limit: 2400 +X-RateLimit-Remaining: 2395 +X-RateLimit-Reset: 1692705338 +x-work-with-us: Love automation? So do we! https://dnsimple.com/jobs +etag: W/"76c5d4c7579b754b94a42ac7fa37a901" +cache-control: max-age=0, private, must-revalidate +x-request-id: e910cd08-3f9c-4da4-9986-50dbe9c3bc55 +x-runtime: 0.022006 +strict-transport-security: max-age=63072000 + +{"data":{"id":101,"account_id":101,"domain_id":101,"contact_id":101,"state":"new","extended_attributes":{},"registry_owner_change":true,"irt_lock_lifted_by":null,"created_at":"2017-02-03T17:43:22Z","updated_at":"2017-02-03T17:43:22Z"}} \ No newline at end of file diff --git a/tests/fixtures/v2/api/listRegistrantChanges/success.http b/tests/fixtures/v2/api/listRegistrantChanges/success.http new file mode 100644 index 0000000..0b4b7b4 --- /dev/null +++ b/tests/fixtures/v2/api/listRegistrantChanges/success.http @@ -0,0 +1,15 @@ +HTTP/1.1 200 +server: nginx +date: Tue, 22 Aug 2023 11:12:49 GMT +content-type: application/json; charset=utf-8 +x-ratelimit-limit: 2400 +x-ratelimit-remaining: 2393 +x-ratelimit-reset: 1692705338 +x-work-with-us: Love automation? So do we! https://dnsimple.com/jobs +etag: W/"0049703ea058b06346df4c0e169eac29" +cache-control: max-age=0, private, must-revalidate +x-request-id: fd0334ce-414a-4872-8889-e548e0b1410c +x-runtime: 0.030759 +strict-transport-security: max-age=63072000 + +{"data":[{"id":101,"account_id":101,"domain_id":101,"contact_id":101,"state":"new","extended_attributes":{},"registry_owner_change":true,"irt_lock_lifted_by":null,"created_at":"2017-02-03T17:43:22Z","updated_at":"2017-02-03T17:43:22Z"}],"pagination":{"current_page":1,"per_page":30,"total_entries":1,"total_pages":1}} \ No newline at end of file diff --git a/tests/registrar_registrant_changes_test.rs b/tests/registrar_registrant_changes_test.rs new file mode 100644 index 0000000..c1083db --- /dev/null +++ b/tests/registrar_registrant_changes_test.rs @@ -0,0 +1,199 @@ +use crate::common::setup_mock_for; +use dnsimple::dnsimple::registrar_registrant_changes::{ + RegistrantChangeCheckPayload, RegistrantChangePayload, +}; +mod common; +use std::collections::HashMap; + +#[test] +fn get_registrant_change_test() { + let setup = setup_mock_for( + "/101/registrar/registrant_changes/101", + "getRegistrantChange/success", + "GET", + ); + let client = setup.0; + let account_id = 101; + let registrant_change_id = 101; + + let response = client + .registrar() + .get_registrant_change(account_id, registrant_change_id) + .unwrap(); + let registrant_change = response.data.unwrap(); + + assert_eq!(101, registrant_change.id); + assert_eq!(101, registrant_change.account_id); + assert_eq!(101, registrant_change.domain_id); + assert_eq!(101, registrant_change.contact_id); + assert_eq!("new", registrant_change.state); + assert_eq!( + HashMap::new(), + registrant_change.extended_attributes.unwrap() + ); + assert!(registrant_change.registry_owner_change); + assert_eq!(None, registrant_change.irt_lock_lifted_by); + + assert_eq!("2017-02-03T17:43:22Z", registrant_change.created_at); + assert_eq!("2017-02-03T17:43:22Z", registrant_change.updated_at); +} + +#[test] +fn check_registrant_change_test() { + let setup = setup_mock_for( + "/101/registrar/registrant_changes/check", + "checkRegistrantChange/success", + "POST", + ); + let client = setup.0; + let account_id = 101; + let domain_id = 101; + let contact_id = 101; + let payload = RegistrantChangeCheckPayload { + domain_id, + contact_id, + }; + + let response = client + .registrar() + .check_registrant_change(account_id, payload) + .unwrap(); + let registrant_change = response.data.unwrap(); + + assert_eq!(101, registrant_change.contact_id); + assert_eq!(101, registrant_change.domain_id); + assert_eq!(0, registrant_change.extended_attributes.unwrap().len()); + assert!(registrant_change.registry_owner_change); +} + +#[test] +fn create_registrant_change_test() { + let setup = setup_mock_for( + "/101/registrar/registrant_changes", + "createRegistrantChange/success", + "POST", + ); + let client = setup.0; + let account_id = 101; + let domain_id = 101; + let contact_id = 101; + let payload = RegistrantChangePayload { + domain_id, + contact_id, + extended_attributes: Option::Some(HashMap::from([( + String::from("foo"), + String::from("bar"), + )])), + }; + + let response = client + .registrar() + .create_registrant_change(account_id, payload) + .unwrap(); + let registrant_change = response.data.unwrap(); + + assert_eq!(101, registrant_change.id); + assert_eq!(101, registrant_change.account_id); + assert_eq!(101, registrant_change.domain_id); + assert_eq!(101, registrant_change.contact_id); + assert_eq!("new", registrant_change.state); + assert_eq!( + HashMap::new(), + registrant_change.extended_attributes.unwrap() + ); + assert!(registrant_change.registry_owner_change); + assert_eq!(None, registrant_change.irt_lock_lifted_by); + + assert_eq!("2017-02-03T17:43:22Z", registrant_change.created_at); + assert_eq!("2017-02-03T17:43:22Z", registrant_change.updated_at); +} + +#[test] +fn list_registrant_changes_test() { + let setup = setup_mock_for( + "/101/registrar/registrant_changes", + "listRegistrantChanges/success", + "GET", + ); + let client = setup.0; + let account_id = 101; + + let response = client + .registrar() + .list_registrant_changes(account_id, None) + .unwrap(); + let registrant_changes = response.data.unwrap(); + let pagination = response.pagination.unwrap(); + + assert_eq!(1, pagination.total_entries); + + assert_eq!(1, registrant_changes.len()); + + let registrant_change = ®istrant_changes[0]; + assert_eq!(101, registrant_change.id); + assert_eq!(101, registrant_change.account_id); + assert_eq!(101, registrant_change.domain_id); + assert_eq!(101, registrant_change.contact_id); + assert_eq!("new", registrant_change.state); + assert_eq!( + &HashMap::new(), + registrant_change.extended_attributes.as_ref().unwrap() + ); + assert!(registrant_change.registry_owner_change); + assert_eq!(None, registrant_change.irt_lock_lifted_by); + + assert_eq!("2017-02-03T17:43:22Z", registrant_change.created_at); + assert_eq!("2017-02-03T17:43:22Z", registrant_change.updated_at); +} + +#[test] +fn delete_registrant_change_test() { + let setup = setup_mock_for( + "/101/registrar/registrant_changes/101", + "deleteRegistrantChange/success", + "DELETE", + ); + let client = setup.0; + let account_id = 101; + let registrant_change_id = 101; + + let response = client + .registrar() + .delete_registrant_change(account_id, registrant_change_id); + + // assert!(response.is_ok()); + assert_eq!(204, response.unwrap().status); +} + +#[test] +fn delete_registrant_change_async_response_test() { + let setup = setup_mock_for( + "/101/registrar/registrant_changes/101", + "deleteRegistrantChange/success_async", + "DELETE", + ); + let client = setup.0; + let account_id = 101; + let registrant_change_id = 101; + + let response = client + .registrar() + .delete_registrant_change(account_id, registrant_change_id) + .unwrap(); + let registrant_change = response.data.unwrap().unwrap(); + + assert_eq!(101, registrant_change.id); + assert_eq!(101, registrant_change.account_id); + assert_eq!(101, registrant_change.domain_id); + assert_eq!(101, registrant_change.contact_id); + assert_eq!("cancelling", registrant_change.state); + assert_eq!( + &HashMap::new(), + registrant_change.extended_attributes.as_ref().unwrap() + ); + assert!(registrant_change.registry_owner_change); + assert_eq!(None, registrant_change.irt_lock_lifted_by); + + assert_eq!("2017-02-03T17:43:22Z", registrant_change.created_at); + assert_eq!("2017-02-03T17:43:22Z", registrant_change.updated_at); +}