Added 'Account Custom Nameserver' extensions

This commit is contained in:
2025-07-10 12:37:14 +02:00
parent 8ae7681fc8
commit 9f97e2b17d
8 changed files with 488 additions and 0 deletions

View File

@@ -0,0 +1,61 @@
using System.Threading;
using System.Threading.Tasks;
using AMWD.Net.Api.Cloudflare.Dns.Internals;
namespace AMWD.Net.Api.Cloudflare.Dns
{
/// <summary>
/// Extensions for <see href="https://developers.cloudflare.com/api/resources/custom_nameservers/">Account Custom Nameservers</see>.
/// </summary>
public static class CustomNameserversExtensions
{
/// <summary>
/// Add Account Custom Nameserver.
/// </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<CustomNameserver>> AddCustomNameserver(this ICloudflareClient client, AddCustomNameserverRequest request, CancellationToken cancellationToken = default)
{
request.AccountId.ValidateCloudflareId();
var req = new InternalAddCustomNameserverRequest
{
NameserverName = request.NameserverName,
NameserverSet = request.NameserverSet
};
return client.PostAsync<CustomNameserver, InternalAddCustomNameserverRequest>($"/accounts/{request.AccountId}/custom_ns", req, null, cancellationToken);
}
/// <summary>
/// Delete Account Custom Nameserver.
/// </summary>
/// <param name="client">The <see cref="ICloudflareClient"/> instance.</param>
/// <param name="accountId">The account identifier.</param>
/// <param name="nameserverId">The nameserver identifier.</param>
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
public static Task<CloudflareResponse<IReadOnlyCollection<string>>> DeleteCustomNameserver(this ICloudflareClient client, string accountId, string nameserverId, CancellationToken cancellationToken = default)
{
accountId.ValidateCloudflareId();
if (string.IsNullOrWhiteSpace(nameserverId))
throw new ArgumentNullException(nameof(nameserverId));
return client.DeleteAsync<IReadOnlyCollection<string>>($"/accounts/{accountId}/custom_ns/{nameserverId}", null, cancellationToken);
}
/// <summary>
/// List an account's custom nameservers.
/// </summary>
/// <param name="client">The <see cref="ICloudflareClient"/> instance.</param>
/// <param name="accountId">The account identifier.</param>
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
public static Task<CloudflareResponse<IReadOnlyCollection<CustomNameserver>>> ListCustomNameserver(this ICloudflareClient client, string accountId, CancellationToken cancellationToken = default)
{
accountId.ValidateCloudflareId();
return client.GetAsync<IReadOnlyCollection<CustomNameserver>>($"/accounts/{accountId}/custom_ns", null, cancellationToken);
}
}
}

View File

@@ -0,0 +1,11 @@
namespace AMWD.Net.Api.Cloudflare.Dns.Internals
{
internal class InternalAddCustomNameserverRequest
{
[JsonProperty("ns_name")]
public string? NameserverName { get; set; }
[JsonProperty("ns_set")]
public int? NameserverSet { get; set; }
}
}

View File

@@ -0,0 +1,121 @@
using System.Runtime.Serialization;
using Newtonsoft.Json.Converters;
namespace AMWD.Net.Api.Cloudflare.Dns
{
/// <summary>
/// A Cloudflare custom nameserver.
/// <see href="https://github.com/cloudflare/cloudflare-typescript/blob/v4.4.1/src/resources/custom-nameservers.ts#L88">Source</see>
/// </summary>
public class CustomNameserver
{
/// <summary>
/// Initializes a new instance of the <see cref="CustomNameserver"/> class.
/// </summary>
/// <param name="nameserverName">The FQDN of the name server.</param>
public CustomNameserver(string nameserverName)
{
NameserverName = nameserverName;
}
/// <summary>
/// A and AAAA records associated with the nameserver.
/// </summary>
[JsonProperty("dns_records")]
public IReadOnlyCollection<CustomNameserverDnsRecord>? DnsRecords { get; set; }
/// <summary>
/// The full qualified domain name (FQDN) of the name server.
/// </summary>
[JsonProperty("ns_name")]
public string NameserverName { get; set; }
/// <summary>
/// Verification status of the nameserver.
/// </summary>
[Obsolete]
[JsonProperty("status")]
public CustomNameserverStatus? Status { get; set; }
/// <summary>
/// Identifier.
/// </summary>
[JsonProperty("zone_tag")]
public string? ZoneTag { get; set; }
/// <summary>
/// The number of the set that this name server belongs to.
/// </summary>
/// <remarks>
/// <c>1 &lt;= X &lt;= 5</c>
/// <br/>
/// Default: 1
/// </remarks>
[JsonProperty("ns_set")]
public int? NameserverSet { get; set; }
}
/// <summary>
/// Records associated with the nameserver.
/// </summary>
public class CustomNameserverDnsRecord
{
/// <summary>
/// DNS record type.
/// </summary>
[JsonProperty("type")]
public CustomNameserverDnsRecordType? Type { get; set; }
/// <summary>
/// DNS record contents (an IPv4 or IPv6 address).
/// </summary>
[JsonProperty("value")]
public string? Value { get; set; }
}
/// <summary>
/// Record types.
/// <see href="https://github.com/cloudflare/cloudflare-typescript/blob/v4.4.1/src/resources/custom-nameservers.ts#L120">Source</see>
/// </summary>
[JsonConverter(typeof(StringEnumConverter))]
public enum CustomNameserverDnsRecordType
{
/// <summary>
/// IPv4 record.
/// </summary>
[EnumMember(Value = "A")]
A = 1,
/// <summary>
/// IPv6 record.
/// </summary>
[EnumMember(Value = "AAAA")]
AAAA = 2
}
/// <summary>
/// Custom nameserver states.
/// <see href="https://github.com/cloudflare/cloudflare-typescript/blob/v4.4.1/src/resources/custom-nameservers.ts#L102">Source</see>
/// </summary>
[JsonConverter(typeof(StringEnumConverter))]
public enum CustomNameserverStatus
{
/// <summary>
/// The nameserver has been moved.
/// </summary>
[EnumMember(Value = "moved")]
Moved = 1,
/// <summary>
/// The nameserver is pending verification.
/// </summary>
[EnumMember(Value = "pending")]
Pending = 2,
/// <summary>
/// The nameserver has been verified.
/// </summary>
[EnumMember(Value = "verified")]
Verified = 3
}
}

View File

@@ -4,6 +4,13 @@ This package contains the feature set of the _DNS_ section of the Cloudflare API
## Implemented Methods
### [Account Custom Nameservers]
- [Add Account Custom Nameserver](https://developers.cloudflare.com/api/resources/custom_nameservers/methods/create/)
- [Delete Account Custom Nameserver](https://developers.cloudflare.com/api/resources/custom_nameservers/methods/delete/)
- [List Account Custom Nameservers](https://developers.cloudflare.com/api/resources/custom_nameservers/methods/get/)
---
@@ -12,3 +19,5 @@ Published under MIT License (see [choose a license])
[choose a license]: https://choosealicense.com/licenses/mit/
[Account Custom Nameservers]: https://developers.cloudflare.com/api/resources/custom_nameservers/

View File

@@ -0,0 +1,34 @@
namespace AMWD.Net.Api.Cloudflare.Dns
{
/// <summary>
/// Represents a request to add a custom nameserver.
/// </summary>
public class AddCustomNameserverRequest
{
/// <summary>
/// Initializes a new instance of the <see cref="AddCustomNameserverRequest"/> class.
/// </summary>
/// <param name="accountId">The account identifier.</param>
/// <param name="nameserverName">The FQDN of the name server.</param>
public AddCustomNameserverRequest(string accountId, string nameserverName)
{
AccountId = accountId;
NameserverName = nameserverName;
}
/// <summary>
/// The account identifier.
/// </summary>
public string AccountId { get; set; }
/// <summary>
/// The FQDN of the name server.
/// </summary>
public string NameserverName { get; set; }
/// <summary>
/// The number of the set that this name server belongs to.
/// </summary>
public int? NameserverSet { get; set; }
}
}

View File

@@ -0,0 +1,84 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AMWD.Net.Api.Cloudflare;
using AMWD.Net.Api.Cloudflare.Dns;
using AMWD.Net.Api.Cloudflare.Dns.Internals;
using Moq;
namespace Cloudflare.Dns.Tests.CustomNameserversExtensions
{
[TestClass]
public class AddCustomNameserverTest
{
private const string AccountId = "023e105f4ecef8ad9ca31a8372d0c353";
private const string Nameserver = "ns1.example.com";
private Mock<ICloudflareClient> _clientMock;
private CloudflareResponse<CustomNameserver> _response;
private List<(string RequestPath, InternalAddCustomNameserverRequest Request)> _callbacks;
private AddCustomNameserverRequest _request;
[TestInitialize]
public void Initialize()
{
_callbacks = [];
_response = new CloudflareResponse<CustomNameserver>
{
Success = true,
Messages = [
new ResponseInfo(1000, "Message 1")
],
Errors = [
new ResponseInfo(1000, "Error 1")
],
Result = new CustomNameserver(Nameserver)
};
_request = new AddCustomNameserverRequest(AccountId, Nameserver);
}
[TestMethod]
public async Task ShouldAddCustomNameserver()
{
// Arrange
var client = GetClient();
// Act
var response = await client.AddCustomNameserver(_request);
// Assert
Assert.IsNotNull(response);
Assert.IsTrue(response.Success);
Assert.AreEqual(_response.Result, response.Result);
Assert.AreEqual(1, _callbacks.Count);
var callback = _callbacks.First();
Assert.AreEqual($"/accounts/{AccountId}/custom_ns", callback.RequestPath);
Assert.IsNotNull(callback.Request);
Assert.AreEqual(_request.NameserverName, callback.Request.NameserverName);
Assert.IsNull(callback.Request.NameserverSet);
_clientMock.Verify(m => m.PostAsync<CustomNameserver, InternalAddCustomNameserverRequest>($"/accounts/{AccountId}/custom_ns", It.IsAny<InternalAddCustomNameserverRequest>(), null, It.IsAny<CancellationToken>()), Times.Once);
_clientMock.VerifyNoOtherCalls();
}
private ICloudflareClient GetClient()
{
_clientMock = new Mock<ICloudflareClient>();
_clientMock
.Setup(m => m.PostAsync<CustomNameserver, InternalAddCustomNameserverRequest>(It.IsAny<string>(), It.IsAny<InternalAddCustomNameserverRequest>(), It.IsAny<IQueryParameterFilter>(), It.IsAny<CancellationToken>()))
.Callback<string, InternalAddCustomNameserverRequest, IQueryParameterFilter, CancellationToken>((requestPath, request, _, _) => _callbacks.Add((requestPath, request)))
.ReturnsAsync(() => _response);
return _clientMock.Object;
}
}
}

View File

@@ -0,0 +1,92 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AMWD.Net.Api.Cloudflare;
using AMWD.Net.Api.Cloudflare.Dns;
using Moq;
namespace Cloudflare.Dns.Tests.CustomNameserversExtensions
{
[TestClass]
public class DeleteCustomNameserverTest
{
private const string AccountId = "023e105f4ecef8ad9ca31a8372d0c353";
private const string Nameserver = "ns1.example.com";
private Mock<ICloudflareClient> _clientMock;
private CloudflareResponse<IReadOnlyCollection<string>> _response;
private List<(string RequestPath, IQueryParameterFilter QueryFilter)> _callbacks;
[TestInitialize]
public void Initialize()
{
_callbacks = [];
_response = new CloudflareResponse<IReadOnlyCollection<string>>
{
Success = true,
Messages = [
new ResponseInfo(1000, "Message 1")
],
Errors = [
new ResponseInfo(1000, "Error 1")
],
Result = []
};
}
[TestMethod]
public async Task ShouldDeleteCustomNameserver()
{
// Arrange
var client = GetClient();
// Act
var response = await client.DeleteCustomNameserver(AccountId, Nameserver);
// Assert
Assert.IsNotNull(response);
Assert.IsTrue(response.Success);
Assert.AreEqual(_response.Result, response.Result);
Assert.AreEqual(1, _callbacks.Count);
var callback = _callbacks.First();
Assert.AreEqual($"/accounts/{AccountId}/custom_ns/{Nameserver}", callback.RequestPath);
Assert.IsNull(callback.QueryFilter);
_clientMock.Verify(m => m.DeleteAsync<IReadOnlyCollection<string>>($"/accounts/{AccountId}/custom_ns/{Nameserver}", null, It.IsAny<CancellationToken>()), Times.Once);
_clientMock.VerifyNoOtherCalls();
}
[DataTestMethod]
[DataRow(null)]
[DataRow("")]
[DataRow(" ")]
[ExpectedException(typeof(ArgumentNullException))]
public async Task ShouldDeleteCustomNameserver(string nameserver)
{
// Arrange
var client = GetClient();
// Act
var response = await client.DeleteCustomNameserver(AccountId, nameserver);
// Assert - ArgumentNullException
}
private ICloudflareClient GetClient()
{
_clientMock = new Mock<ICloudflareClient>();
_clientMock
.Setup(m => m.DeleteAsync<IReadOnlyCollection<string>>(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,76 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AMWD.Net.Api.Cloudflare;
using AMWD.Net.Api.Cloudflare.Dns;
using Moq;
namespace Cloudflare.Dns.Tests.CustomNameserversExtensions
{
[TestClass]
public class ListCustomNameserverTest
{
private const string AccountId = "023e105f4ecef8ad9ca31a8372d0c353";
private const string Nameserver = "ns1.example.com";
private Mock<ICloudflareClient> _clientMock;
private CloudflareResponse<IReadOnlyCollection<CustomNameserver>> _response;
private List<(string RequestPath, IQueryParameterFilter QueryFilter)> _callbacks;
[TestInitialize]
public void Initialize()
{
_callbacks = [];
_response = new CloudflareResponse<IReadOnlyCollection<CustomNameserver>>
{
Success = true,
Messages = [
new ResponseInfo(1000, "Message 1")
],
Errors = [
new ResponseInfo(1000, "Error 1")
],
Result = []
};
}
[TestMethod]
public async Task ShouldListCustomNameserver()
{
// Arrange
var client = GetClient();
// Act
var response = await client.ListCustomNameserver(AccountId);
// Assert
Assert.IsNotNull(response);
Assert.IsTrue(response.Success);
Assert.AreEqual(_response.Result, response.Result);
Assert.AreEqual(1, _callbacks.Count);
var callback = _callbacks.First();
Assert.AreEqual($"/accounts/{AccountId}/custom_ns", callback.RequestPath);
Assert.IsNull(callback.QueryFilter);
_clientMock.Verify(m => m.GetAsync<IReadOnlyCollection<CustomNameserver>>($"/accounts/{AccountId}/custom_ns", null, It.IsAny<CancellationToken>()), Times.Once);
_clientMock.VerifyNoOtherCalls();
}
private ICloudflareClient GetClient()
{
_clientMock = new Mock<ICloudflareClient>();
_clientMock
.Setup(m => m.GetAsync<IReadOnlyCollection<CustomNameserver>>(It.IsAny<string>(), It.IsAny<IQueryParameterFilter>(), It.IsAny<CancellationToken>()))
.Callback<string, IQueryParameterFilter, CancellationToken>((requestPath, queryFilter, _) => _callbacks.Add((requestPath, queryFilter)))
.ReturnsAsync(() => _response);
return _clientMock.Object;
}
}
}