From 9aa1dbb02c11bf618908543c3ddf80c332eb71e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20M=C3=BCller?= Date: Tue, 24 Jun 2025 15:14:54 +0200 Subject: [PATCH] Add "Zone" extensions --- Cloudflare/ClientOptions.cs | 4 +- Extensions/Cloudflare.Zones/Enums/ZoneType.cs | 48 +++ .../Filters/ListZonesFilter.cs | 180 ++++++++ .../Internals/InternalCreateZoneRequest.cs | 22 + .../Internals/InternalEditZoneRequest.cs | 14 + Extensions/Cloudflare.Zones/Models/Zone.cs | 408 ++++++++++++++++++ Extensions/Cloudflare.Zones/README.md | 6 + .../Requests/CreateZoneRequest.cs | 33 ++ .../Requests/EditZoneRequest.cs | 47 ++ .../Cloudflare.Zones/ZonesExtensions.cs | 110 +++++ .../GetDomainTest.cs | 2 +- .../ListDomainsTest.cs | 2 +- .../UpdateDomainTest.cs | 2 +- .../ZonesExtensions/CreateZoneTest.cs | 153 +++++++ .../ZonesExtensions/DeleteZoneTest.cs | 77 ++++ .../ZonesExtensions/EditZoneTest.cs | 246 +++++++++++ .../ZonesExtensions/ListZonesTest.cs | 385 +++++++++++++++++ .../ZonesExtensions/ZoneDetailsTest.cs | 123 ++++++ 18 files changed, 1856 insertions(+), 6 deletions(-) create mode 100644 Extensions/Cloudflare.Zones/Enums/ZoneType.cs create mode 100644 Extensions/Cloudflare.Zones/Filters/ListZonesFilter.cs create mode 100644 Extensions/Cloudflare.Zones/Internals/InternalCreateZoneRequest.cs create mode 100644 Extensions/Cloudflare.Zones/Internals/InternalEditZoneRequest.cs create mode 100644 Extensions/Cloudflare.Zones/Models/Zone.cs create mode 100644 Extensions/Cloudflare.Zones/Requests/CreateZoneRequest.cs create mode 100644 Extensions/Cloudflare.Zones/Requests/EditZoneRequest.cs create mode 100644 Extensions/Cloudflare.Zones/ZonesExtensions.cs rename UnitTests/Cloudflare.Zones.Tests/{ZoneRegistrar => RegistrarExtensions}/GetDomainTest.cs (94%) rename UnitTests/Cloudflare.Zones.Tests/{ZoneRegistrar => RegistrarExtensions}/ListDomainsTest.cs (93%) rename UnitTests/Cloudflare.Zones.Tests/{ZoneRegistrar => RegistrarExtensions}/UpdateDomainTest.cs (94%) create mode 100644 UnitTests/Cloudflare.Zones.Tests/ZonesExtensions/CreateZoneTest.cs create mode 100644 UnitTests/Cloudflare.Zones.Tests/ZonesExtensions/DeleteZoneTest.cs create mode 100644 UnitTests/Cloudflare.Zones.Tests/ZonesExtensions/EditZoneTest.cs create mode 100644 UnitTests/Cloudflare.Zones.Tests/ZonesExtensions/ListZonesTest.cs create mode 100644 UnitTests/Cloudflare.Zones.Tests/ZonesExtensions/ZoneDetailsTest.cs 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; + } + } +}