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]) [![Buy me a Coffee](https://shields.am-wd.de/badge/PayPal-Buy_me_a_Coffee-yellow?style=flat&logo=paypal)](https://link.am-wd.de/donate) -[![built with Codeium](https://codeium.com/badges/main)](https://link.am-wd.de/codeium) -[Cloudflare]: Cloudflare/README.md -[Cloudflare.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(), It.IsAny(), It.IsAny())) + .Callback((requestPath, _, _, _) => _callbacks.Add(requestPath)) + .ReturnsAsync(() => _response); + + return _clientMock.Object; + } + } +} diff --git a/test/Extensions/Cloudflare.Dns.Tests/DnsRecordsExtensions/UpdateDnsRecordTest.cs b/test/Extensions/Cloudflare.Dns.Tests/DnsRecordsExtensions/UpdateDnsRecordTest.cs new file mode 100644 index 0000000..457bc5d --- /dev/null +++ b/test/Extensions/Cloudflare.Dns.Tests/DnsRecordsExtensions/UpdateDnsRecordTest.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using AMWD.Net.Api.Cloudflare; +using AMWD.Net.Api.Cloudflare.Dns; +using AMWD.Net.Api.Cloudflare.Dns.Internals; +using Moq; +using Newtonsoft.Json.Linq; + +namespace Cloudflare.Dns.Tests.DnsRecordsExtensions +{ + [TestClass] + public class UpdateDnsRecordTest + { + private const string ZoneId = "023e105f4ecef8ad9ca31a8372d0c353"; + private const string RecordId = "023e105f4ecef8ad9ca31a8372d0c355"; + + private Mock _clientMock; + private CloudflareResponse _response; + private List<(string RequestPath, InternalDnsRecordRequest Request)> _callbacks; + private UpdateDnsRecordRequest _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, + Name = "*.example.com", + Content = "96.7.128.175", + Proxiable = true, + Proxied = true, + TimeToLive = 1, + Settings = new(), + Meta = new JObject(), + Comment = "Domain verification record", + Tags = [], + CreatedOn = DateTime.Parse("2014-01-01T05:20:00.12345Z"), + ModifiedOn = DateTime.Parse("2014-01-01T05:20:00.12345Z"), + CommentModifiedOn = DateTime.Parse("2024-01-01T05:20:00.12345Z"), + TagsModifiedOn = DateTime.Parse("2025-01-01T05:20:00.12345Z"), + } + }; + + _request = new UpdateDnsRecordRequest(ZoneId, RecordId, "example.com") + { + Type = DnsRecordType.A, + Content = "127.0.1.22" + }; + } + + [TestMethod] + public async Task ShouldUpdateDnsRecord() + { + // Arrange + var client = GetClient(); + + // Act + var response = await client.UpdateDnsRecord(_request); + + // Assert + Assert.IsNotNull(response); + Assert.IsTrue(response.Success); + Assert.AreEqual(_response.Result, response.Result); + + Assert.AreEqual(1, _callbacks.Count); + + var callback = _callbacks.First(); + Assert.AreEqual($"/zones/{ZoneId}/dns_records/{RecordId}", callback.RequestPath); + Assert.IsNotNull(callback.Request); + + Assert.AreEqual("example.com", callback.Request.Name); + Assert.AreEqual(DnsRecordType.A, callback.Request.Type); + Assert.AreEqual("127.0.1.22", callback.Request.Content); + + _clientMock.Verify(m => m.PatchAsync($"/zones/{ZoneId}/dns_records/{RecordId}", It.IsAny(), It.IsAny()), Times.Once); + _clientMock.VerifyNoOtherCalls(); + } + + private ICloudflareClient GetClient() + { + _clientMock = new Mock(); + _clientMock + .Setup(m => m.PatchAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((requestPath, request, _) => _callbacks.Add((requestPath, request))) + .ReturnsAsync(() => _response); + + return _clientMock.Object; + } + } +}