diff --git a/CHANGELOG.md b/CHANGELOG.md index 0653552..780aacc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Additional articles for the documentation. - `DateTime` extensions for ISO 8601 formatting. - DNS Analytics +- Zone Transfers ## [v0.1.0], [zones/v0.1.0], [dns/v0.1.0] - 2025-08-05 diff --git a/src/Extensions/Cloudflare.Dns/DnsZoneTransfersExtensions.cs b/src/Extensions/Cloudflare.Dns/DnsZoneTransfersExtensions.cs new file mode 100644 index 0000000..c0416ff --- /dev/null +++ b/src/Extensions/Cloudflare.Dns/DnsZoneTransfersExtensions.cs @@ -0,0 +1,584 @@ +using System.Linq; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using AMWD.Net.Api.Cloudflare.Dns.Internals; + +namespace AMWD.Net.Api.Cloudflare.Dns +{ + /// + /// Extensions for DNS Zone Transfers. + /// + public static class DnsZoneTransfersExtensions + { + #region ACLs + + /// + /// Create ACL. + /// + /// The instance. + /// The request. + /// A cancellation token used to propagate notification that this operation should be canceled. + public static Task> CreateACL(this ICloudflareClient client, CreateACLRequest request, CancellationToken cancellationToken = default) + { + request.AccountId.ValidateCloudflareId(); + + if (request.IpRangeBaseAddress.AddressFamily == AddressFamily.InterNetwork && request.IpRangeSubnet < 24) + throw new ArgumentOutOfRangeException(nameof(request.IpRange), "CIDRs are limited to a maximum of /24 for IPv4."); + + if (request.IpRangeBaseAddress.AddressFamily == AddressFamily.InterNetworkV6 && request.IpRangeSubnet < 64) + throw new ArgumentOutOfRangeException(nameof(request.IpRange), "CIDRs are limited to a maximum of /64 for IPv6."); + + var req = new InternalDnsZoneTransferAclRequest + { + Name = request.Name, + IpRange = request.IpRange + }; + + return client.PostAsync($"/accounts/{request.AccountId}/secondary_dns/acls", req, null, cancellationToken); + } + + /// + /// Delete ACL. + /// + /// The instance. + /// The account identifier. + /// The access control list identifier. + /// A cancellation token used to propagate notification that this operation should be canceled. + public static Task> DeleteACL(this ICloudflareClient client, string accountId, string aclId, CancellationToken cancellationToken = default) + { + accountId.ValidateCloudflareId(); + aclId.ValidateCloudflareId(); + + return client.DeleteAsync($"/accounts/{accountId}/secondary_dns/acls/{aclId}", null, cancellationToken); + } + + /// + /// Get ACL. + /// + /// The instance. + /// The account identifier. + /// The access control list identifier. + /// A cancellation token used to propagate notification that this operation should be canceled. + public static Task> ACLDetails(this ICloudflareClient client, string accountId, string aclId, CancellationToken cancellationToken = default) + { + accountId.ValidateCloudflareId(); + aclId.ValidateCloudflareId(); + + return client.GetAsync($"/accounts/{accountId}/secondary_dns/acls/{aclId}", null, cancellationToken); + } + + /// + /// List ACLs. + /// + /// The instance. + /// The account identifier. + /// A cancellation token used to propagate notification that this operation should be canceled. + public static Task>> ListACLs(this ICloudflareClient client, string accountId, CancellationToken cancellationToken = default) + { + accountId.ValidateCloudflareId(); + + return client.GetAsync>($"/accounts/{accountId}/secondary_dns/acls", null, cancellationToken); + } + + /// + /// Update ACL. + /// + /// The instance. + /// The request. + /// A cancellation token used to propagate notification that this operation should be canceled. + public static Task> UpdateACL(this ICloudflareClient client, UpdateDnsZoneTransferAclRequest request, CancellationToken cancellationToken = default) + { + request.AccountId.ValidateCloudflareId(); + request.AclId.ValidateCloudflareId(); + + if (request.IpRangeBaseAddress.AddressFamily == AddressFamily.InterNetwork && request.IpRangeSubnet < 24) + throw new ArgumentOutOfRangeException(nameof(request.IpRange), "CIDRs are limited to a maximum of /24 for IPv4."); + + if (request.IpRangeBaseAddress.AddressFamily == AddressFamily.InterNetworkV6 && request.IpRangeSubnet < 64) + throw new ArgumentOutOfRangeException(nameof(request.IpRange), "CIDRs are limited to a maximum of /64 for IPv6."); + + var req = new InternalDnsZoneTransferAclRequest + { + Name = request.Name, + IpRange = request.IpRange + }; + + return client.PutAsync($"/accounts/{request.AccountId}/secondary_dns/acls/{request.AclId}", req, cancellationToken); + } + + #endregion ACLs + + #region Force AXFR + + /// + /// Sends AXFR zone transfer request to primary nameserver(s). + /// + /// The instance. + /// The zone identifier. + /// A cancellation token used to propagate notification that this operation should be canceled. + public static Task> ForceAXFR(this ICloudflareClient client, string zoneId, CancellationToken cancellationToken = default) + { + zoneId.ValidateCloudflareId(); + + return client.PostAsync($"/zones/{zoneId}/secondary_dns/force_axfr", null, null, cancellationToken); + } + + #endregion Force AXFR + + #region Incoming + + /// + /// Create secondary zone configuration for incoming zone transfers. + /// + /// The instance. + /// The request. + /// A cancellation token used to propagate notification that this operation should be canceled. + public static Task> CreateSecondaryZoneConfiguration(this ICloudflareClient client, SecondaryZoneConfigurationRequest request, CancellationToken cancellationToken = default) + { + request.ZoneId.ValidateCloudflareId(); + + if (string.IsNullOrWhiteSpace(request.Name)) + throw new ArgumentNullException(nameof(request.Name), "The zone name is required."); + + if (request.AutoRefreshSeconds < 0) + throw new ArgumentOutOfRangeException(nameof(request.AutoRefreshSeconds), "Auto refresh seconds must be greater than or equal to 0."); + + if (request.Peers.Count == 0) + throw new ArgumentOutOfRangeException(nameof(request.Peers), "At least one peer is required."); + + foreach (string peer in request.Peers) + peer.ValidateCloudflareId(); + + var req = new InternalSecondaryZoneConfigurationRequest + { + Name = request.Name, + AutoRefreshSeconds = request.AutoRefreshSeconds, + Peers = request.Peers.ToList() + }; + + return client.PostAsync($"/zones/{request.ZoneId}/secondary_dns/incoming", req, null, cancellationToken); + } + + /// + /// Delete secondary zone configuration for incoming zone transfers. + /// + /// The instance. + /// The zone identifier. + /// A cancellation token used to propagate notification that this operation should be canceled. + public static Task> DeleteSecondaryZoneConfiguration(this ICloudflareClient client, string zoneId, CancellationToken cancellationToken = default) + { + zoneId.ValidateCloudflareId(); + + return client.DeleteAsync($"/zones/{zoneId}/secondary_dns/incoming", null, cancellationToken); + } + + /// + /// Get secondary zone configuration for incoming zone transfers. + /// + /// The instance. + /// The zone identifier. + /// A cancellation token used to propagate notification that this operation should be canceled. + public static Task> SecondaryZoneConfigurationDetails(this ICloudflareClient client, string zoneId, CancellationToken cancellationToken = default) + { + zoneId.ValidateCloudflareId(); + + return client.GetAsync($"/zones/{zoneId}/secondary_dns/incoming", null, cancellationToken); + } + + /// + /// Update secondary zone configuration for incoming zone transfers. + /// + /// The instance. + /// The request. + /// A cancellation token used to propagate notification that this operation should be canceled. + public static Task> UpdateSecondaryZoneConfiguration(this ICloudflareClient client, SecondaryZoneConfigurationRequest request, CancellationToken cancellationToken = default) + { + request.ZoneId.ValidateCloudflareId(); + + if (string.IsNullOrWhiteSpace(request.Name)) + throw new ArgumentNullException(nameof(request.Name), "The zone name is required."); + + if (request.AutoRefreshSeconds < 0) + throw new ArgumentOutOfRangeException(nameof(request.AutoRefreshSeconds), "Auto refresh seconds must be greater than or equal to 0."); + + if (request.Peers.Count == 0) + throw new ArgumentOutOfRangeException(nameof(request.Peers), "At least one peer is required."); + + foreach (string peer in request.Peers) + peer.ValidateCloudflareId(); + + var req = new InternalSecondaryZoneConfigurationRequest + { + Name = request.Name, + AutoRefreshSeconds = request.AutoRefreshSeconds, + Peers = request.Peers.ToList() + }; + + return client.PutAsync($"/zones/{request.ZoneId}/secondary_dns/incoming", req, cancellationToken); + } + + #endregion Incoming + + #region Outgoing + + /// + /// Create primary zone configuration for outgoing zone transfers. + /// + /// The instance. + /// The request. + /// A cancellation token used to propagate notification that this operation should be canceled. + public static Task> CreatePrimaryZoneConfiguration(this ICloudflareClient client, PrimaryZoneConfigurationRequest request, CancellationToken cancellationToken = default) + { + request.ZoneId.ValidateCloudflareId(); + + if (string.IsNullOrWhiteSpace(request.Name)) + throw new ArgumentNullException(nameof(request.Name), "The zone name is required."); + + if (request.Peers.Count == 0) + throw new ArgumentOutOfRangeException(nameof(request.Peers), "At least one peer is required."); + + foreach (string peer in request.Peers) + peer.ValidateCloudflareId(); + + var req = new InternalPrimaryZoneConfigurationRequest + { + Name = request.Name, + Peers = request.Peers.ToList() + }; + + return client.PostAsync($"/zones/{request.ZoneId}/secondary_dns/outgoing", req, null, cancellationToken); + } + + /// + /// Delete primary zone configuration for outgoing zone transfers. + /// + /// The instance. + /// The zone identifier. + /// A cancellation token used to propagate notification that this operation should be canceled. + public static Task> DeletePrimaryZoneConfiguration(this ICloudflareClient client, string zoneId, CancellationToken cancellationToken = default) + { + zoneId.ValidateCloudflareId(); + + return client.DeleteAsync($"/zones/{zoneId}/secondary_dns/outgoing", null, cancellationToken); + } + + /// + /// Disable outgoing zone transfers for primary zone and clears IXFR backlog of primary zone. + /// + /// The instance. + /// The zone identifier. + /// A cancellation token used to propagate notification that this operation should be canceled. + /// + /// Referring to the + /// documentation, + /// the text value should be Disabled. + /// + public static Task> DisableOutgoingZoneTransfers(this ICloudflareClient client, string zoneId, CancellationToken cancellationToken = default) + { + zoneId.ValidateCloudflareId(); + + return client.PostAsync($"/zones/{zoneId}/secondary_dns/outgoing/disable", null, null, cancellationToken); + } + + /// + /// Enable outgoing zone transfers for primary zone. + /// + /// The instance. + /// The zone identifier. + /// A cancellation token used to propagate notification that this operation should be canceled. + /// + /// Referring to the + /// documentation, + /// the text value should be Enabled. + /// + public static Task> EnableOutgoingZoneTransfers(this ICloudflareClient client, string zoneId, CancellationToken cancellationToken = default) + { + zoneId.ValidateCloudflareId(); + + return client.PostAsync($"/zones/{zoneId}/secondary_dns/outgoing/enable", null, null, cancellationToken); + } + + /// + /// Get primary zone configuration for outgoing zone transfers + /// + /// The instance. + /// The zone identifier. + /// A cancellation token used to propagate notification that this operation should be canceled. + public static Task> PrimaryZoneConfigurationDetails(this ICloudflareClient client, string zoneId, CancellationToken cancellationToken = default) + { + zoneId.ValidateCloudflareId(); + + return client.GetAsync($"/zones/{zoneId}/secondary_dns/outgoing", null, cancellationToken); + } + + /// + /// Update primary zone configuration for outgoing zone transfers. + /// + /// The instance. + /// The request. + /// A cancellation token used to propagate notification that this operation should be canceled. + public static Task> UpdatePrimaryZoneConfiguration(this ICloudflareClient client, PrimaryZoneConfigurationRequest request, CancellationToken cancellationToken = default) + { + request.ZoneId.ValidateCloudflareId(); + + if (string.IsNullOrWhiteSpace(request.Name)) + throw new ArgumentNullException(nameof(request.Name), "The zone name is required."); + + if (request.Peers.Count == 0) + throw new ArgumentOutOfRangeException(nameof(request.Peers), "At least one peer is required."); + + foreach (string peer in request.Peers) + peer.ValidateCloudflareId(); + + var req = new InternalPrimaryZoneConfigurationRequest + { + Name = request.Name, + Peers = request.Peers.ToList() + }; + + return client.PutAsync($"/zones/{request.ZoneId}/secondary_dns/outgoing", req, cancellationToken); + } + + /// + /// Notifies the secondary nameserver(s) and clears IXFR backlog of primary zone. + /// + /// The instance. + /// The zone identifier. + /// A cancellation token used to propagate notification that this operation should be canceled. + /// + /// Referring to the + /// documentation, + /// the text value should be OK. + /// + public static Task> ForceDNSNotify(this ICloudflareClient client, string zoneId, CancellationToken cancellationToken = default) + { + zoneId.ValidateCloudflareId(); + + return client.PostAsync($"/zones/{zoneId}/secondary_dns/outgoing/force_notify", null, null, cancellationToken); + } + + /// + /// Enable outgoing zone transfers for primary zone. + /// + /// The instance. + /// The zone identifier. + /// A cancellation token used to propagate notification that this operation should be canceled. + /// + /// Referring to the + /// documentation, + /// the text value should be Enabled or Disabled. + /// + public static Task> GetOutgoingZoneTransferStatus(this ICloudflareClient client, string zoneId, CancellationToken cancellationToken = default) + { + zoneId.ValidateCloudflareId(); + + return client.GetAsync($"/zones/{zoneId}/secondary_dns/outgoing/status", null, cancellationToken); + } + + #endregion Outgoing + + #region Peers + + /// + /// List Peers. + /// + /// The instance. + /// The account identifier. + /// A cancellation token used to propagate notification that this operation should be canceled. + public static Task>> ListPeers(this ICloudflareClient client, string accountId, CancellationToken cancellationToken = default) + { + accountId.ValidateCloudflareId(); + + return client.GetAsync>($"/accounts/{accountId}/secondary_dns/peers", null, cancellationToken); + } + + /// + /// Get Peer. + /// + /// The instance. + /// The account identifier. + /// The peer identifier. + /// A cancellation token used to propagate notification that this operation should be canceled. + public static Task> PeerDetails(this ICloudflareClient client, string accountId, string peerId, CancellationToken cancellationToken = default) + { + accountId.ValidateCloudflareId(); + peerId.ValidateCloudflareId(); + + return client.GetAsync($"/accounts/{accountId}/secondary_dns/peers/{peerId}", null, cancellationToken); + } + + /// + /// Create Peer. + /// + /// The instance. + /// The request. + /// A cancellation token used to propagate notification that this operation should be canceled. + public static Task> CreatePeer(this ICloudflareClient client, CreatePeerRequest request, CancellationToken cancellationToken = default) + { + request.AccountId.ValidateCloudflareId(); + + if (string.IsNullOrWhiteSpace(request.Name)) + throw new ArgumentNullException(nameof(request.Name), "The peer name is required."); + + var req = new InternalCreatePeerRequest + { + Name = request.Name + }; + + return client.PostAsync($"/accounts/{request.AccountId}/secondary_dns/peers", req, null, cancellationToken); + } + + /// + /// Modify Peer. + /// + /// The instance. + /// The request. + /// A cancellation token used to propagate notification that this operation should be canceled. + public static Task> UpdatePeer(this ICloudflareClient client, UpdatePeerRequest request, CancellationToken cancellationToken = default) + { + request.AccountId.ValidateCloudflareId(); + request.PeerId.ValidateCloudflareId(); + + if (string.IsNullOrWhiteSpace(request.Name)) + throw new ArgumentNullException(nameof(request.Name), "The peer name is required."); + + if (request.Port < 0 || 65535 < request.Port) + throw new ArgumentOutOfRangeException(nameof(request.Port), "The port must be between 0 and 65535."); + + var req = new InternalUpdatePeerRequest + { + Name = request.Name, + Ip = request.IpAddress, + IxfrEnable = request.IXFREnable, + Port = request.Port, + TSigId = request.TSIGId + }; + + return client.PutAsync($"/accounts/{request.AccountId}/secondary_dns/peers/{request.PeerId}", req, cancellationToken); + } + + /// + /// Delete Peer. + /// + /// The instance. + /// The account identifier. + /// The peer identifier. + /// A cancellation token used to propagate notification that this operation should be canceled. + public static Task> DeletePeer(this ICloudflareClient client, string accountId, string peerId, CancellationToken cancellationToken = default) + { + accountId.ValidateCloudflareId(); + peerId.ValidateCloudflareId(); + + return client.DeleteAsync($"/accounts/{accountId}/secondary_dns/peers/{peerId}", null, cancellationToken); + } + + #endregion Peers + + #region TSIGs + + /// + /// List TSIGs. + /// + /// The instance. + /// The account identifier. + /// A cancellation token used to propagate notification that this operation should be canceled. + public static Task>> ListTSIGs(this ICloudflareClient client, string accountId, CancellationToken cancellationToken = default) + { + accountId.ValidateCloudflareId(); + + return client.GetAsync>($"/accounts/{accountId}/secondary_dns/tsigs", null, cancellationToken); + } + + /// + /// Get TSIG. + /// + /// The instance. + /// The account identifier. + /// The TSIG identifier. + /// A cancellation token used to propagate notification that this operation should be canceled. + public static Task> TSIGDetails(this ICloudflareClient client, string accountId, string tsigId, CancellationToken cancellationToken = default) + { + accountId.ValidateCloudflareId(); + tsigId.ValidateCloudflareId(); + + return client.GetAsync($"/accounts/{accountId}/secondary_dns/tsigs/{tsigId}", null, cancellationToken); + } + + /// + /// Create TSIG. + /// + /// The instance. + /// The request. + /// A cancellation token used to propagate notification that this operation should be canceled. + public static Task> CreateTSIG(this ICloudflareClient client, CreateTSIGRequest request, CancellationToken cancellationToken = default) + { + request.AccountId.ValidateCloudflareId(); + + if (string.IsNullOrWhiteSpace(request.Name)) + throw new ArgumentNullException(nameof(request.Name), "The TSIG name is required."); + + if (!Enum.IsDefined(typeof(TSigAlgorithm), request.Algorithm)) + throw new ArgumentOutOfRangeException(nameof(request.Algorithm), "The TSIG algorithm is invalid."); + + if (string.IsNullOrWhiteSpace(request.Secret)) + throw new ArgumentNullException(nameof(request.Secret), "The TSIG secret is required."); + + var req = new InternalTSIGRequest + { + Name = request.Name, + Algorithm = request.Algorithm, + Secret = request.Secret + }; + + return client.PostAsync($"/accounts/{request.AccountId}/secondary_dns/tsigs", req, null, cancellationToken); + } + + /// + /// Modify TSIG. + /// + /// The instance. + /// The request. + /// A cancellation token used to propagate notification that this operation should be canceled. + public static Task> UpdateTSIG(this ICloudflareClient client, UpdateTSIGRequest request, CancellationToken cancellationToken = default) + { + request.AccountId.ValidateCloudflareId(); + request.TSigId.ValidateCloudflareId(); + + if (string.IsNullOrWhiteSpace(request.Name)) + throw new ArgumentNullException(nameof(request.Name), "The TSIG name is required."); + + if (!Enum.IsDefined(typeof(TSigAlgorithm), request.Algorithm)) + throw new ArgumentOutOfRangeException(nameof(request.Algorithm), "The TSIG algorithm is invalid."); + + if (string.IsNullOrWhiteSpace(request.Secret)) + throw new ArgumentNullException(nameof(request.Secret), "The TSIG secret is required."); + + var req = new InternalTSIGRequest + { + Name = request.Name, + Algorithm = request.Algorithm, + Secret = request.Secret + }; + + return client.PutAsync($"/accounts/{request.AccountId}/secondary_dns/tsigs/{request.TSigId}", req, cancellationToken); + } + + /// + /// Delete TSIG. + /// + /// The instance. + /// The account identifier. + /// The TSIG identifier. + /// A cancellation token used to propagate notification that this operation should be canceled. + public static Task> DeleteTSIG(this ICloudflareClient client, string accountId, string tsigId, CancellationToken cancellationToken = default) + { + accountId.ValidateCloudflareId(); + tsigId.ValidateCloudflareId(); + + return client.DeleteAsync($"/accounts/{accountId}/secondary_dns/tsigs/{tsigId}", null, cancellationToken); + } + + #endregion TSIGs + } +} diff --git a/src/Extensions/Cloudflare.Dns/Enums/TSigAlgorithm.cs b/src/Extensions/Cloudflare.Dns/Enums/TSigAlgorithm.cs new file mode 100644 index 0000000..e112e44 --- /dev/null +++ b/src/Extensions/Cloudflare.Dns/Enums/TSigAlgorithm.cs @@ -0,0 +1,100 @@ +using System.Runtime.Serialization; +using Newtonsoft.Json.Converters; + +namespace AMWD.Net.Api.Cloudflare.Dns +{ + /// + /// Available TSIG algorithms as recommended by IANA. + /// + [JsonConverter(typeof(StringEnumConverter))] + public enum TSigAlgorithm + { + /// + /// HMAC SHA1. + /// + /// + /// Implementation: must + ///
+ /// Use: not recommended + ///
+ [EnumMember(Value = "hmac-sha1")] + HMAC_SHA1 = 1, + + /// + /// HMAC SHA224. + /// + /// + /// Implementation: may + ///
+ /// Use: may + ///
+ [EnumMember(Value = "hmac-sha224")] + HMAC_SHA224 = 2, + + /// + /// HMAC SHA256. + /// + /// + /// Implementation: must + ///
+ /// Use: recommended + ///
+ [EnumMember(Value = "hmac-sha256")] + HMAC_SHA256 = 3, + + /// + /// HMAC SHA384. + /// + /// + /// Implementation: may + ///
+ /// Use: may + ///
+ [EnumMember(Value = "hmac-sha384")] + HMAC_SHA384 = 4, + + /// + /// HMAC SHA512. + /// + /// + /// Implementation: may + ///
+ /// Use: may + ///
+ [EnumMember(Value = "hmac-sha512")] + HMAC_SHA512 = 5, + + /// + /// HMAC SHA256 128. + /// + /// + /// Implementation: may + ///
+ /// Use: may + ///
+ [EnumMember(Value = "hmac-sha256-128")] + HMAC_SHA256_128 = 6, + + /// + /// HMAC SHA384 192. + /// + /// + /// Implementation: may + ///
+ /// Use: may + ///
+ [EnumMember(Value = "hmac-sha384-192")] + HMAC_SHA384_192 = 7, + + /// + /// HMAC SHA512 256. + /// + /// + /// Implementation: may + ///
+ /// Use: may + ///
+ [EnumMember(Value = "hmac-sha512-256")] + HMAC_SHA512_256 = 8, + } +} diff --git a/src/Extensions/Cloudflare.Dns/Internals/InternalCreatePeerRequest.cs b/src/Extensions/Cloudflare.Dns/Internals/InternalCreatePeerRequest.cs new file mode 100644 index 0000000..fcd7abb --- /dev/null +++ b/src/Extensions/Cloudflare.Dns/Internals/InternalCreatePeerRequest.cs @@ -0,0 +1,8 @@ +namespace AMWD.Net.Api.Cloudflare.Dns.Internals +{ + internal class InternalCreatePeerRequest + { + [JsonProperty("name")] + public string? Name { get; set; } + } +} diff --git a/src/Extensions/Cloudflare.Dns/Internals/InternalDnsZoneTransferAclRequest.cs b/src/Extensions/Cloudflare.Dns/Internals/InternalDnsZoneTransferAclRequest.cs new file mode 100644 index 0000000..2f93223 --- /dev/null +++ b/src/Extensions/Cloudflare.Dns/Internals/InternalDnsZoneTransferAclRequest.cs @@ -0,0 +1,11 @@ +namespace AMWD.Net.Api.Cloudflare.Dns +{ + internal class InternalDnsZoneTransferAclRequest + { + [JsonProperty("ip_range")] + public string? IpRange { get; set; } + + [JsonProperty("name")] + public string? Name { get; set; } + } +} diff --git a/src/Extensions/Cloudflare.Dns/Internals/InternalPrimaryZoneConfigurationRequest.cs b/src/Extensions/Cloudflare.Dns/Internals/InternalPrimaryZoneConfigurationRequest.cs new file mode 100644 index 0000000..7e9fcf8 --- /dev/null +++ b/src/Extensions/Cloudflare.Dns/Internals/InternalPrimaryZoneConfigurationRequest.cs @@ -0,0 +1,11 @@ +namespace AMWD.Net.Api.Cloudflare.Dns.Internals +{ + internal class InternalPrimaryZoneConfigurationRequest + { + [JsonProperty("name")] + public string? Name { get; set; } + + [JsonProperty("peers")] + public IReadOnlyCollection? Peers { get; set; } + } +} diff --git a/src/Extensions/Cloudflare.Dns/Internals/InternalSecondaryZoneConfigurationRequest.cs b/src/Extensions/Cloudflare.Dns/Internals/InternalSecondaryZoneConfigurationRequest.cs new file mode 100644 index 0000000..cb4fb95 --- /dev/null +++ b/src/Extensions/Cloudflare.Dns/Internals/InternalSecondaryZoneConfigurationRequest.cs @@ -0,0 +1,14 @@ +namespace AMWD.Net.Api.Cloudflare.Dns.Internals +{ + internal class InternalSecondaryZoneConfigurationRequest + { + [JsonProperty("name")] + public string? Name { get; set; } + + [JsonProperty("auto_refresh_seconds")] + public int? AutoRefreshSeconds { get; set; } + + [JsonProperty("peers")] + public IReadOnlyCollection? Peers { get; set; } + } +} diff --git a/src/Extensions/Cloudflare.Dns/Internals/InternalTSIGRequest.cs b/src/Extensions/Cloudflare.Dns/Internals/InternalTSIGRequest.cs new file mode 100644 index 0000000..534f437 --- /dev/null +++ b/src/Extensions/Cloudflare.Dns/Internals/InternalTSIGRequest.cs @@ -0,0 +1,14 @@ +namespace AMWD.Net.Api.Cloudflare.Dns.Internals +{ + internal class InternalTSIGRequest + { + [JsonProperty("algo")] + public TSigAlgorithm? Algorithm { get; set; } + + [JsonProperty("name")] + public string? Name { get; set; } + + [JsonProperty("secret")] + public string? Secret { get; set; } + } +} diff --git a/src/Extensions/Cloudflare.Dns/Internals/InternalUpdatePeerRequest.cs b/src/Extensions/Cloudflare.Dns/Internals/InternalUpdatePeerRequest.cs new file mode 100644 index 0000000..bf084c0 --- /dev/null +++ b/src/Extensions/Cloudflare.Dns/Internals/InternalUpdatePeerRequest.cs @@ -0,0 +1,17 @@ +namespace AMWD.Net.Api.Cloudflare.Dns.Internals +{ + internal class InternalUpdatePeerRequest : InternalCreatePeerRequest + { + [JsonProperty("ip")] + public string? Ip { get; set; } + + [JsonProperty("ixfr_enable")] + public bool? IxfrEnable { get; set; } + + [JsonProperty("port")] + public int? Port { get; set; } + + [JsonProperty("tsig_id")] + public string? TSigId { get; set; } + } +} diff --git a/src/Extensions/Cloudflare.Dns/Models/ACL.cs b/src/Extensions/Cloudflare.Dns/Models/ACL.cs new file mode 100644 index 0000000..57fbd33 --- /dev/null +++ b/src/Extensions/Cloudflare.Dns/Models/ACL.cs @@ -0,0 +1,31 @@ +namespace AMWD.Net.Api.Cloudflare.Dns +{ + /// + /// Represents an Access Control List (ACL) entry, which defines the allowed IP ranges for DNS zone transfers. + /// + public class ACL + { + /// + /// The unique identifier. + /// + [JsonProperty("id")] + public string? Id { get; set; } + + /// + /// Allowed IPv4/IPv6 address range of primary or secondary nameservers. + /// This will be applied for the entire account. + /// + /// + /// The IP range is used to allow additional NOTIFY IPs for secondary zones and IPs Cloudflare allows AXFR/IXFR requests from for primary zones. + /// CIDRs are limited to a maximum of /24 for IPv4 and /64 for IPv6 respectively. + /// + [JsonProperty("ip_range")] + public string? IpRange { get; set; } + + /// + /// The name of the ACL. + /// + [JsonProperty("name")] + public string? Name { get; set; } + } +} diff --git a/src/Extensions/Cloudflare.Dns/Models/IncomingZoneConfiguration.cs b/src/Extensions/Cloudflare.Dns/Models/IncomingZoneConfiguration.cs new file mode 100644 index 0000000..94f8f5d --- /dev/null +++ b/src/Extensions/Cloudflare.Dns/Models/IncomingZoneConfiguration.cs @@ -0,0 +1,57 @@ +namespace AMWD.Net.Api.Cloudflare.Dns +{ + /// + /// Represents a response of a secondary zone configuration. + /// + public class IncomingZoneConfiguration + { + /// + /// The unique identifier. + /// + [JsonProperty("id")] + public string? Id { get; set; } + + /// + /// How often should a secondary zone auto refresh regardless of DNS NOTIFY. + /// Not applicable for primary zones. + /// + [JsonProperty("auto_refresh_seconds")] + public int? AutoRefreshSeconds { get; set; } + + /// + /// The time for a specific event. + /// + [JsonProperty("checked_time")] + public DateTime? CheckedTime { get; set; } + + /// + /// The time for a specific event. + /// + [JsonProperty("created_time")] + public DateTime? CreatedTime { get; set; } + + /// + /// The time for a specific event. + /// + [JsonProperty("modified_time")] + public DateTime? ModifiedTime { get; set; } + + /// + /// The zone name. + /// + [JsonProperty("name")] + public string? Name { get; set; } + + /// + /// A list of peer tags. + /// + [JsonProperty("peers")] + public IReadOnlyCollection? Peers { get; set; } + + /// + /// The serial number of the SOA for the given zone. + /// + [JsonProperty("soa_serial")] + public int? SoaSerial { get; set; } + } +} diff --git a/src/Extensions/Cloudflare.Dns/Models/OutgoingZoneConfiguration.cs b/src/Extensions/Cloudflare.Dns/Models/OutgoingZoneConfiguration.cs new file mode 100644 index 0000000..b93ec5e --- /dev/null +++ b/src/Extensions/Cloudflare.Dns/Models/OutgoingZoneConfiguration.cs @@ -0,0 +1,50 @@ +namespace AMWD.Net.Api.Cloudflare.Dns +{ + /// + /// Represents a response of a secondary zone configuration. + /// + public class OutgoingZoneConfiguration + { + /// + /// The unique identifier. + /// + [JsonProperty("id")] + public string? Id { get; set; } + + /// + /// The time for a specific event. + /// + [JsonProperty("checked_time")] + public DateTime? CheckedTime { get; set; } + + /// + /// The time for a specific event. + /// + [JsonProperty("created_time")] + public DateTime? CreatedTime { get; set; } + + /// + /// The time for a specific event. + /// + [JsonProperty("last_transferred_time")] + public DateTime? LastTransferredTime { get; set; } + + /// + /// The zone name. + /// + [JsonProperty("name")] + public string? Name { get; set; } + + /// + /// A list of peer tags. + /// + [JsonProperty("peers")] + public IReadOnlyCollection? Peers { get; set; } + + /// + /// The serial number of the SOA for the given zone. + /// + [JsonProperty("soa_serial")] + public int? SoaSerial { get; set; } + } +} diff --git a/src/Extensions/Cloudflare.Dns/Models/Peer.cs b/src/Extensions/Cloudflare.Dns/Models/Peer.cs new file mode 100644 index 0000000..a29b422 --- /dev/null +++ b/src/Extensions/Cloudflare.Dns/Models/Peer.cs @@ -0,0 +1,61 @@ +namespace AMWD.Net.Api.Cloudflare.Dns +{ + /// + /// A Cloudflare peer. + /// Source + /// + public class Peer + { + /// + /// Initializes a new instance of the class. + /// + /// The unique identifier. + /// The name of the peer. + public Peer(string id, string name) + { + Id = id; + Name = name; + } + + /// + /// The unique identifier. + /// + [JsonProperty("id")] + public string Id { get; set; } + + /// + /// The name of the peer. + /// + [JsonProperty("name")] + public string Name { get; set; } + + /// + /// IPv4/IPv6 address of primary or secondary nameserver, depending on what zone this peer is linked to. + /// + /// For primary zones this IP defines the IP of the secondary nameserver Cloudflare will NOTIFY upon zone changes. + /// For secondary zones this IP defines the IP of the primary nameserver Cloudflare will send AXFR/IXFR requests to. + /// + /// + [JsonProperty("ip")] + public string? IpAddress { get; set; } + + /// + /// Enable IXFR transfer protocol, default is AXFR. + /// Only applicable to secondary zones. + /// + [JsonProperty("ixfr_enable")] + public bool? IXFREnabled { get; set; } + + /// + /// DNS port of primary or secondary nameserver, depending on what zone this peer is linked to. + /// + [JsonProperty("port")] + public int? Port { get; set; } + + /// + /// TSIG authentication will be used for zone transfer if configured. + /// + [JsonProperty("tsig_id")] + public string? TSIGId { get; set; } + } +} diff --git a/src/Extensions/Cloudflare.Dns/Models/TSIG.cs b/src/Extensions/Cloudflare.Dns/Models/TSIG.cs new file mode 100644 index 0000000..d68e2cd --- /dev/null +++ b/src/Extensions/Cloudflare.Dns/Models/TSIG.cs @@ -0,0 +1,35 @@ +using System.Runtime.Serialization; +using Newtonsoft.Json.Converters; + +namespace AMWD.Net.Api.Cloudflare.Dns +{ + /// + /// Represents a Transaction Signature (TSIG) used for securing DNS messages. + /// + public class TSIG + { + /// + /// The unique identifier. + /// + [JsonProperty("id")] + public string? Id { get; set; } + + /// + /// TSIG algorithm. + /// + [JsonProperty("algo")] + public TSigAlgorithm? Algorithm { get; set; } + + /// + /// TSIG key name. + /// + [JsonProperty("name")] + public string? Name { get; set; } + + /// + /// TSIG secret. + /// + [JsonProperty("secret")] + public string? Secret { get; set; } + } +} diff --git a/src/Extensions/Cloudflare.Dns/README.md b/src/Extensions/Cloudflare.Dns/README.md index 54ab320..8c2ce02 100644 --- a/src/Extensions/Cloudflare.Dns/README.md +++ b/src/Extensions/Cloudflare.Dns/README.md @@ -59,6 +59,59 @@ This package contains the feature set of the _DNS_ section of the Cloudflare API - [Show DNS Settings](https://developers.cloudflare.com/api/resources/dns/subresources/settings/subresources/zone/methods/get/) +#### [Zone Transfers] + +##### [ACLs] + +- [List ACLs](https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/acls/methods/list/) +- [ACL Details](https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/acls/methods/get/) +- [Create ACL](https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/acls/methods/create/) +- [Update ACL](https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/acls/methods/update/) +- [Delete ACL](https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/acls/methods/delete/) + + +##### Force AXFR + +- [Force AXFR](https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/force_axfr/methods/create/) + + +##### [Incoming] + +- [Create Secondary Zone Configuration](https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/incoming/methods/create/) +- [Delete Secondary Zone Configuration](https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/incoming/methods/delete/) +- [Secondary Zone Configuration Details](https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/incoming/methods/get/) +- [Update Secondary Zone Configuration](https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/incoming/methods/update/) + + +##### [Outgoing] + +- [Create Primary Zone Configuration](https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/outgoing/methods/create/) +- [Delete Primary Zone Configuration](https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/outgoing/methods/delete/) +- [Disable Outgoing Zone Transfers](https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/outgoing/methods/disable/) +- [Enable Outgoing Zone Transfers](https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/outgoing/methods/enable/) +- [Primary Zone Configuration Details](https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/outgoing/methods/get/) +- [Update Primary Zone Configuration](https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/outgoing/methods/update/) +- [Force DNS Notify](https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/outgoing/methods/force_notify/) +- [Get Outgoing Zone Transfer Status](https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/outgoing/subresources/status/methods/get/) + + +##### [Peers] + +- [List Peers](https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/peers/methods/list/) +- [Peer Details](https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/peers/methods/get/) +- [Create Peer](https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/peers/methods/create/) +- [Update Peer](https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/peers/methods/update/) +- [Delete Peer](https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/peers/methods/delete/) + + +##### [TSIGs] (Transaction SIGnatures) + +- [List TSIGs](https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/tsigs/methods/list/) +- [TSIG Details](https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/tsigs/methods/get/) +- [Create TSIG](https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/tsigs/methods/create/) +- [Update TSIG](https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/tsigs/methods/update/) +- [Delete TSIG](https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/tsigs/methods/delete/) + --- @@ -77,3 +130,9 @@ Published under MIT License (see [choose a license]) [Settings]: https://developers.cloudflare.com/api/resources/dns/subresources/settings/ [Account]: https://developers.cloudflare.com/api/resources/dns/subresources/settings/subresources/account/ [Zone]: https://developers.cloudflare.com/api/resources/dns/subresources/settings/subresources/zone/ + [Zone Transfers]: https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/ + [ACLs]: https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/acls/ + [Incoming]: https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/incoming/ + [Outgoing]: https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/outgoing/ + [Peers]: https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/peers/ + [TSIGs]: https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/tsigs/ diff --git a/src/Extensions/Cloudflare.Dns/Requests/CreateACLRequest.cs b/src/Extensions/Cloudflare.Dns/Requests/CreateACLRequest.cs new file mode 100644 index 0000000..c49daca --- /dev/null +++ b/src/Extensions/Cloudflare.Dns/Requests/CreateACLRequest.cs @@ -0,0 +1,77 @@ +using System.Net; +using System.Net.Sockets; + +namespace AMWD.Net.Api.Cloudflare.Dns +{ + /// + /// Represents a request to create a DNS zone transfer access control list (ACL). + /// + public class CreateACLRequest + { + /// + /// Initializes a new instance of the class. + /// + /// The account identifier. + /// Allowed IPv4/IPv6 address range of primary or secondary nameservers (CIDR). + /// The name of the ACL. + public CreateACLRequest(string accountId, string ipRange, string name) + { + IpRangeBaseAddress = IPAddress.None; + IpRangeSubnet = 0; + + AccountId = accountId; + IpRange = ipRange; + Name = name; + } + + /// + /// The account identifier. + /// + public string AccountId { get; set; } + + /// + /// Allowed IPv4/IPv6 address range of primary or secondary nameservers. + /// This will be applied for the entire account. + /// + /// + /// The IP range is used to allow additional NOTIFY IPs for secondary zones and IPs Cloudflare allows AXFR/IXFR requests from for primary zones. + /// CIDRs are limited to a maximum of /24 for IPv4 and /64 for IPv6 respectively. + /// + public string IpRange + { + get => $"{IpRangeBaseAddress}/{IpRangeSubnet}"; + set + { + if (string.IsNullOrWhiteSpace(value)) + throw new ArgumentNullException(nameof(value), $"{nameof(IpRange)} cannot be null or empty."); + + string[] parts = value.Split('/'); + if (parts.Length != 2) + throw new FormatException("Invalid IP range format."); + + var prefix = IPAddress.Parse(parts[0]); + + if (!int.TryParse(parts[1], out int prefixLength)) + throw new FormatException("Invalid IP range subnet format."); + + if (prefix.AddressFamily == AddressFamily.InterNetwork && (prefixLength < 0 || 32 < prefixLength)) + throw new FormatException("Invalid subnet length for IPv4."); + + if (prefix.AddressFamily == AddressFamily.InterNetworkV6 && (prefixLength < 0 || 128 < prefixLength)) + throw new FormatException("Invalid subnet length for IPv6."); + + IpRangeBaseAddress = prefix; + IpRangeSubnet = prefixLength; + } + } + + /// + /// The name of the ACL. + /// + public string Name { get; set; } + + internal IPAddress IpRangeBaseAddress { get; set; } + + internal int IpRangeSubnet { get; set; } + } +} diff --git a/src/Extensions/Cloudflare.Dns/Requests/CreatePeerRequest.cs b/src/Extensions/Cloudflare.Dns/Requests/CreatePeerRequest.cs new file mode 100644 index 0000000..015a18d --- /dev/null +++ b/src/Extensions/Cloudflare.Dns/Requests/CreatePeerRequest.cs @@ -0,0 +1,29 @@ +namespace AMWD.Net.Api.Cloudflare.Dns +{ + /// + /// Represents a request to create a new peer within a specific account. + /// + public class CreatePeerRequest + { + /// + /// Initializes a new instance of the class. + /// + /// The account identifier. + /// The name of the peer. + public CreatePeerRequest(string accountId, string name) + { + AccountId = accountId; + Name = name; + } + + /// + /// The account identifier. + /// + public string AccountId { get; set; } + + /// + /// The name of the peer. + /// + public string Name { get; set; } + } +} diff --git a/src/Extensions/Cloudflare.Dns/Requests/CreateTSIGRequest.cs b/src/Extensions/Cloudflare.Dns/Requests/CreateTSIGRequest.cs new file mode 100644 index 0000000..037273e --- /dev/null +++ b/src/Extensions/Cloudflare.Dns/Requests/CreateTSIGRequest.cs @@ -0,0 +1,43 @@ +namespace AMWD.Net.Api.Cloudflare.Dns +{ + /// + /// Represents a request to create a TSIG (Transaction Signature) key for DNS operations. + /// + public class CreateTSIGRequest + { + /// + /// Initializes a new instance of the class. + /// + /// The account identifier. + /// TSIG key name. + /// TSIG secret. + public CreateTSIGRequest(string accountId, string name, string secret) + { + Algorithm = TSigAlgorithm.HMAC_SHA256; + + AccountId = accountId; + Name = name; + Secret = secret; + } + + /// + /// The account identifier. + /// + public string AccountId { get; set; } + + /// + /// TSIG algorithm. + /// + public TSigAlgorithm Algorithm { get; set; } + + /// + /// TSIG key name. + /// + public string Name { get; set; } + + /// + /// TSIG secret. + /// + public string Secret { get; set; } + } +} diff --git a/src/Extensions/Cloudflare.Dns/Requests/PrimaryZoneConfigurationRequest.cs b/src/Extensions/Cloudflare.Dns/Requests/PrimaryZoneConfigurationRequest.cs new file mode 100644 index 0000000..2d18046 --- /dev/null +++ b/src/Extensions/Cloudflare.Dns/Requests/PrimaryZoneConfigurationRequest.cs @@ -0,0 +1,34 @@ +namespace AMWD.Net.Api.Cloudflare.Dns +{ + /// + /// Represents a request to create a primary zone configuration. + /// + public class PrimaryZoneConfigurationRequest + { + /// + /// Initializes a new instance of the class. + /// + /// The zone identifier. + /// The zone name. + public PrimaryZoneConfigurationRequest(string zoneId, string name) + { + ZoneId = zoneId; + Name = name; + } + + /// + /// The zone identifier. + /// + public string ZoneId { get; set; } + + /// + /// The zone name. + /// + public string Name { get; set; } + + /// + /// A list of peer tags. + /// + public IReadOnlyCollection Peers { get; set; } = []; + } +} diff --git a/src/Extensions/Cloudflare.Dns/Requests/SecondaryZoneConfigurationRequest.cs b/src/Extensions/Cloudflare.Dns/Requests/SecondaryZoneConfigurationRequest.cs new file mode 100644 index 0000000..4090caa --- /dev/null +++ b/src/Extensions/Cloudflare.Dns/Requests/SecondaryZoneConfigurationRequest.cs @@ -0,0 +1,41 @@ +namespace AMWD.Net.Api.Cloudflare.Dns +{ + /// + /// Represents a request to create a secondary zone configuration. + /// + public class SecondaryZoneConfigurationRequest + { + /// + /// Initializes a new instance of the class. + /// + /// The zone identifier. + /// The zone name. + public SecondaryZoneConfigurationRequest(string zoneId, string name) + { + ZoneId = zoneId; + Name = name; + + Peers = []; + } + + /// + /// The zone identifier. + /// + public string ZoneId { get; set; } + + /// + /// The Zone name. + /// + public string Name { get; set; } + + /// + /// How often should a secondary zone auto refresh regardless of DNS NOTIFY. Not applicable for primary zones. + /// + public int AutoRefreshSeconds { get; set; } + + /// + /// A list of peer tags. + /// + public IReadOnlyCollection Peers { get; set; } + } +} diff --git a/src/Extensions/Cloudflare.Dns/Requests/UpdateDnsZoneTransferAclRequest.cs b/src/Extensions/Cloudflare.Dns/Requests/UpdateDnsZoneTransferAclRequest.cs new file mode 100644 index 0000000..a9566b4 --- /dev/null +++ b/src/Extensions/Cloudflare.Dns/Requests/UpdateDnsZoneTransferAclRequest.cs @@ -0,0 +1,26 @@ +namespace AMWD.Net.Api.Cloudflare.Dns +{ + /// + /// Represents a request to update an existing DNS zone transfer access control list (ACL). + /// + public class UpdateDnsZoneTransferAclRequest : CreateACLRequest + { + /// + /// Initializes a new instance of the class. + /// + /// The account identifier. + /// The access control list identifier. + /// Allowed IPv4/IPv6 address range of primary or secondary nameservers (CIDR). + /// The name of the ACL. + public UpdateDnsZoneTransferAclRequest(string accountId, string aclId, string ipRange, string name) + : base(accountId, ipRange, name) + { + AclId = aclId; + } + + /// + /// The access control list identifier. + /// + public string AclId { get; set; } + } +} diff --git a/src/Extensions/Cloudflare.Dns/Requests/UpdatePeerRequest.cs b/src/Extensions/Cloudflare.Dns/Requests/UpdatePeerRequest.cs new file mode 100644 index 0000000..2d2a9a4 --- /dev/null +++ b/src/Extensions/Cloudflare.Dns/Requests/UpdatePeerRequest.cs @@ -0,0 +1,49 @@ +namespace AMWD.Net.Api.Cloudflare.Dns +{ + /// + /// Represents a request to update an existing peer with new configuration details. + /// + public class UpdatePeerRequest : CreatePeerRequest + { + /// + /// Initializes a new instance of the class. + /// + /// The account identifier. + /// The peer identifier. + /// The name of the peer + public UpdatePeerRequest(string accountId, string peerId, string name) + : base(accountId, name) + { + PeerId = peerId; + } + + /// + /// The peer identifier. + /// + public string PeerId { get; set; } + + /// + /// IPv4/IPv6 address of primary or secondary nameserver, depending on what zone this peer is linked to. + /// + /// For primary zones this IP defines the IP of the secondary nameserver Cloudflare will NOTIFY upon zone changes. + /// For secondary zones this IP defines the IP of the primary nameserver Cloudflare will send AXFR/IXFR requests to. + /// + /// + public string? IpAddress { get; set; } + + /// + /// Enable IXFR transfer protocol, default is AXFR. Only applicable to secondary zones. + /// + public bool? IXFREnable { get; set; } + + /// + /// DNS port of primary or secondary nameserver, depending on what zone this peer is linked to. + /// + public int? Port { get; set; } + + /// + /// TSIG authentication will be used for zone transfer if configured. + /// + public string? TSIGId { get; set; } + } +} diff --git a/src/Extensions/Cloudflare.Dns/Requests/UpdateTSIGRequest.cs b/src/Extensions/Cloudflare.Dns/Requests/UpdateTSIGRequest.cs new file mode 100644 index 0000000..4026c7b --- /dev/null +++ b/src/Extensions/Cloudflare.Dns/Requests/UpdateTSIGRequest.cs @@ -0,0 +1,26 @@ +namespace AMWD.Net.Api.Cloudflare.Dns +{ + /// + /// Represents a request to update an existing TSIG (Transaction Signature) key. + /// + public class UpdateTSIGRequest : CreateTSIGRequest + { + /// + /// Initializes a new instance of the class. + /// + /// The account identifier. + /// The TSIG identifier. + /// TSIG key name. + /// TSIG secret. + public UpdateTSIGRequest(string accountId, string tsigId, string name, string secret) + : base(accountId, name, secret) + { + TSigId = tsigId; + } + + /// + /// The TSIG identifier. + /// + public string TSigId { get; set; } + } +} diff --git a/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/ACLs/ACLDetailsTest.cs b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/ACLs/ACLDetailsTest.cs new file mode 100644 index 0000000..c0eada6 --- /dev/null +++ b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/ACLs/ACLDetailsTest.cs @@ -0,0 +1,75 @@ +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.DnsZoneTransfersExtensions.ACLs +{ + [TestClass] + public class ACLDetailsTest + { + public TestContext TestContext { get; set; } + + private const string AccountId = "023e105f4ecef8ad9ca31a8372d0c353"; + private const string AclId = "23ff594956f20c2a721606e94745a8aa"; + + 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 ACL + { + Id = AclId, + IpRange = "192.0.2.0/24", + Name = "Test ACL" + } + }; + } + + [TestMethod] + public async Task ShouldGetAclDetails() + { + // Arrange + var client = GetClient(); + + // Act + var response = await client.ACLDetails(AccountId, AclId, TestContext.CancellationToken); + + // Assert + Assert.IsNotNull(response); + Assert.IsTrue(response.Success); + Assert.AreEqual(_response.Result, response.Result); + + Assert.HasCount(1, _callbacks); + + string requestPath = _callbacks.First(); + Assert.AreEqual($"/accounts/{AccountId}/secondary_dns/acls/{AclId}", requestPath); + + _clientMock.Verify(m => m.GetAsync($"/accounts/{AccountId}/secondary_dns/acls/{AclId}", null, TestContext.CancellationToken), Times.Once); + _clientMock.VerifyNoOtherCalls(); + } + + private ICloudflareClient GetClient() + { + _clientMock = new Mock(); + _clientMock + .Setup(m => m.GetAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((requestPath, _, _) => _callbacks.Add(requestPath)) + .ReturnsAsync(() => _response); + + return _clientMock.Object; + } + } +} diff --git a/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/ACLs/CreateACLTest.cs b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/ACLs/CreateACLTest.cs new file mode 100644 index 0000000..dc6a158 --- /dev/null +++ b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/ACLs/CreateACLTest.cs @@ -0,0 +1,166 @@ +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.DnsZoneTransfersExtensions.ACLs +{ + [TestClass] + public class CreateACLTest + { + public TestContext TestContext { get; set; } + + private const string AccountId = "023e105f4ecef8ad9ca31a8372d0c353"; + + private Mock _clientMock; + private CloudflareResponse _response; + private List<(string RequestPath, InternalDnsZoneTransferAclRequest Request)> _callbacks; + private CreateACLRequest _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 ACL + { + Id = "23ff594956f20c2a721606e94745a8aa", + IpRange = "192.0.2.53/28", + Name = "my-acl-1" + } + }; + + _request = new CreateACLRequest( + accountId: AccountId, + ipRange: "192.0.2.53/28", + name: "my-acl-1" + ); + } + + [TestMethod] + public async Task ShouldCreateAcl() + { + // Arrange + var client = GetClient(); + + // Act + var response = await client.CreateACL(_request, TestContext.CancellationToken); + + // Assert + Assert.IsNotNull(response); + Assert.IsTrue(response.Success); + Assert.AreEqual(_response.Result, response.Result); + + Assert.HasCount(1, _callbacks); + + var (requestPath, request) = _callbacks.First(); + Assert.AreEqual($"/accounts/{AccountId}/secondary_dns/acls", requestPath); + + Assert.IsNotNull(request); + Assert.AreEqual(_request.Name, request.Name); + Assert.AreEqual(_request.IpRange, request.IpRange); + + Assert.AreEqual("192.0.2.53", _request.IpRangeBaseAddress.ToString()); + Assert.AreEqual(28, _request.IpRangeSubnet); + + _clientMock.Verify(m => m.PostAsync($"/accounts/{AccountId}/secondary_dns/acls", It.IsAny(), null, TestContext.CancellationToken), Times.Once); + _clientMock.VerifyNoOtherCalls(); + } + + [TestMethod] + [DataRow("127.0.0.1/20")] + [DataRow("fd00::/56")] + public async Task ShouldThrowArumentOutOfRangeExceptionForWrongSubnetDefinition(string ipRange) + { + // Arrange + _request.IpRange = ipRange; + var client = GetClient(); + + // Act & Assert + await Assert.ThrowsExactlyAsync(async () => + { + await client.CreateACL(_request, TestContext.CancellationToken); + }); + } + + [TestMethod] + [DataRow(null)] + [DataRow("")] + [DataRow("\t")] + [DataRow(" ")] + public void ShouldThrowArgumentNullExceptionForIpRange(string ipRange) + { + // Arrange + + // Act & Assert + Assert.ThrowsExactly(() => + { + _ = new CreateACLRequest(AccountId, ipRange, "my-acl-1"); + }); + } + + [TestMethod] + [DataRow("192.0.2.53")] + [DataRow("192.0.2.53/28/28")] + public void ShouldThrowFormatExceptionForInvalidFormat(string ipRange) + { + // Arrange + + // Act & Assert + Assert.ThrowsExactly(() => + { + _ = new CreateACLRequest(AccountId, ipRange, "my-acl-1"); + }); + } + + [TestMethod] + public void ShouldThrowFormatExceptionForInvalidSubnetFormat() + { + // Arrange + + // Act & Assert + Assert.ThrowsExactly(() => + { + _ = new CreateACLRequest(AccountId, "192.0.2.53/a", "my-acl-1"); + }); + } + + [TestMethod] + [DataRow("127.0.0.1/-1")] + [DataRow("127.0.0.1/33")] + [DataRow("fd00::/-1")] + [DataRow("fd00::/129")] + public void ShouldThrowFormatExceptionForInvalidSubnetLength(string ipRange) + { + // Arrange + + // Act & Assert + Assert.ThrowsExactly(() => + { + _ = new CreateACLRequest(AccountId, ipRange, "my-acl-1"); + }); + } + + private ICloudflareClient GetClient() + { + _clientMock = new Mock(); + _clientMock + .Setup(m => m.PostAsync(It.IsAny(), 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/DnsZoneTransfersExtensions/ACLs/DeleteACLTest.cs b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/ACLs/DeleteACLTest.cs new file mode 100644 index 0000000..ef0ee46 --- /dev/null +++ b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/ACLs/DeleteACLTest.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +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.DnsZoneTransfersExtensions.ACLs +{ + [TestClass] + public class DeleteACLTest + { + public TestContext TestContext { get; set; } + + private const string AccountId = "023e105f4ecef8ad9ca31a8372d0c353"; + private const string AclId = "23ff594956f20c2a721606e94745a8aa"; + + private Mock _clientMock; + private CloudflareResponse _response; + private List _callbacks; + + [TestInitialize] + public void Initialize() + { + _callbacks = new List(); + + _response = new CloudflareResponse + { + Success = true, + Messages = [new ResponseInfo(1000, "Message 1")], + Errors = [new ResponseInfo(1000, "Error 1")], + Result = new Identifier + { + Id = AclId + } + }; + } + + [TestMethod] + public async Task ShouldDeleteAcl() + { + // Arrange + var client = GetClient(); + + // Act + var response = await client.DeleteACL(AccountId, AclId, TestContext.CancellationToken); + + // Assert + Assert.IsNotNull(response); + Assert.IsTrue(response.Success); + Assert.AreEqual(_response.Result, response.Result); + + Assert.HasCount(1, _callbacks); + + string requestPath = _callbacks.First(); + Assert.AreEqual($"/accounts/{AccountId}/secondary_dns/acls/{AclId}", requestPath); + + _clientMock.Verify(m => m.DeleteAsync($"/accounts/{AccountId}/secondary_dns/acls/{AclId}", null, TestContext.CancellationToken), Times.Once); + _clientMock.VerifyNoOtherCalls(); + } + + private ICloudflareClient GetClient() + { + _clientMock = new Mock(); + _clientMock + .Setup(m => m.DeleteAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((requestPath, _, _) => _callbacks.Add(requestPath)) + .ReturnsAsync(() => _response); + + return _clientMock.Object; + } + } +} diff --git a/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/ACLs/ListACLsTest.cs b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/ACLs/ListACLsTest.cs new file mode 100644 index 0000000..f8d7988 --- /dev/null +++ b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/ACLs/ListACLsTest.cs @@ -0,0 +1,77 @@ +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.DnsZoneTransfersExtensions.ACLs +{ + [TestClass] + public class ListACLsTest + { + public TestContext TestContext { get; set; } + + private const string AccountId = "023e105f4ecef8ad9ca31a8372d0c353"; + + private Mock _clientMock; + private CloudflareResponse> _response; + private List _callbacks; + + [TestInitialize] + public void Initialize() + { + _callbacks = new List(); + + _response = new CloudflareResponse> + { + Success = true, + Messages = new List { new ResponseInfo(1000, "Message 1") }, + Errors = new List { new ResponseInfo(1000, "Error 1") }, + Result = new List + { + new ACL + { + Id = "23ff594956f20c2a721606e94745a8aa", + IpRange = "192.0.2.0/24", + Name = "Test ACL" + } + } + }; + } + + [TestMethod] + public async Task ShouldListAcls() + { + // Arrange + var client = GetClient(); + + // Act + var response = await client.ListACLs(AccountId, TestContext.CancellationToken); + + // Assert + Assert.IsNotNull(response); + Assert.IsTrue(response.Success); + Assert.AreEqual(_response.Result, response.Result); + + Assert.HasCount(1, _callbacks); + + string requestPath = _callbacks.First(); + Assert.AreEqual($"/accounts/{AccountId}/secondary_dns/acls", requestPath); + + _clientMock.Verify(m => m.GetAsync>($"/accounts/{AccountId}/secondary_dns/acls", null, TestContext.CancellationToken), Times.Once); + _clientMock.VerifyNoOtherCalls(); + } + + private ICloudflareClient GetClient() + { + _clientMock = new Mock(); + _clientMock + .Setup(m => m.GetAsync>(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((requestPath, _, _) => _callbacks.Add(requestPath)) + .ReturnsAsync(() => _response); + + return _clientMock.Object; + } + } +} diff --git a/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/ACLs/UpdateACLTest.cs b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/ACLs/UpdateACLTest.cs new file mode 100644 index 0000000..289a641 --- /dev/null +++ b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/ACLs/UpdateACLTest.cs @@ -0,0 +1,110 @@ +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.DnsZoneTransfersExtensions.ACLs +{ + [TestClass] + public class UpdateACLTest + { + public TestContext TestContext { get; set; } + + private const string AccountId = "023e105f4ecef8ad9ca31a8372d0c353"; + private const string AclId = "23ff594956f20c2a721606e94745a8aa"; + + private Mock _clientMock; + private CloudflareResponse _response; + private List<(string RequestPath, InternalDnsZoneTransferAclRequest Request)> _callbacks; + private UpdateDnsZoneTransferAclRequest _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 ACL + { + Id = AclId, + IpRange = "192.0.2.53/28", + Name = "my-acl-1" + } + }; + + _request = new UpdateDnsZoneTransferAclRequest( + accountId: AccountId, + aclId: AclId, + ipRange: "192.0.2.53/28", + name: "my-acl-1" + ); + } + + [TestMethod] + public async Task ShouldUpdateAcl() + { + // Arrange + var client = GetClient(); + + // Act + var response = await client.UpdateACL(_request, TestContext.CancellationToken); + + // Assert + Assert.IsNotNull(response); + Assert.IsTrue(response.Success); + Assert.AreEqual(_response.Result, response.Result); + + Assert.HasCount(1, _callbacks); + + var (requestPath, request) = _callbacks.First(); + Assert.AreEqual($"/accounts/{AccountId}/secondary_dns/acls/{AclId}", requestPath); + + Assert.IsNotNull(request); + Assert.AreEqual(_request.Name, request.Name); + Assert.AreEqual(_request.IpRange, request.IpRange); + + Assert.AreEqual("192.0.2.53", _request.IpRangeBaseAddress.ToString()); + Assert.AreEqual(28, _request.IpRangeSubnet); + + _clientMock.Verify(m => m.PutAsync($"/accounts/{AccountId}/secondary_dns/acls/{AclId}", It.IsAny(), TestContext.CancellationToken), Times.Once); + _clientMock.VerifyNoOtherCalls(); + } + + [TestMethod] + [DataRow("127.0.0.1/20")] + [DataRow("fd00::/56")] + public async Task ShouldThrowArumentOutOfRangeExceptionForWrongSubnetDefinition(string ipRange) + { + // Arrange + _request.IpRange = ipRange; + var client = GetClient(); + + // Act & Assert + await Assert.ThrowsExactlyAsync(async () => + { + await client.UpdateACL(_request, TestContext.CancellationToken); + }); + } + + 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/DnsZoneTransfersExtensions/ForceAXFRTest.cs b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/ForceAXFRTest.cs new file mode 100644 index 0000000..1376f17 --- /dev/null +++ b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/ForceAXFRTest.cs @@ -0,0 +1,75 @@ +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.DnsZoneTransfersExtensions +{ + [TestClass] + public class ForceAXFRTest + { + public TestContext TestContext { get; set; } + + private const string ZoneId = "023e105f4ecef8ad9ca31a8372d0c353"; + + private Mock _clientMock; + private CloudflareResponse _response; + private List<(string RequestPath, object Request)> _callbacks; + + [TestInitialize] + public void Initialize() + { + _callbacks = []; + + _response = new CloudflareResponse + { + Success = true, + Messages = [ + new ResponseInfo(1000, "Message 1") + ], + Errors = [ + new ResponseInfo(1000, "Error 1") + ], + Result = "OK" + }; + } + + [TestMethod] + public async Task ShouldForceAXFR() + { + // Arrange + var client = GetClient(); + + // Act + var response = await client.ForceAXFR(ZoneId, TestContext.CancellationToken); + + // Assert + Assert.IsNotNull(response); + Assert.IsTrue(response.Success); + Assert.AreEqual(_response.Result, response.Result); + + Assert.HasCount(1, _callbacks); + + var (requestPath, request) = _callbacks.First(); + Assert.AreEqual($"/zones/{ZoneId}/secondary_dns/force_axfr", requestPath); + + Assert.IsNull(request); + + _clientMock.Verify(m => m.PostAsync($"/zones/{ZoneId}/secondary_dns/force_axfr", null, null, TestContext.CancellationToken), 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, _, _) => _callbacks.Add((requestPath, request))) + .ReturnsAsync(() => _response); + + return _clientMock.Object; + } + } +} diff --git a/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/Incoming/CreateSecondaryZoneConfigurationTest.cs b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/Incoming/CreateSecondaryZoneConfigurationTest.cs new file mode 100644 index 0000000..c3dda78 --- /dev/null +++ b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/Incoming/CreateSecondaryZoneConfigurationTest.cs @@ -0,0 +1,157 @@ +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.DnsZoneTransfersExtensions.Incoming +{ + [TestClass] + public class CreateSecondaryZoneConfigurationTest + { + public TestContext TestContext { get; set; } + + private const string ZoneId = "023e105f4ecef8ad9ca31a8372d0c353"; + private const string PeerId = "23ff594956f20c2a721606e94745a8aa"; + + private Mock _clientMock; + private CloudflareResponse _response; + private List<(string RequestPath, InternalSecondaryZoneConfigurationRequest Request)> _callbacks; + private SecondaryZoneConfigurationRequest _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 IncomingZoneConfiguration + { + Id = "269d8f4853475ca241c4e730be286b20", + Name = "www.example.com", + AutoRefreshSeconds = 86400, + Peers = [PeerId] + } + }; + + _request = new SecondaryZoneConfigurationRequest(ZoneId, "www.example.com") + { + AutoRefreshSeconds = 86400, + Peers = [PeerId] + }; + } + + [TestMethod] + public async Task ShouldCreateSecondaryZoneConfiguration() + { + // Arrange + var client = GetClient(); + + // Act + var response = await client.CreateSecondaryZoneConfiguration(_request, TestContext.CancellationToken); + + // Assert + Assert.IsNotNull(response); + Assert.IsTrue(response.Success); + Assert.AreEqual(_response.Result, response.Result); + + Assert.HasCount(1, _callbacks); + + var (requestPath, request) = _callbacks.First(); + Assert.AreEqual($"/zones/{ZoneId}/secondary_dns/incoming", requestPath); + Assert.IsNotNull(request); + + Assert.AreEqual(_request.Name, request.Name); + Assert.AreEqual(_request.AutoRefreshSeconds, request.AutoRefreshSeconds); + CollectionAssert.AreEqual(_request.Peers.ToList(), request.Peers.ToList()); + + _clientMock.Verify(m => m.PostAsync( + $"/zones/{ZoneId}/secondary_dns/incoming", + It.IsAny(), + null, + It.IsAny()), Times.Once); + + _clientMock.VerifyNoOtherCalls(); + } + + [TestMethod] + [DataRow(null)] + [DataRow("")] + [DataRow(" ")] + public async Task ShouldThrowArgumentNullExceptionForName(string name) + { + // Arrange + _request.Name = name; + var client = GetClient(); + + // Act & Assert + await Assert.ThrowsExactlyAsync(async () => + { + await client.CreateSecondaryZoneConfiguration(_request, TestContext.CancellationToken); + }); + } + + [TestMethod] + public async Task ShouldThrowArgumentOutOfRangeExceptionForAutoRefreshSeconds() + { + // Arrange + _request.AutoRefreshSeconds = -1; + var client = GetClient(); + + // Act & Assert + await Assert.ThrowsExactlyAsync(async () => + { + await client.CreateSecondaryZoneConfiguration(_request, TestContext.CancellationToken); + }); + } + + [TestMethod] + public async Task ShouldThrowArgumentOutOfRangeExceptionForEmptyPeers() + { + // Arrange + _request.Peers = []; + var client = GetClient(); + + // Act & Assert + await Assert.ThrowsExactlyAsync(async () => + { + await client.CreateSecondaryZoneConfiguration(_request, TestContext.CancellationToken); + }); + } + + [TestMethod] + public async Task ShouldThrowArgumentExceptionForInvalidPeerId() + { + // Arrange + _request.Peers = ["invalid-peer-id"]; // invalid format + var client = GetClient(); + + // Act & Assert + await Assert.ThrowsExactlyAsync(async () => + { + await client.CreateSecondaryZoneConfiguration(_request, TestContext.CancellationToken); + }); + } + + private ICloudflareClient GetClient() + { + _clientMock = new Mock(); + _clientMock + .Setup(m => m.PostAsync( + It.IsAny(), + 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/DnsZoneTransfersExtensions/Incoming/DeleteSecondaryZoneConfigurationTest.cs b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/Incoming/DeleteSecondaryZoneConfigurationTest.cs new file mode 100644 index 0000000..e1c7394 --- /dev/null +++ b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/Incoming/DeleteSecondaryZoneConfigurationTest.cs @@ -0,0 +1,71 @@ +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.DnsZoneTransfersExtensions.Incoming +{ + [TestClass] + public class DeleteSecondaryZoneConfigurationTest + { + public TestContext TestContext { get; set; } + + private const string ZoneId = "023e105f4ecef8ad9ca31a8372d0c353"; + + private Mock _clientMock; + private CloudflareResponse _response; + private List<(string RequestPath, IQueryParameterFilter QueryFilter)> _callbacks; + + [TestInitialize] + public void Initialize() + { + _callbacks = new List<(string, IQueryParameterFilter)>(); + + _response = new CloudflareResponse + { + Success = true, + Messages = new List { new ResponseInfo(1000, "Message 1") }, + Errors = new List(), + Result = new Identifier { Id = "269d8f4853475ca241c4e730be286b20" } + }; + } + + [TestMethod] + public async Task ShouldDeleteSecondaryZoneConfiguration() + { + // Arrange + var client = GetClient(); + + // Act + var response = await client.DeleteSecondaryZoneConfiguration(ZoneId, TestContext.CancellationToken); + + // Assert + Assert.IsNotNull(response); + Assert.IsTrue(response.Success); + Assert.IsNotNull(response.Result); + Assert.AreEqual("269d8f4853475ca241c4e730be286b20", response.Result.Id); + + Assert.HasCount(1, _callbacks); + + var (requestPath, queryFilter) = _callbacks.First(); + Assert.AreEqual($"/zones/{ZoneId}/secondary_dns/incoming", requestPath); + Assert.IsNull(queryFilter); + + _clientMock.Verify(m => m.DeleteAsync($"/zones/{ZoneId}/secondary_dns/incoming", null, TestContext.CancellationToken), 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/DnsZoneTransfersExtensions/Incoming/SecondaryZoneConfigurationDetailsTest.cs b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/Incoming/SecondaryZoneConfigurationDetailsTest.cs new file mode 100644 index 0000000..d0f43f2 --- /dev/null +++ b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/Incoming/SecondaryZoneConfigurationDetailsTest.cs @@ -0,0 +1,78 @@ +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.DnsZoneTransfersExtensions.Incoming +{ + [TestClass] + public class SecondaryZoneConfigurationDetailsTest + { + public TestContext TestContext { get; set; } + + private const string ZoneId = "023e105f4ecef8ad9ca31a8372d0c353"; + private const string PeerId = "23ff594956f20c2a721606e94745a8aa"; + + 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 = [], + Result = new IncomingZoneConfiguration + { + Id = "269d8f4853475ca241c4e730be286b20", + Name = "www.example.com", + AutoRefreshSeconds = 86400, + Peers = [PeerId] + } + }; + } + + [TestMethod] + public async Task ShouldGetSecondaryZoneConfiguration() + { + // Arrange + var client = GetClient(); + + // Act + var response = await client.SecondaryZoneConfigurationDetails(ZoneId, TestContext.CancellationToken); + + // Assert + Assert.IsNotNull(response); + Assert.IsTrue(response.Success); + Assert.IsNotNull(response.Result); + Assert.AreEqual("269d8f4853475ca241c4e730be286b20", response.Result.Id); + + Assert.HasCount(1, _callbacks); + + var (requestPath, queryFilter) = _callbacks.First(); + Assert.AreEqual($"/zones/{ZoneId}/secondary_dns/incoming", requestPath); + Assert.IsNull(queryFilter); + + _clientMock.Verify(m => m.GetAsync($"/zones/{ZoneId}/secondary_dns/incoming", null, TestContext.CancellationToken), 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/DnsZoneTransfersExtensions/Incoming/UpdateSecondaryZoneConfigurationTest.cs b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/Incoming/UpdateSecondaryZoneConfigurationTest.cs new file mode 100644 index 0000000..b24a142 --- /dev/null +++ b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/Incoming/UpdateSecondaryZoneConfigurationTest.cs @@ -0,0 +1,155 @@ +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.DnsZoneTransfersExtensions.Incoming +{ + [TestClass] + public class UpdateSecondaryZoneConfigurationTest + { + public TestContext TestContext { get; set; } + + private const string ZoneId = "023e105f4ecef8ad9ca31a8372d0c353"; + private const string PeerId = "23ff594956f20c2a721606e94745a8aa"; + + private Mock _clientMock; + private CloudflareResponse _response; + private List<(string RequestPath, InternalSecondaryZoneConfigurationRequest Request)> _callbacks; + private SecondaryZoneConfigurationRequest _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 IncomingZoneConfiguration + { + Id = "269d8f4853475ca241c4e730be286b20", + Name = "www.example.com", + AutoRefreshSeconds = 86400, + Peers = [PeerId] + } + }; + + _request = new SecondaryZoneConfigurationRequest(ZoneId, "www.example.com") + { + AutoRefreshSeconds = 86400, + Peers = [PeerId] + }; + } + + [TestMethod] + public async Task ShouldUpdateSecondaryZoneConfiguration() + { + // Arrange + var client = GetClient(); + + // Act + var response = await client.UpdateSecondaryZoneConfiguration(_request, TestContext.CancellationToken); + + // Assert + Assert.IsNotNull(response); + Assert.IsTrue(response.Success); + Assert.AreEqual(_response.Result, response.Result); + + Assert.HasCount(1, _callbacks); + + var (requestPath, request) = _callbacks.First(); + Assert.AreEqual($"/zones/{ZoneId}/secondary_dns/incoming", requestPath); + Assert.IsNotNull(request); + + Assert.AreEqual(_request.Name, request.Name); + Assert.AreEqual(_request.AutoRefreshSeconds, request.AutoRefreshSeconds); + CollectionAssert.AreEqual(_request.Peers.ToList(), request.Peers.ToList()); + + _clientMock.Verify(m => m.PutAsync( + $"/zones/{ZoneId}/secondary_dns/incoming", + It.IsAny(), + TestContext.CancellationToken), Times.Once); + + _clientMock.VerifyNoOtherCalls(); + } + + [TestMethod] + [DataRow(null)] + [DataRow("")] + [DataRow(" ")] + public async Task ShouldThrowArgumentNullExceptionForName(string name) + { + // Arrange + _request.Name = name; + var client = GetClient(); + + // Act & Assert + await Assert.ThrowsExactlyAsync(async () => + { + await client.UpdateSecondaryZoneConfiguration(_request, TestContext.CancellationToken); + }); + } + + [TestMethod] + public async Task ShouldThrowArgumentOutOfRangeExceptionForAutoRefreshSeconds() + { + // Arrange + _request.AutoRefreshSeconds = -1; + var client = GetClient(); + + // Act & Assert + await Assert.ThrowsExactlyAsync(async () => + { + await client.UpdateSecondaryZoneConfiguration(_request, TestContext.CancellationToken); + }); + } + + [TestMethod] + public async Task ShouldThrowArgumentOutOfRangeExceptionForEmptyPeers() + { + // Arrange + _request.Peers = []; + var client = GetClient(); + + // Act & Assert + await Assert.ThrowsExactlyAsync(async () => + { + await client.UpdateSecondaryZoneConfiguration(_request, TestContext.CancellationToken); + }); + } + + [TestMethod] + public async Task ShouldThrowArgumentExceptionForInvalidPeerId() + { + // Arrange + _request.Peers = ["invalid-peer-id"]; // invalid format + var client = GetClient(); + + // Act & Assert + await Assert.ThrowsExactlyAsync(async () => + { + await client.UpdateSecondaryZoneConfiguration(_request, TestContext.CancellationToken); + }); + } + + 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/DnsZoneTransfersExtensions/Outgoing/CreatePrimaryZoneConfigurationTest.cs b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/Outgoing/CreatePrimaryZoneConfigurationTest.cs new file mode 100644 index 0000000..28eafe9 --- /dev/null +++ b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/Outgoing/CreatePrimaryZoneConfigurationTest.cs @@ -0,0 +1,140 @@ +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.DnsZoneTransfersExtensions.Outgoing +{ + [TestClass] + public class CreatePrimaryZoneConfigurationTest + { + public TestContext TestContext { get; set; } + + private const string ZoneId = "023e105f4ecef8ad9ca31a8372d0c353"; + private const string PeerId = "23ff594956f20c2a721606e94745a8aa"; + + private Mock _clientMock; + private CloudflareResponse _response; + private List<(string RequestPath, InternalPrimaryZoneConfigurationRequest Request)> _callbacks; + private PrimaryZoneConfigurationRequest _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 OutgoingZoneConfiguration + { + Id = "269d8f4853475ca241c4e730be286b20", + Name = "www.example.com", + Peers = [PeerId] + } + }; + + _request = new PrimaryZoneConfigurationRequest(ZoneId, "www.example.com") + { + Peers = [PeerId] + }; + } + + [TestMethod] + public async Task ShouldCreatePrimaryZoneConfiguration() + { + // Arrange + var client = GetClient(); + + // Act + var response = await client.CreatePrimaryZoneConfiguration(_request, TestContext.CancellationToken); + + // Assert + Assert.IsNotNull(response); + Assert.IsTrue(response.Success); + Assert.AreEqual(_response.Result, response.Result); + + Assert.HasCount(1, _callbacks); + + var (requestPath, request) = _callbacks.First(); + Assert.AreEqual($"/zones/{ZoneId}/secondary_dns/outgoing", requestPath); + Assert.IsNotNull(request); + + Assert.AreEqual(_request.Name, request.Name); + CollectionAssert.AreEqual(_request.Peers.ToList(), request.Peers.ToList()); + + _clientMock.Verify(m => m.PostAsync( + $"/zones/{ZoneId}/secondary_dns/outgoing", + It.IsAny(), + null, + TestContext.CancellationToken), Times.Once); + + _clientMock.VerifyNoOtherCalls(); + } + + [TestMethod] + [DataRow(null)] + [DataRow("")] + [DataRow(" ")] + public async Task ShouldThrowArgumentNullExceptionForName(string name) + { + // Arrange + _request.Name = name; + var client = GetClient(); + + // Act & Assert + await Assert.ThrowsExactlyAsync(async () => + { + await client.CreatePrimaryZoneConfiguration(_request, TestContext.CancellationToken); + }); + } + + [TestMethod] + public async Task ShouldThrowArgumentOutOfRangeExceptionForEmptyPeers() + { + // Arrange + _request.Peers = []; + var client = GetClient(); + + // Act & Assert + await Assert.ThrowsExactlyAsync(async () => + { + await client.CreatePrimaryZoneConfiguration(_request, TestContext.CancellationToken); + }); + } + + [TestMethod] + public async Task ShouldThrowArgumentExceptionForInvalidPeerId() + { + // Arrange + _request.Peers = ["invalid-peer-id"]; // invalid format + var client = GetClient(); + + // Act & Assert + await Assert.ThrowsExactlyAsync(async () => + { + await client.CreatePrimaryZoneConfiguration(_request, TestContext.CancellationToken); + }); + } + + private ICloudflareClient GetClient() + { + _clientMock = new Mock(); + _clientMock + .Setup(m => m.PostAsync( + It.IsAny(), + 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/DnsZoneTransfersExtensions/Outgoing/DeletePrimaryZoneConfigurationTest.cs b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/Outgoing/DeletePrimaryZoneConfigurationTest.cs new file mode 100644 index 0000000..f42ae8f --- /dev/null +++ b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/Outgoing/DeletePrimaryZoneConfigurationTest.cs @@ -0,0 +1,74 @@ +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.DnsZoneTransfersExtensions.Outgoing +{ + [TestClass] + public class DeletePrimaryZoneConfigurationTest + { + public TestContext TestContext { get; set; } + + private const string ZoneId = "023e105f4ecef8ad9ca31a8372d0c353"; + + private Mock _clientMock; + private CloudflareResponse _response; + private List<(string RequestPath, IQueryParameterFilter QueryFilter)> _callbacks; + + [TestInitialize] + public void Initialize() + { + _callbacks = []; + + _response = new CloudflareResponse + { + Success = true, + Messages = new List { new(1000, "Message 1") }, + Errors = [], + Result = new Identifier + { + Id = "269d8f4853475ca241c4e730be286b20" + } + }; + } + + [TestMethod] + public async Task ShouldDeletePrimaryZoneConfiguration() + { + // Arrange + var client = GetClient(); + + // Act + var response = await client.DeletePrimaryZoneConfiguration(ZoneId, TestContext.CancellationToken); + + // Assert + Assert.IsNotNull(response); + Assert.IsTrue(response.Success); + Assert.IsNotNull(response.Result); + Assert.AreEqual("269d8f4853475ca241c4e730be286b20", response.Result.Id); + + Assert.HasCount(1, _callbacks); + + var (requestPath, queryFilter) = _callbacks.First(); + Assert.AreEqual($"/zones/{ZoneId}/secondary_dns/outgoing", requestPath); + Assert.IsNull(queryFilter); + + _clientMock.Verify(m => m.DeleteAsync($"/zones/{ZoneId}/secondary_dns/outgoing", null, TestContext.CancellationToken), 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/DnsZoneTransfersExtensions/Outgoing/DisableOutgoingZoneTransfersTest.cs b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/Outgoing/DisableOutgoingZoneTransfersTest.cs new file mode 100644 index 0000000..ad70ec1 --- /dev/null +++ b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/Outgoing/DisableOutgoingZoneTransfersTest.cs @@ -0,0 +1,78 @@ +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.DnsZoneTransfersExtensions.Outgoing +{ + [TestClass] + public class DisableOutgoingZoneTransfersTest + { + public TestContext TestContext { get; set; } + + private const string ZoneId = "023e105f4ecef8ad9ca31a8372d0c353"; + + private Mock _clientMock; + private CloudflareResponse _response; + private List<(string RequestPath, object Request)> _callbacks; + + [TestInitialize] + public void Initialize() + { + _callbacks = []; + + _response = new CloudflareResponse + { + Success = true, + Messages = [ + new ResponseInfo(1000, "Message 1") + ], + Errors = [ + new ResponseInfo(1000, "Error 1") + ], + Result = "Disabled" + }; + } + + [TestMethod] + public async Task ShouldDisableOutgoingZoneTransfers() + { + // Arrange + var client = GetClient(); + + // Act + var response = await client.DisableOutgoingZoneTransfers(ZoneId, TestContext.CancellationToken); + + // Assert + Assert.IsNotNull(response); + Assert.IsTrue(response.Success); + Assert.AreEqual(_response.Result, response.Result); + + Assert.HasCount(1, _callbacks); + + var (requestPath, request) = _callbacks.First(); + Assert.AreEqual($"/zones/{ZoneId}/secondary_dns/outgoing/disable", requestPath); + Assert.IsNull(request); + + _clientMock.Verify(m => m.PostAsync( + $"/zones/{ZoneId}/secondary_dns/outgoing/disable", + null, + null, + TestContext.CancellationToken), 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, _, _) => _callbacks.Add((requestPath, request))) + .ReturnsAsync(() => _response); + + return _clientMock.Object; + } + } +} diff --git a/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/Outgoing/EnableOutgoingZoneTransfersTest.cs b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/Outgoing/EnableOutgoingZoneTransfersTest.cs new file mode 100644 index 0000000..b2706fa --- /dev/null +++ b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/Outgoing/EnableOutgoingZoneTransfersTest.cs @@ -0,0 +1,78 @@ +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.DnsZoneTransfersExtensions.Outgoing +{ + [TestClass] + public class EnableOutgoingZoneTransfersTest + { + public TestContext TestContext { get; set; } + + private const string ZoneId = "023e105f4ecef8ad9ca31a8372d0c353"; + + private Mock _clientMock; + private CloudflareResponse _response; + private List<(string RequestPath, object Request)> _callbacks; + + [TestInitialize] + public void Initialize() + { + _callbacks = []; + + _response = new CloudflareResponse + { + Success = true, + Messages = [ + new ResponseInfo(1000, "Message 1") + ], + Errors = [ + new ResponseInfo(1000, "Error 1") + ], + Result = "Enabled" + }; + } + + [TestMethod] + public async Task ShouldDisableOutgoingZoneTransfers() + { + // Arrange + var client = GetClient(); + + // Act + var response = await client.EnableOutgoingZoneTransfers(ZoneId, TestContext.CancellationToken); + + // Assert + Assert.IsNotNull(response); + Assert.IsTrue(response.Success); + Assert.AreEqual(_response.Result, response.Result); + + Assert.HasCount(1, _callbacks); + + var (requestPath, request) = _callbacks.First(); + Assert.AreEqual($"/zones/{ZoneId}/secondary_dns/outgoing/enable", requestPath); + Assert.IsNull(request); + + _clientMock.Verify(m => m.PostAsync( + $"/zones/{ZoneId}/secondary_dns/outgoing/enable", + null, + null, + TestContext.CancellationToken), 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, _, _) => _callbacks.Add((requestPath, request))) + .ReturnsAsync(() => _response); + + return _clientMock.Object; + } + } +} diff --git a/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/Outgoing/ForceDNSNotifyTest.cs b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/Outgoing/ForceDNSNotifyTest.cs new file mode 100644 index 0000000..273e0a6 --- /dev/null +++ b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/Outgoing/ForceDNSNotifyTest.cs @@ -0,0 +1,75 @@ +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.DnsZoneTransfersExtensions.Outgoing +{ + [TestClass] + public class ForceDNSNotifyTest + { + public TestContext TestContext { get; set; } + + private const string ZoneId = "023e105f4ecef8ad9ca31a8372d0c353"; + + private Mock _clientMock; + private CloudflareResponse _response; + private List<(string RequestPath, object Request)> _callbacks; + + [TestInitialize] + public void Initialize() + { + _callbacks = []; + + _response = new CloudflareResponse + { + Success = true, + Messages = [new ResponseInfo(1000, "Message 1")], + Errors = [new ResponseInfo(1000, "Error 1")], + Result = "OK" + }; + } + + [TestMethod] + public async Task ShouldForceDNSNotify() + { + // Arrange + var client = GetClient(); + + // Act + var response = await client.ForceDNSNotify(ZoneId, TestContext.CancellationToken); + + // Assert + Assert.IsNotNull(response); + Assert.IsTrue(response.Success); + Assert.AreEqual(_response.Result, response.Result); + + Assert.HasCount(1, _callbacks); + + var (requestPath, request) = _callbacks.First(); + Assert.AreEqual($"/zones/{ZoneId}/secondary_dns/outgoing/force_notify", requestPath); + Assert.IsNull(request); + + _clientMock.Verify(m => m.PostAsync( + $"/zones/{ZoneId}/secondary_dns/outgoing/force_notify", + null, + null, + TestContext.CancellationToken), 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, _, _) => _callbacks.Add((requestPath, request))) + .ReturnsAsync(() => _response); + + return _clientMock.Object; + } + } +} diff --git a/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/Outgoing/GetOutgoingZoneTransferStatusTest.cs b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/Outgoing/GetOutgoingZoneTransferStatusTest.cs new file mode 100644 index 0000000..2922c77 --- /dev/null +++ b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/Outgoing/GetOutgoingZoneTransferStatusTest.cs @@ -0,0 +1,70 @@ +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.DnsZoneTransfersExtensions.Outgoing +{ + [TestClass] + public class GetOutgoingZoneTransferStatusTest + { + public TestContext TestContext { get; set; } + + private const string ZoneId = "023e105f4ecef8ad9ca31a8372d0c353"; + + private Mock _clientMock; + private CloudflareResponse _response; + private List<(string RequestPath, IQueryParameterFilter QueryFilter)> _callbacks; + + [TestInitialize] + public void Initialize() + { + _callbacks = []; + + _response = new CloudflareResponse + { + Success = true, + Messages = [new ResponseInfo(1000, "Message 1")], + Errors = [], + Result = "Enabled" + }; + } + + [TestMethod] + public async Task ShouldGetOutgoingZoneTransferStatus() + { + // Arrange + var client = GetClient(); + + // Act + var response = await client.GetOutgoingZoneTransferStatus(ZoneId, TestContext.CancellationToken); + + // Assert + Assert.IsNotNull(response); + Assert.IsTrue(response.Success); + Assert.AreEqual(_response.Result, response.Result); + + Assert.HasCount(1, _callbacks); + + var (requestPath, queryFilter) = _callbacks.First(); + Assert.AreEqual($"/zones/{ZoneId}/secondary_dns/outgoing/status", requestPath); + Assert.IsNull(queryFilter); + + _clientMock.Verify(m => m.GetAsync($"/zones/{ZoneId}/secondary_dns/outgoing/status", null, TestContext.CancellationToken), 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/DnsZoneTransfersExtensions/Outgoing/PrimaryZoneConfigurationDetailsTest.cs b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/Outgoing/PrimaryZoneConfigurationDetailsTest.cs new file mode 100644 index 0000000..97e24e5 --- /dev/null +++ b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/Outgoing/PrimaryZoneConfigurationDetailsTest.cs @@ -0,0 +1,78 @@ +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.DnsZoneTransfersExtensions.Outgoing +{ + [TestClass] + public class PrimaryZoneConfigurationDetailsTest + { + public TestContext TestContext { get; set; } + + private const string ZoneId = "023e105f4ecef8ad9ca31a8372d0c353"; + private const string PeerId = "23ff594956f20c2a721606e94745a8aa"; + + 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 = [], + Result = new OutgoingZoneConfiguration + { + Id = "269d8f4853475ca241c4e730be286b20", + Name = "www.example.com", + Peers = [PeerId], + SoaSerial = 12345 + } + }; + } + + [TestMethod] + public async Task ShouldGetPrimaryZoneConfiguration() + { + // Arrange + var client = GetClient(); + + // Act + var response = await client.PrimaryZoneConfigurationDetails(ZoneId, TestContext.CancellationToken); + + // Assert + Assert.IsNotNull(response); + Assert.IsTrue(response.Success); + Assert.IsNotNull(response.Result); + Assert.AreEqual("269d8f4853475ca241c4e730be286b20", response.Result.Id); + + Assert.HasCount(1, _callbacks); + + var (requestPath, queryFilter) = _callbacks.First(); + Assert.AreEqual($"/zones/{ZoneId}/secondary_dns/outgoing", requestPath); + Assert.IsNull(queryFilter); + + _clientMock.Verify(m => m.GetAsync($"/zones/{ZoneId}/secondary_dns/outgoing", null, TestContext.CancellationToken), 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/DnsZoneTransfersExtensions/Outgoing/UpdatePrimaryZoneConfigurationTest.cs b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/Outgoing/UpdatePrimaryZoneConfigurationTest.cs new file mode 100644 index 0000000..0c91342 --- /dev/null +++ b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/Outgoing/UpdatePrimaryZoneConfigurationTest.cs @@ -0,0 +1,126 @@ +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.DnsZoneTransfersExtensions.Outgoing +{ + [TestClass] + public class UpdatePrimaryZoneConfigurationTest + { + public TestContext TestContext { get; set; } + + private const string ZoneId = "023e105f4ecef8ad9ca31a8372d0c353"; + private const string PeerId = "23ff594956f20c2a721606e94745a8aa"; + + private Mock _clientMock; + private CloudflareResponse _response; + private List<(string RequestPath, InternalPrimaryZoneConfigurationRequest Request)> _callbacks; + private PrimaryZoneConfigurationRequest _request; + + [TestInitialize] + public void Initialize() + { + _callbacks = []; + + _response = new CloudflareResponse + { + Success = true, + Messages = [new ResponseInfo(1000, "Message 1")], + Errors = [], + Result = new OutgoingZoneConfiguration + { + Id = "269d8f4853475ca241c4e730be286b20", + Name = "www.example.com", + Peers = [PeerId], + SoaSerial = 12345 + } + }; + + _request = new PrimaryZoneConfigurationRequest(ZoneId, "www.example.com") + { + Peers = [PeerId] + }; + } + + [TestMethod] + public async Task ShouldUpdatePrimaryZoneConfiguration() + { + // Arrange + var client = GetClient(); + + // Act + var response = await client.UpdatePrimaryZoneConfiguration(_request, TestContext.CancellationToken); + + // Assert + Assert.IsNotNull(response); + Assert.IsTrue(response.Success); + Assert.IsNotNull(response.Result); + Assert.AreEqual(_response.Result, response.Result); + + Assert.HasCount(1, _callbacks); + + var (requestPath, req) = _callbacks.First(); + Assert.AreEqual($"/zones/{ZoneId}/secondary_dns/outgoing", requestPath); + Assert.IsNotNull(req); + + Assert.AreEqual(_request.Name, req.Name); + CollectionAssert.AreEqual(_request.Peers.ToList(), req.Peers.ToList()); + + _clientMock.Verify(m => m.PutAsync( + $"/zones/{ZoneId}/secondary_dns/outgoing", + It.IsAny(), + TestContext.CancellationToken), Times.Once); + + _clientMock.VerifyNoOtherCalls(); + } + + [TestMethod] + [DataRow(null)] + [DataRow("")] + [DataRow(" ")] + public async Task ShouldThrowArgumentNullExceptionForName(string name) + { + // Arrange + _request.Name = name; + var client = GetClient(); + + // Act & Assert + await Assert.ThrowsExactlyAsync(async () => + { + await client.UpdatePrimaryZoneConfiguration(_request, TestContext.CancellationToken); + }); + } + + [TestMethod] + public async Task ShouldThrowArgumentOutOfRangeExceptionForEmptyPeers() + { + // Arrange + _request.Peers = []; + var client = GetClient(); + + // Act & Assert + await Assert.ThrowsExactlyAsync(async () => + { + await client.UpdatePrimaryZoneConfiguration(_request, TestContext.CancellationToken); + }); + } + + 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/DnsZoneTransfersExtensions/Peers/CreatePeerTest.cs b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/Peers/CreatePeerTest.cs new file mode 100644 index 0000000..3142208 --- /dev/null +++ b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/Peers/CreatePeerTest.cs @@ -0,0 +1,104 @@ +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.DnsZoneTransfersExtensions.Peers +{ + [TestClass] + public class CreatePeerTest + { + public TestContext TestContext { get; set; } + + private const string AccountId = "023e105f4ecef8ad9ca31a8372d0c353"; + + private Mock _clientMock; + private CloudflareResponse _response; + private List<(string RequestPath, InternalCreatePeerRequest Request)> _callbacks; + private CreatePeerRequest _request; + + [TestInitialize] + public void Initialize() + { + _callbacks = new List<(string, InternalCreatePeerRequest)>(); + + _response = new CloudflareResponse + { + Success = true, + Messages = new List { new ResponseInfo(1000, "Message 1") }, + Errors = new List(), + Result = new Peer("023e105f4ecef8ad9ca31a8372d0c351", "peer-a") + { + IpAddress = "192.0.2.1", + IXFREnabled = true, + Port = 5353, + TSIGId = "tsig-1" + } + }; + + _request = new CreatePeerRequest(AccountId, "peer-a"); + } + + [TestMethod] + public async Task ShouldCreatePeer() + { + // Arrange + var client = GetClient(); + + // Act + var response = await client.CreatePeer(_request, TestContext.CancellationToken); + + // Assert + Assert.IsNotNull(response); + Assert.IsTrue(response.Success); + Assert.IsNotNull(response.Result); + Assert.AreEqual(_response.Result, response.Result); + + Assert.HasCount(1, _callbacks); + + var (requestPath, req) = _callbacks.First(); + Assert.AreEqual($"/accounts/{AccountId}/secondary_dns/peers", requestPath); + Assert.IsNotNull(req); + Assert.AreEqual(_request.Name, req.Name); + + _clientMock.Verify(m => m.PostAsync( + $"/accounts/{AccountId}/secondary_dns/peers", + It.IsAny(), + null, + TestContext.CancellationToken), Times.Once); + + _clientMock.VerifyNoOtherCalls(); + } + + [TestMethod] + [DataRow(null)] + [DataRow("")] + [DataRow(" ")] + public async Task ShouldThrowArgumentNullExceptionForName(string name) + { + // Arrange + _request.Name = name; + var client = GetClient(); + + // Act & Assert + await Assert.ThrowsExactlyAsync(async () => + { + await client.CreatePeer(_request, TestContext.CancellationToken); + }); + } + + private ICloudflareClient GetClient() + { + _clientMock = new Mock(); + _clientMock + .Setup(m => m.PostAsync(It.IsAny(), 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/DnsZoneTransfersExtensions/Peers/DeletePeerTest.cs b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/Peers/DeletePeerTest.cs new file mode 100644 index 0000000..0d32311 --- /dev/null +++ b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/Peers/DeletePeerTest.cs @@ -0,0 +1,75 @@ +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.DnsZoneTransfersExtensions.Peers +{ + [TestClass] + public class DeletePeerTest + { + public TestContext TestContext { get; set; } + + private const string AccountId = "023e105f4ecef8ad9ca31a8372d0c353"; + private const string PeerId = "23ff594956f20c2a721606e94745a8aa"; + + 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 = [], + Result = new Identifier + { + Id = PeerId + } + }; + } + + [TestMethod] + public async Task ShouldDeletePeer() + { + // Arrange + var client = GetClient(); + + // Act + var response = await client.DeletePeer(AccountId, PeerId, TestContext.CancellationToken); + + // Assert + Assert.IsNotNull(response); + Assert.IsTrue(response.Success); + Assert.IsNotNull(response.Result); + Assert.AreEqual(PeerId, response.Result.Id); + + Assert.HasCount(1, _callbacks); + + var (requestPath, queryFilter) = _callbacks.First(); + Assert.AreEqual($"/accounts/{AccountId}/secondary_dns/peers/{PeerId}", requestPath); + Assert.IsNull(queryFilter); + + _clientMock.Verify(m => m.DeleteAsync($"/accounts/{AccountId}/secondary_dns/peers/{PeerId}", null, TestContext.CancellationToken), 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/DnsZoneTransfersExtensions/Peers/ListPeersTest.cs b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/Peers/ListPeersTest.cs new file mode 100644 index 0000000..ad28839 --- /dev/null +++ b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/Peers/ListPeersTest.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 Moq; + +namespace Cloudflare.Dns.Tests.DnsZoneTransfersExtensions.Peers +{ + [TestClass] + public class ListPeersTest + { + public TestContext TestContext { get; set; } + + private const string AccountId = "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 = [], + Result = + [ + new Peer("023e105f4ecef8ad9ca31a8372d0c351", "peer-a") + { + IpAddress = "192.0.2.1", + IXFREnabled = true, + Port = 5353, + TSIGId = "tsig-1" + }, + new Peer("023e105f4ecef8ad9ca31a8372d0c352", "peer-b") + { + IpAddress = "2001:db8::1", + IXFREnabled = false, + Port = 53, + TSIGId = null + } + ] + }; + } + + [TestMethod] + public async Task ShouldListPeers() + { + // Arrange + var client = GetClient(); + + // Act + var response = await client.ListPeers(AccountId, TestContext.CancellationToken); + + // Assert + Assert.IsNotNull(response); + Assert.IsTrue(response.Success); + Assert.IsNotNull(response.Result); + Assert.HasCount(2, response.Result); + + var peers = response.Result.ToList(); + Assert.AreEqual("peer-a", peers[0].Name); + Assert.AreEqual("192.0.2.1", peers[0].IpAddress); + Assert.IsTrue(peers[0].IXFREnabled ?? false); + Assert.AreEqual(5353, peers[0].Port); + Assert.AreEqual("tsig-1", peers[0].TSIGId); + + Assert.AreEqual("peer-b", peers[1].Name); + Assert.AreEqual("2001:db8::1", peers[1].IpAddress); + Assert.IsFalse(peers[1].IXFREnabled ?? false); + Assert.AreEqual(53, peers[1].Port); + Assert.IsNull(peers[1].TSIGId); + + Assert.HasCount(1, _callbacks); + var (requestPath, queryFilter) = _callbacks.First(); + Assert.AreEqual($"/accounts/{AccountId}/secondary_dns/peers", requestPath); + Assert.IsNull(queryFilter); + + _clientMock.Verify(m => m.GetAsync>($"/accounts/{AccountId}/secondary_dns/peers", null, TestContext.CancellationToken), 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/DnsZoneTransfersExtensions/Peers/PeerDetailsTest.cs b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/Peers/PeerDetailsTest.cs new file mode 100644 index 0000000..b23760d --- /dev/null +++ b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/Peers/PeerDetailsTest.cs @@ -0,0 +1,82 @@ +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.DnsZoneTransfersExtensions.Peers +{ + [TestClass] + public class PeerDetailsTest + { + public TestContext TestContext { get; set; } + + private const string AccountId = "023e105f4ecef8ad9ca31a8372d0c353"; + private const string PeerId = "23ff594956f20c2a721606e94745a8aa"; + + 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 = [], + Result = new Peer(PeerId, "peer-a") + { + IpAddress = "192.0.2.1", + IXFREnabled = true, + Port = 5353, + TSIGId = "tsig-1" + } + }; + } + + [TestMethod] + public async Task ShouldGetPeerDetails() + { + // Arrange + var client = GetClient(); + + // Act + var response = await client.PeerDetails(AccountId, PeerId, TestContext.CancellationToken); + + // Assert + Assert.IsNotNull(response); + Assert.IsTrue(response.Success); + Assert.IsNotNull(response.Result); + Assert.AreEqual(PeerId, response.Result.Id); + Assert.AreEqual("peer-a", response.Result.Name); + Assert.AreEqual("192.0.2.1", response.Result.IpAddress); + Assert.IsTrue(response.Result.IXFREnabled ?? false); + Assert.AreEqual(5353, response.Result.Port); + Assert.AreEqual("tsig-1", response.Result.TSIGId); + + Assert.HasCount(1, _callbacks); + var (requestPath, queryFilter) = _callbacks.First(); + Assert.AreEqual($"/accounts/{AccountId}/secondary_dns/peers/{PeerId}", requestPath); + Assert.IsNull(queryFilter); + + _clientMock.Verify(m => m.GetAsync($"/accounts/{AccountId}/secondary_dns/peers/{PeerId}", null, TestContext.CancellationToken), 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/DnsZoneTransfersExtensions/Peers/UpdatePeerTest.cs b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/Peers/UpdatePeerTest.cs new file mode 100644 index 0000000..a79d169 --- /dev/null +++ b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/Peers/UpdatePeerTest.cs @@ -0,0 +1,136 @@ +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.DnsZoneTransfersExtensions.Peers +{ + [TestClass] + public class UpdatePeerTest + { + public TestContext TestContext { get; set; } + + private const string AccountId = "023e105f4ecef8ad9ca31a8372d0c353"; + private const string PeerId = "23ff594956f20c2a721606e94745a8aa"; + + private Mock _clientMock; + private CloudflareResponse _response; + private List<(string RequestPath, InternalUpdatePeerRequest Request)> _callbacks; + private UpdatePeerRequest _request; + + [TestInitialize] + public void Initialize() + { + _callbacks = []; + + _response = new CloudflareResponse + { + Success = true, + Messages = [new ResponseInfo(1000, "Message 1")], + Errors = [], + Result = new Peer(PeerId, "peer-a") + { + IpAddress = "192.0.2.1", + IXFREnabled = true, + Port = 5353, + TSIGId = "tsig-1" + } + }; + + _request = new UpdatePeerRequest(AccountId, PeerId, "peer-a") + { + IpAddress = "192.0.2.1", + IXFREnable = true, + Port = 5353, + TSIGId = "tsig-1" + }; + } + + [TestMethod] + public async Task ShouldUpdatePeer() + { + // Arrange + var client = GetClient(); + + // Act + var response = await client.UpdatePeer(_request, TestContext.CancellationToken); + + // Assert + Assert.IsNotNull(response); + Assert.IsTrue(response.Success); + Assert.IsNotNull(response.Result); + Assert.AreEqual(_response.Result, response.Result); + + Assert.HasCount(1, _callbacks); + var (requestPath, req) = _callbacks.First(); + Assert.AreEqual($"/accounts/{AccountId}/secondary_dns/peers/{PeerId}", requestPath); + + Assert.IsNotNull(req); + Assert.AreEqual(_request.Name, req.Name); + Assert.AreEqual(_request.IpAddress, req.Ip); + Assert.AreEqual(_request.IXFREnable, req.IxfrEnable); + Assert.AreEqual(_request.Port, req.Port); + Assert.AreEqual(_request.TSIGId, req.TSigId); + + _clientMock.Verify(m => m.PutAsync( + $"/accounts/{AccountId}/secondary_dns/peers/{PeerId}", + It.IsAny(), + TestContext.CancellationToken), Times.Once); + + _clientMock.VerifyNoOtherCalls(); + } + + [TestMethod] + [DataRow(null)] + [DataRow("")] + [DataRow(" ")] + public async Task ShouldThrowArgumentNullExceptionForName(string name) + { + _request.Name = name; + var client = GetClient(); + + await Assert.ThrowsExactlyAsync(async () => + { + await client.UpdatePeer(_request, TestContext.CancellationToken); + }); + } + + [TestMethod] + public async Task ShouldThrowArgumentOutOfRangeExceptionForPortLessThanZero() + { + _request.Port = -1; + var client = GetClient(); + + await Assert.ThrowsExactlyAsync(async () => + { + await client.UpdatePeer(_request, TestContext.CancellationToken); + }); + } + + [TestMethod] + public async Task ShouldThrowArgumentOutOfRangeExceptionForPortGreaterThan65535() + { + _request.Port = 70000; + var client = GetClient(); + + await Assert.ThrowsExactlyAsync(async () => + { + await client.UpdatePeer(_request, TestContext.CancellationToken); + }); + } + + 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/DnsZoneTransfersExtensions/TSIGs/CreateTSIGTest.cs b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/TSIGs/CreateTSIGTest.cs new file mode 100644 index 0000000..5070eb2 --- /dev/null +++ b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/TSIGs/CreateTSIGTest.cs @@ -0,0 +1,143 @@ +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.DnsZoneTransfersExtensions.TSIGs +{ + [TestClass] + public class CreateTSIGTest + { + public TestContext TestContext { get; set; } + + private const string AccountId = "023e105f4ecef8ad9ca31a8372d0c353"; + + private Mock _clientMock; + private CloudflareResponse _response; + private List<(string RequestPath, InternalTSIGRequest Request)> _callbacks; + private CreateTSIGRequest _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 TSIG + { + Id = "tsig-1", + Name = "tsig-key-a", + Secret = "very-secret", + Algorithm = TSigAlgorithm.HMAC_SHA256 + } + }; + + _request = new CreateTSIGRequest( + accountId: AccountId, + name: "tsig-key-a", + secret: "very-secret" + ) + { + Algorithm = TSigAlgorithm.HMAC_SHA512 + }; + } + + [TestMethod] + public async Task ShouldCreateTsig() + { + // Arrange + var client = GetClient(); + + // Act + var response = await client.CreateTSIG(_request, TestContext.CancellationToken); + + // Assert + Assert.IsNotNull(response); + Assert.IsTrue(response.Success); + Assert.AreEqual(_response.Result, response.Result); + + Assert.HasCount(1, _callbacks); + + var (requestPath, request) = _callbacks.First(); + Assert.AreEqual($"/accounts/{AccountId}/secondary_dns/tsigs", requestPath); + + Assert.IsNotNull(request); + Assert.AreEqual(_request.Name, request.Name); + Assert.AreEqual(_request.Secret, request.Secret); + Assert.AreEqual(_request.Algorithm, request.Algorithm); + + _clientMock.Verify(m => m.PostAsync($"/accounts/{AccountId}/secondary_dns/tsigs", It.IsAny(), null, TestContext.CancellationToken), Times.Once); + _clientMock.VerifyNoOtherCalls(); + } + + [TestMethod] + [DataRow(null)] + [DataRow("")] + [DataRow(" ")] + public async Task ShouldThrowArgumentNullExceptionForName(string name) + { + // Arrange + _request.Name = name; + var client = GetClient(); + + // Act & Assert + await Assert.ThrowsExactlyAsync(async () => + { + await client.CreateTSIG(_request, TestContext.CancellationToken); + }); + } + + [TestMethod] + public async Task ShouldThrowArgumentOutOfRangeExceptionForAlgorithm() + { + // Arrange + _request.Algorithm = 0; + var client = GetClient(); + + // Act & Assert + await Assert.ThrowsExactlyAsync(async () => + { + await client.CreateTSIG(_request, TestContext.CancellationToken); + }); + } + + [TestMethod] + [DataRow(null)] + [DataRow("")] + [DataRow(" ")] + public async Task ShouldThrowArgumentNullExceptionForSecret(string secret) + { + // Arrange + _request.Secret = secret; + var client = GetClient(); + + // Act & Assert + await Assert.ThrowsExactlyAsync(async () => + { + await client.CreateTSIG(_request, TestContext.CancellationToken); + }); + } + + private ICloudflareClient GetClient() + { + _clientMock = new Mock(); + _clientMock + .Setup(m => m.PostAsync(It.IsAny(), 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/DnsZoneTransfersExtensions/TSIGs/DeleteTSIGTest.cs b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/TSIGs/DeleteTSIGTest.cs new file mode 100644 index 0000000..ac13889 --- /dev/null +++ b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/TSIGs/DeleteTSIGTest.cs @@ -0,0 +1,81 @@ +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.DnsZoneTransfersExtensions.TSIGs +{ + [TestClass] + public class DeleteTSIGTest + { + public TestContext TestContext { get; set; } + + private const string AccountId = "023e105f4ecef8ad9ca31a8372d0c353"; + private const string TsigId = "023e105f4ecef8ad9ca31a8372d0c351"; + + 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 = TsigId + } + }; + } + + [TestMethod] + public async Task ShouldDeleteTSIG() + { + // Arrange + var client = GetClient(); + + // Act + var response = await client.DeleteTSIG(AccountId, TsigId, TestContext.CancellationToken); + + // Assert + Assert.IsNotNull(response); + Assert.IsTrue(response.Success); + Assert.IsNotNull(response.Result); + Assert.AreEqual(_response.Result, response.Result); + + Assert.HasCount(1, _callbacks); + + var (requestPath, queryFilter) = _callbacks.First(); + Assert.AreEqual($"/accounts/{AccountId}/secondary_dns/tsigs/{TsigId}", requestPath); + Assert.IsNull(queryFilter); + + _clientMock.Verify(m => m.DeleteAsync($"/accounts/{AccountId}/secondary_dns/tsigs/{TsigId}", null, TestContext.CancellationToken), 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/DnsZoneTransfersExtensions/TSIGs/ListTSIGsTest.cs b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/TSIGs/ListTSIGsTest.cs new file mode 100644 index 0000000..ee76800 --- /dev/null +++ b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/TSIGs/ListTSIGsTest.cs @@ -0,0 +1,95 @@ +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.DnsZoneTransfersExtensions.TSIGs +{ + [TestClass] + public class ListTSIGsTest + { + public TestContext TestContext { get; set; } + + private const string AccountId = "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 = [], + Result = + [ + new TSIG + { + Id = "tsig-1", + Name = "tsig-key-a", + Secret = "secretA", + Algorithm = TSigAlgorithm.HMAC_SHA256 + }, + new TSIG + { + Id = "tsig-2", + Name = "tsig-key-b", + Secret = "secretB", + Algorithm = TSigAlgorithm.HMAC_SHA1 + } + ] + }; + } + + [TestMethod] + public async Task ShouldListTSIGs() + { + // Arrange + var client = GetClient(); + + // Act + var response = await client.ListTSIGs(AccountId, TestContext.CancellationToken); + + // Assert + Assert.IsNotNull(response); + Assert.IsTrue(response.Success); + Assert.IsNotNull(response.Result); + Assert.HasCount(2, response.Result); + + var tsigs = response.Result.ToList(); + Assert.AreEqual("tsig-key-a", tsigs[0].Name); + Assert.AreEqual("secretA", tsigs[0].Secret); + Assert.AreEqual(TSigAlgorithm.HMAC_SHA256, tsigs[0].Algorithm); + + Assert.AreEqual("tsig-key-b", tsigs[1].Name); + Assert.AreEqual("secretB", tsigs[1].Secret); + Assert.AreEqual(TSigAlgorithm.HMAC_SHA1, tsigs[1].Algorithm); + + Assert.HasCount(1, _callbacks); + var (requestPath, queryFilter) = _callbacks.First(); + Assert.AreEqual($"/accounts/{AccountId}/secondary_dns/tsigs", requestPath); + Assert.IsNull(queryFilter); + + _clientMock.Verify(m => m.GetAsync>($"/accounts/{AccountId}/secondary_dns/tsigs", null, TestContext.CancellationToken), 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/DnsZoneTransfersExtensions/TSIGs/TSIGDetailsTest.cs b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/TSIGs/TSIGDetailsTest.cs new file mode 100644 index 0000000..b49c4d5 --- /dev/null +++ b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/TSIGs/TSIGDetailsTest.cs @@ -0,0 +1,78 @@ +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.DnsZoneTransfersExtensions.TSIGs +{ + [TestClass] + public class TSIGDetailsTest + { + public TestContext TestContext { get; set; } + + private const string AccountId = "023e105f4ecef8ad9ca31a8372d0c353"; + private const string TsigId = "023e105f4ecef8ad9ca31a8372d0c351"; + + private Mock _clientMock; + private CloudflareResponse _response; + private List<(string RequestPath, IQueryParameterFilter QueryFilter)> _callbacks; + + [TestInitialize] + public void Initialize() + { + _callbacks = new List<(string, IQueryParameterFilter)>(); + + _response = new CloudflareResponse + { + Success = true, + Messages = [new ResponseInfo(1000, "Message 1")], + Errors = [new ResponseInfo(1000, "Error 1")], + Result = new TSIG + { + Id = TsigId, + Name = "tsig-key-a", + Secret = "very-secret", + Algorithm = TSigAlgorithm.HMAC_SHA256 + } + }; + } + + [TestMethod] + public async Task ShouldGetTSIGDetails() + { + // Arrange + var client = GetClient(); + + // Act + var response = await client.TSIGDetails(AccountId, TsigId, TestContext.CancellationToken); + + // Assert + Assert.IsNotNull(response); + Assert.IsTrue(response.Success); + Assert.IsNotNull(response.Result); + Assert.AreEqual(_response.Result, response.Result); + + Assert.HasCount(1, _callbacks); + + var (requestPath, queryFilter) = _callbacks.First(); + Assert.AreEqual($"/accounts/{AccountId}/secondary_dns/tsigs/{TsigId}", requestPath); + Assert.IsNull(queryFilter); + + _clientMock.Verify(m => m.GetAsync($"/accounts/{AccountId}/secondary_dns/tsigs/{TsigId}", null, TestContext.CancellationToken), 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/DnsZoneTransfersExtensions/TSIGs/UpdateTSIGTest.cs b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/TSIGs/UpdateTSIGTest.cs new file mode 100644 index 0000000..04b06c6 --- /dev/null +++ b/test/Extensions/Cloudflare.Dns.Tests/DnsZoneTransfersExtensions/TSIGs/UpdateTSIGTest.cs @@ -0,0 +1,147 @@ +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.DnsZoneTransfersExtensions.TSIGs +{ + [TestClass] + public class UpdateTSIGTest + { + public TestContext TestContext { get; set; } + + private const string AccountId = "023e105f4ecef8ad9ca31a8372d0c353"; + private const string TsigId = "023e105f4ecef8ad9ca31a8372d0c351"; + + private Mock _clientMock; + private CloudflareResponse _response; + private List<(string RequestPath, InternalTSIGRequest Request)> _callbacks; + private UpdateTSIGRequest _request; + + [TestInitialize] + public void Initialize() + { + _callbacks = new List<(string, InternalTSIGRequest)>(); + + _response = new CloudflareResponse + { + Success = true, + Messages = new[] + { + new ResponseInfo(1000, "Message 1") + }, + Errors = new[] + { + new ResponseInfo(1000, "Error 1") + }, + Result = new TSIG + { + Id = TsigId, + Name = "tsig-key-a", + Secret = "very-secret", + Algorithm = TSigAlgorithm.HMAC_SHA256 + } + }; + + _request = new UpdateTSIGRequest( + accountId: AccountId, + tsigId: TsigId, + name: "tsig-key-a", + secret: "very-secret" + ) + { + Algorithm = TSigAlgorithm.HMAC_SHA512 + }; + } + + [TestMethod] + public async Task ShouldUpdateTsig() + { + // Arrange + var client = GetClient(); + + // Act + var response = await client.UpdateTSIG(_request, TestContext.CancellationToken); + + // Assert + Assert.IsNotNull(response); + Assert.IsTrue(response.Success); + Assert.AreEqual(_response.Result, response.Result); + + Assert.HasCount(1, _callbacks); + + var (requestPath, request) = _callbacks.First(); + Assert.AreEqual($"/accounts/{AccountId}/secondary_dns/tsigs/{TsigId}", requestPath); + + Assert.IsNotNull(request); + Assert.AreEqual(_request.Name, request.Name); + Assert.AreEqual(_request.Secret, request.Secret); + Assert.AreEqual(_request.Algorithm, request.Algorithm); + + _clientMock.Verify(m => m.PutAsync($"/accounts/{AccountId}/secondary_dns/tsigs/{TsigId}", It.IsAny(), TestContext.CancellationToken), Times.Once); + _clientMock.VerifyNoOtherCalls(); + } + + [TestMethod] + [DataRow(null)] + [DataRow("")] + [DataRow(" ")] + public async Task ShouldThrowArgumentNullExceptionForName(string name) + { + // Arrange + _request.Name = name; + var client = GetClient(); + + // Act & Assert + await Assert.ThrowsExactlyAsync(async () => + { + await client.UpdateTSIG(_request, TestContext.CancellationToken); + }); + } + + [TestMethod] + public async Task ShouldThrowArgumentOutOfRangeExceptionForAlgorithm() + { + // Arrange + _request.Algorithm = 0; + var client = GetClient(); + + // Act & Assert + await Assert.ThrowsExactlyAsync(async () => + { + await client.UpdateTSIG(_request, TestContext.CancellationToken); + }); + } + + [TestMethod] + [DataRow(null)] + [DataRow("")] + [DataRow(" ")] + public async Task ShouldThrowArgumentNullExceptionForSecret(string secret) + { + // Arrange + _request.Secret = secret; + var client = GetClient(); + + // Act & Assert + await Assert.ThrowsExactlyAsync(async () => + { + await client.UpdateTSIG(_request, TestContext.CancellationToken); + }); + } + + 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; + } + } +}