Add "ZonePlans" extensions

This commit is contained in:
2025-06-26 11:11:32 +02:00
parent 2e451bcdab
commit b5279b60a8
8 changed files with 461 additions and 1 deletions

View File

@@ -230,7 +230,8 @@ namespace AMWD.Net.Api.Cloudflare
var errorResponse = JsonConvert.DeserializeObject<CloudflareResponse<object>>(content, _jsonSerializerSettings) var errorResponse = JsonConvert.DeserializeObject<CloudflareResponse<object>>(content, _jsonSerializerSettings)
?? throw new CloudflareException("Response is not a valid Cloudflare API response."); ?? throw new CloudflareException("Response is not a valid Cloudflare API response.");
throw new AuthenticationException(string.Join(Environment.NewLine, errorResponse.Errors.Select(e => $"{e.Code}: {e.Message}"))); string[] errors = errorResponse.Errors?.Select(e => $"{e.Code}: {e.Message}").ToArray() ?? [];
throw new AuthenticationException(string.Join(Environment.NewLine, errors));
default: default:
try try

View File

@@ -0,0 +1,69 @@
namespace AMWD.Net.Api.Cloudflare.Zones
{
/// <summary>
/// A Cloudflare available plan.
/// <see href="https://github.com/cloudflare/cloudflare-typescript/blob/v4.4.1/src/resources/zones/plans.ts#L60">Source</see>
/// </summary>
public class AvailableRatePlan
{
/// <summary>
/// Identifier.
/// </summary>
[JsonProperty("id")]
public string? Id { get; set; }
/// <summary>
/// Indicates whether you can subscribe to this plan.
/// </summary>
[JsonProperty("can_subscribe")]
public bool? CanSubscribe { get; set; }
/// <summary>
/// The monetary unit in which pricing information is displayed.
/// </summary>
[JsonProperty("currency")]
public string? Currency { get; set; }
/// <summary>
/// Indicates whether this plan is managed externally.
/// </summary>
[JsonProperty("externally_managed")]
public bool? ExternallyManaged { get; set; }
/// <summary>
/// The frequency at which you will be billed for this plan.
/// </summary>
[JsonProperty("frequency")]
public RenewFrequency? Frequency { get; set; }
/// <summary>
/// Indicates whether you are currently subscribed to this plan.
/// </summary>
[JsonProperty("is_subscribed")]
public bool? IsSubscribed { get; set; }
/// <summary>
/// Indicates whether this plan has a legacy discount applied.
/// </summary>
[JsonProperty("legacy_discount")]
public bool? LegacyDiscount { get; set; }
/// <summary>
/// The legacy identifier for this rate plan, if any.
/// </summary>
[JsonProperty("legacy_id")]
public string? LegacyId { get; set; }
/// <summary>
/// The plan name.
/// </summary>
[JsonProperty("name")]
public string? Name { get; set; }
/// <summary>
/// The amount you will be billed for this plan.
/// </summary>
[JsonProperty("price")]
public decimal? Price { get; set; }
}
}

View File

@@ -33,6 +33,16 @@ This package contains the feature set of the _Domain/Zone Management_ section of
- [Get Zone Hold](https://developers.cloudflare.com/api/resources/zones/subresources/holds/methods/get/) - [Get Zone Hold](https://developers.cloudflare.com/api/resources/zones/subresources/holds/methods/get/)
##### [Plans]
- [Available Plan Details](https://developers.cloudflare.com/api/resources/zones/subresources/plans/methods/get/)
- [List Available Plans](https://developers.cloudflare.com/api/resources/zones/subresources/plans/methods/list/)
##### [Rate Plans]
- [List Available Rate Plans](https://developers.cloudflare.com/api/resources/zones/subresources/rate_plans/methods/get/)
@@ -49,3 +59,5 @@ Published under MIT License (see [choose a license])
[Zones]: https://developers.cloudflare.com/api/resources/zones/ [Zones]: https://developers.cloudflare.com/api/resources/zones/
[Activation Check]: https://developers.cloudflare.com/api/resources/zones/subresources/activation_check/ [Activation Check]: https://developers.cloudflare.com/api/resources/zones/subresources/activation_check/
[Holds]: https://developers.cloudflare.com/api/resources/zones/subresources/holds/ [Holds]: https://developers.cloudflare.com/api/resources/zones/subresources/holds/
[Plans]: https://developers.cloudflare.com/api/resources/zones/subresources/plans/
[Rate Plans]: https://developers.cloudflare.com/api/resources/zones/subresources/rate_plans/

View File

@@ -0,0 +1,103 @@
using System.Runtime.Serialization;
using Newtonsoft.Json.Converters;
namespace AMWD.Net.Api.Cloudflare.Zones
{
/// <summary>
/// <see href="https://github.com/cloudflare/cloudflare-typescript/blob/v4.4.1/src/resources/zones/rate-plans.ts#L36">Source</see>
/// </summary>
public class RatePlanGetResponse
{
/// <summary>
/// Plan identifier tag.
/// </summary>
[JsonProperty("id")]
public string? Id { get; set; }
/// <summary>
/// Array of available components values for the plan.
/// </summary>
[JsonProperty("components")]
public IReadOnlyCollection<Component>? Components { get; set; }
/// <summary>
/// The monetary unit in which pricing information is displayed.
/// </summary>
[JsonProperty("currency")]
public string? Currency { get; set; }
/// <summary>
/// The duration of the plan subscription.
/// </summary>
[JsonProperty("duration")]
public int? Duration { get; set; }
/// <summary>
/// The frequency at which you will be billed for this plan.
/// </summary>
[JsonProperty("frequency")]
public RenewFrequency? Frequency { get; set; }
/// <summary>
/// The plan name.
/// </summary>
[JsonProperty("name")]
public string? Name { get; set; }
/// <summary>
/// Rate plan component.
/// </summary>
public class Component
{
/// <summary>
/// The default amount allocated.
/// </summary>
[JsonProperty("default")]
public decimal? Default { get; set; }
/// <summary>
/// The unique component.
/// </summary>
[JsonProperty("name")]
public ComponentName? Name { get; set; }
/// <summary>
/// The unit price of the addon.
/// </summary>
[JsonProperty("unit_price")]
public decimal? UnitPrice { get; set; }
}
/// <summary>
/// Rate plan component name.
/// <see href="https://github.com/cloudflare/cloudflare-typescript/blob/v4.4.1/src/resources/zones/rate-plans.ts#L78">Source</see>
/// </summary>
[JsonConverter(typeof(StringEnumConverter))]
public enum ComponentName
{
/// <summary>
/// Zones
/// </summary>
[EnumMember(Value = "zones")]
Zones = 1,
/// <summary>
/// Page rules
/// </summary>
[EnumMember(Value = "page_rules")]
PageRules = 2,
/// <summary>
/// Dedicated certificates
/// </summary>
[EnumMember(Value = "dedicated_certificates")]
DedicatedCertificatese = 3,
/// <summary>
/// Custom dedicated certificates
/// </summary>
[EnumMember(Value = "dedicated_certificates_custom")]
DedicatedCertificatesCustom = 4
}
}
}

View File

@@ -0,0 +1,52 @@
using System.Threading;
using System.Threading.Tasks;
namespace AMWD.Net.Api.Cloudflare.Zones
{
/// <summary>
/// Extensions for <see href="https://developers.cloudflare.com/api/resources/zones/subresources/plans/">Zone Plans</see>.
/// </summary>
public static class ZonePlansExtensions
{
/// <summary>
/// Details of the available plan that the zone can subscribe to.
/// </summary>
/// <param name="client">The <see cref="ICloudflareClient"/> instance.</param>
/// <param name="zoneId">The zone identifier.</param>
/// <param name="planId">The plan identifier.</param>
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
public static Task<CloudflareResponse<AvailableRatePlan>> AvailablePlanDetails(this ICloudflareClient client, string zoneId, string planId, CancellationToken cancellationToken = default)
{
zoneId.ValidateCloudflareId();
planId.ValidateCloudflareId();
return client.GetAsync<AvailableRatePlan>($"/zones/{zoneId}/available_plans/{planId}", cancellationToken: cancellationToken);
}
/// <summary>
/// Lists available plans the zone can subscribe to.
/// </summary>
/// <param name="client">The <see cref="ICloudflareClient"/> instance.</param>
/// <param name="zoneId">The zone identifier.</param>
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
public static Task<CloudflareResponse<IReadOnlyCollection<AvailableRatePlan>>> ListAvailablePlans(this ICloudflareClient client, string zoneId, CancellationToken cancellationToken = default)
{
zoneId.ValidateCloudflareId();
return client.GetAsync<IReadOnlyCollection<AvailableRatePlan>>($"/zones/{zoneId}/available_plans", cancellationToken: cancellationToken);
}
/// <summary>
/// Lists all rate plans the zone can subscribe to.
/// </summary>
/// <param name="client">The <see cref="ICloudflareClient"/> instance.</param>
/// <param name="zoneId">The zone identifier.</param>
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
public static Task<CloudflareResponse<RatePlanGetResponse>> ListAvailableRatePlans(this ICloudflareClient client, string zoneId, CancellationToken cancellationToken = default)
{
zoneId.ValidateCloudflareId();
return client.GetAsync<RatePlanGetResponse>($"/zones/{zoneId}/available_rate_plans", cancellationToken: cancellationToken);
}
}
}

View File

@@ -0,0 +1,75 @@
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.ZonePlansExtensions
{
[TestClass]
public class AvailablePlanDetailsTest
{
private const string ZoneId = "023e105f4ecef8ad9ca31a8372d0c353";
private const string PlanId = "023e105f4ecef8ad9ca31a8372d0c354";
private Mock<ICloudflareClient> _clientMock;
private CloudflareResponse<AvailableRatePlan> _response;
private List<(string RequestPath, IQueryParameterFilter QueryFilter)> _callbacks;
[TestInitialize]
public void Initialize()
{
_callbacks = [];
_response = new CloudflareResponse<AvailableRatePlan>
{
Success = true,
Messages = [
new ResponseInfo(1000, "Message 1")
],
Errors = [
new ResponseInfo(1000, "Error 1")
],
Result = new AvailableRatePlan()
};
}
[TestMethod]
public async Task ShouldReturnAvailablePlan()
{
// Arrange
var client = GetClient();
// Act
var response = await client.AvailablePlanDetails(ZoneId, PlanId);
// 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}/available_plans/{PlanId}", callback.RequestPath);
Assert.IsNull(callback.QueryFilter);
_clientMock.Verify(m => m.GetAsync<AvailableRatePlan>($"/zones/{ZoneId}/available_plans/{PlanId}", null, It.IsAny<CancellationToken>()), Times.Once);
_clientMock.VerifyNoOtherCalls();
}
private ICloudflareClient GetClient()
{
_clientMock = new Mock<ICloudflareClient>();
_clientMock
.Setup(m => m.GetAsync<AvailableRatePlan>(It.IsAny<string>(), It.IsAny<IQueryParameterFilter>(), It.IsAny<CancellationToken>()))
.Callback<string, IQueryParameterFilter, CancellationToken>((requestPath, queryFilter, _) => _callbacks.Add((requestPath, queryFilter)))
.ReturnsAsync(() => _response);
return _clientMock.Object;
}
}
}

View File

@@ -0,0 +1,74 @@
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.ZonePlansExtensions
{
[TestClass]
public class ListAvailablePlansTest
{
private const string ZoneId = "023e105f4ecef8ad9ca31a8372d0c353";
private Mock<ICloudflareClient> _clientMock;
private CloudflareResponse<IReadOnlyCollection<AvailableRatePlan>> _response;
private List<(string RequestPath, IQueryParameterFilter QueryFilter)> _callbacks;
[TestInitialize]
public void Initialize()
{
_callbacks = [];
_response = new CloudflareResponse<IReadOnlyCollection<AvailableRatePlan>>
{
Success = true,
Messages = [
new ResponseInfo(1000, "Message 1")
],
Errors = [
new ResponseInfo(1000, "Error 1")
],
Result = []
};
}
[TestMethod]
public async Task ShouldReturnAvailablePlan()
{
// Arrange
var client = GetClient();
// Act
var response = await client.ListAvailablePlans(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}/available_plans", callback.RequestPath);
Assert.IsNull(callback.QueryFilter);
_clientMock.Verify(m => m.GetAsync<IReadOnlyCollection<AvailableRatePlan>>($"/zones/{ZoneId}/available_plans", null, It.IsAny<CancellationToken>()), Times.Once);
_clientMock.VerifyNoOtherCalls();
}
private ICloudflareClient GetClient()
{
_clientMock = new Mock<ICloudflareClient>();
_clientMock
.Setup(m => m.GetAsync<IReadOnlyCollection<AvailableRatePlan>>(It.IsAny<string>(), It.IsAny<IQueryParameterFilter>(), It.IsAny<CancellationToken>()))
.Callback<string, IQueryParameterFilter, CancellationToken>((requestPath, queryFilter, _) => _callbacks.Add((requestPath, queryFilter)))
.ReturnsAsync(() => _response);
return _clientMock.Object;
}
}
}

View File

@@ -0,0 +1,74 @@
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.ZonePlansExtensions
{
[TestClass]
public class ListAvailableRatePlansTest
{
private const string ZoneId = "023e105f4ecef8ad9ca31a8372d0c353";
private Mock<ICloudflareClient> _clientMock;
private CloudflareResponse<RatePlanGetResponse> _response;
private List<(string RequestPath, IQueryParameterFilter QueryFilter)> _callbacks;
[TestInitialize]
public void Initialize()
{
_callbacks = [];
_response = new CloudflareResponse<RatePlanGetResponse>
{
Success = true,
Messages = [
new ResponseInfo(1000, "Message 1")
],
Errors = [
new ResponseInfo(1000, "Error 1")
],
Result = new()
};
}
[TestMethod]
public async Task ShouldReturnAvailablePlans()
{
// Arrange
var client = GetClient();
// Act
var response = await client.ListAvailableRatePlans(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}/available_rate_plans", callback.RequestPath);
Assert.IsNull(callback.QueryFilter);
_clientMock.Verify(m => m.GetAsync<RatePlanGetResponse>($"/zones/{ZoneId}/available_rate_plans", null, It.IsAny<CancellationToken>()), Times.Once);
_clientMock.VerifyNoOtherCalls();
}
private ICloudflareClient GetClient()
{
_clientMock = new Mock<ICloudflareClient>();
_clientMock
.Setup(m => m.GetAsync<RatePlanGetResponse>(It.IsAny<string>(), It.IsAny<IQueryParameterFilter>(), It.IsAny<CancellationToken>()))
.Callback<string, IQueryParameterFilter, CancellationToken>((requestPath, queryFilter, _) => _callbacks.Add((requestPath, queryFilter)))
.ReturnsAsync(() => _response);
return _clientMock.Object;
}
}
}