diff --git a/src/dnsimple-test/FixtureLoader.cs b/src/dnsimple-test/FixtureLoader.cs index dd99d73..b891acc 100644 --- a/src/dnsimple-test/FixtureLoader.cs +++ b/src/dnsimple-test/FixtureLoader.cs @@ -1,4 +1,5 @@ using System; +using System.Net; using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -25,7 +26,7 @@ public FixtureLoader(string version, string fixture) private string JsonPartFrom(string fixture) { Fixture = fixture; - LoadFixture(); + LoadFixture(); var lastLine = GetLines(true).Last(); return IsValidJson(lastLine) ? lastLine : ""; } @@ -54,7 +55,7 @@ private IEnumerable GetLines(bool removeEmptyLines = false) return LoadFixture().Split(new[] { "\r\n", "\r", "\n" }, removeEmptyLines ? StringSplitOptions.RemoveEmptyEntries : StringSplitOptions.None); } - + private string LoadFixture() { @@ -70,16 +71,22 @@ public List ExtractHeaders() foreach (var line in GetLines()) { - if(String.IsNullOrEmpty(line)) + if (String.IsNullOrEmpty(line)) break; if (line.Contains(':')) { var header = line.Split(':'); - headers.Add(new Parameter(header[0], header[1], ParameterType.HttpHeader)); + headers.Add(new Parameter(header[0], header[1], ParameterType.HttpHeader)); } } return headers; } + + public HttpStatusCode ExtractStatusCode() + { + return (HttpStatusCode)Enum.Parse(typeof(HttpStatusCode), + GetLines().First().Split(' ')[1]); + } } -} \ No newline at end of file +} diff --git a/src/dnsimple-test/MockDnsimpleClient.cs b/src/dnsimple-test/MockDnsimpleClient.cs index bc6d866..aa430dd 100644 --- a/src/dnsimple-test/MockDnsimpleClient.cs +++ b/src/dnsimple-test/MockDnsimpleClient.cs @@ -52,12 +52,12 @@ public MockDnsimpleClient(string fixture) Webhooks = new WebhooksService(this); Zones = new ZonesService(this); } - + public void StatusCode(HttpStatusCode statusCode) { ((MockHttpService)Http).StatusCode = statusCode; } - + public void ChangeBaseUrlTo(string baseUrl) { // Not needed at the moment, but having to implement... @@ -80,17 +80,17 @@ public void SetUserAgent(string customUserAgent) public string RequestSentTo() { - return ((MockHttpService) Http).RequestUrlSent.UrlDecode(); + return ((MockHttpService)Http).RequestUrlSent.UrlDecode(); } public Method HttpMethodUsed() { - return ((MockHttpService) Http).MethodSent; + return ((MockHttpService)Http).MethodSent; } public string PayloadSent() { - return ((MockHttpService) Http).PayloadSent; + return ((MockHttpService)Http).PayloadSent; } } @@ -98,7 +98,7 @@ public class MockHttpService : HttpService { private readonly FixtureLoader _fixtureLoader; private readonly string _baseUrl; - + public HttpStatusCode StatusCode { get; set; } public string RequestUrlSent { get; private set; } public Method MethodSent { get; private set; } @@ -122,7 +122,7 @@ public override IRestResponse Execute(IRestRequest request) MethodSent = request.Method; try { - PayloadSent = (string) request.Parameters.Find(x => + PayloadSent = (string)request.Parameters.Find(x => x.ContentType.Equals("application/json")).Value; } catch (Exception) @@ -156,13 +156,14 @@ public class MockResponse : RestResponse { public MockResponse(FixtureLoader loader) { + StatusCode = loader.ExtractStatusCode(); Content = loader.ExtractJsonPayload(); Headers = loader.ExtractHeaders(); } - + public void SetHeaders(List headers) { Headers = headers; } } -} \ No newline at end of file +} diff --git a/src/dnsimple-test/Services/RegistrarTest.cs b/src/dnsimple-test/Services/RegistrarTest.cs index e4aacbc..575d441 100644 --- a/src/dnsimple-test/Services/RegistrarTest.cs +++ b/src/dnsimple-test/Services/RegistrarTest.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.Linq; using System.Net; using dnsimple; +using dnsimple.Services.ListOptions; using dnsimple.Services; using NUnit.Framework; @@ -53,6 +55,24 @@ public class RegistrarServicesTest private const string AuthorizeTransferOutFixture = "authorizeDomainTransferOut/success.http"; + private const string CheckRegistrantChangeFixture = + "checkRegistrantChange/success.http"; + + private const string GetRegistrantChangeFixture = + "getRegistrantChange/success.http"; + + private const string ListRegistrantChangeFixture = + "listRegistrantChanges/success.http"; + + private const string CreateRegistrantChangeFixture = + "createRegistrantChange/success.http"; + + private const string DeleteRegistrantChangeFixture = + "deleteRegistrantChange/success.http"; + + private const string DeleteRegistrantChangeAsyncFixture = + "deleteRegistrantChange/success_async.http"; + private DateTime CreatedAt { get; } = DateTime.ParseExact( "2016-12-09T19:35:31Z", "yyyy-MM-ddTHH:mm:ssZ", CultureInfo.CurrentCulture); @@ -128,6 +148,184 @@ public void GetDomainPricesFailure(long accountId, string domainName, }); } + [Test] + [TestCase(1010, "example.com", + "https://api.sandbox.dnsimple.com/v2/1010/registrar/registrant_changes/check")] + public void CheckRegistrantChange(long accountId, string domainId, string expectedUrl) + { + var client = new MockDnsimpleClient(CheckRegistrantChangeFixture); + var checkInput = new CheckRegistrantChangeInput + { + DomainId = domainId, + ContactId = 101 + }; + var check = client.Registrar.CheckRegistrantChange(accountId, checkInput) + .Data; + + Assert.Multiple(() => + { + Assert.AreEqual(101, check.ContactId); + Assert.AreEqual(101, check.DomainId); + Assert.IsInstanceOf>(check.ExtendedAttributes); + Assert.AreEqual(true, check.RegistryOwnerChange); + + Assert.AreEqual(expectedUrl, client.RequestSentTo()); + }); + } + + [Test] + [TestCase(1010, 101, + "https://api.sandbox.dnsimple.com/v2/1010/registrar/registrant_changes/101")] + public void GetRegistrantChange(long accountId, long registrantChangeId, string expectedUrl) + { + var client = new MockDnsimpleClient(GetRegistrantChangeFixture); + var check = client.Registrar.GetRegistrantChange(accountId, registrantChangeId) + .Data; + + Assert.Multiple(() => + { + Assert.AreEqual(101, check.Id); + Assert.AreEqual(101, check.AccountId); + Assert.AreEqual(101, check.ContactId); + Assert.AreEqual(101, check.DomainId); + Assert.AreEqual("new", check.State); + Assert.IsInstanceOf>(check.ExtendedAttributes); + Assert.AreEqual(true, check.RegistryOwnerChange); + Assert.AreEqual(null, check.IrtLockLiftedBy); + Assert.AreEqual(CreatedAt, check.CreatedAt); + Assert.AreEqual(UpdatedAt, check.UpdatedAt); + + Assert.AreEqual(expectedUrl, client.RequestSentTo()); + }); + } + + + [Test] + [TestCase(1010, "example.com", + "https://api.sandbox.dnsimple.com/v2/1010/registrar/registrant_changes")] + public void CreateRegistrantChange(long accountId, object domainId, string expectedUrl) + { + var client = new MockDnsimpleClient(CreateRegistrantChangeFixture); + var createInput = new CreateRegistrantChangeInput + { + DomainId = domainId, + ContactId = 101, + ExtendedAttributes = new Dictionary + { + { "x-foo", "bar" } + } + }; + var check = client.Registrar.CreateRegistrantChange(accountId, createInput) + .Data; + + Assert.Multiple(() => + { + Assert.AreEqual(101, check.Id); + Assert.AreEqual(101, check.AccountId); + Assert.AreEqual(101, check.ContactId); + Assert.AreEqual(101, check.DomainId); + Assert.AreEqual("new", check.State); + Assert.IsInstanceOf>(check.ExtendedAttributes); + Assert.AreEqual(true, check.RegistryOwnerChange); + Assert.AreEqual(null, check.IrtLockLiftedBy); + Assert.AreEqual(CreatedAt, check.CreatedAt); + Assert.AreEqual(UpdatedAt, check.UpdatedAt); + + Assert.AreEqual(expectedUrl, client.RequestSentTo()); + }); + } + + [Test] + [TestCase(1010, + "https://api.sandbox.dnsimple.com/v2/1010/registrar/registrant_changes")] + public void ListRegistrantChange(long accountId, string expectedUrl) + { + var client = new MockDnsimpleClient(ListRegistrantChangeFixture); + var registrantChanges = client.Registrar.ListRegistrantChanges(accountId) + .Data; + + var registrantChange = registrantChanges.First(); + + Assert.Multiple(() => + { + Assert.AreEqual(101, registrantChange.Id); + Assert.AreEqual(101, registrantChange.AccountId); + Assert.AreEqual(101, registrantChange.ContactId); + Assert.AreEqual(101, registrantChange.DomainId); + Assert.AreEqual("new", registrantChange.State); + Assert.IsInstanceOf>(registrantChange.ExtendedAttributes); + Assert.AreEqual(true, registrantChange.RegistryOwnerChange); + Assert.AreEqual(null, registrantChange.IrtLockLiftedBy); + + Assert.AreEqual(expectedUrl, client.RequestSentTo()); + }); + } + + [Test] + [TestCase(1010, + "https://api.sandbox.dnsimple.com/v2/1010/registrar/registrant_changes?sort=id:asc&per_page=42&page=7")] + public void ListRegistrantChangesWithSortingAndFiltering(long accountId, string expectedUrl) + { + var client = new MockDnsimpleClient(ListRegistrantChangeFixture); + var options = new RegistrantChangesListOptions + { + Pagination = new Pagination + { + PerPage = 42, + Page = 7 + } + + }.SortById(Order.asc); + + client.Registrar.ListRegistrantChanges(accountId, options); + + Assert.AreEqual(expectedUrl, client.RequestSentTo()); + } + + + [Test] + [TestCase(1010, 101, + "https://api.sandbox.dnsimple.com/v2/1010/registrar/registrant_changes/101")] + public void DeleteRegistrantChange(long accountId, long registrantChangeId, string expectedUrl) + { + var client = new MockDnsimpleClient(DeleteRegistrantChangeFixture); + var response = client.Registrar.DeleteRegistrantChange(accountId, registrantChangeId); + var data = response.Data; + + Assert.Multiple(() => + { + Assert.AreEqual(true, response.IsEmpty); + // data is an empty RegistrantChange object + Assert.AreEqual(0, data.Id); + Assert.AreEqual(expectedUrl, client.RequestSentTo()); + }); + } + + + [Test] + [TestCase(1010, 101, + "https://api.sandbox.dnsimple.com/v2/1010/registrar/registrant_changes/101")] + public void DeleteRegistrantChangeAsync(long accountId, long registrantChangeId, string expectedUrl) + { + var client = new MockDnsimpleClient(DeleteRegistrantChangeAsyncFixture); + var response = client.Registrar.DeleteRegistrantChange(accountId, registrantChangeId); + var registrantChange = response.Data; + + Assert.Multiple(() => + { + Assert.AreEqual(false, response.IsEmpty); + Assert.AreEqual(101, registrantChange.Id); + Assert.AreEqual(101, registrantChange.AccountId); + Assert.AreEqual(101, registrantChange.ContactId); + Assert.AreEqual(101, registrantChange.DomainId); + Assert.AreEqual("cancelling", registrantChange.State); + Assert.IsInstanceOf>(registrantChange.ExtendedAttributes); + Assert.AreEqual(true, registrantChange.RegistryOwnerChange); + Assert.AreEqual(null, registrantChange.IrtLockLiftedBy); + + Assert.AreEqual(expectedUrl, client.RequestSentTo()); + }); + } [Test] [TestCase(1010, "ruby.codes", diff --git a/src/dnsimple/Services/ListOptions/ListRegistrantChangesOptions.cs b/src/dnsimple/Services/ListOptions/ListRegistrantChangesOptions.cs new file mode 100644 index 0000000..cbd1524 --- /dev/null +++ b/src/dnsimple/Services/ListOptions/ListRegistrantChangesOptions.cs @@ -0,0 +1,66 @@ +namespace dnsimple.Services.ListOptions +{ + /// + /// Defines the options you may want to send to list registrant changes, such as + /// pagination, sorting and filtering. + /// + /// + public class RegistrantChangesListOptions : ListOptionsWithFiltering + { + /// + /// Sets the account to be filtered by. + /// + /// The account id we want to filter by. + /// The instance of the RegistrantChangesListOptions + public RegistrantChangesListOptions FilterByAccount(object accountId) + { + AddFilter(new Filter { Field = "account", Value = accountId.ToString() }); + return this; + } + + /// + /// Sets the domain to be filtered by. + /// + /// The domain ID + /// The instance of the RegistrantChangesListOptions + public RegistrantChangesListOptions FilterByDomain(long domainId) + { + AddFilter(new Filter { Field = "domain_id", Value = domainId.ToString() }); + return this; + } + + /// + /// Sets the contact to be filtered by. + /// + /// The contact ID + /// The instance of the RegistrantChangesListOptions + public RegistrantChangesListOptions FilterByContact(long contactId) + { + AddFilter(new Filter { Field = "contact_id", Value = contactId.ToString() }); + return this; + } + + /// + /// Sets the state to be filtered by. + /// + /// The state we want to filter by. + /// The instance of the RegistrantChangesListOptions + public RegistrantChangesListOptions FilterByState(string state) + { + AddFilter(new Filter { Field = "state", Value = state }); + return this; + } + + /// + /// Sets the order by which to sort by id. + /// + /// The order in which we want to sort (asc or desc) + /// The instance of the RegistrantChangesListOptions + /// + public RegistrantChangesListOptions SortById(Order order) + { + AddSortCriteria(new Sort { Field = "id", Order = order }); + return this; + } + } +} diff --git a/src/dnsimple/Services/Paths.cs b/src/dnsimple/Services/Paths.cs index 4e2fc5b..dc69716 100644 --- a/src/dnsimple/Services/Paths.cs +++ b/src/dnsimple/Services/Paths.cs @@ -209,6 +209,22 @@ public static string ContactPath(long accountId, long contactId) return $"{ContactsPath(accountId)}/{contactId}"; } + public static string CheckRegistrantChangePath(long accountId) + { + return $"{accountId}/registrar/registrant_changes/check"; + } + + public static string RegistrantChangePath(long accountId, + long registrantChangeId) + { + return $"{accountId}/registrar/registrant_changes/{registrantChangeId}"; + } + + public static string RegistrantChangesPath(long accountId) + { + return $"{accountId}/registrar/registrant_changes"; + } + public static string ServicesPath() { return "/services"; diff --git a/src/dnsimple/Services/Registrar.cs b/src/dnsimple/Services/Registrar.cs index 2bce3ce..f4c59c3 100644 --- a/src/dnsimple/Services/Registrar.cs +++ b/src/dnsimple/Services/Registrar.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using dnsimple.Services.ListOptions; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; using RestSharp; @@ -90,7 +91,8 @@ public SimpleResponse GetDomainRegistration(long accountId, /// https://developer.dnsimple.com/v2/registrar/#transferDomain public SimpleResponse TransferDomain(long accountId, string domainName, DomainTransferInput transferInput) { - if (transferInput.AuthCode == null) { + if (transferInput.AuthCode == null) + { throw new DnsimpleException("Please provide an AuthCode"); } var builder = BuildRequestForPath(TransferDomainPath(accountId, domainName)); @@ -116,6 +118,80 @@ public SimpleResponse GetDomainTransfer(long accountId, string d return new SimpleResponse(Execute(builder.Request)); } + /// + /// Checks what are the requirements for a registrant change. + /// + /// The account ID + /// The check registrant change command + /// RegistrantChangeCheck + public SimpleResponse CheckRegistrantChange(long accountId, CheckRegistrantChangeInput input) + { + var builder = BuildRequestForPath(CheckRegistrantChangePath(accountId)); + builder.Method(Method.POST); + builder.AddJsonPayload(input); + + return new SimpleResponse(Execute(builder.Request)); + } + + /// + /// Retrieves the details of an existing registrant change. + /// + /// The account ID + /// The registrant change ID + /// RegistrantChange + public SimpleResponse GetRegistrantChange(long accountId, long registrantChangeId) + { + var builder = BuildRequestForPath(RegistrantChangePath(accountId, registrantChangeId)); + + return new SimpleResponse(Execute(builder.Request)); + } + + /// + /// Start registrant change. + /// + /// The account ID + /// the attributes to start a registrant change + /// RegistrantChange + public SimpleResponse CreateRegistrantChange(long accountId, CreateRegistrantChangeInput input) + { + var builder = BuildRequestForPath(RegistrantChangesPath(accountId)); + builder.Method(Method.POST); + builder.AddJsonPayload(input); + + return new SimpleResponse(Execute(builder.Request)); + } + + + /// + /// Lists the registrant changes for the account. + /// + /// The account ID + /// Options passed to the list (sorting and + /// pagination). + /// A list of all the registrant changes for the account RegistrantChange + public PaginatedResponse ListRegistrantChanges(long accountId, RegistrantChangesListOptions options = null) + { + var builder = BuildRequestForPath(RegistrantChangesPath(accountId)); + AddListOptionsToRequest(options, ref builder); + + return new PaginatedResponse(Execute(builder.Request)); + } + + /// + /// Cancels a registrant change. + /// + /// The account ID + /// The registrant change ID + /// Returns an empty response if cancellation was successful immediately. + /// If cancellation is not immediate, returns a registrant change. + public SimpleResponseOrEmpty DeleteRegistrantChange(long accountId, long registrantChangeId) + { + var builder = BuildRequestForPath(RegistrantChangePath(accountId, registrantChangeId)); + builder.Method(Method.DELETE); + + return new SimpleResponseOrEmpty(Execute(builder.Request)); + } + /// /// Cancels an in progress domain transfer. /// @@ -203,6 +279,36 @@ public struct DomainCheck public bool Premium { get; set; } } + /// + /// Represents a domain registrant change check. + /// + [JsonObject(NamingStrategyType = typeof(SnakeCaseNamingStrategy))] + public struct RegistrantChangeCheck + { + public long ContactId { get; set; } + public long DomainId { get; set; } + public List ExtendedAttributes { get; set; } + public bool RegistryOwnerChange { get; set; } + } + + /// + /// Represents a registrant change. + /// + [JsonObject(NamingStrategyType = typeof(SnakeCaseNamingStrategy))] + public struct RegistrantChange + { + public long Id { get; set; } + public long AccountId { get; set; } + public long ContactId { get; set; } + public long DomainId { get; set; } + public string State { get; set; } + public Dictionary ExtendedAttributes { get; set; } + public bool RegistryOwnerChange { get; set; } + public string IrtLockLiftedBy { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } + } + /// /// Represents a domain registration. /// @@ -313,4 +419,49 @@ public struct DomainTransferInput public List ExtendedAttributes { get; set; } } + /// + /// Represents the data sent to check requirements for registrant change. + /// + [JsonObject(NamingStrategyType = typeof(SnakeCaseNamingStrategy))] + public struct CheckRegistrantChangeInput + { + /// + /// The contact ID to be used as the new registrant. + /// + [JsonProperty(Required = Required.Always)] + public long ContactId { get; set; } + + /// + /// The domain ID for which the registrant change is being requested. + /// Can be a string or an int. + /// + [JsonProperty(Required = Required.Always)] + public object DomainId { get; set; } + } + + /// + /// Represents the data sent to create a registrant change. + /// + [JsonObject(NamingStrategyType = typeof(SnakeCaseNamingStrategy))] + public struct CreateRegistrantChangeInput + { + /// + /// The contact ID to be used as the new registrant. + /// + [JsonProperty(Required = Required.Always)] + public long ContactId { get; set; } + + /// + /// The domain ID for which the registrant change is being requested. + /// Can be a string or an int. + /// + [JsonProperty(Required = Required.Always)] + public object DomainId { get; set; } + + /// + /// The extended attributes to be used as the new registrant. + /// + public Dictionary ExtendedAttributes { get; set; } + } + } diff --git a/src/dnsimple/Services/ServiceBase.cs b/src/dnsimple/Services/ServiceBase.cs index d0c3167..582200e 100644 --- a/src/dnsimple/Services/ServiceBase.cs +++ b/src/dnsimple/Services/ServiceBase.cs @@ -60,7 +60,7 @@ public Response(IRestResponse response) private string ExtractValueFromHeader(string headerName) { - return (string) Headers.Where(header => + return (string)Headers.Where(header => header.Name != null && header.Name.Equals(headerName)) .First().Value; } @@ -94,6 +94,35 @@ public SimpleResponse(IRestResponse response) : base(response) } } + /// + /// Represents a response from a call to the DNSimple API containing a + /// single object or an empty response (204 No Content). + /// + /// The Data object type contained in the response + public class SimpleResponseOrEmpty : Response + { + /// + /// Represents the struct containing the data. + /// + public T Data { get; protected set; } + + public bool IsEmpty { get; protected set; } + + public SimpleResponseOrEmpty(IRestResponse response) : base(response) + { + if (response.StatusCode == System.Net.HttpStatusCode.NoContent) + { + Data = default(T); + IsEmpty = true; + } + else + { + Data = JsonTools.DeserializeObject("data", JObject.Parse(response.Content)); + IsEmpty = false; + } + } + } + /// /// Represents a response from a call to the DNSimple API containing a list