Added 'DnsRecords' extensions

This commit is contained in:
2025-07-16 20:09:27 +02:00
parent 6a3b2d93c6
commit 3614d0344b
54 changed files with 6165 additions and 12 deletions

View File

@@ -2,6 +2,7 @@
This project aims to implement the [Cloudflare API] in an extensible way. This project aims to implement the [Cloudflare API] in an extensible way.
## Overview ## Overview
There should be a package for each API section as defined by Cloudflare. There should be a package for each API section as defined by Cloudflare.
@@ -9,12 +10,18 @@ There should be a package for each API section as defined by Cloudflare.
### [Cloudflare] ### [Cloudflare]
This is the base client, that will perform the request itself. It has the base url and holds the credentials. This is the core. It contains the base client, that will perform the request itself. It contains the base url and holds the credentials.
### [Cloudflare.Dns]
This package contains the feature set of the _DNS_ section of the Cloudflare API.
### [Cloudflare.Zones] ### [Cloudflare.Zones]
If you install this package, you will get all methods to handle a DNS zone. This package contains the feature set of the _Domain/Zone Management_ section of the Cloudflare API.
--- ---
@@ -22,12 +29,14 @@ If you install this package, you will get all methods to handle a DNS zone.
Published under [MIT License] (see [choose a license]) Published under [MIT License] (see [choose a license])
[![Buy me a Coffee](https://shields.am-wd.de/badge/PayPal-Buy_me_a_Coffee-yellow?style=flat&logo=paypal)](https://link.am-wd.de/donate) [![Buy me a Coffee](https://shields.am-wd.de/badge/PayPal-Buy_me_a_Coffee-yellow?style=flat&logo=paypal)](https://link.am-wd.de/donate)
[![built with Codeium](https://codeium.com/badges/main)](https://link.am-wd.de/codeium)
[Cloudflare]: Cloudflare/README.md [Cloudflare]: src/Cloudflare/README.md
[Cloudflare.Zones]: Extensions/Cloudflare.Zones/README.md [Cloudflare.Dns]: src/Extensions/Cloudflare.Dns/README.md
[Cloudflare.Zones]: src/Extensions/Cloudflare.Zones/README.md
[Cloudflare API]: https://developers.cloudflare.com/api/ [Cloudflare API]: https://developers.cloudflare.com/api/
[MIT License]: LICENSE.txt [MIT License]: LICENSE.txt

View File

@@ -29,5 +29,11 @@
/// </summary> /// </summary>
[JsonProperty("total_count")] [JsonProperty("total_count")]
public int? TotalCount { get; set; } public int? TotalCount { get; set; }
/// <summary>
/// Total number of pages of results.
/// </summary>
[JsonProperty("total_pages")]
public int? TotalPages { get; set; }
} }
} }

View File

@@ -15,7 +15,8 @@
</PropertyGroup> </PropertyGroup>
<!-- Only build package for tagged releases or Debug on CI (only dev NuGet feed) --> <!-- Only build package for tagged releases or Debug on CI (only dev NuGet feed) -->
<PropertyGroup Condition="$([System.Text.RegularExpressions.Regex]::IsMatch('$(CI_COMMIT_TAG)', '^dns\/v[0-9.]+')) or ('$(Configuration)' == 'Debug' and '$(GITLAB_CI)' == 'true')"> <!--<PropertyGroup Condition="$([System.Text.RegularExpressions.Regex]::IsMatch('$(CI_COMMIT_TAG)', '^dns\/v[0-9.]+')) or ('$(Configuration)' == 'Debug' and '$(GITLAB_CI)' == 'true')">-->
<PropertyGroup Condition="$([System.Text.RegularExpressions.Regex]::IsMatch('$(CI_COMMIT_TAG)', '^dns\/v[0-9.]+')) or '$(Configuration)' == 'Debug'">
<GeneratePackageOnBuild>true</GeneratePackageOnBuild> <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
</PropertyGroup> </PropertyGroup>

View File

@@ -0,0 +1,638 @@
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using AMWD.Net.Api.Cloudflare.Dns.Internals;
namespace AMWD.Net.Api.Cloudflare.Dns
{
/// <summary>
/// Extensions for <see href="https://developers.cloudflare.com/api/resources/dns/subresources/records/">DNS Records</see>.
/// </summary>
public static class DnsRecordsExtensions
{
private static readonly IReadOnlyCollection<DnsRecordType> _dataComponentTypes = [
DnsRecordType.CAA,
DnsRecordType.CERT,
DnsRecordType.DNSKEY,
DnsRecordType.DS,
DnsRecordType.HTTPS,
DnsRecordType.LOC,
DnsRecordType.NAPTR,
DnsRecordType.SMIMEA,
DnsRecordType.SRV,
DnsRecordType.SSHFP,
DnsRecordType.SVCB,
DnsRecordType.TLSA,
DnsRecordType.URI,
];
private static readonly IReadOnlyCollection<DnsRecordType> _priorityTypes = [
DnsRecordType.MX,
DnsRecordType.SRV,
DnsRecordType.URI,
];
/// <summary>
/// Send a Batch of DNS Record API calls to be executed together.
/// </summary>
/// <remarks>
/// Notes:
/// <list type="bullet">
/// <item>
/// Although Cloudflare will execute the batched operations in a single database
/// transaction, Cloudflare's distributed KV store must treat each record change
/// as a single key-value pair. This means that the propagation of changes is not
/// atomic. See
/// <see href="https://developers.cloudflare.com/dns/manage-dns-records/how-to/batch-record-changes/">the documentation</see>
/// for more information.
/// </item>
/// <item>
/// The operations you specify within the /batch request body are always executed
/// in the following order:
/// <list type="number">
/// <item>Deletes (DELETE)</item>
/// <item>Updates (PATCH)</item>
/// <item>Overwrites (PUT)</item>
/// <item>Creates (POST)</item>
/// </list>
/// </item>
/// </list>
/// </remarks>
/// <param name="client">The <see cref="ICloudflareClient"/> instance.</param>
/// <param name="request">The request.</param>
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
public static Task<CloudflareResponse<BatchDnsRecordsResponse>> BatchDnsRecords(this ICloudflareClient client, BatchDnsRecordsRequest request, CancellationToken cancellationToken = default)
{
request.ZoneId.ValidateCloudflareId();
// Deletes (DELETE)
var deletes = new List<Identifier>();
foreach (string delete in request.Deletes ?? [])
{
delete.ValidateCloudflareId();
deletes.Add(new Identifier { Id = delete });
}
// Updates (PATCH)
var patches = new List<InternalBatchUpdateRequest>();
foreach (var patch in request.Updates ?? [])
{
patch.Id.ValidateCloudflareId();
var req = ValidateRequest(patch);
req.Id = patch.Id;
patches.Add(req);
}
// Creates (POST)
var posts = new List<InternalDnsRecordRequest>();
foreach (var post in request.Creates ?? [])
{
var req = (InternalDnsRecordRequest)ValidateRequest(post);
posts.Add(req);
}
// Overwrites (PUT)
var puts = new List<InternalBatchUpdateRequest>();
foreach (var put in request.Overwrites ?? [])
{
put.Id.ValidateCloudflareId();
var req = ValidateRequest(put);
req.Id = put.Id;
puts.Add(req);
}
var batchReq = new InternalBatchRequest();
if (deletes.Count > 0)
batchReq.Deletes = deletes;
if (patches.Count > 0)
batchReq.Patches = patches;
if (posts.Count > 0)
batchReq.Posts = posts;
if (puts.Count > 0)
batchReq.Puts = puts;
return client.PostAsync<BatchDnsRecordsResponse, InternalBatchRequest>($"/zones/{request.ZoneId}/dns_records/batch", batchReq, null, cancellationToken);
}
/// <summary>
/// Create a new DNS record for a zone.
/// </summary>
/// <remarks>
/// Notes:
/// <list type="bullet">
/// <item>A/AAAA records cannot exist on the same name as CNAME records.</item>
/// <item>NS records cannot exist on the same name as any other record type.</item>
/// <item>Domain names are always represented in Punycode, even if Unicode characters were used when creating the record.</item>
/// </list>
/// </remarks>
/// <param name="client">The <see cref="ICloudflareClient"/> instance.</param>
/// <param name="request">The request.</param>
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
public static Task<CloudflareResponse<DnsRecord>> CreateDnsRecord(this ICloudflareClient client, CreateDnsRecordRequest request, CancellationToken cancellationToken = default)
{
request.ZoneId.ValidateCloudflareId();
var req = ValidateRequest(request);
return client.PostAsync<DnsRecord, InternalDnsRecordRequest>($"/zones/{request.ZoneId}/dns_records", req, null, cancellationToken);
}
/// <summary>
/// Delete DNS Record.
/// </summary>
/// <param name="client">The <see cref="ICloudflareClient"/> instance.</param>
/// <param name="zoneId">The zone identifier.</param>
/// <param name="recordId">The record identifier.</param>
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
public static Task<CloudflareResponse<Identifier>> DeleteDnsRecord(this ICloudflareClient client, string zoneId, string recordId, CancellationToken cancellationToken = default)
{
zoneId.ValidateCloudflareId();
recordId.ValidateCloudflareId();
return client.DeleteAsync<Identifier>($"/zones/{zoneId}/dns_records/{recordId}", null, cancellationToken);
}
/// <summary>
/// Update an existing DNS record.
/// </summary>
/// <remarks>
/// Notes:
/// <list type="bullet">
/// <item>A/AAAA records cannot exist on the same name as CNAME records.</item>
/// <item>NS records cannot exist on the same name as any other record type.</item>
/// <item>Domain names are always represented in Punycode, even if Unicode characters were used when creating the record.</item>
/// </list>
/// </remarks>
/// <param name="client">The <see cref="ICloudflareClient"/> instance.</param>
/// <param name="request">The request.</param>
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
public static Task<CloudflareResponse<DnsRecord>> UpdateDnsRecord(this ICloudflareClient client, UpdateDnsRecordRequest request, CancellationToken cancellationToken = default)
{
request.ZoneId.ValidateCloudflareId();
request.RecordId.ValidateCloudflareId();
var req = ValidateRequest(request);
return client.PatchAsync<DnsRecord, InternalDnsRecordRequest>($"/zones/{request.ZoneId}/dns_records/{request.RecordId}", req, cancellationToken);
}
/// <summary>
/// You can export your <see href="https://en.wikipedia.org/wiki/Zone_file">BIND config</see> through this endpoint.
/// </summary>
/// <param name="client">The <see cref="ICloudflareClient"/> instance.</param>
/// <param name="zoneId">The zone identifier.</param>
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
public static Task<CloudflareResponse<string>> ExportDnsRecords(this ICloudflareClient client, string zoneId, CancellationToken cancellationToken = default)
{
zoneId.ValidateCloudflareId();
return client.GetAsync<string>($"/zones/{zoneId}/dns_records/export", null, cancellationToken);
}
/// <summary>
/// DNS Record Details.
/// </summary>
/// <param name="client">The <see cref="ICloudflareClient"/> instance.</param>
/// <param name="zoneId">The zone identifier.</param>
/// <param name="recordId">The record identifier.</param>
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
public static Task<CloudflareResponse<DnsRecord>> DnsRecordDetails(this ICloudflareClient client, string zoneId, string recordId, CancellationToken cancellationToken = default)
{
zoneId.ValidateCloudflareId();
recordId.ValidateCloudflareId();
return client.GetAsync<DnsRecord>($"/zones/{zoneId}/dns_records/{recordId}", null, cancellationToken);
}
/// <summary>
/// You can upload your <see href="https://en.wikipedia.org/wiki/Zone_file">BIND config</see> through this endpoint.
/// It assumes that cURL is called from a location with bind_config.txt (valid BIND config) present.
/// </summary>
/// <remarks>
/// See <see href="https://developers.cloudflare.com/dns/manage-dns-records/how-to/import-and-export/">the documentation</see> for more information.
/// </remarks>
/// <param name="client">The <see cref="ICloudflareClient"/> instance.</param>
/// <param name="request">The request.</param>
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
public static Task<CloudflareResponse<RecordImportResponse>> ImportDnsRecords(this ICloudflareClient client, ImportDnsRecordsRequest request, CancellationToken cancellationToken = default)
{
request.ZoneId.ValidateCloudflareId();
if (string.IsNullOrWhiteSpace(request.File))
throw new ArgumentNullException(nameof(request.File));
var req = new MultipartFormDataContent();
if (request.Proxied.HasValue)
req.Add(new StringContent(request.Proxied.Value.ToString().ToLowerInvariant()), "proxied");
byte[] fileBytes = File.Exists(request.File)
? File.ReadAllBytes(request.File)
: Encoding.UTF8.GetBytes(request.File);
req.Add(new ByteArrayContent(fileBytes), "file");
return client.PostAsync<RecordImportResponse, MultipartFormDataContent>($"/zones/{request.ZoneId}/dns_records/import", req, null, cancellationToken);
}
/// <summary>
/// List, search, sort, and filter a zones' DNS records.
/// </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<IReadOnlyCollection<DnsRecord>>> ListDnsRecords(this ICloudflareClient client, string zoneId, ListDnsRecordsFilter? options = null, CancellationToken cancellationToken = default)
{
zoneId.ValidateCloudflareId();
return client.GetAsync<IReadOnlyCollection<DnsRecord>>($"/zones/{zoneId}/dns_records", options, cancellationToken);
}
/// <summary>
/// Scan for common DNS records on your domain and automatically add them to your zone.
/// Useful if you haven't updated your nameservers yet.
/// </summary>
/// <param name="client">The <see cref="ICloudflareClient"/> instance.</param>
/// <param name="zoneId">The zone identifier.</param>
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
public static Task<CloudflareResponse<RecordScanResponse>> ScanDnsRecords(this ICloudflareClient client, string zoneId, CancellationToken cancellationToken = default)
{
zoneId.ValidateCloudflareId();
return client.PostAsync<RecordScanResponse, object>($"/zones/{zoneId}/dns_records/scan", null, null, cancellationToken);
}
/// <summary>
/// Overwrite an existing DNS record.
/// </summary>
/// <remarks>
/// Notes:
/// <list type="bullet">
/// <item>A/AAAA records cannot exist on the same name as CNAME records.</item>
/// <item>NS records cannot exist on the same name as any other record type.</item>
/// <item>Domain names are always represented in Punycode, even if Unicode characters were used when creating the record.</item>
/// </list>
/// </remarks>
/// <param name="client">The <see cref="ICloudflareClient"/> instance.</param>
/// <param name="request">The request.</param>
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
public static Task<CloudflareResponse<DnsRecord>> OverwriteDnsRecord(this ICloudflareClient client, OverwriteDnsRecordRequest request, CancellationToken cancellationToken = default)
{
request.ZoneId.ValidateCloudflareId();
request.RecordId.ValidateCloudflareId();
var req = ValidateRequest(request);
return client.PutAsync<DnsRecord, InternalDnsRecordRequest>($"/zones/{request.ZoneId}/dns_records/{request.RecordId}", req, cancellationToken);
}
private static InternalDnsRecordRequest ValidateRequest(CreateDnsRecordRequest request)
{
if (string.IsNullOrWhiteSpace(request.Name))
throw new ArgumentNullException(nameof(request.Name));
if (!Enum.IsDefined(typeof(DnsRecordType), request.Type))
throw new ArgumentOutOfRangeException(nameof(request.Type), request.Type, $"The value '{request.Type}' is not valid. Valid value are in {nameof(DnsRecordType)}.");
var req = new InternalDnsRecordRequest
{
Name = request.Name.Trim(),
Type = request.Type,
Comment = request.Comment?.Trim(),
Proxied = request.Proxied,
Tags = request.Tags?
.Where(t => !string.IsNullOrWhiteSpace(t))
.Select(t => t.Trim())
.ToList()
};
if (!_dataComponentTypes.Contains(request.Type) && string.IsNullOrWhiteSpace(request.Content))
{
throw new ArgumentNullException(nameof(request.Content));
}
else
{
req.Content = request.Content?.Trim();
}
if (request.Type == DnsRecordType.CNAME && request.Settings != null && request.Settings is CNAMERecordSettings cnameSettings)
req.Settings = cnameSettings;
if (_dataComponentTypes.Contains(request.Type) && request.Data == null)
throw new ArgumentNullException(nameof(request.Data));
if (_priorityTypes.Contains(request.Type))
{
if (!request.Priority.HasValue)
throw new ArgumentNullException(nameof(request.Priority));
req.Priority = request.Priority.Value;
}
if (request.TimeToLive.HasValue)
{
if (request.TimeToLive == 1)
{
req.Ttl = 1;
}
else if (request.TimeToLive < 30 || 86400 < request.TimeToLive)
{
throw new ArgumentOutOfRangeException(nameof(request.TimeToLive), request.TimeToLive, $"The value '{request.TimeToLive}' is not valid. Valid value are between 60 (30 for Enterprise) and 86400.");
}
else
{
req.Ttl = request.TimeToLive.Value;
}
}
switch (request.Type)
{
case DnsRecordType.CAA:
if (request.Data is CAARecordData caaData)
{
if (string.IsNullOrWhiteSpace(caaData.Tag))
throw new ArgumentNullException($"{nameof(request.Data)}.{nameof(caaData.Tag)}");
if (string.IsNullOrWhiteSpace(caaData.Value))
throw new ArgumentNullException($"{nameof(request.Data)}.{nameof(caaData.Value)}");
req.Data = caaData;
}
else
{
throw new ArgumentException($"The type of the value '{nameof(request.Data)}' has to be '{nameof(CAARecordData)}'", nameof(request.Data));
}
break;
case DnsRecordType.CERT:
if (request.Data is CERTRecordData certData)
{
if (string.IsNullOrWhiteSpace(certData.Certificate))
throw new ArgumentNullException($"{nameof(request.Data)}.{nameof(certData.Certificate)}");
req.Data = certData;
}
else
{
throw new ArgumentException($"The type of the value '{nameof(request.Data)}' has to be '{nameof(CAARecordData)}'", nameof(request.Data));
}
break;
case DnsRecordType.DNSKEY:
if (request.Data is DNSKEYRecordData dnskeyData)
{
if (string.IsNullOrWhiteSpace(dnskeyData.PublicKey))
throw new ArgumentNullException($"{nameof(request.Data)}.{nameof(dnskeyData.PublicKey)}");
req.Data = dnskeyData;
}
else
{
throw new ArgumentException($"The type of the value '{nameof(request.Data)}' has to be '{nameof(DNSKEYRecordData)}'", nameof(request.Data));
}
break;
case DnsRecordType.DS:
if (request.Data is DSRecordData dsData)
{
if (string.IsNullOrWhiteSpace(dsData.Digest))
throw new ArgumentNullException($"{nameof(request.Data)}.{nameof(dsData.Digest)}");
req.Data = dsData;
}
else
{
throw new ArgumentException($"The type of the value '{nameof(request.Data)}' has to be '{nameof(DSRecordData)}'", nameof(request.Data));
}
break;
case DnsRecordType.HTTPS:
if (request.Data is HTTPSRecordData httpsData)
{
if (string.IsNullOrWhiteSpace(httpsData.Target))
throw new ArgumentNullException($"{nameof(request.Data)}.{nameof(httpsData.Target)}");
if (string.IsNullOrWhiteSpace(httpsData.Value))
throw new ArgumentNullException($"{nameof(request.Data)}.{nameof(httpsData.Value)}");
req.Data = httpsData;
}
else
{
throw new ArgumentException($"The type of the value '{nameof(request.Data)}' has to be '{nameof(HTTPSRecordData)}'", nameof(request.Data));
}
break;
case DnsRecordType.LOC:
if (request.Data is LOCRecordData locData)
{
if (locData.LatitudeDegrees.HasValue && (locData.LatitudeDegrees < 0 || 90 < locData.LatitudeDegrees))
throw new ArgumentOutOfRangeException($"{nameof(request.Data)}.{nameof(locData.LatitudeDegrees)}", locData.LatitudeDegrees, $"The value '{locData.LatitudeDegrees}' is not valid. Valid value are between {0} and {90}.");
if (locData.LatitudeMinutes.HasValue && (locData.LatitudeMinutes < 0 || 59 < locData.LatitudeMinutes))
throw new ArgumentOutOfRangeException($"{nameof(request.Data)}.{nameof(locData.LatitudeMinutes)}", locData.LatitudeMinutes, $"The value '{locData.LatitudeMinutes}' is not valid. Valid value are between {0} and {59}.");
if (locData.LatitudeSeconds.HasValue && (locData.LatitudeSeconds < 0 || 59.999 < locData.LatitudeSeconds))
throw new ArgumentOutOfRangeException($"{nameof(request.Data)}.{nameof(locData.LatitudeSeconds)}", locData.LatitudeSeconds, $"The value '{locData.LatitudeSeconds}' is not valid. Valid value are between {0} and {59.999}.");
if (locData.LatitudeDirection.HasValue && !Enum.IsDefined(typeof(LOCRecordLatitudeDirection), locData.LatitudeDirection))
throw new ArgumentOutOfRangeException($"{nameof(request.Data)}.{nameof(locData.LatitudeDirection)}", locData.LatitudeDirection, $"The value '{locData.LatitudeDirection}' is not valid.");
if (locData.LongitudeDegrees.HasValue && (locData.LongitudeDegrees < 0 || 180 < locData.LongitudeDegrees))
throw new ArgumentOutOfRangeException($"{nameof(request.Data)}.{nameof(locData.LongitudeDegrees)}", locData.LongitudeDegrees, $"The value '{locData.LongitudeDegrees}' is not valid. Valid value are between {0} and {180}.");
if (locData.LongitudeMinutes.HasValue && (locData.LongitudeMinutes < 0 || 59 < locData.LongitudeMinutes))
throw new ArgumentOutOfRangeException($"{nameof(request.Data)}.{nameof(locData.LongitudeMinutes)}", locData.LongitudeMinutes, $"The value '{locData.LongitudeMinutes}' is not valid. Valid value are between {0} and {59}.");
if (locData.LongitudeSeconds.HasValue && (locData.LongitudeSeconds < 0 || 59.999 < locData.LongitudeSeconds))
throw new ArgumentOutOfRangeException($"{nameof(request.Data)}.{nameof(locData.LongitudeSeconds)}", locData.LongitudeSeconds, $"The value '{locData.LongitudeSeconds}' is not valid. Valid value are between {0} and {59.999}.");
if (locData.LongitudeDirection.HasValue && (!Enum.IsDefined(typeof(LOCRecordLongitudeDirection), locData.LongitudeDirection)))
throw new ArgumentOutOfRangeException($"{nameof(request.Data)}.{nameof(locData.LongitudeDirection)}", locData.LongitudeDirection, $"The value '{locData.LongitudeDirection}' is not valid.");
if (locData.Altitude.HasValue && (locData.Altitude < -100_000 || 42_849_672.95 < locData.Altitude))
throw new ArgumentOutOfRangeException($"{nameof(request.Data)}.{nameof(locData.Altitude)}", locData.Size, $"The value '{locData.Altitude}' is not valid. Valid value are between {-100_000} and {42_849_672.95}.");
if (locData.Size.HasValue && (locData.Size < 0 || 90_000_000 < locData.Size))
throw new ArgumentOutOfRangeException($"{nameof(request.Data)}.{nameof(locData.Size)}", locData.Size, $"The value '{locData.Size}' is not valid. Valid value are between {0} and {90_000_000}.");
if (locData.PrecisionHorizontal.HasValue && (locData.PrecisionHorizontal < 0 || 90_000_000 < locData.PrecisionHorizontal))
throw new ArgumentOutOfRangeException($"{nameof(request.Data)}.{nameof(locData.PrecisionHorizontal)}", locData.Size, $"The value '{locData.PrecisionHorizontal}' is not valid. Valid value are between {0} and {90_000_000}.");
if (locData.PrecisionVertical.HasValue && (locData.PrecisionVertical < 0 || 90_000_000 < locData.PrecisionVertical))
throw new ArgumentOutOfRangeException($"{nameof(request.Data)}.{nameof(locData.PrecisionVertical)}", locData.PrecisionVertical, $"The value '{locData.PrecisionVertical}' is not valid. Valid value are between {0} and {90_000_000}.");
if (locData.LatitudeSeconds.HasValue)
locData.LatitudeSeconds = Math.Floor(locData.LatitudeSeconds.Value * 1000) / 1000; // Truncate to 3 decimal places
if (locData.LongitudeSeconds.HasValue)
locData.LongitudeSeconds = Math.Floor(locData.LongitudeSeconds.Value * 1000) / 1000; // Truncate to 3 decimal places
if (locData.Altitude.HasValue)
locData.Altitude = Math.Floor(locData.Altitude.Value * 100) / 100; // Truncate to 2 decimal places
req.Data = locData;
}
else
{
throw new ArgumentException($"The type of the value '{nameof(request.Data)}' has to be '{nameof(LOCRecordData)}'", nameof(request.Data));
}
break;
case DnsRecordType.NAPTR:
if (request.Data is NAPTRRecordData naptrData)
{
if (string.IsNullOrWhiteSpace(naptrData.Flags))
throw new ArgumentNullException($"{nameof(request.Data)}.{nameof(naptrData.Flags)}");
if (string.IsNullOrWhiteSpace(naptrData.Regex))
throw new ArgumentNullException($"{nameof(request.Data)}.{nameof(naptrData.Regex)}");
if (string.IsNullOrWhiteSpace(naptrData.Replacement))
throw new ArgumentNullException($"{nameof(request.Data)}.{nameof(naptrData.Replacement)}");
if (string.IsNullOrWhiteSpace(naptrData.Service))
throw new ArgumentNullException($"{nameof(request.Data)}.{nameof(naptrData.Service)}");
req.Data = naptrData;
}
else
{
throw new ArgumentException($"The type of the value '{nameof(request.Data)}' has to be '{nameof(NAPTRRecordData)}'", nameof(request.Data));
}
break;
case DnsRecordType.SMIMEA:
if (request.Data is SMIMEARecordData smimeaData)
{
if (string.IsNullOrWhiteSpace(smimeaData.Certificate))
throw new ArgumentNullException($"{nameof(request.Data)}.{nameof(smimeaData.Certificate)}");
req.Data = smimeaData;
}
else
{
throw new ArgumentException($"The type of the value '{nameof(request.Data)}' has to be '{nameof(SMIMEARecordData)}'", nameof(request.Data));
}
break;
case DnsRecordType.SRV:
if (request.Data is SRVRecordData srvData)
{
if (string.IsNullOrWhiteSpace(srvData.Target))
throw new ArgumentNullException($"{nameof(request.Data)}.{nameof(srvData.Target)}");
req.Data = srvData;
}
else
{
throw new ArgumentException($"The type of the value '{nameof(request.Data)}' has to be '{nameof(SRVRecordData)}'", nameof(request.Data));
}
break;
case DnsRecordType.SSHFP:
if (request.Data is SSHFPRecordData sshfpData)
{
if (string.IsNullOrWhiteSpace(sshfpData.Fingerprint))
throw new ArgumentNullException($"{nameof(request.Data)}.{nameof(sshfpData.Fingerprint)}");
req.Data = sshfpData;
}
else
{
throw new ArgumentException($"The type of the value '{nameof(request.Data)}' has to be '{nameof(SSHFPRecordData)}'", nameof(request.Data));
}
break;
case DnsRecordType.SVCB:
if (request.Data is SVCBRecordData svcbData)
{
if (string.IsNullOrWhiteSpace(svcbData.Target))
throw new ArgumentNullException($"{nameof(request.Data)}.{nameof(svcbData.Target)}");
if (string.IsNullOrWhiteSpace(svcbData.Value))
throw new ArgumentNullException($"{nameof(request.Data)}.{nameof(svcbData.Value)}");
req.Data = svcbData;
}
else
{
throw new ArgumentException($"The type of the value '{nameof(request.Data)}' has to be '{nameof(SVCBRecordData)}'", nameof(request.Data));
}
break;
case DnsRecordType.TLSA:
if (request.Data is TLSARecordData tlsaData)
{
if (string.IsNullOrWhiteSpace(tlsaData.Certificate))
throw new ArgumentNullException($"{nameof(request.Data)}.{nameof(tlsaData.Certificate)}");
req.Data = tlsaData;
}
else
{
throw new ArgumentException($"The type of the value '{nameof(request.Data)}' has to be '{nameof(TLSARecordData)}'", nameof(request.Data));
}
break;
case DnsRecordType.URI:
if (request.Data is URIRecordData uriData)
{
if (string.IsNullOrWhiteSpace(uriData.Target))
throw new ArgumentNullException($"{nameof(request.Data)}.{nameof(uriData.Target)}");
req.Data = uriData;
}
else
{
throw new ArgumentException($"The type of the value '{nameof(request.Data)}' has to be '{nameof(URIRecordData)}'", nameof(request.Data));
}
break;
}
return req;
}
private static InternalBatchUpdateRequest ValidateRequest(BatchDnsRecordsRequest.Post request)
{
var req = ValidateRequest(new CreateDnsRecordRequest("", request.Name)
{
Comment = request.Comment,
Content = request.Content,
Data = request.Data,
Priority = request.Priority,
Proxied = request.Proxied,
Settings = request.Settings,
Tags = request.Tags,
TimeToLive = request.TimeToLive,
Type = request.Type
});
return new InternalBatchUpdateRequest
{
Comment = req.Comment,
Content = req.Content,
Data = req.Data,
Name = req.Name,
Priority = req.Priority,
Proxied = req.Proxied,
Settings = req.Settings,
Tags = req.Tags,
Ttl = req.Ttl,
Type = req.Type
};
}
}
}

View File

@@ -0,0 +1,277 @@
using System.Runtime.Serialization;
using Newtonsoft.Json.Converters;
namespace AMWD.Net.Api.Cloudflare.Dns
{
/// <summary>
/// DNS record types.
/// <see href="https://github.com/cloudflare/cloudflare-typescript/blob/v4.4.1/src/resources/dns/records.ts">Source</see>
/// </summary>
/// <remarks>
/// A list with short description can be found <see href="https://en.wikipedia.org/wiki/List_of_DNS_record_types">@wikipedia</see>.
/// </remarks>
[JsonConverter(typeof(StringEnumConverter))]
public enum DnsRecordType : int
{
/// <summary>
/// Address record.
/// </summary>
/// <remarks>
/// Returns a 32-bit IPv4 address, most commonly used to map hostnames to an IP address of the host, but it is also used for DNSBLs, storing subnet masks in RFC 1101, etc.
/// <code>
/// example.com. 3600 IN A 96.7.128.175
/// </code>
/// </remarks>
[EnumMember(Value = "A")]
A = 1,
/// <summary>
/// IPv6 address record.
/// </summary>
/// <remarks>
/// Returns a 128-bit IPv6 address, most commonly used to map hostnames to an IP address of the host.
/// <code>
/// example.com. 3600 IN AAAA 2600:1408:ec00:36::1736:7f31
/// </code>
/// </remarks>
[EnumMember(Value = "AAAA")]
AAAA = 28,
/// <summary>
/// Certification Authority Authorization.
/// </summary>
/// <remarks>
/// DNS Certification Authority Authorization, constraining acceptable CAs for a host/domain.
/// <code>
/// example.com. 604800 IN CAA 0 issue "letsencrypt.org"
/// </code>
/// </remarks>
[EnumMember(Value = "CAA")]
CAA = 257,
/// <summary>
/// Certificate record.
/// </summary>
/// <remarks>
/// Stores PKIX, SPKI, PGP, etc.
/// <code>
/// example.com. 86400 IN CERT 2 77 2 TUlJQ1l6Q0NBY3lnQXdJQkFnSUJBREFOQmdrcWh
/// </code>
/// </remarks>
[EnumMember(Value = "CERT")]
CERT = 37,
/// <summary>
/// Canonical name record.
/// </summary>
/// <remarks>
/// Alias of one name to another: the DNS lookup will continue by retrying the lookup with the new name.
/// <code>
/// autodiscover.example.com. 86400 IN CNAME mail.example.com.
/// </code>
/// </remarks>
[EnumMember(Value = "CNAME")]
CNAME = 5,
/// <summary>
/// DNS Key record.
/// </summary>
/// <remarks>
/// The key record used in DNSSEC. Uses the same format as the KEY record.
/// <code>
/// example.com. 3600 IN DNSKEY 256 3 13 OtuN/SL9sE+SDQ0tOLeezr1KzUNi77FflTjxQylUhm3V7m13Vz9tYQuc SGK0pyxISo9CQsszubAwJSypq3li3g==
/// </code>
/// </remarks>
[EnumMember(Value = "DNSKEY")]
DNSKEY = 48,
/// <summary>
/// Delegation signer.
/// </summary>
/// <remarks>
/// The record used to identify the DNSSEC signing key of a delegated zone.
/// <code>
/// example.com. 86400 IN DS 370 13 2 BE74359954660069D5C63D200C39F5603827D7DD02B56F120EE9F3A8 6764247C
/// </code>
/// </remarks>
[EnumMember(Value = "DS")]
DS = 43,
/// <summary>
/// HTTPS Binding.
/// </summary>
/// <remarks>
/// RR that improves performance for clients that need to resolve many resources to access a domain.
/// <code>
/// example.com. 3600 IN HTTPS 1 svc.example.com. alpn=h2
/// </code>
/// </remarks>
[EnumMember(Value = "HTTPS")]
HTTPS = 65,
/// <summary>
/// Location record.
/// </summary>
/// <remarks>
/// Specifies a geographical location associated with a domain name.
/// <code>
/// SW1A2AA.find.me.uk. 2592000 IN LOC 51 30 12.748 N 0 7 39.611 W 0.00m 0.00m 0.00m 0.00m
/// </code>
/// </remarks>
[EnumMember(Value = "LOC")]
LOC = 29,
/// <summary>
/// Mail exchange record.
/// </summary>
/// <remarks>
/// List of mail exchange servers that accept email for a domain.
/// <code>
/// example.com. 43200 IN MX 0 mail.example.com.
/// </code>
/// </remarks>
[EnumMember(Value = "MX")]
MX = 15,
/// <summary>
/// Naming Authority Pointer.
/// </summary>
/// <remarks>
/// Allows regular-expression-based rewriting of domain names which can then be used as URIs, further domain names to lookups, etc.
/// <code>
/// example.com. 86400 IN NAPTR 100 10 "S" "SIP+D2T" "" _sip._tcp.example.com.
/// </code>
/// </remarks>
[EnumMember(Value = "NAPTR")]
NAPTR = 35,
/// <summary>
/// Name server record.
/// </summary>
/// <remarks>
/// Delegates a DNS zone to use the given authoritative name servers.
/// <code>
/// example.com. 86400 IN NS a.iana-servers.net.
/// </code>
/// </remarks>
[EnumMember(Value = "NS")]
NS = 2,
/// <summary>
/// OpenPGP public key record.
/// </summary>
/// <remarks>
/// A DNS-based Authentication of Named Entities (DANE) method for publishing and locating OpenPGP
/// public keys in DNS for a specific email address using an OPENPGPKEY DNS resource record.
/// <code>
/// 00d8d3f11739d2f3537099982b4674c29fc59a8fda350fca1379613a._openpgpkey.example.com. 3600 IN OPENPGPKEY a2V5S0VZMTIzNGtleUtFWQ==
/// </code>
/// </remarks>
[EnumMember(Value = "OPENPGPKEY")]
OPENPGPKEY = 61,
/// <summary>
/// PTR Resource Record.
/// </summary>
/// <remarks>
/// Pointer to a canonical name.
/// Unlike a CNAME, DNS processing stops and just the name is returned.
/// The most common use is for implementing reverse DNS lookups, but other uses include such things as DNS-SD.
/// <code>
/// 14.215.184.93.in-addr.arpa. 86400 IN PTR example.com.
/// </code>
/// </remarks>
[EnumMember(Value = "PTR")]
PTR = 12,
/// <summary>
/// S/MIME cert association.
/// </summary>
/// <remarks>
/// Associates an S/MIME certificate with a domain name for sender authentication.
/// <code>
/// example.com. 3600 IN SMIMEA 0 0 0 keyKEY1234keyKEY
/// </code>
/// </remarks>
[EnumMember(Value = "SMIMEA")]
SMIMEA = 53,
/// <summary>
/// Service locator.
/// </summary>
/// <remarks>
/// Generalized service location record, used for newer protocols instead of creating protocol-specific records such as MX.
/// <code>
/// _autodiscover._tcp.example.com. 604800 IN SRV 1 0 443 mail.example.com.
/// </code>
/// </remarks>
[EnumMember(Value = "SRV")]
SRV = 33,
/// <summary>
/// SSH Public Key Fingerprint.
/// </summary>
/// <remarks>
/// Resource record for publishing SSH public host key fingerprints in the DNS, in order to aid in verifying the authenticity of the host.
/// RFC 6594 defines ECC SSH keys and SHA-256 hashes. See the IANA SSHFP RR parameters registry for details.
/// <code>
/// example.com. 600 IN SSHFP 2 1 123456789abcdef67890123456789abcdef67890
/// </code>
/// </remarks>
[EnumMember(Value = "SSHFP")]
SSHFP = 44,
/// <summary>
/// Service Binding.
/// </summary>
/// <remarks>
/// RR that improves performance for clients that need to resolve many resources to access a domain.
/// <code>
/// example.com. 3600 IN SVCB 1 . alpn="h2,http/1.1"
/// </code>
/// </remarks>
[EnumMember(Value = "SVCB")]
SVCB = 64,
/// <summary>
/// TLSA certificate association.
/// </summary>
/// <remarks>
/// A record for DANE. RFC 6698 defines "The TLSA DNS resource record is used to associate a TLS server certificate
/// or public key with the domain name where the record is found, thus forming a 'TLSA certificate association'".
/// <code>
/// _443._tcp.example.com. 3600 IN TLSA 3 0 18cb0fc6c527506a053f4f14c8464bebbd6dede2738d11468dd953d7d6a3021f1
/// </code>
/// </remarks>
[EnumMember(Value = "TLSA")]
TLSA = 52,
/// <summary>
/// Text record.
/// </summary>
/// <remarks>
/// Originally for arbitrary human-readable text in a DNS record.
/// Since the early 1990s, however, this record more often carries machine-readable data, such as specified by RFC 1464,
/// opportunistic encryption, Sender Policy Framework, DKIM, DMARC, DNS-SD, etc.
/// <br/>
/// More information about TXT records on <see href="https://www.cloudflare.com/learning/dns/dns-records/dns-txt-record/">Cloudflare</see>.
/// <code>
/// example.com. 86400 IN TXT "v=spf1 -all"
/// </code>
/// </remarks>
[EnumMember(Value = "TXT")]
TXT = 16,
/// <summary>
/// Uniform Resource Identifier.
/// </summary>
/// <remarks>
/// Can be used for publishing mappings from hostnames to URIs.
/// <code>
/// _ftp._tcp.example.com. 3600 IN URI 10 1 "ftp://ftp.example.com/public"
/// </code>
/// </remarks>
[EnumMember(Value = "URI")]
URI = 256,
}
}

View File

@@ -0,0 +1,24 @@
using System.Runtime.Serialization;
using Newtonsoft.Json.Converters;
namespace AMWD.Net.Api.Cloudflare.Dns
{
/// <summary>
/// Options how to match the query filter.
/// </summary>
[JsonConverter(typeof(StringEnumConverter))]
public enum FilterMatchType
{
/// <summary>
/// Match any rule.
/// </summary>
[EnumMember(Value = "any")]
Any = 1,
/// <summary>
/// Match all rules.
/// </summary>
[EnumMember(Value = "all")]
All = 2
}
}

View File

@@ -0,0 +1,439 @@
using System.Runtime.Serialization;
using Newtonsoft.Json.Converters;
namespace AMWD.Net.Api.Cloudflare.Dns
{
/// <summary>
/// Filter for listing DNS records.
/// </summary>
public class ListDnsRecordsFilter : IQueryParameterFilter
{
#region Comment
/// <summary>
/// Exact value of the DNS record comment.
/// This is a convenience alias for <see cref="CommentExact"/>.
/// </summary>
public string? Comment { get; set; }
/// <summary>
/// If this parameter is present, only records <em>without</em> a comment are returned.
/// </summary>
public bool? CommentAbsent { get; set; }
/// <summary>
/// Substring of the DNS record comment.
/// Comment filters are case-insensitive.
/// </summary>
public string? CommentContains { get; set; }
/// <summary>
/// Suffix of the DNS record comment.
/// Comment filters are case-insensitive.
/// </summary>
public string? CommentEndsWith { get; set; }
/// <summary>
/// Exact value of the DNS record comment.
/// Comment filters are case-insensitive.
/// </summary>
public string? CommentExact { get; set; }
/// <summary>
/// If this parameter is present, only records <em>with</em> a comment are returned.
/// </summary>
public bool? CommentPresent { get; set; }
/// <summary>
/// Prefix of the DNS record comment.
/// Comment filters are case-insensitive.
/// </summary>
public string? CommentStartsWith { get; set; }
#endregion Comment
#region Content
/// <summary>
/// Exact value of the DNS record Content.
/// This is a convenience alias for <see cref="ContentExact"/>.
/// </summary>
public string? Content { get; set; }
/// <summary>
/// Substring of the DNS record Content.
/// Content filters are case-insensitive.
/// </summary>
public string? ContentContains { get; set; }
/// <summary>
/// Suffix of the DNS record Content.
/// Content filters are case-insensitive.
/// </summary>
public string? ContentEndsWith { get; set; }
/// <summary>
/// Exact value of the DNS record Content.
/// Content filters are case-insensitive.
/// </summary>
public string? ContentExact { get; set; }
/// <summary>
/// Prefix of the DNS record Content.
/// Content filters are case-insensitive.
/// </summary>
public string? ContentStartsWith { get; set; }
#endregion Content
/// <summary>
/// Direction to order DNS records in.
/// </summary>
public SortDirection? Direction { get; set; }
/// <summary>
/// Whether to match all search requirements or at least one (any).
/// </summary>
/// <remarks>
/// <para>
/// If set to <see cref="FilterMatchType.All"/>, acts like a logical AND between filters.
/// <br/>
/// If set to <see cref="FilterMatchType.Any"/>, acts like a logical OR instead.
/// </para>
/// <para>
/// Note that the interaction between tag filters is controlled by the <see cref="TagMatch"/> parameter instead.
/// </para>
/// </remarks>
public FilterMatchType? Match { get; set; }
#region Name
/// <summary>
/// Exact value of the DNS record Name.
/// This is a convenience alias for <see cref="NameExact"/>.
/// </summary>
public string? Name { get; set; }
/// <summary>
/// Substring of the DNS record Name.
/// Name filters are case-insensitive.
/// </summary>
public string? NameContains { get; set; }
/// <summary>
/// Suffix of the DNS record Name.
/// Name filters are case-insensitive.
/// </summary>
public string? NameEndsWith { get; set; }
/// <summary>
/// Exact value of the DNS record Name.
/// Name filters are case-insensitive.
/// </summary>
public string? NameExact { get; set; }
/// <summary>
/// Prefix of the DNS record Name.
/// Name filters are case-insensitive.
/// </summary>
public string? NameStartsWith { get; set; }
#endregion Name
/// <summary>
/// Field to order DNS records by.
/// </summary>
public DnsRecordsOrderBy? OrderBy { get; set; }
/// <summary>
/// Page number of paginated results.
/// </summary>
/// <value>1 &lt;= X</value>
public int? Page { get; set; }
/// <summary>
/// Number of DNS records per page.
/// </summary>
/// <value>1 &lt;= X &lt;= 5,000,000</value>
public int? PerPage { get; set; }
/// <summary>
/// Whether the record is receiving the performance and security benefits of Cloudflare.
/// </summary>
public bool? Proxied { get; set; }
/// <summary>
/// Allows searching in multiple properties of a DNS record simultaneously.
/// </summary>
/// <remarks>
/// <para>
/// This parameter is intended for human users, not automation.
/// Its exact behavior is intentionally left unspecified and is subject to change in the future.
/// </para>
/// <para>
/// <em>
/// This parameter works independently of the <see cref="Match"/> setting.
/// <br/>
/// For automated searches, please use the other available parameters.
/// </em>
/// </para>
/// </remarks>
public string? Search { get; set; }
#region Tag
/// <summary>
/// Condition on the DNS record tag.
/// </summary>
/// <remarks>
/// <para>
/// Parameter values can be of the form <c>&lt;tag-name&gt;:&lt;tag-value&gt;</c> to search for an exact name:value pair,
/// or just <c>&lt;tag-name&gt;</c> to search for records with a specific tag name regardless of its value.
/// </para>
/// <para>
/// This is a convenience shorthand for the more powerful <c>tag.&lt;predicate&gt;</c> parameters.
/// <br/>
/// Examples:
/// <list type="bullet">
/// <item><c>tag=important</c> is equivalent to <c>tag.present=important</c></item>
/// <item><c>tag=team:DNS</c> is equivalent to <c>tag.exact=team:DNS</c></item>
/// </list>
/// </para>
/// </remarks>
public string? Tag { get; set; }
/// <summary>
/// Name of a tag which must <em>not</em> be present on the DNS record.
/// Tag filters are case-insensitive.
/// </summary>
public string? TagAbsent { get; set; }
/// <summary>
/// A tag and value, of the form <c>&lt;tag-name&gt;:&lt;tag-value&gt;</c>.
/// Tag filters are case-insensitive.
/// </summary>
/// <remarks>
/// The API will only return DNS records that have a tag named <c>&lt;tag-name&gt;</c> whose value contains <c>&lt;tag-value&gt;</c>.
/// </remarks>
public string? TagContains { get; set; }
/// <summary>
/// A tag and value, of the form <c>&lt;tag-name&gt;:&lt;tag-value&gt;</c>.
/// Tag filters are case-insensitive.
/// </summary>
/// <remarks>
/// The API will only return DNS records that have a tag named <c>&lt;tag-name&gt;</c> whose value ends with <c>&lt;tag-value&gt;</c>.
/// </remarks>
public string? TagEndsWith { get; set; }
/// <summary>
/// A tag and value, of the form <c>&lt;tag-name&gt;:&lt;tag-value&gt;</c>.
/// Tag filters are case-insensitive.
/// </summary>
/// <remarks>
/// The API will only return DNS records that have a tag named <c>&lt;tag-name&gt;</c> whose value is <c>&lt;tag-value&gt;</c>.
/// </remarks>
public string? TagExact { get; set; }
/// <summary>
/// Name of a tag which must be present on the DNS record.
/// Tag filters are case-insensitive.
/// </summary>
public string? TagPresent { get; set; }
/// <summary>
/// A tag and value, of the form <c>&lt;tag-name&gt;:&lt;tag-value&gt;</c>.
/// Tag filters are case-insensitive.
/// </summary>
/// <remarks>
/// The API will only return DNS records that have a tag named <c>&lt;tag-name&gt;</c> whose value starts with <c>&lt;tag-value&gt;</c>.
/// </remarks>
public string? TagStartsWith { get; set; }
#endregion Tag
/// <summary>
/// Whether to match all tag search requirements or at least one (any).
/// </summary>
/// <remarks>
/// <para>
/// If set to <see cref="FilterMatchType.All"/>, acts like a logical AND between filters.
/// <br/>
/// If set to <see cref="FilterMatchType.Any"/>, acts like a logical OR instead.
/// </para>
/// <para>
/// Note that the regular <see cref="Match"/> parameter is still used to combine the resulting condition with other filters that aren't related to tags.
/// </para>
/// </remarks>
public FilterMatchType? TagMatch { get; set; }
/// <summary>
/// Record type.
/// </summary>
public DnsRecordType? Type { get; set; }
/// <inheritdoc />
public IDictionary<string, string> GetQueryParameters()
{
var dict = new Dictionary<string, string>();
#pragma warning disable CS8602, CS8604 // There will be no null value below.
#region Comment
if (!string.IsNullOrWhiteSpace(Comment))
dict.Add("comment", Comment.Trim());
if (CommentAbsent.HasValue && CommentAbsent.Value)
dict.Add("comment.absent", "true");
if (!string.IsNullOrWhiteSpace(CommentContains))
dict.Add("comment.contains", CommentContains.Trim());
if (!string.IsNullOrWhiteSpace(CommentEndsWith))
dict.Add("comment.endswith", CommentEndsWith.Trim());
if (!string.IsNullOrWhiteSpace(CommentExact))
dict.Add("comment.exact", CommentExact.Trim());
if (CommentPresent.HasValue && CommentPresent.Value)
dict.Add("comment.present", "true");
if (!string.IsNullOrWhiteSpace(CommentStartsWith))
dict.Add("comment.startswith", CommentStartsWith.Trim());
#endregion Comment
#region Content
if (!string.IsNullOrWhiteSpace(Content))
dict.Add("content", Content.Trim());
if (!string.IsNullOrWhiteSpace(ContentContains))
dict.Add("content.contains", ContentContains.Trim());
if (!string.IsNullOrWhiteSpace(ContentEndsWith))
dict.Add("content.endswith", ContentEndsWith.Trim());
if (!string.IsNullOrWhiteSpace(ContentExact))
dict.Add("content.exact", ContentExact.Trim());
if (!string.IsNullOrWhiteSpace(ContentStartsWith))
dict.Add("content.startswith", ContentStartsWith.Trim());
#endregion Content
if (Direction.HasValue && Enum.IsDefined(typeof(SortDirection), Direction.Value))
dict.Add("direction", Direction.Value.GetEnumMemberValue());
if (Match.HasValue && Enum.IsDefined(typeof(FilterMatchType), Match.Value))
dict.Add("match", Match.Value.GetEnumMemberValue());
#region Name
if (!string.IsNullOrWhiteSpace(Name))
dict.Add("name", Name.Trim());
if (!string.IsNullOrWhiteSpace(NameContains))
dict.Add("name.contains", NameContains.Trim());
if (!string.IsNullOrWhiteSpace(NameEndsWith))
dict.Add("name.endswith", NameEndsWith.Trim());
if (!string.IsNullOrWhiteSpace(NameExact))
dict.Add("name.exact", NameExact.Trim());
if (!string.IsNullOrWhiteSpace(NameStartsWith))
dict.Add("name.startswith", NameStartsWith.Trim());
#endregion Name
if (OrderBy.HasValue && Enum.IsDefined(typeof(DnsRecordsOrderBy), OrderBy.Value))
dict.Add("order", OrderBy.Value.GetEnumMemberValue());
if (Page.HasValue && Page.Value >= 1)
dict.Add("page", Page.Value.ToString());
if (PerPage.HasValue && PerPage.Value >= 1 && PerPage.Value <= 5_000_000)
dict.Add("per_page", PerPage.Value.ToString());
if (Proxied.HasValue)
dict.Add("proxied", Proxied.Value.ToString().ToLowerInvariant());
if (!string.IsNullOrWhiteSpace(Search))
dict.Add("search", Search.Trim());
#region Tag
if (!string.IsNullOrWhiteSpace(Tag))
dict.Add("tag", Tag.Trim());
if (!string.IsNullOrWhiteSpace(TagAbsent))
dict.Add("tag.absent", TagAbsent.Trim());
if (!string.IsNullOrWhiteSpace(TagContains))
dict.Add("tag.contains", TagContains.Trim());
if (!string.IsNullOrWhiteSpace(TagEndsWith))
dict.Add("tag.endswith", TagEndsWith.Trim());
if (!string.IsNullOrWhiteSpace(TagExact))
dict.Add("tag.exact", TagExact.Trim());
if (!string.IsNullOrWhiteSpace(TagPresent))
dict.Add("tag.present", TagPresent.Trim());
if (!string.IsNullOrWhiteSpace(TagStartsWith))
dict.Add("tag.startswith", TagStartsWith.Trim());
#endregion Tag
if (TagMatch.HasValue && Enum.IsDefined(typeof(FilterMatchType), TagMatch.Value))
dict.Add("tag_match", TagMatch.Value.GetEnumMemberValue());
if (Type.HasValue && Enum.IsDefined(typeof(DnsRecordType), Type.Value))
dict.Add("type", Type.Value.GetEnumMemberValue());
#pragma warning restore CS8602, CS8604
return dict;
}
}
/// <summary>
/// Possible fields to order DNS records by.
/// </summary>
[JsonConverter(typeof(StringEnumConverter))]
public enum DnsRecordsOrderBy
{
/// <summary>
/// Order by record type.
/// </summary>
[EnumMember(Value = "type")]
Type = 1,
/// <summary>
/// Order by record name.
/// </summary>
[EnumMember(Value = "name")]
Name = 2,
/// <summary>
/// Order by record content.
/// </summary>
[EnumMember(Value = "content")]
Content = 3,
/// <summary>
/// Order by record TTL.
/// </summary>
[EnumMember(Value = "ttl")]
Ttl = 4,
/// <summary>
/// Order by record proxied.
/// </summary>
[EnumMember(Value = "proxied")]
Proxied = 5
}
}

View File

@@ -0,0 +1,23 @@
namespace AMWD.Net.Api.Cloudflare.Dns.Internals
{
internal class InternalBatchRequest
{
[JsonProperty("deletes")]
public IReadOnlyCollection<Identifier>? Deletes { get; set; }
[JsonProperty("patches")]
public IReadOnlyCollection<InternalBatchUpdateRequest>? Patches { get; set; }
[JsonProperty("posts")]
public IReadOnlyCollection<InternalDnsRecordRequest>? Posts { get; set; }
[JsonProperty("puts")]
public IReadOnlyCollection<InternalBatchUpdateRequest>? Puts { get; set; }
}
internal class InternalBatchUpdateRequest : InternalDnsRecordRequest
{
[JsonProperty("id")]
public string? Id { get; set; }
}
}

View File

@@ -0,0 +1,35 @@
namespace AMWD.Net.Api.Cloudflare.Dns.Internals
{
internal class InternalDnsRecordRequest
{
[JsonProperty("comment")]
public string? Comment { get; set; }
[JsonProperty("name")]
public string? Name { get; set; }
[JsonProperty("proxied")]
public bool? Proxied { get; set; }
[JsonProperty("settings")]
public object? Settings { get; set; }
[JsonProperty("tags")]
public IReadOnlyCollection<string>? Tags { get; set; }
[JsonProperty("ttl")]
public int? Ttl { get; set; }
[JsonProperty("content")]
public string? Content { get; set; }
[JsonProperty("type")]
public DnsRecordType Type { get; set; }
[JsonProperty("priority")]
public int? Priority { get; set; }
[JsonProperty("data")]
public object? Data { get; set; }
}
}

View File

@@ -0,0 +1,122 @@
using System.Runtime.Serialization;
using Newtonsoft.Json.Converters;
namespace AMWD.Net.Api.Cloudflare.Dns
{
internal class DNSAnalyticsQuery
{
/// <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.
/// </summary>
[JsonProperty("since")]
public DateTime? Since { get; set; }
/// <summary>
/// Unit of time to group data by.
/// </summary>
[JsonProperty("time_delta")]
public TimeDeltaUnit? TimeDelta { get; set; }
[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 dimensions to sort by, where each dimension may be prefixed
/// by <c>-</c> (descending) or <c>+</c> (ascending).
/// </summary>
[JsonProperty("sort")]
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,120 @@
using Newtonsoft.Json.Linq;
namespace AMWD.Net.Api.Cloudflare.Dns
{
/// <summary>
/// A DNS record.
/// </summary>
public abstract class DnsRecord
{
/// <summary>
/// Initializes a new instance of the <see cref="DnsRecord"/> class.
/// </summary>
/// <param name="name">DNS record name (or @ for the zone apex) in Punycode.</param>
protected DnsRecord(string name)
{
Name = name;
}
/// <summary>
/// DNS record name (or @ for the zone apex) in Punycode.
/// </summary>
[JsonProperty("name")]
public string Name { get; set; }
/// <summary>
/// Record type.
/// </summary>
[JsonProperty("type")]
public DnsRecordType Type { get; protected set; }
/// <summary>
/// Comments or notes about the DNS record.
/// This field has no effect on DNS responses.
/// </summary>
[JsonProperty("comment")]
public string? Comment { get; set; }
/// <summary>
/// DNS record content.
/// </summary>
[JsonProperty("content")]
public string? Content { get; set; }
/// <summary>
/// Whether the record is receiving the performance and security benefits of
/// Cloudflare.
/// </summary>
[JsonProperty("proxied")]
public bool? Proxied { get; set; }
/// <summary>
/// Settings for the DNS record.
/// </summary>
[JsonProperty("settings")]
public DnsRecordSettings? Settings { get; set; }
/// <summary>
/// Custom tags for the DNS record.
/// This field has no effect on DNS responses.
/// </summary>
[JsonProperty("tags")]
public IReadOnlyCollection<string>? Tags { get; set; }
/// <summary>
/// Time To Live (TTL) of the DNS record in seconds.
/// </summary>
/// <remarks>
/// Setting to <c>1</c> means 'automatic'. Value must be between <c>60</c> and <c>86400</c>, with the
/// minimum reduced to <c>30</c> for Enterprise zones.
/// </remarks>
[JsonProperty("ttl")]
public int? TimeToLive { get; set; }
#region Response
/// <summary>
/// Identifier.
/// </summary>
[JsonProperty("id")]
public string? Id { get; set; }
/// <summary>
/// When the record was created.
/// </summary>
[JsonProperty("created_on")]
public DateTime CreatedOn { get; set; }
/// <summary>
/// Extra Cloudflare-specific information about the record.
/// </summary>
[JsonProperty("meta")]
public JToken? Meta { get; set; }
/// <summary>
/// When the record was last modified.
/// </summary>
[JsonProperty("modified_on")]
public DateTime ModifiedOn { get; set; }
/// <summary>
/// Whether the record can be proxied by Cloudflare or not.
/// </summary>
[JsonProperty("proxiable")]
public bool Proxiable { get; set; }
/// <summary>
/// When the record comment was last modified. Omitted if there is no comment.
/// </summary>
[JsonProperty("comment_modified_on")]
public DateTime? CommentModifiedOn { get; set; }
/// <summary>
/// When the record tags were last modified. Omitted if there are no tags.
/// </summary>
[JsonProperty("tags_modified_on")]
public DateTime? TagsModifiedOn { get; set; }
#endregion Response
}
}

View File

@@ -0,0 +1,26 @@
namespace AMWD.Net.Api.Cloudflare.Dns
{
/// <summary>
/// IP Settings for the DNS record.
/// </summary>
public class DnsRecordSettings
{
/// <summary>
/// When enabled, only A records will be generated, and AAAA records will not be
/// created. This setting is intended for exceptional cases. Note that this option
/// only applies to proxied records and it has no effect on whether Cloudflare
/// communicates with the origin using IPv4 or IPv6.
/// </summary>
[JsonProperty("ipv4_only")]
public bool? IPv4Only { get; set; }
/// <summary>
/// When enabled, only AAAA records will be generated, and A records will not be
/// created. This setting is intended for exceptional cases. Note that this option
/// only applies to proxied records and it has no effect on whether Cloudflare
/// communicates with the origin using IPv4 or IPv6.
/// </summary>
[JsonProperty("ipv6_only")]
public bool? IPv6Only { get; set; }
}
}

View File

@@ -0,0 +1,18 @@
namespace AMWD.Net.Api.Cloudflare.Dns
{
/// <summary>
/// IPv6 address record.
/// </summary>
public class AAAARecord : DnsRecord
{
/// <summary>
/// Initializes a new instance of the <see cref="ARecord"/> class.
/// </summary>
/// <param name="name">DNS record name (or @ for the zone apex) in Punycode.</param>
public AAAARecord(string name)
: base(name)
{
Type = DnsRecordType.AAAA;
}
}
}

View File

@@ -0,0 +1,18 @@
namespace AMWD.Net.Api.Cloudflare.Dns
{
/// <summary>
/// Address record.
/// </summary>
public class ARecord : DnsRecord
{
/// <summary>
/// Initializes a new instance of the <see cref="ARecord"/> class.
/// </summary>
/// <param name="name">DNS record name (or @ for the zone apex) in Punycode.</param>
public ARecord(string name)
: base(name)
{
Type = DnsRecordType.A;
}
}
}

View File

@@ -0,0 +1,48 @@
namespace AMWD.Net.Api.Cloudflare.Dns
{
/// <summary>
/// Certification Authority Authorization record.
/// </summary>
public class CAARecord : DnsRecord
{
/// <summary>
/// Initializes a new instance of the <see cref="CAARecord"/> class.
/// </summary>
/// <param name="name">DNS record name (or @ for the zone apex) in Punycode.</param>
public CAARecord(string name)
: base(name)
{
Type = DnsRecordType.CAA;
}
/// <summary>
/// Components of a CAA record.
/// </summary>
[JsonProperty("data")]
public CAARecordData? Data { get; set; }
}
/// <summary>
/// Components of a CAA record.
/// </summary>
public class CAARecordData
{
/// <summary>
/// Flags for the CAA record.
/// </summary>
[JsonProperty("flags")]
public int? Flags { get; set; }
/// <summary>
/// Name of the property controlled by this record (e.g.: issue, issuewild, iodef).
/// </summary>
[JsonProperty("tag")]
public string? Tag { get; set; }
/// <summary>
/// Value of the record. This field's semantics depend on the chosen tag.
/// </summary>
[JsonProperty("value")]
public string? Value { get; set; }
}
}

View File

@@ -0,0 +1,54 @@
namespace AMWD.Net.Api.Cloudflare.Dns
{
/// <summary>
/// Certificate record.
/// </summary>
public class CERTRecord : DnsRecord
{
/// <summary>
/// Initializes a new instance of the <see cref="CERTRecord"/> class.
/// </summary>
/// <param name="name">DNS record name (or @ for the zone apex) in Punycode.</param>
public CERTRecord(string name)
: base(name)
{
Type = DnsRecordType.CERT;
}
/// <summary>
/// Components of a CERT record.
/// </summary>
[JsonProperty("data")]
public CERTRecordData? Data { get; set; }
}
/// <summary>
/// Components of a CERT record.
/// </summary>
public class CERTRecordData
{
/// <summary>
/// Algorithm.
/// </summary>
[JsonProperty("algorithm")]
public int? Algorithm { get; set; }
/// <summary>
/// Certificate.
/// </summary>
[JsonProperty("certificate")]
public string? Certificate { get; set; }
/// <summary>
/// Key tag.
/// </summary>
[JsonProperty("key_tag")]
public int? KeyTag { get; set; }
/// <summary>
/// Type.
/// </summary>
[JsonProperty("type")]
public int? Type { get; set; }
}
}

View File

@@ -0,0 +1,41 @@
namespace AMWD.Net.Api.Cloudflare.Dns
{
/// <summary>
/// Canonical Name record.
/// </summary>
public class CNAMERecord : DnsRecord
{
/// <summary>
/// Initializes a new instance of the <see cref="CNAMERecord"/> class.
/// </summary>
/// <param name="name">DNS record name (or @ for the zone apex) in Punycode.</param>
public CNAMERecord(string name)
: base(name)
{
Type = DnsRecordType.CNAME;
}
/// <summary>
/// Settings for the DNS record.
/// </summary>
[JsonProperty("settings")]
public new CNAMERecordSettings? Settings { get; set; }
}
/// <summary>
/// Settings for the DNS record.
/// </summary>
public class CNAMERecordSettings : DnsRecordSettings
{
/// <summary>
/// If enabled, causes the CNAME record to be resolved externally and the resulting
/// address records (e.g., A and AAAA) to be returned instead of the CNAME record
/// itself.
/// </summary>
/// <remarks>
/// This setting is unavailable for proxied records, since they are always flattened.
/// </remarks>
[JsonProperty("flatten_cname")]
public bool? FlattenCname { get; set; }
}
}

View File

@@ -0,0 +1,54 @@
namespace AMWD.Net.Api.Cloudflare.Dns
{
/// <summary>
/// DNS Key record.
/// </summary>
public class DNSKEYRecord : DnsRecord
{
/// <summary>
/// Initializes a new instance of the <see cref="DNSKEYRecord"/> class.
/// </summary>
/// <param name="name">DNS record name (or @ for the zone apex) in Punycode.</param>
public DNSKEYRecord(string name)
: base(name)
{
Type = DnsRecordType.DNSKEY;
}
/// <summary>
/// Components of a DNSKEY record.
/// </summary>
[JsonProperty("data")]
public DNSKEYRecordData? Data { get; set; }
}
/// <summary>
/// Components of a DNSKEY record.
/// </summary>
public class DNSKEYRecordData
{
/// <summary>
/// Algorithm.
/// </summary>
[JsonProperty("algorithm")]
public int? Algorithm { get; set; }
/// <summary>
/// Flags.
/// </summary>
[JsonProperty("flags")]
public int? Flags { get; set; }
/// <summary>
/// Protocol.
/// </summary>
[JsonProperty("protocol")]
public int? Protocol { get; set; }
/// <summary>
/// Public key.
/// </summary>
[JsonProperty("public_key")]
public string? PublicKey { get; set; }
}
}

View File

@@ -0,0 +1,54 @@
namespace AMWD.Net.Api.Cloudflare.Dns
{
/// <summary>
/// Delegate Signer record.
/// </summary>
public class DSRecord : DnsRecord
{
/// <summary>
/// Initializes a new instance of the <see cref="DSRecord"/> class.
/// </summary>
/// <param name="name">DNS record name (or @ for the zone apex) in Punycode.</param>
public DSRecord(string name)
: base(name)
{
Type = DnsRecordType.DS;
}
/// <summary>
/// Components of a DS record.
/// </summary>
[JsonProperty("data")]
public DSRecordData? Data { get; set; }
}
/// <summary>
/// Components of a DS record.
/// </summary>
public class DSRecordData
{
/// <summary>
/// Algorithm.
/// </summary>
[JsonProperty("algorithm")]
public int? Algorithm { get; set; }
/// <summary>
/// Digest.
/// </summary>
[JsonProperty("digest")]
public string? Digest { get; set; }
/// <summary>
/// Digest type.
/// </summary>
[JsonProperty("digest_type")]
public int? DigestType { get; set; }
/// <summary>
/// Key tag.
/// </summary>
[JsonProperty("key_tag")]
public int? KeyTag { get; set; }
}
}

View File

@@ -0,0 +1,48 @@
namespace AMWD.Net.Api.Cloudflare.Dns
{
/// <summary>
/// HTTPS binding record.
/// </summary>
public class HTTPSRecord : DnsRecord
{
/// <summary>
/// Initializes a new instance of the <see cref="HTTPSRecord"/> class.
/// </summary>
/// <param name="name">DNS record name (or @ for the zone apex) in Punycode.</param>
public HTTPSRecord(string name)
: base(name)
{
Type = DnsRecordType.HTTPS;
}
/// <summary>
/// Components of a HTTPS record.
/// </summary>
[JsonProperty("data")]
public HTTPSRecordData? Data { get; set; }
}
/// <summary>
/// Components of a HTTPS record.
/// </summary>
public class HTTPSRecordData
{
/// <summary>
/// Priority.
/// </summary>
[JsonProperty("priority")]
public int? Priority { get; set; }
/// <summary>
/// Target.
/// </summary>
[JsonProperty("target")]
public string? Target { get; set; }
/// <summary>
/// Value.
/// </summary>
[JsonProperty("value")]
public string? Value { get; set; }
}
}

View File

@@ -0,0 +1,143 @@
using System.Runtime.Serialization;
using Newtonsoft.Json.Converters;
namespace AMWD.Net.Api.Cloudflare.Dns
{
/// <summary>
/// Location record.
/// </summary>
public class LOCRecord : DnsRecord
{
/// <summary>
/// Initializes a new instance of the <see cref="LOCRecord"/> class.
/// </summary>
/// <param name="name">DNS record name (or @ for the zone apex) in Punycode.</param>
public LOCRecord(string name)
: base(name)
{
Type = DnsRecordType.LOC;
}
/// <summary>
/// Components of a LOC record.
/// </summary>
[JsonProperty("data")]
public LOCRecordData? Data { get; set; }
}
/// <summary>
/// Components of a LOC record.
/// </summary>
public class LOCRecordData
{
/// <summary>
/// Altitude of location in meters.
/// </summary>
[JsonProperty("altitude")]
public double? Altitude { get; set; }
/// <summary>
/// Degrees of latitude.
/// </summary>
[JsonProperty("lat_degrees")]
public double? LatitudeDegrees { get; set; }
/// <summary>
/// Latitude direction.
/// </summary>
[JsonProperty("lat_direction")]
public LOCRecordLatitudeDirection? LatitudeDirection { get; set; }
/// <summary>
/// Minutes of latitude.
/// </summary>
[JsonProperty("lat_minutes")]
public double? LatitudeMinutes { get; set; }
/// <summary>
/// Seconds of latitude.
/// </summary>
[JsonProperty("lat_seconds")]
public double? LatitudeSeconds { get; set; }
/// <summary>
/// Degrees of longitude.
/// </summary>
[JsonProperty("long_degrees")]
public double? LongitudeDegrees { get; set; }
/// <summary>
/// Longitude direction.
/// </summary>
[JsonProperty("long_direction")]
public LOCRecordLongitudeDirection? LongitudeDirection { get; set; }
/// <summary>
/// Minutes of longitude.
/// </summary>
[JsonProperty("long_minutes")]
public double? LongitudeMinutes { get; set; }
/// <summary>
/// Seconds of longitude.
/// </summary>
[JsonProperty("long_seconds")]
public double? LongitudeSeconds { get; set; }
/// <summary>
/// Horizontal precision of location.
/// </summary>
[JsonProperty("precision_horz")]
public double? PrecisionHorizontal { get; set; }
/// <summary>
/// Vertical precision of location.
/// </summary>
[JsonProperty("precision_vert")]
public double? PrecisionVertical { get; set; }
/// <summary>
/// Size of location in meters.
/// </summary>
[JsonProperty("size")]
public double? Size { get; set; }
}
/// <summary>
/// Location record latitude direction.
/// </summary>
[JsonConverter(typeof(StringEnumConverter))]
public enum LOCRecordLatitudeDirection
{
/// <summary>
/// North.
/// </summary>
[EnumMember(Value = "N")]
North = 1,
/// <summary>
/// South.
/// </summary>
[EnumMember(Value = "S")]
South = 2
}
/// <summary>
/// Location record longitude direction.
/// </summary>
[JsonConverter(typeof(StringEnumConverter))]
public enum LOCRecordLongitudeDirection
{
/// <summary>
/// East.
/// </summary>
[EnumMember(Value = "E")]
East = 1,
/// <summary>
/// West.
/// </summary>
[EnumMember(Value = "W")]
West = 2
}
}

View File

@@ -0,0 +1,25 @@
namespace AMWD.Net.Api.Cloudflare.Dns
{
/// <summary>
/// Mail Exchange record.
/// </summary>
public class MXRecord : DnsRecord
{
/// <summary>
/// Initializes a new instance of the <see cref="MXRecord"/> class.
/// </summary>
/// <param name="name">DNS record name (or @ for the zone apex) in Punycode.</param>
public MXRecord(string name)
: base(name)
{
Type = DnsRecordType.MX;
}
/// <summary>
/// Required for MX, SRV and URI records; unused by other record types.
/// Records with lower priorities are preferred.
/// </summary>
[JsonProperty("priority")]
public int? Priority { get; set; }
}
}

View File

@@ -0,0 +1,66 @@
namespace AMWD.Net.Api.Cloudflare.Dns
{
/// <summary>
/// Naming authority pointer record.
/// </summary>
public class NAPTRRecord : DnsRecord
{
/// <summary>
/// Initializes a new instance of the <see cref="NAPTRRecord"/> class.
/// </summary>
/// <param name="name">DNS record name (or @ for the zone apex) in Punycode.</param>
public NAPTRRecord(string name)
: base(name)
{
Type = DnsRecordType.NAPTR;
}
/// <summary>
/// Components of a NAPTR record.
/// </summary>
[JsonProperty("data")]
public NAPTRRecordData? Data { get; set; }
}
/// <summary>
/// Components of a NAPTR record.
/// </summary>
public class NAPTRRecordData
{
/// <summary>
/// Flags.
/// </summary>
[JsonProperty("flags")]
public string? Flags { get; set; }
/// <summary>
/// Order.
/// </summary>
[JsonProperty("order")]
public int? Order { get; set; }
/// <summary>
/// Preference.
/// </summary>
[JsonProperty("preference")]
public int? Preference { get; set; }
/// <summary>
/// Regular expression.
/// </summary>
[JsonProperty("regex")]
public string? Regex { get; set; }
/// <summary>
/// Replacement.
/// </summary>
[JsonProperty("replacement")]
public string? Replacement { get; set; }
/// <summary>
/// Service.
/// </summary>
[JsonProperty("service")]
public string? Service { get; set; }
}
}

View File

@@ -0,0 +1,18 @@
namespace AMWD.Net.Api.Cloudflare.Dns
{
/// <summary>
/// Name Server record.
/// </summary>
public class NSRecord : DnsRecord
{
/// <summary>
/// Initializes a new instance of the <see cref="NSRecord"/> class.
/// </summary>
/// <param name="name">DNS record name (or @ for the zone apex) in Punycode.</param>
public NSRecord(string name)
: base(name)
{
Type = DnsRecordType.NS;
}
}
}

View File

@@ -0,0 +1,24 @@
namespace AMWD.Net.Api.Cloudflare.Dns
{
/// <summary>
/// OpenPGP Publi Key record.
/// </summary>
public class OPENPGPKEYRecord : DnsRecord
{
/// <summary>
/// Initializes a new instance of the <see cref="OPENPGPKEYRecord"/> class.
/// </summary>
/// <param name="name">DNS record name (or @ for the zone apex) in Punycode.</param>
public OPENPGPKEYRecord(string name)
: base(name)
{
Type = DnsRecordType.OPENPGPKEY;
}
/// <summary>
/// A single Base64-encoded OpenPGP Transferable Public Key (RFC 4880 Section 11.1).
/// </summary>
[JsonProperty("content")]
public new string? Content { get; set; }
}
}

View File

@@ -0,0 +1,18 @@
namespace AMWD.Net.Api.Cloudflare.Dns
{
/// <summary>
/// PTR resource record.
/// </summary>
public class PTRRecord : DnsRecord
{
/// <summary>
/// Initializes a new instance of the <see cref="PTRRecord"/> class.
/// </summary>
/// <param name="name">DNS record name (or @ for the zone apex) in Punycode.</param>
public PTRRecord(string name)
: base(name)
{
Type = DnsRecordType.PTR;
}
}
}

View File

@@ -0,0 +1,54 @@
namespace AMWD.Net.Api.Cloudflare.Dns
{
/// <summary>
/// S/MIME cert association record.
/// </summary>
public class SMIMEARecord : DnsRecord
{
/// <summary>
/// Initializes a new instance of the <see cref="SMIMEARecord"/> class.
/// </summary>
/// <param name="name">DNS record name (or @ for the zone apex) in Punycode.</param>
public SMIMEARecord(string name)
: base(name)
{
Type = DnsRecordType.SMIMEA;
}
/// <summary>
/// Components of a SMIMEA record.
/// </summary>
[JsonProperty("data")]
public SMIMEARecordData? Data { get; set; }
}
/// <summary>
/// Components of a SMIMEA record.
/// </summary>
public class SMIMEARecordData
{
/// <summary>
/// Certificate.
/// </summary>
[JsonProperty("certificate")]
public string? Certificate { get; set; }
/// <summary>
/// Matching type.
/// </summary>
[JsonProperty("matching_type")]
public int? MatchingType { get; set; }
/// <summary>
/// Selector.
/// </summary>
[JsonProperty("selector")]
public int? Selector { get; set; }
/// <summary>
/// Usage.
/// </summary>
[JsonProperty("usage")]
public int? Usage { get; set; }
}
}

View File

@@ -0,0 +1,55 @@
namespace AMWD.Net.Api.Cloudflare.Dns
{
/// <summary>
/// Service locator record.
/// </summary>
public class SRVRecord : DnsRecord
{
/// <summary>
/// Initializes a new instance of the <see cref="SRVRecord"/> class.
/// </summary>
/// <param name="name">DNS record name (or @ for the zone apex) in Punycode.</param>
public SRVRecord(string name)
: base(name)
{
Type = DnsRecordType.SRV;
}
/// <summary>
/// Components of a SRV record.
/// </summary>
[JsonProperty("data")]
public SRVRecordData? Data { get; set; }
}
/// <summary>
/// Components of a SRV record.
/// </summary>
public class SRVRecordData
{
/// <summary>
/// The port of the service.
/// </summary>
[JsonProperty("port")]
public int? Port { get; set; }
/// <summary>
/// Required for MX, SRV and URI records; unused by other record types.
/// Records with lower priorities are preferred.
/// </summary>
[JsonProperty("priority")]
public int? Priority { get; set; }
/// <summary>
/// A valid hostname.
/// </summary>
[JsonProperty("target")]
public string? Target { get; set; }
/// <summary>
/// The record weight.
/// </summary>
[JsonProperty("weight")]
public int? Weight { get; set; }
}
}

View File

@@ -0,0 +1,48 @@
namespace AMWD.Net.Api.Cloudflare.Dns
{
/// <summary>
/// SSH Public Key Fingerprint record.
/// </summary>
public class SSHFPRecord : DnsRecord
{
/// <summary>
/// Initializes a new instance of the <see cref="SSHFPRecord"/> class.
/// </summary>
/// <param name="name">DNS record name (or @ for the zone apex) in Punycode.</param>
public SSHFPRecord(string name)
: base(name)
{
Type = DnsRecordType.SSHFP;
}
/// <summary>
/// Components of a SSHFP record.
/// </summary>
[JsonProperty("data")]
public SSHFPRecordData? Data { get; set; }
}
/// <summary>
/// Components of a SSHFP record.
/// </summary>
public class SSHFPRecordData
{
/// <summary>
/// Algorithm.
/// </summary>
[JsonProperty("algorithm")]
public int? Algorithm { get; set; }
/// <summary>
/// Fingerprint.
/// </summary>
[JsonProperty("fingerprint")]
public string? Fingerprint { get; set; }
/// <summary>
/// Type.
/// </summary>
[JsonProperty("type")]
public int? Type { get; set; }
}
}

View File

@@ -0,0 +1,48 @@
namespace AMWD.Net.Api.Cloudflare.Dns
{
/// <summary>
/// Service Binding record.
/// </summary>
public class SVCBRecord : DnsRecord
{
/// <summary>
/// Initializes a new instance of the <see cref="SVCBRecord"/> class.
/// </summary>
/// <param name="name">DNS record name (or @ for the zone apex) in Punycode.</param>
public SVCBRecord(string name)
: base(name)
{
Type = DnsRecordType.SVCB;
}
/// <summary>
/// Components of a SVCB record.
/// </summary>
[JsonProperty("data")]
public SVCBRecordData? Data { get; set; }
}
/// <summary>
/// Components of a SVCB record.
/// </summary>
public class SVCBRecordData
{
/// <summary>
/// Priority.
/// </summary>
[JsonProperty("priority")]
public int? Priority { get; set; }
/// <summary>
/// Target.
/// </summary>
[JsonProperty("target")]
public string? Target { get; set; }
/// <summary>
/// Value.
/// </summary>
[JsonProperty("value")]
public string? Value { get; set; }
}
}

View File

@@ -0,0 +1,54 @@
namespace AMWD.Net.Api.Cloudflare.Dns
{
/// <summary>
/// TLSA certificate association record.
/// </summary>
public class TLSARecord : DnsRecord
{
/// <summary>
/// Initializes a new instance of the <see cref="TLSARecord"/> class.
/// </summary>
/// <param name="name">DNS record name (or @ for the zone apex) in Punycode.</param>
public TLSARecord(string name)
: base(name)
{
Type = DnsRecordType.TLSA;
}
/// <summary>
/// Components of a TLSA record.
/// </summary>
[JsonProperty("data")]
public TLSARecordData? Data { get; set; }
}
/// <summary>
/// Components of a TLSA record.
/// </summary>
public class TLSARecordData
{
/// <summary>
/// Certificate.
/// </summary>
[JsonProperty("certificate")]
public string? Certificate { get; set; }
/// <summary>
/// Matching type.
/// </summary>
[JsonProperty("matching_type")]
public int? MatchingType { get; set; }
/// <summary>
/// Selector.
/// </summary>
[JsonProperty("selector")]
public int? Selector { get; set; }
/// <summary>
/// Usage.
/// </summary>
[JsonProperty("usage")]
public int? Usage { get; set; }
}
}

View File

@@ -0,0 +1,29 @@
namespace AMWD.Net.Api.Cloudflare.Dns
{
/// <summary>
/// Text record.
/// </summary>
public class TXTRecord : DnsRecord
{
/// <summary>
/// Initializes a new instance of the <see cref="TXTRecord"/> class.
/// </summary>
/// <param name="name">DNS record name (or @ for the zone apex) in Punycode.</param>
public TXTRecord(string name)
: base(name)
{
Type = DnsRecordType.TXT;
}
/// <summary>
/// Text content for the record. The content must consist of quoted "character
/// strings" (RFC 1035), each with a length of up to 255 bytes. Strings exceeding
/// this allowed maximum length are automatically split.
/// </summary>
/// <remarks>
/// Learn more at <see href="https://www.cloudflare.com/learning/dns/dns-records/dns-txt-record/"/>.
/// </remarks>
[JsonProperty("content")]
public new string? Content { get; set; }
}
}

View File

@@ -0,0 +1,49 @@
namespace AMWD.Net.Api.Cloudflare.Dns
{
/// <summary>
/// Uniform Resource Identifier record.
/// </summary>
public class URIRecord : DnsRecord
{
/// <summary>
/// Initializes a new instance of the <see cref="URIRecord"/> class.
/// </summary>
/// <param name="name">DNS record name (or @ for the zone apex) in Punycode.</param>
public URIRecord(string name)
: base(name)
{
Type = DnsRecordType.URI;
}
/// <summary>
/// Required for MX, SRV and URI records; unused by other record types.
/// Records with lower priorities are preferred.
/// </summary>
[JsonProperty("priority")]
public int? Priority { get; set; }
/// <summary>
/// Components of a URI record.
/// </summary>
[JsonProperty("data")]
public URIRecordData? Data { get; set; }
}
/// <summary>
/// Components of a URI record.
/// </summary>
public class URIRecordData
{
/// <summary>
/// The record content.
/// </summary>
[JsonProperty("target")]
public string? Target { get; set; }
/// <summary>
/// The record weight.
/// </summary>
[JsonProperty("weight")]
public int? Weight { get; set; }
}
}

View File

@@ -11,6 +11,21 @@ This package contains the feature set of the _DNS_ section of the Cloudflare API
- [List Account Custom Nameservers](https://developers.cloudflare.com/api/resources/custom_nameservers/methods/get/) - [List Account Custom Nameservers](https://developers.cloudflare.com/api/resources/custom_nameservers/methods/get/)
### [DNS]
#### [Records]
- [Batch DNS Records](https://developers.cloudflare.com/api/resources/dns/subresources/records/methods/batch/)
- [Create DNS Record](https://developers.cloudflare.com/api/resources/dns/subresources/records/methods/create/)
- [Delete DNS Record](https://developers.cloudflare.com/api/resources/dns/subresources/records/methods/delete/)
- [Update DNS Record](https://developers.cloudflare.com/api/resources/dns/subresources/records/methods/edit/)
- [Export DNS Records](https://developers.cloudflare.com/api/resources/dns/subresources/records/methods/export/)
- [DNS Record Details](https://developers.cloudflare.com/api/resources/dns/subresources/records/methods/get/)
- [Import DNS Records](https://developers.cloudflare.com/api/resources/dns/subresources/records/methods/import/)
- [List DNS Records](https://developers.cloudflare.com/api/resources/dns/subresources/records/methods/list/)
- [Scan DNS Records](https://developers.cloudflare.com/api/resources/dns/subresources/records/methods/scan/)
- [Overwrite DNS Record](https://developers.cloudflare.com/api/resources/dns/subresources/records/methods/update/)
--- ---
@@ -21,3 +36,6 @@ Published under MIT License (see [choose a license])
[choose a license]: https://choosealicense.com/licenses/mit/ [choose a license]: https://choosealicense.com/licenses/mit/
[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/
[Records]: https://developers.cloudflare.com/api/resources/dns/subresources/records/

View File

@@ -0,0 +1,159 @@
namespace AMWD.Net.Api.Cloudflare.Dns
{
/// <summary>
/// Represents a request to manipulate DNS records.
/// </summary>
public class BatchDnsRecordsRequest
{
/// <summary>
/// Initializes a new instance of the <see cref="BatchDnsRecordsRequest"/> class.
/// </summary>
/// <param name="zoneId">The zone identifier.</param>
public BatchDnsRecordsRequest(string zoneId)
{
ZoneId = zoneId;
}
/// <summary>
/// The zone identifier.
/// </summary>
public string ZoneId { get; set; }
/// <summary>
/// The DNS records to delete.
/// </summary>
public IReadOnlyCollection<string>? Deletes { get; set; }
/// <summary>
/// The DNS records to update.
/// </summary>
public IReadOnlyCollection<Patch>? Updates { get; set; }
/// <summary>
/// The DNS records to create.
/// </summary>
public IReadOnlyCollection<Post>? Creates { get; set; }
/// <summary>
/// The DNS records to overwrite.
/// </summary>
public IReadOnlyCollection<Put>? Overwrites { get; set; }
/// <summary>
/// Represents a request to update a DNS record.
/// </summary>
public class Patch : Post
{
/// <summary>
/// Initializes a new instance of the <see cref="Patch"/> class.
/// </summary>
/// <param name="id">The DNS record identifier.</param>
/// <param name="name">DNS record name (or @ for the zone apex) in Punycode.</param>
public Patch(string id, string name)
: base(name)
{
Id = id;
}
/// <summary>
/// The DNS record identifier.
/// </summary>
public string Id { get; set; }
}
/// <summary>
/// Represents a request to create a DNS record.
/// </summary>
public class Post
{
/// <summary>
/// Initializes a new instance of the <see cref="Post"/> class.
/// </summary>
/// <param name="name">DNS record name (or @ for the zone apex) in Punycode.</param>
public Post(string name)
{
Name = name;
}
/// <summary>
/// DNS record name (or @ for the zone apex) in Punycode.
/// </summary>
public string Name { get; set; }
/// <summary>
/// Time To Live (TTL) of the DNS record in seconds.
/// </summary>
/// <remarks>
/// Setting to <c>1</c> means 'automatic'. Value must be between <c>60</c> and <c>86400</c>, with the
/// minimum reduced to <c>30</c> for Enterprise zones.
/// </remarks>
public int? TimeToLive { get; set; }
/// <summary>
/// DNS record type.
/// </summary>
public DnsRecordType Type { get; set; }
/// <summary>
/// Comments or notes about the DNS record.
/// This field has no effect on DNS responses.
/// </summary>
public string? Comment { get; set; }
/// <summary>
/// The content of the DNS record.
/// </summary>
public string? Content { get; set; }
/// <summary>
/// Components of a record.
/// </summary>
public object? Data { get; set; }
/// <summary>
/// Required for MX, SRV and URI records; unused by other record types.
/// Records with lower priorities are preferred.
/// </summary>
public int? Priority { get; set; }
/// <summary>
/// Whether the record is receiving the performance and security benefits of
/// Cloudflare.
/// </summary>
public bool? Proxied { get; set; }
/// <summary>
/// Settings for the DNS record.
/// </summary>
public DnsRecordSettings? Settings { get; set; }
/// <summary>
/// Custom tags for the DNS record.
/// This field has no effect on DNS responses.
/// </summary>
public IReadOnlyCollection<string>? Tags { get; set; }
}
/// <summary>
/// Represents a request to overwrite a DNS record.
/// </summary>
public class Put : Post
{
/// <summary>
/// Initializes a new instance of the <see cref="Put"/> class.
/// </summary>
/// <param name="id">The DNS record identifier.</param>
/// <param name="name">DNS record name (or @ for the zone apex) in Punycode.</param>
public Put(string id, string name)
: base(name)
{
Id = id;
}
/// <summary>
/// The DNS record identifier.
/// </summary>
public string Id { get; set; }
}
}
}

View File

@@ -0,0 +1,82 @@
namespace AMWD.Net.Api.Cloudflare.Dns
{
/// <summary>
/// Represents a request to create a DNS record.
/// </summary>
public class CreateDnsRecordRequest
{
/// <summary>
/// Initializes a new instance of the <see cref="CreateDnsRecordRequest"/> class.
/// </summary>
/// <param name="zoneId">The zone identifier.</param>
/// <param name="name">DNS record name (or @ for the zone apex) in Punycode.</param>
public CreateDnsRecordRequest(string zoneId, string name)
{
ZoneId = zoneId;
Name = name;
}
/// <summary>
/// The zone identifier.
/// </summary>
public string ZoneId { get; set; }
/// <summary>
/// DNS record name (or @ for the zone apex) in Punycode.
/// </summary>
public string Name { get; set; }
/// <summary>
/// Time To Live (TTL) of the DNS record in seconds.
/// </summary>
/// <remarks>
/// Setting to <c>1</c> means 'automatic'. Value must be between <c>60</c> and <c>86400</c>, with the
/// minimum reduced to <c>30</c> for Enterprise zones.
/// </remarks>
public int? TimeToLive { get; set; }
/// <summary>
/// DNS record type.
/// </summary>
public DnsRecordType Type { get; set; }
/// <summary>
/// Comments or notes about the DNS record.
/// This field has no effect on DNS responses.
/// </summary>
public string? Comment { get; set; }
/// <summary>
/// The content of the DNS record.
/// </summary>
public string? Content { get; set; }
/// <summary>
/// Components of a record.
/// </summary>
public object? Data { get; set; }
/// <summary>
/// Required for MX, SRV and URI records; unused by other record types.
/// Records with lower priorities are preferred.
/// </summary>
public int? Priority { get; set; }
/// <summary>
/// Whether the record is receiving the performance and security benefits of
/// Cloudflare.
/// </summary>
public bool? Proxied { get; set; }
/// <summary>
/// Settings for the DNS record.
/// </summary>
public DnsRecordSettings? Settings { get; set; }
/// <summary>
/// Custom tags for the DNS record.
/// This field has no effect on DNS responses.
/// </summary>
public IReadOnlyCollection<string>? Tags { get; set; }
}
}

View File

@@ -0,0 +1,29 @@
namespace AMWD.Net.Api.Cloudflare.Dns
{
/// <summary>
/// Represents a request to delete a DNS record.
/// </summary>
public class DeleteDnsRecordRequest
{
/// <summary>
/// Initializes a new instance of the <see cref="DeleteDnsRecordRequest"/> class.
/// </summary>
/// <param name="zoneId">The zone identifier.</param>
/// <param name="recordId">The DNS record identifier.</param>
public DeleteDnsRecordRequest(string zoneId, string recordId)
{
ZoneId = zoneId;
RecordId = recordId;
}
/// <summary>
/// The zone identifier.
/// </summary>
public string ZoneId { get; set; }
/// <summary>
/// The DNS record identifier.
/// </summary>
public string RecordId { get; set; }
}
}

View File

@@ -0,0 +1,33 @@
namespace AMWD.Net.Api.Cloudflare.Dns
{
/// <summary>
/// Represents a request to import DNS records.
/// </summary>
public class ImportDnsRecordsRequest
{
/// <summary>
/// Initializes a new instance of the <see cref="ImportDnsRecordsRequest"/> class.
/// </summary>
/// <param name="zoneId">The zone identifier.</param>
public ImportDnsRecordsRequest(string zoneId)
{
ZoneId = zoneId;
}
/// <summary>
/// The zone identifier.
/// </summary>
public string ZoneId { get; set; }
/// <summary>
/// BIND config to import.
/// </summary>
public string? File { get; set; }
/// <summary>
/// Whether or not proxiable records should receive the performance and
/// security benefits of Cloudflare.
/// </summary>
public bool? Proxied { get; set; }
}
}

View File

@@ -0,0 +1,25 @@
namespace AMWD.Net.Api.Cloudflare.Dns
{
/// <summary>
/// Represents a request to overwrite a DNS record.
/// </summary>
public class OverwriteDnsRecordRequest : CreateDnsRecordRequest
{
/// <summary>
/// Initializes a new instance of the <see cref="OverwriteDnsRecordRequest"/> class.
/// </summary>
/// <param name="zoneId">The zone identifier.</param>
/// <param name="recordId">The DNS record identifier.</param>
/// <param name="name">DNS record name (or @ for the zone apex) in Punycode.</param>
public OverwriteDnsRecordRequest(string zoneId, string recordId, string name)
: base(zoneId, name)
{
RecordId = recordId;
}
/// <summary>
/// The DNS record identifier.
/// </summary>
public string RecordId { get; set; }
}
}

View File

@@ -0,0 +1,25 @@
namespace AMWD.Net.Api.Cloudflare.Dns
{
/// <summary>
/// Represents a request to update a DNS record.
/// </summary>
public class UpdateDnsRecordRequest : CreateDnsRecordRequest
{
/// <summary>
/// Initializes a new instance of the <see cref="UpdateDnsRecordRequest"/> class.
/// </summary>
/// <param name="zoneId">The zone identifier.</param>
/// <param name="recordId">The DNS record identifier.</param>
/// <param name="name">DNS record name (or @ for the zone apex) in Punycode.</param>
public UpdateDnsRecordRequest(string zoneId, string recordId, string name)
: base(zoneId, name)
{
RecordId = recordId;
}
/// <summary>
/// The DNS record identifier.
/// </summary>
public string RecordId { get; set; }
}
}

View File

@@ -0,0 +1,32 @@
namespace AMWD.Net.Api.Cloudflare.Dns
{
/// <summary>
/// The response for a batch update request.
/// </summary>
public class BatchDnsRecordsResponse
{
/// <summary>
/// The records that were deleted (DELETE).
/// </summary>
[JsonProperty("deletes")]
public IReadOnlyCollection<DnsRecord>? Deletes { get; set; }
/// <summary>
/// The records that were updated (PATCH).
/// </summary>
[JsonProperty("patches")]
public IReadOnlyCollection<DnsRecord>? Updates { get; set; }
/// <summary>
/// The records that were created (POST).
/// </summary>
[JsonProperty("posts")]
public IReadOnlyCollection<DnsRecord>? Creates { get; set; }
/// <summary>
/// The records that were overwritten (PUT).
/// </summary>
[JsonProperty("puts")]
public IReadOnlyCollection<DnsRecord>? Overwrites { get; set; }
}
}

View File

@@ -0,0 +1,20 @@
namespace AMWD.Net.Api.Cloudflare.Dns
{
/// <summary>
/// The response for a DNS record import.
/// </summary>
public class RecordImportResponse
{
/// <summary>
/// Number of DNS records added.
/// </summary>
[JsonProperty("recs_added")]
public int? RecordsAdded { get; set; }
/// <summary>
/// Total number of DNS records parsed.
/// </summary>
[JsonProperty("total_records_parsed")]
public int? TotalRecordsParsed { get; set; }
}
}

View File

@@ -0,0 +1,20 @@
namespace AMWD.Net.Api.Cloudflare.Dns
{
/// <summary>
/// The response for a DNS record scan.
/// </summary>
public class RecordScanResponse
{
/// <summary>
/// Number of DNS records added.
/// </summary>
[JsonProperty("recs_added")]
public int? RecordsAdded { get; set; }
/// <summary>
/// Total number of DNS records parsed.
/// </summary>
[JsonProperty("total_records_parsed")]
public int? TotalRecordsParsed { get; set; }
}
}

View File

@@ -20,12 +20,12 @@ This package contains the feature set of the _Domain/Zone Management_ section of
- [List Zones](https://developers.cloudflare.com/api/resources/zones/methods/list/) - [List Zones](https://developers.cloudflare.com/api/resources/zones/methods/list/)
##### [Activation Check] #### [Activation Check]
- [Rerun The Activation Check](https://developers.cloudflare.com/api/resources/zones/subresources/activation_check/methods/trigger/) - [Rerun The Activation Check](https://developers.cloudflare.com/api/resources/zones/subresources/activation_check/methods/trigger/)
##### [Holds] #### [Holds]
- [Create Zone Hold](https://developers.cloudflare.com/api/resources/zones/subresources/holds/methods/create/) - [Create Zone Hold](https://developers.cloudflare.com/api/resources/zones/subresources/holds/methods/create/)
- [Remove Zone Hold](https://developers.cloudflare.com/api/resources/zones/subresources/holds/methods/delete/) - [Remove Zone Hold](https://developers.cloudflare.com/api/resources/zones/subresources/holds/methods/delete/)
@@ -33,18 +33,18 @@ This package contains the feature set of the _Domain/Zone Management_ section of
- [Get Zone Hold](https://developers.cloudflare.com/api/resources/zones/subresources/holds/methods/get/) - [Get Zone Hold](https://developers.cloudflare.com/api/resources/zones/subresources/holds/methods/get/)
##### [Plans] #### [Plans]
- [Available Plan Details](https://developers.cloudflare.com/api/resources/zones/subresources/plans/methods/get/) - [Available Plan Details](https://developers.cloudflare.com/api/resources/zones/subresources/plans/methods/get/)
- [List Available Plans](https://developers.cloudflare.com/api/resources/zones/subresources/plans/methods/list/) - [List Available Plans](https://developers.cloudflare.com/api/resources/zones/subresources/plans/methods/list/)
##### [Rate Plans] #### [Rate Plans]
- [List Available Rate Plans](https://developers.cloudflare.com/api/resources/zones/subresources/rate_plans/methods/get/) - [List Available Rate Plans](https://developers.cloudflare.com/api/resources/zones/subresources/rate_plans/methods/get/)
##### [Settings] #### [Settings]
- **DEPRECATED** [Edit Multiple Zone Settings](https://developers.cloudflare.com/api/resources/zones/subresources/settings/methods/bulk_edit/) - **DEPRECATED** [Edit Multiple Zone Settings](https://developers.cloudflare.com/api/resources/zones/subresources/settings/methods/bulk_edit/)
- [Edit Zone Setting](https://developers.cloudflare.com/api/resources/zones/subresources/settings/methods/edit/) - [Edit Zone Setting](https://developers.cloudflare.com/api/resources/zones/subresources/settings/methods/edit/)
@@ -52,7 +52,7 @@ This package contains the feature set of the _Domain/Zone Management_ section of
- **DEPRECATED** [Get All Zone Settings](https://developers.cloudflare.com/api/resources/zones/subresources/settings/methods/list/) - **DEPRECATED** [Get All Zone Settings](https://developers.cloudflare.com/api/resources/zones/subresources/settings/methods/list/)
##### [Subscriptions] #### [Subscriptions]
- [Create Zone Subscription](https://developers.cloudflare.com/api/resources/zones/subresources/subscriptions/methods/create/) - [Create Zone Subscription](https://developers.cloudflare.com/api/resources/zones/subresources/subscriptions/methods/create/)
- [Zone Subscription Details](https://developers.cloudflare.com/api/resources/zones/subresources/subscriptions/methods/get/) - [Zone Subscription Details](https://developers.cloudflare.com/api/resources/zones/subresources/subscriptions/methods/get/)

View File

@@ -0,0 +1,111 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AMWD.Net.Api.Cloudflare;
using AMWD.Net.Api.Cloudflare.Dns;
using AMWD.Net.Api.Cloudflare.Dns.Internals;
using Moq;
namespace Cloudflare.Dns.Tests.DnsRecordsExtensions
{
[TestClass]
public class BatchDnsRecordsTest
{
private const string ZoneId = "023e105f4ecef8ad9ca31a8372d0c353";
private const string RecordId = "023e105f4ecef8ad9ca31a8372d0c355";
private const string DomainName = "example.com";
private const string IpContent = "96.7.128.175";
private Mock<ICloudflareClient> _clientMock;
private List<(string RequestPath, InternalBatchRequest Request, IQueryParameterFilter QueryFilter)> _callbacks;
private CloudflareResponse<BatchDnsRecordsResponse> _response;
private BatchDnsRecordsRequest _request;
[TestInitialize]
public void Initialize()
{
_callbacks = [];
_response = new CloudflareResponse<BatchDnsRecordsResponse>
{
Success = true,
Messages = [
new ResponseInfo(1000, "Message 1")
],
Errors = [
new ResponseInfo(1000, "Error 1")
],
Result = new BatchDnsRecordsResponse()
};
_request = new BatchDnsRecordsRequest(ZoneId)
{
Deletes = [RecordId],
Updates = [new BatchDnsRecordsRequest.Patch(RecordId, DomainName) { Type = DnsRecordType.A, Content = IpContent }],
Overwrites = [new BatchDnsRecordsRequest.Put(RecordId, DomainName) { Type = DnsRecordType.A, Content = IpContent }],
Creates = [new BatchDnsRecordsRequest.Post(DomainName) { Type = DnsRecordType.A, Content = IpContent }],
};
}
[TestMethod]
public async Task ShouldExecuteBatchDnsRecords()
{
// Arrange
var client = GetClient();
// Act
var response = await client.BatchDnsRecords(_request);
// Assert
Assert.IsNotNull(response);
Assert.IsTrue(response.Success);
Assert.AreEqual(_response.Result, response.Result);
Assert.AreEqual(1, _callbacks.Count);
var callback = _callbacks.First();
Assert.AreEqual($"/zones/{ZoneId}/dns_records/batch", callback.RequestPath);
Assert.IsNull(callback.QueryFilter);
Assert.IsNotNull(callback.Request);
Assert.AreEqual(1, callback.Request.Deletes.Count);
Assert.AreEqual(RecordId, callback.Request.Deletes.First().Id);
Assert.AreEqual(1, callback.Request.Patches.Count);
var patch = callback.Request.Patches.First();
Assert.AreEqual(RecordId, patch.Id);
Assert.AreEqual(DomainName, patch.Name);
Assert.AreEqual(DnsRecordType.A, patch.Type);
Assert.AreEqual(IpContent, patch.Content);
Assert.AreEqual(1, callback.Request.Puts.Count);
var put = callback.Request.Puts.First();
Assert.AreEqual(RecordId, put.Id);
Assert.AreEqual(DomainName, put.Name);
Assert.AreEqual(DnsRecordType.A, put.Type);
Assert.AreEqual(IpContent, put.Content);
Assert.AreEqual(1, callback.Request.Posts.Count);
var post = callback.Request.Posts.First();
Assert.AreEqual(DomainName, post.Name);
Assert.AreEqual(DnsRecordType.A, post.Type);
Assert.AreEqual(IpContent, post.Content);
_clientMock.Verify(m => m.PostAsync<BatchDnsRecordsResponse, InternalBatchRequest>($"/zones/{ZoneId}/dns_records/batch", It.IsAny<InternalBatchRequest>(), null, It.IsAny<CancellationToken>()), Times.Once);
_clientMock.VerifyNoOtherCalls();
}
private ICloudflareClient GetClient()
{
_clientMock = new Mock<ICloudflareClient>();
_clientMock
.Setup(m => m.PostAsync<BatchDnsRecordsResponse, InternalBatchRequest>(It.IsAny<string>(), It.IsAny<InternalBatchRequest>(), It.IsAny<IQueryParameterFilter>(), It.IsAny<CancellationToken>()))
.Callback<string, InternalBatchRequest, IQueryParameterFilter, CancellationToken>((requestPath, request, queryFilter, _) => _callbacks.Add((requestPath, request, queryFilter)))
.ReturnsAsync(() => _response);
return _clientMock.Object;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,76 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AMWD.Net.Api.Cloudflare;
using AMWD.Net.Api.Cloudflare.Dns;
using Moq;
namespace Cloudflare.Dns.Tests.DnsRecordsExtensions
{
[TestClass]
public class DeleteDnsRecordTest
{
private const string ZoneId = "023e105f4ecef8ad9ca31a8372d0c353";
private const string RecordId = "023e105f4ecef8ad9ca31a8372d0c355";
private Mock<ICloudflareClient> _clientMock;
private CloudflareResponse<Identifier> _response;
private List<(string RequestPath, IQueryParameterFilter QueryFilter)> _callbacks;
[TestInitialize]
public void Initialize()
{
_callbacks = [];
_response = new CloudflareResponse<Identifier>
{
Success = true,
Messages = [
new ResponseInfo(1000, "Message 1")
],
Errors = [
new ResponseInfo(1000, "Error 1")
],
Result = new Identifier
{
Id = RecordId,
}
};
}
[TestMethod]
public async Task ShouldDeleteDnsRecord()
{
// Arrange
var client = GetClient();
// Act
var response = await client.DeleteDnsRecord(ZoneId, RecordId);
// Assert
Assert.IsNotNull(response);
Assert.IsTrue(response.Success);
Assert.AreEqual(_response.Result, response.Result);
Assert.AreEqual(1, _callbacks.Count);
var callback = _callbacks.First();
Assert.AreEqual($"/zones/{ZoneId}/dns_records/{RecordId}", callback.RequestPath);
Assert.IsNull(callback.QueryFilter);
_clientMock.Verify(m => m.DeleteAsync<Identifier>($"/zones/{ZoneId}/dns_records/{RecordId}", null, It.IsAny<CancellationToken>()), Times.Once);
_clientMock.VerifyNoOtherCalls();
}
private ICloudflareClient GetClient()
{
_clientMock = new Mock<ICloudflareClient>();
_clientMock
.Setup(m => m.DeleteAsync<Identifier>(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,87 @@
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.DnsRecordsExtensions
{
[TestClass]
public class DnsRecordDetailsTest
{
private const string ZoneId = "023e105f4ecef8ad9ca31a8372d0c353";
private const string RecordId = "023e105f4ecef8ad9ca31a8372d0c355";
private Mock<ICloudflareClient> _clientMock;
private CloudflareResponse<DnsRecord> _response;
private List<(string RequestPath, IQueryParameterFilter QueryFilter)> _callbacks;
[TestInitialize]
public void Initialize()
{
_callbacks = [];
_response = new CloudflareResponse<DnsRecord>
{
Success = true,
Messages = [
new ResponseInfo(1000, "Message 1")
],
Errors = [
new ResponseInfo(1000, "Error 1")
],
Result = new ARecord("example.com")
{
Id = RecordId,
Content = "96.7.128.175",
Proxiable = true,
Proxied = true,
TimeToLive = 1,
Settings = new(),
Comment = "Domain verification record",
Tags = [],
CreatedOn = DateTime.Parse("2014-01-01T05:20:00.12345Z"),
ModifiedOn = DateTime.Parse("2014-01-01T05:20:00.12345Z"),
CommentModifiedOn = DateTime.Parse("2024-01-01T05:20:00.12345Z"),
TagsModifiedOn = DateTime.Parse("2025-01-01T05:20:00.12345Z"),
}
};
}
[TestMethod]
public async Task ShouldDeleteDnsRecord()
{
// Arrange
var client = GetClient();
// Act
var response = await client.DnsRecordDetails(ZoneId, RecordId);
// Assert
Assert.IsNotNull(response);
Assert.IsTrue(response.Success);
Assert.AreEqual(_response.Result, response.Result);
Assert.AreEqual(1, _callbacks.Count);
var callback = _callbacks.First();
Assert.AreEqual($"/zones/{ZoneId}/dns_records/{RecordId}", callback.RequestPath);
Assert.IsNull(callback.QueryFilter);
_clientMock.Verify(m => m.GetAsync<DnsRecord>($"/zones/{ZoneId}/dns_records/{RecordId}", null, It.IsAny<CancellationToken>()), Times.Once);
_clientMock.VerifyNoOtherCalls();
}
private ICloudflareClient GetClient()
{
_clientMock = new Mock<ICloudflareClient>();
_clientMock
.Setup(m => m.GetAsync<DnsRecord>(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,66 @@
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.DnsRecordsExtensions
{
[TestClass]
public class ExportDnsRecordsTest
{
private const string ZoneId = "023e105f4ecef8ad9ca31a8372d0c353";
private Mock<ICloudflareClient> _clientMock;
private CloudflareResponse<string> _response;
private List<(string RequestPath, IQueryParameterFilter QueryFilter)> _callbacks;
[TestInitialize]
public void Initialize()
{
_callbacks = [];
_response = new CloudflareResponse<string>
{
Success = true,
Result = "This is my BIND export file."
};
}
[TestMethod]
public async Task ShouldExportZoneDnsRecords()
{
// Arrange
var client = GetClient();
// Act
var response = await client.ExportDnsRecords(ZoneId);
// Assert
Assert.IsNotNull(response);
Assert.IsTrue(response.Success);
Assert.AreEqual(_response.Result, response.Result);
Assert.AreEqual(1, _callbacks.Count);
var callback = _callbacks.First();
Assert.AreEqual($"/zones/{ZoneId}/dns_records/export", callback.RequestPath);
Assert.IsNull(callback.QueryFilter);
_clientMock.Verify(m => m.GetAsync<string>($"/zones/{ZoneId}/dns_records/export", null, It.IsAny<CancellationToken>()), Times.Once);
_clientMock.VerifyNoOtherCalls();
}
private ICloudflareClient GetClient()
{
_clientMock = new Mock<ICloudflareClient>();
_clientMock
.Setup(m => m.GetAsync<string>(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,239 @@
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using AMWD.Net.Api.Cloudflare;
using AMWD.Net.Api.Cloudflare.Dns;
using Moq;
namespace Cloudflare.Dns.Tests.DnsRecordsExtensions
{
[TestClass]
public class ImportDnsRecordsTest
{
private const string ZoneId = "023e105f4ecef8ad9ca31a8372d0c353";
private const string BindConfigContent = "www.example.com. 300 IN A 127.0.0.1";
private Mock<ICloudflareClient> _clientMock;
private List<(string RequestPath, MultipartFormDataContent Request, IQueryParameterFilter QueryFilter)> _callbacks;
private CloudflareResponse<RecordImportResponse> _response;
private ImportDnsRecordsRequest _request;
[TestInitialize]
public void Initialize()
{
_callbacks = [];
_response = new CloudflareResponse<RecordImportResponse>
{
Success = true,
Messages = [
new ResponseInfo(1000, "Message 1")
],
Errors = [
new ResponseInfo(1000, "Error 1")
],
Result = new RecordImportResponse
{
RecordsAdded = 5,
TotalRecordsParsed = 6,
}
};
_request = new ImportDnsRecordsRequest(ZoneId)
{
File = BindConfigContent
};
}
[TestMethod]
public async Task ShouldImportDnsRecordsFromString()
{
// Arrange
var client = GetClient();
// Act
var response = await client.ImportDnsRecords(_request);
// Assert
Assert.IsNotNull(response);
Assert.IsTrue(response.Success);
Assert.AreEqual(_response.Result, response.Result);
Assert.AreEqual(1, _callbacks.Count);
var callback = _callbacks.First();
Assert.AreEqual($"/zones/{ZoneId}/dns_records/import", callback.RequestPath);
Assert.IsNotNull(callback.Request);
Assert.AreEqual(1, callback.Request.Count());
var part = callback.Request.First();
Assert.AreEqual("file", part.Headers.ContentDisposition.Name);
Assert.IsInstanceOfType<ByteArrayContent>(part);
Assert.AreEqual(BindConfigContent, await part.ReadAsStringAsync());
_clientMock.Verify(m => m.PostAsync<RecordImportResponse, MultipartFormDataContent>($"/zones/{ZoneId}/dns_records/import", It.IsAny<MultipartFormDataContent>(), It.IsAny<IQueryParameterFilter>(), It.IsAny<CancellationToken>()), Times.Once);
_clientMock.VerifyNoOtherCalls();
}
[TestMethod]
public async Task ShouldImportDnsRecordsFromFile()
{
// Arrange
string file = Path.GetTempFileName();
try
{
File.WriteAllText(file, BindConfigContent);
_request.File = file;
var client = GetClient();
// Act
var response = await client.ImportDnsRecords(_request);
// Assert
Assert.IsNotNull(response);
Assert.IsTrue(response.Success);
Assert.AreEqual(_response.Result, response.Result);
Assert.AreEqual(1, _callbacks.Count);
var callback = _callbacks.First();
Assert.AreEqual($"/zones/{ZoneId}/dns_records/import", callback.RequestPath);
Assert.IsNotNull(callback.Request);
Assert.AreEqual(1, callback.Request.Count());
var part = callback.Request.First();
Assert.AreEqual("file", part.Headers.ContentDisposition.Name);
Assert.IsInstanceOfType<ByteArrayContent>(part);
Assert.AreEqual(BindConfigContent, await part.ReadAsStringAsync());
_clientMock.Verify(m => m.PostAsync<RecordImportResponse, MultipartFormDataContent>($"/zones/{ZoneId}/dns_records/import", It.IsAny<MultipartFormDataContent>(), It.IsAny<IQueryParameterFilter>(), It.IsAny<CancellationToken>()), Times.Once);
_clientMock.VerifyNoOtherCalls();
}
finally
{
File.Delete(file);
}
}
[DataTestMethod]
[DataRow(true)]
[DataRow(false)]
public async Task ShouldImportDnsRecordsFromStringWithProxied(bool proxied)
{
// Arrange
_request.Proxied = proxied;
var client = GetClient();
// Act
var response = await client.ImportDnsRecords(_request);
// Assert
Assert.IsNotNull(response);
Assert.IsTrue(response.Success);
Assert.AreEqual(_response.Result, response.Result);
Assert.AreEqual(1, _callbacks.Count);
var callback = _callbacks.First();
Assert.AreEqual($"/zones/{ZoneId}/dns_records/import", callback.RequestPath);
Assert.IsNotNull(callback.Request);
Assert.AreEqual(2, callback.Request.Count());
var part = callback.Request.First();
Assert.AreEqual("proxied", part.Headers.ContentDisposition.Name);
Assert.IsInstanceOfType<StringContent>(part);
Assert.AreEqual(proxied.ToString().ToLower(), await part.ReadAsStringAsync());
part = callback.Request.Last();
Assert.AreEqual("file", part.Headers.ContentDisposition.Name);
Assert.IsInstanceOfType<ByteArrayContent>(part);
Assert.AreEqual(BindConfigContent, await part.ReadAsStringAsync());
_clientMock.Verify(m => m.PostAsync<RecordImportResponse, MultipartFormDataContent>($"/zones/{ZoneId}/dns_records/import", It.IsAny<MultipartFormDataContent>(), It.IsAny<IQueryParameterFilter>(), It.IsAny<CancellationToken>()), Times.Once);
_clientMock.VerifyNoOtherCalls();
}
[DataTestMethod]
[DataRow(true)]
[DataRow(false)]
public async Task ShouldImportDnsRecordsFromFileWithProxied(bool proxied)
{
// Arrange
string file = Path.GetTempFileName();
try
{
File.WriteAllText(file, BindConfigContent);
_request.File = file;
_request.Proxied = proxied;
var client = GetClient();
// Act
var response = await client.ImportDnsRecords(_request);
// Assert
Assert.IsNotNull(response);
Assert.IsTrue(response.Success);
Assert.AreEqual(_response.Result, response.Result);
Assert.AreEqual(1, _callbacks.Count);
var callback = _callbacks.First();
Assert.AreEqual($"/zones/{ZoneId}/dns_records/import", callback.RequestPath);
Assert.IsNotNull(callback.Request);
Assert.AreEqual(2, callback.Request.Count());
var part = callback.Request.First();
Assert.AreEqual("proxied", part.Headers.ContentDisposition.Name);
Assert.IsInstanceOfType<StringContent>(part);
Assert.AreEqual(proxied.ToString().ToLower(), await part.ReadAsStringAsync());
part = callback.Request.Last();
Assert.AreEqual("file", part.Headers.ContentDisposition.Name);
Assert.IsInstanceOfType<ByteArrayContent>(part);
Assert.AreEqual(BindConfigContent, await part.ReadAsStringAsync());
_clientMock.Verify(m => m.PostAsync<RecordImportResponse, MultipartFormDataContent>($"/zones/{ZoneId}/dns_records/import", It.IsAny<MultipartFormDataContent>(), It.IsAny<IQueryParameterFilter>(), It.IsAny<CancellationToken>()), Times.Once);
_clientMock.VerifyNoOtherCalls();
}
finally
{
File.Delete(file);
}
}
[DataTestMethod]
[DataRow(null)]
[DataRow("")]
[DataRow(" ")]
[ExpectedException(typeof(ArgumentNullException))]
public async Task ShouldThrowArgumentNullExceptionForFile(string file)
{
// Arrange
_request.File = file;
var client = GetClient();
// Act
await client.ImportDnsRecords(_request);
// Assert - ArgumentNullException
}
private ICloudflareClient GetClient()
{
_clientMock = new Mock<ICloudflareClient>();
_clientMock
.Setup(m => m.PostAsync<RecordImportResponse, MultipartFormDataContent>(It.IsAny<string>(), It.IsAny<MultipartFormDataContent>(), It.IsAny<IQueryParameterFilter>(), It.IsAny<CancellationToken>()))
.Callback<string, MultipartFormDataContent, IQueryParameterFilter, CancellationToken>((requestPath, request, queryFilter, _) => _callbacks.Add((requestPath, request, queryFilter)))
.ReturnsAsync(() => _response);
return _clientMock.Object;
}
}
}

View File

@@ -0,0 +1,813 @@
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.DnsRecordsExtensions
{
[TestClass]
public class ListDnsRecordsTest
{
private const string ZoneId = "023e105f4ecef8ad9ca31a8372d0c353";
private Mock<ICloudflareClient> _clientMock;
private CloudflareResponse<IReadOnlyCollection<DnsRecord>> _response;
private List<(string RequestPath, IQueryParameterFilter QueryFilter)> _callbacks;
[TestInitialize]
public void Initialize()
{
_callbacks = [];
_response = new CloudflareResponse<IReadOnlyCollection<DnsRecord>>
{
Success = true,
Messages = [
new ResponseInfo(1000, "Message 1")
],
Errors = [
new ResponseInfo(1000, "Error 1")
],
ResultInfo = new PaginationInfo
{
Count = 1,
Page = 1,
PerPage = 20,
TotalCount = 2000,
TotalPages = 100,
},
Result = [
new ARecord("example.com")
{
Id = "023e105f4ecef8ad9ca31a8372d0c353",
Content = "96.7.128.175",
Proxiable = true,
Proxied = true,
TimeToLive = 1,
Settings = new(),
Comment = "Domain verification record",
Tags = [],
CreatedOn = DateTime.Parse("2014-01-01T05:20:00.12345Z"),
ModifiedOn = DateTime.Parse("2014-01-01T05:20:00.12345Z"),
CommentModifiedOn = DateTime.Parse("2024-01-01T05:20:00.12345Z"),
TagsModifiedOn = DateTime.Parse("2025-01-01T05:20:00.12345Z"),
},
new AAAARecord("example.com")
{
Id = "023e105f4ecef8ad9ca31a8372d0c355",
Content = "2600:1408:ec00:36::1736:7f31",
Proxiable = true,
Proxied = true,
TimeToLive = 1,
Settings = new(),
Comment = "Domain verification record",
Tags = [],
CreatedOn = DateTime.Parse("2014-01-01T05:20:00.12345Z"),
ModifiedOn = DateTime.Parse("2014-01-01T05:20:00.12345Z"),
CommentModifiedOn = DateTime.Parse("2024-01-01T05:20:00.12345Z"),
TagsModifiedOn = DateTime.Parse("2025-01-01T05:20:00.12345Z"),
}
]
};
}
[TestMethod]
public async Task ShouldListDnsRecords()
{
// Arrange
var client = GetClient();
// Act
var response = await client.ListDnsRecords(ZoneId);
// Assert
Assert.IsNotNull(response);
Assert.IsTrue(response.Success);
Assert.AreEqual(_response.Result, response.Result);
Assert.AreEqual(1, _callbacks.Count);
var callback = _callbacks.First();
Assert.AreEqual($"/zones/{ZoneId}/dns_records", callback.RequestPath);
Assert.IsNull(callback.QueryFilter);
_clientMock.Verify(m => m.GetAsync<IReadOnlyCollection<DnsRecord>>($"/zones/{ZoneId}/dns_records", null, It.IsAny<CancellationToken>()), Times.Once);
_clientMock.VerifyNoOtherCalls();
}
[TestMethod]
public async Task ShouldListDnsRecordsWithFilter()
{
// Arrange
var filter = new ListDnsRecordsFilter
{
Name = "example.com"
};
var client = GetClient();
// Act
var response = await client.ListDnsRecords(ZoneId, filter);
// Assert
Assert.IsNotNull(response);
Assert.IsTrue(response.Success);
Assert.AreEqual(_response.Result, response.Result);
Assert.AreEqual(1, _callbacks.Count);
var callback = _callbacks.First();
Assert.AreEqual($"/zones/{ZoneId}/dns_records", callback.RequestPath);
Assert.IsNotNull(callback.QueryFilter);
Assert.IsInstanceOfType<ListDnsRecordsFilter>(callback.QueryFilter);
Assert.AreEqual("example.com", ((ListDnsRecordsFilter)callback.QueryFilter).Name);
_clientMock.Verify(m => m.GetAsync<IReadOnlyCollection<DnsRecord>>($"/zones/{ZoneId}/dns_records", filter, It.IsAny<CancellationToken>()), Times.Once);
_clientMock.VerifyNoOtherCalls();
}
[TestMethod]
public void ShouldReturnEmptyParameterList()
{
// Arrange
var filter = new ListDnsRecordsFilter();
// Act
var dict = filter.GetQueryParameters();
// Assert
Assert.IsNotNull(dict);
Assert.AreEqual(0, dict.Count);
}
[TestMethod]
public void ShouldReturnFullParameterList()
{
// Arrange
var filter = new ListDnsRecordsFilter
{
Comment = "Hello World",
CommentAbsent = true,
CommentContains = "lo Wor",
CommentEndsWith = "ld",
CommentExact = "Hello World",
CommentPresent = true,
CommentStartsWith = "He",
Content = "127.0.0.1",
ContentContains = "7.0.0",
ContentEndsWith = "0.1",
ContentExact = "127.0.0.1",
ContentStartsWith = "127",
Direction = SortDirection.Descending,
Match = FilterMatchType.All,
Name = "example.com",
NameContains = "ample",
NameEndsWith = ".com",
NameExact = "example.com",
NameStartsWith = "ex",
OrderBy = DnsRecordsOrderBy.Name,
Page = 2,
PerPage = 5,
Proxied = true,
Search = "Some Search",
Tag = "team:DNS",
TagAbsent = "important",
TagContains = "greeting:ello",
TagEndsWith = "greeting:rld",
TagExact = "greeting:Hello World",
TagPresent = "important",
TagStartsWith = "greeting:Hel",
TagMatch = FilterMatchType.Any,
Type = DnsRecordType.A,
};
// Act
var dict = filter.GetQueryParameters();
// Assert
Assert.IsNotNull(dict);
Assert.AreEqual(33, dict.Count);
Assert.IsTrue(dict.ContainsKey("comment"));
Assert.IsTrue(dict.ContainsKey("comment.absent"));
Assert.IsTrue(dict.ContainsKey("comment.contains"));
Assert.IsTrue(dict.ContainsKey("comment.endswith"));
Assert.IsTrue(dict.ContainsKey("comment.exact"));
Assert.IsTrue(dict.ContainsKey("comment.present"));
Assert.IsTrue(dict.ContainsKey("comment.startswith"));
Assert.IsTrue(dict.ContainsKey("content"));
Assert.IsTrue(dict.ContainsKey("content.contains"));
Assert.IsTrue(dict.ContainsKey("content.endswith"));
Assert.IsTrue(dict.ContainsKey("content.exact"));
Assert.IsTrue(dict.ContainsKey("content.startswith"));
Assert.IsTrue(dict.ContainsKey("direction"));
Assert.IsTrue(dict.ContainsKey("match"));
Assert.IsTrue(dict.ContainsKey("name"));
Assert.IsTrue(dict.ContainsKey("name.contains"));
Assert.IsTrue(dict.ContainsKey("name.endswith"));
Assert.IsTrue(dict.ContainsKey("name.exact"));
Assert.IsTrue(dict.ContainsKey("name.startswith"));
Assert.IsTrue(dict.ContainsKey("order"));
Assert.IsTrue(dict.ContainsKey("page"));
Assert.IsTrue(dict.ContainsKey("per_page"));
Assert.IsTrue(dict.ContainsKey("proxied"));
Assert.IsTrue(dict.ContainsKey("search"));
Assert.IsTrue(dict.ContainsKey("tag"));
Assert.IsTrue(dict.ContainsKey("tag.absent"));
Assert.IsTrue(dict.ContainsKey("tag.contains"));
Assert.IsTrue(dict.ContainsKey("tag.endswith"));
Assert.IsTrue(dict.ContainsKey("tag.exact"));
Assert.IsTrue(dict.ContainsKey("tag.present"));
Assert.IsTrue(dict.ContainsKey("tag.startswith"));
Assert.IsTrue(dict.ContainsKey("tag_match"));
Assert.IsTrue(dict.ContainsKey("type"));
Assert.AreEqual("Hello World", dict["comment"]);
Assert.AreEqual("true", dict["comment.absent"]);
Assert.AreEqual("lo Wor", dict["comment.contains"]);
Assert.AreEqual("ld", dict["comment.endswith"]);
Assert.AreEqual("Hello World", dict["comment.exact"]);
Assert.AreEqual("true", dict["comment.present"]);
Assert.AreEqual("He", dict["comment.startswith"]);
Assert.AreEqual("127.0.0.1", dict["content"]);
Assert.AreEqual("7.0.0", dict["content.contains"]);
Assert.AreEqual("0.1", dict["content.endswith"]);
Assert.AreEqual("127.0.0.1", dict["content.exact"]);
Assert.AreEqual("127", dict["content.startswith"]);
Assert.AreEqual("desc", dict["direction"]);
Assert.AreEqual("all", dict["match"]);
Assert.AreEqual("example.com", dict["name"]);
Assert.AreEqual("ample", dict["name.contains"]);
Assert.AreEqual(".com", dict["name.endswith"]);
Assert.AreEqual("example.com", dict["name.exact"]);
Assert.AreEqual("ex", dict["name.startswith"]);
Assert.AreEqual("name", dict["order"]);
Assert.AreEqual("2", dict["page"]);
Assert.AreEqual("5", dict["per_page"]);
Assert.AreEqual("true", dict["proxied"]);
Assert.AreEqual("Some Search", dict["search"]);
Assert.AreEqual("team:DNS", dict["tag"]);
Assert.AreEqual("important", dict["tag.absent"]);
Assert.AreEqual("greeting:ello", dict["tag.contains"]);
Assert.AreEqual("greeting:rld", dict["tag.endswith"]);
Assert.AreEqual("greeting:Hello World", dict["tag.exact"]);
Assert.AreEqual("important", dict["tag.present"]);
Assert.AreEqual("greeting:Hel", dict["tag.startswith"]);
Assert.AreEqual("any", dict["tag_match"]);
Assert.AreEqual("A", dict["type"]);
}
[DataTestMethod]
[DataRow(null)]
[DataRow("")]
[DataRow(" ")]
public void ShouldNotAddComment(string str)
{
// Arrange
var filter = new ListDnsRecordsFilter { Comment = str };
// Act
var dict = filter.GetQueryParameters();
// Assert
Assert.IsNotNull(dict);
Assert.AreEqual(0, dict.Count);
}
[DataTestMethod]
[DataRow(null)]
[DataRow(false)]
public void ShouldNotAddCommentAbsent(bool? b)
{
// Arrange
var filter = new ListDnsRecordsFilter { CommentAbsent = b };
// Act
var dict = filter.GetQueryParameters();
// Assert
Assert.IsNotNull(dict);
Assert.AreEqual(0, dict.Count);
}
[DataTestMethod]
[DataRow(null)]
[DataRow("")]
[DataRow(" ")]
public void ShouldNotAddCommentContains(string str)
{
// Arrange
var filter = new ListDnsRecordsFilter { CommentContains = str };
// Act
var dict = filter.GetQueryParameters();
// Assert
Assert.IsNotNull(dict);
Assert.AreEqual(0, dict.Count);
}
[DataTestMethod]
[DataRow(null)]
[DataRow("")]
[DataRow(" ")]
public void ShouldNotAddCommentEndsWith(string str)
{
// Arrange
var filter = new ListDnsRecordsFilter { CommentEndsWith = str };
// Act
var dict = filter.GetQueryParameters();
// Assert
Assert.IsNotNull(dict);
Assert.AreEqual(0, dict.Count);
}
[DataTestMethod]
[DataRow(null)]
[DataRow("")]
[DataRow(" ")]
public void ShouldNotAddCommentExact(string str)
{
// Arrange
var filter = new ListDnsRecordsFilter { CommentExact = str };
// Act
var dict = filter.GetQueryParameters();
// Assert
Assert.IsNotNull(dict);
Assert.AreEqual(0, dict.Count);
}
[DataTestMethod]
[DataRow(null)]
[DataRow(false)]
public void ShouldNotAddCommentPresent(bool? b)
{
// Arrange
var filter = new ListDnsRecordsFilter { CommentPresent = b };
// Act
var dict = filter.GetQueryParameters();
// Assert
Assert.IsNotNull(dict);
Assert.AreEqual(0, dict.Count);
}
[DataTestMethod]
[DataRow(null)]
[DataRow("")]
[DataRow(" ")]
public void ShouldNotAddCommentStartsWith(string str)
{
// Arrange
var filter = new ListDnsRecordsFilter { CommentStartsWith = str };
// Act
var dict = filter.GetQueryParameters();
// Assert
Assert.IsNotNull(dict);
Assert.AreEqual(0, dict.Count);
}
[DataTestMethod]
[DataRow(null)]
[DataRow("")]
[DataRow(" ")]
public void ShouldNotAddContent(string str)
{
// Arrange
var filter = new ListDnsRecordsFilter { Content = str };
// Act
var dict = filter.GetQueryParameters();
// Assert
Assert.IsNotNull(dict);
Assert.AreEqual(0, dict.Count);
}
[DataTestMethod]
[DataRow(null)]
[DataRow("")]
[DataRow(" ")]
public void ShouldNotAddContentContains(string str)
{
// Arrange
var filter = new ListDnsRecordsFilter { ContentContains = str };
// Act
var dict = filter.GetQueryParameters();
// Assert
Assert.IsNotNull(dict);
Assert.AreEqual(0, dict.Count);
}
[DataTestMethod]
[DataRow(null)]
[DataRow("")]
[DataRow(" ")]
public void ShouldNotAddContentEndsWith(string str)
{
// Arrange
var filter = new ListDnsRecordsFilter { ContentEndsWith = str };
// Act
var dict = filter.GetQueryParameters();
// Assert
Assert.IsNotNull(dict);
Assert.AreEqual(0, dict.Count);
}
[DataTestMethod]
[DataRow(null)]
[DataRow("")]
[DataRow(" ")]
public void ShouldNotAddContentExact(string str)
{
// Arrange
var filter = new ListDnsRecordsFilter { ContentExact = str };
// Act
var dict = filter.GetQueryParameters();
// Assert
Assert.IsNotNull(dict);
Assert.AreEqual(0, dict.Count);
}
[DataTestMethod]
[DataRow(null)]
[DataRow("")]
[DataRow(" ")]
public void ShouldNotAddContentStartsWith(string str)
{
// Arrange
var filter = new ListDnsRecordsFilter { ContentStartsWith = str };
// Act
var dict = filter.GetQueryParameters();
// Assert
Assert.IsNotNull(dict);
Assert.AreEqual(0, dict.Count);
}
[DataTestMethod]
[DataRow(null)]
[DataRow((SortDirection)0)]
public void ShouldNotAddDirection(SortDirection? direction)
{
// Arrange
var filter = new ListDnsRecordsFilter { Direction = direction };
// Act
var dict = filter.GetQueryParameters();
// Assert
Assert.IsNotNull(dict);
Assert.AreEqual(0, dict.Count);
}
[DataTestMethod]
[DataRow(null)]
[DataRow((FilterMatchType)0)]
public void ShouldNotAddMatch(FilterMatchType? match)
{
// Arrange
var filter = new ListDnsRecordsFilter { Match = match };
// Act
var dict = filter.GetQueryParameters();
// Assert
Assert.IsNotNull(dict);
Assert.AreEqual(0, dict.Count);
}
[DataTestMethod]
[DataRow(null)]
[DataRow("")]
[DataRow(" ")]
public void ShouldNotAddName(string str)
{
// Arrange
var filter = new ListDnsRecordsFilter { Name = str };
// Act
var dict = filter.GetQueryParameters();
// Assert
Assert.IsNotNull(dict);
Assert.AreEqual(0, dict.Count);
}
[DataTestMethod]
[DataRow(null)]
[DataRow("")]
[DataRow(" ")]
public void ShouldNotAddNameContains(string str)
{
// Arrange
var filter = new ListDnsRecordsFilter { NameContains = str };
// Act
var dict = filter.GetQueryParameters();
// Assert
Assert.IsNotNull(dict);
Assert.AreEqual(0, dict.Count);
}
[DataTestMethod]
[DataRow(null)]
[DataRow("")]
[DataRow(" ")]
public void ShouldNotAddNameEndsWith(string str)
{
// Arrange
var filter = new ListDnsRecordsFilter { NameEndsWith = str };
// Act
var dict = filter.GetQueryParameters();
// Assert
Assert.IsNotNull(dict);
Assert.AreEqual(0, dict.Count);
}
[DataTestMethod]
[DataRow(null)]
[DataRow("")]
[DataRow(" ")]
public void ShouldNotAddNameExact(string str)
{
// Arrange
var filter = new ListDnsRecordsFilter { NameExact = str };
// Act
var dict = filter.GetQueryParameters();
// Assert
Assert.IsNotNull(dict);
Assert.AreEqual(0, dict.Count);
}
[DataTestMethod]
[DataRow(null)]
[DataRow("")]
[DataRow(" ")]
public void ShouldNotAddNameStartsWith(string str)
{
// Arrange
var filter = new ListDnsRecordsFilter { NameStartsWith = str };
// Act
var dict = filter.GetQueryParameters();
// Assert
Assert.IsNotNull(dict);
Assert.AreEqual(0, dict.Count);
}
[DataTestMethod]
[DataRow(null)]
[DataRow((DnsRecordsOrderBy)0)]
public void ShouldNotAddOrder(DnsRecordsOrderBy? order)
{
// Arrange
var filter = new ListDnsRecordsFilter { OrderBy = order };
// Act
var dict = filter.GetQueryParameters();
// Assert
Assert.IsNotNull(dict);
Assert.AreEqual(0, dict.Count);
}
[DataTestMethod]
[DataRow(null)]
[DataRow(0)]
public void ShouldNotAddPage(int? page)
{
// Arrange
var filter = new ListDnsRecordsFilter { Page = page };
// Act
var dict = filter.GetQueryParameters();
// Assert
Assert.IsNotNull(dict);
Assert.AreEqual(0, dict.Count);
}
[DataTestMethod]
[DataRow(null)]
[DataRow(0)]
[DataRow(5_000_001)]
public void ShouldNotAddPerPage(int? perPage)
{
// Arrange
var filter = new ListDnsRecordsFilter { PerPage = perPage };
// Act
var dict = filter.GetQueryParameters();
// Assert
Assert.IsNotNull(dict);
Assert.AreEqual(0, dict.Count);
}
[DataTestMethod]
[DataRow(null)]
[DataRow("")]
[DataRow(" ")]
public void ShouldNotAddTag(string str)
{
// Arrange
var filter = new ListDnsRecordsFilter { Tag = str };
// Act
var dict = filter.GetQueryParameters();
// Assert
Assert.IsNotNull(dict);
Assert.AreEqual(0, dict.Count);
}
[DataTestMethod]
[DataRow(null)]
[DataRow("")]
[DataRow(" ")]
public void ShouldNotAddTagAbsent(string str)
{
// Arrange
var filter = new ListDnsRecordsFilter { TagAbsent = str };
// Act
var dict = filter.GetQueryParameters();
// Assert
Assert.IsNotNull(dict);
Assert.AreEqual(0, dict.Count);
}
[DataTestMethod]
[DataRow(null)]
[DataRow("")]
[DataRow(" ")]
public void ShouldNotAddTagContains(string str)
{
// Arrange
var filter = new ListDnsRecordsFilter { TagContains = str };
// Act
var dict = filter.GetQueryParameters();
// Assert
Assert.IsNotNull(dict);
Assert.AreEqual(0, dict.Count);
}
[DataTestMethod]
[DataRow(null)]
[DataRow("")]
[DataRow(" ")]
public void ShouldNotAddTagEndsWith(string str)
{
// Arrange
var filter = new ListDnsRecordsFilter { TagEndsWith = str };
// Act
var dict = filter.GetQueryParameters();
// Assert
Assert.IsNotNull(dict);
Assert.AreEqual(0, dict.Count);
}
[DataTestMethod]
[DataRow(null)]
[DataRow("")]
[DataRow(" ")]
public void ShouldNotAddTagExact(string str)
{
// Arrange
var filter = new ListDnsRecordsFilter { TagExact = str };
// Act
var dict = filter.GetQueryParameters();
// Assert
Assert.IsNotNull(dict);
Assert.AreEqual(0, dict.Count);
}
[DataTestMethod]
[DataRow(null)]
[DataRow("")]
[DataRow(" ")]
public void ShouldNotAddTagPresent(string str)
{
// Arrange
var filter = new ListDnsRecordsFilter { TagPresent = str };
// Act
var dict = filter.GetQueryParameters();
// Assert
Assert.IsNotNull(dict);
Assert.AreEqual(0, dict.Count);
}
[DataTestMethod]
[DataRow(null)]
[DataRow("")]
[DataRow(" ")]
public void ShouldNotAddTagStartsWith(string str)
{
// Arrange
var filter = new ListDnsRecordsFilter { TagStartsWith = str };
// Act
var dict = filter.GetQueryParameters();
// Assert
Assert.IsNotNull(dict);
Assert.AreEqual(0, dict.Count);
}
[DataTestMethod]
[DataRow(null)]
[DataRow((FilterMatchType)0)]
public void ShouldNotAddTagMatch(FilterMatchType? tagMatch)
{
// Arrange
var filter = new ListDnsRecordsFilter { TagMatch = tagMatch };
// Act
var dict = filter.GetQueryParameters();
// Assert
Assert.IsNotNull(dict);
Assert.AreEqual(0, dict.Count);
}
[DataTestMethod]
[DataRow(null)]
[DataRow((DnsRecordType)0)]
public void ShouldNotAddType(DnsRecordType? type)
{
// Arrange
var filter = new ListDnsRecordsFilter { Type = type };
// Act
var dict = filter.GetQueryParameters();
// Assert
Assert.IsNotNull(dict);
Assert.AreEqual(0, dict.Count);
}
private ICloudflareClient GetClient()
{
_clientMock = new Mock<ICloudflareClient>();
_clientMock
.Setup(m => m.GetAsync<IReadOnlyCollection<DnsRecord>>(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,99 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AMWD.Net.Api.Cloudflare;
using AMWD.Net.Api.Cloudflare.Dns;
using AMWD.Net.Api.Cloudflare.Dns.Internals;
using Moq;
namespace Cloudflare.Dns.Tests.DnsRecordsExtensions
{
[TestClass]
public class OverwriteDnsRecordTest
{
private const string ZoneId = "023e105f4ecef8ad9ca31a8372d0c353";
private const string RecordId = "023e105f4ecef8ad9ca31a8372d0c355";
private Mock<ICloudflareClient> _clientMock;
private CloudflareResponse<DnsRecord> _response;
private List<(string RequestPath, InternalDnsRecordRequest Request)> _callbacks;
private OverwriteDnsRecordRequest _request;
[TestInitialize]
public void Initialize()
{
_callbacks = [];
_response = new CloudflareResponse<DnsRecord>
{
Success = true,
Messages = [
new ResponseInfo(1000, "Message 1")
],
Errors = [
new ResponseInfo(1000, "Error 1")
],
Result = new ARecord("*.example.com")
{
Id = RecordId,
Content = "96.7.128.175",
Proxiable = true,
Proxied = true,
TimeToLive = 1,
Settings = new(),
Comment = "Domain verification record",
Tags = [],
CreatedOn = DateTime.Parse("2014-01-01T05:20:00.12345Z"),
ModifiedOn = DateTime.Parse("2014-01-01T05:20:00.12345Z"),
CommentModifiedOn = DateTime.Parse("2024-01-01T05:20:00.12345Z"),
TagsModifiedOn = DateTime.Parse("2025-01-01T05:20:00.12345Z"),
}
};
_request = new OverwriteDnsRecordRequest(ZoneId, RecordId, "example.com")
{
Type = DnsRecordType.A,
Content = "127.0.1.22"
};
}
[TestMethod]
public async Task ShouldOverwriteDnsRecord()
{
// Arrange
var client = GetClient();
// Act
var response = await client.OverwriteDnsRecord(_request);
// Assert
Assert.IsNotNull(response);
Assert.IsTrue(response.Success);
Assert.AreEqual(_response.Result, response.Result);
Assert.AreEqual(1, _callbacks.Count);
var callback = _callbacks.First();
Assert.AreEqual($"/zones/{ZoneId}/dns_records/{RecordId}", callback.RequestPath);
Assert.IsNotNull(callback.Request);
Assert.AreEqual("example.com", callback.Request.Name);
Assert.AreEqual(DnsRecordType.A, callback.Request.Type);
Assert.AreEqual("127.0.1.22", callback.Request.Content);
_clientMock.Verify(m => m.PutAsync<DnsRecord, InternalDnsRecordRequest>($"/zones/{ZoneId}/dns_records/{RecordId}", It.IsAny<InternalDnsRecordRequest>(), It.IsAny<CancellationToken>()), Times.Once);
_clientMock.VerifyNoOtherCalls();
}
private ICloudflareClient GetClient()
{
_clientMock = new Mock<ICloudflareClient>();
_clientMock
.Setup(m => m.PutAsync<DnsRecord, InternalDnsRecordRequest>(It.IsAny<string>(), It.IsAny<InternalDnsRecordRequest>(), It.IsAny<CancellationToken>()))
.Callback<string, InternalDnsRecordRequest, CancellationToken>((requestPath, request, _) => _callbacks.Add((requestPath, request)))
.ReturnsAsync(() => _response);
return _clientMock.Object;
}
}
}

View File

@@ -0,0 +1,73 @@
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.DnsRecordsExtensions
{
[TestClass]
public class ScanDnsRecordsTest
{
private const string ZoneId = "023e105f4ecef8ad9ca31a8372d0c353";
private Mock<ICloudflareClient> _clientMock;
private CloudflareResponse<RecordScanResponse> _response;
private List<string> _callbacks;
[TestInitialize]
public void Initialize()
{
_callbacks = [];
_response = new CloudflareResponse<RecordScanResponse>
{
Success = true,
Messages = [
new ResponseInfo(1000, "Message 1")
],
Errors = [
new ResponseInfo(1000, "Error 1")
],
Result = new RecordScanResponse
{
RecordsAdded = 5,
TotalRecordsParsed = 6,
}
};
}
[TestMethod]
public async Task ShouldScanDnsRecords()
{
// Arrange
var client = GetClient();
// Act
var response = await client.ScanDnsRecords(ZoneId);
// Assert
Assert.IsNotNull(response);
Assert.IsTrue(response.Success);
Assert.AreEqual(_response.Result, response.Result);
Assert.AreEqual(1, _callbacks.Count);
Assert.AreEqual($"/zones/{ZoneId}/dns_records/scan", _callbacks.First());
_clientMock.Verify(m => m.PostAsync<RecordScanResponse, object>($"/zones/{ZoneId}/dns_records/scan", null, null, It.IsAny<CancellationToken>()), Times.Once);
_clientMock.VerifyNoOtherCalls();
}
private ICloudflareClient GetClient()
{
_clientMock = new Mock<ICloudflareClient>();
_clientMock
.Setup(m => m.PostAsync<RecordScanResponse, object>(It.IsAny<string>(), It.IsAny<object>(), It.IsAny<IQueryParameterFilter>(), It.IsAny<CancellationToken>()))
.Callback<string, object, IQueryParameterFilter, CancellationToken>((requestPath, _, _, _) => _callbacks.Add(requestPath))
.ReturnsAsync(() => _response);
return _clientMock.Object;
}
}
}

View File

@@ -0,0 +1,105 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using AMWD.Net.Api.Cloudflare;
using AMWD.Net.Api.Cloudflare.Dns;
using AMWD.Net.Api.Cloudflare.Dns.Internals;
using Moq;
using Newtonsoft.Json.Linq;
namespace Cloudflare.Dns.Tests.DnsRecordsExtensions
{
[TestClass]
public class UpdateDnsRecordTest
{
private const string ZoneId = "023e105f4ecef8ad9ca31a8372d0c353";
private const string RecordId = "023e105f4ecef8ad9ca31a8372d0c355";
private Mock<ICloudflareClient> _clientMock;
private CloudflareResponse<DnsRecord> _response;
private List<(string RequestPath, InternalDnsRecordRequest Request)> _callbacks;
private UpdateDnsRecordRequest _request;
[TestInitialize]
public void Initialize()
{
_callbacks = [];
_response = new CloudflareResponse<DnsRecord>
{
Success = true,
Messages = [
new ResponseInfo(1000, "Message 1")
],
Errors = [
new ResponseInfo(1000, "Error 1")
],
Result = new ARecord("example.com")
{
Id = RecordId,
Name = "*.example.com",
Content = "96.7.128.175",
Proxiable = true,
Proxied = true,
TimeToLive = 1,
Settings = new(),
Meta = new JObject(),
Comment = "Domain verification record",
Tags = [],
CreatedOn = DateTime.Parse("2014-01-01T05:20:00.12345Z"),
ModifiedOn = DateTime.Parse("2014-01-01T05:20:00.12345Z"),
CommentModifiedOn = DateTime.Parse("2024-01-01T05:20:00.12345Z"),
TagsModifiedOn = DateTime.Parse("2025-01-01T05:20:00.12345Z"),
}
};
_request = new UpdateDnsRecordRequest(ZoneId, RecordId, "example.com")
{
Type = DnsRecordType.A,
Content = "127.0.1.22"
};
}
[TestMethod]
public async Task ShouldUpdateDnsRecord()
{
// Arrange
var client = GetClient();
// Act
var response = await client.UpdateDnsRecord(_request);
// Assert
Assert.IsNotNull(response);
Assert.IsTrue(response.Success);
Assert.AreEqual(_response.Result, response.Result);
Assert.AreEqual(1, _callbacks.Count);
var callback = _callbacks.First();
Assert.AreEqual($"/zones/{ZoneId}/dns_records/{RecordId}", callback.RequestPath);
Assert.IsNotNull(callback.Request);
Assert.AreEqual("example.com", callback.Request.Name);
Assert.AreEqual(DnsRecordType.A, callback.Request.Type);
Assert.AreEqual("127.0.1.22", callback.Request.Content);
_clientMock.Verify(m => m.PatchAsync<DnsRecord, InternalDnsRecordRequest>($"/zones/{ZoneId}/dns_records/{RecordId}", It.IsAny<InternalDnsRecordRequest>(), It.IsAny<CancellationToken>()), Times.Once);
_clientMock.VerifyNoOtherCalls();
}
private ICloudflareClient GetClient()
{
_clientMock = new Mock<ICloudflareClient>();
_clientMock
.Setup(m => m.PatchAsync<DnsRecord, InternalDnsRecordRequest>(It.IsAny<string>(), It.IsAny<InternalDnsRecordRequest>(), It.IsAny<CancellationToken>()))
.Callback<string, InternalDnsRecordRequest, CancellationToken>((requestPath, request, _) => _callbacks.Add((requestPath, request)))
.ReturnsAsync(() => _response);
return _clientMock.Object;
}
}
}