From c5d0073e6dcdeb47bec9cfef2b00fe69fa387f16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20M=C3=BCller?= Date: Thu, 31 Jul 2025 12:10:39 +0200 Subject: [PATCH] Add 'DNS Account Settings > Internal Views' --- CHANGELOG.md | 5 +- .../DnsAccountSettingsExtensions.cs | 99 +++++ .../Filters/ListInternalDnsViewsFilter.cs | 153 ++++++++ .../InternalModifyInternalDnsViewRequest.cs | 11 + .../Cloudflare.Dns/Models/InternalDnsView.cs | 49 +++ src/Extensions/Cloudflare.Dns/README.md | 5 + .../Requests/CreateInternalDnsViewRequest.cs | 34 ++ .../Requests/UpdateInternalDnsViewRequest.cs | 25 ++ .../ShowDnsAccountSettingsTest.cs | 4 +- .../Views/CreateInternalDnsViewTest.cs | 115 ++++++ .../Views/DeleteInternalDnsViewTest.cs | 76 ++++ .../Views/InternalDnsViewDetailsTest.cs | 80 ++++ .../Views/ListInternalDnsViewsTest.cs | 354 ++++++++++++++++++ .../Views/UpdateInternalDnsViewTest.cs | 121 ++++++ 14 files changed, 1127 insertions(+), 4 deletions(-) create mode 100644 src/Extensions/Cloudflare.Dns/Filters/ListInternalDnsViewsFilter.cs create mode 100644 src/Extensions/Cloudflare.Dns/Internals/InternalModifyInternalDnsViewRequest.cs create mode 100644 src/Extensions/Cloudflare.Dns/Models/InternalDnsView.cs create mode 100644 src/Extensions/Cloudflare.Dns/Requests/CreateInternalDnsViewRequest.cs create mode 100644 src/Extensions/Cloudflare.Dns/Requests/UpdateInternalDnsViewRequest.cs create mode 100644 test/Extensions/Cloudflare.Dns.Tests/DnsAccountSettingsExtensions/Views/CreateInternalDnsViewTest.cs create mode 100644 test/Extensions/Cloudflare.Dns.Tests/DnsAccountSettingsExtensions/Views/DeleteInternalDnsViewTest.cs create mode 100644 test/Extensions/Cloudflare.Dns.Tests/DnsAccountSettingsExtensions/Views/InternalDnsViewDetailsTest.cs create mode 100644 test/Extensions/Cloudflare.Dns.Tests/DnsAccountSettingsExtensions/Views/ListInternalDnsViewsTest.cs create mode 100644 test/Extensions/Cloudflare.Dns.Tests/DnsAccountSettingsExtensions/Views/UpdateInternalDnsViewTest.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ef0eda..6879451 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,8 +10,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- `Cloudflare` with basic functionality to communicate with Cloudflare's API -- `Cloudflare.Zones` extending the core package with specific methods to manage Cloudflare's DNS zones +- `Cloudflare` with the core functionality to communicate with Cloudflare's API endpoint +- `Cloudflare.Dns` extending the core package with specific methods to manage Cloudflare's DNS settings +- `Cloudflare.Zones` extending the core package with specific methods to manage Cloudflare's Domain/Zone Management diff --git a/src/Extensions/Cloudflare.Dns/DnsAccountSettingsExtensions.cs b/src/Extensions/Cloudflare.Dns/DnsAccountSettingsExtensions.cs index eae7df3..080e21d 100644 --- a/src/Extensions/Cloudflare.Dns/DnsAccountSettingsExtensions.cs +++ b/src/Extensions/Cloudflare.Dns/DnsAccountSettingsExtensions.cs @@ -86,5 +86,104 @@ namespace AMWD.Net.Api.Cloudflare.Dns return client.GetAsync($"/accounts/{accountId}/dns_settings", null, cancellationToken); } + + #region Views + + /// + /// Create Internal DNS View for an account. + /// + /// The instance. + /// The request. + /// A cancellation token used to propagate notification that this operation should be canceled. + public static Task> CreateInternalDnsView(this ICloudflareClient client, CreateInternalDnsViewRequest request, CancellationToken cancellationToken = default) + { + request.AccountId.ValidateCloudflareId(); + + if (string.IsNullOrWhiteSpace(request.Name)) + throw new ArgumentNullException(nameof(request.Name)); + + if (request.Name.Length > 255) + throw new ArgumentOutOfRangeException(nameof(request.Name), request.Name, "The Name length must be between 1 and 255 characters."); + + var req = new InternalModifyInternalDnsViewRequest + { + Name = request.Name, + Zones = request.ZoneIds + }; + + return client.PostAsync($"/accounts/{request.AccountId}/dns_settings/views", req, null, cancellationToken); + } + + /// + /// Delete an existing Internal DNS View. + /// + /// The instance. + /// The account identifier. + /// The view identifier. + /// A cancellation token used to propagate notification that this operation should be canceled. + public static Task> DeleteInternalDnsView(this ICloudflareClient client, string accountId, string viewId, CancellationToken cancellationToken = default) + { + accountId.ValidateCloudflareId(); + viewId.ValidateCloudflareId(); + + return client.DeleteAsync($"/accounts/{accountId}/dns_settings/views/{viewId}", null, cancellationToken); + } + + /// + /// Update an existing Internal DNS View. + /// + /// The instance. + /// The request. + /// A cancellation token used to propagate notification that this operation should be canceled. + public static Task> UpdateInternalDnsView(this ICloudflareClient client, UpdateInternalDnsViewRequest request, CancellationToken cancellationToken = default) + { + request.AccountId.ValidateCloudflareId(); + request.ViewId.ValidateCloudflareId(); + + if (string.IsNullOrWhiteSpace(request.Name)) + throw new ArgumentNullException(nameof(request.Name)); + + if (request.Name.Length > 255) + throw new ArgumentOutOfRangeException(nameof(request.Name), request.Name, "The Name length must be between 1 and 255 characters."); + + var req = new InternalModifyInternalDnsViewRequest + { + Name = request.Name, + Zones = request.ZoneIds + }; + + return client.PatchAsync($"/accounts/{request.AccountId}/dns_settings/views/{request.ViewId}", req, cancellationToken); + } + + /// + /// Get DNS Internal View. + /// + /// The instance. + /// The account identifier. + /// The view identifier. + /// A cancellation token used to propagate notification that this operation should be canceled. + public static Task> InternalDnsViewDetails(this ICloudflareClient client, string accountId, string viewId, CancellationToken cancellationToken = default) + { + accountId.ValidateCloudflareId(); + viewId.ValidateCloudflareId(); + + return client.GetAsync($"/accounts/{accountId}/dns_settings/views/{viewId}", null, cancellationToken); + } + + /// + /// List DNS Internal Views for an Account. + /// + /// The instance. + /// The account identifier. + /// Filter options (optional). + /// A cancellation token used to propagate notification that this operation should be canceled. + public static Task>> ListInternalDnsViews(this ICloudflareClient client, string accountId, ListInternalDnsViewsFilter? options = null, CancellationToken cancellationToken = default) + { + accountId.ValidateCloudflareId(); + + return client.GetAsync>($"/accounts/{accountId}/dns_settings/views", options, cancellationToken); + } + + #endregion Views } } diff --git a/src/Extensions/Cloudflare.Dns/Filters/ListInternalDnsViewsFilter.cs b/src/Extensions/Cloudflare.Dns/Filters/ListInternalDnsViewsFilter.cs new file mode 100644 index 0000000..0fbc21e --- /dev/null +++ b/src/Extensions/Cloudflare.Dns/Filters/ListInternalDnsViewsFilter.cs @@ -0,0 +1,153 @@ +using System.Runtime.Serialization; +using Newtonsoft.Json.Converters; + +namespace AMWD.Net.Api.Cloudflare.Dns +{ + /// + /// Filter for listing internal DNS views. + /// + public class ListInternalDnsViewsFilter : IQueryParameterFilter + { + /// + /// Direction to order DNS views in. + /// + public SortDirection? Direction { get; set; } + + /// + /// Whether to match all search requirements or at least one (any). + /// + /// + /// + /// If set to , acts like a logical AND between filters. + ///
+ /// If set to , acts like a logical OR instead. + ///
+ ///
+ public FilterMatchType? Match { get; set; } + + #region Name + + /// + /// Substring of the DNS record Name. + /// Name filters are case-insensitive. + /// + public string? NameContains { get; set; } + + /// + /// Suffix of the DNS record Name. + /// Name filters are case-insensitive. + /// + public string? NameEndsWith { get; set; } + + /// + /// Exact value of the DNS record Name. + /// Name filters are case-insensitive. + /// + public string? NameExact { get; set; } + + /// + /// Prefix of the DNS record Name. + /// Name filters are case-insensitive. + /// + public string? NameStartsWith { get; set; } + + #endregion Name + + /// + /// Field to order DNS views by. + /// + public InternalDnsViewsOrderBy? OrderBy { get; set; } + + /// + /// Page number of paginated results. + /// + /// 1 <= X + public int? Page { get; set; } + + /// + /// Number of DNS records per page. + /// + /// 1 <= X <= 5,000,000 + public int? PerPage { get; set; } + + /// + /// A zone ID that exists in the zones list for the view. + /// + public string? ZoneId { get; set; } + + /// + /// A zone name that exists in the zones list for the view. + /// + public string? ZoneName { get; set; } + + /// + public IReadOnlyDictionary GetQueryParameters() + { + var dict = new Dictionary(); + +#pragma warning disable CS8602, CS8604 // There will be no null value below. + + if (Direction.HasValue && Enum.IsDefined(typeof(SortDirection), Direction.Value)) + dict.Add("direction", Direction.Value.GetEnumMemberValue()); + + if (Match.HasValue && Enum.IsDefined(typeof(FilterMatchType), Match.Value)) + dict.Add("match", Match.Value.GetEnumMemberValue()); + + if (!string.IsNullOrWhiteSpace(NameContains)) + dict.Add("name.contains", NameContains.Trim()); + + if (!string.IsNullOrWhiteSpace(NameEndsWith)) + dict.Add("name.endswith", NameEndsWith.Trim()); + + if (!string.IsNullOrWhiteSpace(NameExact)) + dict.Add("name.exact", NameExact.Trim()); + + if (!string.IsNullOrWhiteSpace(NameStartsWith)) + dict.Add("name.startswith", NameStartsWith.Trim()); + + if (OrderBy.HasValue && Enum.IsDefined(typeof(InternalDnsViewsOrderBy), OrderBy.Value)) + dict.Add("order", OrderBy.Value.GetEnumMemberValue()); + + if (Page.HasValue && Page.Value >= 1) + dict.Add("page", Page.Value.ToString()); + + if (PerPage.HasValue && PerPage.Value >= 1 && PerPage.Value <= 5_000_000) + dict.Add("per_page", PerPage.Value.ToString()); + + if (!string.IsNullOrWhiteSpace(ZoneId)) + dict.Add("zone_id", ZoneId.Trim()); + + if (!string.IsNullOrWhiteSpace(ZoneName)) + dict.Add("zone_name", ZoneName.Trim()); + +#pragma warning restore CS8602, CS8604 + + return dict; + } + } + + /// + /// Possible fields to order internal DNS views by. + /// + [JsonConverter(typeof(StringEnumConverter))] + public enum InternalDnsViewsOrderBy + { + /// + /// Order by name. + /// + [EnumMember(Value = "name")] + Name = 1, + + /// + /// Order by creation date. + /// + [EnumMember(Value = "created_on")] + CreatedOn = 2, + + /// + /// Order by last modified date. + /// + [EnumMember(Value = "modified_on")] + ModifiedOn = 3 + } +} diff --git a/src/Extensions/Cloudflare.Dns/Internals/InternalModifyInternalDnsViewRequest.cs b/src/Extensions/Cloudflare.Dns/Internals/InternalModifyInternalDnsViewRequest.cs new file mode 100644 index 0000000..42cf9cf --- /dev/null +++ b/src/Extensions/Cloudflare.Dns/Internals/InternalModifyInternalDnsViewRequest.cs @@ -0,0 +1,11 @@ +namespace AMWD.Net.Api.Cloudflare.Dns.Internals +{ + internal class InternalModifyInternalDnsViewRequest + { + [JsonProperty("name")] + public string? Name { get; set; } + + [JsonProperty("zones")] + public IReadOnlyCollection? Zones { get; set; } + } +} diff --git a/src/Extensions/Cloudflare.Dns/Models/InternalDnsView.cs b/src/Extensions/Cloudflare.Dns/Models/InternalDnsView.cs new file mode 100644 index 0000000..eb39597 --- /dev/null +++ b/src/Extensions/Cloudflare.Dns/Models/InternalDnsView.cs @@ -0,0 +1,49 @@ +namespace AMWD.Net.Api.Cloudflare.Dns +{ + /// + /// A Cloudflare internal DNS view. + /// + public class InternalDnsView + { + /// + /// Initializes a new instance of the class. + /// + /// The identifier. + /// The name of the view. + public InternalDnsView(string id, string name) + { + Id = id; + Name = name; + } + + /// + /// The identifier. + /// + [JsonProperty("id")] + public string Id { get; set; } + + /// + /// When the view was created. + /// + [JsonProperty("created_time")] + public DateTime? CreatedTime { get; set; } + + /// + /// When the view was last modified. + /// + [JsonProperty("modified_time")] + public DateTime? ModifiedTime { get; set; } + + /// + /// The name of the view. + /// + [JsonProperty("name")] + public string Name { get; set; } + + /// + /// The list of zones linked to this view. + /// + [JsonProperty("zones")] + public IReadOnlyCollection ZoneIds { get; set; } = []; + } +} diff --git a/src/Extensions/Cloudflare.Dns/README.md b/src/Extensions/Cloudflare.Dns/README.md index ad62faf..d9ccadc 100644 --- a/src/Extensions/Cloudflare.Dns/README.md +++ b/src/Extensions/Cloudflare.Dns/README.md @@ -33,6 +33,11 @@ This package contains the feature set of the _DNS_ section of the Cloudflare API - [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/) +- [Create Internal DNS View](https://developers.cloudflare.com/api/resources/dns/subresources/settings/subresources/account/subresources/views/methods/create/) +- [Delete Internal DNS View](https://developers.cloudflare.com/api/resources/dns/subresources/settings/subresources/account/subresources/views/methods/delete/) +- [Update Internal DNS View](https://developers.cloudflare.com/api/resources/dns/subresources/settings/subresources/account/subresources/views/methods/edit/) +- [DNS Internal View Details](https://developers.cloudflare.com/api/resources/dns/subresources/settings/subresources/account/subresources/views/methods/get/) +- [List Internal DNS Views](https://developers.cloudflare.com/api/resources/dns/subresources/settings/subresources/account/subresources/views/methods/list/) ##### [Zone] diff --git a/src/Extensions/Cloudflare.Dns/Requests/CreateInternalDnsViewRequest.cs b/src/Extensions/Cloudflare.Dns/Requests/CreateInternalDnsViewRequest.cs new file mode 100644 index 0000000..42e49b0 --- /dev/null +++ b/src/Extensions/Cloudflare.Dns/Requests/CreateInternalDnsViewRequest.cs @@ -0,0 +1,34 @@ +namespace AMWD.Net.Api.Cloudflare.Dns +{ + /// + /// Represents a request to create an internal DNS view. + /// + public class CreateInternalDnsViewRequest + { + /// + /// Initializes a new instance of the class. + /// + /// The account identifier. + /// The name of the view. + public CreateInternalDnsViewRequest(string accountId, string name) + { + AccountId = accountId; + Name = name; + } + + /// + /// The account identifier. + /// + public string AccountId { get; set; } + + /// + /// The name of the view. + /// + public string Name { get; set; } + + /// + /// The list of zones linked to this view. + /// + public IReadOnlyCollection ZoneIds { get; set; } = []; + } +} diff --git a/src/Extensions/Cloudflare.Dns/Requests/UpdateInternalDnsViewRequest.cs b/src/Extensions/Cloudflare.Dns/Requests/UpdateInternalDnsViewRequest.cs new file mode 100644 index 0000000..0076923 --- /dev/null +++ b/src/Extensions/Cloudflare.Dns/Requests/UpdateInternalDnsViewRequest.cs @@ -0,0 +1,25 @@ +namespace AMWD.Net.Api.Cloudflare.Dns +{ + /// + /// Represents a request to update an internal DNS view. + /// + public class UpdateInternalDnsViewRequest : CreateInternalDnsViewRequest + { + /// + /// Initializes a new instance of the class. + /// + /// The account identifier. + /// The view identifier. + /// The name of the view. + public UpdateInternalDnsViewRequest(string accountId, string viewId, string name) + : base(accountId, name) + { + ViewId = viewId; + } + + /// + /// The view identifier. + /// + public string ViewId { get; set; } + } +} diff --git a/test/Extensions/Cloudflare.Dns.Tests/DnsAccountSettingsExtensions/ShowDnsAccountSettingsTest.cs b/test/Extensions/Cloudflare.Dns.Tests/DnsAccountSettingsExtensions/ShowDnsAccountSettingsTest.cs index e421938..9699978 100644 --- a/test/Extensions/Cloudflare.Dns.Tests/DnsAccountSettingsExtensions/ShowDnsAccountSettingsTest.cs +++ b/test/Extensions/Cloudflare.Dns.Tests/DnsAccountSettingsExtensions/ShowDnsAccountSettingsTest.cs @@ -83,8 +83,8 @@ namespace Cloudflare.Dns.Tests.DnsAccountSettingsExtensions Assert.IsNull(callback.QueryFilter); - _clientMock?.Verify(m => m.GetAsync($"/accounts/{AccountId}/dns_settings", null, It.IsAny()), Times.Once); - _clientMock?.VerifyNoOtherCalls(); + _clientMock.Verify(m => m.GetAsync($"/accounts/{AccountId}/dns_settings", null, It.IsAny()), Times.Once); + _clientMock.VerifyNoOtherCalls(); } private ICloudflareClient GetClient() diff --git a/test/Extensions/Cloudflare.Dns.Tests/DnsAccountSettingsExtensions/Views/CreateInternalDnsViewTest.cs b/test/Extensions/Cloudflare.Dns.Tests/DnsAccountSettingsExtensions/Views/CreateInternalDnsViewTest.cs new file mode 100644 index 0000000..3552095 --- /dev/null +++ b/test/Extensions/Cloudflare.Dns.Tests/DnsAccountSettingsExtensions/Views/CreateInternalDnsViewTest.cs @@ -0,0 +1,115 @@ +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.Views +{ + [TestClass] + public class CreateInternalDnsViewTest + { + private const string AccountId = "023e105f4ecef8ad9ca31a8372d0c353"; + private const string ViewId = "023e105f4ecef8ad9ca31a8372d0c354"; + private const string ViewName = "InternalView"; + + private Mock _clientMock; + private CloudflareResponse _response; + private List<(string RequestPath, InternalModifyInternalDnsViewRequest Request)> _callbacks; + private CreateInternalDnsViewRequest _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 InternalDnsView(ViewId, ViewName) + }; + + _request = new CreateInternalDnsViewRequest(AccountId, ViewName) + { + ZoneIds = ["zone1", "zone2"] + }; + } + + [TestMethod] + public async Task ShouldCreateInternalDnsView() + { + // Arrange + var client = GetClient(); + + // Act + var response = await client.CreateInternalDnsView(_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/views", callback.RequestPath); + Assert.IsNotNull(callback.Request); + + Assert.AreEqual(ViewName, callback.Request.Name); + CollectionAssert.AreEqual(_request.ZoneIds.ToList(), callback.Request.Zones.ToList()); + + _clientMock.Verify(m => m.PostAsync($"/accounts/{AccountId}/dns_settings/views", It.IsAny(), null, It.IsAny()), Times.Once); + _clientMock.VerifyNoOtherCalls(); + } + + [DataTestMethod] + [DataRow(null)] + [DataRow("")] + [DataRow(" ")] + [ExpectedException(typeof(ArgumentNullException))] + public async Task ShouldThrowArgumentNullExceptionWhenNameIsNull(string name) + { + // Arrange + _request.Name = name; + var client = GetClient(); + + // Act + var response = await client.CreateInternalDnsView(_request); + + // Assert - ArgumentNullException + } + + [TestMethod] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public async Task ShouldThrowArgumentOutOfRangeExceptionWhenNameTooLong() + { + // Arrange + _request.Name = new string('a', 256); + var client = GetClient(); + + // Act + var response = await client.CreateInternalDnsView(_request); + + // Assert - ArgumentOutOfRangeException + } + + 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/DnsAccountSettingsExtensions/Views/DeleteInternalDnsViewTest.cs b/test/Extensions/Cloudflare.Dns.Tests/DnsAccountSettingsExtensions/Views/DeleteInternalDnsViewTest.cs new file mode 100644 index 0000000..f28d948 --- /dev/null +++ b/test/Extensions/Cloudflare.Dns.Tests/DnsAccountSettingsExtensions/Views/DeleteInternalDnsViewTest.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.DnsAccountSettingsExtensions.Views +{ + [TestClass] + public class DeleteInternalDnsViewTest + { + private const string AccountId = "023e105f4ecef8ad9ca31a8372d0c353"; + private const string ViewId = "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 Identifier + { + Id = ViewId + } + }; + } + + [TestMethod] + public async Task ShouldDeleteInternalDnsView() + { + // Arrange + var client = GetClient(); + + // Act + var response = await client.DeleteInternalDnsView(AccountId, ViewId); + + // 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/views/{ViewId}", callback.RequestPath); + Assert.IsNull(callback.QueryFilter); + + _clientMock.Verify(m => m.DeleteAsync($"/accounts/{AccountId}/dns_settings/views/{ViewId}", null, It.IsAny()), Times.Once); + _clientMock.VerifyNoOtherCalls(); + } + + 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/DnsAccountSettingsExtensions/Views/InternalDnsViewDetailsTest.cs b/test/Extensions/Cloudflare.Dns.Tests/DnsAccountSettingsExtensions/Views/InternalDnsViewDetailsTest.cs new file mode 100644 index 0000000..cfa8160 --- /dev/null +++ b/test/Extensions/Cloudflare.Dns.Tests/DnsAccountSettingsExtensions/Views/InternalDnsViewDetailsTest.cs @@ -0,0 +1,80 @@ +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.Views +{ + [TestClass] + public class InternalDnsViewDetailsTest + { + private const string AccountId = "023e105f4ecef8ad9ca31a8372d0c353"; + private const string ViewId = "023e105f4ecef8ad9ca31a8372d0c354"; + private const string ViewName = "InternalView"; + + 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 InternalDnsView(ViewId, ViewName) + }; + } + + [TestMethod] + public async Task ShouldGetInternalDnsViewDetails() + { + // Arrange + var client = GetClient(); + + // Act + var response = await client.InternalDnsViewDetails(AccountId, ViewId); + + // 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/views/{ViewId}", callback.RequestPath); + Assert.IsNull(callback.QueryFilter); + + _clientMock.Verify(m => m.GetAsync( + $"/accounts/{AccountId}/dns_settings/views/{ViewId}", + 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/Views/ListInternalDnsViewsTest.cs b/test/Extensions/Cloudflare.Dns.Tests/DnsAccountSettingsExtensions/Views/ListInternalDnsViewsTest.cs new file mode 100644 index 0000000..69a4add --- /dev/null +++ b/test/Extensions/Cloudflare.Dns.Tests/DnsAccountSettingsExtensions/Views/ListInternalDnsViewsTest.cs @@ -0,0 +1,354 @@ +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.Views +{ + [TestClass] + public class ListInternalDnsViewsTest + { + private const string AccountId = "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 InternalDnsView("023e105f4ecef8ad9ca31a8372d0c354", "View1"), + new InternalDnsView("023e105f4ecef8ad9ca31a8372d0c355", "View2") + ] + }; + } + + [TestMethod] + public async Task ShouldListInternalDnsViews() + { + // Arrange + var client = GetClient(); + + // Act + var response = await client.ListInternalDnsViews(AccountId); + + // Assert + Assert.IsNotNull(response); + Assert.IsTrue(response.Success); + Assert.IsNotNull(response.Result); + Assert.AreEqual(2, response.Result.Count); + + Assert.AreEqual(1, _callbacks.Count); + + var callback = _callbacks.First(); + Assert.AreEqual($"/accounts/{AccountId}/dns_settings/views", callback.RequestPath); + Assert.IsNull(callback.QueryFilter); + + _clientMock.Verify(m => m.GetAsync>( + $"/accounts/{AccountId}/dns_settings/views", + null, + It.IsAny()), Times.Once); + _clientMock.VerifyNoOtherCalls(); + } + + [TestMethod] + public async Task ShouldListInternalDnsViewsWithFilter() + { + // Arrange + var client = GetClient(); + var filter = new ListInternalDnsViewsFilter + { + NameContains = "View" + }; + + // Act + var response = await client.ListInternalDnsViews(AccountId, filter); + + // Assert + Assert.IsNotNull(response); + Assert.IsTrue(response.Success); + Assert.IsNotNull(response.Result); + + Assert.AreEqual(1, _callbacks.Count); + + var callback = _callbacks.First(); + Assert.AreEqual($"/accounts/{AccountId}/dns_settings/views", callback.RequestPath); + Assert.AreEqual(filter, callback.QueryFilter); + + _clientMock.Verify(m => m.GetAsync>( + $"/accounts/{AccountId}/dns_settings/views", + filter, + It.IsAny()), Times.Once); + _clientMock.VerifyNoOtherCalls(); + } + + #region QueryFilter + + [TestMethod] + public void ShouldReturnEmptyParameterList() + { + // Arrange + var filter = new ListInternalDnsViewsFilter(); + + // Act + var dict = filter.GetQueryParameters(); + + // Assert + Assert.IsNotNull(dict); + Assert.AreEqual(0, dict.Count); + } + + [TestMethod] + public void ShouldReturnFullParameterList() + { + // Arrange + var filter = new ListInternalDnsViewsFilter + { + Direction = SortDirection.Descending, + Match = FilterMatchType.All, + NameContains = "view", + NameEndsWith = "end", + NameExact = "exactView", + NameStartsWith = "start", + OrderBy = InternalDnsViewsOrderBy.ModifiedOn, + Page = 2, + PerPage = 100, + ZoneId = "zone123", + ZoneName = "zone.example.com", + }; + + // Act + var dict = filter.GetQueryParameters(); + + // Assert + Assert.IsNotNull(dict); + Assert.AreEqual(11, dict.Count); + + Assert.AreEqual("desc", dict["direction"]); + Assert.AreEqual("all", dict["match"]); + Assert.AreEqual("view", dict["name.contains"]); + Assert.AreEqual("end", dict["name.endswith"]); + Assert.AreEqual("exactView", dict["name.exact"]); + Assert.AreEqual("start", dict["name.startswith"]); + Assert.AreEqual("modified_on", dict["order"]); + Assert.AreEqual("2", dict["page"]); + Assert.AreEqual("100", dict["per_page"]); + Assert.AreEqual("zone123", dict["zone_id"]); + Assert.AreEqual("zone.example.com", dict["zone_name"]); + } + + [DataTestMethod] + [DataRow(null)] + [DataRow("")] + [DataRow(" ")] + public void ShouldNotAddNameContains(string str) + { + // Arrange + var filter = new ListInternalDnsViewsFilter { NameContains = str }; + + // Act + var dict = filter.GetQueryParameters(); + + // Assert + Assert.IsNotNull(dict); + Assert.AreEqual(0, dict.Count); + } + + [DataTestMethod] + [DataRow(null)] + [DataRow("")] + [DataRow(" ")] + public void ShouldNotAddNameEndsWith(string str) + { + // Arrange + var filter = new ListInternalDnsViewsFilter { NameEndsWith = str }; + + // Act + var dict = filter.GetQueryParameters(); + + // Assert + Assert.IsNotNull(dict); + Assert.AreEqual(0, dict.Count); + } + + [DataTestMethod] + [DataRow(null)] + [DataRow("")] + [DataRow(" ")] + public void ShouldNotAddNameExact(string str) + { + // Arrange + var filter = new ListInternalDnsViewsFilter { NameExact = str }; + + // Act + var dict = filter.GetQueryParameters(); + + // Assert + Assert.IsNotNull(dict); + Assert.AreEqual(0, dict.Count); + } + + [DataTestMethod] + [DataRow(null)] + [DataRow("")] + [DataRow(" ")] + public void ShouldNotAddNameStartsWith(string str) + { + // Arrange + var filter = new ListInternalDnsViewsFilter { NameStartsWith = str }; + + // Act + var dict = filter.GetQueryParameters(); + + // Assert + Assert.IsNotNull(dict); + Assert.AreEqual(0, dict.Count); + } + + [DataTestMethod] + [DataRow(null)] + [DataRow((SortDirection)0)] + public void ShouldNotAddDirection(SortDirection? direction) + { + // Arrange + var filter = new ListInternalDnsViewsFilter { Direction = direction }; + + // Act + var dict = filter.GetQueryParameters(); + + // Assert + Assert.IsNotNull(dict); + Assert.AreEqual(0, dict.Count); + } + + [DataTestMethod] + [DataRow(null)] + [DataRow((FilterMatchType)0)] + public void ShouldNotAddMatch(FilterMatchType? match) + { + // Arrange + var filter = new ListInternalDnsViewsFilter { Match = match }; + + // Act + var dict = filter.GetQueryParameters(); + + // Assert + Assert.IsNotNull(dict); + Assert.AreEqual(0, dict.Count); + } + + [DataTestMethod] + [DataRow(null)] + [DataRow((InternalDnsViewsOrderBy)0)] + public void ShouldNotAddOrder(InternalDnsViewsOrderBy? order) + { + // Arrange + var filter = new ListInternalDnsViewsFilter { OrderBy = order }; + + // Act + var dict = filter.GetQueryParameters(); + + // Assert + Assert.IsNotNull(dict); + Assert.AreEqual(0, dict.Count); + } + + [DataTestMethod] + [DataRow(null)] + [DataRow(0)] + public void ShouldNotAddPage(int? page) + { + // Arrange + var filter = new ListInternalDnsViewsFilter { Page = page }; + + // Act + var dict = filter.GetQueryParameters(); + + // Assert + Assert.IsNotNull(dict); + Assert.AreEqual(0, dict.Count); + } + + [DataTestMethod] + [DataRow(null)] + [DataRow(0)] + [DataRow(5_000_001)] + public void ShouldNotAddPerPage(int? perPage) + { + // Arrange + var filter = new ListInternalDnsViewsFilter { PerPage = perPage }; + + // Act + var dict = filter.GetQueryParameters(); + + // Assert + Assert.IsNotNull(dict); + Assert.AreEqual(0, dict.Count); + } + + [DataTestMethod] + [DataRow(null)] + [DataRow("")] + [DataRow(" ")] + public void ShouldNotAddZoneId(string str) + { + // Arrange + var filter = new ListInternalDnsViewsFilter { ZoneId = str }; + + // Act + var dict = filter.GetQueryParameters(); + + // Assert + Assert.IsNotNull(dict); + Assert.AreEqual(0, dict.Count); + } + + [DataTestMethod] + [DataRow(null)] + [DataRow("")] + [DataRow(" ")] + public void ShouldNotAddZoneName(string str) + { + // Arrange + var filter = new ListInternalDnsViewsFilter { ZoneName = str }; + + // Act + var dict = filter.GetQueryParameters(); + + // Assert + Assert.IsNotNull(dict); + Assert.AreEqual(0, dict.Count); + } + + #endregion QueryFilter + + 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/Views/UpdateInternalDnsViewTest.cs b/test/Extensions/Cloudflare.Dns.Tests/DnsAccountSettingsExtensions/Views/UpdateInternalDnsViewTest.cs new file mode 100644 index 0000000..3a35c37 --- /dev/null +++ b/test/Extensions/Cloudflare.Dns.Tests/DnsAccountSettingsExtensions/Views/UpdateInternalDnsViewTest.cs @@ -0,0 +1,121 @@ +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.Views +{ + [TestClass] + public class UpdateInternalDnsViewTest + { + private const string AccountId = "023e105f4ecef8ad9ca31a8372d0c353"; + private const string ViewId = "023e105f4ecef8ad9ca31a8372d0c354"; + private const string ViewName = "InternalView"; + + private Mock _clientMock; + private CloudflareResponse _response; + private List<(string RequestPath, InternalModifyInternalDnsViewRequest Request)> _callbacks; + private UpdateInternalDnsViewRequest _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 InternalDnsView(ViewId, ViewName) + }; + + _request = new UpdateInternalDnsViewRequest(AccountId, ViewId, ViewName) + { + ZoneIds = ["zone1", "zone2"] + }; + } + + [TestMethod] + public async Task ShouldUpdateInternalDnsView() + { + // Arrange + var client = GetClient(); + + // Act + var response = await client.UpdateInternalDnsView(_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/views/{ViewId}", callback.RequestPath); + Assert.IsNotNull(callback.Request); + + Assert.AreEqual(ViewName, callback.Request.Name); + CollectionAssert.AreEqual(_request.ZoneIds.ToList(), callback.Request.Zones.ToList()); + + _clientMock.Verify(m => m.PatchAsync( + $"/accounts/{AccountId}/dns_settings/views/{ViewId}", + It.IsAny(), + It.IsAny()), Times.Once); + _clientMock.VerifyNoOtherCalls(); + } + + [DataTestMethod] + [DataRow(null)] + [DataRow("")] + [DataRow(" ")] + [ExpectedException(typeof(ArgumentNullException))] + public async Task ShouldThrowArgumentNullExceptionWhenNameIsNull(string name) + { + // Arrange + _request.Name = name; + var client = GetClient(); + + // Act + var response = await client.UpdateInternalDnsView(_request); + + // Assert - ArgumentNullException + } + + [TestMethod] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public async Task ShouldThrowArgumentOutOfRangeExceptionWhenNameTooLong() + { + // Arrange + _request.Name = new string('a', 256); + var client = GetClient(); + + // Act + var response = await client.UpdateInternalDnsView(_request); + + // Assert - ArgumentOutOfRangeException + } + + 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; + } + } +}