diff --git a/Extensions/Cloudflare.Zones/Internals/InternalUpdateDomainRequest.cs b/Extensions/Cloudflare.Zones/Internals/InternalUpdateDomainRequest.cs new file mode 100644 index 0000000..2e944bb --- /dev/null +++ b/Extensions/Cloudflare.Zones/Internals/InternalUpdateDomainRequest.cs @@ -0,0 +1,14 @@ +namespace AMWD.Net.Api.Cloudflare.Zones.Internals +{ + internal class InternalUpdateDomainRequest + { + [JsonProperty("auto_renew")] + public bool? AutoRenew { get; set; } + + [JsonProperty("locked")] + public bool? Locked { get; set; } + + [JsonProperty("privacy")] + public bool? Privacy { get; set; } + } +} diff --git a/Extensions/Cloudflare.Zones/Models/Domain.cs b/Extensions/Cloudflare.Zones/Models/Domain.cs new file mode 100644 index 0000000..cdb12ff --- /dev/null +++ b/Extensions/Cloudflare.Zones/Models/Domain.cs @@ -0,0 +1,394 @@ +using System.Runtime.Serialization; +using Newtonsoft.Json.Converters; + +namespace AMWD.Net.Api.Cloudflare.Zones +{ + /// + /// A Cloudflare registrar domain. + /// Source + /// + public class Domain + { + /// + /// Domain identifier. + /// + [JsonProperty("id")] + public string? Id { get; set; } + + /// + /// Shows if a domain is available for transferring into Cloudflare Registrar. + /// + [JsonProperty("available")] + public bool? Available { get; set; } + + /// + /// Indicates if the domain can be registered as a new domain. + /// + [JsonProperty("can_register")] + public bool? CanRegister { get; set; } + + /// + /// Shows time of creation. + /// + [JsonProperty("created_at")] + public DateTime? CreatedAt { get; set; } + + /// + /// Shows name of current registrar. + /// + [JsonProperty("current_registrar")] + public string? CurrentRegistrar { get; set; } + + /// + /// Shows when domain name registration expires. + /// + [JsonProperty("expires_at")] + public DateTime? ExpiresAt { get; set; } + + /// + /// Shows whether a registrar lock is in place for a domain. + /// + [JsonProperty("locked")] + public bool? Locked { get; set; } + + /// + /// Shows contact information for domain registrant. + /// + [JsonProperty("registrant_contact")] + public DomainRegistrantContact? RegistrantContact { get; set; } + + /// + /// A comma-separated list of registry status codes. + /// + /// + /// A full list of status codes can be found at EPP Status Codes. + /// + [JsonProperty("registry_statuses")] + public string? RegistryStatuses { get; set; } + + /// + /// Whether a particular TLD is currently supported by Cloudflare Registrar. + /// + /// + /// Refer to TLD Policies for a list of supported TLDs. + /// + [JsonProperty("supported_tld")] + public bool? SupportedTld { get; set; } + + /// + /// Statuses for domain transfers into Cloudflare Registrar. + /// + [JsonProperty("transfer_in")] + public DomainTransferIn? TransferIn { get; set; } + + /// + /// Last updated. + /// + [JsonProperty("updated_at")] + public DateTime? UpdatedAt { get; set; } + } + + /// + /// Shows contact information for domain registrant. + /// Source + /// + public class DomainRegistrantContact + { + /// + /// Initializes a new instance of the class. + /// + /// Address. + /// City. + /// State. + /// User's organization. + public DomainRegistrantContact(string address, string city, string state, string organization) + { + Address = address; + City = city; + State = state; + Organization = organization; + } + + /// + /// Address. + /// + [JsonProperty("address")] + public string Address { get; set; } + + /// + /// City. + /// + [JsonProperty("city")] + public string City { get; set; } + + /// + /// The country in which the user lives.. + /// + [JsonProperty("country")] + public string? Country { get; set; } + + /// + /// User's first name. + /// + [JsonProperty("first_name")] + public string? FirstName { get; set; } + + /// + /// User's last name. + /// + [JsonProperty("last_name")] + public string? LastName { get; set; } + + /// + /// Name of organization. + /// + [JsonProperty("organization")] + public string Organization { get; set; } + + /// + /// User's telephone number. + /// + [JsonProperty("phone")] + public string? Phone { get; set; } + + /// + /// State. + /// + [JsonProperty("state")] + public string State { get; set; } + + /// + /// The zipcode or postal code where the user lives. + /// + [JsonProperty("zip")] + public string? ZipCode { get; set; } + + /// + /// Contact Identifier. + /// + [JsonProperty("id")] + public string? Id { get; set; } + + /// + /// Optional address line for unit, floor, suite, etc. + /// + [JsonProperty("address2")] + public string? Address2 { get; set; } + + /// + /// The contact email address of the user. + /// + [JsonProperty("email")] + public string? Email { get; set; } + + /// + /// Contact fax number. + /// + [JsonProperty("fax")] + public string? Fax { get; set; } + } + + /// + /// Statuses for domain transfers into Cloudflare Registrar. + /// Source + /// + public class DomainTransferIn + { + /// + /// Form of authorization has been accepted by the registrant. + /// + [JsonProperty("accept_foa")] + public DomainTransferInAcceptFoa? AcceptFoa { get; set; } + + /// + /// Shows transfer status with the registry. + /// + [JsonProperty("approve_transfer")] + public DomainTransferInApproveTransfer? ApproveTransfer { get; set; } + + /// + /// Indicates if cancellation is still possible. + /// + [JsonProperty("can_cancel_transfer")] + public bool? CanCancelTransfer { get; set; } + + /// + /// Privacy guards are disabled at the foreign registrar. + /// + [JsonProperty("disable_privacy")] + public DomainTransferInDisablePrivacy? DisablePrivacy { get; set; } + + /// + /// Auth code has been entered and verified. + /// + [JsonProperty("enter_auth_code")] + public DomainTransferInEnterAuthCode? EnterAuthCode { get; set; } + + /// + /// Domain is unlocked at the foreign registrar. + /// + [JsonProperty("unlock_domain")] + public DomainTransferInUnlockDomain? UnlockDomain { get; set; } + } + + /// + /// Source + /// + [JsonConverter(typeof(StringEnumConverter))] + public enum DomainTransferInAcceptFoa + { + /// + /// Needed. + /// + [EnumMember(Value = "needed")] + Needed = 1, + + /// + /// Ok. + /// + [EnumMember(Value = "ok")] + Ok = 2 + } + + /// + /// Source + /// + [JsonConverter(typeof(StringEnumConverter))] + public enum DomainTransferInApproveTransfer + { + /// + /// Needed. + /// + [EnumMember(Value = "needed")] + Needed = 1, + + /// + /// Ok. + /// + [EnumMember(Value = "ok")] + Ok = 2, + + /// + /// Pending. + /// + [EnumMember(Value = "pending")] + Pending = 3, + + /// + /// Trying. + /// + [EnumMember(Value = "trying")] + Trying = 4, + + /// + /// Rejected. + /// + [EnumMember(Value = "rejected")] + Rejected = 5, + + /// + /// Unknown. + /// + [EnumMember(Value = "unknown")] + Unknown = 6 + } + + /// + /// Source + /// + [JsonConverter(typeof(StringEnumConverter))] + public enum DomainTransferInDisablePrivacy + { + /// + /// Needed. + /// + [EnumMember(Value = "needed")] + Needed = 1, + + /// + /// Ok. + /// + [EnumMember(Value = "ok")] + Ok = 2, + + /// + /// Unknown. + /// + [EnumMember(Value = "unknown")] + Unknown = 3 + } + + /// + /// Source + /// + [JsonConverter(typeof(StringEnumConverter))] + public enum DomainTransferInEnterAuthCode + { + /// + /// Needed. + /// + [EnumMember(Value = "needed")] + Needed = 1, + + /// + /// Ok. + /// + [EnumMember(Value = "ok")] + Ok = 2, + + /// + /// Pending. + /// + [EnumMember(Value = "pending")] + Pending = 3, + + /// + /// Trying. + /// + [EnumMember(Value = "trying")] + Trying = 4, + + /// + /// Rejected. + /// + [EnumMember(Value = "rejected")] + Rejected = 5 + } + + /// + /// Source + /// + [JsonConverter(typeof(StringEnumConverter))] + public enum DomainTransferInUnlockDomain + { + /// + /// Needed. + /// + [EnumMember(Value = "needed")] + Needed = 1, + + /// + /// Ok. + /// + [EnumMember(Value = "ok")] + Ok = 2, + + /// + /// Pending. + /// + [EnumMember(Value = "pending")] + Pending = 3, + + /// + /// Trying. + /// + [EnumMember(Value = "trying")] + Trying = 4, + + /// + /// Unknown. + /// + [EnumMember(Value = "unknown")] + Unknown = 5 + } +} diff --git a/Extensions/Cloudflare.Zones/README.md b/Extensions/Cloudflare.Zones/README.md index 9c60022..45092cd 100644 --- a/Extensions/Cloudflare.Zones/README.md +++ b/Extensions/Cloudflare.Zones/README.md @@ -4,32 +4,20 @@ This package contains the feature set of the _Domain/Zone Management_ section of ## Implemented Methods -### Zone +### [Registrar] + +- [Get Domain](https://developers.cloudflare.com/api/resources/registrar/subresources/domains/methods/get/) +- [List Domains](https://developers.cloudflare.com/api/resources/registrar/subresources/domains/methods/list/) +- [Update Domain](https://developers.cloudflare.com/api/resources/registrar/subresources/domains/methods/update/) + -- [ListZones](https://developers.cloudflare.com/api/operations/zones-get) -- [CreateZone](https://developers.cloudflare.com/api/operations/zones-post) -- [DeleteZone](https://developers.cloudflare.com/api/operations/zones-0-delete) -- [ZoneDetails](https://developers.cloudflare.com/api/operations/zones-0-get) -- [EditZone](https://developers.cloudflare.com/api/operations/zones-0-patch) -- [RerunActivationCheck](https://developers.cloudflare.com/api/operations/put-zones-zone_id-activation_check) -- [PurgeCachedContent](https://developers.cloudflare.com/api/operations/zone-purge) -### Zone Holds - -- [DeleteZoneHold](https://developers.cloudflare.com/api/operations/zones-0-hold-delete) -- [GetZoneHold](https://developers.cloudflare.com/api/operations/zones-0-hold-get) -- [CreateZoneHold](https://developers.cloudflare.com/api/operations/zones-0-hold-post) -### DNS Settings for a Zone - -- TBD -### DNS Records for a Zone -- TBD --- @@ -39,3 +27,5 @@ Published under MIT License (see [choose a license]) [choose a license]: https://choosealicense.com/licenses/mit/ + +[Registrar]: https://developers.cloudflare.com/api/resources/registrar/ diff --git a/Extensions/Cloudflare.Zones/RegistrarExtensions.cs b/Extensions/Cloudflare.Zones/RegistrarExtensions.cs new file mode 100644 index 0000000..9c970f4 --- /dev/null +++ b/Extensions/Cloudflare.Zones/RegistrarExtensions.cs @@ -0,0 +1,66 @@ +using System.Threading; +using System.Threading.Tasks; +using AMWD.Net.Api.Cloudflare.Zones.Internals; +using Newtonsoft.Json.Linq; + +namespace AMWD.Net.Api.Cloudflare.Zones +{ + /// + /// Extensions for Registrar. + /// + public static class RegistrarExtensions + { + /// + /// Show individual domain. + /// + /// The instance. + /// The account id. + /// The domain name. + /// A cancellation token used to propagate notification that this operation should be canceled. + public static Task> GetDomain(this ICloudflareClient client, string accountId, string domainName, CancellationToken cancellationToken = default) + { + accountId.ValidateCloudflareId(); + + if (string.IsNullOrWhiteSpace(domainName)) + throw new ArgumentNullException(nameof(domainName)); + + return client.GetAsync($"/accounts/{accountId}/registrar/domains/{domainName}", cancellationToken: cancellationToken); + } + + /// + /// List domains handled by Registrar. + /// + /// The instance. + /// The account id. + /// A cancellation token used to propagate notification that this operation should be canceled. + public static Task>> ListDomains(this ICloudflareClient client, string accountId, CancellationToken cancellationToken = default) + { + accountId.ValidateCloudflareId(); + + return client.GetAsync>($"/accounts/{accountId}/registrar/domains", cancellationToken: cancellationToken); + } + + /// + /// Update individual domain. + /// + /// The instance. + /// The request. + /// A cancellation token used to propagate notification that this operation should be canceled. + public static Task> UpdateDomain(this ICloudflareClient client, UpdateDomainRequest request, CancellationToken cancellationToken = default) + { + request.AccountId.ValidateCloudflareId(); + + if (string.IsNullOrWhiteSpace(request.DomainName)) + throw new ArgumentNullException(nameof(request.DomainName)); + + var req = new InternalUpdateDomainRequest + { + AutoRenew = request.AutoRenew, + Locked = request.Locked, + Privacy = request.Privacy + }; + + return client.PutAsync($"/accounts/{request.AccountId}/registrar/domains/{request.DomainName}", req, cancellationToken); + } + } +} diff --git a/Extensions/Cloudflare.Zones/Requests/UpdateDomainRequest.cs b/Extensions/Cloudflare.Zones/Requests/UpdateDomainRequest.cs new file mode 100644 index 0000000..f3685e3 --- /dev/null +++ b/Extensions/Cloudflare.Zones/Requests/UpdateDomainRequest.cs @@ -0,0 +1,45 @@ +namespace AMWD.Net.Api.Cloudflare.Zones +{ + /// + /// Represents a request to update a domain. + /// Source + /// + public class UpdateDomainRequest + { + /// + /// Initializes a new instance of the class. + /// + /// Identifier. + /// Domain name. + public UpdateDomainRequest(string accountId, string domainName) + { + AccountId = accountId; + DomainName = domainName; + } + + /// + /// Identifier. + /// + public string AccountId { get; set; } + + /// + /// Domain name. + /// + public string DomainName { get; set; } + + /// + /// Auto-renew controls whether subscription is automatically renewed upon domain expiration. + /// + public bool? AutoRenew { get; set; } + + /// + /// Shows whether a registrar lock is in place for a domain. + /// + public bool? Locked { get; set; } + + /// + /// Privacy option controls redacting WHOIS information. + /// + public bool? Privacy { get; set; } + } +} diff --git a/UnitTests/Cloudflare.Zones.Tests/ZoneRegistrar/GetDomainTest.cs b/UnitTests/Cloudflare.Zones.Tests/ZoneRegistrar/GetDomainTest.cs new file mode 100644 index 0000000..7c81158 --- /dev/null +++ b/UnitTests/Cloudflare.Zones.Tests/ZoneRegistrar/GetDomainTest.cs @@ -0,0 +1,81 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AMWD.Net.Api.Cloudflare; +using AMWD.Net.Api.Cloudflare.Zones; +using Moq; +using Newtonsoft.Json.Linq; + +namespace Cloudflare.Zones.Tests.ZoneRegistrar +{ + [TestClass] + public class GetDomainTest + { + private const string AccountId = "023e105f4ecef8ad9ca31a8372d0c353"; + + private const string DomainName = "example.com"; + + private Mock _clientMock; + + private CloudflareResponse _response; + + private List<(string RequestPath, IQueryParameterFilter QueryFilter)> _callbacks; + + [TestInitialize] + public void Initialize() + { + _callbacks = []; + + _response = new CloudflareResponse(); + } + + [TestMethod] + public async Task ShouldGetRegistrarDomain() + { + // Arrange + var client = GetClient(); + + // Act + var result = await client.GetDomain(AccountId, DomainName); + + // Assert + Assert.AreEqual(_response, result); + + Assert.AreEqual(1, _callbacks.Count); + + var callback = _callbacks.First(); + Assert.AreEqual($"/accounts/{AccountId}/registrar/domains/{DomainName}", callback.RequestPath); + Assert.IsNull(callback.QueryFilter); + + _clientMock.Verify(m => m.GetAsync($"/accounts/{AccountId}/registrar/domains/{DomainName}", null, It.IsAny()), Times.Once); + _clientMock.VerifyNoOtherCalls(); + } + + [DataTestMethod] + [DataRow(null)] + [DataRow("")] + [DataRow(" ")] + [ExpectedException(typeof(ArgumentNullException))] + public async Task ShouldThrowArgumentNullExceptionOnDomainName(string domainName) + { + // Arrange + var client = GetClient(); + + // Act + var result = await client.GetDomain(AccountId, domainName); + + // Assert - ArgumentNullException + } + + private ICloudflareClient GetClient() + { + _clientMock = new Mock(); + _clientMock + .Setup(m => m.GetAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((requestPath, queryFilter, _) => _callbacks.Add((requestPath, queryFilter))) + .ReturnsAsync(() => _response); + + return _clientMock.Object; + } + } +} diff --git a/UnitTests/Cloudflare.Zones.Tests/ZoneRegistrar/ListDomainsTest.cs b/UnitTests/Cloudflare.Zones.Tests/ZoneRegistrar/ListDomainsTest.cs new file mode 100644 index 0000000..74a6103 --- /dev/null +++ b/UnitTests/Cloudflare.Zones.Tests/ZoneRegistrar/ListDomainsTest.cs @@ -0,0 +1,62 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AMWD.Net.Api.Cloudflare; +using AMWD.Net.Api.Cloudflare.Zones; +using Moq; + +namespace Cloudflare.Zones.Tests.ZoneRegistrar +{ + [TestClass] + public class ListDomainsTest + { + private const string AccountId = "023e105f4ecef8ad9ca31a8372d0c353"; + + private Mock _clientMock; + + private CloudflareResponse> _response; + + private List<(string RequestPath, IQueryParameterFilter QueryFilter)> _callbacks; + + [TestInitialize] + public void Initialize() + { + _callbacks = []; + + _response = new CloudflareResponse>(); + } + + [TestMethod] + public async Task ShouldListRegistrarDomains() + { + // Arrange + var client = GetClient(); + + // Act + var result = await client.ListDomains(AccountId); + + // Assert + Assert.AreEqual(_response, result); + + Assert.AreEqual(1, _callbacks.Count); + + var callback = _callbacks.First(); + Assert.AreEqual($"/accounts/{AccountId}/registrar/domains", callback.RequestPath); + Assert.IsNull(callback.QueryFilter); + + _clientMock.Verify(m => m.GetAsync>($"/accounts/{AccountId}/registrar/domains", null, It.IsAny()), Times.Once); + _clientMock.VerifyNoOtherCalls(); + } + + private ICloudflareClient GetClient() + { + _clientMock = new Mock(); + _clientMock + .Setup(m => m.GetAsync>(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((requestPath, queryFilter, _) => _callbacks.Add((requestPath, queryFilter))) + .ReturnsAsync(() => _response); + + return _clientMock.Object; + } + } +} diff --git a/UnitTests/Cloudflare.Zones.Tests/ZoneRegistrar/UpdateDomainTest.cs b/UnitTests/Cloudflare.Zones.Tests/ZoneRegistrar/UpdateDomainTest.cs new file mode 100644 index 0000000..eef8e35 --- /dev/null +++ b/UnitTests/Cloudflare.Zones.Tests/ZoneRegistrar/UpdateDomainTest.cs @@ -0,0 +1,95 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AMWD.Net.Api.Cloudflare; +using AMWD.Net.Api.Cloudflare.Zones; +using AMWD.Net.Api.Cloudflare.Zones.Internals; +using Moq; +using Newtonsoft.Json.Linq; + +namespace Cloudflare.Zones.Tests.ZoneRegistrar +{ + [TestClass] + public class UpdateDomainTest + { + private const string AccountId = "023e105f4ecef8ad9ca31a8372d0c353"; + + private const string DomainName = "example.com"; + + private Mock _clientMock; + + private CloudflareResponse _response; + + private List<(string RequestPath, InternalUpdateDomainRequest Request)> _callbacks; + + private UpdateDomainRequest _request; + + [TestInitialize] + public void Initialize() + { + _callbacks = []; + + _response = new CloudflareResponse(); + + _request = new UpdateDomainRequest(AccountId, DomainName) + { + AutoRenew = true, + Privacy = false + }; + } + + [TestMethod] + public async Task ShouldUpdateRegistrarDomain() + { + // Arrange + var client = GetClient(); + + // Act + var result = await client.UpdateDomain(_request); + + // Assert + Assert.AreEqual(_response, result); + + Assert.AreEqual(1, _callbacks.Count); + + var callback = _callbacks.First(); + Assert.AreEqual($"/accounts/{AccountId}/registrar/domains/{DomainName}", callback.RequestPath); + + Assert.IsNotNull(callback.Request); + Assert.AreEqual(_request.AutoRenew, callback.Request.AutoRenew); + Assert.AreEqual(_request.Locked, callback.Request.Locked); + Assert.AreEqual(_request.Privacy, callback.Request.Privacy); + + _clientMock.Verify(m => m.PutAsync($"/accounts/{AccountId}/registrar/domains/{DomainName}", It.IsAny(), It.IsAny()), Times.Once); + _clientMock.VerifyNoOtherCalls(); + } + + [DataTestMethod] + [DataRow(null)] + [DataRow("")] + [DataRow(" ")] + [ExpectedException(typeof(ArgumentNullException))] + public async Task ShouldThrowArgumentNullExceptionOnDomainName(string domainName) + { + // Arrange + _request.DomainName = domainName; + var client = GetClient(); + + // Act + var result = await client.UpdateDomain(_request); + + // Assert - ArgumentNullException + } + + private ICloudflareClient GetClient() + { + _clientMock = new Mock(); + _clientMock + .Setup(m => m.PutAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((requestPath, request, _) => _callbacks.Add((requestPath, request))) + .ReturnsAsync(() => _response); + + return _clientMock.Object; + } + } +}