From 682f25ae758c777168fac7b916d0d179fe45a5a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20M=C3=BCller?= Date: Tue, 24 Jun 2025 20:44:09 +0200 Subject: [PATCH] Add "ZoneHold" extensions --- .../Internals/InternalCreateZoneHoldFilter.cs | 17 ++ .../Internals/InternalRemoveZoneHoldFilter.cs | 17 ++ .../InternalUpdateZoneHoldRequest.cs | 11 ++ .../Cloudflare.Zones/Models/ZoneHold.cs | 27 ++++ Extensions/Cloudflare.Zones/README.md | 8 + .../Requests/CreateZoneHoldRequest.cs | 32 ++++ .../Requests/RemoveZoneHoldRequest.cs | 29 ++++ .../Requests/UpdateZoneHoldRequest.cs | 37 +++++ .../Cloudflare.Zones/ZoneHoldsExtensions.cs | 82 ++++++++++ .../ZoneHoldsExtensions/CreateZoneHoldTest.cs | 148 ++++++++++++++++++ .../ZoneHoldsExtensions/GetZoneHoldTest.cs | 80 ++++++++++ .../ZoneHoldsExtensions/RemoveZoneHoldTest.cs | 147 +++++++++++++++++ .../ZoneHoldsExtensions/UpdateZoneHoldTest.cs | 89 +++++++++++ 13 files changed, 724 insertions(+) create mode 100644 Extensions/Cloudflare.Zones/Internals/InternalCreateZoneHoldFilter.cs create mode 100644 Extensions/Cloudflare.Zones/Internals/InternalRemoveZoneHoldFilter.cs create mode 100644 Extensions/Cloudflare.Zones/Internals/InternalUpdateZoneHoldRequest.cs create mode 100644 Extensions/Cloudflare.Zones/Models/ZoneHold.cs create mode 100644 Extensions/Cloudflare.Zones/Requests/CreateZoneHoldRequest.cs create mode 100644 Extensions/Cloudflare.Zones/Requests/RemoveZoneHoldRequest.cs create mode 100644 Extensions/Cloudflare.Zones/Requests/UpdateZoneHoldRequest.cs create mode 100644 Extensions/Cloudflare.Zones/ZoneHoldsExtensions.cs create mode 100644 UnitTests/Cloudflare.Zones.Tests/ZoneHoldsExtensions/CreateZoneHoldTest.cs create mode 100644 UnitTests/Cloudflare.Zones.Tests/ZoneHoldsExtensions/GetZoneHoldTest.cs create mode 100644 UnitTests/Cloudflare.Zones.Tests/ZoneHoldsExtensions/RemoveZoneHoldTest.cs create mode 100644 UnitTests/Cloudflare.Zones.Tests/ZoneHoldsExtensions/UpdateZoneHoldTest.cs diff --git a/Extensions/Cloudflare.Zones/Internals/InternalCreateZoneHoldFilter.cs b/Extensions/Cloudflare.Zones/Internals/InternalCreateZoneHoldFilter.cs new file mode 100644 index 0000000..4038804 --- /dev/null +++ b/Extensions/Cloudflare.Zones/Internals/InternalCreateZoneHoldFilter.cs @@ -0,0 +1,17 @@ +namespace AMWD.Net.Api.Cloudflare.Zones.Internals +{ + internal class InternalCreateZoneHoldFilter : IQueryParameterFilter + { + public bool? IncludeSubdomains { get; set; } + + public IDictionary GetQueryParameters() + { + var dict = new Dictionary(); + + if (IncludeSubdomains.HasValue) + dict.Add("include_subdomains", IncludeSubdomains.Value ? "true" : "false"); + + return dict; + } + } +} diff --git a/Extensions/Cloudflare.Zones/Internals/InternalRemoveZoneHoldFilter.cs b/Extensions/Cloudflare.Zones/Internals/InternalRemoveZoneHoldFilter.cs new file mode 100644 index 0000000..330c43f --- /dev/null +++ b/Extensions/Cloudflare.Zones/Internals/InternalRemoveZoneHoldFilter.cs @@ -0,0 +1,17 @@ +namespace AMWD.Net.Api.Cloudflare.Zones.Internals +{ + internal class InternalRemoveZoneHoldFilter : IQueryParameterFilter + { + public DateTime? HoldAfter { get; set; } + + public IDictionary GetQueryParameters() + { + var dict = new Dictionary(); + + if (HoldAfter.HasValue) + dict.Add("hold_after", HoldAfter.Value.ToUniversalTime().ToString("yyyy-MM-dd'T'HH:mm:ss'Z'")); + + return dict; + } + } +} diff --git a/Extensions/Cloudflare.Zones/Internals/InternalUpdateZoneHoldRequest.cs b/Extensions/Cloudflare.Zones/Internals/InternalUpdateZoneHoldRequest.cs new file mode 100644 index 0000000..3926cd7 --- /dev/null +++ b/Extensions/Cloudflare.Zones/Internals/InternalUpdateZoneHoldRequest.cs @@ -0,0 +1,11 @@ +namespace AMWD.Net.Api.Cloudflare.Zones.Internals +{ + internal class InternalUpdateZoneHoldRequest + { + [JsonProperty("hold_after")] + public DateTime? HoldAfter { get; set; } + + [JsonProperty("include_subdomains")] + public bool? IncludeSubdomains { get; set; } + } +} diff --git a/Extensions/Cloudflare.Zones/Models/ZoneHold.cs b/Extensions/Cloudflare.Zones/Models/ZoneHold.cs new file mode 100644 index 0000000..6933ed9 --- /dev/null +++ b/Extensions/Cloudflare.Zones/Models/ZoneHold.cs @@ -0,0 +1,27 @@ +namespace AMWD.Net.Api.Cloudflare.Zones +{ + /// + /// A Cloudflare zone hold. + /// Source + /// + public class ZoneHold + { + /// + /// Whether the zone is on hold. + /// + [JsonProperty("hold")] + public bool? Hold { get; set; } + + /// + /// The hold is enabled if the value is in the past. + /// + [JsonProperty("hold_after")] + public DateTime? HoldAfter { get; set; } + + /// + /// Whether to include subdomains in the hold. + /// + [JsonProperty("include_subdomains")] + public bool? IncludeSubdomains { get; set; } + } +} diff --git a/Extensions/Cloudflare.Zones/README.md b/Extensions/Cloudflare.Zones/README.md index 9d40db1..1d337c0 100644 --- a/Extensions/Cloudflare.Zones/README.md +++ b/Extensions/Cloudflare.Zones/README.md @@ -25,6 +25,13 @@ This package contains the feature set of the _Domain/Zone Management_ section of - [Rerun The Activation Check](https://developers.cloudflare.com/api/resources/zones/subresources/activation_check/methods/trigger/) +##### [Holds] + +- [Create Zone Hold](https://developers.cloudflare.com/api/resources/zones/subresources/holds/methods/create/) +- [Remove Zone Hold](https://developers.cloudflare.com/api/resources/zones/subresources/holds/methods/delete/) +- [Update Zone Hold](https://developers.cloudflare.com/api/resources/zones/subresources/holds/methods/edit/) +- [Get Zone Hold](https://developers.cloudflare.com/api/resources/zones/subresources/holds/methods/get/) + @@ -41,3 +48,4 @@ Published under MIT License (see [choose a license]) [Registrar]: https://developers.cloudflare.com/api/resources/registrar/ [Zones]: https://developers.cloudflare.com/api/resources/zones/ [Activation Check]: https://developers.cloudflare.com/api/resources/zones/subresources/activation_check/ +[Holds]: https://developers.cloudflare.com/api/resources/zones/subresources/holds/ diff --git a/Extensions/Cloudflare.Zones/Requests/CreateZoneHoldRequest.cs b/Extensions/Cloudflare.Zones/Requests/CreateZoneHoldRequest.cs new file mode 100644 index 0000000..32fc956 --- /dev/null +++ b/Extensions/Cloudflare.Zones/Requests/CreateZoneHoldRequest.cs @@ -0,0 +1,32 @@ +namespace AMWD.Net.Api.Cloudflare.Zones +{ + /// + /// Represents a request to create a zone hold. + /// + public class CreateZoneHoldRequest + { + /// + /// Initializes a new instance of the class. + /// + /// The zone identifier. + public CreateZoneHoldRequest(string zoneId) + { + ZoneId = zoneId; + } + + /// + /// The zone identifier. + /// + public string ZoneId { get; set; } + + /// + /// If provided, the zone hold will extend to block any subdomain of the given zone, as well as SSL4SaaS Custom Hostnames. + /// + /// + /// For example, a zone hold on a zone with the hostname 'example.com' and + /// = will block + /// 'example.com', 'staging.example.com', 'api.staging.example.com', etc. + /// + public bool? IncludeSubdomains { get; set; } + } +} diff --git a/Extensions/Cloudflare.Zones/Requests/RemoveZoneHoldRequest.cs b/Extensions/Cloudflare.Zones/Requests/RemoveZoneHoldRequest.cs new file mode 100644 index 0000000..ee1c46a --- /dev/null +++ b/Extensions/Cloudflare.Zones/Requests/RemoveZoneHoldRequest.cs @@ -0,0 +1,29 @@ +namespace AMWD.Net.Api.Cloudflare.Zones +{ + /// + /// Represents a request to remove a zone hold. + /// + public class RemoveZoneHoldRequest + { + /// + /// Initializes a new instance of the class. + /// + /// The zone identifier. + public RemoveZoneHoldRequest(string zoneId) + { + ZoneId = zoneId; + } + + /// + /// The zone identifier. + /// + public string ZoneId { get; set; } + + /// + /// If it is provided, the hold will be temporarily disabled, + /// then automatically re-enabled by the system at the time specified in this timestamp. + /// Otherwise, the hold will be disabled indefinitely. + /// + public DateTime? HoldAfter { get; set; } + } +} diff --git a/Extensions/Cloudflare.Zones/Requests/UpdateZoneHoldRequest.cs b/Extensions/Cloudflare.Zones/Requests/UpdateZoneHoldRequest.cs new file mode 100644 index 0000000..b3feb3c --- /dev/null +++ b/Extensions/Cloudflare.Zones/Requests/UpdateZoneHoldRequest.cs @@ -0,0 +1,37 @@ +namespace AMWD.Net.Api.Cloudflare.Zones +{ + /// + /// Represents a request to update a zone hold. + /// + public class UpdateZoneHoldRequest + { + /// + /// Initializes a new instance of the class. + /// + /// The zone identifier. + public UpdateZoneHoldRequest(string zoneId) + { + ZoneId = zoneId; + } + + /// + /// The zone identifier. + /// + public string ZoneId { get; set; } + + /// + /// If the value is provided and future-dated, the hold will be temporarily disabled, + /// then automatically re-enabled by the system at the time specified in this timestamp. + /// A past-dated value will have no effect on an existing, enabled hold. + /// Providing an empty string will set its value to the current time. + /// + public DateTime? HoldAfter { get; set; } + + /// + /// If , the zone hold will extend to block any subdomain of the given zone, as well as SSL4SaaS Custom Hostnames. + /// For example, a zone hold on a zone with the hostname 'example.com' and = + /// will block 'example.com', 'staging.example.com', 'api.staging.example.com', etc. + /// + public bool? IncludeSubdomains { get; set; } + } +} diff --git a/Extensions/Cloudflare.Zones/ZoneHoldsExtensions.cs b/Extensions/Cloudflare.Zones/ZoneHoldsExtensions.cs new file mode 100644 index 0000000..a322d79 --- /dev/null +++ b/Extensions/Cloudflare.Zones/ZoneHoldsExtensions.cs @@ -0,0 +1,82 @@ +using System.Threading; +using System.Threading.Tasks; +using AMWD.Net.Api.Cloudflare.Zones.Internals; + +namespace AMWD.Net.Api.Cloudflare.Zones +{ + /// + /// Extensions for Zone Holds. + /// + public static class ZoneHoldsExtensions + { + /// + /// Enforce a zone hold on the zone, blocking the creation and activation of zones with this zone's hostname. + /// + /// The instance. + /// The request. + /// A cancellation token used to propagate notification that this operation should be canceled. + public static Task> CreateZoneHold(this ICloudflareClient client, CreateZoneHoldRequest request, CancellationToken cancellationToken = default) + { + request.ZoneId.ValidateCloudflareId(); + + var filter = new InternalCreateZoneHoldFilter + { + IncludeSubdomains = request.IncludeSubdomains + }; + + return client.PostAsync($"/zones/{request.ZoneId}/hold", null, filter, cancellationToken); + } + + /// + /// Stop enforcement of a zone hold on the zone, permanently or temporarily, + /// allowing the creation and activation of zones with this zone's hostname. + /// + /// The instance. + /// The request. + /// A cancellation token used to propagate notification that this operation should be canceled. + public static Task> RemoveZoneHold(this ICloudflareClient client, RemoveZoneHoldRequest request, CancellationToken cancellationToken = default) + { + request.ZoneId.ValidateCloudflareId(); + + var filter = new InternalRemoveZoneHoldFilter + { + HoldAfter = request.HoldAfter + }; + + return client.DeleteAsync($"/zones/{request.ZoneId}/hold", filter, cancellationToken); + } + + /// + /// Update the and/or values on an existing zone hold. + /// The hold is enabled if the is in the past. + /// + /// The instance. + /// The request. + /// A cancellation token used to propagate notification that this operation should be canceled. + public static Task> UpdateZoneHold(this ICloudflareClient client, UpdateZoneHoldRequest request, CancellationToken cancellationToken = default) + { + request.ZoneId.ValidateCloudflareId(); + + var req = new InternalUpdateZoneHoldRequest + { + HoldAfter = request.HoldAfter, + IncludeSubdomains = request.IncludeSubdomains + }; + + return client.PatchAsync($"/zones/{request.ZoneId}/hold", req, cancellationToken); + } + + /// + /// Retrieve whether the zone is subject to a zone hold, and metadata about the hold. + /// + /// The instance. + /// The zone identifier. + /// A cancellation token used to propagate notification that this operation should be canceled. + public static Task> GetZoneHold(this ICloudflareClient client, string zoneId, CancellationToken cancellationToken = default) + { + zoneId.ValidateCloudflareId(); + + return client.GetAsync($"/zones/{zoneId}/hold", cancellationToken: cancellationToken); + } + } +} diff --git a/UnitTests/Cloudflare.Zones.Tests/ZoneHoldsExtensions/CreateZoneHoldTest.cs b/UnitTests/Cloudflare.Zones.Tests/ZoneHoldsExtensions/CreateZoneHoldTest.cs new file mode 100644 index 0000000..04b5b7d --- /dev/null +++ b/UnitTests/Cloudflare.Zones.Tests/ZoneHoldsExtensions/CreateZoneHoldTest.cs @@ -0,0 +1,148 @@ +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.ZoneHoldsExtensions +{ + [TestClass] + public class CreateZoneHoldTest + { + private readonly DateTime _date = new(2025, 10, 10, 20, 30, 40, 0, DateTimeKind.Utc); + private const string ZoneId = "023e105f4ecef8ad9ca31a8372d0c353"; + + private Mock _clientMock; + + private CloudflareResponse _response; + + private List<(string RequestPath, object Request, IQueryParameterFilter QueryFilter)> _callbacks; + + private CreateZoneHoldRequest _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 ZoneHold + { + Hold = true, + HoldAfter = _date, + IncludeSubdomains = false + } + }; + + _request = new CreateZoneHoldRequest(ZoneId); + } + + [TestMethod] + public async Task ShouldCreateZoneHold() + { + // Arrange + var client = GetClient(); + + // Act + var response = await client.CreateZoneHold(_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}/hold", callback.RequestPath); + Assert.IsNotNull(callback.QueryFilter); + + Assert.IsInstanceOfType(callback.QueryFilter); + Assert.IsNull(((InternalCreateZoneHoldFilter)callback.QueryFilter).IncludeSubdomains); + + _clientMock.Verify(m => m.PostAsync($"/zones/{ZoneId}/hold", null, It.IsAny(), It.IsAny()), Times.Once); + _clientMock.VerifyNoOtherCalls(); + } + + [TestMethod] + public async Task ShouldCreateZoneHoldWithSubdomains() + { + // Arrange + _request.IncludeSubdomains = true; + var client = GetClient(); + + // Act + var response = await client.CreateZoneHold(_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}/hold", callback.RequestPath); + Assert.IsNotNull(callback.QueryFilter); + + Assert.IsInstanceOfType(callback.QueryFilter); + Assert.IsTrue(((InternalCreateZoneHoldFilter)callback.QueryFilter).IncludeSubdomains); + + _clientMock.Verify(m => m.PostAsync($"/zones/{ZoneId}/hold", null, It.IsAny(), It.IsAny()), Times.Once); + _clientMock.VerifyNoOtherCalls(); + } + + [TestMethod] + public void ShouldReturnEmptyDictionary() + { + // Arrange + var filter = new InternalCreateZoneHoldFilter(); + + // Act + var dict = filter.GetQueryParameters(); + + // Assert + Assert.IsNotNull(dict); + Assert.AreEqual(0, dict.Count); + } + + [DataTestMethod] + [DataRow(true)] + [DataRow(false)] + public void ShouldReturnQueryParameter(bool includeSubdomains) + { + // Arrange + var filter = new InternalCreateZoneHoldFilter { IncludeSubdomains = includeSubdomains }; + + // Act + var dict = filter.GetQueryParameters(); + + // Assert + Assert.IsNotNull(dict); + Assert.AreEqual(1, dict.Count); + Assert.IsTrue(dict.ContainsKey("include_subdomains")); + Assert.AreEqual(includeSubdomains.ToString().ToLower(), dict["include_subdomains"]); + } + + private ICloudflareClient GetClient() + { + _clientMock = new Mock(); + _clientMock + .Setup(m => m.PostAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((requestPath, request, queryFilter, _) => _callbacks.Add((requestPath, request, queryFilter))) + .ReturnsAsync(() => _response); + + return _clientMock.Object; + } + } +} diff --git a/UnitTests/Cloudflare.Zones.Tests/ZoneHoldsExtensions/GetZoneHoldTest.cs b/UnitTests/Cloudflare.Zones.Tests/ZoneHoldsExtensions/GetZoneHoldTest.cs new file mode 100644 index 0000000..a69a0d2 --- /dev/null +++ b/UnitTests/Cloudflare.Zones.Tests/ZoneHoldsExtensions/GetZoneHoldTest.cs @@ -0,0 +1,80 @@ +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.ZoneHoldsExtensions +{ + [TestClass] + public class GetZoneHoldTest + { + private readonly DateTime _date = new DateTime(2024, 10, 10, 20, 30, 40, 0, DateTimeKind.Utc); + 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 ZoneHold + { + Hold = true, + HoldAfter = _date, + IncludeSubdomains = false + } + }; + } + + [TestMethod] + public async Task ShouldGetZoneHold() + { + // Arrange + var client = GetClient(); + + // Act + var response = await client.GetZoneHold(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}/hold", callback.RequestPath); + Assert.IsNull(callback.QueryFilter); + + _clientMock.Verify(m => m.GetAsync($"/zones/{ZoneId}/hold", null, It.IsAny()), Times.Once); + _clientMock.VerifyNoOtherCalls(); + } + + private ICloudflareClient GetClient() + { + _clientMock = new Mock(); + _clientMock + .Setup(m => m.GetAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((requestPath, queryFilter, _) => _callbacks.Add((requestPath, queryFilter))) + .ReturnsAsync(() => _response); + + return _clientMock.Object; + } + } +} diff --git a/UnitTests/Cloudflare.Zones.Tests/ZoneHoldsExtensions/RemoveZoneHoldTest.cs b/UnitTests/Cloudflare.Zones.Tests/ZoneHoldsExtensions/RemoveZoneHoldTest.cs new file mode 100644 index 0000000..56faf16 --- /dev/null +++ b/UnitTests/Cloudflare.Zones.Tests/ZoneHoldsExtensions/RemoveZoneHoldTest.cs @@ -0,0 +1,147 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AMWD.Net.Api.Cloudflare; +using AMWD.Net.Api.Cloudflare.Zones; +using AMWD.Net.Api.Cloudflare.Zones.Internals; +using Moq; + +namespace Cloudflare.Zones.Tests.ZoneHoldsExtensions +{ + [TestClass] + public class RemoveZoneHoldTest + { + // Local: Europe/Berlin (Germany) - [CEST +2] | CET +1 + private readonly DateTime _date = new(2025, 10, 10, 20, 30, 40, 0, DateTimeKind.Unspecified); + private const string ZoneId = "023e105f4ecef8ad9ca31a8372d0c353"; + + private Mock _clientMock; + + private CloudflareResponse _response; + + private List<(string RequestPath, IQueryParameterFilter QueryFilter)> _callbacks; + + private RemoveZoneHoldRequest _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 ZoneHold + { + Hold = true, + HoldAfter = _date, + IncludeSubdomains = true + } + }; + + _request = new RemoveZoneHoldRequest(ZoneId); + } + + [TestMethod] + public async Task ShouldRemoveZoneHold() + { + // Arrange + var client = GetClient(); + + // Act + var response = await client.RemoveZoneHold(_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}/hold", callback.RequestPath); + Assert.IsNotNull(callback.QueryFilter); + + Assert.IsInstanceOfType(callback.QueryFilter); + Assert.IsNull(((InternalRemoveZoneHoldFilter)callback.QueryFilter).HoldAfter); + + _clientMock.Verify(m => m.DeleteAsync($"/zones/{ZoneId}/hold", It.IsAny(), It.IsAny()), Times.Once); + _clientMock.VerifyNoOtherCalls(); + } + + [TestMethod] + public async Task ShouldRemoveZoneHoldTemporarily() + { + // Arrange + _request.HoldAfter = _date; + var client = GetClient(); + + // Act + var response = await client.RemoveZoneHold(_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}/hold", callback.RequestPath); + Assert.IsNotNull(callback.QueryFilter); + + Assert.IsInstanceOfType(callback.QueryFilter); + Assert.AreEqual(_date, ((InternalRemoveZoneHoldFilter)callback.QueryFilter).HoldAfter); + + _clientMock.Verify(m => m.DeleteAsync($"/zones/{ZoneId}/hold", It.IsAny(), It.IsAny()), Times.Once); + _clientMock.VerifyNoOtherCalls(); + } + + [TestMethod] + public void ShouldReturnEmptyDictionary() + { + // Arrange + var filter = new InternalRemoveZoneHoldFilter(); + + // Act + var dict = filter.GetQueryParameters(); + + // Assert + Assert.IsNotNull(dict); + Assert.AreEqual(0, dict.Count); + } + + [TestMethod] + public void ShouldReturnQueryParameter() + { + // Arrange + var filter = new InternalRemoveZoneHoldFilter { HoldAfter = _date }; + + // Act + var dict = filter.GetQueryParameters(); + + // Assert + Assert.IsNotNull(dict); + Assert.AreEqual(1, dict.Count); + Assert.IsTrue(dict.ContainsKey("hold_after")); + Assert.AreEqual("2025-10-10T18:30:40Z", dict["hold_after"]); + } + + 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/ZoneHoldsExtensions/UpdateZoneHoldTest.cs b/UnitTests/Cloudflare.Zones.Tests/ZoneHoldsExtensions/UpdateZoneHoldTest.cs new file mode 100644 index 0000000..9462c48 --- /dev/null +++ b/UnitTests/Cloudflare.Zones.Tests/ZoneHoldsExtensions/UpdateZoneHoldTest.cs @@ -0,0 +1,89 @@ +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.ZoneHoldsExtensions +{ + [TestClass] + public class UpdateZoneHoldTest + { + private readonly DateTime _date = new DateTime(2024, 10, 10, 20, 30, 40, 0, DateTimeKind.Utc); + private const string ZoneId = "023e105f4ecef8ad9ca31a8372d0c353"; + + private Mock _clientMock; + + private CloudflareResponse _response; + + private List<(string RequestPath, InternalUpdateZoneHoldRequest Request)> _callbacks; + + private UpdateZoneHoldRequest _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 ZoneHold + { + Hold = true, + HoldAfter = _date, + IncludeSubdomains = false + } + }; + + _request = new UpdateZoneHoldRequest(ZoneId) + { + HoldAfter = _date, + IncludeSubdomains = true + }; + } + + [TestMethod] + public async Task ShouldUpdateZoneHold() + { + // Arrange + var client = GetClient(); + + // Act + var response = await client.UpdateZoneHold(_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}/hold", callback.RequestPath); + + Assert.IsNotNull(callback.Request); + Assert.AreEqual(_date, callback.Request.HoldAfter); + Assert.IsTrue(callback.Request.IncludeSubdomains); + } + + 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; + } + } +}