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;
+ }
+ }
+}