diff --git a/README.md b/README.md
index 556c85a..5a1e402 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,7 @@
This project aims to implement the [Cloudflare API] in an extensible way.
+
## Overview
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]
-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]
-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])
[](https://link.am-wd.de/donate)
-[](https://link.am-wd.de/codeium)
-[Cloudflare]: Cloudflare/README.md
-[Cloudflare.Zones]: Extensions/Cloudflare.Zones/README.md
+[Cloudflare]: src/Cloudflare/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/
[MIT License]: LICENSE.txt
diff --git a/src/Cloudflare/Models/PaginationInfo.cs b/src/Cloudflare/Models/PaginationInfo.cs
index 6799878..c028249 100644
--- a/src/Cloudflare/Models/PaginationInfo.cs
+++ b/src/Cloudflare/Models/PaginationInfo.cs
@@ -29,5 +29,11 @@
///
[JsonProperty("total_count")]
public int? TotalCount { get; set; }
+
+ ///
+ /// Total number of pages of results.
+ ///
+ [JsonProperty("total_pages")]
+ public int? TotalPages { get; set; }
}
}
diff --git a/src/Extensions/Cloudflare.Dns/Cloudflare.Dns.csproj b/src/Extensions/Cloudflare.Dns/Cloudflare.Dns.csproj
index efd85b3..2a06539 100644
--- a/src/Extensions/Cloudflare.Dns/Cloudflare.Dns.csproj
+++ b/src/Extensions/Cloudflare.Dns/Cloudflare.Dns.csproj
@@ -15,7 +15,8 @@
-
+
+
true
diff --git a/src/Extensions/Cloudflare.Dns/DnsRecordsExtensions.cs b/src/Extensions/Cloudflare.Dns/DnsRecordsExtensions.cs
new file mode 100644
index 0000000..f1ccfbb
--- /dev/null
+++ b/src/Extensions/Cloudflare.Dns/DnsRecordsExtensions.cs
@@ -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
+{
+ ///
+ /// Extensions for DNS Records.
+ ///
+ public static class DnsRecordsExtensions
+ {
+ private static readonly IReadOnlyCollection _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 _priorityTypes = [
+ DnsRecordType.MX,
+ DnsRecordType.SRV,
+ DnsRecordType.URI,
+ ];
+
+ ///
+ /// Send a Batch of DNS Record API calls to be executed together.
+ ///
+ ///
+ /// Notes:
+ ///
+ /// -
+ /// 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
+ /// the documentation
+ /// for more information.
+ ///
+ /// -
+ /// The operations you specify within the /batch request body are always executed
+ /// in the following order:
+ ///
+ /// - Deletes (DELETE)
+ /// - Updates (PATCH)
+ /// - Overwrites (PUT)
+ /// - Creates (POST)
+ ///
+ ///
+ ///
+ ///
+ /// The instance.
+ /// The request.
+ /// A cancellation token used to propagate notification that this operation should be canceled.
+ public static Task> BatchDnsRecords(this ICloudflareClient client, BatchDnsRecordsRequest request, CancellationToken cancellationToken = default)
+ {
+ request.ZoneId.ValidateCloudflareId();
+
+ // Deletes (DELETE)
+ var deletes = new List();
+ foreach (string delete in request.Deletes ?? [])
+ {
+ delete.ValidateCloudflareId();
+ deletes.Add(new Identifier { Id = delete });
+ }
+
+ // Updates (PATCH)
+ var patches = new List();
+ 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();
+ foreach (var post in request.Creates ?? [])
+ {
+ var req = (InternalDnsRecordRequest)ValidateRequest(post);
+ posts.Add(req);
+ }
+
+ // Overwrites (PUT)
+ var puts = new List();
+ 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($"/zones/{request.ZoneId}/dns_records/batch", batchReq, null, cancellationToken);
+ }
+
+ ///
+ /// Create a new DNS record for a zone.
+ ///
+ ///
+ /// Notes:
+ ///
+ /// - A/AAAA records cannot exist on the same name as CNAME records.
+ /// - NS records cannot exist on the same name as any other record type.
+ /// - Domain names are always represented in Punycode, even if Unicode characters were used when creating the record.
+ ///
+ ///
+ /// The instance.
+ /// The request.
+ /// A cancellation token used to propagate notification that this operation should be canceled.
+ public static Task> CreateDnsRecord(this ICloudflareClient client, CreateDnsRecordRequest request, CancellationToken cancellationToken = default)
+ {
+ request.ZoneId.ValidateCloudflareId();
+
+ var req = ValidateRequest(request);
+
+ return client.PostAsync($"/zones/{request.ZoneId}/dns_records", req, null, cancellationToken);
+ }
+
+ ///
+ /// Delete DNS Record.
+ ///
+ /// The instance.
+ /// The zone identifier.
+ /// The record identifier.
+ /// A cancellation token used to propagate notification that this operation should be canceled.
+ public static Task> DeleteDnsRecord(this ICloudflareClient client, string zoneId, string recordId, CancellationToken cancellationToken = default)
+ {
+ zoneId.ValidateCloudflareId();
+ recordId.ValidateCloudflareId();
+
+ return client.DeleteAsync($"/zones/{zoneId}/dns_records/{recordId}", null, cancellationToken);
+ }
+
+ ///
+ /// Update an existing DNS record.
+ ///
+ ///
+ /// Notes:
+ ///
+ /// - A/AAAA records cannot exist on the same name as CNAME records.
+ /// - NS records cannot exist on the same name as any other record type.
+ /// - Domain names are always represented in Punycode, even if Unicode characters were used when creating the record.
+ ///
+ ///
+ /// The instance.
+ /// The request.
+ /// A cancellation token used to propagate notification that this operation should be canceled.
+ public static Task> UpdateDnsRecord(this ICloudflareClient client, UpdateDnsRecordRequest request, CancellationToken cancellationToken = default)
+ {
+ request.ZoneId.ValidateCloudflareId();
+ request.RecordId.ValidateCloudflareId();
+
+ var req = ValidateRequest(request);
+
+ return client.PatchAsync($"/zones/{request.ZoneId}/dns_records/{request.RecordId}", req, cancellationToken);
+ }
+
+ ///
+ /// You can export your BIND config through this endpoint.
+ ///
+ /// The instance.
+ /// The zone identifier.
+ /// A cancellation token used to propagate notification that this operation should be canceled.
+ public static Task> ExportDnsRecords(this ICloudflareClient client, string zoneId, CancellationToken cancellationToken = default)
+ {
+ zoneId.ValidateCloudflareId();
+
+ return client.GetAsync($"/zones/{zoneId}/dns_records/export", null, cancellationToken);
+ }
+
+ ///
+ /// DNS Record Details.
+ ///
+ /// The instance.
+ /// The zone identifier.
+ /// The record identifier.
+ /// A cancellation token used to propagate notification that this operation should be canceled.
+ public static Task> DnsRecordDetails(this ICloudflareClient client, string zoneId, string recordId, CancellationToken cancellationToken = default)
+ {
+ zoneId.ValidateCloudflareId();
+ recordId.ValidateCloudflareId();
+
+ return client.GetAsync($"/zones/{zoneId}/dns_records/{recordId}", null, cancellationToken);
+ }
+
+ ///
+ /// You can upload your BIND config through this endpoint.
+ /// It assumes that cURL is called from a location with bind_config.txt (valid BIND config) present.
+ ///
+ ///
+ /// See the documentation for more information.
+ ///
+ /// The instance.
+ /// The request.
+ /// A cancellation token used to propagate notification that this operation should be canceled.
+ public static Task> 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($"/zones/{request.ZoneId}/dns_records/import", req, null, cancellationToken);
+ }
+
+ ///
+ /// List, search, sort, and filter a zones' DNS records.
+ ///
+ /// The instance.
+ /// The zone identifier.
+ /// Filter options (optional).
+ /// A cancellation token used to propagate notification that this operation should be canceled.
+ public static Task>> ListDnsRecords(this ICloudflareClient client, string zoneId, ListDnsRecordsFilter? options = null, CancellationToken cancellationToken = default)
+ {
+ zoneId.ValidateCloudflareId();
+
+ return client.GetAsync>($"/zones/{zoneId}/dns_records", options, cancellationToken);
+ }
+
+ ///
+ /// Scan for common DNS records on your domain and automatically add them to your zone.
+ /// Useful if you haven't updated your nameservers yet.
+ ///
+ /// The instance.
+ /// The zone identifier.
+ /// A cancellation token used to propagate notification that this operation should be canceled.
+ public static Task> ScanDnsRecords(this ICloudflareClient client, string zoneId, CancellationToken cancellationToken = default)
+ {
+ zoneId.ValidateCloudflareId();
+
+ return client.PostAsync($"/zones/{zoneId}/dns_records/scan", null, null, cancellationToken);
+ }
+
+ ///
+ /// Overwrite an existing DNS record.
+ ///
+ ///
+ /// Notes:
+ ///
+ /// - A/AAAA records cannot exist on the same name as CNAME records.
+ /// - NS records cannot exist on the same name as any other record type.
+ /// - Domain names are always represented in Punycode, even if Unicode characters were used when creating the record.
+ ///
+ ///
+ /// The instance.
+ /// The request.
+ /// A cancellation token used to propagate notification that this operation should be canceled.
+ public static Task> OverwriteDnsRecord(this ICloudflareClient client, OverwriteDnsRecordRequest request, CancellationToken cancellationToken = default)
+ {
+ request.ZoneId.ValidateCloudflareId();
+ request.RecordId.ValidateCloudflareId();
+
+ var req = ValidateRequest(request);
+
+ return client.PutAsync($"/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
+ };
+ }
+ }
+}
diff --git a/src/Extensions/Cloudflare.Dns/Enums/DnsRecordType.cs b/src/Extensions/Cloudflare.Dns/Enums/DnsRecordType.cs
new file mode 100644
index 0000000..16cf1e6
--- /dev/null
+++ b/src/Extensions/Cloudflare.Dns/Enums/DnsRecordType.cs
@@ -0,0 +1,277 @@
+using System.Runtime.Serialization;
+using Newtonsoft.Json.Converters;
+
+namespace AMWD.Net.Api.Cloudflare.Dns
+{
+ ///
+ /// DNS record types.
+ /// Source
+ ///
+ ///
+ /// A list with short description can be found @wikipedia.
+ ///
+ [JsonConverter(typeof(StringEnumConverter))]
+ public enum DnsRecordType : int
+ {
+ ///
+ /// Address record.
+ ///
+ ///
+ /// 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.
+ ///
+ /// example.com. 3600 IN A 96.7.128.175
+ ///
+ ///
+ [EnumMember(Value = "A")]
+ A = 1,
+
+ ///
+ /// IPv6 address record.
+ ///
+ ///
+ /// Returns a 128-bit IPv6 address, most commonly used to map hostnames to an IP address of the host.
+ ///
+ /// example.com. 3600 IN AAAA 2600:1408:ec00:36::1736:7f31
+ ///
+ ///
+ [EnumMember(Value = "AAAA")]
+ AAAA = 28,
+
+ ///
+ /// Certification Authority Authorization.
+ ///
+ ///
+ /// DNS Certification Authority Authorization, constraining acceptable CAs for a host/domain.
+ ///
+ /// example.com. 604800 IN CAA 0 issue "letsencrypt.org"
+ ///
+ ///
+ [EnumMember(Value = "CAA")]
+ CAA = 257,
+
+ ///
+ /// Certificate record.
+ ///
+ ///
+ /// Stores PKIX, SPKI, PGP, etc.
+ ///
+ /// example.com. 86400 IN CERT 2 77 2 TUlJQ1l6Q0NBY3lnQXdJQkFnSUJBREFOQmdrcWh
+ ///
+ ///
+ [EnumMember(Value = "CERT")]
+ CERT = 37,
+
+ ///
+ /// Canonical name record.
+ ///
+ ///
+ /// Alias of one name to another: the DNS lookup will continue by retrying the lookup with the new name.
+ ///
+ /// autodiscover.example.com. 86400 IN CNAME mail.example.com.
+ ///
+ ///
+ [EnumMember(Value = "CNAME")]
+ CNAME = 5,
+
+ ///
+ /// DNS Key record.
+ ///
+ ///
+ /// The key record used in DNSSEC. Uses the same format as the KEY record.
+ ///
+ /// example.com. 3600 IN DNSKEY 256 3 13 OtuN/SL9sE+SDQ0tOLeezr1KzUNi77FflTjxQylUhm3V7m13Vz9tYQuc SGK0pyxISo9CQsszubAwJSypq3li3g==
+ ///
+ ///
+ [EnumMember(Value = "DNSKEY")]
+ DNSKEY = 48,
+
+ ///
+ /// Delegation signer.
+ ///
+ ///
+ /// The record used to identify the DNSSEC signing key of a delegated zone.
+ ///
+ /// example.com. 86400 IN DS 370 13 2 BE74359954660069D5C63D200C39F5603827D7DD02B56F120EE9F3A8 6764247C
+ ///
+ ///
+ [EnumMember(Value = "DS")]
+ DS = 43,
+
+ ///
+ /// HTTPS Binding.
+ ///
+ ///
+ /// RR that improves performance for clients that need to resolve many resources to access a domain.
+ ///
+ /// example.com. 3600 IN HTTPS 1 svc.example.com. alpn=h2
+ ///
+ ///
+ [EnumMember(Value = "HTTPS")]
+ HTTPS = 65,
+
+ ///
+ /// Location record.
+ ///
+ ///
+ /// Specifies a geographical location associated with a domain name.
+ ///
+ /// 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
+ ///
+ ///
+ [EnumMember(Value = "LOC")]
+ LOC = 29,
+
+ ///
+ /// Mail exchange record.
+ ///
+ ///
+ /// List of mail exchange servers that accept email for a domain.
+ ///
+ /// example.com. 43200 IN MX 0 mail.example.com.
+ ///
+ ///
+ [EnumMember(Value = "MX")]
+ MX = 15,
+
+ ///
+ /// Naming Authority Pointer.
+ ///
+ ///
+ /// Allows regular-expression-based rewriting of domain names which can then be used as URIs, further domain names to lookups, etc.
+ ///
+ /// example.com. 86400 IN NAPTR 100 10 "S" "SIP+D2T" "" _sip._tcp.example.com.
+ ///
+ ///
+ [EnumMember(Value = "NAPTR")]
+ NAPTR = 35,
+
+ ///
+ /// Name server record.
+ ///
+ ///
+ /// Delegates a DNS zone to use the given authoritative name servers.
+ ///
+ /// example.com. 86400 IN NS a.iana-servers.net.
+ ///
+ ///
+ [EnumMember(Value = "NS")]
+ NS = 2,
+
+ ///
+ /// OpenPGP public key record.
+ ///
+ ///
+ /// 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.
+ ///
+ /// 00d8d3f11739d2f3537099982b4674c29fc59a8fda350fca1379613a._openpgpkey.example.com. 3600 IN OPENPGPKEY a2V5S0VZMTIzNGtleUtFWQ==
+ ///
+ ///
+ [EnumMember(Value = "OPENPGPKEY")]
+ OPENPGPKEY = 61,
+
+ ///
+ /// PTR Resource Record.
+ ///
+ ///
+ /// 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.
+ ///
+ /// 14.215.184.93.in-addr.arpa. 86400 IN PTR example.com.
+ ///
+ ///
+ [EnumMember(Value = "PTR")]
+ PTR = 12,
+
+ ///
+ /// S/MIME cert association.
+ ///
+ ///
+ /// Associates an S/MIME certificate with a domain name for sender authentication.
+ ///
+ /// example.com. 3600 IN SMIMEA 0 0 0 keyKEY1234keyKEY
+ ///
+ ///
+ [EnumMember(Value = "SMIMEA")]
+ SMIMEA = 53,
+
+ ///
+ /// Service locator.
+ ///
+ ///
+ /// Generalized service location record, used for newer protocols instead of creating protocol-specific records such as MX.
+ ///
+ /// _autodiscover._tcp.example.com. 604800 IN SRV 1 0 443 mail.example.com.
+ ///
+ ///
+ [EnumMember(Value = "SRV")]
+ SRV = 33,
+
+ ///
+ /// SSH Public Key Fingerprint.
+ ///
+ ///
+ /// 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.
+ ///
+ /// example.com. 600 IN SSHFP 2 1 123456789abcdef67890123456789abcdef67890
+ ///
+ ///
+ [EnumMember(Value = "SSHFP")]
+ SSHFP = 44,
+
+ ///
+ /// Service Binding.
+ ///
+ ///
+ /// RR that improves performance for clients that need to resolve many resources to access a domain.
+ ///
+ /// example.com. 3600 IN SVCB 1 . alpn="h2,http/1.1"
+ ///
+ ///
+ [EnumMember(Value = "SVCB")]
+ SVCB = 64,
+
+ ///
+ /// TLSA certificate association.
+ ///
+ ///
+ /// 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'".
+ ///
+ /// _443._tcp.example.com. 3600 IN TLSA 3 0 18cb0fc6c527506a053f4f14c8464bebbd6dede2738d11468dd953d7d6a3021f1
+ ///
+ ///
+ [EnumMember(Value = "TLSA")]
+ TLSA = 52,
+
+ ///
+ /// Text record.
+ ///
+ ///
+ /// 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.
+ ///
+ /// More information about TXT records on Cloudflare.
+ ///
+ /// example.com. 86400 IN TXT "v=spf1 -all"
+ ///
+ ///
+ [EnumMember(Value = "TXT")]
+ TXT = 16,
+
+ ///
+ /// Uniform Resource Identifier.
+ ///
+ ///
+ /// Can be used for publishing mappings from hostnames to URIs.
+ ///
+ /// _ftp._tcp.example.com. 3600 IN URI 10 1 "ftp://ftp.example.com/public"
+ ///
+ ///
+ [EnumMember(Value = "URI")]
+ URI = 256,
+ }
+}
diff --git a/src/Extensions/Cloudflare.Dns/Enums/FilterMatchType.cs b/src/Extensions/Cloudflare.Dns/Enums/FilterMatchType.cs
new file mode 100644
index 0000000..ad03725
--- /dev/null
+++ b/src/Extensions/Cloudflare.Dns/Enums/FilterMatchType.cs
@@ -0,0 +1,24 @@
+using System.Runtime.Serialization;
+using Newtonsoft.Json.Converters;
+
+namespace AMWD.Net.Api.Cloudflare.Dns
+{
+ ///
+ /// Options how to match the query filter.
+ ///
+ [JsonConverter(typeof(StringEnumConverter))]
+ public enum FilterMatchType
+ {
+ ///
+ /// Match any rule.
+ ///
+ [EnumMember(Value = "any")]
+ Any = 1,
+
+ ///
+ /// Match all rules.
+ ///
+ [EnumMember(Value = "all")]
+ All = 2
+ }
+}
diff --git a/src/Extensions/Cloudflare.Dns/Filters/ListDnsRecordsFilter.cs b/src/Extensions/Cloudflare.Dns/Filters/ListDnsRecordsFilter.cs
new file mode 100644
index 0000000..9dc193d
--- /dev/null
+++ b/src/Extensions/Cloudflare.Dns/Filters/ListDnsRecordsFilter.cs
@@ -0,0 +1,439 @@
+using System.Runtime.Serialization;
+using Newtonsoft.Json.Converters;
+
+namespace AMWD.Net.Api.Cloudflare.Dns
+{
+ ///
+ /// Filter for listing DNS records.
+ ///
+ public class ListDnsRecordsFilter : IQueryParameterFilter
+ {
+ #region Comment
+
+ ///
+ /// Exact value of the DNS record comment.
+ /// This is a convenience alias for .
+ ///
+ public string? Comment { get; set; }
+
+ ///
+ /// If this parameter is present, only records without a comment are returned.
+ ///
+ public bool? CommentAbsent { get; set; }
+
+ ///
+ /// Substring of the DNS record comment.
+ /// Comment filters are case-insensitive.
+ ///
+ public string? CommentContains { get; set; }
+
+ ///
+ /// Suffix of the DNS record comment.
+ /// Comment filters are case-insensitive.
+ ///
+ public string? CommentEndsWith { get; set; }
+
+ ///
+ /// Exact value of the DNS record comment.
+ /// Comment filters are case-insensitive.
+ ///
+ public string? CommentExact { get; set; }
+
+ ///
+ /// If this parameter is present, only records with a comment are returned.
+ ///
+ public bool? CommentPresent { get; set; }
+
+ ///
+ /// Prefix of the DNS record comment.
+ /// Comment filters are case-insensitive.
+ ///
+ public string? CommentStartsWith { get; set; }
+
+ #endregion Comment
+
+ #region Content
+
+ ///
+ /// Exact value of the DNS record Content.
+ /// This is a convenience alias for .
+ ///
+ public string? Content { get; set; }
+
+ ///
+ /// Substring of the DNS record Content.
+ /// Content filters are case-insensitive.
+ ///
+ public string? ContentContains { get; set; }
+
+ ///
+ /// Suffix of the DNS record Content.
+ /// Content filters are case-insensitive.
+ ///
+ public string? ContentEndsWith { get; set; }
+
+ ///
+ /// Exact value of the DNS record Content.
+ /// Content filters are case-insensitive.
+ ///
+ public string? ContentExact { get; set; }
+
+ ///
+ /// Prefix of the DNS record Content.
+ /// Content filters are case-insensitive.
+ ///
+ public string? ContentStartsWith { get; set; }
+
+ #endregion Content
+
+ ///
+ /// Direction to order DNS records in.
+ ///
+ public SortDirection? Direction { get; set; }
+
+ ///
+ /// Whether to match all search requirements or at least one (any).
+ ///
+ ///
+ ///
+ /// If set to , acts like a logical AND between filters.
+ ///
+ /// If set to , acts like a logical OR instead.
+ ///
+ ///
+ /// Note that the interaction between tag filters is controlled by the parameter instead.
+ ///
+ ///
+ public FilterMatchType? Match { get; set; }
+
+ #region Name
+
+ ///
+ /// Exact value of the DNS record Name.
+ /// This is a convenience alias for .
+ ///
+ public string? Name { get; set; }
+
+ ///
+ /// Substring of the DNS record Name.
+ /// Name filters are case-insensitive.
+ ///
+ public string? NameContains { get; set; }
+
+ ///
+ /// Suffix of the DNS record Name.
+ /// Name filters are case-insensitive.
+ ///
+ public string? NameEndsWith { get; set; }
+
+ ///
+ /// Exact value of the DNS record Name.
+ /// Name filters are case-insensitive.
+ ///
+ public string? NameExact { get; set; }
+
+ ///
+ /// Prefix of the DNS record Name.
+ /// Name filters are case-insensitive.
+ ///
+ public string? NameStartsWith { get; set; }
+
+ #endregion Name
+
+ ///
+ /// Field to order DNS records by.
+ ///
+ public DnsRecordsOrderBy? OrderBy { get; set; }
+
+ ///
+ /// Page number of paginated results.
+ ///
+ /// 1 <= X
+ public int? Page { get; set; }
+
+ ///
+ /// Number of DNS records per page.
+ ///
+ /// 1 <= X <= 5,000,000
+ public int? PerPage { get; set; }
+
+ ///
+ /// Whether the record is receiving the performance and security benefits of Cloudflare.
+ ///
+ public bool? Proxied { get; set; }
+
+ ///
+ /// Allows searching in multiple properties of a DNS record simultaneously.
+ ///
+ ///
+ ///
+ /// This parameter is intended for human users, not automation.
+ /// Its exact behavior is intentionally left unspecified and is subject to change in the future.
+ ///
+ ///
+ ///
+ /// This parameter works independently of the setting.
+ ///
+ /// For automated searches, please use the other available parameters.
+ ///
+ ///
+ ///
+ public string? Search { get; set; }
+
+ #region Tag
+
+ ///
+ /// Condition on the DNS record tag.
+ ///
+ ///
+ ///
+ /// Parameter values can be of the form <tag-name>:<tag-value> to search for an exact name:value pair,
+ /// or just <tag-name> to search for records with a specific tag name regardless of its value.
+ ///
+ ///
+ /// This is a convenience shorthand for the more powerful tag.<predicate> parameters.
+ ///
+ /// Examples:
+ ///
+ /// - tag=important is equivalent to tag.present=important
+ /// - tag=team:DNS is equivalent to tag.exact=team:DNS
+ ///
+ ///
+ ///
+ public string? Tag { get; set; }
+
+ ///
+ /// Name of a tag which must not be present on the DNS record.
+ /// Tag filters are case-insensitive.
+ ///
+ public string? TagAbsent { get; set; }
+
+ ///
+ /// A tag and value, of the form <tag-name>:<tag-value>.
+ /// Tag filters are case-insensitive.
+ ///
+ ///
+ /// The API will only return DNS records that have a tag named <tag-name> whose value contains <tag-value>.
+ ///
+ public string? TagContains { get; set; }
+
+ ///
+ /// A tag and value, of the form <tag-name>:<tag-value>.
+ /// Tag filters are case-insensitive.
+ ///
+ ///
+ /// The API will only return DNS records that have a tag named <tag-name> whose value ends with <tag-value>.
+ ///
+ public string? TagEndsWith { get; set; }
+
+ ///
+ /// A tag and value, of the form <tag-name>:<tag-value>.
+ /// Tag filters are case-insensitive.
+ ///
+ ///
+ /// The API will only return DNS records that have a tag named <tag-name> whose value is <tag-value>.
+ ///
+ public string? TagExact { get; set; }
+
+ ///
+ /// Name of a tag which must be present on the DNS record.
+ /// Tag filters are case-insensitive.
+ ///
+ public string? TagPresent { get; set; }
+
+ ///
+ /// A tag and value, of the form <tag-name>:<tag-value>.
+ /// Tag filters are case-insensitive.
+ ///
+ ///
+ /// The API will only return DNS records that have a tag named <tag-name> whose value starts with <tag-value>.
+ ///
+ public string? TagStartsWith { get; set; }
+
+ #endregion Tag
+
+ ///
+ /// Whether to match all tag search requirements or at least one (any).
+ ///
+ ///
+ ///
+ /// If set to , acts like a logical AND between filters.
+ ///
+ /// If set to , acts like a logical OR instead.
+ ///
+ ///
+ /// Note that the regular parameter is still used to combine the resulting condition with other filters that aren't related to tags.
+ ///
+ ///
+ public FilterMatchType? TagMatch { get; set; }
+
+ ///
+ /// Record type.
+ ///
+ public DnsRecordType? Type { get; set; }
+
+ ///
+ public IDictionary GetQueryParameters()
+ {
+ var dict = new Dictionary();
+
+#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;
+ }
+ }
+
+ ///
+ /// Possible fields to order DNS records by.
+ ///
+ [JsonConverter(typeof(StringEnumConverter))]
+ public enum DnsRecordsOrderBy
+ {
+ ///
+ /// Order by record type.
+ ///
+ [EnumMember(Value = "type")]
+ Type = 1,
+
+ ///
+ /// Order by record name.
+ ///
+ [EnumMember(Value = "name")]
+ Name = 2,
+
+ ///
+ /// Order by record content.
+ ///
+ [EnumMember(Value = "content")]
+ Content = 3,
+
+ ///
+ /// Order by record TTL.
+ ///
+ [EnumMember(Value = "ttl")]
+ Ttl = 4,
+
+ ///
+ /// Order by record proxied.
+ ///
+ [EnumMember(Value = "proxied")]
+ Proxied = 5
+ }
+}
diff --git a/src/Extensions/Cloudflare.Dns/Internals/InternalBatchRequest.cs b/src/Extensions/Cloudflare.Dns/Internals/InternalBatchRequest.cs
new file mode 100644
index 0000000..79e2acb
--- /dev/null
+++ b/src/Extensions/Cloudflare.Dns/Internals/InternalBatchRequest.cs
@@ -0,0 +1,23 @@
+namespace AMWD.Net.Api.Cloudflare.Dns.Internals
+{
+ internal class InternalBatchRequest
+ {
+ [JsonProperty("deletes")]
+ public IReadOnlyCollection? Deletes { get; set; }
+
+ [JsonProperty("patches")]
+ public IReadOnlyCollection? Patches { get; set; }
+
+ [JsonProperty("posts")]
+ public IReadOnlyCollection? Posts { get; set; }
+
+ [JsonProperty("puts")]
+ public IReadOnlyCollection? Puts { get; set; }
+ }
+
+ internal class InternalBatchUpdateRequest : InternalDnsRecordRequest
+ {
+ [JsonProperty("id")]
+ public string? Id { get; set; }
+ }
+}
diff --git a/src/Extensions/Cloudflare.Dns/Internals/InternalDnsRecordRequest.cs b/src/Extensions/Cloudflare.Dns/Internals/InternalDnsRecordRequest.cs
new file mode 100644
index 0000000..05e390d
--- /dev/null
+++ b/src/Extensions/Cloudflare.Dns/Internals/InternalDnsRecordRequest.cs
@@ -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? 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; }
+ }
+}
diff --git a/src/Extensions/Cloudflare.Dns/Models/DNSAnalyticsQuery.cs b/src/Extensions/Cloudflare.Dns/Models/DNSAnalyticsQuery.cs
new file mode 100644
index 0000000..d17d5bb
--- /dev/null
+++ b/src/Extensions/Cloudflare.Dns/Models/DNSAnalyticsQuery.cs
@@ -0,0 +1,122 @@
+using System.Runtime.Serialization;
+using Newtonsoft.Json.Converters;
+
+namespace AMWD.Net.Api.Cloudflare.Dns
+{
+ internal class DNSAnalyticsQuery
+ {
+ ///
+ /// Array of dimension names.
+ ///
+ [JsonProperty("dimensions")]
+ public IReadOnlyCollection? Dimensions { get; set; }
+
+ ///
+ /// Limit number of returned metrics.
+ ///
+ [JsonProperty("limit")]
+ public int? Limit { get; set; }
+
+ ///
+ /// Array of metric names.
+ ///
+ [JsonProperty("metrics")]
+ public IReadOnlyCollection? Metrics { get; set; }
+
+ ///
+ /// Start date and time of requesting data period.
+ ///
+ [JsonProperty("since")]
+ public DateTime? Since { get; set; }
+
+ ///
+ /// Unit of time to group data by.
+ ///
+ [JsonProperty("time_delta")]
+ public TimeDeltaUnit? TimeDelta { get; set; }
+
+ [JsonProperty("until")]
+ public DateTime? Until { get; set; }
+
+ ///
+ /// Segmentation filter in 'attribute operator value' format.
+ ///
+ [JsonProperty("filters")]
+ public string? Filters { get; set; }
+
+ ///
+ /// Array of dimensions to sort by, where each dimension may be prefixed
+ /// by - (descending) or + (ascending).
+ ///
+ [JsonProperty("sort")]
+ public IReadOnlyCollection? Sort { get; set; }
+ }
+
+ ///
+ /// Time delta units.
+ /// Source
+ ///
+ [JsonConverter(typeof(StringEnumConverter))]
+ public enum TimeDeltaUnit
+ {
+ ///
+ /// All time.
+ ///
+ [EnumMember(Value = "all")]
+ All = 1,
+
+ ///
+ /// Auto.
+ ///
+ [EnumMember(Value = "auto")]
+ Auto = 2,
+
+ ///
+ /// Year.
+ ///
+ [EnumMember(Value = "year")]
+ Year = 3,
+
+ ///
+ /// Quarter.
+ ///
+ [EnumMember(Value = "quarter")]
+ Quarter = 4,
+
+ ///
+ /// Month.
+ ///
+ [EnumMember(Value = "month")]
+ Month = 5,
+
+ ///
+ /// Week.
+ ///
+ [EnumMember(Value = "week")]
+ Week = 6,
+
+ ///
+ /// Day.
+ ///
+ [EnumMember(Value = "day")]
+ Day = 7,
+
+ ///
+ /// Hour.
+ ///
+ [EnumMember(Value = "hour")]
+ Hour = 8,
+
+ ///
+ /// Dekaminute.
+ ///
+ [EnumMember(Value = "dekaminute")]
+ DekaMinute = 9,
+
+ ///
+ /// Minute.
+ ///
+ [EnumMember(Value = "minute")]
+ Minute = 10
+ }
+}
diff --git a/src/Extensions/Cloudflare.Dns/Models/DnsRecord.cs b/src/Extensions/Cloudflare.Dns/Models/DnsRecord.cs
new file mode 100644
index 0000000..53e8dd1
--- /dev/null
+++ b/src/Extensions/Cloudflare.Dns/Models/DnsRecord.cs
@@ -0,0 +1,120 @@
+using Newtonsoft.Json.Linq;
+
+namespace AMWD.Net.Api.Cloudflare.Dns
+{
+ ///
+ /// A DNS record.
+ ///
+ public abstract class DnsRecord
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// DNS record name (or @ for the zone apex) in Punycode.
+ protected DnsRecord(string name)
+ {
+ Name = name;
+ }
+
+ ///
+ /// DNS record name (or @ for the zone apex) in Punycode.
+ ///
+ [JsonProperty("name")]
+ public string Name { get; set; }
+
+ ///
+ /// Record type.
+ ///
+ [JsonProperty("type")]
+ public DnsRecordType Type { get; protected set; }
+
+ ///
+ /// Comments or notes about the DNS record.
+ /// This field has no effect on DNS responses.
+ ///
+ [JsonProperty("comment")]
+ public string? Comment { get; set; }
+
+ ///
+ /// DNS record content.
+ ///
+ [JsonProperty("content")]
+ public string? Content { get; set; }
+
+ ///
+ /// Whether the record is receiving the performance and security benefits of
+ /// Cloudflare.
+ ///
+ [JsonProperty("proxied")]
+ public bool? Proxied { get; set; }
+
+ ///
+ /// Settings for the DNS record.
+ ///
+ [JsonProperty("settings")]
+ public DnsRecordSettings? Settings { get; set; }
+
+ ///
+ /// Custom tags for the DNS record.
+ /// This field has no effect on DNS responses.
+ ///
+ [JsonProperty("tags")]
+ public IReadOnlyCollection? Tags { get; set; }
+
+ ///
+ /// Time To Live (TTL) of the DNS record in seconds.
+ ///
+ ///
+ /// Setting to 1 means 'automatic'. Value must be between 60 and 86400, with the
+ /// minimum reduced to 30 for Enterprise zones.
+ ///
+ [JsonProperty("ttl")]
+ public int? TimeToLive { get; set; }
+
+ #region Response
+
+ ///
+ /// Identifier.
+ ///
+ [JsonProperty("id")]
+ public string? Id { get; set; }
+
+ ///
+ /// When the record was created.
+ ///
+ [JsonProperty("created_on")]
+ public DateTime CreatedOn { get; set; }
+
+ ///
+ /// Extra Cloudflare-specific information about the record.
+ ///
+ [JsonProperty("meta")]
+ public JToken? Meta { get; set; }
+
+ ///
+ /// When the record was last modified.
+ ///
+ [JsonProperty("modified_on")]
+ public DateTime ModifiedOn { get; set; }
+
+ ///
+ /// Whether the record can be proxied by Cloudflare or not.
+ ///
+ [JsonProperty("proxiable")]
+ public bool Proxiable { get; set; }
+
+ ///
+ /// When the record comment was last modified. Omitted if there is no comment.
+ ///
+ [JsonProperty("comment_modified_on")]
+ public DateTime? CommentModifiedOn { get; set; }
+
+ ///
+ /// When the record tags were last modified. Omitted if there are no tags.
+ ///
+ [JsonProperty("tags_modified_on")]
+ public DateTime? TagsModifiedOn { get; set; }
+
+ #endregion Response
+ }
+}
diff --git a/src/Extensions/Cloudflare.Dns/Models/DnsRecordSettings.cs b/src/Extensions/Cloudflare.Dns/Models/DnsRecordSettings.cs
new file mode 100644
index 0000000..d1158ae
--- /dev/null
+++ b/src/Extensions/Cloudflare.Dns/Models/DnsRecordSettings.cs
@@ -0,0 +1,26 @@
+namespace AMWD.Net.Api.Cloudflare.Dns
+{
+ ///
+ /// IP Settings for the DNS record.
+ ///
+ public class DnsRecordSettings
+ {
+ ///
+ /// 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.
+ ///
+ [JsonProperty("ipv4_only")]
+ public bool? IPv4Only { get; set; }
+
+ ///
+ /// 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.
+ ///
+ [JsonProperty("ipv6_only")]
+ public bool? IPv6Only { get; set; }
+ }
+}
diff --git a/src/Extensions/Cloudflare.Dns/Models/Records/AAAARecord.cs b/src/Extensions/Cloudflare.Dns/Models/Records/AAAARecord.cs
new file mode 100644
index 0000000..91f0bc0
--- /dev/null
+++ b/src/Extensions/Cloudflare.Dns/Models/Records/AAAARecord.cs
@@ -0,0 +1,18 @@
+namespace AMWD.Net.Api.Cloudflare.Dns
+{
+ ///
+ /// IPv6 address record.
+ ///
+ public class AAAARecord : DnsRecord
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// DNS record name (or @ for the zone apex) in Punycode.
+ public AAAARecord(string name)
+ : base(name)
+ {
+ Type = DnsRecordType.AAAA;
+ }
+ }
+}
diff --git a/src/Extensions/Cloudflare.Dns/Models/Records/ARecord.cs b/src/Extensions/Cloudflare.Dns/Models/Records/ARecord.cs
new file mode 100644
index 0000000..91091bc
--- /dev/null
+++ b/src/Extensions/Cloudflare.Dns/Models/Records/ARecord.cs
@@ -0,0 +1,18 @@
+namespace AMWD.Net.Api.Cloudflare.Dns
+{
+ ///
+ /// Address record.
+ ///
+ public class ARecord : DnsRecord
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// DNS record name (or @ for the zone apex) in Punycode.
+ public ARecord(string name)
+ : base(name)
+ {
+ Type = DnsRecordType.A;
+ }
+ }
+}
diff --git a/src/Extensions/Cloudflare.Dns/Models/Records/CAARecord.cs b/src/Extensions/Cloudflare.Dns/Models/Records/CAARecord.cs
new file mode 100644
index 0000000..130c8e4
--- /dev/null
+++ b/src/Extensions/Cloudflare.Dns/Models/Records/CAARecord.cs
@@ -0,0 +1,48 @@
+namespace AMWD.Net.Api.Cloudflare.Dns
+{
+ ///
+ /// Certification Authority Authorization record.
+ ///
+ public class CAARecord : DnsRecord
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// DNS record name (or @ for the zone apex) in Punycode.
+ public CAARecord(string name)
+ : base(name)
+ {
+ Type = DnsRecordType.CAA;
+ }
+
+ ///
+ /// Components of a CAA record.
+ ///
+ [JsonProperty("data")]
+ public CAARecordData? Data { get; set; }
+ }
+
+ ///
+ /// Components of a CAA record.
+ ///
+ public class CAARecordData
+ {
+ ///
+ /// Flags for the CAA record.
+ ///
+ [JsonProperty("flags")]
+ public int? Flags { get; set; }
+
+ ///
+ /// Name of the property controlled by this record (e.g.: issue, issuewild, iodef).
+ ///
+ [JsonProperty("tag")]
+ public string? Tag { get; set; }
+
+ ///
+ /// Value of the record. This field's semantics depend on the chosen tag.
+ ///
+ [JsonProperty("value")]
+ public string? Value { get; set; }
+ }
+}
diff --git a/src/Extensions/Cloudflare.Dns/Models/Records/CERTRecord.cs b/src/Extensions/Cloudflare.Dns/Models/Records/CERTRecord.cs
new file mode 100644
index 0000000..cf5a83a
--- /dev/null
+++ b/src/Extensions/Cloudflare.Dns/Models/Records/CERTRecord.cs
@@ -0,0 +1,54 @@
+namespace AMWD.Net.Api.Cloudflare.Dns
+{
+ ///
+ /// Certificate record.
+ ///
+ public class CERTRecord : DnsRecord
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// DNS record name (or @ for the zone apex) in Punycode.
+ public CERTRecord(string name)
+ : base(name)
+ {
+ Type = DnsRecordType.CERT;
+ }
+
+ ///
+ /// Components of a CERT record.
+ ///
+ [JsonProperty("data")]
+ public CERTRecordData? Data { get; set; }
+ }
+
+ ///
+ /// Components of a CERT record.
+ ///
+ public class CERTRecordData
+ {
+ ///
+ /// Algorithm.
+ ///
+ [JsonProperty("algorithm")]
+ public int? Algorithm { get; set; }
+
+ ///
+ /// Certificate.
+ ///
+ [JsonProperty("certificate")]
+ public string? Certificate { get; set; }
+
+ ///
+ /// Key tag.
+ ///
+ [JsonProperty("key_tag")]
+ public int? KeyTag { get; set; }
+
+ ///
+ /// Type.
+ ///
+ [JsonProperty("type")]
+ public int? Type { get; set; }
+ }
+}
diff --git a/src/Extensions/Cloudflare.Dns/Models/Records/CNAMERecord.cs b/src/Extensions/Cloudflare.Dns/Models/Records/CNAMERecord.cs
new file mode 100644
index 0000000..f2145b4
--- /dev/null
+++ b/src/Extensions/Cloudflare.Dns/Models/Records/CNAMERecord.cs
@@ -0,0 +1,41 @@
+namespace AMWD.Net.Api.Cloudflare.Dns
+{
+ ///
+ /// Canonical Name record.
+ ///
+ public class CNAMERecord : DnsRecord
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// DNS record name (or @ for the zone apex) in Punycode.
+ public CNAMERecord(string name)
+ : base(name)
+ {
+ Type = DnsRecordType.CNAME;
+ }
+
+ ///
+ /// Settings for the DNS record.
+ ///
+ [JsonProperty("settings")]
+ public new CNAMERecordSettings? Settings { get; set; }
+ }
+
+ ///
+ /// Settings for the DNS record.
+ ///
+ public class CNAMERecordSettings : DnsRecordSettings
+ {
+ ///
+ /// 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.
+ ///
+ ///
+ /// This setting is unavailable for proxied records, since they are always flattened.
+ ///
+ [JsonProperty("flatten_cname")]
+ public bool? FlattenCname { get; set; }
+ }
+}
diff --git a/src/Extensions/Cloudflare.Dns/Models/Records/DNSKEYRecord.cs b/src/Extensions/Cloudflare.Dns/Models/Records/DNSKEYRecord.cs
new file mode 100644
index 0000000..5bb658e
--- /dev/null
+++ b/src/Extensions/Cloudflare.Dns/Models/Records/DNSKEYRecord.cs
@@ -0,0 +1,54 @@
+namespace AMWD.Net.Api.Cloudflare.Dns
+{
+ ///
+ /// DNS Key record.
+ ///
+ public class DNSKEYRecord : DnsRecord
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// DNS record name (or @ for the zone apex) in Punycode.
+ public DNSKEYRecord(string name)
+ : base(name)
+ {
+ Type = DnsRecordType.DNSKEY;
+ }
+
+ ///
+ /// Components of a DNSKEY record.
+ ///
+ [JsonProperty("data")]
+ public DNSKEYRecordData? Data { get; set; }
+ }
+
+ ///
+ /// Components of a DNSKEY record.
+ ///
+ public class DNSKEYRecordData
+ {
+ ///
+ /// Algorithm.
+ ///
+ [JsonProperty("algorithm")]
+ public int? Algorithm { get; set; }
+
+ ///
+ /// Flags.
+ ///
+ [JsonProperty("flags")]
+ public int? Flags { get; set; }
+
+ ///
+ /// Protocol.
+ ///
+ [JsonProperty("protocol")]
+ public int? Protocol { get; set; }
+
+ ///
+ /// Public key.
+ ///
+ [JsonProperty("public_key")]
+ public string? PublicKey { get; set; }
+ }
+}
diff --git a/src/Extensions/Cloudflare.Dns/Models/Records/DSRecord.cs b/src/Extensions/Cloudflare.Dns/Models/Records/DSRecord.cs
new file mode 100644
index 0000000..e6402f0
--- /dev/null
+++ b/src/Extensions/Cloudflare.Dns/Models/Records/DSRecord.cs
@@ -0,0 +1,54 @@
+namespace AMWD.Net.Api.Cloudflare.Dns
+{
+ ///
+ /// Delegate Signer record.
+ ///
+ public class DSRecord : DnsRecord
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// DNS record name (or @ for the zone apex) in Punycode.
+ public DSRecord(string name)
+ : base(name)
+ {
+ Type = DnsRecordType.DS;
+ }
+
+ ///
+ /// Components of a DS record.
+ ///
+ [JsonProperty("data")]
+ public DSRecordData? Data { get; set; }
+ }
+
+ ///
+ /// Components of a DS record.
+ ///
+ public class DSRecordData
+ {
+ ///
+ /// Algorithm.
+ ///
+ [JsonProperty("algorithm")]
+ public int? Algorithm { get; set; }
+
+ ///
+ /// Digest.
+ ///
+ [JsonProperty("digest")]
+ public string? Digest { get; set; }
+
+ ///
+ /// Digest type.
+ ///
+ [JsonProperty("digest_type")]
+ public int? DigestType { get; set; }
+
+ ///
+ /// Key tag.
+ ///
+ [JsonProperty("key_tag")]
+ public int? KeyTag { get; set; }
+ }
+}
diff --git a/src/Extensions/Cloudflare.Dns/Models/Records/HTTPSRecord.cs b/src/Extensions/Cloudflare.Dns/Models/Records/HTTPSRecord.cs
new file mode 100644
index 0000000..f789326
--- /dev/null
+++ b/src/Extensions/Cloudflare.Dns/Models/Records/HTTPSRecord.cs
@@ -0,0 +1,48 @@
+namespace AMWD.Net.Api.Cloudflare.Dns
+{
+ ///
+ /// HTTPS binding record.
+ ///
+ public class HTTPSRecord : DnsRecord
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// DNS record name (or @ for the zone apex) in Punycode.
+ public HTTPSRecord(string name)
+ : base(name)
+ {
+ Type = DnsRecordType.HTTPS;
+ }
+
+ ///
+ /// Components of a HTTPS record.
+ ///
+ [JsonProperty("data")]
+ public HTTPSRecordData? Data { get; set; }
+ }
+
+ ///
+ /// Components of a HTTPS record.
+ ///
+ public class HTTPSRecordData
+ {
+ ///
+ /// Priority.
+ ///
+ [JsonProperty("priority")]
+ public int? Priority { get; set; }
+
+ ///
+ /// Target.
+ ///
+ [JsonProperty("target")]
+ public string? Target { get; set; }
+
+ ///
+ /// Value.
+ ///
+ [JsonProperty("value")]
+ public string? Value { get; set; }
+ }
+}
diff --git a/src/Extensions/Cloudflare.Dns/Models/Records/LOCRecord.cs b/src/Extensions/Cloudflare.Dns/Models/Records/LOCRecord.cs
new file mode 100644
index 0000000..0ca52e2
--- /dev/null
+++ b/src/Extensions/Cloudflare.Dns/Models/Records/LOCRecord.cs
@@ -0,0 +1,143 @@
+using System.Runtime.Serialization;
+using Newtonsoft.Json.Converters;
+
+namespace AMWD.Net.Api.Cloudflare.Dns
+{
+ ///
+ /// Location record.
+ ///
+ public class LOCRecord : DnsRecord
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// DNS record name (or @ for the zone apex) in Punycode.
+ public LOCRecord(string name)
+ : base(name)
+ {
+ Type = DnsRecordType.LOC;
+ }
+
+ ///
+ /// Components of a LOC record.
+ ///
+ [JsonProperty("data")]
+ public LOCRecordData? Data { get; set; }
+ }
+
+ ///
+ /// Components of a LOC record.
+ ///
+ public class LOCRecordData
+ {
+ ///
+ /// Altitude of location in meters.
+ ///
+ [JsonProperty("altitude")]
+ public double? Altitude { get; set; }
+
+ ///
+ /// Degrees of latitude.
+ ///
+ [JsonProperty("lat_degrees")]
+ public double? LatitudeDegrees { get; set; }
+
+ ///
+ /// Latitude direction.
+ ///
+ [JsonProperty("lat_direction")]
+ public LOCRecordLatitudeDirection? LatitudeDirection { get; set; }
+
+ ///
+ /// Minutes of latitude.
+ ///
+ [JsonProperty("lat_minutes")]
+ public double? LatitudeMinutes { get; set; }
+
+ ///
+ /// Seconds of latitude.
+ ///
+ [JsonProperty("lat_seconds")]
+ public double? LatitudeSeconds { get; set; }
+
+ ///
+ /// Degrees of longitude.
+ ///
+ [JsonProperty("long_degrees")]
+ public double? LongitudeDegrees { get; set; }
+
+ ///
+ /// Longitude direction.
+ ///
+ [JsonProperty("long_direction")]
+ public LOCRecordLongitudeDirection? LongitudeDirection { get; set; }
+
+ ///
+ /// Minutes of longitude.
+ ///
+ [JsonProperty("long_minutes")]
+ public double? LongitudeMinutes { get; set; }
+
+ ///
+ /// Seconds of longitude.
+ ///
+ [JsonProperty("long_seconds")]
+ public double? LongitudeSeconds { get; set; }
+
+ ///
+ /// Horizontal precision of location.
+ ///
+ [JsonProperty("precision_horz")]
+ public double? PrecisionHorizontal { get; set; }
+
+ ///
+ /// Vertical precision of location.
+ ///
+ [JsonProperty("precision_vert")]
+ public double? PrecisionVertical { get; set; }
+
+ ///
+ /// Size of location in meters.
+ ///
+ [JsonProperty("size")]
+ public double? Size { get; set; }
+ }
+
+ ///
+ /// Location record latitude direction.
+ ///
+ [JsonConverter(typeof(StringEnumConverter))]
+ public enum LOCRecordLatitudeDirection
+ {
+ ///
+ /// North.
+ ///
+ [EnumMember(Value = "N")]
+ North = 1,
+
+ ///
+ /// South.
+ ///
+ [EnumMember(Value = "S")]
+ South = 2
+ }
+
+ ///
+ /// Location record longitude direction.
+ ///
+ [JsonConverter(typeof(StringEnumConverter))]
+ public enum LOCRecordLongitudeDirection
+ {
+ ///
+ /// East.
+ ///
+ [EnumMember(Value = "E")]
+ East = 1,
+
+ ///
+ /// West.
+ ///
+ [EnumMember(Value = "W")]
+ West = 2
+ }
+}
diff --git a/src/Extensions/Cloudflare.Dns/Models/Records/MXRecord.cs b/src/Extensions/Cloudflare.Dns/Models/Records/MXRecord.cs
new file mode 100644
index 0000000..0678ecb
--- /dev/null
+++ b/src/Extensions/Cloudflare.Dns/Models/Records/MXRecord.cs
@@ -0,0 +1,25 @@
+namespace AMWD.Net.Api.Cloudflare.Dns
+{
+ ///
+ /// Mail Exchange record.
+ ///
+ public class MXRecord : DnsRecord
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// DNS record name (or @ for the zone apex) in Punycode.
+ public MXRecord(string name)
+ : base(name)
+ {
+ Type = DnsRecordType.MX;
+ }
+
+ ///
+ /// Required for MX, SRV and URI records; unused by other record types.
+ /// Records with lower priorities are preferred.
+ ///
+ [JsonProperty("priority")]
+ public int? Priority { get; set; }
+ }
+}
diff --git a/src/Extensions/Cloudflare.Dns/Models/Records/NAPTRRecord.cs b/src/Extensions/Cloudflare.Dns/Models/Records/NAPTRRecord.cs
new file mode 100644
index 0000000..82bbd32
--- /dev/null
+++ b/src/Extensions/Cloudflare.Dns/Models/Records/NAPTRRecord.cs
@@ -0,0 +1,66 @@
+namespace AMWD.Net.Api.Cloudflare.Dns
+{
+ ///
+ /// Naming authority pointer record.
+ ///
+ public class NAPTRRecord : DnsRecord
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// DNS record name (or @ for the zone apex) in Punycode.
+ public NAPTRRecord(string name)
+ : base(name)
+ {
+ Type = DnsRecordType.NAPTR;
+ }
+
+ ///
+ /// Components of a NAPTR record.
+ ///
+ [JsonProperty("data")]
+ public NAPTRRecordData? Data { get; set; }
+ }
+
+ ///
+ /// Components of a NAPTR record.
+ ///
+ public class NAPTRRecordData
+ {
+ ///
+ /// Flags.
+ ///
+ [JsonProperty("flags")]
+ public string? Flags { get; set; }
+
+ ///
+ /// Order.
+ ///
+ [JsonProperty("order")]
+ public int? Order { get; set; }
+
+ ///
+ /// Preference.
+ ///
+ [JsonProperty("preference")]
+ public int? Preference { get; set; }
+
+ ///
+ /// Regular expression.
+ ///
+ [JsonProperty("regex")]
+ public string? Regex { get; set; }
+
+ ///
+ /// Replacement.
+ ///
+ [JsonProperty("replacement")]
+ public string? Replacement { get; set; }
+
+ ///
+ /// Service.
+ ///
+ [JsonProperty("service")]
+ public string? Service { get; set; }
+ }
+}
diff --git a/src/Extensions/Cloudflare.Dns/Models/Records/NSRecord.cs b/src/Extensions/Cloudflare.Dns/Models/Records/NSRecord.cs
new file mode 100644
index 0000000..d7b38af
--- /dev/null
+++ b/src/Extensions/Cloudflare.Dns/Models/Records/NSRecord.cs
@@ -0,0 +1,18 @@
+namespace AMWD.Net.Api.Cloudflare.Dns
+{
+ ///
+ /// Name Server record.
+ ///
+ public class NSRecord : DnsRecord
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// DNS record name (or @ for the zone apex) in Punycode.
+ public NSRecord(string name)
+ : base(name)
+ {
+ Type = DnsRecordType.NS;
+ }
+ }
+}
diff --git a/src/Extensions/Cloudflare.Dns/Models/Records/OPENPGPKEYRecord.cs b/src/Extensions/Cloudflare.Dns/Models/Records/OPENPGPKEYRecord.cs
new file mode 100644
index 0000000..421bb00
--- /dev/null
+++ b/src/Extensions/Cloudflare.Dns/Models/Records/OPENPGPKEYRecord.cs
@@ -0,0 +1,24 @@
+namespace AMWD.Net.Api.Cloudflare.Dns
+{
+ ///
+ /// OpenPGP Publi Key record.
+ ///
+ public class OPENPGPKEYRecord : DnsRecord
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// DNS record name (or @ for the zone apex) in Punycode.
+ public OPENPGPKEYRecord(string name)
+ : base(name)
+ {
+ Type = DnsRecordType.OPENPGPKEY;
+ }
+
+ ///
+ /// A single Base64-encoded OpenPGP Transferable Public Key (RFC 4880 Section 11.1).
+ ///
+ [JsonProperty("content")]
+ public new string? Content { get; set; }
+ }
+}
diff --git a/src/Extensions/Cloudflare.Dns/Models/Records/PTRRecord.cs b/src/Extensions/Cloudflare.Dns/Models/Records/PTRRecord.cs
new file mode 100644
index 0000000..6ba6702
--- /dev/null
+++ b/src/Extensions/Cloudflare.Dns/Models/Records/PTRRecord.cs
@@ -0,0 +1,18 @@
+namespace AMWD.Net.Api.Cloudflare.Dns
+{
+ ///
+ /// PTR resource record.
+ ///
+ public class PTRRecord : DnsRecord
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// DNS record name (or @ for the zone apex) in Punycode.
+ public PTRRecord(string name)
+ : base(name)
+ {
+ Type = DnsRecordType.PTR;
+ }
+ }
+}
diff --git a/src/Extensions/Cloudflare.Dns/Models/Records/SMIMEARecord.cs b/src/Extensions/Cloudflare.Dns/Models/Records/SMIMEARecord.cs
new file mode 100644
index 0000000..2111f73
--- /dev/null
+++ b/src/Extensions/Cloudflare.Dns/Models/Records/SMIMEARecord.cs
@@ -0,0 +1,54 @@
+namespace AMWD.Net.Api.Cloudflare.Dns
+{
+ ///
+ /// S/MIME cert association record.
+ ///
+ public class SMIMEARecord : DnsRecord
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// DNS record name (or @ for the zone apex) in Punycode.
+ public SMIMEARecord(string name)
+ : base(name)
+ {
+ Type = DnsRecordType.SMIMEA;
+ }
+
+ ///
+ /// Components of a SMIMEA record.
+ ///
+ [JsonProperty("data")]
+ public SMIMEARecordData? Data { get; set; }
+ }
+
+ ///
+ /// Components of a SMIMEA record.
+ ///
+ public class SMIMEARecordData
+ {
+ ///
+ /// Certificate.
+ ///
+ [JsonProperty("certificate")]
+ public string? Certificate { get; set; }
+
+ ///
+ /// Matching type.
+ ///
+ [JsonProperty("matching_type")]
+ public int? MatchingType { get; set; }
+
+ ///
+ /// Selector.
+ ///
+ [JsonProperty("selector")]
+ public int? Selector { get; set; }
+
+ ///
+ /// Usage.
+ ///
+ [JsonProperty("usage")]
+ public int? Usage { get; set; }
+ }
+}
diff --git a/src/Extensions/Cloudflare.Dns/Models/Records/SRVRecord.cs b/src/Extensions/Cloudflare.Dns/Models/Records/SRVRecord.cs
new file mode 100644
index 0000000..5337e5d
--- /dev/null
+++ b/src/Extensions/Cloudflare.Dns/Models/Records/SRVRecord.cs
@@ -0,0 +1,55 @@
+namespace AMWD.Net.Api.Cloudflare.Dns
+{
+ ///
+ /// Service locator record.
+ ///
+ public class SRVRecord : DnsRecord
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// DNS record name (or @ for the zone apex) in Punycode.
+ public SRVRecord(string name)
+ : base(name)
+ {
+ Type = DnsRecordType.SRV;
+ }
+
+ ///
+ /// Components of a SRV record.
+ ///
+ [JsonProperty("data")]
+ public SRVRecordData? Data { get; set; }
+ }
+
+ ///
+ /// Components of a SRV record.
+ ///
+ public class SRVRecordData
+ {
+ ///
+ /// The port of the service.
+ ///
+ [JsonProperty("port")]
+ public int? Port { get; set; }
+
+ ///
+ /// Required for MX, SRV and URI records; unused by other record types.
+ /// Records with lower priorities are preferred.
+ ///
+ [JsonProperty("priority")]
+ public int? Priority { get; set; }
+
+ ///
+ /// A valid hostname.
+ ///
+ [JsonProperty("target")]
+ public string? Target { get; set; }
+
+ ///
+ /// The record weight.
+ ///
+ [JsonProperty("weight")]
+ public int? Weight { get; set; }
+ }
+}
diff --git a/src/Extensions/Cloudflare.Dns/Models/Records/SSHFPRecord.cs b/src/Extensions/Cloudflare.Dns/Models/Records/SSHFPRecord.cs
new file mode 100644
index 0000000..ce09115
--- /dev/null
+++ b/src/Extensions/Cloudflare.Dns/Models/Records/SSHFPRecord.cs
@@ -0,0 +1,48 @@
+namespace AMWD.Net.Api.Cloudflare.Dns
+{
+ ///
+ /// SSH Public Key Fingerprint record.
+ ///
+ public class SSHFPRecord : DnsRecord
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// DNS record name (or @ for the zone apex) in Punycode.
+ public SSHFPRecord(string name)
+ : base(name)
+ {
+ Type = DnsRecordType.SSHFP;
+ }
+
+ ///
+ /// Components of a SSHFP record.
+ ///
+ [JsonProperty("data")]
+ public SSHFPRecordData? Data { get; set; }
+ }
+
+ ///
+ /// Components of a SSHFP record.
+ ///
+ public class SSHFPRecordData
+ {
+ ///
+ /// Algorithm.
+ ///
+ [JsonProperty("algorithm")]
+ public int? Algorithm { get; set; }
+
+ ///
+ /// Fingerprint.
+ ///
+ [JsonProperty("fingerprint")]
+ public string? Fingerprint { get; set; }
+
+ ///
+ /// Type.
+ ///
+ [JsonProperty("type")]
+ public int? Type { get; set; }
+ }
+}
diff --git a/src/Extensions/Cloudflare.Dns/Models/Records/SVCBRecord.cs b/src/Extensions/Cloudflare.Dns/Models/Records/SVCBRecord.cs
new file mode 100644
index 0000000..b838b4d
--- /dev/null
+++ b/src/Extensions/Cloudflare.Dns/Models/Records/SVCBRecord.cs
@@ -0,0 +1,48 @@
+namespace AMWD.Net.Api.Cloudflare.Dns
+{
+ ///
+ /// Service Binding record.
+ ///
+ public class SVCBRecord : DnsRecord
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// DNS record name (or @ for the zone apex) in Punycode.
+ public SVCBRecord(string name)
+ : base(name)
+ {
+ Type = DnsRecordType.SVCB;
+ }
+
+ ///
+ /// Components of a SVCB record.
+ ///
+ [JsonProperty("data")]
+ public SVCBRecordData? Data { get; set; }
+ }
+
+ ///
+ /// Components of a SVCB record.
+ ///
+ public class SVCBRecordData
+ {
+ ///
+ /// Priority.
+ ///
+ [JsonProperty("priority")]
+ public int? Priority { get; set; }
+
+ ///
+ /// Target.
+ ///
+ [JsonProperty("target")]
+ public string? Target { get; set; }
+
+ ///
+ /// Value.
+ ///
+ [JsonProperty("value")]
+ public string? Value { get; set; }
+ }
+}
diff --git a/src/Extensions/Cloudflare.Dns/Models/Records/TLSARecord.cs b/src/Extensions/Cloudflare.Dns/Models/Records/TLSARecord.cs
new file mode 100644
index 0000000..ace8c9e
--- /dev/null
+++ b/src/Extensions/Cloudflare.Dns/Models/Records/TLSARecord.cs
@@ -0,0 +1,54 @@
+namespace AMWD.Net.Api.Cloudflare.Dns
+{
+ ///
+ /// TLSA certificate association record.
+ ///
+ public class TLSARecord : DnsRecord
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// DNS record name (or @ for the zone apex) in Punycode.
+ public TLSARecord(string name)
+ : base(name)
+ {
+ Type = DnsRecordType.TLSA;
+ }
+
+ ///
+ /// Components of a TLSA record.
+ ///
+ [JsonProperty("data")]
+ public TLSARecordData? Data { get; set; }
+ }
+
+ ///
+ /// Components of a TLSA record.
+ ///
+ public class TLSARecordData
+ {
+ ///
+ /// Certificate.
+ ///
+ [JsonProperty("certificate")]
+ public string? Certificate { get; set; }
+
+ ///
+ /// Matching type.
+ ///
+ [JsonProperty("matching_type")]
+ public int? MatchingType { get; set; }
+
+ ///
+ /// Selector.
+ ///
+ [JsonProperty("selector")]
+ public int? Selector { get; set; }
+
+ ///
+ /// Usage.
+ ///
+ [JsonProperty("usage")]
+ public int? Usage { get; set; }
+ }
+}
diff --git a/src/Extensions/Cloudflare.Dns/Models/Records/TXTRecord.cs b/src/Extensions/Cloudflare.Dns/Models/Records/TXTRecord.cs
new file mode 100644
index 0000000..d860339
--- /dev/null
+++ b/src/Extensions/Cloudflare.Dns/Models/Records/TXTRecord.cs
@@ -0,0 +1,29 @@
+namespace AMWD.Net.Api.Cloudflare.Dns
+{
+ ///
+ /// Text record.
+ ///
+ public class TXTRecord : DnsRecord
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// DNS record name (or @ for the zone apex) in Punycode.
+ public TXTRecord(string name)
+ : base(name)
+ {
+ Type = DnsRecordType.TXT;
+ }
+
+ ///
+ /// 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.
+ ///
+ ///
+ /// Learn more at .
+ ///
+ [JsonProperty("content")]
+ public new string? Content { get; set; }
+ }
+}
diff --git a/src/Extensions/Cloudflare.Dns/Models/Records/URIRecord.cs b/src/Extensions/Cloudflare.Dns/Models/Records/URIRecord.cs
new file mode 100644
index 0000000..e24fd14
--- /dev/null
+++ b/src/Extensions/Cloudflare.Dns/Models/Records/URIRecord.cs
@@ -0,0 +1,49 @@
+namespace AMWD.Net.Api.Cloudflare.Dns
+{
+ ///
+ /// Uniform Resource Identifier record.
+ ///
+ public class URIRecord : DnsRecord
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// DNS record name (or @ for the zone apex) in Punycode.
+ public URIRecord(string name)
+ : base(name)
+ {
+ Type = DnsRecordType.URI;
+ }
+
+ ///
+ /// Required for MX, SRV and URI records; unused by other record types.
+ /// Records with lower priorities are preferred.
+ ///
+ [JsonProperty("priority")]
+ public int? Priority { get; set; }
+
+ ///
+ /// Components of a URI record.
+ ///
+ [JsonProperty("data")]
+ public URIRecordData? Data { get; set; }
+ }
+
+ ///
+ /// Components of a URI record.
+ ///
+ public class URIRecordData
+ {
+ ///
+ /// The record content.
+ ///
+ [JsonProperty("target")]
+ public string? Target { get; set; }
+
+ ///
+ /// The record weight.
+ ///
+ [JsonProperty("weight")]
+ public int? Weight { get; set; }
+ }
+}
diff --git a/src/Extensions/Cloudflare.Dns/README.md b/src/Extensions/Cloudflare.Dns/README.md
index 7f9f404..fb86f83 100644
--- a/src/Extensions/Cloudflare.Dns/README.md
+++ b/src/Extensions/Cloudflare.Dns/README.md
@@ -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/)
+### [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/
[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/
diff --git a/src/Extensions/Cloudflare.Dns/Requests/BatchDnsRecordsRequest.cs b/src/Extensions/Cloudflare.Dns/Requests/BatchDnsRecordsRequest.cs
new file mode 100644
index 0000000..61cb09b
--- /dev/null
+++ b/src/Extensions/Cloudflare.Dns/Requests/BatchDnsRecordsRequest.cs
@@ -0,0 +1,159 @@
+namespace AMWD.Net.Api.Cloudflare.Dns
+{
+ ///
+ /// Represents a request to manipulate DNS records.
+ ///
+ public class BatchDnsRecordsRequest
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The zone identifier.
+ public BatchDnsRecordsRequest(string zoneId)
+ {
+ ZoneId = zoneId;
+ }
+
+ ///
+ /// The zone identifier.
+ ///
+ public string ZoneId { get; set; }
+
+ ///
+ /// The DNS records to delete.
+ ///
+ public IReadOnlyCollection? Deletes { get; set; }
+
+ ///
+ /// The DNS records to update.
+ ///
+ public IReadOnlyCollection? Updates { get; set; }
+
+ ///
+ /// The DNS records to create.
+ ///
+ public IReadOnlyCollection? Creates { get; set; }
+
+ ///
+ /// The DNS records to overwrite.
+ ///
+ public IReadOnlyCollection? Overwrites { get; set; }
+
+ ///
+ /// Represents a request to update a DNS record.
+ ///
+ public class Patch : Post
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The DNS record identifier.
+ /// DNS record name (or @ for the zone apex) in Punycode.
+ public Patch(string id, string name)
+ : base(name)
+ {
+ Id = id;
+ }
+
+ ///
+ /// The DNS record identifier.
+ ///
+ public string Id { get; set; }
+ }
+
+ ///
+ /// Represents a request to create a DNS record.
+ ///
+ public class Post
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// DNS record name (or @ for the zone apex) in Punycode.
+ public Post(string name)
+ {
+ Name = name;
+ }
+
+ ///
+ /// DNS record name (or @ for the zone apex) in Punycode.
+ ///
+ public string Name { get; set; }
+
+ ///
+ /// Time To Live (TTL) of the DNS record in seconds.
+ ///
+ ///
+ /// Setting to 1 means 'automatic'. Value must be between 60 and 86400, with the
+ /// minimum reduced to 30 for Enterprise zones.
+ ///
+ public int? TimeToLive { get; set; }
+
+ ///
+ /// DNS record type.
+ ///
+ public DnsRecordType Type { get; set; }
+
+ ///
+ /// Comments or notes about the DNS record.
+ /// This field has no effect on DNS responses.
+ ///
+ public string? Comment { get; set; }
+
+ ///
+ /// The content of the DNS record.
+ ///
+ public string? Content { get; set; }
+
+ ///
+ /// Components of a record.
+ ///
+ public object? Data { get; set; }
+
+ ///
+ /// Required for MX, SRV and URI records; unused by other record types.
+ /// Records with lower priorities are preferred.
+ ///
+ public int? Priority { get; set; }
+
+ ///
+ /// Whether the record is receiving the performance and security benefits of
+ /// Cloudflare.
+ ///
+ public bool? Proxied { get; set; }
+
+ ///
+ /// Settings for the DNS record.
+ ///
+ public DnsRecordSettings? Settings { get; set; }
+
+ ///
+ /// Custom tags for the DNS record.
+ /// This field has no effect on DNS responses.
+ ///
+ public IReadOnlyCollection? Tags { get; set; }
+ }
+
+ ///
+ /// Represents a request to overwrite a DNS record.
+ ///
+ public class Put : Post
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The DNS record identifier.
+ /// DNS record name (or @ for the zone apex) in Punycode.
+ public Put(string id, string name)
+ : base(name)
+ {
+ Id = id;
+ }
+
+ ///
+ /// The DNS record identifier.
+ ///
+ public string Id { get; set; }
+ }
+ }
+}
diff --git a/src/Extensions/Cloudflare.Dns/Requests/CreateDnsRecordRequest.cs b/src/Extensions/Cloudflare.Dns/Requests/CreateDnsRecordRequest.cs
new file mode 100644
index 0000000..a7eeac5
--- /dev/null
+++ b/src/Extensions/Cloudflare.Dns/Requests/CreateDnsRecordRequest.cs
@@ -0,0 +1,82 @@
+namespace AMWD.Net.Api.Cloudflare.Dns
+{
+ ///
+ /// Represents a request to create a DNS record.
+ ///
+ public class CreateDnsRecordRequest
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The zone identifier.
+ /// DNS record name (or @ for the zone apex) in Punycode.
+ public CreateDnsRecordRequest(string zoneId, string name)
+ {
+ ZoneId = zoneId;
+ Name = name;
+ }
+
+ ///
+ /// The zone identifier.
+ ///
+ public string ZoneId { get; set; }
+
+ ///
+ /// DNS record name (or @ for the zone apex) in Punycode.
+ ///
+ public string Name { get; set; }
+
+ ///
+ /// Time To Live (TTL) of the DNS record in seconds.
+ ///
+ ///
+ /// Setting to 1 means 'automatic'. Value must be between 60 and 86400, with the
+ /// minimum reduced to 30 for Enterprise zones.
+ ///
+ public int? TimeToLive { get; set; }
+
+ ///
+ /// DNS record type.
+ ///
+ public DnsRecordType Type { get; set; }
+
+ ///
+ /// Comments or notes about the DNS record.
+ /// This field has no effect on DNS responses.
+ ///
+ public string? Comment { get; set; }
+
+ ///
+ /// The content of the DNS record.
+ ///
+ public string? Content { get; set; }
+
+ ///
+ /// Components of a record.
+ ///
+ public object? Data { get; set; }
+
+ ///
+ /// Required for MX, SRV and URI records; unused by other record types.
+ /// Records with lower priorities are preferred.
+ ///
+ public int? Priority { get; set; }
+
+ ///
+ /// Whether the record is receiving the performance and security benefits of
+ /// Cloudflare.
+ ///
+ public bool? Proxied { get; set; }
+
+ ///
+ /// Settings for the DNS record.
+ ///
+ public DnsRecordSettings? Settings { get; set; }
+
+ ///
+ /// Custom tags for the DNS record.
+ /// This field has no effect on DNS responses.
+ ///
+ public IReadOnlyCollection? Tags { get; set; }
+ }
+}
diff --git a/src/Extensions/Cloudflare.Dns/Requests/DeleteDnsRecordRequest.cs b/src/Extensions/Cloudflare.Dns/Requests/DeleteDnsRecordRequest.cs
new file mode 100644
index 0000000..5040aaa
--- /dev/null
+++ b/src/Extensions/Cloudflare.Dns/Requests/DeleteDnsRecordRequest.cs
@@ -0,0 +1,29 @@
+namespace AMWD.Net.Api.Cloudflare.Dns
+{
+ ///
+ /// Represents a request to delete a DNS record.
+ ///
+ public class DeleteDnsRecordRequest
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The zone identifier.
+ /// The DNS record identifier.
+ public DeleteDnsRecordRequest(string zoneId, string recordId)
+ {
+ ZoneId = zoneId;
+ RecordId = recordId;
+ }
+
+ ///
+ /// The zone identifier.
+ ///
+ public string ZoneId { get; set; }
+
+ ///
+ /// The DNS record identifier.
+ ///
+ public string RecordId { get; set; }
+ }
+}
diff --git a/src/Extensions/Cloudflare.Dns/Requests/ImportDnsRecordsRequest.cs b/src/Extensions/Cloudflare.Dns/Requests/ImportDnsRecordsRequest.cs
new file mode 100644
index 0000000..11c8638
--- /dev/null
+++ b/src/Extensions/Cloudflare.Dns/Requests/ImportDnsRecordsRequest.cs
@@ -0,0 +1,33 @@
+namespace AMWD.Net.Api.Cloudflare.Dns
+{
+ ///
+ /// Represents a request to import DNS records.
+ ///
+ public class ImportDnsRecordsRequest
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The zone identifier.
+ public ImportDnsRecordsRequest(string zoneId)
+ {
+ ZoneId = zoneId;
+ }
+
+ ///
+ /// The zone identifier.
+ ///
+ public string ZoneId { get; set; }
+
+ ///
+ /// BIND config to import.
+ ///
+ public string? File { get; set; }
+
+ ///
+ /// Whether or not proxiable records should receive the performance and
+ /// security benefits of Cloudflare.
+ ///
+ public bool? Proxied { get; set; }
+ }
+}
diff --git a/src/Extensions/Cloudflare.Dns/Requests/OverwriteDnsRecordRequest.cs b/src/Extensions/Cloudflare.Dns/Requests/OverwriteDnsRecordRequest.cs
new file mode 100644
index 0000000..72e7404
--- /dev/null
+++ b/src/Extensions/Cloudflare.Dns/Requests/OverwriteDnsRecordRequest.cs
@@ -0,0 +1,25 @@
+namespace AMWD.Net.Api.Cloudflare.Dns
+{
+ ///
+ /// Represents a request to overwrite a DNS record.
+ ///
+ public class OverwriteDnsRecordRequest : CreateDnsRecordRequest
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The zone identifier.
+ /// The DNS record identifier.
+ /// DNS record name (or @ for the zone apex) in Punycode.
+ public OverwriteDnsRecordRequest(string zoneId, string recordId, string name)
+ : base(zoneId, name)
+ {
+ RecordId = recordId;
+ }
+
+ ///
+ /// The DNS record identifier.
+ ///
+ public string RecordId { get; set; }
+ }
+}
diff --git a/src/Extensions/Cloudflare.Dns/Requests/UpdateDnsRecordRequest.cs b/src/Extensions/Cloudflare.Dns/Requests/UpdateDnsRecordRequest.cs
new file mode 100644
index 0000000..f58bb78
--- /dev/null
+++ b/src/Extensions/Cloudflare.Dns/Requests/UpdateDnsRecordRequest.cs
@@ -0,0 +1,25 @@
+namespace AMWD.Net.Api.Cloudflare.Dns
+{
+ ///
+ /// Represents a request to update a DNS record.
+ ///
+ public class UpdateDnsRecordRequest : CreateDnsRecordRequest
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The zone identifier.
+ /// The DNS record identifier.
+ /// DNS record name (or @ for the zone apex) in Punycode.
+ public UpdateDnsRecordRequest(string zoneId, string recordId, string name)
+ : base(zoneId, name)
+ {
+ RecordId = recordId;
+ }
+
+ ///
+ /// The DNS record identifier.
+ ///
+ public string RecordId { get; set; }
+ }
+}
diff --git a/src/Extensions/Cloudflare.Dns/Responses/BatchDnsRecordsResponse.cs b/src/Extensions/Cloudflare.Dns/Responses/BatchDnsRecordsResponse.cs
new file mode 100644
index 0000000..54f40b3
--- /dev/null
+++ b/src/Extensions/Cloudflare.Dns/Responses/BatchDnsRecordsResponse.cs
@@ -0,0 +1,32 @@
+namespace AMWD.Net.Api.Cloudflare.Dns
+{
+ ///
+ /// The response for a batch update request.
+ ///
+ public class BatchDnsRecordsResponse
+ {
+ ///
+ /// The records that were deleted (DELETE).
+ ///
+ [JsonProperty("deletes")]
+ public IReadOnlyCollection? Deletes { get; set; }
+
+ ///
+ /// The records that were updated (PATCH).
+ ///
+ [JsonProperty("patches")]
+ public IReadOnlyCollection? Updates { get; set; }
+
+ ///
+ /// The records that were created (POST).
+ ///
+ [JsonProperty("posts")]
+ public IReadOnlyCollection? Creates { get; set; }
+
+ ///
+ /// The records that were overwritten (PUT).
+ ///
+ [JsonProperty("puts")]
+ public IReadOnlyCollection? Overwrites { get; set; }
+ }
+}
diff --git a/src/Extensions/Cloudflare.Dns/Responses/RecordImportResponse.cs b/src/Extensions/Cloudflare.Dns/Responses/RecordImportResponse.cs
new file mode 100644
index 0000000..28e6332
--- /dev/null
+++ b/src/Extensions/Cloudflare.Dns/Responses/RecordImportResponse.cs
@@ -0,0 +1,20 @@
+namespace AMWD.Net.Api.Cloudflare.Dns
+{
+ ///
+ /// The response for a DNS record import.
+ ///
+ public class RecordImportResponse
+ {
+ ///
+ /// Number of DNS records added.
+ ///
+ [JsonProperty("recs_added")]
+ public int? RecordsAdded { get; set; }
+
+ ///
+ /// Total number of DNS records parsed.
+ ///
+ [JsonProperty("total_records_parsed")]
+ public int? TotalRecordsParsed { get; set; }
+ }
+}
diff --git a/src/Extensions/Cloudflare.Dns/Responses/RecordScanResponse.cs b/src/Extensions/Cloudflare.Dns/Responses/RecordScanResponse.cs
new file mode 100644
index 0000000..2a51cb0
--- /dev/null
+++ b/src/Extensions/Cloudflare.Dns/Responses/RecordScanResponse.cs
@@ -0,0 +1,20 @@
+namespace AMWD.Net.Api.Cloudflare.Dns
+{
+ ///
+ /// The response for a DNS record scan.
+ ///
+ public class RecordScanResponse
+ {
+ ///
+ /// Number of DNS records added.
+ ///
+ [JsonProperty("recs_added")]
+ public int? RecordsAdded { get; set; }
+
+ ///
+ /// Total number of DNS records parsed.
+ ///
+ [JsonProperty("total_records_parsed")]
+ public int? TotalRecordsParsed { get; set; }
+ }
+}
diff --git a/src/Extensions/Cloudflare.Zones/README.md b/src/Extensions/Cloudflare.Zones/README.md
index 8f4ce49..81b21a6 100644
--- a/src/Extensions/Cloudflare.Zones/README.md
+++ b/src/Extensions/Cloudflare.Zones/README.md
@@ -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/)
-##### [Activation Check]
+#### [Activation Check]
- [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/)
- [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/)
-##### [Plans]
+#### [Plans]
- [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/)
-##### [Rate Plans]
+#### [Rate Plans]
- [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/)
- [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/)
-##### [Subscriptions]
+#### [Subscriptions]
- [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/)
diff --git a/test/Extensions/Cloudflare.Dns.Tests/DnsRecordsExtensions/BatchDnsRecordsTest.cs b/test/Extensions/Cloudflare.Dns.Tests/DnsRecordsExtensions/BatchDnsRecordsTest.cs
new file mode 100644
index 0000000..fc5c5f7
--- /dev/null
+++ b/test/Extensions/Cloudflare.Dns.Tests/DnsRecordsExtensions/BatchDnsRecordsTest.cs
@@ -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 _clientMock;
+
+ private List<(string RequestPath, InternalBatchRequest Request, IQueryParameterFilter QueryFilter)> _callbacks;
+ private CloudflareResponse _response;
+ private BatchDnsRecordsRequest _request;
+
+ [TestInitialize]
+ public void Initialize()
+ {
+ _callbacks = [];
+
+ _response = new CloudflareResponse
+ {
+ 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($"/zones/{ZoneId}/dns_records/batch", It.IsAny(), null, It.IsAny()), Times.Once);
+ _clientMock.VerifyNoOtherCalls();
+ }
+
+ private ICloudflareClient GetClient()
+ {
+ _clientMock = new Mock();
+ _clientMock
+ .Setup(m => m.PostAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()))
+ .Callback((requestPath, request, queryFilter, _) => _callbacks.Add((requestPath, request, queryFilter)))
+ .ReturnsAsync(() => _response);
+
+ return _clientMock.Object;
+ }
+ }
+}
diff --git a/test/Extensions/Cloudflare.Dns.Tests/DnsRecordsExtensions/CreateDnsRecordTest.cs b/test/Extensions/Cloudflare.Dns.Tests/DnsRecordsExtensions/CreateDnsRecordTest.cs
new file mode 100644
index 0000000..2995346
--- /dev/null
+++ b/test/Extensions/Cloudflare.Dns.Tests/DnsRecordsExtensions/CreateDnsRecordTest.cs
@@ -0,0 +1,1355 @@
+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;
+using Newtonsoft.Json.Linq;
+
+namespace Cloudflare.Dns.Tests.DnsRecordsExtensions
+{
+ [TestClass]
+ public class CreateDnsRecordTest
+ {
+ private const string ZoneId = "023e105f4ecef8ad9ca31a8372d0c353";
+
+ private Mock _clientMock;
+
+ private List<(string RequestPath, InternalDnsRecordRequest Request, IQueryParameterFilter QueryFilter)> _callbacks;
+ private CloudflareResponse _response;
+ private CreateDnsRecordRequest _request;
+
+ [TestInitialize]
+ public void Initialize()
+ {
+ _callbacks = [];
+
+ _response = new CloudflareResponse
+ {
+ Success = true,
+ Messages = [
+ new ResponseInfo(1000, "Message 1")
+ ],
+ Errors = [
+ new ResponseInfo(1000, "Error 1")
+ ],
+ Result = new CNAMERecord("example.com")
+ {
+ Id = "023e105f4ecef8ad9ca31a8372d0c353",
+ Name = "test.example.com",
+ Content = "public.r2.dev",
+ Proxiable = true,
+ Proxied = true,
+ TimeToLive = 1,
+ Settings = new CNAMERecordSettings(),
+ Meta = new JObject
+ {
+ ["r2_bucket"] = "test",
+ ["read_only"] = true
+ },
+ Comment = "Certificate authority 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 CreateDnsRecordRequest(ZoneId, "example.com")
+ {
+ Comment = "Server location record",
+ Data = new LOCRecordData
+ {
+ LatitudeDegrees = 48,
+ LatitudeMinutes = 8,
+ LatitudeSeconds = 8.12682,
+ LatitudeDirection = LOCRecordLatitudeDirection.North,
+
+ LongitudeDegrees = 11,
+ LongitudeMinutes = 34,
+ LongitudeSeconds = 30.9576,
+ LongitudeDirection = LOCRecordLongitudeDirection.East,
+
+ Altitude = 100,
+ Size = 80,
+ PrecisionHorizontal = 500,
+ PrecisionVertical = 400
+ },
+ Proxied = false,
+ Tags = ["important"],
+ TimeToLive = 1,
+ Type = DnsRecordType.LOC
+ };
+ }
+
+ [TestMethod]
+ public async Task ShouldCreateDnsRecord()
+ {
+ // Arrange
+ var client = GetClient();
+
+ // Act
+ var response = await client.CreateDnsRecord(_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", callback.RequestPath);
+ Assert.IsNull(callback.QueryFilter);
+
+ Assert.IsNotNull(callback.Request);
+ Assert.AreEqual(_request.Name, callback.Request.Name);
+ Assert.AreEqual(_request.Type, callback.Request.Type);
+ Assert.IsNull(callback.Request.Content);
+ Assert.IsNotNull(callback.Request.Data);
+ Assert.IsNull(callback.Request.Settings);
+ Assert.IsNull(callback.Request.Priority);
+ Assert.AreEqual(_request.Proxied, callback.Request.Proxied);
+ Assert.AreEqual(_request.Comment, callback.Request.Comment);
+ Assert.AreEqual(_request.TimeToLive, callback.Request.Ttl);
+ CollectionAssert.AreEqual(_request.Tags.ToArray(), callback.Request.Tags.ToArray());
+
+ Assert.IsInstanceOfType(callback.Request.Data);
+ var locData = callback.Request.Data as LOCRecordData;
+ Assert.AreEqual(48, locData.LatitudeDegrees);
+ Assert.AreEqual(8, locData.LatitudeMinutes);
+ Assert.AreEqual(8.126, locData.LatitudeSeconds);
+ Assert.AreEqual(LOCRecordLatitudeDirection.North, locData.LatitudeDirection);
+ Assert.AreEqual(11, locData.LongitudeDegrees);
+ Assert.AreEqual(34, locData.LongitudeMinutes);
+ Assert.AreEqual(30.957, locData.LongitudeSeconds);
+ Assert.AreEqual(LOCRecordLongitudeDirection.East, locData.LongitudeDirection);
+ Assert.AreEqual(100, locData.Altitude);
+ Assert.AreEqual(80, locData.Size);
+ Assert.AreEqual(500, locData.PrecisionHorizontal);
+ Assert.AreEqual(400, locData.PrecisionVertical);
+
+ _clientMock.Verify(m => m.PostAsync($"/zones/{ZoneId}/dns_records", It.IsAny(), null, It.IsAny()), Times.Once);
+ _clientMock.VerifyNoOtherCalls();
+ }
+
+ [DataTestMethod]
+ [DataRow(null)]
+ [DataRow("")]
+ [DataRow(" ")]
+ [ExpectedException(typeof(ArgumentNullException))]
+ public async Task ShouldThrowArgumentNullExceptionForName(string str)
+ {
+ // Arrange
+ _request.Name = str;
+ var client = GetClient();
+
+ // Act
+ await client.CreateDnsRecord(_request);
+
+ // Assert - ArgumentNullException
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(ArgumentOutOfRangeException))]
+ public async Task ShouldThrowArgumentOutOfRangeExceptionForType()
+ {
+ // Arrange
+ _request.Type = 0;
+ var client = GetClient();
+
+ // Act
+ await client.CreateDnsRecord(_request);
+
+ // Assert - ArgumentOutOfRangeException
+ }
+
+ [DataTestMethod]
+ [DataRow(null)]
+ [DataRow("")]
+ [DataRow(" ")]
+ [ExpectedException(typeof(ArgumentNullException))]
+ public async Task ShouldThrowArgumentNullExceptionForContent(string str)
+ {
+ // Arrange
+ _request.Type = DnsRecordType.A;
+ _request.Content = str;
+ var client = GetClient();
+
+ // Act
+ await client.CreateDnsRecord(_request);
+
+ // Assert - ArgumentNullException
+ }
+
+ [TestMethod]
+ public async Task ShouldSetContentForType()
+ {
+ // Arrange
+ _request.Type = DnsRecordType.A;
+ _request.Content = "127.0.1.53";
+ var client = GetClient();
+
+ // Act
+ var response = await client.CreateDnsRecord(_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(_request.Content, callback.Request.Content);
+
+ _clientMock.Verify(m => m.PostAsync($"/zones/{ZoneId}/dns_records", It.IsAny(), null, It.IsAny()), Times.Once);
+ _clientMock.VerifyNoOtherCalls();
+ }
+
+ [TestMethod]
+ public async Task ShouldNotSetSettingsWhenObjectIsNull()
+ {
+ // Arrange
+ _request.Type = DnsRecordType.CNAME;
+ _request.Content = "www.example.com.";
+ _request.Settings = null;
+ var client = GetClient();
+
+ // Act
+ var response = await client.CreateDnsRecord(_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.IsNull(callback.Request.Settings);
+
+ _clientMock.Verify(m => m.PostAsync($"/zones/{ZoneId}/dns_records", It.IsAny(), null, It.IsAny()), Times.Once);
+ _clientMock.VerifyNoOtherCalls();
+ }
+
+ [TestMethod]
+ public async Task ShouldNotSetSettingsWhenObjectIsNotCorrectType()
+ {
+ // Arrange
+ _request.Type = DnsRecordType.CNAME;
+ _request.Content = "www.example.com.";
+ _request.Settings = new();
+ var client = GetClient();
+
+ // Act
+ var response = await client.CreateDnsRecord(_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.IsNull(callback.Request.Settings);
+
+ _clientMock.Verify(m => m.PostAsync($"/zones/{ZoneId}/dns_records", It.IsAny(), null, It.IsAny()), Times.Once);
+ _clientMock.VerifyNoOtherCalls();
+ }
+
+ [TestMethod]
+ public async Task ShouldSetSettings()
+ {
+ // Arrange
+ _request.Type = DnsRecordType.CNAME;
+ _request.Content = "www.example.com.";
+ _request.Settings = new CNAMERecordSettings { FlattenCname = true };
+ var client = GetClient();
+
+ // Act
+ var response = await client.CreateDnsRecord(_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.IsNotNull(callback.Request.Settings);
+
+ Assert.IsInstanceOfType(callback.Request.Settings);
+ Assert.AreEqual(true, ((CNAMERecordSettings)callback.Request.Settings).FlattenCname);
+
+ _clientMock.Verify(m => m.PostAsync($"/zones/{ZoneId}/dns_records", It.IsAny(), null, It.IsAny()), Times.Once);
+ _clientMock.VerifyNoOtherCalls();
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(ArgumentNullException))]
+ public async Task ShouldThrowArgumentNullExceptionForData()
+ {
+ // Arrange
+ _request.Data = null;
+ var client = GetClient();
+
+ // Act
+ await client.CreateDnsRecord(_request);
+
+ // Assert - ArgumentNullException
+ }
+
+ [DataTestMethod]
+ [DataRow(DnsRecordType.MX)]
+ [DataRow(DnsRecordType.SRV)]
+ [DataRow(DnsRecordType.URI)]
+ public async Task ShouldSetPriority(DnsRecordType type)
+ {
+ // Arrange
+ _request.Type = type;
+ _request.Priority = (int)type;
+
+ switch (type)
+ {
+ case DnsRecordType.MX: _request.Content = "www.example.com."; break;
+ case DnsRecordType.SRV: _request.Data = new SRVRecordData { Port = 10, Priority = 0, Target = ".", Weight = 10 }; break;
+ case DnsRecordType.URI: _request.Data = new URIRecordData { Target = ".", Weight = 10 }; break;
+ }
+
+ var client = GetClient();
+
+ // Act
+ var response = await client.CreateDnsRecord(_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(_request.Priority, callback.Request.Priority);
+
+ _clientMock.Verify(m => m.PostAsync($"/zones/{ZoneId}/dns_records", It.IsAny(), null, It.IsAny()), Times.Once);
+ _clientMock.VerifyNoOtherCalls();
+ }
+
+ [DataTestMethod]
+ [DataRow(DnsRecordType.MX)]
+ [DataRow(DnsRecordType.SRV)]
+ [DataRow(DnsRecordType.URI)]
+ [ExpectedException(typeof(ArgumentNullException))]
+ public async Task ShouldThrowArgumentNullExceptionForPriority(DnsRecordType type)
+ {
+ // Arrange
+ _request.Type = type;
+ var client = GetClient();
+
+ // Act
+ var response = await client.CreateDnsRecord(_request);
+
+ // Assert - ArgumentNullException
+ }
+
+ [DataTestMethod]
+ [DataRow(1)]
+ [DataRow(3600)]
+ public async Task ShouldSetTtl(int ttl)
+ {
+ // Arrange
+ _request.TimeToLive = ttl;
+ var client = GetClient();
+
+ // Act
+ var response = await client.CreateDnsRecord(_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(ttl, callback.Request.Ttl);
+
+ _clientMock.Verify(m => m.PostAsync($"/zones/{ZoneId}/dns_records", It.IsAny(), null, It.IsAny()), Times.Once);
+ _clientMock.VerifyNoOtherCalls();
+ }
+
+ [DataTestMethod]
+ [DataRow(0)]
+ [DataRow(20)]
+ [DataRow(86401)]
+ [ExpectedException(typeof(ArgumentOutOfRangeException))]
+ public async Task ShouldThrowArgumentOutOfRangeExceptionForTtl(int ttl)
+ {
+ // Arrange
+ _request.TimeToLive = ttl;
+ var client = GetClient();
+
+ // Act
+ await client.CreateDnsRecord(_request);
+
+ // Assert - ArgumentOutOfRangeException
+ }
+
+ [DataTestMethod]
+ [DataRow(DnsRecordType.CAA)]
+ [DataRow(DnsRecordType.CERT)]
+ [DataRow(DnsRecordType.DNSKEY)]
+ [DataRow(DnsRecordType.DS)]
+ [DataRow(DnsRecordType.HTTPS)]
+ [DataRow(DnsRecordType.LOC)]
+ [DataRow(DnsRecordType.NAPTR)]
+ [DataRow(DnsRecordType.SMIMEA)]
+ [DataRow(DnsRecordType.SRV)]
+ [DataRow(DnsRecordType.SSHFP)]
+ [DataRow(DnsRecordType.SVCB)]
+ [DataRow(DnsRecordType.TLSA)]
+ [DataRow(DnsRecordType.URI)]
+ [ExpectedException(typeof(ArgumentException))]
+ public async Task ShouldThrowArgumentExceptionForInvalidTypeOfData(DnsRecordType type)
+ {
+ // Arrange
+ _request.Type = type;
+ _request.Priority = 10;
+ _request.Data = new object();
+ var client = GetClient();
+
+ // Act
+ await client.CreateDnsRecord(_request);
+
+ // Assert - ArgumentException
+ }
+
+ #region CAA
+
+ [TestMethod]
+ public async Task ShouldSetCaaData()
+ {
+ // Arrange
+ _request.Type = DnsRecordType.CAA;
+ _request.Data = new CAARecordData { Flags = 1, Tag = "issue", Value = "letsencrypt.org" };
+ var client = GetClient();
+
+ // Act
+ var response = await client.CreateDnsRecord(_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.IsNotNull(callback.Request.Data);
+ Assert.IsInstanceOfType(callback.Request.Data);
+
+ var data = (CAARecordData)callback.Request.Data;
+ Assert.AreEqual(1, data.Flags);
+ Assert.AreEqual("issue", data.Tag);
+ Assert.AreEqual("letsencrypt.org", data.Value);
+ }
+
+ [DataTestMethod]
+ [DataRow(null)]
+ [DataRow("")]
+ [DataRow(" ")]
+ [ExpectedException(typeof(ArgumentNullException))]
+ public async Task ShouldThrowArgumentNullExceptionForCaaDataTag(string str)
+ {
+ // Arrange
+ _request.Type = DnsRecordType.CAA;
+ _request.Data = new CAARecordData { Flags = 1, Tag = str, Value = "letsencrypt.org" };
+ var client = GetClient();
+
+ // Act
+ var response = await client.CreateDnsRecord(_request);
+
+ // Assert - ArgumentNullException
+ }
+
+ [DataTestMethod]
+ [DataRow(null)]
+ [DataRow("")]
+ [DataRow(" ")]
+ [ExpectedException(typeof(ArgumentNullException))]
+ public async Task ShouldThrowArgumentNullExceptionForCaaDataValue(string str)
+ {
+ // Arrange
+ _request.Type = DnsRecordType.CAA;
+ _request.Data = new CAARecordData { Flags = 1, Tag = "issue", Value = str };
+ var client = GetClient();
+
+ // Act
+ var response = await client.CreateDnsRecord(_request);
+
+ // Assert - ArgumentNullException
+ }
+
+ #endregion CAA
+
+ #region CERT
+
+ [TestMethod]
+ public async Task ShouldSetCertData()
+ {
+ // Arrange
+ _request.Type = DnsRecordType.CERT;
+ _request.Data = new CERTRecordData { Algorithm = 1, Certificate = "test", KeyTag = 2, Type = 3 };
+ var client = GetClient();
+
+ // Act
+ var response = await client.CreateDnsRecord(_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.IsNotNull(callback.Request.Data);
+ Assert.IsInstanceOfType(callback.Request.Data, typeof(CERTRecordData));
+
+ var data = (CERTRecordData)callback.Request.Data;
+ Assert.AreEqual(1, data.Algorithm);
+ Assert.AreEqual("test", data.Certificate);
+ Assert.AreEqual(2, data.KeyTag);
+ Assert.AreEqual(3, data.Type);
+ }
+
+ [DataTestMethod]
+ [DataRow(null)]
+ [DataRow("")]
+ [DataRow(" ")]
+ [ExpectedException(typeof(ArgumentNullException))]
+ public async Task ShouldThrowArgumentNullExceptionForCertDataValue(string str)
+ {
+ // Arrange
+ _request.Type = DnsRecordType.CERT;
+ _request.Data = new CERTRecordData { Algorithm = 1, Certificate = str, KeyTag = 2, Type = 3 };
+ var client = GetClient();
+
+ // Act
+ var response = await client.CreateDnsRecord(_request);
+
+ // Assert - ArgumentNullException
+ }
+
+ #endregion CERT
+
+ #region DNSKEY
+
+ [TestMethod]
+ public async Task ShouldSetDnsKeyData()
+ {
+ // Arrange
+ _request.Type = DnsRecordType.DNSKEY;
+ _request.Data = new DNSKEYRecordData { Algorithm = 1, Flags = 2, Protocol = 3, PublicKey = "test" };
+ var client = GetClient();
+
+ // Act
+ var response = await client.CreateDnsRecord(_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.IsNotNull(callback.Request.Data);
+ Assert.IsInstanceOfType(callback.Request.Data, typeof(DNSKEYRecordData));
+
+ var data = (DNSKEYRecordData)callback.Request.Data;
+ Assert.AreEqual(1, data.Algorithm);
+ Assert.AreEqual(2, data.Flags);
+ Assert.AreEqual(3, data.Protocol);
+ Assert.AreEqual("test", data.PublicKey);
+ }
+
+ [DataTestMethod]
+ [DataRow(null)]
+ [DataRow("")]
+ [DataRow(" ")]
+ [ExpectedException(typeof(ArgumentNullException))]
+ public async Task ShouldThrowArgumentNullExceptionForDnsKeyDataPublicKey(string str)
+ {
+ // Arrange
+ _request.Type = DnsRecordType.DNSKEY;
+ _request.Data = new DNSKEYRecordData { Algorithm = 1, Flags = 2, Protocol = 3, PublicKey = str };
+ var client = GetClient();
+
+ // Act
+ var response = await client.CreateDnsRecord(_request);
+
+ // Assert - ArgumentNullException
+ }
+
+ #endregion DNSKEY
+
+ #region DS
+
+ [TestMethod]
+ public async Task ShouldSetDsData()
+ {
+ // Arrange
+ _request.Type = DnsRecordType.DS;
+ _request.Data = new DSRecordData { Algorithm = 1, Digest = "test", DigestType = 3, KeyTag = 4 };
+ var client = GetClient();
+
+ // Act
+ var response = await client.CreateDnsRecord(_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.IsNotNull(callback.Request.Data);
+ Assert.IsInstanceOfType(callback.Request.Data, typeof(DSRecordData));
+
+ var data = (DSRecordData)callback.Request.Data;
+ Assert.AreEqual(1, data.Algorithm);
+ Assert.AreEqual("test", data.Digest);
+ Assert.AreEqual(3, data.DigestType);
+ Assert.AreEqual(4, data.KeyTag);
+ }
+
+ [DataTestMethod]
+ [DataRow(null)]
+ [DataRow("")]
+ [DataRow(" ")]
+ [ExpectedException(typeof(ArgumentNullException))]
+ public async Task ShouldThrowArgumentNullExceptionForDsDataDigest(string str)
+ {
+ // Arrange
+ _request.Type = DnsRecordType.DS;
+ _request.Data = new DSRecordData { Algorithm = 1, Digest = str, DigestType = 3, KeyTag = 4 };
+ var client = GetClient();
+
+ // Act
+ var response = await client.CreateDnsRecord(_request);
+
+ // Assert - ArgumentNullException
+ }
+
+ #endregion DS
+
+ #region HTTPS
+
+ [TestMethod]
+ public async Task ShouldSetHttpsData()
+ {
+ // Arrange
+ _request.Type = DnsRecordType.HTTPS;
+ _request.Data = new HTTPSRecordData { Priority = 10, Target = ".", Value = "foo.bar" };
+ var client = GetClient();
+
+ // Act
+ var response = await client.CreateDnsRecord(_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.IsNotNull(callback.Request.Data);
+ Assert.IsInstanceOfType(callback.Request.Data, typeof(HTTPSRecordData));
+
+ var data = (HTTPSRecordData)callback.Request.Data;
+ Assert.AreEqual(10, data.Priority);
+ Assert.AreEqual(".", data.Target);
+ Assert.AreEqual("foo.bar", data.Value);
+ }
+
+ [DataTestMethod]
+ [DataRow(null)]
+ [DataRow("")]
+ [DataRow(" ")]
+ [ExpectedException(typeof(ArgumentNullException))]
+ public async Task ShouldThrowArgumentNullExceptionForHttpsDataTarget(string str)
+ {
+ // Arrange
+ _request.Type = DnsRecordType.HTTPS;
+ _request.Data = new HTTPSRecordData { Priority = 10, Target = str, Value = "foo.bar" };
+ var client = GetClient();
+
+ // Act
+ var response = await client.CreateDnsRecord(_request);
+
+ // Assert - ArgumentNullException
+ }
+
+ [DataTestMethod]
+ [DataRow(null)]
+ [DataRow("")]
+ [DataRow(" ")]
+ [ExpectedException(typeof(ArgumentNullException))]
+ public async Task ShouldThrowArgumentNullExceptionForHttpsDataValue(string str)
+ {
+ // Arrange
+ _request.Type = DnsRecordType.HTTPS;
+ _request.Data = new HTTPSRecordData { Priority = 10, Target = ".", Value = str };
+ var client = GetClient();
+
+ // Act
+ var response = await client.CreateDnsRecord(_request);
+
+ // Assert - ArgumentNullException
+ }
+
+ #endregion HTTPS
+
+ #region LOC
+
+ [DataTestMethod]
+ [DataRow(-1)]
+ [DataRow(91)]
+ [ExpectedException(typeof(ArgumentOutOfRangeException))]
+ public async Task ShouldThrowArgumentOutOfRangeExceptionForLocDataLatitudeDegree(int val)
+ {
+ // Arrange
+ ((LOCRecordData)_request.Data).LatitudeDegrees = val;
+ var client = GetClient();
+
+ // Act
+ var response = await client.CreateDnsRecord(_request);
+
+ // Assert - ArgumentOutOfRangeException
+ }
+
+ [DataTestMethod]
+ [DataRow(-1)]
+ [DataRow(60)]
+ [ExpectedException(typeof(ArgumentOutOfRangeException))]
+ public async Task ShouldThrowArgumentOutOfRangeExceptionForLocDataLatitudeMinutes(int val)
+ {
+ // Arrange
+ ((LOCRecordData)_request.Data).LatitudeMinutes = val;
+ var client = GetClient();
+
+ // Act
+ var response = await client.CreateDnsRecord(_request);
+
+ // Assert - ArgumentOutOfRangeException
+ }
+
+ [DataTestMethod]
+ [DataRow(-1.0)]
+ [DataRow(59.9991)]
+ [ExpectedException(typeof(ArgumentOutOfRangeException))]
+ public async Task ShouldThrowArgumentOutOfRangeExceptionForLocDataLatitudeSeconds(double val)
+ {
+ // Arrange
+ ((LOCRecordData)_request.Data).LatitudeSeconds = val;
+ var client = GetClient();
+
+ // Act
+ var response = await client.CreateDnsRecord(_request);
+
+ // Assert - ArgumentOutOfRangeException
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(ArgumentOutOfRangeException))]
+ public async Task ShouldThrowArgumentOutOfRangeExceptionForLocDataLatitudeDirection()
+ {
+ // Arrange
+ ((LOCRecordData)_request.Data).LatitudeDirection = 0;
+ var client = GetClient();
+
+ // Act
+ var response = await client.CreateDnsRecord(_request);
+
+ // Assert - ArgumentOutOfRangeException
+ }
+
+ [DataTestMethod]
+ [DataRow(-1)]
+ [DataRow(181)]
+ [ExpectedException(typeof(ArgumentOutOfRangeException))]
+ public async Task ShouldThrowArgumentOutOfRangeExceptionForLocDataLongitudeDegree(int val)
+ {
+ // Arrange
+ ((LOCRecordData)_request.Data).LongitudeDegrees = val;
+ var client = GetClient();
+
+ // Act
+ var response = await client.CreateDnsRecord(_request);
+
+ // Assert - ArgumentOutOfRangeException
+ }
+
+ [DataTestMethod]
+ [DataRow(-1)]
+ [DataRow(60)]
+ [ExpectedException(typeof(ArgumentOutOfRangeException))]
+ public async Task ShouldThrowArgumentOutOfRangeExceptionForLocDataLongitudeMinutes(int val)
+ {
+ // Arrange
+ ((LOCRecordData)_request.Data).LongitudeMinutes = val;
+ var client = GetClient();
+
+ // Act
+ var response = await client.CreateDnsRecord(_request);
+
+ // Assert - ArgumentOutOfRangeException
+ }
+
+ [DataTestMethod]
+ [DataRow(-1.0)]
+ [DataRow(59.9991)]
+ [ExpectedException(typeof(ArgumentOutOfRangeException))]
+ public async Task ShouldThrowArgumentOutOfRangeExceptionForLocDataLongitudeSeconds(double val)
+ {
+ // Arrange
+ ((LOCRecordData)_request.Data).LongitudeSeconds = val;
+ var client = GetClient();
+
+ // Act
+ var response = await client.CreateDnsRecord(_request);
+
+ // Assert - ArgumentOutOfRangeException
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(ArgumentOutOfRangeException))]
+ public async Task ShouldThrowArgumentOutOfRangeExceptionForLocDataLongitudeDirection()
+ {
+ // Arrange
+ ((LOCRecordData)_request.Data).LongitudeDirection = 0;
+ var client = GetClient();
+
+ // Act
+ var response = await client.CreateDnsRecord(_request);
+
+ // Assert - ArgumentOutOfRangeException
+ }
+
+ [DataTestMethod]
+ [DataRow(-100_000.1)]
+ [DataRow(42_849_672.951)]
+ [ExpectedException(typeof(ArgumentOutOfRangeException))]
+ public async Task ShouldThrowArgumentOutOfRangeExceptionForLocDataAltitde(double val)
+ {
+ // Arrange
+ ((LOCRecordData)_request.Data).Altitude = val;
+ var client = GetClient();
+
+ // Act
+ var response = await client.CreateDnsRecord(_request);
+
+ // Assert - ArgumentOutOfRangeException
+ }
+
+ [DataTestMethod]
+ [DataRow(-1)]
+ [DataRow(90_000_001)]
+ [ExpectedException(typeof(ArgumentOutOfRangeException))]
+ public async Task ShouldThrowArgumentOutOfRangeExceptionForLocDataSize(int val)
+ {
+ // Arrange
+ ((LOCRecordData)_request.Data).Size = val;
+ var client = GetClient();
+
+ // Act
+ var response = await client.CreateDnsRecord(_request);
+
+ // Assert - ArgumentOutOfRangeException
+ }
+
+ [DataTestMethod]
+ [DataRow(-1)]
+ [DataRow(90_000_001)]
+ [ExpectedException(typeof(ArgumentOutOfRangeException))]
+ public async Task ShouldThrowArgumentOutOfRangeExceptionForLocDataPrecisionHorizontal(int val)
+ {
+ // Arrange
+ ((LOCRecordData)_request.Data).PrecisionHorizontal = val;
+ var client = GetClient();
+
+ // Act
+ var response = await client.CreateDnsRecord(_request);
+
+ // Assert - ArgumentOutOfRangeException
+ }
+
+ [DataTestMethod]
+ [DataRow(-1)]
+ [DataRow(90_000_001)]
+ [ExpectedException(typeof(ArgumentOutOfRangeException))]
+ public async Task ShouldThrowArgumentOutOfRangeExceptionForLocDataPrecisionVertical(int val)
+ {
+ // Arrange
+ ((LOCRecordData)_request.Data).PrecisionVertical = val;
+ var client = GetClient();
+
+ // Act
+ var response = await client.CreateDnsRecord(_request);
+
+ // Assert - ArgumentOutOfRangeException
+ }
+
+ #endregion LOC
+
+ #region NAPTR
+
+ [TestMethod]
+ public async Task ShouldSetNaPtrData()
+ {
+ // Arrange
+ _request.Type = DnsRecordType.NAPTR;
+ _request.Data = new NAPTRRecordData { Flags = "ab", Order = 1, Preference = 2, Regex = "cd", Replacement = "ef", Service = "gh" };
+ var client = GetClient();
+
+ // Act
+ var response = await client.CreateDnsRecord(_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.IsNotNull(callback.Request.Data);
+ Assert.IsInstanceOfType(callback.Request.Data, typeof(NAPTRRecordData));
+
+ var data = (NAPTRRecordData)callback.Request.Data;
+ Assert.AreEqual("ab", data.Flags);
+ Assert.AreEqual(1, data.Order);
+ Assert.AreEqual(2, data.Preference);
+ Assert.AreEqual("cd", data.Regex);
+ Assert.AreEqual("ef", data.Replacement);
+ Assert.AreEqual("gh", data.Service);
+ }
+
+ [DataTestMethod]
+ [DataRow(null)]
+ [DataRow("")]
+ [DataRow(" ")]
+ [ExpectedException(typeof(ArgumentNullException))]
+ public async Task ShouldThrowArgumentNullExceptionForNaPtrDataFlags(string str)
+ {
+ // Arrange
+ _request.Type = DnsRecordType.NAPTR;
+ _request.Data = new NAPTRRecordData { Flags = str, Order = 1, Preference = 2, Regex = "cd", Replacement = "ef", Service = "gh" };
+ var client = GetClient();
+
+ // Act
+ var response = await client.CreateDnsRecord(_request);
+
+ // Assert - ArgumentNullException
+ }
+
+ [DataTestMethod]
+ [DataRow(null)]
+ [DataRow("")]
+ [DataRow(" ")]
+ [ExpectedException(typeof(ArgumentNullException))]
+ public async Task ShouldThrowArgumentNullExceptionForNaPtrDataRegex(string str)
+ {
+ // Arrange
+ _request.Type = DnsRecordType.NAPTR;
+ _request.Data = new NAPTRRecordData { Flags = "ab", Order = 1, Preference = 2, Regex = str, Replacement = "ef", Service = "gh" };
+ var client = GetClient();
+
+ // Act
+ var response = await client.CreateDnsRecord(_request);
+
+ // Assert - ArgumentNullException
+ }
+
+ [DataTestMethod]
+ [DataRow(null)]
+ [DataRow("")]
+ [DataRow(" ")]
+ [ExpectedException(typeof(ArgumentNullException))]
+ public async Task ShouldThrowArgumentNullExceptionForNaPtrDataReplacement(string str)
+ {
+ // Arrange
+ _request.Type = DnsRecordType.NAPTR;
+ _request.Data = new NAPTRRecordData { Flags = "ab", Order = 1, Preference = 2, Regex = "cd", Replacement = str, Service = "gh" };
+ var client = GetClient();
+
+ // Act
+ var response = await client.CreateDnsRecord(_request);
+
+ // Assert - ArgumentNullException
+ }
+
+ [DataTestMethod]
+ [DataRow(null)]
+ [DataRow("")]
+ [DataRow(" ")]
+ [ExpectedException(typeof(ArgumentNullException))]
+ public async Task ShouldThrowArgumentNullExceptionForNaPtrDataService(string str)
+ {
+ // Arrange
+ _request.Type = DnsRecordType.NAPTR;
+ _request.Data = new NAPTRRecordData { Flags = "ab", Order = 1, Preference = 2, Regex = "cd", Replacement = "ef", Service = str };
+ var client = GetClient();
+
+ // Act
+ var response = await client.CreateDnsRecord(_request);
+
+ // Assert - ArgumentNullException
+ }
+
+ #endregion NAPTR
+
+ #region SMIMEA
+
+ [TestMethod]
+ public async Task ShouldSetSMimeAData()
+ {
+ // Arrange
+ _request.Type = DnsRecordType.SMIMEA;
+ _request.Data = new SMIMEARecordData { Certificate = "cert", MatchingType = 1, Selector = 2, Usage = 3 };
+ var client = GetClient();
+
+ // Act
+ var response = await client.CreateDnsRecord(_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.IsNotNull(callback.Request.Data);
+ Assert.IsInstanceOfType(callback.Request.Data, typeof(SMIMEARecordData));
+
+ var data = (SMIMEARecordData)callback.Request.Data;
+ Assert.AreEqual("cert", data.Certificate);
+ Assert.AreEqual(1, data.MatchingType);
+ Assert.AreEqual(2, data.Selector);
+ Assert.AreEqual(3, data.Usage);
+ }
+
+ [DataTestMethod]
+ [DataRow(null)]
+ [DataRow("")]
+ [DataRow(" ")]
+ [ExpectedException(typeof(ArgumentNullException))]
+ public async Task ShouldThrowArgumentNullExceptionForSMimeACertificate(string str)
+ {
+ // Arrange
+ _request.Type = DnsRecordType.SMIMEA;
+ _request.Data = new SMIMEARecordData { Certificate = str, MatchingType = 1, Selector = 2, Usage = 3 };
+ var client = GetClient();
+
+ // Act
+ var response = await client.CreateDnsRecord(_request);
+
+ // Assert - ArgumentNullException
+ }
+
+ #endregion SMIMEA
+
+ #region SRV
+
+ [TestMethod]
+ public async Task ShouldSetSrvData()
+ {
+ // Arrange
+ _request.Type = DnsRecordType.SRV;
+ _request.Priority = 10;
+ _request.Data = new SRVRecordData { Port = 123, Priority = 345, Target = ".", Weight = 456 };
+ var client = GetClient();
+
+ // Act
+ var response = await client.CreateDnsRecord(_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.IsNotNull(callback.Request.Data);
+ Assert.IsInstanceOfType(callback.Request.Data, typeof(SRVRecordData));
+
+ var data = (SRVRecordData)callback.Request.Data;
+ Assert.AreEqual(123, data.Port);
+ Assert.AreEqual(345, data.Priority);
+ Assert.AreEqual(".", data.Target);
+ Assert.AreEqual(456, data.Weight);
+ }
+
+ [DataTestMethod]
+ [DataRow(null)]
+ [DataRow("")]
+ [DataRow(" ")]
+ [ExpectedException(typeof(ArgumentNullException))]
+ public async Task ShouldThrowArgumentNullExceptionForSrvTarget(string str)
+ {
+ // Arrange
+ _request.Type = DnsRecordType.SRV;
+ _request.Priority = 10;
+ _request.Data = new SRVRecordData { Port = 123, Priority = 345, Target = str, Weight = 456 };
+ var client = GetClient();
+
+ // Act
+ var response = await client.CreateDnsRecord(_request);
+
+ // Assert - ArgumentNullException
+ }
+
+ #endregion SRV
+
+ #region SSHFP
+
+ [TestMethod]
+ public async Task ShouldSetSshFpData()
+ {
+ // Arrange
+ _request.Type = DnsRecordType.SSHFP;
+ _request.Data = new SSHFPRecordData { Algorithm = 1, Fingerprint = "fingerprint", Type = 2 };
+ var client = GetClient();
+
+ // Act
+ var response = await client.CreateDnsRecord(_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.IsNotNull(callback.Request.Data);
+ Assert.IsInstanceOfType(callback.Request.Data, typeof(SSHFPRecordData));
+
+ var data = (SSHFPRecordData)callback.Request.Data;
+ Assert.AreEqual(1, data.Algorithm);
+ Assert.AreEqual("fingerprint", data.Fingerprint);
+ Assert.AreEqual(2, data.Type);
+ }
+
+ [DataTestMethod]
+ [DataRow(null)]
+ [DataRow("")]
+ [DataRow(" ")]
+ [ExpectedException(typeof(ArgumentNullException))]
+ public async Task ShouldThrowArgumentNullExceptionForSshFpFingerprint(string str)
+ {
+ // Arrange
+ _request.Type = DnsRecordType.SSHFP;
+ _request.Data = new SSHFPRecordData { Algorithm = 1, Fingerprint = str, Type = 2 };
+ var client = GetClient();
+
+ // Act
+ var response = await client.CreateDnsRecord(_request);
+
+ // Assert - ArgumentNullException
+ }
+
+ #endregion SSHFP
+
+ #region SVCB
+
+ [TestMethod]
+ public async Task ShouldSetSvcBData()
+ {
+ // Arrange
+ _request.Type = DnsRecordType.SVCB;
+ _request.Data = new SVCBRecordData { Priority = 10, Target = ".", Value = "example.com" };
+ var client = GetClient();
+
+ // Act
+ var response = await client.CreateDnsRecord(_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.IsNotNull(callback.Request.Data);
+ Assert.IsInstanceOfType(callback.Request.Data, typeof(SVCBRecordData));
+
+ var data = (SVCBRecordData)callback.Request.Data;
+ Assert.AreEqual(10, data.Priority);
+ Assert.AreEqual(".", data.Target);
+ Assert.AreEqual("example.com", data.Value);
+ }
+
+ [DataTestMethod]
+ [DataRow(null)]
+ [DataRow("")]
+ [DataRow(" ")]
+ [ExpectedException(typeof(ArgumentNullException))]
+ public async Task ShouldThrowArgumentNullExceptionForSvcBTarget(string str)
+ {
+ // Arrange
+ _request.Type = DnsRecordType.SVCB;
+ _request.Data = new SVCBRecordData { Priority = 10, Target = str, Value = "example.com" };
+ var client = GetClient();
+
+ // Act
+ var response = await client.CreateDnsRecord(_request);
+
+ // Assert - ArgumentNullException
+ }
+
+ [DataTestMethod]
+ [DataRow(null)]
+ [DataRow("")]
+ [DataRow(" ")]
+ [ExpectedException(typeof(ArgumentNullException))]
+ public async Task ShouldThrowArgumentNullExceptionForSvcBValue(string str)
+ {
+ // Arrange
+ _request.Type = DnsRecordType.SVCB;
+ _request.Data = new SVCBRecordData { Priority = 10, Target = ".", Value = str };
+ var client = GetClient();
+
+ // Act
+ var response = await client.CreateDnsRecord(_request);
+
+ // Assert - ArgumentNullException
+ }
+
+ #endregion SVCB
+
+ #region TLSA
+
+ [TestMethod]
+ public async Task ShouldSetTlsAData()
+ {
+ // Arrange
+ _request.Type = DnsRecordType.TLSA;
+ _request.Data = new TLSARecordData { Certificate = "cert", MatchingType = 1, Selector = 2, Usage = 3 };
+ var client = GetClient();
+
+ // Act
+ var response = await client.CreateDnsRecord(_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.IsNotNull(callback.Request.Data);
+ Assert.IsInstanceOfType(callback.Request.Data, typeof(TLSARecordData));
+
+ var data = (TLSARecordData)callback.Request.Data;
+ Assert.AreEqual("cert", data.Certificate);
+ Assert.AreEqual(1, data.MatchingType);
+ Assert.AreEqual(2, data.Selector);
+ Assert.AreEqual(3, data.Usage);
+ }
+
+ [DataTestMethod]
+ [DataRow(null)]
+ [DataRow("")]
+ [DataRow(" ")]
+ [ExpectedException(typeof(ArgumentNullException))]
+ public async Task ShouldThrowArgumentNullExceptionForTlsACertificate(string str)
+ {
+ // Arrange
+ _request.Type = DnsRecordType.TLSA;
+ _request.Data = new TLSARecordData { Certificate = str, MatchingType = 1, Selector = 2, Usage = 3 };
+ var client = GetClient();
+
+ // Act
+ var response = await client.CreateDnsRecord(_request);
+
+ // Assert - ArgumentNullException
+ }
+
+ #endregion TLSA
+
+ #region URI
+
+ [TestMethod]
+ public async Task ShouldSetUriData()
+ {
+ // Arrange
+ _request.Type = DnsRecordType.URI;
+ _request.Priority = 10;
+ _request.Data = new URIRecordData { Target = "aim", Weight = 10 };
+ var client = GetClient();
+
+ // Act
+ var response = await client.CreateDnsRecord(_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.IsNotNull(callback.Request.Data);
+ Assert.IsInstanceOfType(callback.Request.Data, typeof(URIRecordData));
+
+ var data = (URIRecordData)callback.Request.Data;
+ Assert.AreEqual("aim", data.Target);
+ Assert.AreEqual(10, data.Weight);
+ }
+
+ [DataTestMethod]
+ [DataRow(null)]
+ [DataRow("")]
+ [DataRow(" ")]
+ [ExpectedException(typeof(ArgumentNullException))]
+ public async Task ShouldThrowArgumentNullExceptionForUriTarget(string str)
+ {
+ // Arrange
+ _request.Type = DnsRecordType.URI;
+ _request.Priority = 10;
+ _request.Data = new URIRecordData { Target = str, Weight = 10 };
+ var client = GetClient();
+
+ // Act
+ var response = await client.CreateDnsRecord(_request);
+
+ // Assert - ArgumentNullException
+ }
+
+ #endregion URI
+
+ private ICloudflareClient GetClient()
+ {
+ _clientMock = new Mock();
+ _clientMock
+ .Setup(m => m.PostAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()))
+ .Callback((requestPath, request, queryFilter, _) => _callbacks.Add((requestPath, request, queryFilter)))
+ .ReturnsAsync(() => _response);
+
+ return _clientMock.Object;
+ }
+ }
+}
diff --git a/test/Extensions/Cloudflare.Dns.Tests/DnsRecordsExtensions/DeleteDnsRecordTest.cs b/test/Extensions/Cloudflare.Dns.Tests/DnsRecordsExtensions/DeleteDnsRecordTest.cs
new file mode 100644
index 0000000..d7a0936
--- /dev/null
+++ b/test/Extensions/Cloudflare.Dns.Tests/DnsRecordsExtensions/DeleteDnsRecordTest.cs
@@ -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 _clientMock;
+ private CloudflareResponse _response;
+ private List<(string RequestPath, IQueryParameterFilter QueryFilter)> _callbacks;
+
+ [TestInitialize]
+ public void Initialize()
+ {
+ _callbacks = [];
+
+ _response = new CloudflareResponse
+ {
+ Success = true,
+ Messages = [
+ new ResponseInfo(1000, "Message 1")
+ ],
+ Errors = [
+ new ResponseInfo(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($"/zones/{ZoneId}/dns_records/{RecordId}", null, It.IsAny()), Times.Once);
+ _clientMock.VerifyNoOtherCalls();
+ }
+
+ private ICloudflareClient GetClient()
+ {
+ _clientMock = new Mock();
+ _clientMock
+ .Setup(m => m.DeleteAsync(It.IsAny(), It.IsAny(), It.IsAny()))
+ .Callback((requestPath, queryFilter, _) => _callbacks.Add((requestPath, queryFilter)))
+ .ReturnsAsync(() => _response);
+
+ return _clientMock.Object;
+ }
+ }
+}
diff --git a/test/Extensions/Cloudflare.Dns.Tests/DnsRecordsExtensions/DnsRecordDetailsTest.cs b/test/Extensions/Cloudflare.Dns.Tests/DnsRecordsExtensions/DnsRecordDetailsTest.cs
new file mode 100644
index 0000000..541165e
--- /dev/null
+++ b/test/Extensions/Cloudflare.Dns.Tests/DnsRecordsExtensions/DnsRecordDetailsTest.cs
@@ -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 _clientMock;
+ private CloudflareResponse _response;
+ private List<(string RequestPath, IQueryParameterFilter QueryFilter)> _callbacks;
+
+ [TestInitialize]
+ public void Initialize()
+ {
+ _callbacks = [];
+
+ _response = new CloudflareResponse
+ {
+ Success = true,
+ Messages = [
+ new ResponseInfo(1000, "Message 1")
+ ],
+ Errors = [
+ new ResponseInfo(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($"/zones/{ZoneId}/dns_records/{RecordId}", null, It.IsAny()), Times.Once);
+ _clientMock.VerifyNoOtherCalls();
+ }
+
+ private ICloudflareClient GetClient()
+ {
+ _clientMock = new Mock();
+ _clientMock
+ .Setup(m => m.GetAsync(It.IsAny(), It.IsAny(), It.IsAny()))
+ .Callback((requestPath, queryFilter, _) => _callbacks.Add((requestPath, queryFilter)))
+ .ReturnsAsync(() => _response);
+
+ return _clientMock.Object;
+ }
+ }
+}
diff --git a/test/Extensions/Cloudflare.Dns.Tests/DnsRecordsExtensions/ExportDnsRecordsTest.cs b/test/Extensions/Cloudflare.Dns.Tests/DnsRecordsExtensions/ExportDnsRecordsTest.cs
new file mode 100644
index 0000000..953a976
--- /dev/null
+++ b/test/Extensions/Cloudflare.Dns.Tests/DnsRecordsExtensions/ExportDnsRecordsTest.cs
@@ -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 _clientMock;
+ private CloudflareResponse _response;
+ private List<(string RequestPath, IQueryParameterFilter QueryFilter)> _callbacks;
+
+ [TestInitialize]
+ public void Initialize()
+ {
+ _callbacks = [];
+
+ _response = new CloudflareResponse
+ {
+ 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($"/zones/{ZoneId}/dns_records/export", null, It.IsAny()), Times.Once);
+ _clientMock.VerifyNoOtherCalls();
+ }
+
+ private ICloudflareClient GetClient()
+ {
+ _clientMock = new Mock();
+ _clientMock
+ .Setup(m => m.GetAsync(It.IsAny(), It.IsAny(), It.IsAny()))
+ .Callback((requestPath, queryFilter, _) => _callbacks.Add((requestPath, queryFilter)))
+ .ReturnsAsync(() => _response);
+
+ return _clientMock.Object;
+ }
+ }
+}
diff --git a/test/Extensions/Cloudflare.Dns.Tests/DnsRecordsExtensions/ImportDnsRecordsTest.cs b/test/Extensions/Cloudflare.Dns.Tests/DnsRecordsExtensions/ImportDnsRecordsTest.cs
new file mode 100644
index 0000000..9307be1
--- /dev/null
+++ b/test/Extensions/Cloudflare.Dns.Tests/DnsRecordsExtensions/ImportDnsRecordsTest.cs
@@ -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 _clientMock;
+
+ private List<(string RequestPath, MultipartFormDataContent Request, IQueryParameterFilter QueryFilter)> _callbacks;
+ private CloudflareResponse _response;
+ private ImportDnsRecordsRequest _request;
+
+ [TestInitialize]
+ public void Initialize()
+ {
+ _callbacks = [];
+
+ _response = new CloudflareResponse
+ {
+ 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(part);
+ Assert.AreEqual(BindConfigContent, await part.ReadAsStringAsync());
+
+ _clientMock.Verify(m => m.PostAsync($"/zones/{ZoneId}/dns_records/import", It.IsAny(), It.IsAny(), It.IsAny()), 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(part);
+ Assert.AreEqual(BindConfigContent, await part.ReadAsStringAsync());
+
+ _clientMock.Verify(m => m.PostAsync($"/zones/{ZoneId}/dns_records/import", It.IsAny(), It.IsAny(), It.IsAny()), 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(part);
+ Assert.AreEqual(proxied.ToString().ToLower(), await part.ReadAsStringAsync());
+
+ part = callback.Request.Last();
+ Assert.AreEqual("file", part.Headers.ContentDisposition.Name);
+ Assert.IsInstanceOfType(part);
+ Assert.AreEqual(BindConfigContent, await part.ReadAsStringAsync());
+
+ _clientMock.Verify(m => m.PostAsync($"/zones/{ZoneId}/dns_records/import", It.IsAny(), It.IsAny(), It.IsAny()), 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(part);
+ Assert.AreEqual(proxied.ToString().ToLower(), await part.ReadAsStringAsync());
+
+ part = callback.Request.Last();
+ Assert.AreEqual("file", part.Headers.ContentDisposition.Name);
+ Assert.IsInstanceOfType(part);
+ Assert.AreEqual(BindConfigContent, await part.ReadAsStringAsync());
+
+ _clientMock.Verify(m => m.PostAsync($"/zones/{ZoneId}/dns_records/import", It.IsAny(), It.IsAny(), It.IsAny()), 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();
+ _clientMock
+ .Setup(m => m.PostAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()))
+ .Callback((requestPath, request, queryFilter, _) => _callbacks.Add((requestPath, request, queryFilter)))
+ .ReturnsAsync(() => _response);
+
+ return _clientMock.Object;
+ }
+ }
+}
diff --git a/test/Extensions/Cloudflare.Dns.Tests/DnsRecordsExtensions/ListDnsRecordsTest.cs b/test/Extensions/Cloudflare.Dns.Tests/DnsRecordsExtensions/ListDnsRecordsTest.cs
new file mode 100644
index 0000000..df1111a
--- /dev/null
+++ b/test/Extensions/Cloudflare.Dns.Tests/DnsRecordsExtensions/ListDnsRecordsTest.cs
@@ -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 _clientMock;
+
+ private CloudflareResponse> _response;
+
+ private List<(string RequestPath, IQueryParameterFilter QueryFilter)> _callbacks;
+
+ [TestInitialize]
+ public void Initialize()
+ {
+ _callbacks = [];
+
+ _response = new CloudflareResponse>
+ {
+ Success = true,
+ Messages = [
+ new ResponseInfo(1000, "Message 1")
+ ],
+ Errors = [
+ new ResponseInfo(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>($"/zones/{ZoneId}/dns_records", null, It.IsAny()), 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(callback.QueryFilter);
+ Assert.AreEqual("example.com", ((ListDnsRecordsFilter)callback.QueryFilter).Name);
+
+ _clientMock.Verify(m => m.GetAsync>($"/zones/{ZoneId}/dns_records", filter, It.IsAny()), 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();
+ _clientMock
+ .Setup(m => m.GetAsync>(It.IsAny(), It.IsAny(), It.IsAny()))
+ .Callback((requestPath, queryFilter, _) => _callbacks.Add((requestPath, queryFilter)))
+ .ReturnsAsync(() => _response);
+
+ return _clientMock.Object;
+ }
+ }
+}
diff --git a/test/Extensions/Cloudflare.Dns.Tests/DnsRecordsExtensions/OverwriteDnsRecordTest.cs b/test/Extensions/Cloudflare.Dns.Tests/DnsRecordsExtensions/OverwriteDnsRecordTest.cs
new file mode 100644
index 0000000..efe7507
--- /dev/null
+++ b/test/Extensions/Cloudflare.Dns.Tests/DnsRecordsExtensions/OverwriteDnsRecordTest.cs
@@ -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 _clientMock;
+ private CloudflareResponse _response;
+ private List<(string RequestPath, InternalDnsRecordRequest Request)> _callbacks;
+ private OverwriteDnsRecordRequest _request;
+
+ [TestInitialize]
+ public void Initialize()
+ {
+ _callbacks = [];
+
+ _response = new CloudflareResponse
+ {
+ 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($"/zones/{ZoneId}/dns_records/{RecordId}", It.IsAny(), It.IsAny()), Times.Once);
+ _clientMock.VerifyNoOtherCalls();
+ }
+
+ private ICloudflareClient GetClient()
+ {
+ _clientMock = new Mock();
+ _clientMock
+ .Setup(m => m.PutAsync(It.IsAny(), It.IsAny(), It.IsAny()))
+ .Callback((requestPath, request, _) => _callbacks.Add((requestPath, request)))
+ .ReturnsAsync(() => _response);
+
+ return _clientMock.Object;
+ }
+ }
+}
diff --git a/test/Extensions/Cloudflare.Dns.Tests/DnsRecordsExtensions/ScanDnsRecordsTest.cs b/test/Extensions/Cloudflare.Dns.Tests/DnsRecordsExtensions/ScanDnsRecordsTest.cs
new file mode 100644
index 0000000..e9c77ff
--- /dev/null
+++ b/test/Extensions/Cloudflare.Dns.Tests/DnsRecordsExtensions/ScanDnsRecordsTest.cs
@@ -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 _clientMock;
+ private CloudflareResponse _response;
+ private List _callbacks;
+
+ [TestInitialize]
+ public void Initialize()
+ {
+ _callbacks = [];
+
+ _response = new CloudflareResponse
+ {
+ 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($"/zones/{ZoneId}/dns_records/scan", null, null, It.IsAny()), Times.Once);
+ _clientMock.VerifyNoOtherCalls();
+ }
+
+ private ICloudflareClient GetClient()
+ {
+ _clientMock = new Mock();
+ _clientMock
+ .Setup(m => m.PostAsync(It.IsAny(), It.IsAny