diff --git a/src/Extensions/Cloudflare.Dns/DnsAccountSettingsExtensions.cs b/src/Extensions/Cloudflare.Dns/DnsAccountSettingsExtensions.cs new file mode 100644 index 0000000..eae7df3 --- /dev/null +++ b/src/Extensions/Cloudflare.Dns/DnsAccountSettingsExtensions.cs @@ -0,0 +1,90 @@ +using System.Threading; +using System.Threading.Tasks; +using AMWD.Net.Api.Cloudflare.Dns.Internals; + +namespace AMWD.Net.Api.Cloudflare.Dns +{ + /// + /// Extensions for DNS Account Settings. + /// + public static class DnsAccountSettingsExtensions + { + /// + /// Update DNS settings for a zone default of an account. + /// + /// The instance. + /// The request. + /// A cancellation token used to propagate notification that this operation should be canceled. + public static Task> UpdateDnsAccountSettings(this ICloudflareClient client, UpdateDnsAccountSettingsRequest request, CancellationToken cancellationToken = default) + { + request.AccountId.ValidateCloudflareId(); + + var req = new InternalUpdateDnsAccountSettingsRequest(); + + if (request.ZoneDefaults != null) + { + req.ZoneDefaults = new InternalDnsAccountZoneDefaults(); + + if (request.ZoneDefaults.ZoneMode.HasValue && !Enum.IsDefined(typeof(DnsZoneMode), request.ZoneDefaults.ZoneMode)) + throw new ArgumentOutOfRangeException($"{nameof(request.ZoneDefaults)}.{nameof(request.ZoneDefaults.ZoneMode)}", request.ZoneDefaults.ZoneMode, "Value must be one of the ZoneMode enum values."); + + if (request.ZoneDefaults.Nameservers != null && !Enum.IsDefined(typeof(DnsAccountNameserversType), request.ZoneDefaults.Nameservers.Type)) + throw new ArgumentOutOfRangeException($"{nameof(request.ZoneDefaults)}.{nameof(request.ZoneDefaults.Nameservers)}.{nameof(request.ZoneDefaults.Nameservers.Type)}", request.ZoneDefaults.Nameservers.Type, "Value must be one of the NameserverType enum values."); + + if (request.ZoneDefaults.NameserverTtl.HasValue && (request.ZoneDefaults.NameserverTtl < 30 || 86400 < request.ZoneDefaults.NameserverTtl)) + throw new ArgumentOutOfRangeException($"{nameof(request.ZoneDefaults)}.{nameof(request.ZoneDefaults.NameserverTtl)}", request.ZoneDefaults.NameserverTtl, "Value must be between 30 and 86400."); + + if (request.ZoneDefaults.SOA != null) + { + string paramNameBase = $"{nameof(request.ZoneDefaults)}.{nameof(request.ZoneDefaults.SOA)}"; + + if (request.ZoneDefaults.SOA.Expire < 86400 || 2419200 < request.ZoneDefaults.SOA.Expire) + throw new ArgumentOutOfRangeException($"{paramNameBase}.{nameof(request.ZoneDefaults.SOA.Expire)}", request.ZoneDefaults.SOA.Expire, "Value must be between 86400 and 2419200."); + + if (request.ZoneDefaults.SOA.MinimumTtl < 60 || 86400 < request.ZoneDefaults.SOA.MinimumTtl) + throw new ArgumentOutOfRangeException($"{paramNameBase}.{nameof(request.ZoneDefaults.SOA.MinimumTtl)}", request.ZoneDefaults.SOA.MinimumTtl, "Value must be between 60 and 86400."); + + if (string.IsNullOrWhiteSpace(request.ZoneDefaults.SOA.PrimaryNameserver)) + throw new ArgumentNullException($"{paramNameBase}.{nameof(request.ZoneDefaults.SOA.PrimaryNameserver)}"); + + if (request.ZoneDefaults.SOA.Refresh < 600 || 86400 < request.ZoneDefaults.SOA.Refresh) + throw new ArgumentOutOfRangeException($"{paramNameBase}.{nameof(request.ZoneDefaults.SOA.Refresh)}", request.ZoneDefaults.SOA.Refresh, "Value must be between 600 and 86400."); + + if (request.ZoneDefaults.SOA.Retry < 600 || 86400 < request.ZoneDefaults.SOA.Retry) + throw new ArgumentOutOfRangeException($"{paramNameBase}.{nameof(request.ZoneDefaults.SOA.Retry)}", request.ZoneDefaults.SOA.Retry, "Value must be between 600 and 86400."); + + if (request.ZoneDefaults.SOA.TimeToLive < 300 || 86400 < request.ZoneDefaults.SOA.TimeToLive) + throw new ArgumentOutOfRangeException($"{paramNameBase}.{nameof(request.ZoneDefaults.SOA.TimeToLive)}", request.ZoneDefaults.SOA.TimeToLive, "Value must be between 300 and 86400."); + + if (string.IsNullOrWhiteSpace(request.ZoneDefaults.SOA.ZoneAdministrator)) + throw new ArgumentNullException($"{paramNameBase}.{nameof(request.ZoneDefaults.SOA.ZoneAdministrator)}"); + } + + req.ZoneDefaults.FlattenAllCnames = request.ZoneDefaults.FlattenAllCnames; + req.ZoneDefaults.FoundationDns = request.ZoneDefaults.FoundationDns; + req.ZoneDefaults.InternalDns = request.ZoneDefaults.InternalDns; + req.ZoneDefaults.MultiProvider = request.ZoneDefaults.MultiProvider; + req.ZoneDefaults.Nameservers = request.ZoneDefaults.Nameservers; + req.ZoneDefaults.NameserverTtl = request.ZoneDefaults.NameserverTtl; + req.ZoneDefaults.SecondaryOverrides = request.ZoneDefaults.SecondaryOverrides; + req.ZoneDefaults.SOA = request.ZoneDefaults.SOA; + req.ZoneDefaults.ZoneMode = request.ZoneDefaults.ZoneMode; + } + + return client.PatchAsync($"/accounts/{request.AccountId}/dns_settings", req, cancellationToken); + } + + /// + /// Show DNS settings for a zone default of an account. + /// + /// The instance. + /// The account identifier. + /// A cancellation token used to propagate notification that this operation should be canceled. + public static Task> ShowDnsAccountSettings(this ICloudflareClient client, string accountId, CancellationToken cancellationToken = default) + { + accountId.ValidateCloudflareId(); + + return client.GetAsync($"/accounts/{accountId}/dns_settings", null, cancellationToken); + } + } +} diff --git a/src/Extensions/Cloudflare.Dns/Internals/InternalDnsAccountZoneDefaults.cs b/src/Extensions/Cloudflare.Dns/Internals/InternalDnsAccountZoneDefaults.cs new file mode 100644 index 0000000..9e2ac9d --- /dev/null +++ b/src/Extensions/Cloudflare.Dns/Internals/InternalDnsAccountZoneDefaults.cs @@ -0,0 +1,32 @@ +namespace AMWD.Net.Api.Cloudflare.Dns.Internals +{ + internal class InternalDnsAccountZoneDefaults + { + [JsonProperty("flatten_all_cnames")] + public bool? FlattenAllCnames { get; set; } + + [JsonProperty("foundation_dns")] + public bool? FoundationDns { get; set; } + + [JsonProperty("internal_dns")] + public DnsAccountInternalDns? InternalDns { get; set; } + + [JsonProperty("multi_provider")] + public bool? MultiProvider { get; set; } + + [JsonProperty("nameservers")] + public DnsAccountNameservers? 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/Internals/InternalUpdateDnsAccountSettingsRequest.cs b/src/Extensions/Cloudflare.Dns/Internals/InternalUpdateDnsAccountSettingsRequest.cs new file mode 100644 index 0000000..b5eb423 --- /dev/null +++ b/src/Extensions/Cloudflare.Dns/Internals/InternalUpdateDnsAccountSettingsRequest.cs @@ -0,0 +1,8 @@ +namespace AMWD.Net.Api.Cloudflare.Dns.Internals +{ + internal class InternalUpdateDnsAccountSettingsRequest + { + [JsonProperty("zone_defaults")] + public InternalDnsAccountZoneDefaults? ZoneDefaults { get; set; } + } +} diff --git a/src/Extensions/Cloudflare.Dns/Models/DnsAccountInternalDns.cs b/src/Extensions/Cloudflare.Dns/Models/DnsAccountInternalDns.cs new file mode 100644 index 0000000..495a616 --- /dev/null +++ b/src/Extensions/Cloudflare.Dns/Models/DnsAccountInternalDns.cs @@ -0,0 +1,14 @@ +namespace AMWD.Net.Api.Cloudflare.Dns +{ + /// + /// Settings for this internal zone. + /// + public class DnsAccountInternalDns + { + /// + /// 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/DnsAccountNameservers.cs b/src/Extensions/Cloudflare.Dns/Models/DnsAccountNameservers.cs new file mode 100644 index 0000000..f59ccc9 --- /dev/null +++ b/src/Extensions/Cloudflare.Dns/Models/DnsAccountNameservers.cs @@ -0,0 +1,58 @@ +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 DnsAccountNameservers + { + /// + /// Initializes a new instance of the class. + /// + /// Nameserver type. + public DnsAccountNameservers(DnsAccountNameserversType type) + { + Type = type; + } + + /// + /// Nameserver type. + /// + [JsonProperty("type")] + public DnsAccountNameserversType Type { get; set; } + } + + /// + /// The type of a DNS zone nameserver. + /// Source + /// + [JsonConverter(typeof(StringEnumConverter))] + public enum DnsAccountNameserversType + { + /// + /// The Cloudflare standard nameservers. + /// + [EnumMember(Value = "cloudflare.standard")] + Standard = 1, + + /// + /// The zone specific nameservers. + /// + [EnumMember(Value = "cloudflare.standard.random")] + Random = 2, + + /// + /// The account specific nameservers. + /// + [EnumMember(Value = "custom.account")] + Account = 3, + + /// + /// The tenant specific nameservers. + /// + [EnumMember(Value = "custom.tenant")] + Tenant = 4, + } +} diff --git a/src/Extensions/Cloudflare.Dns/Models/DnsAccountSettings.cs b/src/Extensions/Cloudflare.Dns/Models/DnsAccountSettings.cs new file mode 100644 index 0000000..e2fbd5c --- /dev/null +++ b/src/Extensions/Cloudflare.Dns/Models/DnsAccountSettings.cs @@ -0,0 +1,14 @@ +namespace AMWD.Net.Api.Cloudflare.Dns +{ + /// + /// Settings for a Cloudflare DNS zone on account level. + /// + public class DnsAccountSettings + { + /// + /// Settings zone defaults. + /// + [JsonProperty("zone_defaults")] + public DnsAccountZoneDefaults? ZoneDefaults { get; set; } + } +} diff --git a/src/Extensions/Cloudflare.Dns/Models/DnsAccountZoneDefaults.cs b/src/Extensions/Cloudflare.Dns/Models/DnsAccountZoneDefaults.cs new file mode 100644 index 0000000..b9f318d --- /dev/null +++ b/src/Extensions/Cloudflare.Dns/Models/DnsAccountZoneDefaults.cs @@ -0,0 +1,66 @@ +namespace AMWD.Net.Api.Cloudflare.Dns +{ + /// + /// Settings zone defaults. + /// + public class DnsAccountZoneDefaults + { + /// + /// 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 DnsAccountInternalDns? 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 DnsAccountNameservers? 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/README.md b/src/Extensions/Cloudflare.Dns/README.md index 2d3ed39..ad62faf 100644 --- a/src/Extensions/Cloudflare.Dns/README.md +++ b/src/Extensions/Cloudflare.Dns/README.md @@ -29,6 +29,12 @@ This package contains the feature set of the _DNS_ section of the Cloudflare API #### [Settings] +##### [Account] + +- [Update DNS Settings](https://developers.cloudflare.com/api/resources/dns/subresources/settings/subresources/account/methods/edit/) +- [Show DNS Settings](https://developers.cloudflare.com/api/resources/dns/subresources/settings/subresources/account/methods/get/) + + ##### [Zone] - [Update DNS Settings](https://developers.cloudflare.com/api/resources/dns/subresources/settings/subresources/zone/methods/edit/) @@ -49,4 +55,5 @@ Published under MIT License (see [choose a license]) [Records]: https://developers.cloudflare.com/api/resources/dns/subresources/records/ [Settings]: https://developers.cloudflare.com/api/resources/dns/subresources/settings/ +[Account]: https://developers.cloudflare.com/api/resources/dns/subresources/settings/subresources/account/ [Zone]: https://developers.cloudflare.com/api/resources/dns/subresources/settings/subresources/zone/ diff --git a/src/Extensions/Cloudflare.Dns/Requests/UpdateDnsAccountSettingsRequest.cs b/src/Extensions/Cloudflare.Dns/Requests/UpdateDnsAccountSettingsRequest.cs new file mode 100644 index 0000000..af9d1bc --- /dev/null +++ b/src/Extensions/Cloudflare.Dns/Requests/UpdateDnsAccountSettingsRequest.cs @@ -0,0 +1,27 @@ +namespace AMWD.Net.Api.Cloudflare.Dns +{ + /// + /// Represents a request to update DNS zone defaults on account level. + /// + public class UpdateDnsAccountSettingsRequest + { + /// + /// Initializes a new instance of the class. + /// + /// The account identifier. + public UpdateDnsAccountSettingsRequest(string accountId) + { + AccountId = accountId; + } + + /// + /// The zone identifier. + /// + public string AccountId { get; set; } + + /// + /// The DNS zone defaults. + /// + public DnsAccountZoneDefaults? ZoneDefaults { get; set; } + } +} diff --git a/test/Extensions/Cloudflare.Dns.Tests/DnsAccountSettingsExtensions/ShowDnsAccountSettingsTest.cs b/test/Extensions/Cloudflare.Dns.Tests/DnsAccountSettingsExtensions/ShowDnsAccountSettingsTest.cs new file mode 100644 index 0000000..e421938 --- /dev/null +++ b/test/Extensions/Cloudflare.Dns.Tests/DnsAccountSettingsExtensions/ShowDnsAccountSettingsTest.cs @@ -0,0 +1,101 @@ +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.DnsAccountSettingsExtensions +{ + [TestClass] + public class ShowDnsAccountSettingsTest + { + private const string AccountId = "023e105f4ecef8ad9ca31a8372d0c353"; + private const string ZoneId = "023e105f4ecef8ad9ca31a8372d0c354"; + + 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 DnsAccountSettings + { + ZoneDefaults = new DnsAccountZoneDefaults + { + FlattenAllCnames = true, + FoundationDns = false, + InternalDns = new DnsAccountInternalDns + { + ReferenceZoneId = ZoneId + }, + MultiProvider = false, + Nameservers = new DnsAccountNameservers( + type: DnsAccountNameserversType.Random + ), + 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.ShowDnsAccountSettings(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}/dns_settings", callback.RequestPath); + + Assert.IsNull(callback.QueryFilter); + + _clientMock?.Verify(m => m.GetAsync($"/accounts/{AccountId}/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/DnsAccountSettingsExtensions/UpdateDnsAccountSettingsTest.cs b/test/Extensions/Cloudflare.Dns.Tests/DnsAccountSettingsExtensions/UpdateDnsAccountSettingsTest.cs new file mode 100644 index 0000000..b2040f2 --- /dev/null +++ b/test/Extensions/Cloudflare.Dns.Tests/DnsAccountSettingsExtensions/UpdateDnsAccountSettingsTest.cs @@ -0,0 +1,373 @@ +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.DnsAccountSettingsExtensions +{ + [TestClass] + public class UpdateDnsAccountSettingsTest + { + private const string AccountId = "023e105f4ecef8ad9ca31a8372d0c353"; + private const string ZoneId = "023e105f4ecef8ad9ca31a8372d0c354"; + + private Mock _clientMock; + private CloudflareResponse _response; + private List<(string RequestPath, InternalUpdateDnsAccountSettingsRequest Request)> _callbacks; + private UpdateDnsAccountSettingsRequest _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 DnsAccountSettings + { + ZoneDefaults = new DnsAccountZoneDefaults + { + FlattenAllCnames = true, + FoundationDns = false, + InternalDns = new DnsAccountInternalDns + { + ReferenceZoneId = ZoneId + }, + MultiProvider = false, + Nameservers = new DnsAccountNameservers( + type: DnsAccountNameserversType.Standard + ), + 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 UpdateDnsAccountSettingsRequest(AccountId) + { + ZoneDefaults = new DnsAccountZoneDefaults + { + FlattenAllCnames = true, + FoundationDns = false, + InternalDns = new DnsAccountInternalDns + { + ReferenceZoneId = ZoneId + }, + MultiProvider = false, + Nameservers = new DnsAccountNameservers( + type: DnsAccountNameserversType.Random + ), + 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.UpdateDnsAccountSettings(_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}/dns_settings", callback.RequestPath); + + Assert.IsNotNull(callback.Request); + Assert.IsTrue(callback.Request.ZoneDefaults.FlattenAllCnames); + Assert.IsFalse(callback.Request.ZoneDefaults.FoundationDns); + Assert.IsNotNull(callback.Request.ZoneDefaults.InternalDns); + Assert.AreEqual(ZoneId, callback.Request.ZoneDefaults.InternalDns.ReferenceZoneId); + Assert.IsFalse(callback.Request.ZoneDefaults.MultiProvider); + Assert.IsNotNull(callback.Request.ZoneDefaults.Nameservers); + Assert.AreEqual(DnsAccountNameserversType.Random, callback.Request.ZoneDefaults.Nameservers.Type); + Assert.AreEqual(86400, callback.Request.ZoneDefaults.NameserverTtl); + Assert.IsFalse(callback.Request.ZoneDefaults.SecondaryOverrides); + Assert.IsNotNull(callback.Request.ZoneDefaults.SOA); + Assert.AreEqual(604800, callback.Request.ZoneDefaults.SOA.Expire); + Assert.AreEqual(1800, callback.Request.ZoneDefaults.SOA.MinimumTtl); + Assert.AreEqual("ns1.example.org", callback.Request.ZoneDefaults.SOA.PrimaryNameserver); + Assert.AreEqual(28800, callback.Request.ZoneDefaults.SOA.Refresh); + Assert.AreEqual(3600, callback.Request.ZoneDefaults.SOA.Retry); + Assert.AreEqual(43200, callback.Request.ZoneDefaults.SOA.TimeToLive); + Assert.AreEqual("admin.example.org", callback.Request.ZoneDefaults.SOA.ZoneAdministrator); + Assert.AreEqual(DnsZoneMode.Standard, callback.Request.ZoneDefaults.ZoneMode); + + _clientMock.Verify(m => m.PatchAsync($"/accounts/{AccountId}/dns_settings", It.IsAny(), It.IsAny()), Times.Once); + _clientMock.VerifyNoOtherCalls(); + } + + [TestMethod] + public async Task ShouldUpdateDnsSettingsNone() + { + // Arrange + var request = new UpdateDnsAccountSettingsRequest(AccountId); + var client = GetClient(); + + // Act + var response = await client.UpdateDnsAccountSettings(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}/dns_settings", callback.RequestPath); + + Assert.IsNotNull(callback.Request); + Assert.IsNull(callback.Request.ZoneDefaults); + + _clientMock.Verify(m => m.PatchAsync($"/accounts/{AccountId}/dns_settings", It.IsAny(), It.IsAny()), Times.Once); + _clientMock.VerifyNoOtherCalls(); + } + + [TestMethod] + public async Task ShouldUpdateDnsSettingsNoneDefaults() + { + // Arrange + var request = new UpdateDnsAccountSettingsRequest(AccountId) + { + ZoneDefaults = new DnsAccountZoneDefaults() + }; + var client = GetClient(); + + // Act + var response = await client.UpdateDnsAccountSettings(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}/dns_settings", callback.RequestPath); + + Assert.IsNotNull(callback.Request); + Assert.IsNotNull(callback.Request.ZoneDefaults); + Assert.IsNull(callback.Request.ZoneDefaults.FlattenAllCnames); + Assert.IsNull(callback.Request.ZoneDefaults.FoundationDns); + Assert.IsNull(callback.Request.ZoneDefaults.MultiProvider); + Assert.IsNull(callback.Request.ZoneDefaults.Nameservers); + Assert.IsNull(callback.Request.ZoneDefaults.NameserverTtl); + Assert.IsNull(callback.Request.ZoneDefaults.SecondaryOverrides); + Assert.IsNull(callback.Request.ZoneDefaults.SOA); + Assert.IsNull(callback.Request.ZoneDefaults.ZoneMode); + + _clientMock.Verify(m => m.PatchAsync($"/accounts/{AccountId}/dns_settings", It.IsAny(), It.IsAny()), Times.Once); + _clientMock.VerifyNoOtherCalls(); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public async Task ShouldThrowArgumentOutOfRangeExceptionForInvalidMode() + { + // Arrange + _request.ZoneDefaults.ZoneMode = 0; + var client = GetClient(); + + // Act + await client.UpdateDnsAccountSettings(_request); + + // Assert - ArgumentOutOfRangeException + } + + [TestMethod] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public async Task ShouldThrowArgumentOutOfRangeExceptionForInvalidNameserverType() + { + // Arrange + _request.ZoneDefaults.Nameservers.Type = 0; + var client = GetClient(); + + // Act + await client.UpdateDnsAccountSettings(_request); + + // Assert - ArgumentOutOfRangeException + } + + [DataTestMethod] + [DataRow(29)] + [DataRow(86401)] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public async Task ShouldThrowArgumentOutOfRangeExceptionForInvalidNameserverTtl(int ttl) + { + // Arrange + _request.ZoneDefaults.NameserverTtl = ttl; + var client = GetClient(); + + // Act + await client.UpdateDnsAccountSettings(_request); + + // Assert - ArgumentOutOfRangeException + } + + [DataTestMethod] + [DataRow(86399)] + [DataRow(2419201)] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public async Task ShouldThrowArgumentOutOfRangeExceptionForInvalidSoaExpire(int ttl) + { + // Arrange + _request.ZoneDefaults.SOA.Expire = ttl; + var client = GetClient(); + + // Act + await client.UpdateDnsAccountSettings(_request); + + // Assert - ArgumentOutOfRangeException + } + + [DataTestMethod] + [DataRow(59)] + [DataRow(86401)] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public async Task ShouldThrowArgumentOutOfRangeExceptionForInvalidSoaMinimumTtl(int ttl) + { + // Arrange + _request.ZoneDefaults.SOA.MinimumTtl = ttl; + var client = GetClient(); + + // Act + await client.UpdateDnsAccountSettings(_request); + + // Assert - ArgumentOutOfRangeException + } + + [DataTestMethod] + [DataRow(null)] + [DataRow("")] + [DataRow(" ")] + [ExpectedException(typeof(ArgumentNullException))] + public async Task ShouldThrowArgumentNullExceptionForMissingSoaNameserver(string nameserver) + { + // Arrange + _request.ZoneDefaults.SOA.PrimaryNameserver = nameserver; + var client = GetClient(); + + // Act + await client.UpdateDnsAccountSettings(_request); + + // Assert - ArgumentNullException + } + + [DataTestMethod] + [DataRow(599)] + [DataRow(86401)] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public async Task ShouldThrowArgumentOutOfRangeExceptionForInvalidSoaRefresh(int ttl) + { + // Arrange + _request.ZoneDefaults.SOA.Refresh = ttl; + var client = GetClient(); + + // Act + await client.UpdateDnsAccountSettings(_request); + + // Assert - ArgumentOutOfRangeException + } + + [DataTestMethod] + [DataRow(599)] + [DataRow(86401)] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public async Task ShouldThrowArgumentOutOfRangeExceptionForInvalidSoaRetry(int ttl) + { + // Arrange + _request.ZoneDefaults.SOA.Retry = ttl; + var client = GetClient(); + + // Act + await client.UpdateDnsAccountSettings(_request); + + // Assert - ArgumentOutOfRangeException + } + + [DataTestMethod] + [DataRow(299)] + [DataRow(86401)] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public async Task ShouldThrowArgumentOutOfRangeExceptionForInvalidSoaTtl(int ttl) + { + // Arrange + _request.ZoneDefaults.SOA.TimeToLive = ttl; + var client = GetClient(); + + // Act + await client.UpdateDnsAccountSettings(_request); + + // Assert - ArgumentOutOfRangeException + } + + [DataTestMethod] + [DataRow(null)] + [DataRow("")] + [DataRow(" ")] + [ExpectedException(typeof(ArgumentNullException))] + public async Task ShouldThrowArgumentNullExceptionForMissingSoaAdministrator(string admin) + { + // Arrange + _request.ZoneDefaults.SOA.ZoneAdministrator = admin; + var client = GetClient(); + + // Act + await client.UpdateDnsAccountSettings(_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; + } + } +}