From e49e975388cd6aa5975da249b81e0a9e76a0df49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20M=C3=BCller?= Date: Tue, 29 Jul 2025 11:06:07 +0200 Subject: [PATCH] Add 'DNS Zone Settings' implementation --- .../DnsZoneSettingsExtensions.cs | 86 +++++ .../Cloudflare.Dns/Enums/DnsZoneMode.cs | 31 ++ .../InternalUpdateDnsZoneSettingsRequest.cs | 32 ++ .../Models/DnsZoneInternalDns.cs | 14 + .../Models/DnsZoneNameservers.cs | 64 ++++ .../Cloudflare.Dns/Models/DnsZoneSettings.cs | 66 ++++ .../Cloudflare.Dns/Models/DnsZoneSoa.cs | 75 ++++ src/Extensions/Cloudflare.Dns/README.md | 11 + .../Requests/UpdateDnsZoneSettingsRequest.cs | 71 ++++ .../ShowDnsZoneSettingsTest.cs | 97 +++++ .../UpdateDnsZoneSettingsTest.cs | 335 ++++++++++++++++++ 11 files changed, 882 insertions(+) create mode 100644 src/Extensions/Cloudflare.Dns/DnsZoneSettingsExtensions.cs create mode 100644 src/Extensions/Cloudflare.Dns/Enums/DnsZoneMode.cs create mode 100644 src/Extensions/Cloudflare.Dns/Internals/InternalUpdateDnsZoneSettingsRequest.cs create mode 100644 src/Extensions/Cloudflare.Dns/Models/DnsZoneInternalDns.cs create mode 100644 src/Extensions/Cloudflare.Dns/Models/DnsZoneNameservers.cs create mode 100644 src/Extensions/Cloudflare.Dns/Models/DnsZoneSettings.cs create mode 100644 src/Extensions/Cloudflare.Dns/Models/DnsZoneSoa.cs create mode 100644 src/Extensions/Cloudflare.Dns/Requests/UpdateDnsZoneSettingsRequest.cs create mode 100644 test/Extensions/Cloudflare.Dns.Tests/DnsZoneSettingsExtensions/ShowDnsZoneSettingsTest.cs create mode 100644 test/Extensions/Cloudflare.Dns.Tests/DnsZoneSettingsExtensions/UpdateDnsZoneSettingsTest.cs diff --git a/src/Extensions/Cloudflare.Dns/DnsZoneSettingsExtensions.cs b/src/Extensions/Cloudflare.Dns/DnsZoneSettingsExtensions.cs new file mode 100644 index 0000000..e87380b --- /dev/null +++ b/src/Extensions/Cloudflare.Dns/DnsZoneSettingsExtensions.cs @@ -0,0 +1,86 @@ +using System.Threading; +using System.Threading.Tasks; +using AMWD.Net.Api.Cloudflare.Dns.Internals; + +namespace AMWD.Net.Api.Cloudflare.Dns +{ + /// + /// Extensions for DNS Zone Settings. + /// + public static class DnsZoneSettingsExtensions + { + /// + /// Update DNS settings for a zone. + /// + /// The instance. + /// The request. + /// A cancellation token used to propagate notification that this operation should be canceled. + public static Task> UpdateDnsZoneSettings(this ICloudflareClient client, UpdateDnsZoneSettingsRequest request, CancellationToken cancellationToken = default) + { + request.ZoneId.ValidateCloudflareId(); + + if (request.ZoneMode.HasValue && !Enum.IsDefined(typeof(DnsZoneMode), request.ZoneMode)) + throw new ArgumentOutOfRangeException(nameof(request.ZoneMode), request.ZoneMode, "Value must be one of the ZoneMode enum values."); + + if (request.Nameservers != null && !Enum.IsDefined(typeof(DnsZoneNameserversType), request.Nameservers.Type)) + throw new ArgumentOutOfRangeException($"{nameof(request.Nameservers)}.{nameof(request.Nameservers.Type)}", request.Nameservers.Type, "Value must be one of the NameserverType enum values."); + + if (request.NameserverTtl.HasValue && (request.NameserverTtl < 30 || 86400 < request.NameserverTtl)) + throw new ArgumentOutOfRangeException(nameof(request.NameserverTtl), request.NameserverTtl, "Value must be between 30 and 86400."); + + if (request.SOA != null) + { + string paramNameBase = $"{nameof(request.SOA)}"; + + if (request.SOA.Expire < 86400 || 2419200 < request.SOA.Expire) + throw new ArgumentOutOfRangeException($"{paramNameBase}.{nameof(request.SOA.Expire)}", request.SOA.Expire, "Value must be between 86400 and 2419200."); + + if (request.SOA.MinimumTtl < 60 || 86400 < request.SOA.MinimumTtl) + throw new ArgumentOutOfRangeException($"{paramNameBase}.{nameof(request.SOA.MinimumTtl)}", request.SOA.MinimumTtl, "Value must be between 60 and 86400."); + + if (string.IsNullOrWhiteSpace(request.SOA.PrimaryNameserver)) + throw new ArgumentNullException($"{paramNameBase}.{nameof(request.SOA.PrimaryNameserver)}"); + + if (request.SOA.Refresh < 600 || 86400 < request.SOA.Refresh) + throw new ArgumentOutOfRangeException($"{paramNameBase}.{nameof(request.SOA.Refresh)}", request.SOA.Refresh, "Value must be between 600 and 86400."); + + if (request.SOA.Retry < 600 || 86400 < request.SOA.Retry) + throw new ArgumentOutOfRangeException($"{paramNameBase}.{nameof(request.SOA.Retry)}", request.SOA.Retry, "Value must be between 600 and 86400."); + + if (request.SOA.TimeToLive < 300 || 86400 < request.SOA.TimeToLive) + throw new ArgumentOutOfRangeException($"{paramNameBase}.{nameof(request.SOA.TimeToLive)}", request.SOA.TimeToLive, "Value must be between 300 and 86400."); + + if (string.IsNullOrWhiteSpace(request.SOA.ZoneAdministrator)) + throw new ArgumentNullException($"{paramNameBase}.{nameof(request.SOA.ZoneAdministrator)}"); + } + + var req = new InternalUpdateDnsZoneSettingsRequest + { + FlattenAllCnames = request.FlattenAllCnames, + FoundationDns = request.FoundationDns, + InternalDns = request.InternalDns, + MultiProvider = request.MultiProvider, + Nameservers = request.Nameservers, + NameserverTtl = request.NameserverTtl, + SecondaryOverrides = request.SecondaryOverrides, + SOA = request.SOA, + ZoneMode = request.ZoneMode + }; + + return client.PatchAsync($"/zones/{request.ZoneId}/dns_settings", req, cancellationToken); + } + + /// + /// Show DNS settings for a zone. + /// + /// The instance. + /// The zone identifier. + /// A cancellation token used to propagate notification that this operation should be canceled. + public static Task> ShowDnsZoneSettings(this ICloudflareClient client, string zoneId, CancellationToken cancellationToken = default) + { + zoneId.ValidateCloudflareId(); + + return client.GetAsync($"/zones/{zoneId}/dns_settings", null, cancellationToken); + } + } +} diff --git a/src/Extensions/Cloudflare.Dns/Enums/DnsZoneMode.cs b/src/Extensions/Cloudflare.Dns/Enums/DnsZoneMode.cs new file mode 100644 index 0000000..2b3b4c6 --- /dev/null +++ b/src/Extensions/Cloudflare.Dns/Enums/DnsZoneMode.cs @@ -0,0 +1,31 @@ +using System.Runtime.Serialization; +using Newtonsoft.Json.Converters; + +namespace AMWD.Net.Api.Cloudflare.Dns +{ + /// + /// The mode of a DNS zone. + /// Source + /// + [JsonConverter(typeof(StringEnumConverter))] + public enum DnsZoneMode + { + /// + /// The standard mode. + /// + [EnumMember(Value = "standard")] + Standard = 1, + + /// + /// The CDN-only mode. + /// + [EnumMember(Value = "cdn_only")] + CdnOnly = 2, + + /// + /// The DNS-only mode. + /// + [EnumMember(Value = "dns_only")] + DnsOnly = 3 + } +} diff --git a/src/Extensions/Cloudflare.Dns/Internals/InternalUpdateDnsZoneSettingsRequest.cs b/src/Extensions/Cloudflare.Dns/Internals/InternalUpdateDnsZoneSettingsRequest.cs new file mode 100644 index 0000000..c2126ad --- /dev/null +++ b/src/Extensions/Cloudflare.Dns/Internals/InternalUpdateDnsZoneSettingsRequest.cs @@ -0,0 +1,32 @@ +namespace AMWD.Net.Api.Cloudflare.Dns.Internals +{ + internal class InternalUpdateDnsZoneSettingsRequest + { + [JsonProperty("flatten_all_cnames")] + public bool? FlattenAllCnames { get; set; } + + [JsonProperty("foundation_dns")] + public bool? FoundationDns { get; set; } + + [JsonProperty("internal_dns")] + public DnsZoneInternalDns? InternalDns { get; set; } + + [JsonProperty("multi_provider")] + public bool? MultiProvider { get; set; } + + [JsonProperty("nameservers")] + public DnsZoneNameservers? Nameservers { get; set; } + + [JsonProperty("ns_ttl")] + public int? NameserverTtl { get; set; } + + [JsonProperty("secondary_overrides")] + public bool? SecondaryOverrides { get; set; } + + [JsonProperty("soa")] + public DnsZoneSoa? SOA { get; set; } + + [JsonProperty("zone_mode")] + public DnsZoneMode? ZoneMode { get; set; } + } +} diff --git a/src/Extensions/Cloudflare.Dns/Models/DnsZoneInternalDns.cs b/src/Extensions/Cloudflare.Dns/Models/DnsZoneInternalDns.cs new file mode 100644 index 0000000..8b72ca9 --- /dev/null +++ b/src/Extensions/Cloudflare.Dns/Models/DnsZoneInternalDns.cs @@ -0,0 +1,14 @@ +namespace AMWD.Net.Api.Cloudflare.Dns +{ + /// + /// Settings for this internal zone. + /// + public class DnsZoneInternalDns + { + /// + /// The ID of the zone to fallback to. + /// + [JsonProperty("reference_zone_id")] + public string? ReferenceZoneId { get; set; } + } +} diff --git a/src/Extensions/Cloudflare.Dns/Models/DnsZoneNameservers.cs b/src/Extensions/Cloudflare.Dns/Models/DnsZoneNameservers.cs new file mode 100644 index 0000000..955bace --- /dev/null +++ b/src/Extensions/Cloudflare.Dns/Models/DnsZoneNameservers.cs @@ -0,0 +1,64 @@ +using System.Runtime.Serialization; +using Newtonsoft.Json.Converters; + +namespace AMWD.Net.Api.Cloudflare.Dns +{ + /// + /// Settings determining the nameservers through which the zone should be available. + /// + public class DnsZoneNameservers + { + /// + /// Initializes a new instance of the class. + /// + /// Nameserver type. + public DnsZoneNameservers(DnsZoneNameserversType type) + { + Type = type; + } + + /// + /// Nameserver type. + /// + [JsonProperty("type")] + public DnsZoneNameserversType Type { get; set; } + + /// + /// Configured nameserver set to be used for this zone. + /// + [JsonProperty("ns_set")] + public int? NameserverSet { get; set; } + } + + /// + /// The type of a DNS zone nameserver. + /// Source + /// + [JsonConverter(typeof(StringEnumConverter))] + public enum DnsZoneNameserversType + { + /// + /// The Cloudflare standard nameservers. + /// + [EnumMember(Value = "cloudflare.standard")] + Standard = 1, + + /// + /// The account specific nameservers. + /// + [EnumMember(Value = "custom.account")] + Account = 2, + + /// + /// The tenant specific nameservers. + /// + [EnumMember(Value = "custom.tenant")] + Tenant = 3, + + /// + /// The zone specific nameservers. + /// + [EnumMember(Value = "custom.zone")] + Zone = 4 + } +} diff --git a/src/Extensions/Cloudflare.Dns/Models/DnsZoneSettings.cs b/src/Extensions/Cloudflare.Dns/Models/DnsZoneSettings.cs new file mode 100644 index 0000000..f0f8378 --- /dev/null +++ b/src/Extensions/Cloudflare.Dns/Models/DnsZoneSettings.cs @@ -0,0 +1,66 @@ +namespace AMWD.Net.Api.Cloudflare.Dns +{ + /// + /// The response for a DNS zone edit. + /// + public class DnsZoneSettings + { + /// + /// Whether to flatten all CNAME records in the zone. Note that, due to DNS + /// limitations, a CNAME record at the zone apex will always be flattened. + /// + [JsonProperty("flatten_all_cnames")] + public bool? FlattenAllCnames { get; set; } + + /// + /// Whether to enable Foundation DNS Advanced Nameservers on the zone. + /// + [JsonProperty("foundation_dns")] + public bool? FoundationDns { get; set; } + + /// + /// Settings for this internal zone. + /// + [JsonProperty("internal_dns")] + public DnsZoneInternalDns? InternalDns { get; set; } + + /// + /// Whether to enable multi-provider DNS, which causes Cloudflare to activate the + /// zone even when non-Cloudflare NS records exist, and to respect NS records at the + /// zone apex during outbound zone transfers. + /// + [JsonProperty("multi_provider")] + public bool? MultiProvider { get; set; } + + /// + /// Settings determining the nameservers through which the zone should be available. + /// + [JsonProperty("nameservers")] + public DnsZoneNameservers? Nameservers { get; set; } + + /// + /// The time to live (TTL) of the zone's nameserver (NS) records. + /// + [JsonProperty("ns_ttl")] + public int? NameserverTtl { get; set; } + + /// + /// Allows a Secondary DNS zone to use (proxied) override records and CNAME + /// flattening at the zone apex. + /// + [JsonProperty("secondary_overrides")] + public bool? SecondaryOverrides { get; set; } + + /// + /// Components of the zone's SOA record. + /// + [JsonProperty("soa")] + public DnsZoneSoa? SOA { get; set; } + + /// + /// Whether the zone mode is a regular or CDN/DNS only zone. + /// + [JsonProperty("zone_mode")] + public DnsZoneMode? ZoneMode { get; set; } + } +} diff --git a/src/Extensions/Cloudflare.Dns/Models/DnsZoneSoa.cs b/src/Extensions/Cloudflare.Dns/Models/DnsZoneSoa.cs new file mode 100644 index 0000000..667e66a --- /dev/null +++ b/src/Extensions/Cloudflare.Dns/Models/DnsZoneSoa.cs @@ -0,0 +1,75 @@ +namespace AMWD.Net.Api.Cloudflare.Dns +{ + /// + /// Components of the zone's SOA record. + /// + public class DnsZoneSoa + { + /// + /// Initializes a new instance of the class. + /// + /// Time in seconds of being unable to query the primary server after which secondary servers should stop serving the zone. + /// The time to live (TTL) for negative caching of records within the zone. + /// The primary nameserver for the zone. + /// Time in seconds after which secondary servers should re-check the SOA record to see if the zone has been updated. + /// The time to live (TTL) for negative caching of records within the zone. + /// The email address of the zone administrator. + /// The time to live (TTL) of the SOA record itself. + public DnsZoneSoa(int expire, int minttl, string mname, int refresh, int retry, string rname, int ttl) + { + Expire = expire; + MinimumTtl = minttl; + PrimaryNameserver = mname; + Refresh = refresh; + Retry = retry; + ZoneAdministrator = rname; + TimeToLive = ttl; + } + + /// + /// Time in seconds of being unable to query the primary server after which + /// secondary servers should stop serving the zone. + /// + [JsonProperty("expire")] + public int Expire { get; set; } + + /// + /// The time to live (TTL) for negative caching of records within the zone. + /// + [JsonProperty("min_ttl")] + public int MinimumTtl { get; set; } + + /// + /// The primary nameserver, which may be used for outbound zone transfers. + /// + [JsonProperty("mname")] + public string PrimaryNameserver { get; set; } + + /// + /// Time in seconds after which secondary servers should re-check the SOA record to + /// see if the zone has been updated. + /// + [JsonProperty("refresh")] + public int Refresh { get; set; } + + /// + /// Time in seconds after which secondary servers should retry queries after the + /// primary server was unresponsive. + /// + [JsonProperty("retry")] + public int Retry { get; set; } + + /// + /// The email address of the zone administrator, with the first label representing + /// the local part of the email address. + /// + [JsonProperty("rname")] + public string ZoneAdministrator { get; set; } + + /// + /// The time to live (TTL) of the SOA record itself. + /// + [JsonProperty("ttl")] + public int TimeToLive { get; set; } + } +} diff --git a/src/Extensions/Cloudflare.Dns/README.md b/src/Extensions/Cloudflare.Dns/README.md index fb86f83..2d3ed39 100644 --- a/src/Extensions/Cloudflare.Dns/README.md +++ b/src/Extensions/Cloudflare.Dns/README.md @@ -27,6 +27,14 @@ This package contains the feature set of the _DNS_ section of the Cloudflare API - [Overwrite DNS Record](https://developers.cloudflare.com/api/resources/dns/subresources/records/methods/update/) +#### [Settings] + +##### [Zone] + +- [Update DNS Settings](https://developers.cloudflare.com/api/resources/dns/subresources/settings/subresources/zone/methods/edit/) +- [Show DNS Settings](https://developers.cloudflare.com/api/resources/dns/subresources/settings/subresources/zone/methods/get/) + + --- Published under MIT License (see [choose a license]) @@ -39,3 +47,6 @@ Published under MIT License (see [choose a license]) [DNS]: https://developers.cloudflare.com/api/resources/dns/ [Records]: https://developers.cloudflare.com/api/resources/dns/subresources/records/ + +[Settings]: https://developers.cloudflare.com/api/resources/dns/subresources/settings/ +[Zone]: https://developers.cloudflare.com/api/resources/dns/subresources/settings/subresources/zone/ diff --git a/src/Extensions/Cloudflare.Dns/Requests/UpdateDnsZoneSettingsRequest.cs b/src/Extensions/Cloudflare.Dns/Requests/UpdateDnsZoneSettingsRequest.cs new file mode 100644 index 0000000..f5baba0 --- /dev/null +++ b/src/Extensions/Cloudflare.Dns/Requests/UpdateDnsZoneSettingsRequest.cs @@ -0,0 +1,71 @@ +namespace AMWD.Net.Api.Cloudflare.Dns +{ + /// + /// Represents a request to update DNS zone settings. + /// + public class UpdateDnsZoneSettingsRequest + { + /// + /// Initializes a new instance of the class. + /// + /// The zone identifier. + public UpdateDnsZoneSettingsRequest(string zoneId) + { + ZoneId = zoneId; + } + + /// + /// The zone identifier. + /// + public string ZoneId { get; set; } + + /// + /// Whether to flatten all CNAME records in the zone. Note that, due to + /// DNS limitations, a CNAME record at the zone apex will always be flattened. + /// + public bool? FlattenAllCnames { get; set; } + + /// + /// Whether to enable Foundation DNS Advanced Nameservers on the zone. + /// + public bool? FoundationDns { get; set; } + + /// + /// Settings for this internal zone. + /// + public DnsZoneInternalDns? InternalDns { get; set; } + + /// + /// Whether to enable multi-provider DNS, which causes Cloudflare to + /// activate the zone even when non-Cloudflare NS records exist, and to respect NS + /// records at the zone apex during outbound zone transfers. + /// + public bool? MultiProvider { get; set; } + + /// + /// Settings determining the nameservers through which the zone should be available. + /// + public DnsZoneNameservers? Nameservers { get; set; } + + /// + /// The time to live (TTL) of the zone's nameserver (NS) records. + /// + public int? NameserverTtl { get; set; } + + /// + /// Allows a Secondary DNS zone to use (proxied) override records and CNAME + /// flattening at the zone apex. + /// + public bool? SecondaryOverrides { get; set; } + + /// + /// Components of the zone's SOA record. + /// + public DnsZoneSoa? SOA { get; set; } + + /// + /// Whether the zone mode is a regular or CDN/DNS only zone. + /// + public DnsZoneMode? ZoneMode { get; set; } + } +} diff --git a/test/Extensions/Cloudflare.Dns.Tests/DnsZoneSettingsExtensions/ShowDnsZoneSettingsTest.cs b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneSettingsExtensions/ShowDnsZoneSettingsTest.cs new file mode 100644 index 0000000..13f7547 --- /dev/null +++ b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneSettingsExtensions/ShowDnsZoneSettingsTest.cs @@ -0,0 +1,97 @@ +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.DnsZoneSettingsExtensions +{ + [TestClass] + public class ShowDnsZoneSettingsTest + { + private const string ZoneId = "023e105f4ecef8ad9ca31a8372d0c353"; + + 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 = new DnsZoneSettings + { + FlattenAllCnames = true, + FoundationDns = false, + InternalDns = new DnsZoneInternalDns + { + ReferenceZoneId = ZoneId + }, + MultiProvider = false, + Nameservers = new DnsZoneNameservers( + type: DnsZoneNameserversType.Zone + ), + NameserverTtl = 86400, + SecondaryOverrides = false, + SOA = new DnsZoneSoa( + expire: 604800, + minttl: 1800, + mname: "bob.ns.example.com", + refresh: 10000, + retry: 2400, + rname: "admin.example.com", + ttl: 3600 + ), + ZoneMode = DnsZoneMode.DnsOnly + } + }; + } + + [TestMethod] + public async Task ShouldGetDnsSettings() + { + // Arrange + var client = GetClient(); + + // Act + var response = await client.ShowDnsZoneSettings(ZoneId); + + // 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($"/zones/{ZoneId}/dns_settings", callback.RequestPath); + + Assert.IsNull(callback.QueryFilter); + + _clientMock?.Verify(m => m.GetAsync($"/zones/{ZoneId}/dns_settings", 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/test/Extensions/Cloudflare.Dns.Tests/DnsZoneSettingsExtensions/UpdateDnsZoneSettingsTest.cs b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneSettingsExtensions/UpdateDnsZoneSettingsTest.cs new file mode 100644 index 0000000..237976f --- /dev/null +++ b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneSettingsExtensions/UpdateDnsZoneSettingsTest.cs @@ -0,0 +1,335 @@ +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.DnsZoneSettingsExtensions +{ + [TestClass] + public class UpdateDnsZoneSettingsTest + { + private const string ZoneId = "023e105f4ecef8ad9ca31a8372d0c353"; + + private Mock _clientMock; + private CloudflareResponse _response; + private List<(string RequestPath, InternalUpdateDnsZoneSettingsRequest Request)> _callbacks; + private UpdateDnsZoneSettingsRequest _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 DnsZoneSettings + { + FlattenAllCnames = true, + FoundationDns = false, + InternalDns = new DnsZoneInternalDns + { + ReferenceZoneId = ZoneId + }, + MultiProvider = false, + Nameservers = new DnsZoneNameservers( + type: DnsZoneNameserversType.Zone + ), + NameserverTtl = 86400, + SecondaryOverrides = false, + SOA = new DnsZoneSoa( + expire: 604800, + minttl: 1800, + mname: "bob.ns.example.com", + refresh: 10000, + retry: 2400, + rname: "admin.example.com", + ttl: 3600 + ), + ZoneMode = DnsZoneMode.DnsOnly + } + }; + + _request = new UpdateDnsZoneSettingsRequest(ZoneId) + { + FlattenAllCnames = true, + FoundationDns = false, + InternalDns = new DnsZoneInternalDns + { + ReferenceZoneId = ZoneId + }, + MultiProvider = false, + Nameservers = new DnsZoneNameservers( + type: DnsZoneNameserversType.Standard + ), + NameserverTtl = 86400, + SecondaryOverrides = false, + SOA = new DnsZoneSoa( + expire: 604800, + minttl: 1800, + mname: "ns1.example.org", + refresh: 28800, + retry: 3600, + rname: "admin.example.org", + ttl: 43200 + ), + ZoneMode = DnsZoneMode.Standard + }; + } + + [TestMethod] + public async Task ShouldUpdateDnsSettingsFull() + { + // Arrange + var client = GetClient(); + + // Act + var response = await client.UpdateDnsZoneSettings(_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($"/zones/{ZoneId}/dns_settings", callback.RequestPath); + + Assert.IsNotNull(callback.Request); + Assert.IsTrue(callback.Request.FlattenAllCnames); + Assert.IsFalse(callback.Request.FoundationDns); + Assert.IsNotNull(callback.Request.InternalDns); + Assert.AreEqual(ZoneId, callback.Request.InternalDns.ReferenceZoneId); + Assert.IsFalse(callback.Request.MultiProvider); + Assert.IsNotNull(callback.Request.Nameservers); + Assert.AreEqual(DnsZoneNameserversType.Standard, callback.Request.Nameservers.Type); + Assert.AreEqual(86400, callback.Request.NameserverTtl); + Assert.IsFalse(callback.Request.SecondaryOverrides); + Assert.IsNotNull(callback.Request.SOA); + Assert.AreEqual(604800, callback.Request.SOA.Expire); + Assert.AreEqual(1800, callback.Request.SOA.MinimumTtl); + Assert.AreEqual("ns1.example.org", callback.Request.SOA.PrimaryNameserver); + Assert.AreEqual(28800, callback.Request.SOA.Refresh); + Assert.AreEqual(3600, callback.Request.SOA.Retry); + Assert.AreEqual(43200, callback.Request.SOA.TimeToLive); + Assert.AreEqual("admin.example.org", callback.Request.SOA.ZoneAdministrator); + Assert.AreEqual(DnsZoneMode.Standard, callback.Request.ZoneMode); + + _clientMock.Verify(m => m.PatchAsync($"/zones/{ZoneId}/dns_settings", It.IsAny(), It.IsAny()), Times.Once); + _clientMock.VerifyNoOtherCalls(); + } + + [TestMethod] + public async Task ShouldUpdateDnsSettingsNone() + { + // Arrange + var request = new UpdateDnsZoneSettingsRequest(ZoneId); + var client = GetClient(); + + // Act + var response = await client.UpdateDnsZoneSettings(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($"/zones/{ZoneId}/dns_settings", callback.RequestPath); + + Assert.IsNotNull(callback.Request); + Assert.IsNull(callback.Request.FlattenAllCnames); + Assert.IsNull(callback.Request.FoundationDns); + Assert.IsNull(callback.Request.MultiProvider); + Assert.IsNull(callback.Request.Nameservers); + Assert.IsNull(callback.Request.NameserverTtl); + Assert.IsNull(callback.Request.SecondaryOverrides); + Assert.IsNull(callback.Request.SOA); + Assert.IsNull(callback.Request.ZoneMode); + + _clientMock.Verify(m => m.PatchAsync($"/zones/{ZoneId}/dns_settings", It.IsAny(), It.IsAny()), Times.Once); + _clientMock.VerifyNoOtherCalls(); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public async Task ShouldThrowArgumentOutOfRangeExceptionForInvalidMode() + { + // Arrange + _request.ZoneMode = 0; + var client = GetClient(); + + // Act + await client.UpdateDnsZoneSettings(_request); + + // Assert - ArgumentOutOfRangeException + } + + [TestMethod] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public async Task ShouldThrowArgumentOutOfRangeExceptionForInvalidNameserverType() + { + // Arrange + _request.Nameservers.Type = 0; + var client = GetClient(); + + // Act + await client.UpdateDnsZoneSettings(_request); + + // Assert - ArgumentOutOfRangeException + } + + [DataTestMethod] + [DataRow(29)] + [DataRow(86401)] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public async Task ShouldThrowArgumentOutOfRangeExceptionForInvalidNameserverTtl(int ttl) + { + // Arrange + _request.NameserverTtl = ttl; + var client = GetClient(); + + // Act + await client.UpdateDnsZoneSettings(_request); + + // Assert - ArgumentOutOfRangeException + } + + [DataTestMethod] + [DataRow(86399)] + [DataRow(2419201)] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public async Task ShouldThrowArgumentOutOfRangeExceptionForInvalidSoaExpire(int ttl) + { + // Arrange + _request.SOA.Expire = ttl; + var client = GetClient(); + + // Act + await client.UpdateDnsZoneSettings(_request); + + // Assert - ArgumentOutOfRangeException + } + + [DataTestMethod] + [DataRow(59)] + [DataRow(86401)] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public async Task ShouldThrowArgumentOutOfRangeExceptionForInvalidSoaMinimumTtl(int ttl) + { + // Arrange + _request.SOA.MinimumTtl = ttl; + var client = GetClient(); + + // Act + await client.UpdateDnsZoneSettings(_request); + + // Assert - ArgumentOutOfRangeException + } + + [DataTestMethod] + [DataRow(null)] + [DataRow("")] + [DataRow(" ")] + [ExpectedException(typeof(ArgumentNullException))] + public async Task ShouldThrowArgumentNullExceptionForMissingSoaNameserver(string nameserver) + { + // Arrange + _request.SOA.PrimaryNameserver = nameserver; + var client = GetClient(); + + // Act + await client.UpdateDnsZoneSettings(_request); + + // Assert - ArgumentNullException + } + + [DataTestMethod] + [DataRow(599)] + [DataRow(86401)] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public async Task ShouldThrowArgumentOutOfRangeExceptionForInvalidSoaRefresh(int ttl) + { + // Arrange + _request.SOA.Refresh = ttl; + var client = GetClient(); + + // Act + await client.UpdateDnsZoneSettings(_request); + + // Assert - ArgumentOutOfRangeException + } + + [DataTestMethod] + [DataRow(599)] + [DataRow(86401)] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public async Task ShouldThrowArgumentOutOfRangeExceptionForInvalidSoaRetry(int ttl) + { + // Arrange + _request.SOA.Retry = ttl; + var client = GetClient(); + + // Act + await client.UpdateDnsZoneSettings(_request); + + // Assert - ArgumentOutOfRangeException + } + + [DataTestMethod] + [DataRow(299)] + [DataRow(86401)] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public async Task ShouldThrowArgumentOutOfRangeExceptionForInvalidSoaTtl(int ttl) + { + // Arrange + _request.SOA.TimeToLive = ttl; + var client = GetClient(); + + // Act + await client.UpdateDnsZoneSettings(_request); + + // Assert - ArgumentOutOfRangeException + } + + [DataTestMethod] + [DataRow(null)] + [DataRow("")] + [DataRow(" ")] + [ExpectedException(typeof(ArgumentNullException))] + public async Task ShouldThrowArgumentNullExceptionForMissingSoaAdministrator(string admin) + { + // Arrange + _request.SOA.ZoneAdministrator = admin; + var client = GetClient(); + + // Act + await client.UpdateDnsZoneSettings(_request); + + // Assert - ArgumentNullException + } + + private ICloudflareClient GetClient() + { + _clientMock = new Mock(); + _clientMock + .Setup(m => m.PatchAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((requestPath, request, _) => _callbacks.Add((requestPath, request))) + .ReturnsAsync(() => _response); + + return _clientMock.Object; + } + } +}