From 9f97e2b17d66b651a6b18dbcbed730603b04ba0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20M=C3=BCller?= Date: Thu, 10 Jul 2025 12:37:14 +0200 Subject: [PATCH] Added 'Account Custom Nameserver' extensions --- .../CustomNameserversExtensions.cs | 61 +++++++++ .../InternalAddCustomNameserverRequest.cs | 11 ++ .../Cloudflare.Dns/Models/CustomNameserver.cs | 121 ++++++++++++++++++ src/Extensions/Cloudflare.Dns/README.md | 9 ++ .../Requests/AddCustomNameserverRequest.cs | 34 +++++ .../AddCustomNameserverTest.cs | 84 ++++++++++++ .../DeleteCustomNameserverTest.cs | 92 +++++++++++++ .../ListCustomNameserverTest.cs | 76 +++++++++++ 8 files changed, 488 insertions(+) create mode 100644 src/Extensions/Cloudflare.Dns/CustomNameserversExtensions.cs create mode 100644 src/Extensions/Cloudflare.Dns/Internals/InternalAddCustomNameserverRequest.cs create mode 100644 src/Extensions/Cloudflare.Dns/Models/CustomNameserver.cs create mode 100644 src/Extensions/Cloudflare.Dns/Requests/AddCustomNameserverRequest.cs create mode 100644 test/Extensions/Cloudflare.Dns.Tests/CustomNameserversExtensions/AddCustomNameserverTest.cs create mode 100644 test/Extensions/Cloudflare.Dns.Tests/CustomNameserversExtensions/DeleteCustomNameserverTest.cs create mode 100644 test/Extensions/Cloudflare.Dns.Tests/CustomNameserversExtensions/ListCustomNameserverTest.cs diff --git a/src/Extensions/Cloudflare.Dns/CustomNameserversExtensions.cs b/src/Extensions/Cloudflare.Dns/CustomNameserversExtensions.cs new file mode 100644 index 0000000..d4e5be7 --- /dev/null +++ b/src/Extensions/Cloudflare.Dns/CustomNameserversExtensions.cs @@ -0,0 +1,61 @@ +using System.Threading; +using System.Threading.Tasks; +using AMWD.Net.Api.Cloudflare.Dns.Internals; + +namespace AMWD.Net.Api.Cloudflare.Dns +{ + /// + /// Extensions for Account Custom Nameservers. + /// + public static class CustomNameserversExtensions + { + /// + /// Add Account Custom Nameserver. + /// + /// The instance. + /// The request. + /// A cancellation token used to propagate notification that this operation should be canceled. + public static Task> 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($"/accounts/{request.AccountId}/custom_ns", req, null, cancellationToken); + } + + /// + /// Delete Account Custom Nameserver. + /// + /// The instance. + /// The account identifier. + /// The nameserver identifier. + /// A cancellation token used to propagate notification that this operation should be canceled. + public static Task>> 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>($"/accounts/{accountId}/custom_ns/{nameserverId}", null, cancellationToken); + } + + /// + /// List an account's custom nameservers. + /// + /// The instance. + /// The account identifier. + /// A cancellation token used to propagate notification that this operation should be canceled. + public static Task>> ListCustomNameserver(this ICloudflareClient client, string accountId, CancellationToken cancellationToken = default) + { + accountId.ValidateCloudflareId(); + + return client.GetAsync>($"/accounts/{accountId}/custom_ns", null, cancellationToken); + } + } +} diff --git a/src/Extensions/Cloudflare.Dns/Internals/InternalAddCustomNameserverRequest.cs b/src/Extensions/Cloudflare.Dns/Internals/InternalAddCustomNameserverRequest.cs new file mode 100644 index 0000000..16840e4 --- /dev/null +++ b/src/Extensions/Cloudflare.Dns/Internals/InternalAddCustomNameserverRequest.cs @@ -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; } + } +} diff --git a/src/Extensions/Cloudflare.Dns/Models/CustomNameserver.cs b/src/Extensions/Cloudflare.Dns/Models/CustomNameserver.cs new file mode 100644 index 0000000..25201bc --- /dev/null +++ b/src/Extensions/Cloudflare.Dns/Models/CustomNameserver.cs @@ -0,0 +1,121 @@ +using System.Runtime.Serialization; +using Newtonsoft.Json.Converters; + +namespace AMWD.Net.Api.Cloudflare.Dns +{ + /// + /// A Cloudflare custom nameserver. + /// Source + /// + public class CustomNameserver + { + /// + /// Initializes a new instance of the class. + /// + /// The FQDN of the name server. + public CustomNameserver(string nameserverName) + { + NameserverName = nameserverName; + } + + /// + /// A and AAAA records associated with the nameserver. + /// + [JsonProperty("dns_records")] + public IReadOnlyCollection? DnsRecords { get; set; } + + /// + /// The full qualified domain name (FQDN) of the name server. + /// + [JsonProperty("ns_name")] + public string NameserverName { get; set; } + + /// + /// Verification status of the nameserver. + /// + [Obsolete] + [JsonProperty("status")] + public CustomNameserverStatus? Status { get; set; } + + /// + /// Identifier. + /// + [JsonProperty("zone_tag")] + public string? ZoneTag { get; set; } + + /// + /// The number of the set that this name server belongs to. + /// + /// + /// 1 <= X <= 5 + ///
+ /// Default: 1 + ///
+ [JsonProperty("ns_set")] + public int? NameserverSet { get; set; } + } + + /// + /// Records associated with the nameserver. + /// + public class CustomNameserverDnsRecord + { + /// + /// DNS record type. + /// + [JsonProperty("type")] + public CustomNameserverDnsRecordType? Type { get; set; } + + /// + /// DNS record contents (an IPv4 or IPv6 address). + /// + [JsonProperty("value")] + public string? Value { get; set; } + } + + /// + /// Record types. + /// Source + /// + [JsonConverter(typeof(StringEnumConverter))] + public enum CustomNameserverDnsRecordType + { + /// + /// IPv4 record. + /// + [EnumMember(Value = "A")] + A = 1, + + /// + /// IPv6 record. + /// + [EnumMember(Value = "AAAA")] + AAAA = 2 + } + + /// + /// Custom nameserver states. + /// Source + /// + [JsonConverter(typeof(StringEnumConverter))] + public enum CustomNameserverStatus + { + /// + /// The nameserver has been moved. + /// + [EnumMember(Value = "moved")] + Moved = 1, + + /// + /// The nameserver is pending verification. + /// + [EnumMember(Value = "pending")] + Pending = 2, + + /// + /// The nameserver has been verified. + /// + [EnumMember(Value = "verified")] + Verified = 3 + } +} diff --git a/src/Extensions/Cloudflare.Dns/README.md b/src/Extensions/Cloudflare.Dns/README.md index 1d7a63a..7f9f404 100644 --- a/src/Extensions/Cloudflare.Dns/README.md +++ b/src/Extensions/Cloudflare.Dns/README.md @@ -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/ diff --git a/src/Extensions/Cloudflare.Dns/Requests/AddCustomNameserverRequest.cs b/src/Extensions/Cloudflare.Dns/Requests/AddCustomNameserverRequest.cs new file mode 100644 index 0000000..c01b0b9 --- /dev/null +++ b/src/Extensions/Cloudflare.Dns/Requests/AddCustomNameserverRequest.cs @@ -0,0 +1,34 @@ +namespace AMWD.Net.Api.Cloudflare.Dns +{ + /// + /// Represents a request to add a custom nameserver. + /// + public class AddCustomNameserverRequest + { + /// + /// Initializes a new instance of the class. + /// + /// The account identifier. + /// The FQDN of the name server. + public AddCustomNameserverRequest(string accountId, string nameserverName) + { + AccountId = accountId; + NameserverName = nameserverName; + } + + /// + /// The account identifier. + /// + public string AccountId { get; set; } + + /// + /// The FQDN of the name server. + /// + public string NameserverName { get; set; } + + /// + /// The number of the set that this name server belongs to. + /// + public int? NameserverSet { get; set; } + } +} diff --git a/test/Extensions/Cloudflare.Dns.Tests/CustomNameserversExtensions/AddCustomNameserverTest.cs b/test/Extensions/Cloudflare.Dns.Tests/CustomNameserversExtensions/AddCustomNameserverTest.cs new file mode 100644 index 0000000..73e726f --- /dev/null +++ b/test/Extensions/Cloudflare.Dns.Tests/CustomNameserversExtensions/AddCustomNameserverTest.cs @@ -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 _clientMock; + + private CloudflareResponse _response; + + private List<(string RequestPath, InternalAddCustomNameserverRequest Request)> _callbacks; + + private AddCustomNameserverRequest _request; + + [TestInitialize] + public void Initialize() + { + _callbacks = []; + + _response = new CloudflareResponse + { + 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($"/accounts/{AccountId}/custom_ns", It.IsAny(), null, It.IsAny()), Times.Once); + _clientMock.VerifyNoOtherCalls(); + } + + private ICloudflareClient GetClient() + { + _clientMock = new Mock(); + _clientMock + .Setup(m => m.PostAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((requestPath, request, _, _) => _callbacks.Add((requestPath, request))) + .ReturnsAsync(() => _response); + + return _clientMock.Object; + } + } +} diff --git a/test/Extensions/Cloudflare.Dns.Tests/CustomNameserversExtensions/DeleteCustomNameserverTest.cs b/test/Extensions/Cloudflare.Dns.Tests/CustomNameserversExtensions/DeleteCustomNameserverTest.cs new file mode 100644 index 0000000..d91e50f --- /dev/null +++ b/test/Extensions/Cloudflare.Dns.Tests/CustomNameserversExtensions/DeleteCustomNameserverTest.cs @@ -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 _clientMock; + + private CloudflareResponse> _response; + + private List<(string RequestPath, IQueryParameterFilter QueryFilter)> _callbacks; + + [TestInitialize] + public void Initialize() + { + _callbacks = []; + + _response = new CloudflareResponse> + { + 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>($"/accounts/{AccountId}/custom_ns/{Nameserver}", null, It.IsAny()), 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(); + _clientMock + .Setup(m => m.DeleteAsync>(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((requestPath, queryFilter, _) => _callbacks.Add((requestPath, queryFilter))) + .ReturnsAsync(() => _response); + + return _clientMock.Object; + } + } +} diff --git a/test/Extensions/Cloudflare.Dns.Tests/CustomNameserversExtensions/ListCustomNameserverTest.cs b/test/Extensions/Cloudflare.Dns.Tests/CustomNameserversExtensions/ListCustomNameserverTest.cs new file mode 100644 index 0000000..4bb2372 --- /dev/null +++ b/test/Extensions/Cloudflare.Dns.Tests/CustomNameserversExtensions/ListCustomNameserverTest.cs @@ -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 _clientMock; + + private CloudflareResponse> _response; + + private List<(string RequestPath, IQueryParameterFilter QueryFilter)> _callbacks; + + [TestInitialize] + public void Initialize() + { + _callbacks = []; + + _response = new CloudflareResponse> + { + 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>($"/accounts/{AccountId}/custom_ns", 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; + } + } +}