diff --git a/Cloudflare/ClientOptions.cs b/Cloudflare/ClientOptions.cs
index 299364a..71bf579 100644
--- a/Cloudflare/ClientOptions.cs
+++ b/Cloudflare/ClientOptions.cs
@@ -1,6 +1,4 @@
-using System;
-using System.Collections.Generic;
-using System.Net;
+using System.Net;
namespace AMWD.Net.Api.Cloudflare
{
diff --git a/Extensions/Cloudflare.Zones/Enums/ZoneType.cs b/Extensions/Cloudflare.Zones/Enums/ZoneType.cs
new file mode 100644
index 0000000..715eee0
--- /dev/null
+++ b/Extensions/Cloudflare.Zones/Enums/ZoneType.cs
@@ -0,0 +1,48 @@
+using System.Runtime.Serialization;
+using Newtonsoft.Json.Converters;
+
+namespace AMWD.Net.Api.Cloudflare.Zones
+{
+ ///
+ /// Zone types.
+ /// Source
+ ///
+ [JsonConverter(typeof(StringEnumConverter))]
+ public enum ZoneType
+ {
+ ///
+ /// Full Setup (most common).
+ ///
+ ///
+ /// Use Cloudflare as your primary DNS provider and manage your DNS records on Cloudflare.
+ ///
+ [EnumMember(Value = "full")]
+ Full = 1,
+
+ ///
+ /// Partial (CNAME) setup.
+ ///
+ ///
+ /// Keep your primary DNS provider and only use Cloudflare's reverse proxy for individual subdomains.
+ ///
+ [EnumMember(Value = "partial")]
+ Partial = 2,
+
+ ///
+ /// Zone transfers.
+ ///
+ ///
+ /// Use Cloudflare and another DNS provider together across your entire zone to increase availability and fault tolerance.
+ ///
+ /// DNS records will be transferred between providers using AXFR or IXFR.
+ ///
+ [EnumMember(Value = "secondary")]
+ Secondary = 3,
+
+ ///
+ /// An internal zone.
+ ///
+ [EnumMember(Value = "internal")]
+ Internal = 4
+ }
+}
diff --git a/Extensions/Cloudflare.Zones/Filters/ListZonesFilter.cs b/Extensions/Cloudflare.Zones/Filters/ListZonesFilter.cs
new file mode 100644
index 0000000..b5c6dca
--- /dev/null
+++ b/Extensions/Cloudflare.Zones/Filters/ListZonesFilter.cs
@@ -0,0 +1,180 @@
+using System.Runtime.Serialization;
+using Newtonsoft.Json.Converters;
+
+namespace AMWD.Net.Api.Cloudflare.Zones
+{
+ ///
+ /// Filter for listing zones.
+ ///
+ public class ListZonesFilter : IQueryParameterFilter
+ {
+ ///
+ /// An account ID.
+ ///
+ public string? AccountId { get; set; }
+
+ ///
+ /// An account Name.
+ ///
+ ///
+ /// Optional filter operators can be provided to extend refine the search:
+ ///
+ /// - equal (default)
+ /// - not_equal
+ /// - starts_with
+ /// - ends_with
+ /// - contains
+ /// - starts_with_case_sensitive
+ /// - ends_with_case_sensitive
+ /// - contains_case_sensitive
+ ///
+ ///
+ /// Dev Account
+ /// contains:Test
+ public string? AccountName { get; set; }
+
+ ///
+ /// Direction to order zones.
+ ///
+ public SortDirection? Direction { get; set; }
+
+ ///
+ /// Whether to match all search requirements or at least one (any).
+ ///
+ public ListZonesMatch? Match { get; set; }
+
+ ///
+ /// A domain name.
+ ///
+ ///
+ /// Optional filter operators can be provided to extend refine the search:
+ ///
+ /// - equal (default)
+ /// - not_equal
+ /// - starts_with
+ /// - ends_with
+ /// - contains
+ /// - starts_with_case_sensitive
+ /// - ends_with_case_sensitive
+ /// - contains_case_sensitive
+ ///
+ ///
+ /// example.com
+ /// contains:.org
+ /// ends_with:arpa
+ /// starts_with:dev
+ public string? Name { get; set; }
+
+ ///
+ /// Field to order zones by.
+ ///
+ public ListZonesOrderBy? OrderBy { get; set; }
+
+ ///
+ /// Page number of paginated results.
+ ///
+ /// 1 <= X
+ public int? Page { get; set; }
+
+ ///
+ /// Number of zones per page.
+ ///
+ /// 5 <= X <= 50
+ public int? PerPage { get; set; }
+
+ ///
+ /// A zone status.
+ ///
+ public ZoneStatus? Status { get; set; }
+
+ ///
+ public IDictionary GetQueryParameters()
+ {
+ var dict = new Dictionary();
+
+#pragma warning disable CS8602, CS8604 // There will be no null value below.
+
+ if (!string.IsNullOrWhiteSpace(AccountId))
+ dict.Add("account.id", AccountId.Trim());
+
+ if (!string.IsNullOrWhiteSpace(AccountName))
+ dict.Add("account.name", AccountName.Trim());
+
+ if (Direction.HasValue && Enum.IsDefined(typeof(SortDirection), Direction.Value))
+ dict.Add("direction", Direction.Value.GetEnumMemberValue());
+
+ if (Match.HasValue && Enum.IsDefined(typeof(ListZonesMatch), Match.Value))
+ dict.Add("match", Match.Value.GetEnumMemberValue());
+
+ if (!string.IsNullOrWhiteSpace(Name))
+ dict.Add("name", Name);
+
+ if (OrderBy.HasValue && Enum.IsDefined(typeof(ListZonesOrderBy), OrderBy.Value))
+ dict.Add("order", OrderBy.Value.GetEnumMemberValue());
+
+ if (Page.HasValue && Page.Value >= 1)
+ dict.Add("page", Page.Value.ToString());
+
+ if (PerPage.HasValue && PerPage.Value >= 5 && PerPage.Value <= 50)
+ dict.Add("per_page", PerPage.Value.ToString());
+
+ if (Status.HasValue && Enum.IsDefined(typeof(ZoneStatus), Status.Value))
+ dict.Add("status", Status.Value.GetEnumMemberValue());
+
+#pragma warning restore CS8602, CS8604
+
+ return dict;
+ }
+ }
+
+ ///
+ /// Match type for listing zones.
+ /// Source
+ ///
+ [JsonConverter(typeof(StringEnumConverter))]
+ public enum ListZonesMatch
+ {
+ ///
+ /// Match all search requirements.
+ ///
+ [EnumMember(Value = "all")]
+ All = 1,
+
+ ///
+ /// Match at least one search requirement.
+ ///
+ [EnumMember(Value = "any")]
+ Any = 2
+ }
+
+ ///
+ /// Field to order zones by.
+ /// Source
+ ///
+ public enum ListZonesOrderBy
+ {
+ ///
+ /// Order by zone name.
+ ///
+ [EnumMember(Value = "name")]
+ Name = 1,
+
+ ///
+ /// Order by zone status.
+ ///
+ [EnumMember(Value = "status")]
+ Status = 2,
+
+ ///
+ /// Order by account ID.
+ ///
+ [EnumMember(Value = "account.id")]
+ AccountId = 3,
+
+ ///
+ /// Order by account name.
+ ///
+ [EnumMember(Value = "account.name")]
+ AccountName = 4,
+ }
+}
diff --git a/Extensions/Cloudflare.Zones/Internals/InternalCreateZoneRequest.cs b/Extensions/Cloudflare.Zones/Internals/InternalCreateZoneRequest.cs
new file mode 100644
index 0000000..72572bc
--- /dev/null
+++ b/Extensions/Cloudflare.Zones/Internals/InternalCreateZoneRequest.cs
@@ -0,0 +1,22 @@
+namespace AMWD.Net.Api.Cloudflare.Zones.Internals
+{
+ internal class InternalCreateZoneRequest
+ {
+ public InternalCreateZoneRequest(string? accountId, string name, ZoneType? type)
+ {
+ Account = new Identifier { Id = accountId };
+
+ Name = name;
+ Type = type;
+ }
+
+ [JsonProperty("account")]
+ public Identifier Account { get; set; }
+
+ [JsonProperty("name")]
+ public string Name { get; set; }
+
+ [JsonProperty("type")]
+ public ZoneType? Type { get; set; }
+ }
+}
diff --git a/Extensions/Cloudflare.Zones/Internals/InternalEditZoneRequest.cs b/Extensions/Cloudflare.Zones/Internals/InternalEditZoneRequest.cs
new file mode 100644
index 0000000..d3888aa
--- /dev/null
+++ b/Extensions/Cloudflare.Zones/Internals/InternalEditZoneRequest.cs
@@ -0,0 +1,14 @@
+namespace AMWD.Net.Api.Cloudflare.Zones.Internals
+{
+ internal class InternalEditZoneRequest
+ {
+ [JsonProperty("paused")]
+ public bool? Paused { get; set; }
+
+ [JsonProperty("type")]
+ public ZoneType? Type { get; set; }
+
+ [JsonProperty("vanity_name_servers")]
+ public IReadOnlyCollection? VanityNameServers { get; set; }
+ }
+}
diff --git a/Extensions/Cloudflare.Zones/Models/Zone.cs b/Extensions/Cloudflare.Zones/Models/Zone.cs
new file mode 100644
index 0000000..4a13b95
--- /dev/null
+++ b/Extensions/Cloudflare.Zones/Models/Zone.cs
@@ -0,0 +1,408 @@
+using System.Runtime.Serialization;
+using Newtonsoft.Json.Converters;
+
+namespace AMWD.Net.Api.Cloudflare.Zones
+{
+ ///
+ /// A Cloudflare zone.
+ /// Source
+ ///
+ public class Zone
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Identifier.
+ /// The domain name.
+ /// The name servers Cloudflare assigns to the zone.
+ /// The account the zone belongs to.
+ /// Metadata about the zone.
+ /// The owner of the zone.
+ public Zone(string id, string name, IReadOnlyCollection nameServers, ZoneAccount account, ZoneMeta meta, ZoneOwner owner)
+ {
+ Id = id;
+ Account = account;
+ Meta = meta;
+ Name = name;
+ NameServers = nameServers;
+ Owner = owner;
+
+#pragma warning disable CS0612
+ Plan = new();
+#pragma warning restore CS0612
+ }
+
+ ///
+ /// Identifier.
+ ///
+ [JsonProperty("id")]
+ public string Id { get; set; }
+
+ ///
+ /// The account the zone belongs to.
+ ///
+ [JsonProperty("account")]
+ public ZoneAccount Account { get; set; }
+
+ ///
+ /// The last time proof of ownership was detected and the zone was made active.
+ ///
+ [JsonProperty("activated_on")]
+ public DateTime? ActivatedOn { get; set; }
+
+ ///
+ /// When the zone was created.
+ ///
+ [JsonProperty("created_on")]
+ public DateTime CreatedOn { get; set; }
+
+ ///
+ /// The interval (in seconds) from when development mode expires (positive integer)
+ /// or last expired (negative integer) for the domain. If development mode has never
+ /// been enabled, this value is 0.
+ ///
+ [JsonProperty("development_mode")]
+ public int DevelopmentMode { get; set; }
+
+ ///
+ /// Metadata about the zone.
+ ///
+ [JsonProperty("meta")]
+ public ZoneMeta Meta { get; set; }
+
+ ///
+ /// When the zone was last modified.
+ ///
+ [JsonProperty("modified_on")]
+ public DateTime ModifiedOn { get; set; }
+
+ ///
+ /// The domain name.
+ ///
+ [JsonProperty("name")]
+ public string Name { get; set; }
+
+ ///
+ /// The name servers Cloudflare assigns to a zone.
+ ///
+ [JsonProperty("name_servers")]
+ public IReadOnlyCollection NameServers { get; set; }
+
+ ///
+ /// DNS host at the time of switching to Cloudflare.
+ ///
+ [JsonProperty("original_dnshost")]
+ public string? OriginalDnsHost { get; set; }
+
+ ///
+ /// Original name servers before moving to Cloudflare.
+ ///
+ [JsonProperty("original_name_servers")]
+ public IReadOnlyCollection? OriginalNameServers { get; set; }
+
+ ///
+ /// Registrar for the domain at the time of switching to Cloudflare.
+ ///
+ [JsonProperty("original_registrar")]
+ public string? OriginalRegistrar { get; set; }
+
+ ///
+ /// The owner of the zone.
+ ///
+ [JsonProperty("owner")]
+ public ZoneOwner Owner { get; set; }
+
+ ///
+ /// A Zones subscription information.
+ ///
+ [Obsolete]
+ [JsonProperty("plan")]
+ public ZonePlan Plan { get; set; }
+
+ ///
+ /// Allows the customer to use a custom apex.
+ /// Tenants Only Configuration.
+ ///
+ [JsonProperty("cname_suffix")]
+ public string? CnameSuffix { get; set; }
+
+ ///
+ /// Indicates whether the zone is only using Cloudflare DNS services.
+ ///
+ ///
+ /// A value means the zone will not receive security or performance benefits.
+ ///
+ [JsonProperty("paused")]
+ public bool? Paused { get; set; }
+
+ ///
+ /// Legacy permissions based on legacy user membership information.
+ ///
+ [Obsolete]
+ [JsonProperty("permissions")]
+ public IReadOnlyCollection? Permissions { get; set; }
+
+ ///
+ /// The zone status on Cloudflare.
+ ///
+ [JsonProperty("status")]
+ public ZoneStatus? Status { get; set; }
+
+ ///
+ /// The root organizational unit that this zone belongs to (such as a tenant or organization).
+ ///
+ [JsonProperty("tenant")]
+ public ZoneTenant? Tenant { get; set; }
+
+ ///
+ /// The immediate parent organizational unit that this zone belongs to (such as under a tenant or sub-organization).
+ ///
+ [JsonProperty("tenant_unit")]
+ public ZoneTenantUnit? TenantUnit { get; set; }
+
+ ///
+ /// A full zone implies that DNS is hosted with Cloudflare.
+ /// A partial zone is typically a partner-hosted zone or a CNAME setup.
+ ///
+ [JsonProperty("type")]
+ public ZoneType? Type { get; set; }
+
+ ///
+ /// An array of domains used for custom name servers.
+ /// This is only available for Business and Enterprise plans.
+ ///
+ [JsonProperty("vanity_name_servers")]
+ public IReadOnlyCollection? VanityNameServers { get; set; }
+
+ ///
+ /// Verification key for partial zone setup.
+ ///
+ [JsonProperty("verification_key")]
+ public string? VerificationKey { get; set; }
+ }
+
+ ///
+ /// The account the zone belongs to.
+ /// Source
+ ///
+ public class ZoneAccount
+ {
+ ///
+ /// Identifier.
+ ///
+ [JsonProperty("id")]
+ public string? Id { get; set; }
+
+ ///
+ /// The name of the account.
+ ///
+ [JsonProperty("name")]
+ public string? Name { get; set; }
+ }
+
+ ///
+ /// Metadata about the zone.
+ /// Source
+ ///
+ public class ZoneMeta
+ {
+ ///
+ /// The zone is only configured for CDN.
+ ///
+ [JsonProperty("cdn_only")]
+ public bool? CdnOnly { get; set; }
+
+ ///
+ /// Number of Custom Certificates the zone can have.
+ ///
+ [JsonProperty("custom_certificate_quota")]
+ public int? CustomCertificateQuota { get; set; }
+
+ ///
+ /// The zone is only configured for DNS.
+ ///
+ [JsonProperty("dns_only")]
+ public bool? DnsOnly { get; set; }
+
+ ///
+ /// The zone is setup with Foundation DNS.
+ ///
+ [JsonProperty("foundation_dns")]
+ public bool? FoundationDns { get; set; }
+
+ ///
+ /// Number of Page Rules a zone can have.
+ ///
+ [JsonProperty("page_rule_quota")]
+ public int? PageRuleQuota { get; set; }
+
+ ///
+ /// The zone has been flagged for phishing.
+ ///
+ [JsonProperty("phishing_detected")]
+ public bool? PhishingDetected { get; set; }
+
+ ///
+ /// Step.
+ ///
+ [JsonProperty("step")]
+ public int? Step { get; set; }
+ }
+
+ ///
+ /// The owner of the zone.
+ /// Source
+ ///
+ public class ZoneOwner
+ {
+ ///
+ /// Identifier.
+ ///
+ [JsonProperty("id")]
+ public string? Id { get; set; }
+
+ ///
+ /// Name of the owner.
+ ///
+ [JsonProperty("name")]
+ public string? Name { get; set; }
+
+ ///
+ /// The type of owner.
+ ///
+ [JsonProperty("type")]
+ public string? Type { get; set; }
+ }
+
+ ///
+ /// A Zones subscription information.
+ /// Source
+ ///
+ [Obsolete]
+ public class ZonePlan
+ {
+ ///
+ /// Identifier.
+ ///
+ [JsonProperty("id")]
+ public string? Id { get; set; }
+
+ ///
+ /// States if the subscription can be activated.
+ ///
+ [JsonProperty("can_subscribe")]
+ public bool? CanSubscribe { get; set; }
+
+ ///
+ /// The denomination of the customer.
+ ///
+ [JsonProperty("currency")]
+ public string? Currency { get; set; }
+
+ ///
+ /// If this Zone is managed by another company.
+ ///
+ [JsonProperty("externally_managed")]
+ public bool? ExternallyManaged { get; set; }
+
+ ///
+ /// How often the customer is billed.
+ ///
+ [JsonProperty("frequency")]
+ public string? Frequency { get; set; }
+
+ ///
+ /// States if the subscription active.
+ ///
+ [JsonProperty("is_subscribed")]
+ public bool? IsSubscribed { get; set; }
+
+ ///
+ /// If the legacy discount applies to this Zone.
+ ///
+ [JsonProperty("legacy_discount")]
+ public bool? LegacyDiscount { get; set; }
+
+ ///
+ /// The legacy name of the plan.
+ ///
+ [JsonProperty("legacy_id")]
+ public string? LegacyId { get; set; }
+
+ ///
+ /// Name of the owner.
+ ///
+ [JsonProperty("name")]
+ public string? Name { get; set; }
+
+ ///
+ /// How much the customer is paying.
+ ///
+ [JsonProperty("price")]
+ public decimal? Price { get; set; }
+ }
+
+ ///
+ /// Zone status.
+ /// Source
+ ///
+ [JsonConverter(typeof(StringEnumConverter))]
+ public enum ZoneStatus
+ {
+ ///
+ /// Initializing.
+ ///
+ [EnumMember(Value = "initializing")]
+ Initializing = 1,
+
+ ///
+ /// Pending.
+ ///
+ [EnumMember(Value = "pending")]
+ Pending = 2,
+
+ ///
+ /// Active.
+ ///
+ [EnumMember(Value = "active")]
+ Active = 3,
+
+ ///
+ /// Moved.
+ ///
+ [EnumMember(Value = "moved")]
+ Moved = 4
+ }
+
+ ///
+ /// The root organizational unit that this zone belongs to (such as a tenant or organization).
+ /// Source
+ ///
+ public class ZoneTenant
+ {
+ ///
+ /// Identifier.
+ ///
+ [JsonProperty("id")]
+ public string? Id { get; set; }
+
+ ///
+ /// The name of the Tenant account.
+ ///
+ [JsonProperty("name")]
+ public string? Name { get; set; }
+ }
+
+ ///
+ /// The immediate parent organizational unit that this zone belongs to (such as under a tenant or sub-organization).
+ /// Source
+ ///
+ public class ZoneTenantUnit
+ {
+ ///
+ /// Identifier.
+ ///
+ [JsonProperty("id")]
+ public string? Id { get; set; }
+ }
+}
diff --git a/Extensions/Cloudflare.Zones/README.md b/Extensions/Cloudflare.Zones/README.md
index 45092cd..747a47c 100644
--- a/Extensions/Cloudflare.Zones/README.md
+++ b/Extensions/Cloudflare.Zones/README.md
@@ -11,7 +11,13 @@ This package contains the feature set of the _Domain/Zone Management_ section of
- [Update Domain](https://developers.cloudflare.com/api/resources/registrar/subresources/domains/methods/update/)
+### [Zones]
+- [Create Zone](https://developers.cloudflare.com/api/resources/zones/methods/create/)
+- [Delete Zone](https://developers.cloudflare.com/api/resources/zones/methods/delete/)
+- [Edit Zone](https://developers.cloudflare.com/api/resources/zones/methods/edit/)
+- [Zone Details](https://developers.cloudflare.com/api/resources/zones/methods/get/)
+- [List Zones](https://developers.cloudflare.com/api/resources/zones/methods/list/)
diff --git a/Extensions/Cloudflare.Zones/Requests/CreateZoneRequest.cs b/Extensions/Cloudflare.Zones/Requests/CreateZoneRequest.cs
new file mode 100644
index 0000000..67c3f2a
--- /dev/null
+++ b/Extensions/Cloudflare.Zones/Requests/CreateZoneRequest.cs
@@ -0,0 +1,33 @@
+namespace AMWD.Net.Api.Cloudflare.Zones
+{
+ ///
+ /// Represents a request to create a zone.
+ ///
+ public class CreateZoneRequest
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The domain name.
+ public CreateZoneRequest(string name)
+ {
+ Name = name;
+ }
+
+ ///
+ /// The account identifier.
+ ///
+ public string? AccountId { get; set; }
+
+ ///
+ /// The domain name.
+ ///
+ public string Name { get; set; }
+
+ ///
+ /// A full zone implies that DNS is hosted with Cloudflare.
+ /// A partial zone is typically a partner-hosted zone or a CNAME setup.
+ ///
+ public ZoneType? Type { get; set; }
+ }
+}
diff --git a/Extensions/Cloudflare.Zones/Requests/EditZoneRequest.cs b/Extensions/Cloudflare.Zones/Requests/EditZoneRequest.cs
new file mode 100644
index 0000000..a2fc5a6
--- /dev/null
+++ b/Extensions/Cloudflare.Zones/Requests/EditZoneRequest.cs
@@ -0,0 +1,47 @@
+namespace AMWD.Net.Api.Cloudflare.Zones
+{
+ ///
+ /// Represents a request to edit a zone.
+ ///
+ public class EditZoneRequest
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The zone identifier.
+ public EditZoneRequest(string zoneId)
+ {
+ ZoneId = zoneId;
+ }
+
+ ///
+ /// The zone identifier.
+ ///
+ public string ZoneId { get; set; }
+
+ ///
+ /// Indicates whether the zone is only using Cloudflare DNS services.
+ ///
+ ///
+ /// A value means the zone will not receive security or performance benefits.
+ ///
+ public bool? Paused { get; set; }
+
+ ///
+ /// A full zone implies that DNS is hosted with Cloudflare.
+ /// A partial zone is typically a partner-hosted zone or a CNAME setup.
+ ///
+ ///
+ /// This parameter is only available to Enterprise customers or if it has been explicitly enabled on a zone.
+ ///
+ public ZoneType? Type { get; set; }
+
+ ///
+ /// A list of domains used for custom name servers.
+ ///
+ ///
+ /// This is only available for Business and Enterprise plans.
+ ///
+ public IReadOnlyCollection? VanityNameServers { get; set; }
+ }
+}
diff --git a/Extensions/Cloudflare.Zones/ZonesExtensions.cs b/Extensions/Cloudflare.Zones/ZonesExtensions.cs
new file mode 100644
index 0000000..2364afb
--- /dev/null
+++ b/Extensions/Cloudflare.Zones/ZonesExtensions.cs
@@ -0,0 +1,110 @@
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using AMWD.Net.Api.Cloudflare.Zones.Internals;
+
+namespace AMWD.Net.Api.Cloudflare.Zones
+{
+ ///
+ /// Extensions for Zones.
+ ///
+ public static class ZonesExtensions
+ {
+ ///
+ /// Create Zone.
+ ///
+ /// The instance.
+ /// The request.
+ /// A cancellation token used to propagate notification that this operation should be canceled.
+ public static Task> CreateZone(this ICloudflareClient client, CreateZoneRequest request, CancellationToken cancellationToken = default)
+ {
+ request.AccountId?.ValidateCloudflareId();
+ request.Name.ValidateLength(253, nameof(request.Name));
+
+ if (request.Type.HasValue && !Enum.IsDefined(typeof(ZoneType), request.Type.Value))
+ throw new ArgumentOutOfRangeException(nameof(request.Type));
+
+ var req = new InternalCreateZoneRequest(
+ request.AccountId,
+ request.Name,
+ request.Type
+ );
+
+ return client.PostAsync($"/zones", req, cancellationToken: cancellationToken);
+ }
+
+ ///
+ /// Deletes an existing zone.
+ ///
+ /// The instance.
+ /// The zone identifier.
+ /// A cancellation token used to propagate notification that this operation should be canceled.
+ public static Task> DeleteZone(this ICloudflareClient client, string zoneId, CancellationToken cancellationToken = default)
+ {
+ zoneId.ValidateCloudflareId();
+
+ return client.DeleteAsync($"/zones/{zoneId}", cancellationToken: cancellationToken);
+ }
+
+ ///
+ /// Edits a zone.
+ ///
+ ///
+ /// Only one zone property can be changed at a time.
+ ///
+ /// The instance.
+ /// The request.
+ /// A cancellation token used to propagate notification that this operation should be canceled.
+ public static Task> EditZone(this ICloudflareClient client, EditZoneRequest request, CancellationToken cancellationToken = default)
+ {
+ request.ZoneId.ValidateCloudflareId();
+
+ if (request.Paused.HasValue && (request.Type.HasValue || request.VanityNameServers != null))
+ throw new CloudflareException("Only one zone property can be changed at a time.");
+
+ if (request.Type.HasValue && request.VanityNameServers != null)
+ throw new CloudflareException("Only one zone property can be changed at a time.");
+
+ if (request.Type.HasValue && !Enum.IsDefined(typeof(ZoneType), request.Type.Value))
+ throw new ArgumentOutOfRangeException(nameof(request.Type));
+
+ var req = new InternalEditZoneRequest
+ {
+ Paused = request.Paused,
+ Type = request.Type
+ };
+
+ if (request.VanityNameServers != null)
+ req.VanityNameServers = request.VanityNameServers.Where(s => !string.IsNullOrWhiteSpace(s)).ToArray();
+
+ return client.PatchAsync($"/zones/{request.ZoneId}", req, cancellationToken: cancellationToken);
+ }
+
+ ///
+ /// Zone Details.
+ ///
+ /// The instance.
+ /// The zone identifier.
+ /// A cancellation token used to propagate notification that this operation should be canceled.
+ public static Task> ZoneDetails(this ICloudflareClient client, string zoneId, CancellationToken cancellationToken = default)
+ {
+ zoneId.ValidateCloudflareId();
+
+ return client.GetAsync($"/zones/{zoneId}", cancellationToken: cancellationToken);
+ }
+
+ ///
+ /// Lists, searches, sorts, and filters your zones.
+ ///
+ ///
+ /// Listing zones across more than 500 accounts is currently not allowed.
+ ///
+ /// The instance.
+ /// Filter options.
+ /// A cancellation token used to propagate notification that this operation should be canceled.
+ public static Task>> ListZones(this ICloudflareClient client, ListZonesFilter? options = null, CancellationToken cancellationToken = default)
+ {
+ return client.GetAsync>($"/zones", queryFilter: options, cancellationToken: cancellationToken);
+ }
+ }
+}
diff --git a/UnitTests/Cloudflare.Zones.Tests/ZoneRegistrar/GetDomainTest.cs b/UnitTests/Cloudflare.Zones.Tests/RegistrarExtensions/GetDomainTest.cs
similarity index 94%
rename from UnitTests/Cloudflare.Zones.Tests/ZoneRegistrar/GetDomainTest.cs
rename to UnitTests/Cloudflare.Zones.Tests/RegistrarExtensions/GetDomainTest.cs
index 7c81158..23525a4 100644
--- a/UnitTests/Cloudflare.Zones.Tests/ZoneRegistrar/GetDomainTest.cs
+++ b/UnitTests/Cloudflare.Zones.Tests/RegistrarExtensions/GetDomainTest.cs
@@ -6,7 +6,7 @@ using AMWD.Net.Api.Cloudflare.Zones;
using Moq;
using Newtonsoft.Json.Linq;
-namespace Cloudflare.Zones.Tests.ZoneRegistrar
+namespace Cloudflare.Zones.Tests.RegistrarExtensions
{
[TestClass]
public class GetDomainTest
diff --git a/UnitTests/Cloudflare.Zones.Tests/ZoneRegistrar/ListDomainsTest.cs b/UnitTests/Cloudflare.Zones.Tests/RegistrarExtensions/ListDomainsTest.cs
similarity index 93%
rename from UnitTests/Cloudflare.Zones.Tests/ZoneRegistrar/ListDomainsTest.cs
rename to UnitTests/Cloudflare.Zones.Tests/RegistrarExtensions/ListDomainsTest.cs
index 74a6103..1b988c4 100644
--- a/UnitTests/Cloudflare.Zones.Tests/ZoneRegistrar/ListDomainsTest.cs
+++ b/UnitTests/Cloudflare.Zones.Tests/RegistrarExtensions/ListDomainsTest.cs
@@ -5,7 +5,7 @@ using AMWD.Net.Api.Cloudflare;
using AMWD.Net.Api.Cloudflare.Zones;
using Moq;
-namespace Cloudflare.Zones.Tests.ZoneRegistrar
+namespace Cloudflare.Zones.Tests.RegistrarExtensions
{
[TestClass]
public class ListDomainsTest
diff --git a/UnitTests/Cloudflare.Zones.Tests/ZoneRegistrar/UpdateDomainTest.cs b/UnitTests/Cloudflare.Zones.Tests/RegistrarExtensions/UpdateDomainTest.cs
similarity index 94%
rename from UnitTests/Cloudflare.Zones.Tests/ZoneRegistrar/UpdateDomainTest.cs
rename to UnitTests/Cloudflare.Zones.Tests/RegistrarExtensions/UpdateDomainTest.cs
index eef8e35..3ca02a6 100644
--- a/UnitTests/Cloudflare.Zones.Tests/ZoneRegistrar/UpdateDomainTest.cs
+++ b/UnitTests/Cloudflare.Zones.Tests/RegistrarExtensions/UpdateDomainTest.cs
@@ -7,7 +7,7 @@ using AMWD.Net.Api.Cloudflare.Zones.Internals;
using Moq;
using Newtonsoft.Json.Linq;
-namespace Cloudflare.Zones.Tests.ZoneRegistrar
+namespace Cloudflare.Zones.Tests.RegistrarExtensions
{
[TestClass]
public class UpdateDomainTest
diff --git a/UnitTests/Cloudflare.Zones.Tests/ZonesExtensions/CreateZoneTest.cs b/UnitTests/Cloudflare.Zones.Tests/ZonesExtensions/CreateZoneTest.cs
new file mode 100644
index 0000000..d9f1e39
--- /dev/null
+++ b/UnitTests/Cloudflare.Zones.Tests/ZonesExtensions/CreateZoneTest.cs
@@ -0,0 +1,153 @@
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using AMWD.Net.Api.Cloudflare;
+using AMWD.Net.Api.Cloudflare.Zones;
+using AMWD.Net.Api.Cloudflare.Zones.Internals;
+using Moq;
+
+namespace Cloudflare.Zones.Tests.ZonesExtensions
+{
+ [TestClass]
+ public class CreateZoneTest
+ {
+ private const string ZoneId = "023e105f4ecef8ad9ca31a8372d0c353";
+
+ private Mock _clientMock;
+
+ private CloudflareResponse _response;
+
+ private List<(string RequestPath, InternalCreateZoneRequest Request)> _callbacks;
+
+ private CreateZoneRequest _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 Zone(
+ ZoneId,
+ "example.com",
+ [
+ "bob.ns.cloudflare.com",
+ "lola.ns.cloudflare.com"
+ ],
+ new ZoneAccount
+ {
+ Id = "023e105f4ecef8ad9ca31a8372d0c353",
+ Name = "Example Account Name"
+ },
+ new ZoneMeta
+ {
+ CdnOnly = true,
+ CustomCertificateQuota = 1,
+ DnsOnly = true,
+ FoundationDns = true,
+ PageRuleQuota = 100,
+ PhishingDetected = false,
+ Step = 2
+ },
+ new ZoneOwner
+ {
+ Id = "023e105f4ecef8ad9ca31a8372d0c353",
+ Name = "Example Org",
+ Type = "organization"
+ }
+ )
+ {
+ ActivatedOn = DateTime.Parse("2014-01-02T00:01:00.12345Z"),
+ CreatedOn = DateTime.Parse("2014-01-01T05:20:00.12345Z"),
+ DevelopmentMode = 7200,
+ ModifiedOn = DateTime.Parse("2014-01-01T05:20:00.12345Z"),
+ Name = "example.com",
+ OriginalDnsHost = "NameCheap",
+ OriginalNameServers =
+ [
+ "ns1.originaldnshost.com",
+ "ns2.originaldnshost.com"
+ ],
+ OriginalRegistrar = "GoDaddy",
+ Paused = true,
+ Status = ZoneStatus.Initializing,
+ Type = ZoneType.Full,
+ VanityNameServers =
+ [
+ "ns1.example.com",
+ "ns2.example.com"
+ ]
+ }
+ };
+
+ _request = new CreateZoneRequest("example.com")
+ {
+ Type = ZoneType.Full
+ };
+ }
+
+ [DataTestMethod]
+ [DataRow(null)]
+ [DataRow("023e105f4ecef8ad9ca31a8372d0c353")]
+ public async Task ShouldCreateZone(string accountId)
+ {
+ // Arrange
+ _request.AccountId = accountId;
+ var client = GetClient();
+
+ // Act
+ var response = await client.CreateZone(_request);
+
+ // Assert
+ Assert.IsNotNull(response);
+ Assert.IsTrue(response.Success);
+ Assert.AreEqual(_response.Result, response.Result);
+
+ Assert.AreEqual(1, _callbacks.Count);
+
+ var callback = _callbacks.First();
+ Assert.AreEqual("/zones", callback.RequestPath);
+ Assert.IsNotNull(callback.Request);
+
+ Assert.AreEqual(_request.AccountId, callback.Request.Account.Id);
+ Assert.AreEqual(_request.Name, callback.Request.Name);
+ Assert.AreEqual(_request.Type, callback.Request.Type);
+
+ _clientMock.Verify(m => m.PostAsync("/zones", It.IsAny(), It.IsAny(), It.IsAny()), Times.Once);
+ _clientMock.VerifyNoOtherCalls();
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(ArgumentOutOfRangeException))]
+ public async Task ShouldThrowArgumentOutOfRangeExceptionOnInvalidType()
+ {
+ // Arrange
+ _request.Type = 0;
+ var client = GetClient();
+
+ // Act
+ await client.CreateZone(_request);
+
+ // Assert - ArgumentOutOfRangeException
+ }
+
+ 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/UnitTests/Cloudflare.Zones.Tests/ZonesExtensions/DeleteZoneTest.cs b/UnitTests/Cloudflare.Zones.Tests/ZonesExtensions/DeleteZoneTest.cs
new file mode 100644
index 0000000..e002312
--- /dev/null
+++ b/UnitTests/Cloudflare.Zones.Tests/ZonesExtensions/DeleteZoneTest.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.Zones;
+using Moq;
+
+namespace Cloudflare.Zones.Tests.ZonesExtensions
+{
+ [TestClass]
+ public class DeleteZoneTest
+ {
+ private const string ZoneId = "023e105f4ecef8ad9ca31a8372d0c353";
+
+ private Mock _clientMock;
+
+ private CloudflareResponse _response;
+
+ private List<(string RequestPath, IQueryParameterFilter QueryFilter)> _callbacks;
+
+ [TestInitialize]
+ public void Initialize()
+ {
+ _callbacks = [];
+
+ _response = new CloudflareResponse
+ {
+ Success = true,
+ Messages = [
+ new ResponseInfo(1000, "Message 1")
+ ],
+ Errors = [
+ new ResponseInfo(1000, "Error 1")
+ ],
+ Result = new Identifier
+ {
+ Id = ZoneId
+ }
+ };
+ }
+
+ [TestMethod]
+ public async Task ShouldDeleteZone()
+ {
+ // Arrange
+ var client = GetClient();
+
+ // Act
+ var response = await client.DeleteZone(ZoneId);
+
+ // Assert
+ Assert.IsNotNull(response);
+ Assert.IsTrue(response.Success);
+ Assert.AreEqual(_response.Result, response.Result);
+
+ Assert.AreEqual(1, _callbacks.Count);
+
+ var callback = _callbacks.First();
+ Assert.AreEqual($"/zones/{ZoneId}", callback.RequestPath);
+ Assert.IsNull(callback.QueryFilter);
+
+ _clientMock.Verify(m => m.DeleteAsync($"/zones/{ZoneId}", It.IsAny(), It.IsAny()), Times.Once);
+ _clientMock.VerifyNoOtherCalls();
+ }
+
+ private ICloudflareClient GetClient()
+ {
+ _clientMock = new Mock();
+ _clientMock
+ .Setup(m => m.DeleteAsync(It.IsAny(), It.IsAny(), It.IsAny()))
+ .Callback((requestPath, queryFilter, _) => _callbacks.Add((requestPath, queryFilter)))
+ .ReturnsAsync(() => _response);
+
+ return _clientMock.Object;
+ }
+ }
+}
diff --git a/UnitTests/Cloudflare.Zones.Tests/ZonesExtensions/EditZoneTest.cs b/UnitTests/Cloudflare.Zones.Tests/ZonesExtensions/EditZoneTest.cs
new file mode 100644
index 0000000..19fd96e
--- /dev/null
+++ b/UnitTests/Cloudflare.Zones.Tests/ZonesExtensions/EditZoneTest.cs
@@ -0,0 +1,246 @@
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using AMWD.Net.Api.Cloudflare;
+using AMWD.Net.Api.Cloudflare.Zones;
+using AMWD.Net.Api.Cloudflare.Zones.Internals;
+using Moq;
+
+namespace Cloudflare.Zones.Tests.ZonesExtensions
+{
+ [TestClass]
+ public class EditZoneTest
+ {
+ private const string ZoneId = "023e105f4ecef8ad9ca31a8372d0c353";
+
+ private Mock _clientMock;
+
+ private CloudflareResponse _response;
+
+ private List<(string RequestPath, InternalEditZoneRequest Request)> _callbacks;
+
+ private EditZoneRequest _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 Zone(
+ ZoneId,
+ "example.com",
+ [
+ "bob.ns.cloudflare.com",
+ "lola.ns.cloudflare.com"
+ ],
+ new ZoneAccount
+ {
+ Id = "023e105f4ecef8ad9ca31a8372d0c353",
+ Name = "Example Account Name"
+ },
+ new ZoneMeta
+ {
+ CdnOnly = true,
+ CustomCertificateQuota = 1,
+ DnsOnly = true,
+ FoundationDns = true,
+ PageRuleQuota = 100,
+ PhishingDetected = false,
+ Step = 2
+ },
+ new ZoneOwner
+ {
+ Id = "023e105f4ecef8ad9ca31a8372d0c353",
+ Name = "Example Org",
+ Type = "organization"
+ }
+ )
+ {
+ ActivatedOn = DateTime.Parse("2014-01-02T00:01:00.12345Z"),
+ CreatedOn = DateTime.Parse("2014-01-01T05:20:00.12345Z"),
+ DevelopmentMode = 7200,
+ ModifiedOn = DateTime.Parse("2014-01-01T05:20:00.12345Z"),
+ OriginalDnsHost = "NameCheap",
+ OriginalNameServers =
+ [
+ "ns1.originaldnshost.com",
+ "ns2.originaldnshost.com"
+ ],
+ OriginalRegistrar = "GoDaddy",
+ Paused = true,
+ Status = ZoneStatus.Initializing,
+ Type = ZoneType.Full,
+ VanityNameServers =
+ [
+ "ns1.example.com",
+ "ns2.example.com"
+ ]
+ }
+ };
+
+ _request = new EditZoneRequest(ZoneId)
+ {
+ Paused = true,
+ Type = ZoneType.Full,
+ VanityNameServers = ["ns1.example.org", "ns2.example.org"]
+ };
+ }
+
+ [TestMethod]
+ public async Task ShouldReturnModifiedZoneForPaused()
+ {
+ // Arrange
+ _request.Type = null;
+ _request.VanityNameServers = null;
+ var client = GetClient();
+
+ // Act
+ var response = await client.EditZone(_request);
+
+ // Assert
+ Assert.IsNotNull(response);
+ Assert.IsTrue(response.Success);
+ Assert.AreEqual(_response.Result, response.Result);
+
+ Assert.AreEqual(1, _callbacks.Count);
+
+ var callback = _callbacks.First();
+ Assert.AreEqual($"/zones/{ZoneId}", callback.RequestPath);
+ Assert.IsNotNull(callback.Request);
+
+ Assert.IsTrue(callback.Request.Paused);
+ Assert.IsNull(callback.Request.Type);
+ Assert.IsNull(callback.Request.VanityNameServers);
+
+ _clientMock.Verify(m => m.PatchAsync($"/zones/{ZoneId}", It.IsAny(), It.IsAny()), Times.Once);
+ _clientMock.VerifyNoOtherCalls();
+ }
+
+ [TestMethod]
+ public async Task ShouldReturnModifiedZoneForType()
+ {
+ // Arrange
+ _request.Paused = null;
+ _request.VanityNameServers = null;
+ var client = GetClient();
+
+ // Act
+ var response = await client.EditZone(_request);
+
+ // Assert
+ Assert.IsNotNull(response);
+ Assert.IsTrue(response.Success);
+ Assert.AreEqual(_response.Result, response.Result);
+
+ Assert.AreEqual(1, _callbacks.Count);
+
+ var callback = _callbacks.First();
+ Assert.AreEqual($"/zones/{ZoneId}", callback.RequestPath);
+ Assert.IsNotNull(callback.Request);
+
+ Assert.IsNull(callback.Request.Paused);
+ Assert.AreEqual(_request.Type.Value, callback.Request.Type.Value);
+ Assert.IsNull(callback.Request.VanityNameServers);
+
+ _clientMock.Verify(m => m.PatchAsync($"/zones/{ZoneId}", It.IsAny(), It.IsAny()), Times.Once);
+ _clientMock.VerifyNoOtherCalls();
+ }
+
+ [TestMethod]
+ public async Task ShouldReturnModifiedZoneForVanityNameServers()
+ {
+ // Arrange
+ _request.Paused = null;
+ _request.Type = null;
+ _request.VanityNameServers = [.. _request.VanityNameServers, ""];
+ var client = GetClient();
+
+ // Act
+ var response = await client.EditZone(_request);
+
+ // Assert
+ Assert.IsNotNull(response);
+ Assert.IsTrue(response.Success);
+ Assert.AreEqual(_response.Result, response.Result);
+
+ Assert.AreEqual(1, _callbacks.Count);
+
+ var callback = _callbacks.First();
+ Assert.AreEqual($"/zones/{ZoneId}", callback.RequestPath);
+ Assert.IsNotNull(callback.Request);
+
+ Assert.IsNull(callback.Request.Paused);
+ Assert.IsNull(callback.Request.Type);
+ Assert.AreEqual(2, callback.Request.VanityNameServers.Count);
+ Assert.IsTrue(callback.Request.VanityNameServers.Contains("ns1.example.org"));
+ Assert.IsTrue(callback.Request.VanityNameServers.Contains("ns2.example.org"));
+
+ _clientMock.Verify(m => m.PatchAsync($"/zones/{ZoneId}", It.IsAny(), It.IsAny()), Times.Once);
+ _clientMock.VerifyNoOtherCalls();
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(CloudflareException))]
+ public async Task ShouldThrowCloudflareExceptionOnMultiplePropertiesSet1()
+ {
+ // Arrange
+ _request.VanityNameServers = null;
+ var client = GetClient();
+
+ // Act
+ await client.EditZone(_request);
+
+ // Assert - CloudflareException
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(CloudflareException))]
+ public async Task ShouldThrowCloudflareExceptionOnMultiplePropertiesSet2()
+ {
+ // Arrange
+ _request.Paused = null;
+ var client = GetClient();
+
+ // Act
+ await client.EditZone(_request);
+
+ // Assert - CloudflareException
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(ArgumentOutOfRangeException))]
+ public async Task ShouldThrowArgumentOutOfRangeExceptionForInvalidType()
+ {
+ // Arrange
+ _request.Paused = null;
+ _request.Type = 0;
+ _request.VanityNameServers = null;
+ var client = GetClient();
+
+ // Act
+ await client.EditZone(_request);
+
+ // Assert - ArgumentOutOfRangeException
+ }
+
+ private ICloudflareClient GetClient()
+ {
+ _clientMock = new Mock();
+ _clientMock
+ .Setup(m => m.PatchAsync(It.IsAny(), It.IsAny(), It.IsAny()))
+ .Callback((requestPath, request, _) => _callbacks.Add((requestPath, request)))
+ .ReturnsAsync(() => _response);
+
+ return _clientMock.Object;
+ }
+ }
+}
diff --git a/UnitTests/Cloudflare.Zones.Tests/ZonesExtensions/ListZonesTest.cs b/UnitTests/Cloudflare.Zones.Tests/ZonesExtensions/ListZonesTest.cs
new file mode 100644
index 0000000..5531301
--- /dev/null
+++ b/UnitTests/Cloudflare.Zones.Tests/ZonesExtensions/ListZonesTest.cs
@@ -0,0 +1,385 @@
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using AMWD.Net.Api.Cloudflare;
+using AMWD.Net.Api.Cloudflare.Zones;
+using Moq;
+
+namespace Cloudflare.Zones.Tests.ZonesExtensions
+{
+ [TestClass]
+ public class ListZonesTest
+ {
+ private const string ZoneId = "023e105f4ecef8ad9ca31a8372d0c353";
+
+ private Mock _clientMock;
+
+ private CloudflareResponse> _response;
+
+ private List<(string RequestPath, IQueryParameterFilter QueryFilter)> _callbacks;
+
+ [TestInitialize]
+ public void Initialize()
+ {
+ _callbacks = [];
+
+ _response = new CloudflareResponse>
+ {
+ Success = true,
+ Messages = [
+ new ResponseInfo(1000, "Message 1")
+ ],
+ Errors = [
+ new ResponseInfo(1000, "Error 1")
+ ],
+ ResultInfo = new PaginationInfo
+ {
+ Count = 1,
+ Page = 1,
+ PerPage = 20,
+ TotalCount = 2000
+ },
+ Result =
+ [
+ new Zone(
+ ZoneId,
+ "example.com",
+ [
+ "bob.ns.cloudflare.com",
+ "lola.ns.cloudflare.com"
+ ],
+ new ZoneAccount
+ {
+ Id = "023e105f4ecef8ad9ca31a8372d0c353",
+ Name = "Example Account Name"
+ },
+ new ZoneMeta
+ {
+ CdnOnly = true,
+ CustomCertificateQuota = 1,
+ DnsOnly = true,
+ FoundationDns = true,
+ PageRuleQuota = 100,
+ PhishingDetected = false,
+ Step = 2
+ },
+ new ZoneOwner
+ {
+ Id = "023e105f4ecef8ad9ca31a8372d0c353",
+ Name = "Example Org",
+ Type = "organization"
+ }
+ )
+ {
+ ActivatedOn = DateTime.Parse("2014-01-02T00:01:00.12345Z"),
+ CreatedOn = DateTime.Parse("2014-01-01T05:20:00.12345Z"),
+ DevelopmentMode = 7200,
+ ModifiedOn = DateTime.Parse("2014-01-01T05:20:00.12345Z"),
+ OriginalDnsHost = "NameCheap",
+ OriginalNameServers =
+ [
+ "ns1.originaldnshost.com",
+ "ns2.originaldnshost.com"
+ ],
+ OriginalRegistrar = "GoDaddy",
+ Paused = true,
+ Status = ZoneStatus.Initializing,
+ Type = ZoneType.Full,
+ VanityNameServers =
+ [
+ "ns1.example.com",
+ "ns2.example.com"
+ ]
+ }
+ ]
+ };
+ }
+
+ [TestMethod]
+ public async Task ShouldReturnListOfZones()
+ {
+ // Arrange
+ var client = GetClient();
+
+ // Act
+ var response = await client.ListZones();
+
+ // Assert
+ Assert.IsNotNull(response);
+ Assert.IsTrue(response.Success);
+ Assert.AreEqual(_response.Result, response.Result);
+
+ Assert.AreEqual(1, _callbacks.Count);
+
+ var callback = _callbacks.First();
+ Assert.AreEqual("/zones", callback.RequestPath);
+ Assert.IsNull(callback.QueryFilter);
+
+ _clientMock.Verify(m => m.GetAsync>("/zones", null, It.IsAny()), Times.Once);
+ _clientMock.VerifyNoOtherCalls();
+ }
+
+ [TestMethod]
+ public async Task ShouldReturnListOfZonesWithFilter()
+ {
+ // Arrange
+ var filter = new ListZonesFilter
+ {
+ AccountId = "023e105f4ecef8ad9ca31a8372d0c353"
+ };
+
+ var client = GetClient();
+
+ // Act
+ var response = await client.ListZones(filter);
+
+ // Assert
+ Assert.IsNotNull(response);
+ Assert.IsTrue(response.Success);
+ Assert.AreEqual(_response.Result, response.Result);
+
+ Assert.AreEqual(1, _callbacks.Count);
+
+ var callback = _callbacks.First();
+ Assert.AreEqual("/zones", callback.RequestPath);
+ Assert.AreEqual(filter, callback.QueryFilter);
+
+ _clientMock.Verify(m => m.GetAsync>("/zones", filter, It.IsAny()), Times.Once);
+ _clientMock.VerifyNoOtherCalls();
+ }
+
+ [TestMethod]
+ public void ShouldReturnEmptyParameterList()
+ {
+ // Arrange
+ var filter = new ListZonesFilter();
+
+ // Act
+ var dict = filter.GetQueryParameters();
+
+ // Assert
+ Assert.IsNotNull(dict);
+ Assert.AreEqual(0, dict.Count);
+ }
+
+ [TestMethod]
+ public void ShouldReturnFullParameterList()
+ {
+ // Arrange
+ var filter = new ListZonesFilter
+ {
+ AccountId = "023e105f4ecef8ad9ca31a8372d0c353",
+ AccountName = "Example Account Name",
+ Match = ListZonesMatch.Any,
+ Name = "example.com",
+ PerPage = 13,
+ Page = 5,
+ OrderBy = ListZonesOrderBy.AccountName,
+ Direction = SortDirection.Descending,
+ Status = ZoneStatus.Active
+ };
+
+ // Act
+ var dict = filter.GetQueryParameters();
+
+ // Assert
+ Assert.IsNotNull(dict);
+ Assert.AreEqual(9, dict.Count);
+
+ Assert.IsTrue(dict.ContainsKey("account.id"));
+ Assert.IsTrue(dict.ContainsKey("account.name"));
+ Assert.IsTrue(dict.ContainsKey("direction"));
+ Assert.IsTrue(dict.ContainsKey("match"));
+ Assert.IsTrue(dict.ContainsKey("name"));
+ Assert.IsTrue(dict.ContainsKey("order"));
+ Assert.IsTrue(dict.ContainsKey("page"));
+ Assert.IsTrue(dict.ContainsKey("per_page"));
+ Assert.IsTrue(dict.ContainsKey("status"));
+
+ Assert.AreEqual("023e105f4ecef8ad9ca31a8372d0c353", dict["account.id"]);
+ Assert.AreEqual("Example Account Name", dict["account.name"]);
+ Assert.AreEqual("desc", dict["direction"]);
+ Assert.AreEqual("any", dict["match"]);
+ Assert.AreEqual("example.com", dict["name"]);
+ Assert.AreEqual("account.name", dict["order"]);
+ Assert.AreEqual("5", dict["page"]);
+ Assert.AreEqual("13", dict["per_page"]);
+ Assert.AreEqual("active", dict["status"]);
+ }
+
+ [DataTestMethod]
+ [DataRow(null)]
+ [DataRow("")]
+ [DataRow(" ")]
+ public void ShouldNotAddAccountId(string id)
+ {
+ // Arrange
+ var filter = new ListZonesFilter
+ {
+ AccountId = id
+ };
+
+ // Act
+ var dict = filter.GetQueryParameters();
+
+ // Assert
+ Assert.IsNotNull(dict);
+ Assert.AreEqual(0, dict.Count);
+ }
+
+ [DataTestMethod]
+ [DataRow(null)]
+ [DataRow("")]
+ [DataRow(" ")]
+ public void ShouldNotAddAccountName(string name)
+ {
+ // Arrange
+ var filter = new ListZonesFilter
+ {
+ AccountName = name
+ };
+
+ // Act
+ var dict = filter.GetQueryParameters();
+
+ // Assert
+ Assert.IsNotNull(dict);
+ Assert.AreEqual(0, dict.Count);
+ }
+
+ [TestMethod]
+ public void ShouldNotAddDirection()
+ {
+ // Arrange
+ var filter = new ListZonesFilter
+ {
+ Direction = 0
+ };
+
+ // Act
+ var dict = filter.GetQueryParameters();
+
+ // Assert
+ Assert.IsNotNull(dict);
+ Assert.AreEqual(0, dict.Count);
+ }
+
+ [TestMethod]
+ public void ShouldNotAddMatch()
+ {
+ // Arrange
+ var filter = new ListZonesFilter
+ {
+ Match = 0
+ };
+
+ // Act
+ var dict = filter.GetQueryParameters();
+
+ // Assert
+ Assert.IsNotNull(dict);
+ Assert.AreEqual(0, dict.Count);
+ }
+
+ [DataTestMethod]
+ [DataRow(null)]
+ [DataRow("")]
+ [DataRow(" ")]
+ public void ShouldNotAddName(string name)
+ {
+ // Arrange
+ var filter = new ListZonesFilter
+ {
+ Name = name
+ };
+
+ // Act
+ var dict = new Dictionary();
+
+ // Assert
+ Assert.IsNotNull(dict);
+ Assert.AreEqual(0, dict.Count);
+ }
+
+ [TestMethod]
+ public void ShouldNotAddOrder()
+ {
+ // Arrange
+ var filter = new ListZonesFilter
+ {
+ OrderBy = 0
+ };
+
+ // Act
+ var dict = filter.GetQueryParameters();
+
+ // Assert
+ Assert.IsNotNull(dict);
+ Assert.AreEqual(0, dict.Count);
+ }
+
+ [TestMethod]
+ public void ShouldNotAddPage()
+ {
+ // Arrange
+ var filter = new ListZonesFilter
+ {
+ Page = 0
+ };
+
+ // Act
+ var dict = filter.GetQueryParameters();
+
+ // Assert
+ Assert.IsNotNull(dict);
+ Assert.AreEqual(0, dict.Count);
+ }
+
+ [DataTestMethod]
+ [DataRow(4)]
+ [DataRow(51)]
+ public void ShouldNotAddPerPage(int perPage)
+ {
+ // Arrange
+ var filter = new ListZonesFilter
+ {
+ PerPage = perPage
+ };
+
+ // Act
+ var dict = filter.GetQueryParameters();
+
+ // Assert
+ Assert.IsNotNull(dict);
+ Assert.AreEqual(0, dict.Count);
+ }
+
+ [TestMethod]
+ public void ShouldNotAddStatus()
+ {
+ // Arrange
+ var filter = new ListZonesFilter
+ {
+ Status = 0
+ };
+
+ // Act
+ var dict = filter.GetQueryParameters();
+
+ // Assert
+ Assert.IsNotNull(dict);
+ Assert.AreEqual(0, dict.Count);
+ }
+
+ private ICloudflareClient GetClient()
+ {
+ _clientMock = new Mock();
+ _clientMock
+ .Setup(m => m.GetAsync>(It.IsAny(), It.IsAny(), It.IsAny()))
+ .Callback((requestPath, queryFilter, _) => _callbacks.Add((requestPath, queryFilter)))
+ .ReturnsAsync(() => _response);
+
+ return _clientMock.Object;
+ }
+ }
+}
diff --git a/UnitTests/Cloudflare.Zones.Tests/ZonesExtensions/ZoneDetailsTest.cs b/UnitTests/Cloudflare.Zones.Tests/ZonesExtensions/ZoneDetailsTest.cs
new file mode 100644
index 0000000..fc435db
--- /dev/null
+++ b/UnitTests/Cloudflare.Zones.Tests/ZonesExtensions/ZoneDetailsTest.cs
@@ -0,0 +1,123 @@
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using AMWD.Net.Api.Cloudflare;
+using AMWD.Net.Api.Cloudflare.Zones;
+using Moq;
+
+namespace Cloudflare.Zones.Tests.ZonesExtensions
+{
+ [TestClass]
+ public class ZoneDetailsTest
+ {
+ private const string ZoneId = "023e105f4ecef8ad9ca31a8372d0c353";
+
+ private Mock _clientMock;
+
+ private CloudflareResponse _response;
+
+ private List<(string RequestPath, IQueryParameterFilter QueryFilter)> _callbacks;
+
+ [TestInitialize]
+ public void Initialize()
+ {
+ _callbacks = [];
+
+ _response = new CloudflareResponse
+ {
+ Success = true,
+ Messages = [
+ new ResponseInfo(1000, "Message 1")
+ ],
+ Errors = [
+ new ResponseInfo(1000, "Error 1")
+ ],
+ Result = new Zone(
+ ZoneId,
+ "example.com",
+ [
+ "bob.ns.cloudflare.com",
+ "lola.ns.cloudflare.com"
+ ],
+ new ZoneAccount
+ {
+ Id = "023e105f4ecef8ad9ca31a8372d0c353",
+ Name = "Example Account Name"
+ },
+ new ZoneMeta
+ {
+ CdnOnly = true,
+ CustomCertificateQuota = 1,
+ DnsOnly = true,
+ FoundationDns = true,
+ PageRuleQuota = 100,
+ PhishingDetected = false,
+ Step = 2
+ },
+ new ZoneOwner
+ {
+ Id = "023e105f4ecef8ad9ca31a8372d0c353",
+ Name = "Example Org",
+ Type = "organization"
+ }
+ )
+ {
+ ActivatedOn = DateTime.Parse("2014-01-02T00:01:00.12345Z"),
+ CreatedOn = DateTime.Parse("2014-01-01T05:20:00.12345Z"),
+ DevelopmentMode = 7200,
+ ModifiedOn = DateTime.Parse("2014-01-01T05:20:00.12345Z"),
+ OriginalDnsHost = "NameCheap",
+ OriginalNameServers =
+ [
+ "ns1.originaldnshost.com",
+ "ns2.originaldnshost.com"
+ ],
+ OriginalRegistrar = "GoDaddy",
+ Paused = true,
+ Status = ZoneStatus.Initializing,
+ Type = ZoneType.Full,
+ VanityNameServers =
+ [
+ "ns1.example.com",
+ "ns2.example.com"
+ ]
+ }
+ };
+ }
+
+ [TestMethod]
+ public async Task ShouldReturnZoneDetails()
+ {
+ // Arrange
+ var client = GetClient();
+
+ // Act
+ var response = await client.ZoneDetails(ZoneId);
+
+ // Assert
+ Assert.IsNotNull(response);
+ Assert.IsTrue(response.Success);
+ Assert.AreEqual(_response.Result, response.Result);
+
+ Assert.AreEqual(1, _callbacks.Count);
+
+ var callback = _callbacks.First();
+ Assert.AreEqual($"/zones/{ZoneId}", callback.RequestPath);
+ Assert.IsNull(callback.QueryFilter);
+
+ _clientMock.Verify(m => m.GetAsync($"/zones/{ZoneId}", null, It.IsAny()), Times.Once);
+ _clientMock.VerifyNoOtherCalls();
+ }
+
+ private ICloudflareClient GetClient()
+ {
+ _clientMock = new Mock();
+ _clientMock
+ .Setup(m => m.GetAsync(It.IsAny(), It.IsAny(), It.IsAny()))
+ .Callback((requestPath, queryFilter, _) => _callbacks.Add((requestPath, queryFilter)))
+ .ReturnsAsync(() => _response);
+
+ return _clientMock.Object;
+ }
+ }
+}