diff --git a/src/Extensions/Cloudflare.Dns/DnsFirewallExtensions.cs b/src/Extensions/Cloudflare.Dns/DnsFirewallExtensions.cs index 287ce6e..5a33f90 100644 --- a/src/Extensions/Cloudflare.Dns/DnsFirewallExtensions.cs +++ b/src/Extensions/Cloudflare.Dns/DnsFirewallExtensions.cs @@ -155,6 +155,43 @@ namespace AMWD.Net.Api.Cloudflare.Dns #endregion DNS Firewall + #region Analytics + + /// + /// Retrieves a list of summarised aggregate metrics over a given time period. + /// + /// The instance. + /// The account identifier. + /// The DNS firewall identifier. + /// Filter options (optional). + /// A cancellation token used to propagate notification that this operation should be canceled. + /// + public static Task> GetDnsFirewallAnalyticsReport(this ICloudflareClient client, string accountId, string dnsFirewallId, GetDnsAnalyticsReportFilter? options = null, CancellationToken cancellationToken = default) + { + accountId.ValidateCloudflareId(); + dnsFirewallId.ValidateCloudflareId(); + + return client.GetAsync($"/accounts/{accountId}/dns_firewall/{dnsFirewallId}/dns_analytics/report", options, cancellationToken); + } + + /// + /// Retrieves a list of aggregate metrics grouped by time interval. + /// + /// The instance. + /// The account identifier. + /// The DNS firewall identifier. + /// Filter options (optional). + /// A cancellation token used to propagate notification that this operation should be canceled. + public static Task> GetDnsFirewallAnalyticsByTime(this ICloudflareClient client, string accountId, string dnsFirewallId, GetDnsAnalyticsByTimeFilter? options = null, CancellationToken cancellationToken = default) + { + accountId.ValidateCloudflareId(); + dnsFirewallId.ValidateCloudflareId(); + + return client.GetAsync($"/accounts/{accountId}/dns_firewall/{dnsFirewallId}/dns_analytics/report/bytime", options, cancellationToken); + } + + #endregion Analytics + #region Reverse DNS /// diff --git a/src/Extensions/Cloudflare.Dns/README.md b/src/Extensions/Cloudflare.Dns/README.md index 2292e74..6841c95 100644 --- a/src/Extensions/Cloudflare.Dns/README.md +++ b/src/Extensions/Cloudflare.Dns/README.md @@ -122,6 +122,12 @@ This package contains the feature set of the _DNS_ section of the Cloudflare API - [Delete DNS Firewall Cluster](https://developers.cloudflare.com/api/resources/dns_firewall/methods/delete/) +#### [Analytics (Firewall)] + +- [Get Report Table](https://developers.cloudflare.com/api/resources/dns_firewall/subresources/analytics/subresources/reports/methods/get/) +- [Get Report By Time](https://developers.cloudflare.com/api/resources/dns_firewall/subresources/analytics/subresources/reports/subresources/bytimes/methods/get/) + + #### [Reverse DNS] - [Show DNS Firewall Cluster Reverse DNS](https://developers.cloudflare.com/api/resources/dns_firewall/subresources/reverse_dns/methods/get/) @@ -152,5 +158,7 @@ Published under MIT License (see [choose a license]) [Outgoing]: https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/outgoing/ [Peers]: https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/peers/ [TSIGs]: https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/tsigs/ + [DNS Firewall]: https://developers.cloudflare.com/api/resources/dns_firewall/ + [Analytics (Firewall)]: https://developers.cloudflare.com/api/resources/dns_firewall/subresources/analytics/ [Reverse DNS]: https://developers.cloudflare.com/api/resources/dns_firewall/subresources/reverse_dns/ diff --git a/test/Extensions/Cloudflare.Dns.Tests/DnsFirewallExtensions/Analytics/GetDnsFirewallAnalyticsByTimeTest.cs b/test/Extensions/Cloudflare.Dns.Tests/DnsFirewallExtensions/Analytics/GetDnsFirewallAnalyticsByTimeTest.cs new file mode 100644 index 0000000..4b2b6a6 --- /dev/null +++ b/test/Extensions/Cloudflare.Dns.Tests/DnsFirewallExtensions/Analytics/GetDnsFirewallAnalyticsByTimeTest.cs @@ -0,0 +1,276 @@ +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.DnsFirewallExtensions.Analytics +{ + [TestClass] + public class GetDnsFirewallAnalyticsByTimeTest + { + public TestContext TestContext { get; set; } + + private const string AccountId = "023e105f4ecef8ad9ca31a8372d0c353"; + private const string ClusterId = "023e105f4ecef8ad9ca31a8372d0c355"; + + 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(1001, "Error 1")], + ResultInfo = new PaginationInfo + { + Count = 1, + Page = 1, + PerPage = 100, + TotalCount = 100, + TotalPages = 1, + }, + Result = new DnsAnalyticsByTime() + }; + } + + [TestMethod] + public async Task ShouldGetDnsFirewallAnalyticsByTime() + { + // Arrange + var client = GetClient(); + + // Act + var response = await client.GetDnsFirewallAnalyticsByTime(AccountId, ClusterId, cancellationToken: TestContext.CancellationToken); + + // Assert + Assert.IsNotNull(response); + Assert.IsTrue(response.Success); + + Assert.IsNotNull(response.Result); + Assert.IsInstanceOfType(response.Result); + + Assert.HasCount(1, _callbacks); + + var (requestPath, queryFilter) = _callbacks.First(); + Assert.AreEqual($"/accounts/{AccountId}/dns_firewall/{ClusterId}/dns_analytics/report/bytime", requestPath); + Assert.IsNull(queryFilter); + + _clientMock.Verify(m => m.GetAsync($"/accounts/{AccountId}/dns_firewall/{ClusterId}/dns_analytics/report/bytime", null, TestContext.CancellationToken), Times.Once); + _clientMock.VerifyNoOtherCalls(); + } + + [TestMethod] + public async Task ShouldGetDnsFirewallAnalyticsByTimeWithFilter() + { + // Arrange + var filter = new GetDnsAnalyticsByTimeFilter + { + Since = DateTime.UtcNow.AddDays(-7), + Until = DateTime.UtcNow, + TimeDelta = TimeDeltaUnit.Day, + Dimensions = ["queryName", "responseCode"], + Filters = "queryType eq A", + Limit = 500, + Metrics = ["requests", "responses"], + Sort = ["-requests", "+responses"] + }; + + var client = GetClient(); + + // Act + var response = await client.GetDnsFirewallAnalyticsByTime(AccountId, ClusterId, filter, TestContext.CancellationToken); + + // Assert + Assert.IsNotNull(response); + Assert.IsTrue(response.Success); + + Assert.IsNotNull(response.Result); + Assert.IsInstanceOfType(response.Result); + + Assert.HasCount(1, _callbacks); + + var (requestPath, queryFilter) = _callbacks.First(); + Assert.AreEqual($"/accounts/{AccountId}/dns_firewall/{ClusterId}/dns_analytics/report/bytime", requestPath); + Assert.IsNotNull(queryFilter); + + Assert.IsInstanceOfType(queryFilter); + + _clientMock.Verify(m => m.GetAsync($"/accounts/{AccountId}/dns_firewall/{ClusterId}/dns_analytics/report/bytime", filter, TestContext.CancellationToken), Times.Once); + _clientMock.VerifyNoOtherCalls(); + } + + [TestMethod] + public void ShouldReturnEmptyParameterList() + { + // Arrange + var filter = new GetDnsAnalyticsByTimeFilter(); + + // Act + var dict = filter.GetQueryParameters(); + + // Assert + Assert.IsNotNull(dict); + Assert.IsEmpty(dict); + } + + [TestMethod] + public void ShouldReturnFullParameterList() + { + // Arrange + var since = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc); + var until = new DateTime(2024, 1, 8, 0, 0, 0, DateTimeKind.Utc); + var filter = new GetDnsAnalyticsByTimeFilter + { + Dimensions = ["queryName", "responseCode"], + Filters = "queryType eq A", + Limit = 1000, + Metrics = ["requests", "responses"], + Since = since, + Sort = ["-requests", "+responses"], + Until = until, + TimeDelta = TimeDeltaUnit.Hour + }; + + // Act + var dict = filter.GetQueryParameters(); + + // Assert + Assert.IsNotNull(dict); + Assert.HasCount(8, dict); + + Assert.IsTrue(dict.ContainsKey("dimensions")); + Assert.AreEqual("queryName,responseCode", dict["dimensions"]); + + Assert.IsTrue(dict.ContainsKey("filters")); + Assert.AreEqual("queryType eq A", dict["filters"]); + + Assert.IsTrue(dict.ContainsKey("limit")); + Assert.AreEqual("1000", dict["limit"]); + + Assert.IsTrue(dict.ContainsKey("metrics")); + Assert.AreEqual("requests,responses", dict["metrics"]); + + Assert.IsTrue(dict.ContainsKey("since")); + Assert.AreEqual(since.ToIso8601Format(), dict["since"]); + + Assert.IsTrue(dict.ContainsKey("sort")); + Assert.AreEqual("-requests,+responses", dict["sort"]); + + Assert.IsTrue(dict.ContainsKey("until")); + Assert.AreEqual(until.ToIso8601Format(), dict["until"]); + + Assert.IsTrue(dict.ContainsKey("time_delta")); + Assert.AreEqual("hour", dict["time_delta"]); + } + + [TestMethod] + [DataRow(null)] + [DataRow("")] + [DataRow(" ")] + public void ShouldNotAddFilters(string str) + { + // Arrange + var filter = new GetDnsAnalyticsByTimeFilter { Filters = str }; + + // Act + var dict = filter.GetQueryParameters(); + + // Assert + Assert.IsNotNull(dict); + Assert.IsEmpty(dict); + } + + [TestMethod] + [DataRow(null)] + [DataRow(0)] + public void ShouldNotAddLimit(int? limit) + { + // Arrange + var filter = new GetDnsAnalyticsByTimeFilter { Limit = limit }; + + // Act + var dict = filter.GetQueryParameters(); + + // Assert + Assert.IsNotNull(dict); + Assert.IsEmpty(dict); + } + + [TestMethod] + public void ShouldNotAddDimensionsIfEmpty() + { + // Arrange + var filter = new GetDnsAnalyticsByTimeFilter { Dimensions = [] }; + + // Act + var dict = filter.GetQueryParameters(); + + // Assert + Assert.IsNotNull(dict); + Assert.IsEmpty(dict); + } + + [TestMethod] + public void ShouldNotAddMetricsIfEmpty() + { + // Arrange + var filter = new GetDnsAnalyticsByTimeFilter { Metrics = [] }; + + // Act + var dict = filter.GetQueryParameters(); + + // Assert + Assert.IsNotNull(dict); + Assert.IsEmpty(dict); + } + + [TestMethod] + public void ShouldNotAddSortIfEmpty() + { + // Arrange + var filter = new GetDnsAnalyticsByTimeFilter { Sort = [] }; + + // Act + var dict = filter.GetQueryParameters(); + + // Assert + Assert.IsNotNull(dict); + Assert.IsEmpty(dict); + } + + [TestMethod] + [DataRow(null)] + [DataRow((TimeDeltaUnit)0)] + public void ShouldNotAddTimeDelta(TimeDeltaUnit? timeDelta) + { + // Arrange + var filter = new GetDnsAnalyticsByTimeFilter { TimeDelta = timeDelta }; + + // Act + var dict = filter.GetQueryParameters(); + + // Assert + Assert.IsNotNull(dict); + Assert.IsEmpty(dict); + } + + 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/DnsFirewallExtensions/Analytics/GetDnsFirewallAnalyticsReportTest.cs b/test/Extensions/Cloudflare.Dns.Tests/DnsFirewallExtensions/Analytics/GetDnsFirewallAnalyticsReportTest.cs new file mode 100644 index 0000000..f033593 --- /dev/null +++ b/test/Extensions/Cloudflare.Dns.Tests/DnsFirewallExtensions/Analytics/GetDnsFirewallAnalyticsReportTest.cs @@ -0,0 +1,261 @@ +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.DnsFirewallExtensions.Analytics +{ + [TestClass] + public class GetDnsFirewallAnalyticsReportTest + { + public TestContext TestContext { get; set; } + + private const string AccountId = "023e105f4ecef8ad9ca31a8372d0c353"; + private const string ClusterId = "023e105f4ecef8ad9ca31a8372d0c355"; + + 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(1001, "Error 1") + ], + ResultInfo = new PaginationInfo + { + Count = 1, + Page = 1, + PerPage = 100, + TotalCount = 100, + TotalPages = 1, + }, + Result = new DnsAnalyticsReport() + }; + } + + [TestMethod] + public async Task ShouldGetDnsFirewallAnalyticsReport() + { + // Arrange + var client = GetClient(); + + // Act + var response = await client.GetDnsFirewallAnalyticsReport(AccountId, ClusterId, cancellationToken: TestContext.CancellationToken); + + // Assert + Assert.IsNotNull(response); + Assert.IsTrue(response.Success); + + Assert.IsNotNull(response.Result); + Assert.IsInstanceOfType(response.Result); + + Assert.HasCount(1, _callbacks); + + var (requestPath, queryFilter) = _callbacks.First(); + Assert.AreEqual($"/accounts/{AccountId}/dns_firewall/{ClusterId}/dns_analytics/report", requestPath); + Assert.IsNull(queryFilter); + + _clientMock.Verify(m => m.GetAsync($"/accounts/{AccountId}/dns_firewall/{ClusterId}/dns_analytics/report", null, TestContext.CancellationToken), Times.Once); + _clientMock.VerifyNoOtherCalls(); + } + + [TestMethod] + public async Task ShouldGetDnsFirewallAnalyticsReportWithFilter() + { + // Arrange + var filter = new GetDnsAnalyticsReportFilter + { + Since = DateTime.UtcNow.AddDays(-7), + Until = DateTime.UtcNow, + Dimensions = ["queryName", "responseCode"], + Filters = "queryType eq A", + Limit = 500, + Metrics = ["requests", "responses"], + Sort = ["-requests", "+responses"] + }; + + var client = GetClient(); + + // Act + var response = await client.GetDnsFirewallAnalyticsReport(AccountId, ClusterId, filter, TestContext.CancellationToken); + + // Assert + Assert.IsNotNull(response); + Assert.IsTrue(response.Success); + + Assert.IsNotNull(response.Result); + Assert.IsInstanceOfType(response.Result); + + Assert.HasCount(1, _callbacks); + + var (requestPath, queryFilter) = _callbacks.First(); + Assert.AreEqual($"/accounts/{AccountId}/dns_firewall/{ClusterId}/dns_analytics/report", requestPath); + Assert.IsNotNull(queryFilter); + + Assert.IsInstanceOfType(queryFilter); + + _clientMock.Verify(m => m.GetAsync($"/accounts/{AccountId}/dns_firewall/{ClusterId}/dns_analytics/report", filter, TestContext.CancellationToken), Times.Once); + _clientMock.VerifyNoOtherCalls(); + } + + [TestMethod] + public void ShouldReturnEmptyParameterList() + { + // Arrange + var filter = new GetDnsAnalyticsReportFilter(); + + // Act + var dict = filter.GetQueryParameters(); + + // Assert + Assert.IsNotNull(dict); + Assert.IsEmpty(dict); + } + + [TestMethod] + public void ShouldReturnFullParameterList() + { + // Arrange + var since = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc); + var until = new DateTime(2024, 1, 8, 0, 0, 0, DateTimeKind.Utc); + var filter = new GetDnsAnalyticsReportFilter + { + Dimensions = ["queryName", "responseCode"], + Filters = "queryType eq A", + Limit = 1000, + Metrics = ["requests", "responses"], + Since = since, + Sort = ["-requests", "+responses"], + Until = until + }; + + // Act + var dict = filter.GetQueryParameters(); + + // Assert + Assert.IsNotNull(dict); + Assert.HasCount(7, dict); + + Assert.IsTrue(dict.ContainsKey("dimensions")); + Assert.AreEqual("queryName,responseCode", dict["dimensions"]); + + Assert.IsTrue(dict.ContainsKey("filters")); + Assert.AreEqual("queryType eq A", dict["filters"]); + + Assert.IsTrue(dict.ContainsKey("limit")); + Assert.AreEqual("1000", dict["limit"]); + + Assert.IsTrue(dict.ContainsKey("metrics")); + Assert.AreEqual("requests,responses", dict["metrics"]); + + Assert.IsTrue(dict.ContainsKey("since")); + Assert.AreEqual(since.ToIso8601Format(), dict["since"]); + + Assert.IsTrue(dict.ContainsKey("sort")); + Assert.AreEqual("-requests,+responses", dict["sort"]); + + Assert.IsTrue(dict.ContainsKey("until")); + Assert.AreEqual(until.ToIso8601Format(), dict["until"]); + } + + [TestMethod] + [DataRow(null)] + [DataRow("")] + [DataRow(" ")] + public void ShouldNotAddFilters(string str) + { + // Arrange + var filter = new GetDnsAnalyticsReportFilter { Filters = str }; + + // Act + var dict = filter.GetQueryParameters(); + + // Assert + Assert.IsNotNull(dict); + Assert.IsEmpty(dict); + } + + [TestMethod] + [DataRow(null)] + [DataRow(0)] + public void ShouldNotAddLimit(int? limit) + { + // Arrange + var filter = new GetDnsAnalyticsReportFilter { Limit = limit }; + + // Act + var dict = filter.GetQueryParameters(); + + // Assert + Assert.IsNotNull(dict); + Assert.IsEmpty(dict); + } + + [TestMethod] + public void ShouldNotAddDimensionsIfEmpty() + { + // Arrange + var filter = new GetDnsAnalyticsReportFilter { Dimensions = [] }; + + // Act + var dict = filter.GetQueryParameters(); + + // Assert + Assert.IsNotNull(dict); + Assert.IsEmpty(dict); + } + + [TestMethod] + public void ShouldNotAddMetricsIfEmpty() + { + // Arrange + var filter = new GetDnsAnalyticsReportFilter { Metrics = [] }; + + // Act + var dict = filter.GetQueryParameters(); + + // Assert + Assert.IsNotNull(dict); + Assert.IsEmpty(dict); + } + + [TestMethod] + public void ShouldNotAddSortIfEmpty() + { + // Arrange + var filter = new GetDnsAnalyticsReportFilter { Sort = [] }; + + // Act + var dict = filter.GetQueryParameters(); + + // Assert + Assert.IsNotNull(dict); + Assert.IsEmpty(dict); + } + + 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; + } + } +}