Added DNS analytics methods

This commit is contained in:
2025-10-28 21:07:45 +01:00
parent 69816d0c02
commit 6eed338ea8
12 changed files with 960 additions and 76 deletions

View File

@@ -46,7 +46,7 @@ default-test:
- dotnet tool install dotnet-reportgenerator-globaltool --tool-path /dotnet-tools - dotnet tool install dotnet-reportgenerator-globaltool --tool-path /dotnet-tools
script: script:
- dotnet test -c Debug --nologo /p:CoverletOutputFormat=Cobertura - 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 - cat /reports/Summary.txt
artifacts: artifacts:
when: always when: always
@@ -105,7 +105,7 @@ core-test:
- dotnet tool install dotnet-reportgenerator-globaltool --tool-path /dotnet-tools - dotnet tool install dotnet-reportgenerator-globaltool --tool-path /dotnet-tools
script: script:
- dotnet test -c Release --nologo /p:CoverletOutputFormat=Cobertura test/Cloudflare.Tests/Cloudflare.Tests.csproj - 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 - cat /reports/Summary.txt
artifacts: artifacts:
when: always when: always

View File

@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- New automatic documentation generation using docfx. - New automatic documentation generation using docfx.
- Additional articles for the documentation. - Additional articles for the documentation.
- `DateTime` extensions for ISO 8601 formatting. - `DateTime` extensions for ISO 8601 formatting.
- DNS Analytics
## [v0.1.0], [zones/v0.1.0], [dns/v0.1.0] - 2025-08-05 ## [v0.1.0], [zones/v0.1.0], [dns/v0.1.0] - 2025-08-05

View File

@@ -0,0 +1,42 @@
using System.Threading;
using System.Threading.Tasks;
namespace AMWD.Net.Api.Cloudflare.Dns
{
/// <summary>
/// Extensions for <see href="https://developers.cloudflare.com/api/resources/dns/subresources/analytics/">DNS Analytics</see>.
/// </summary>
public static class DnsAnalyticsExtensions
{
/// <summary>
/// Retrieves a list of summarised aggregate metrics over a given time period.
/// </summary>
/// <remarks>
/// See <see href="https://developers.cloudflare.com/dns/reference/analytics-api-properties/">Analytics API properties</see> for detailed information about the available query parameters.
/// </remarks>
/// <param name="client">The <see cref="ICloudflareClient"/> instance.</param>
/// <param name="zoneId">The zone identifier.</param>
/// <param name="options">Filter options (optional).</param>
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
public static Task<CloudflareResponse<DnsAnalyticsReport>> GetDnsAnalyticsReport(this ICloudflareClient client, string zoneId, GetDnsAnalyticsReportFilter? options = null, CancellationToken cancellationToken = default)
{
zoneId.ValidateCloudflareId();
return client.GetAsync<DnsAnalyticsReport>($"/zones/{zoneId}/dns_analytics/report", options, cancellationToken);
}
/// <summary>
/// Retrieves a list of aggregate metrics grouped by time interval.
/// </summary>
/// <param name="client">The <see cref="ICloudflareClient"/> instance.</param>
/// <param name="zoneId">The zone identifier.</param>
/// <param name="options">Filter options (optional).</param>
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
public static Task<CloudflareResponse<DnsAnalyticsByTime>> GetDnsAnalyticsByTime(this ICloudflareClient client, string zoneId, GetDnsAnalyticsByTimeFilter? options = null, CancellationToken cancellationToken = default)
{
zoneId.ValidateCloudflareId();
return client.GetAsync<DnsAnalyticsByTime>($"/zones/{zoneId}/dns_analytics/report/bytime", options, cancellationToken);
}
}
}

View File

@@ -0,0 +1,73 @@
using System.Runtime.Serialization;
using Newtonsoft.Json.Converters;
namespace AMWD.Net.Api.Cloudflare.Dns
{
/// <summary>
/// Time delta units.
/// <see href="https://github.com/cloudflare/cloudflare-typescript/blob/v4.4.1/src/resources/dns/dns.ts#L103">Source</see>
/// </summary>
[JsonConverter(typeof(StringEnumConverter))]
public enum TimeDeltaUnit
{
/// <summary>
/// All time.
/// </summary>
[EnumMember(Value = "all")]
All = 1,
/// <summary>
/// Auto.
/// </summary>
[EnumMember(Value = "auto")]
Auto = 2,
/// <summary>
/// Year.
/// </summary>
[EnumMember(Value = "year")]
Year = 3,
/// <summary>
/// Quarter (3 months).
/// </summary>
[EnumMember(Value = "quarter")]
Quarter = 4,
/// <summary>
/// Month.
/// </summary>
[EnumMember(Value = "month")]
Month = 5,
/// <summary>
/// Week.
/// </summary>
[EnumMember(Value = "week")]
Week = 6,
/// <summary>
/// Day.
/// </summary>
[EnumMember(Value = "day")]
Day = 7,
/// <summary>
/// Hour.
/// </summary>
[EnumMember(Value = "hour")]
Hour = 8,
/// <summary>
/// Dekaminute (10 minutes).
/// </summary>
[EnumMember(Value = "dekaminute")]
DekaMinute = 9,
/// <summary>
/// Minute.
/// </summary>
[EnumMember(Value = "minute")]
Minute = 10
}
}

View File

@@ -0,0 +1,26 @@
using System.Linq;
namespace AMWD.Net.Api.Cloudflare.Dns
{
/// <summary>
/// Filter for DNS analytics report by time.
/// </summary>
public class GetDnsAnalyticsByTimeFilter : GetDnsAnalyticsReportFilter
{
/// <summary>
/// Unit of time to group data by
/// </summary>
public TimeDeltaUnit? TimeDelta { get; set; }
/// <inheritdoc/>
public override IReadOnlyDictionary<string, string> 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;
}
}
}

View File

@@ -0,0 +1,78 @@
namespace AMWD.Net.Api.Cloudflare.Dns
{
/// <summary>
/// Filter for DNS analytics report.
/// </summary>
public class GetDnsAnalyticsReportFilter : IQueryParameterFilter
{
/// <summary>
/// A (comma-separated) list of dimensions to group results by.
/// </summary>
/// <remarks>
/// Further see: <see href="https://developers.cloudflare.com/dns/reference/analytics-api-properties/#dimensions">Cloudflare Docs</see>.
/// </remarks>
public IReadOnlyCollection<string>? Dimensions { get; set; }
/// <summary>
/// Segmentation filter in 'attribute operator value' format.
/// </summary>
/// <remarks>
/// Further see: <see href="https://developers.cloudflare.com/dns/reference/analytics-api-properties/#filters">Cloudflare Docs</see>.
/// </remarks>
public string? Filters { get; set; }
/// <summary>
/// Limit number of returned metrics. (Default: 100.000)
/// </summary>
public int? Limit { get; set; }
/// <summary>
/// A (comma-separated) list of metrics to query.
/// </summary>
public IReadOnlyCollection<string>? Metrics { get; set; }
/// <summary>
/// Start date and time of requesting data period in ISO 8601 format.
/// </summary>
public DateTime? Since { get; set; }
/// <summary>
/// A (comma-separated) list of dimensions to sort by, where each dimension may be prefixed by - (descending) or + (ascending)
/// </summary>
public IReadOnlyCollection<string>? Sort { get; set; }
/// <summary>
/// End date and time of requesting data period in ISO 8601 format.
/// </summary>
public DateTime? Until { get; set; }
/// <inheritdoc/>
public virtual IReadOnlyDictionary<string, string> GetQueryParameters()
{
var dict = new Dictionary<string, string>();
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;
}
}
}

View File

@@ -1,9 +1,9 @@
using System.Runtime.Serialization; namespace AMWD.Net.Api.Cloudflare.Dns
using Newtonsoft.Json.Converters;
namespace AMWD.Net.Api.Cloudflare.Dns
{ {
internal class DNSAnalyticsQuery /// <summary>
/// The DNS Analytics query.
/// </summary>
public class DnsAnalyticsQuery
{ {
/// <summary> /// <summary>
/// Array of dimension names. /// Array of dimension names.
@@ -35,6 +35,9 @@ namespace AMWD.Net.Api.Cloudflare.Dns
[JsonProperty("time_delta")] [JsonProperty("time_delta")]
public TimeDeltaUnit? TimeDelta { get; set; } public TimeDeltaUnit? TimeDelta { get; set; }
/// <summary>
/// End date and time of requesting data period.
/// </summary>
[JsonProperty("until")] [JsonProperty("until")]
public DateTime? Until { get; set; } public DateTime? Until { get; set; }
@@ -51,72 +54,4 @@ namespace AMWD.Net.Api.Cloudflare.Dns
[JsonProperty("sort")] [JsonProperty("sort")]
public IReadOnlyCollection<string>? Sort { get; set; } public IReadOnlyCollection<string>? Sort { get; set; }
} }
/// <summary>
/// Time delta units.
/// <see href="https://github.com/cloudflare/cloudflare-typescript/blob/v4.4.1/src/resources/dns/dns.ts#L103">Source</see>
/// </summary>
[JsonConverter(typeof(StringEnumConverter))]
public enum TimeDeltaUnit
{
/// <summary>
/// All time.
/// </summary>
[EnumMember(Value = "all")]
All = 1,
/// <summary>
/// Auto.
/// </summary>
[EnumMember(Value = "auto")]
Auto = 2,
/// <summary>
/// Year.
/// </summary>
[EnumMember(Value = "year")]
Year = 3,
/// <summary>
/// Quarter.
/// </summary>
[EnumMember(Value = "quarter")]
Quarter = 4,
/// <summary>
/// Month.
/// </summary>
[EnumMember(Value = "month")]
Month = 5,
/// <summary>
/// Week.
/// </summary>
[EnumMember(Value = "week")]
Week = 6,
/// <summary>
/// Day.
/// </summary>
[EnumMember(Value = "day")]
Day = 7,
/// <summary>
/// Hour.
/// </summary>
[EnumMember(Value = "hour")]
Hour = 8,
/// <summary>
/// Dekaminute.
/// </summary>
[EnumMember(Value = "dekaminute")]
DekaMinute = 9,
/// <summary>
/// Minute.
/// </summary>
[EnumMember(Value = "minute")]
Minute = 10
}
} }

View File

@@ -0,0 +1,77 @@
namespace AMWD.Net.Api.Cloudflare.Dns
{
/// <summary>
/// Summarised aggregate metrics over a given time period as report.
/// </summary>
public class DnsAnalyticsByTime
{
/// <summary>
/// Array with one row per combination of dimension values.
/// </summary>
[JsonProperty("data")]
public IReadOnlyCollection<DnsAnalyticsByTimeData>? Data { get; set; }
/// <summary>
/// Number of seconds between current time and last processed event, in another words how many seconds of data could be missing.
/// </summary>
[JsonProperty("data_lag")]
public int? DataLag { get; set; }
/// <summary>
/// Maximum results for each metric (object mapping metric names to values).
/// Currently always an empty object.
/// </summary>
[JsonProperty("max")]
public object? Max { get; set; }
/// <summary>
/// Minimum results for each metric (object mapping metric names to values).
/// Currently always an empty object.
/// </summary>
[JsonProperty("min")]
public object? Min { get; set; }
/// <summary>
/// The query information.
/// </summary>
[JsonProperty("query")]
public DnsAnalyticsQuery? Query { get; set; }
/// <summary>
/// Total number of rows in the result.
/// </summary>
[JsonProperty("rows")]
public int? Rows { get; set; }
/// <summary>
/// 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.
/// </summary>
[JsonProperty("time_intervals")]
public IReadOnlyCollection<IReadOnlyCollection<string>>? TimeIntervals { get; set; }
/// <summary>
/// Total results for metrics across all data (object mapping metric names to values).
/// </summary>
[JsonProperty("totals")]
public object? Totals { get; set; }
}
/// <summary>
/// A combination of dimension values.
/// </summary>
public class DnsAnalyticsByTimeData
{
/// <summary>
/// Array of dimension values, representing the combination of dimension values corresponding to this row.
/// </summary>
[JsonProperty("dimensions")]
public IReadOnlyCollection<string>? Dimensions { get; set; }
/// <summary>
/// Array with one item per requested metric. Each item is an array of values, broken down by time interval.
/// </summary>
[JsonProperty("metrics")]
public IReadOnlyCollection<IReadOnlyCollection<object>>? Metrics { get; set; }
}
}

View File

@@ -0,0 +1,118 @@
namespace AMWD.Net.Api.Cloudflare.Dns
{
/// <summary>
/// Summarised aggregate metrics over a given time period as report.
/// </summary>
public class DnsAnalyticsReport
{
/// <summary>
/// Array with one row per combination of dimension values.
/// </summary>
[JsonProperty("data")]
public IReadOnlyCollection<DnsAnalyticsReportData>? Data { get; set; }
/// <summary>
/// Number of seconds between current time and last processed event, in another words how many seconds of data could be missing.
/// </summary>
[JsonProperty("data_lag")]
public int? DataLag { get; set; }
/// <summary>
/// Maximum results for each metric (object mapping metric names to values).
/// Currently always an empty object.
/// </summary>
[JsonProperty("max")]
public object? Max { get; set; }
/// <summary>
/// Minimum results for each metric (object mapping metric names to values).
/// Currently always an empty object.
/// </summary>
[JsonProperty("min")]
public object? Min { get; set; }
/// <summary>
/// The query information.
/// </summary>
[JsonProperty("query")]
public DnsAnalyticsReportQuery? Query { get; set; }
/// <summary>
/// Total number of rows in the result.
/// </summary>
[JsonProperty("rows")]
public int? Rows { get; set; }
/// <summary>
/// Total results for metrics across all data (object mapping metric names to values).
/// </summary>
[JsonProperty("totals")]
public object? Totals { get; set; }
}
/// <summary>
/// A combination of dimension values.
/// </summary>
public class DnsAnalyticsReportData
{
/// <summary>
/// Array of dimension values, representing the combination of dimension values corresponding to this row.
/// </summary>
[JsonProperty("dimensions")]
public IReadOnlyCollection<string>? Dimensions { get; set; }
/// <summary>
/// Array with one item per requested metric. Each item is a single value.
/// </summary>
[JsonProperty("metrics")]
public IReadOnlyCollection<int>? Metrics { get; set; }
}
/// <summary>
/// The query information.
/// </summary>
public class DnsAnalyticsReportQuery
{
/// <summary>
/// Array of dimension names.
/// </summary>
[JsonProperty("dimensions")]
public IReadOnlyCollection<string>? Dimensions { get; set; }
/// <summary>
/// Limit number of returned metrics.
/// </summary>
[JsonProperty("limit")]
public int? Limit { get; set; }
/// <summary>
/// Array of metric names.
/// </summary>
[JsonProperty("metrics")]
public IReadOnlyCollection<string>? Metrics { get; set; }
/// <summary>
/// Start date and time of requesting data period in ISO 8601 format.
/// </summary>
[JsonProperty("since")]
public DateTime? Since { get; set; }
/// <summary>
/// End date and time of requesting data period in ISO 8601 format.
/// </summary>
[JsonProperty("until")]
public DateTime? Until { get; set; }
/// <summary>
/// Segmentation filter in 'attribute operator value' format.
/// </summary>
[JsonProperty("filters")]
public string? Filters { get; set; }
/// <summary>
/// Array of dimension names to sort by, where each dimension may be prefixed by - (descending) or + (ascending).
/// </summary>
[JsonProperty("sort")]
public IReadOnlyCollection<string>? Sort { get; set; }
}
}

View File

@@ -13,7 +13,13 @@ This package contains the feature set of the _DNS_ section of the Cloudflare API
### [DNS] ### [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/) - [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/) - [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/ [Account Custom Nameservers]: https://developers.cloudflare.com/api/resources/custom_nameservers/
[DNS]: https://developers.cloudflare.com/api/resources/dns/ [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/ [DNSSEC]: https://developers.cloudflare.com/api/resources/dns/subresources/dnssec/
[Records]: https://developers.cloudflare.com/api/resources/dns/subresources/records/ [Records]: https://developers.cloudflare.com/api/resources/dns/subresources/records/
[Settings]: https://developers.cloudflare.com/api/resources/dns/subresources/settings/ [Settings]: https://developers.cloudflare.com/api/resources/dns/subresources/settings/

View File

@@ -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<ICloudflareClient> _clientMock;
private CloudflareResponse<DnsAnalyticsByTime> _response;
private List<(string RequestPath, IQueryParameterFilter QueryFilter)> _callbacks;
[TestInitialize]
public void Initialize()
{
_callbacks = [];
_response = new CloudflareResponse<DnsAnalyticsByTime>
{
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<DnsAnalyticsByTime>(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<DnsAnalyticsByTime>($"/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<DnsAnalyticsByTime>(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<GetDnsAnalyticsByTimeFilter>(queryFilter);
_clientMock.Verify(m => m.GetAsync<DnsAnalyticsByTime>($"/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<ICloudflareClient>();
_clientMock
.Setup(m => m.GetAsync<DnsAnalyticsByTime>(It.IsAny<string>(), It.IsAny<IQueryParameterFilter>(), It.IsAny<CancellationToken>()))
.Callback<string, IQueryParameterFilter, CancellationToken>((requestPath, queryFilter, _) => _callbacks.Add((requestPath, queryFilter)))
.ReturnsAsync(() => _response);
return _clientMock.Object;
}
}
}

View File

@@ -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<ICloudflareClient> _clientMock;
private CloudflareResponse<DnsAnalyticsReport> _response;
private List<(string RequestPath, IQueryParameterFilter QueryFilter)> _callbacks;
[TestInitialize]
public void Initialize()
{
_callbacks = [];
_response = new CloudflareResponse<DnsAnalyticsReport>
{
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<DnsAnalyticsReport>(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<DnsAnalyticsReport>($"/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<DnsAnalyticsReport>(response.Result);
Assert.HasCount(1, _callbacks);
var (requestPath, queryFilter) = _callbacks.First();
Assert.AreEqual($"/zones/{ZoneId}/dns_analytics/report", requestPath);
Assert.IsNotNull(queryFilter);
Assert.IsInstanceOfType<GetDnsAnalyticsReportFilter>(queryFilter);
_clientMock.Verify(m => m.GetAsync<DnsAnalyticsReport>($"/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<ICloudflareClient>();
_clientMock
.Setup(m => m.GetAsync<DnsAnalyticsReport>(It.IsAny<string>(), It.IsAny<IQueryParameterFilter>(), It.IsAny<CancellationToken>()))
.Callback<string, IQueryParameterFilter, CancellationToken>((requestPath, queryFilter, _) => _callbacks.Add((requestPath, queryFilter)))
.ReturnsAsync(() => _response);
return _clientMock.Object;
}
}
}