Add "Registrar" extensions

This commit is contained in:
2025-06-24 10:53:00 +02:00
parent 6f27dd3ff8
commit 2fe9ac0657
8 changed files with 765 additions and 18 deletions

View File

@@ -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; }
}
}

View File

@@ -0,0 +1,394 @@
using System.Runtime.Serialization;
using Newtonsoft.Json.Converters;
namespace AMWD.Net.Api.Cloudflare.Zones
{
/// <summary>
/// A Cloudflare registrar domain.
/// <see href="https://github.com/cloudflare/cloudflare-typescript/blob/v4.4.1/src/resources/registrar/domains.ts#L79">Source</see>
/// </summary>
public class Domain
{
/// <summary>
/// Domain identifier.
/// </summary>
[JsonProperty("id")]
public string? Id { get; set; }
/// <summary>
/// Shows if a domain is available for transferring into Cloudflare Registrar.
/// </summary>
[JsonProperty("available")]
public bool? Available { get; set; }
/// <summary>
/// Indicates if the domain can be registered as a new domain.
/// </summary>
[JsonProperty("can_register")]
public bool? CanRegister { get; set; }
/// <summary>
/// Shows time of creation.
/// </summary>
[JsonProperty("created_at")]
public DateTime? CreatedAt { get; set; }
/// <summary>
/// Shows name of current registrar.
/// </summary>
[JsonProperty("current_registrar")]
public string? CurrentRegistrar { get; set; }
/// <summary>
/// Shows when domain name registration expires.
/// </summary>
[JsonProperty("expires_at")]
public DateTime? ExpiresAt { get; set; }
/// <summary>
/// Shows whether a registrar lock is in place for a domain.
/// </summary>
[JsonProperty("locked")]
public bool? Locked { get; set; }
/// <summary>
/// Shows contact information for domain registrant.
/// </summary>
[JsonProperty("registrant_contact")]
public DomainRegistrantContact? RegistrantContact { get; set; }
/// <summary>
/// A comma-separated list of registry status codes.
/// </summary>
/// <remarks>
/// A full list of status codes can be found at <see href="https://www.icann.org/resources/pages/epp-status-codes-2014-06-16-en">EPP Status Codes</see>.
/// </remarks>
[JsonProperty("registry_statuses")]
public string? RegistryStatuses { get; set; }
/// <summary>
/// Whether a particular TLD is currently supported by Cloudflare Registrar.
/// </summary>
/// <remarks>
/// Refer to <see href="https://www.cloudflare.com/tld-policies/">TLD Policies</see> for a list of supported TLDs.
/// </remarks>
[JsonProperty("supported_tld")]
public bool? SupportedTld { get; set; }
/// <summary>
/// Statuses for domain transfers into Cloudflare Registrar.
/// </summary>
[JsonProperty("transfer_in")]
public DomainTransferIn? TransferIn { get; set; }
/// <summary>
/// Last updated.
/// </summary>
[JsonProperty("updated_at")]
public DateTime? UpdatedAt { get; set; }
}
/// <summary>
/// Shows contact information for domain registrant.
/// <see href="https://github.com/cloudflare/cloudflare-typescript/blob/v4.4.1/src/resources/registrar/domains.ts#L149">Source</see>
/// </summary>
public class DomainRegistrantContact
{
/// <summary>
/// Initializes a new instance of the <see cref="DomainRegistrantContact"/> class.
/// </summary>
/// <param name="address">Address.</param>
/// <param name="city">City.</param>
/// <param name="state">State.</param>
/// <param name="organization">User's organization.</param>
public DomainRegistrantContact(string address, string city, string state, string organization)
{
Address = address;
City = city;
State = state;
Organization = organization;
}
/// <summary>
/// Address.
/// </summary>
[JsonProperty("address")]
public string Address { get; set; }
/// <summary>
/// City.
/// </summary>
[JsonProperty("city")]
public string City { get; set; }
/// <summary>
/// The country in which the user lives..
/// </summary>
[JsonProperty("country")]
public string? Country { get; set; }
/// <summary>
/// User's first name.
/// </summary>
[JsonProperty("first_name")]
public string? FirstName { get; set; }
/// <summary>
/// User's last name.
/// </summary>
[JsonProperty("last_name")]
public string? LastName { get; set; }
/// <summary>
/// Name of organization.
/// </summary>
[JsonProperty("organization")]
public string Organization { get; set; }
/// <summary>
/// User's telephone number.
/// </summary>
[JsonProperty("phone")]
public string? Phone { get; set; }
/// <summary>
/// State.
/// </summary>
[JsonProperty("state")]
public string State { get; set; }
/// <summary>
/// The zipcode or postal code where the user lives.
/// </summary>
[JsonProperty("zip")]
public string? ZipCode { get; set; }
/// <summary>
/// Contact Identifier.
/// </summary>
[JsonProperty("id")]
public string? Id { get; set; }
/// <summary>
/// Optional address line for unit, floor, suite, etc.
/// </summary>
[JsonProperty("address2")]
public string? Address2 { get; set; }
/// <summary>
/// The contact email address of the user.
/// </summary>
[JsonProperty("email")]
public string? Email { get; set; }
/// <summary>
/// Contact fax number.
/// </summary>
[JsonProperty("fax")]
public string? Fax { get; set; }
}
/// <summary>
/// Statuses for domain transfers into Cloudflare Registrar.
/// <see href="https://github.com/cloudflare/cloudflare-typescript/blob/v4.4.1/src/resources/registrar/domains.ts#L219">Source</see>
/// </summary>
public class DomainTransferIn
{
/// <summary>
/// Form of authorization has been accepted by the registrant.
/// </summary>
[JsonProperty("accept_foa")]
public DomainTransferInAcceptFoa? AcceptFoa { get; set; }
/// <summary>
/// Shows transfer status with the registry.
/// </summary>
[JsonProperty("approve_transfer")]
public DomainTransferInApproveTransfer? ApproveTransfer { get; set; }
/// <summary>
/// Indicates if cancellation is still possible.
/// </summary>
[JsonProperty("can_cancel_transfer")]
public bool? CanCancelTransfer { get; set; }
/// <summary>
/// Privacy guards are disabled at the foreign registrar.
/// </summary>
[JsonProperty("disable_privacy")]
public DomainTransferInDisablePrivacy? DisablePrivacy { get; set; }
/// <summary>
/// Auth code has been entered and verified.
/// </summary>
[JsonProperty("enter_auth_code")]
public DomainTransferInEnterAuthCode? EnterAuthCode { get; set; }
/// <summary>
/// Domain is unlocked at the foreign registrar.
/// </summary>
[JsonProperty("unlock_domain")]
public DomainTransferInUnlockDomain? UnlockDomain { get; set; }
}
/// <summary>
/// <see href="https://github.com/cloudflare/cloudflare-typescript/blob/v4.4.1/src/resources/registrar/domains.ts#L223">Source</see>
/// </summary>
[JsonConverter(typeof(StringEnumConverter))]
public enum DomainTransferInAcceptFoa
{
/// <summary>
/// Needed.
/// </summary>
[EnumMember(Value = "needed")]
Needed = 1,
/// <summary>
/// Ok.
/// </summary>
[EnumMember(Value = "ok")]
Ok = 2
}
/// <summary>
/// <see href="https://github.com/cloudflare/cloudflare-typescript/blob/v4.4.1/src/resources/registrar/domains.ts#L228">Source</see>
/// </summary>
[JsonConverter(typeof(StringEnumConverter))]
public enum DomainTransferInApproveTransfer
{
/// <summary>
/// Needed.
/// </summary>
[EnumMember(Value = "needed")]
Needed = 1,
/// <summary>
/// Ok.
/// </summary>
[EnumMember(Value = "ok")]
Ok = 2,
/// <summary>
/// Pending.
/// </summary>
[EnumMember(Value = "pending")]
Pending = 3,
/// <summary>
/// Trying.
/// </summary>
[EnumMember(Value = "trying")]
Trying = 4,
/// <summary>
/// Rejected.
/// </summary>
[EnumMember(Value = "rejected")]
Rejected = 5,
/// <summary>
/// Unknown.
/// </summary>
[EnumMember(Value = "unknown")]
Unknown = 6
}
/// <summary>
/// <see href="https://github.com/cloudflare/cloudflare-typescript/blob/v4.4.1/src/resources/registrar/domains.ts#L238">Source</see>
/// </summary>
[JsonConverter(typeof(StringEnumConverter))]
public enum DomainTransferInDisablePrivacy
{
/// <summary>
/// Needed.
/// </summary>
[EnumMember(Value = "needed")]
Needed = 1,
/// <summary>
/// Ok.
/// </summary>
[EnumMember(Value = "ok")]
Ok = 2,
/// <summary>
/// Unknown.
/// </summary>
[EnumMember(Value = "unknown")]
Unknown = 3
}
/// <summary>
/// <see href="https://github.com/cloudflare/cloudflare-typescript/blob/v4.4.1/src/resources/registrar/domains.ts#L243">Source</see>
/// </summary>
[JsonConverter(typeof(StringEnumConverter))]
public enum DomainTransferInEnterAuthCode
{
/// <summary>
/// Needed.
/// </summary>
[EnumMember(Value = "needed")]
Needed = 1,
/// <summary>
/// Ok.
/// </summary>
[EnumMember(Value = "ok")]
Ok = 2,
/// <summary>
/// Pending.
/// </summary>
[EnumMember(Value = "pending")]
Pending = 3,
/// <summary>
/// Trying.
/// </summary>
[EnumMember(Value = "trying")]
Trying = 4,
/// <summary>
/// Rejected.
/// </summary>
[EnumMember(Value = "rejected")]
Rejected = 5
}
/// <summary>
/// <see href="https://github.com/cloudflare/cloudflare-typescript/blob/v4.4.1/src/resources/registrar/domains.ts#L248">Source</see>
/// </summary>
[JsonConverter(typeof(StringEnumConverter))]
public enum DomainTransferInUnlockDomain
{
/// <summary>
/// Needed.
/// </summary>
[EnumMember(Value = "needed")]
Needed = 1,
/// <summary>
/// Ok.
/// </summary>
[EnumMember(Value = "ok")]
Ok = 2,
/// <summary>
/// Pending.
/// </summary>
[EnumMember(Value = "pending")]
Pending = 3,
/// <summary>
/// Trying.
/// </summary>
[EnumMember(Value = "trying")]
Trying = 4,
/// <summary>
/// Unknown.
/// </summary>
[EnumMember(Value = "unknown")]
Unknown = 5
}
}

View File

@@ -4,32 +4,20 @@ This package contains the feature set of the _Domain/Zone Management_ section of
## Implemented Methods ## 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/ [choose a license]: https://choosealicense.com/licenses/mit/
[Registrar]: https://developers.cloudflare.com/api/resources/registrar/

View File

@@ -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
{
/// <summary>
/// Extensions for <see href="https://developers.cloudflare.com/api/resources/registrar/">Registrar</see>.
/// </summary>
public static class RegistrarExtensions
{
/// <summary>
/// Show individual domain.
/// </summary>
/// <param name="client">The <see cref="ICloudflareClient"/> instance.</param>
/// <param name="accountId">The account id.</param>
/// <param name="domainName">The domain name.</param>
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
public static Task<CloudflareResponse<JToken>> 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<JToken>($"/accounts/{accountId}/registrar/domains/{domainName}", cancellationToken: cancellationToken);
}
/// <summary>
/// List domains handled by Registrar.
/// </summary>
/// <param name="client">The <see cref="ICloudflareClient"/> instance.</param>
/// <param name="accountId">The account id.</param>
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
public static Task<CloudflareResponse<IReadOnlyCollection<Domain>>> ListDomains(this ICloudflareClient client, string accountId, CancellationToken cancellationToken = default)
{
accountId.ValidateCloudflareId();
return client.GetAsync<IReadOnlyCollection<Domain>>($"/accounts/{accountId}/registrar/domains", cancellationToken: cancellationToken);
}
/// <summary>
/// Update individual domain.
/// </summary>
/// <param name="client">The <see cref="ICloudflareClient"/> instance.</param>
/// <param name="request">The request.</param>
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
public static Task<CloudflareResponse<JToken>> 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<JToken, InternalUpdateDomainRequest>($"/accounts/{request.AccountId}/registrar/domains/{request.DomainName}", req, cancellationToken);
}
}
}

View File

@@ -0,0 +1,45 @@
namespace AMWD.Net.Api.Cloudflare.Zones
{
/// <summary>
/// Represents a request to update a domain.
/// <see href="https://github.com/cloudflare/cloudflare-typescript/blob/v4.4.1/src/resources/registrar/domains.ts#L256">Source</see>
/// </summary>
public class UpdateDomainRequest
{
/// <summary>
/// Initializes a new instance of the <see cref="UpdateDomainRequest"/> class.
/// </summary>
/// <param name="accountId">Identifier.</param>
/// <param name="domainName">Domain name.</param>
public UpdateDomainRequest(string accountId, string domainName)
{
AccountId = accountId;
DomainName = domainName;
}
/// <summary>
/// Identifier.
/// </summary>
public string AccountId { get; set; }
/// <summary>
/// Domain name.
/// </summary>
public string DomainName { get; set; }
/// <summary>
/// Auto-renew controls whether subscription is automatically renewed upon domain expiration.
/// </summary>
public bool? AutoRenew { get; set; }
/// <summary>
/// Shows whether a registrar lock is in place for a domain.
/// </summary>
public bool? Locked { get; set; }
/// <summary>
/// Privacy option controls redacting WHOIS information.
/// </summary>
public bool? Privacy { get; set; }
}
}

View File

@@ -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<ICloudflareClient> _clientMock;
private CloudflareResponse<JToken> _response;
private List<(string RequestPath, IQueryParameterFilter QueryFilter)> _callbacks;
[TestInitialize]
public void Initialize()
{
_callbacks = [];
_response = new CloudflareResponse<JToken>();
}
[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<JToken>($"/accounts/{AccountId}/registrar/domains/{DomainName}", null, It.IsAny<CancellationToken>()), 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<ICloudflareClient>();
_clientMock
.Setup(m => m.GetAsync<JToken>(It.IsAny<string>(), It.IsAny<IQueryParameterFilter>(), It.IsAny<CancellationToken>()))
.Callback<string, IQueryParameterFilter, CancellationToken>((requestPath, queryFilter, _) => _callbacks.Add((requestPath, queryFilter)))
.ReturnsAsync(() => _response);
return _clientMock.Object;
}
}
}

View File

@@ -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<ICloudflareClient> _clientMock;
private CloudflareResponse<IReadOnlyCollection<Domain>> _response;
private List<(string RequestPath, IQueryParameterFilter QueryFilter)> _callbacks;
[TestInitialize]
public void Initialize()
{
_callbacks = [];
_response = new CloudflareResponse<IReadOnlyCollection<Domain>>();
}
[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<IReadOnlyCollection<Domain>>($"/accounts/{AccountId}/registrar/domains", null, It.IsAny<CancellationToken>()), Times.Once);
_clientMock.VerifyNoOtherCalls();
}
private ICloudflareClient GetClient()
{
_clientMock = new Mock<ICloudflareClient>();
_clientMock
.Setup(m => m.GetAsync<IReadOnlyCollection<Domain>>(It.IsAny<string>(), It.IsAny<IQueryParameterFilter>(), It.IsAny<CancellationToken>()))
.Callback<string, IQueryParameterFilter, CancellationToken>((requestPath, queryFilter, _) => _callbacks.Add((requestPath, queryFilter)))
.ReturnsAsync(() => _response);
return _clientMock.Object;
}
}
}

View File

@@ -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<ICloudflareClient> _clientMock;
private CloudflareResponse<JToken> _response;
private List<(string RequestPath, InternalUpdateDomainRequest Request)> _callbacks;
private UpdateDomainRequest _request;
[TestInitialize]
public void Initialize()
{
_callbacks = [];
_response = new CloudflareResponse<JToken>();
_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<JToken, InternalUpdateDomainRequest>($"/accounts/{AccountId}/registrar/domains/{DomainName}", It.IsAny<InternalUpdateDomainRequest>(), It.IsAny<CancellationToken>()), 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<ICloudflareClient>();
_clientMock
.Setup(m => m.PutAsync<JToken, InternalUpdateDomainRequest>(It.IsAny<string>(), It.IsAny<InternalUpdateDomainRequest>(), It.IsAny<CancellationToken>()))
.Callback<string, InternalUpdateDomainRequest, CancellationToken>((requestPath, request, _) => _callbacks.Add((requestPath, request)))
.ReturnsAsync(() => _response);
return _clientMock.Object;
}
}
}