From 6eed338ea8156c51c787e00812421d80bb98a76a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20M=C3=BCller?= Date: Tue, 28 Oct 2025 21:07:45 +0100 Subject: [PATCH] Added DNS analytics methods --- .gitlab-ci.yml | 4 +- CHANGELOG.md | 1 + .../Cloudflare.Dns/DnsAnalyticsExtensions.cs | 42 +++ .../Cloudflare.Dns/Enums/TimeDeltaUnit.cs | 73 +++++ .../Filters/GetDnsAnalyticsByTimeFilter.cs | 26 ++ .../Filters/GetDnsAnalyticsReportFilter.cs | 78 +++++ .../Models/DNSAnalyticsQuery.cs | 81 +----- .../Models/DnsAnalyticsByTime.cs | 77 +++++ .../Models/DnsAnalyticsReport.cs | 118 ++++++++ src/Extensions/Cloudflare.Dns/README.md | 9 +- .../GetDnsAnalyticsByTimeTest.cs | 274 ++++++++++++++++++ .../GetDnsAnalyticsReportTest.cs | 253 ++++++++++++++++ 12 files changed, 960 insertions(+), 76 deletions(-) create mode 100644 src/Extensions/Cloudflare.Dns/DnsAnalyticsExtensions.cs create mode 100644 src/Extensions/Cloudflare.Dns/Enums/TimeDeltaUnit.cs create mode 100644 src/Extensions/Cloudflare.Dns/Filters/GetDnsAnalyticsByTimeFilter.cs create mode 100644 src/Extensions/Cloudflare.Dns/Filters/GetDnsAnalyticsReportFilter.cs create mode 100644 src/Extensions/Cloudflare.Dns/Models/DnsAnalyticsByTime.cs create mode 100644 src/Extensions/Cloudflare.Dns/Models/DnsAnalyticsReport.cs create mode 100644 test/Extensions/Cloudflare.Dns.Tests/DnsAnalyticsExtensions/GetDnsAnalyticsByTimeTest.cs create mode 100644 test/Extensions/Cloudflare.Dns.Tests/DnsAnalyticsExtensions/GetDnsAnalyticsReportTest.cs diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index de98e91..6e47589 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -46,7 +46,7 @@ default-test: - dotnet tool install dotnet-reportgenerator-globaltool --tool-path /dotnet-tools script: - dotnet test -c Debug --nologo /p:CoverletOutputFormat=Cobertura - - /dotnet-tools/reportgenerator "-reports:${CI_PROJECT_DIR}/**/coverage.cobertura.xml" "-targetdir:/reports" -reportType:TextSummary + - /dotnet-tools/reportgenerator "-reports:${CI_PROJECT_DIR}/**/coverage.cobertura.xml" "-targetdir:/reports" "-reportType:TextSummary" - cat /reports/Summary.txt artifacts: when: always @@ -105,7 +105,7 @@ core-test: - dotnet tool install dotnet-reportgenerator-globaltool --tool-path /dotnet-tools script: - dotnet test -c Release --nologo /p:CoverletOutputFormat=Cobertura test/Cloudflare.Tests/Cloudflare.Tests.csproj - - /dotnet-tools/reportgenerator "-reports:${CI_PROJECT_DIR}/**/coverage.cobertura.xml" "-targetdir:/reports" -reportType:TextSummary + - /dotnet-tools/reportgenerator "-reports:${CI_PROJECT_DIR}/**/coverage.cobertura.xml" "-targetdir:/reports" "-reportType:TextSummary" - cat /reports/Summary.txt artifacts: when: always diff --git a/CHANGELOG.md b/CHANGELOG.md index da5d196..0653552 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - New automatic documentation generation using docfx. - Additional articles for the documentation. - `DateTime` extensions for ISO 8601 formatting. +- DNS Analytics ## [v0.1.0], [zones/v0.1.0], [dns/v0.1.0] - 2025-08-05 diff --git a/src/Extensions/Cloudflare.Dns/DnsAnalyticsExtensions.cs b/src/Extensions/Cloudflare.Dns/DnsAnalyticsExtensions.cs new file mode 100644 index 0000000..3a47db0 --- /dev/null +++ b/src/Extensions/Cloudflare.Dns/DnsAnalyticsExtensions.cs @@ -0,0 +1,42 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace AMWD.Net.Api.Cloudflare.Dns +{ + /// + /// Extensions for DNS Analytics. + /// + public static class DnsAnalyticsExtensions + { + /// + /// Retrieves a list of summarised aggregate metrics over a given time period. + /// + /// + /// See Analytics API properties for detailed information about the available query parameters. + /// + /// The instance. + /// The zone identifier. + /// Filter options (optional). + /// A cancellation token used to propagate notification that this operation should be canceled. + public static Task> GetDnsAnalyticsReport(this ICloudflareClient client, string zoneId, GetDnsAnalyticsReportFilter? options = null, CancellationToken cancellationToken = default) + { + zoneId.ValidateCloudflareId(); + + return client.GetAsync($"/zones/{zoneId}/dns_analytics/report", options, cancellationToken); + } + + /// + /// Retrieves a list of aggregate metrics grouped by time interval. + /// + /// The instance. + /// The zone identifier. + /// Filter options (optional). + /// A cancellation token used to propagate notification that this operation should be canceled. + public static Task> GetDnsAnalyticsByTime(this ICloudflareClient client, string zoneId, GetDnsAnalyticsByTimeFilter? options = null, CancellationToken cancellationToken = default) + { + zoneId.ValidateCloudflareId(); + + return client.GetAsync($"/zones/{zoneId}/dns_analytics/report/bytime", options, cancellationToken); + } + } +} diff --git a/src/Extensions/Cloudflare.Dns/Enums/TimeDeltaUnit.cs b/src/Extensions/Cloudflare.Dns/Enums/TimeDeltaUnit.cs new file mode 100644 index 0000000..c4d8d67 --- /dev/null +++ b/src/Extensions/Cloudflare.Dns/Enums/TimeDeltaUnit.cs @@ -0,0 +1,73 @@ +using System.Runtime.Serialization; +using Newtonsoft.Json.Converters; + +namespace AMWD.Net.Api.Cloudflare.Dns +{ + /// + /// Time delta units. + /// Source + /// + [JsonConverter(typeof(StringEnumConverter))] + public enum TimeDeltaUnit + { + /// + /// All time. + /// + [EnumMember(Value = "all")] + All = 1, + + /// + /// Auto. + /// + [EnumMember(Value = "auto")] + Auto = 2, + + /// + /// Year. + /// + [EnumMember(Value = "year")] + Year = 3, + + /// + /// Quarter (3 months). + /// + [EnumMember(Value = "quarter")] + Quarter = 4, + + /// + /// Month. + /// + [EnumMember(Value = "month")] + Month = 5, + + /// + /// Week. + /// + [EnumMember(Value = "week")] + Week = 6, + + /// + /// Day. + /// + [EnumMember(Value = "day")] + Day = 7, + + /// + /// Hour. + /// + [EnumMember(Value = "hour")] + Hour = 8, + + /// + /// Dekaminute (10 minutes). + /// + [EnumMember(Value = "dekaminute")] + DekaMinute = 9, + + /// + /// Minute. + /// + [EnumMember(Value = "minute")] + Minute = 10 + } +} diff --git a/src/Extensions/Cloudflare.Dns/Filters/GetDnsAnalyticsByTimeFilter.cs b/src/Extensions/Cloudflare.Dns/Filters/GetDnsAnalyticsByTimeFilter.cs new file mode 100644 index 0000000..18515a1 --- /dev/null +++ b/src/Extensions/Cloudflare.Dns/Filters/GetDnsAnalyticsByTimeFilter.cs @@ -0,0 +1,26 @@ +using System.Linq; + +namespace AMWD.Net.Api.Cloudflare.Dns +{ + /// + /// Filter for DNS analytics report by time. + /// + public class GetDnsAnalyticsByTimeFilter : GetDnsAnalyticsReportFilter + { + /// + /// Unit of time to group data by + /// + public TimeDeltaUnit? TimeDelta { get; set; } + + /// + public override IReadOnlyDictionary GetQueryParameters() + { + var dict = base.GetQueryParameters().ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + + if (TimeDelta.HasValue && Enum.IsDefined(typeof(TimeDeltaUnit), TimeDelta)) + dict.Add("time_delta", TimeDelta.Value.GetEnumMemberValue()!); + + return dict; + } + } +} diff --git a/src/Extensions/Cloudflare.Dns/Filters/GetDnsAnalyticsReportFilter.cs b/src/Extensions/Cloudflare.Dns/Filters/GetDnsAnalyticsReportFilter.cs new file mode 100644 index 0000000..d54c4ec --- /dev/null +++ b/src/Extensions/Cloudflare.Dns/Filters/GetDnsAnalyticsReportFilter.cs @@ -0,0 +1,78 @@ +namespace AMWD.Net.Api.Cloudflare.Dns +{ + /// + /// Filter for DNS analytics report. + /// + public class GetDnsAnalyticsReportFilter : IQueryParameterFilter + { + /// + /// A (comma-separated) list of dimensions to group results by. + /// + /// + /// Further see: Cloudflare Docs. + /// + public IReadOnlyCollection? Dimensions { get; set; } + + /// + /// Segmentation filter in 'attribute operator value' format. + /// + /// + /// Further see: Cloudflare Docs. + /// + public string? Filters { get; set; } + + /// + /// Limit number of returned metrics. (Default: 100.000) + /// + public int? Limit { get; set; } + + /// + /// A (comma-separated) list of metrics to query. + /// + public IReadOnlyCollection? Metrics { get; set; } + + /// + /// Start date and time of requesting data period in ISO 8601 format. + /// + public DateTime? Since { get; set; } + + /// + /// A (comma-separated) list of dimensions to sort by, where each dimension may be prefixed by - (descending) or + (ascending) + /// + public IReadOnlyCollection? Sort { get; set; } + + /// + /// End date and time of requesting data period in ISO 8601 format. + /// + public DateTime? Until { get; set; } + + /// + public virtual IReadOnlyDictionary GetQueryParameters() + { + var dict = new Dictionary(); + + if (Dimensions?.Count > 0) + dict.Add("dimensions", string.Join(",", Dimensions)); + + if (!string.IsNullOrWhiteSpace(Filters)) + dict.Add("filters", Filters!.Trim()); + + if (Limit.HasValue && Limit > 0) + dict.Add("limit", Limit.Value.ToString()); + + if (Metrics?.Count > 0) + dict.Add("metrics", string.Join(",", Metrics)); + + if (Since.HasValue) + dict.Add("since", Since.Value.ToIso8601Format()); + + if (Sort?.Count > 0) + dict.Add("sort", string.Join(",", Sort)); + + if (Until.HasValue) + dict.Add("until", Until.Value.ToIso8601Format()); + + return dict; + } + } +} diff --git a/src/Extensions/Cloudflare.Dns/Models/DNSAnalyticsQuery.cs b/src/Extensions/Cloudflare.Dns/Models/DNSAnalyticsQuery.cs index d17d5bb..0b39f00 100644 --- a/src/Extensions/Cloudflare.Dns/Models/DNSAnalyticsQuery.cs +++ b/src/Extensions/Cloudflare.Dns/Models/DNSAnalyticsQuery.cs @@ -1,9 +1,9 @@ -using System.Runtime.Serialization; -using Newtonsoft.Json.Converters; - -namespace AMWD.Net.Api.Cloudflare.Dns +namespace AMWD.Net.Api.Cloudflare.Dns { - internal class DNSAnalyticsQuery + /// + /// The DNS Analytics query. + /// + public class DnsAnalyticsQuery { /// /// Array of dimension names. @@ -35,6 +35,9 @@ namespace AMWD.Net.Api.Cloudflare.Dns [JsonProperty("time_delta")] public TimeDeltaUnit? TimeDelta { get; set; } + /// + /// End date and time of requesting data period. + /// [JsonProperty("until")] public DateTime? Until { get; set; } @@ -51,72 +54,4 @@ namespace AMWD.Net.Api.Cloudflare.Dns [JsonProperty("sort")] public IReadOnlyCollection? Sort { get; set; } } - - /// - /// Time delta units. - /// Source - /// - [JsonConverter(typeof(StringEnumConverter))] - public enum TimeDeltaUnit - { - /// - /// All time. - /// - [EnumMember(Value = "all")] - All = 1, - - /// - /// Auto. - /// - [EnumMember(Value = "auto")] - Auto = 2, - - /// - /// Year. - /// - [EnumMember(Value = "year")] - Year = 3, - - /// - /// Quarter. - /// - [EnumMember(Value = "quarter")] - Quarter = 4, - - /// - /// Month. - /// - [EnumMember(Value = "month")] - Month = 5, - - /// - /// Week. - /// - [EnumMember(Value = "week")] - Week = 6, - - /// - /// Day. - /// - [EnumMember(Value = "day")] - Day = 7, - - /// - /// Hour. - /// - [EnumMember(Value = "hour")] - Hour = 8, - - /// - /// Dekaminute. - /// - [EnumMember(Value = "dekaminute")] - DekaMinute = 9, - - /// - /// Minute. - /// - [EnumMember(Value = "minute")] - Minute = 10 - } } diff --git a/src/Extensions/Cloudflare.Dns/Models/DnsAnalyticsByTime.cs b/src/Extensions/Cloudflare.Dns/Models/DnsAnalyticsByTime.cs new file mode 100644 index 0000000..d68aa6a --- /dev/null +++ b/src/Extensions/Cloudflare.Dns/Models/DnsAnalyticsByTime.cs @@ -0,0 +1,77 @@ +namespace AMWD.Net.Api.Cloudflare.Dns +{ + /// + /// Summarised aggregate metrics over a given time period as report. + /// + public class DnsAnalyticsByTime + { + /// + /// Array with one row per combination of dimension values. + /// + [JsonProperty("data")] + public IReadOnlyCollection? Data { get; set; } + + /// + /// Number of seconds between current time and last processed event, in another words how many seconds of data could be missing. + /// + [JsonProperty("data_lag")] + public int? DataLag { get; set; } + + /// + /// Maximum results for each metric (object mapping metric names to values). + /// Currently always an empty object. + /// + [JsonProperty("max")] + public object? Max { get; set; } + + /// + /// Minimum results for each metric (object mapping metric names to values). + /// Currently always an empty object. + /// + [JsonProperty("min")] + public object? Min { get; set; } + + /// + /// The query information. + /// + [JsonProperty("query")] + public DnsAnalyticsQuery? Query { get; set; } + + /// + /// Total number of rows in the result. + /// + [JsonProperty("rows")] + public int? Rows { get; set; } + + /// + /// Array of time intervals in the response data. Each interval is represented as an + /// array containing two values: the start time, and the end time. + /// + [JsonProperty("time_intervals")] + public IReadOnlyCollection>? TimeIntervals { get; set; } + + /// + /// Total results for metrics across all data (object mapping metric names to values). + /// + [JsonProperty("totals")] + public object? Totals { get; set; } + } + + /// + /// A combination of dimension values. + /// + public class DnsAnalyticsByTimeData + { + /// + /// Array of dimension values, representing the combination of dimension values corresponding to this row. + /// + [JsonProperty("dimensions")] + public IReadOnlyCollection? Dimensions { get; set; } + + /// + /// Array with one item per requested metric. Each item is an array of values, broken down by time interval. + /// + [JsonProperty("metrics")] + public IReadOnlyCollection>? Metrics { get; set; } + } +} diff --git a/src/Extensions/Cloudflare.Dns/Models/DnsAnalyticsReport.cs b/src/Extensions/Cloudflare.Dns/Models/DnsAnalyticsReport.cs new file mode 100644 index 0000000..6f97953 --- /dev/null +++ b/src/Extensions/Cloudflare.Dns/Models/DnsAnalyticsReport.cs @@ -0,0 +1,118 @@ +namespace AMWD.Net.Api.Cloudflare.Dns +{ + /// + /// Summarised aggregate metrics over a given time period as report. + /// + public class DnsAnalyticsReport + { + /// + /// Array with one row per combination of dimension values. + /// + [JsonProperty("data")] + public IReadOnlyCollection? Data { get; set; } + + /// + /// Number of seconds between current time and last processed event, in another words how many seconds of data could be missing. + /// + [JsonProperty("data_lag")] + public int? DataLag { get; set; } + + /// + /// Maximum results for each metric (object mapping metric names to values). + /// Currently always an empty object. + /// + [JsonProperty("max")] + public object? Max { get; set; } + + /// + /// Minimum results for each metric (object mapping metric names to values). + /// Currently always an empty object. + /// + [JsonProperty("min")] + public object? Min { get; set; } + + /// + /// The query information. + /// + [JsonProperty("query")] + public DnsAnalyticsReportQuery? Query { get; set; } + + /// + /// Total number of rows in the result. + /// + [JsonProperty("rows")] + public int? Rows { get; set; } + + /// + /// Total results for metrics across all data (object mapping metric names to values). + /// + [JsonProperty("totals")] + public object? Totals { get; set; } + } + + /// + /// A combination of dimension values. + /// + public class DnsAnalyticsReportData + { + /// + /// Array of dimension values, representing the combination of dimension values corresponding to this row. + /// + [JsonProperty("dimensions")] + public IReadOnlyCollection? Dimensions { get; set; } + + /// + /// Array with one item per requested metric. Each item is a single value. + /// + [JsonProperty("metrics")] + public IReadOnlyCollection? Metrics { get; set; } + } + + /// + /// The query information. + /// + public class DnsAnalyticsReportQuery + { + /// + /// Array of dimension names. + /// + [JsonProperty("dimensions")] + public IReadOnlyCollection? Dimensions { get; set; } + + /// + /// Limit number of returned metrics. + /// + [JsonProperty("limit")] + public int? Limit { get; set; } + + /// + /// Array of metric names. + /// + [JsonProperty("metrics")] + public IReadOnlyCollection? Metrics { get; set; } + + /// + /// Start date and time of requesting data period in ISO 8601 format. + /// + [JsonProperty("since")] + public DateTime? Since { get; set; } + + /// + /// End date and time of requesting data period in ISO 8601 format. + /// + [JsonProperty("until")] + public DateTime? Until { get; set; } + + /// + /// Segmentation filter in 'attribute operator value' format. + /// + [JsonProperty("filters")] + public string? Filters { get; set; } + + /// + /// Array of dimension names to sort by, where each dimension may be prefixed by - (descending) or + (ascending). + /// + [JsonProperty("sort")] + public IReadOnlyCollection? Sort { get; set; } + } +} diff --git a/src/Extensions/Cloudflare.Dns/README.md b/src/Extensions/Cloudflare.Dns/README.md index 8502d0a..54ab320 100644 --- a/src/Extensions/Cloudflare.Dns/README.md +++ b/src/Extensions/Cloudflare.Dns/README.md @@ -13,7 +13,13 @@ This package contains the feature set of the _DNS_ section of the Cloudflare API ### [DNS] -### [DNSSEC] +#### [Analytics] + +- [Get Report Table](https://developers.cloudflare.com/api/resources/dns/subresources/analytics/subresources/reports/methods/get/) +- [Get Report By Time](https://developers.cloudflare.com/api/resources/dns/subresources/analytics/subresources/reports/subresources/bytimes/methods/get/) + + +#### [DNSSEC] - [Delete DNSSEC Records](https://developers.cloudflare.com/api/resources/dns/subresources/dnssec/methods/delete/) - [Edit DNSSEC Status](https://developers.cloudflare.com/api/resources/dns/subresources/dnssec/methods/edit/) @@ -65,6 +71,7 @@ Published under MIT License (see [choose a license]) [Account Custom Nameservers]: https://developers.cloudflare.com/api/resources/custom_nameservers/ [DNS]: https://developers.cloudflare.com/api/resources/dns/ + [Analytics]: https://developers.cloudflare.com/api/resources/dns/subresources/analytics/ [DNSSEC]: https://developers.cloudflare.com/api/resources/dns/subresources/dnssec/ [Records]: https://developers.cloudflare.com/api/resources/dns/subresources/records/ [Settings]: https://developers.cloudflare.com/api/resources/dns/subresources/settings/ diff --git a/test/Extensions/Cloudflare.Dns.Tests/DnsAnalyticsExtensions/GetDnsAnalyticsByTimeTest.cs b/test/Extensions/Cloudflare.Dns.Tests/DnsAnalyticsExtensions/GetDnsAnalyticsByTimeTest.cs new file mode 100644 index 0000000..6777216 --- /dev/null +++ b/test/Extensions/Cloudflare.Dns.Tests/DnsAnalyticsExtensions/GetDnsAnalyticsByTimeTest.cs @@ -0,0 +1,274 @@ +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.DnsAnalyticsExtensions +{ + [TestClass] + public class GetDnsAnalyticsByTimeTest + { + public TestContext TestContext { get; set; } + + 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(1001, "Error 1") + ], + ResultInfo = new PaginationInfo + { + Count = 1, + Page = 1, + PerPage = 100, + TotalCount = 100, + TotalPages = 1, + }, + Result = new DnsAnalyticsByTime() + }; + } + + [TestMethod] + public async Task ShouldGetDnsAnalyticsByTime() + { + // Arrange + var client = GetClient(); + + // Act + var response = await client.GetDnsAnalyticsByTime(ZoneId, 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($"/zones/{ZoneId}/dns_analytics/report/bytime", requestPath); + Assert.IsNull(queryFilter); + + _clientMock.Verify(m => m.GetAsync($"/zones/{ZoneId}/dns_analytics/report/bytime", null, TestContext.CancellationToken), Times.Once); + _clientMock.VerifyNoOtherCalls(); + } + + [TestMethod] + public async Task ShouldGetDnsAnalyticsByTimeWithFilter() + { + // Arrange + var filter = new GetDnsAnalyticsByTimeFilter + { + Since = DateTime.UtcNow.AddDays(-7), + Until = DateTime.UtcNow, + TimeDelta = TimeDeltaUnit.Day + }; + + var client = GetClient(); + + // Act + var response = await client.GetDnsAnalyticsByTime(ZoneId, 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($"/zones/{ZoneId}/dns_analytics/report/bytime", requestPath); + Assert.IsNotNull(queryFilter); + + Assert.IsInstanceOfType(queryFilter); + + _clientMock.Verify(m => m.GetAsync($"/zones/{ZoneId}/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/DnsAnalyticsExtensions/GetDnsAnalyticsReportTest.cs b/test/Extensions/Cloudflare.Dns.Tests/DnsAnalyticsExtensions/GetDnsAnalyticsReportTest.cs new file mode 100644 index 0000000..a1fef3e --- /dev/null +++ b/test/Extensions/Cloudflare.Dns.Tests/DnsAnalyticsExtensions/GetDnsAnalyticsReportTest.cs @@ -0,0 +1,253 @@ +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.DnsAnalyticsExtensions +{ + [TestClass] + public class GetDnsAnalyticsReportTest + { + public TestContext TestContext { get; set; } + + 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(1001, "Error 1") + ], + ResultInfo = new PaginationInfo + { + Count = 1, + Page = 1, + PerPage = 100, + TotalCount = 100, + TotalPages = 1, + }, + Result = new DnsAnalyticsReport() + }; + } + + [TestMethod] + public async Task ShouldGetDnsAnalyticsReport() + { + // Arrange + var client = GetClient(); + + // Act + var response = await client.GetDnsAnalyticsReport(ZoneId, 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($"/zones/{ZoneId}/dns_analytics/report", requestPath); + Assert.IsNull(queryFilter); + + _clientMock.Verify(m => m.GetAsync($"/zones/{ZoneId}/dns_analytics/report", null, TestContext.CancellationToken), Times.Once); + _clientMock.VerifyNoOtherCalls(); + } + + [TestMethod] + public async Task ShouldGetDnsAnalyticsReportWithFilter() + { + // Arrange + var filter = new GetDnsAnalyticsReportFilter + { + Since = DateTime.UtcNow.AddDays(-7), + Until = DateTime.UtcNow, + }; + + var client = GetClient(); + + // Act + var response = await client.GetDnsAnalyticsReport(ZoneId, 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($"/zones/{ZoneId}/dns_analytics/report", requestPath); + Assert.IsNotNull(queryFilter); + + Assert.IsInstanceOfType(queryFilter); + + _clientMock.Verify(m => m.GetAsync($"/zones/{ZoneId}/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; + } + } +}