Implemented Core client

This commit is contained in:
2024-10-23 21:00:23 +02:00
parent 3c8d5137ff
commit 83620cb450
43 changed files with 4218 additions and 2 deletions

159
.editorconfig Normal file
View File

@@ -0,0 +1,159 @@
# Documentation:
# https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference
# Top-most EditorConfig file
root = true
[*]
insert_final_newline = true
end_of_line = crlf
indent_style = tab
[*.{cs,vb}]
# Sort using and Import directives with System.* appearing first
dotnet_sort_system_directives_first = true
# Avoid "this." and "Me." if not necessary
dotnet_style_qualification_for_event = false:warning
dotnet_style_qualification_for_field = false:warning
dotnet_style_qualification_for_method = false:warning
dotnet_style_qualification_for_property = false:warning
# Use language keywords instead of framework type names for type references
dotnet_style_predefined_type_for_locals_parameters_members = true:warning
dotnet_style_predefined_type_for_member_access = true:warning
# Suggest explicit accessibility modifiers
dotnet_style_require_accessibility_modifiers = always:suggestion
# Suggest more modern language features when available
dotnet_style_explicit_tuple_names = true:warning
dotnet_style_prefer_inferred_tuple_names = true:suggestion
dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
dotnet_style_prefer_conditional_expression_over_assignment = true:none
dotnet_style_prefer_conditional_expression_over_return = true:none
dotnet_style_coalesce_expression = true:suggestion
dotnet_style_null_propagation = true:suggestion
# Definitions
dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum, delegate, type_parameter
dotnet_naming_symbols.methods_properties.applicable_kinds = method, local_function, property
dotnet_naming_symbols.public_symbols.applicable_kinds = property, method, field, event
dotnet_naming_symbols.public_symbols.applicable_accessibilities = public
dotnet_naming_symbols.constant_fields.applicable_kinds = field
dotnet_naming_symbols.constant_fields.required_modifiers = const
dotnet_naming_symbols.private_protected_internal_fields.applicable_kinds = field
dotnet_naming_symbols.private_protected_internal_fields.applicable_accessibilities = private, protected, internal
dotnet_naming_symbols.parameters_locals.applicable_kinds = parameter, local
dotnet_naming_style.pascal_case_style.capitalization = pascal_case
dotnet_naming_style.camel_case_style.capitalization = camel_case
# Name all types using PascalCase
dotnet_naming_rule.types_must_be_capitalized.symbols = types
dotnet_naming_rule.types_must_be_capitalized.style = pascal_case_style
dotnet_naming_rule.types_must_be_capitalized.severity = warning
# Name all methods and properties using PascalCase
dotnet_naming_rule.methods_properties_must_be_capitalized.symbols = methods_properties
dotnet_naming_rule.methods_properties_must_be_capitalized.style = pascal_case_style
dotnet_naming_rule.methods_properties_must_be_capitalized.severity = warning
# Name all public members using PascalCase
dotnet_naming_rule.public_members_must_be_capitalized.symbols = public_symbols
dotnet_naming_rule.public_members_must_be_capitalized.style = pascal_case_style
dotnet_naming_rule.public_members_must_be_capitalized.severity = warning
# Name all constant fields using PascalCase
dotnet_naming_rule.constant_fields_must_be_pascal_case.symbols = constant_fields
dotnet_naming_rule.constant_fields_must_be_pascal_case.style = pascal_case_style
dotnet_naming_rule.constant_fields_must_be_pascal_case.severity = suggestion
# Name all private and internal fields using camelCase
dotnet_naming_rule.private_protected_internal_fields_must_be_camel_case.symbols = private_protected_internal_fields
dotnet_naming_rule.private_protected_internal_fields_must_be_camel_case.style = camel_case_style
dotnet_naming_rule.private_protected_internal_fields_must_be_camel_case.severity = warning
# Name all parameters and locals using camelCase
dotnet_naming_rule.parameters_locals_must_be_camel_case.symbols = parameters_locals
dotnet_naming_rule.parameters_locals_must_be_camel_case.style = camel_case_style
dotnet_naming_rule.parameters_locals_must_be_camel_case.severity = warning
# Name all private fields starting with underscore
dotnet_naming_rule.private_members_with_underscore.symbols = private_fields
dotnet_naming_rule.private_members_with_underscore.style = prefix_underscore
dotnet_naming_rule.private_members_with_underscore.severity = suggestion
dotnet_naming_symbols.private_fields.applicable_kinds = field
dotnet_naming_symbols.private_fields.applicable_accessibilities = private
dotnet_naming_style.prefix_underscore.capitalization = camel_case
dotnet_naming_style.prefix_underscore.required_prefix = _
[*.cs]
csharp_preferred_modifier_order = public, private, protected, internal, static, extern, new, virtual, abstract, sealed, override, readonly, unsafe, volatile, async:suggestion
# Only use "var" when it's obvious what the variable type is
csharp_style_var_for_built_in_types = false:warning
csharp_style_var_when_type_is_apparent = true:suggestion
csharp_style_var_elsewhere = false:none
# Prefer method-like constructs to have a block body
csharp_style_expression_bodied_methods = false:none
csharp_style_expression_bodied_constructors = false:none
csharp_style_expression_bodied_operators = false:none
# Prefer property-like constructs to have an expression-body
csharp_style_expression_bodied_properties = true:none
csharp_style_expression_bodied_indexers = true:none
csharp_style_expression_bodied_accessors = true:none
# Suggest more modern language features when available
csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
csharp_style_inlined_variable_declaration = true:suggestion
csharp_style_throw_expression = true:suggestion
csharp_style_conditional_delegate_call = true:suggestion
csharp_style_pattern_local_over_anonymous_function = true:suggestion
# Newline settings
csharp_new_line_before_open_brace = all
csharp_new_line_before_else = true
csharp_new_line_before_catch = true
csharp_new_line_before_finally = true
csharp_new_line_before_members_in_object_initializers = true
csharp_new_line_before_members_in_anonymous_types = true
# Indentation preferences
csharp_indent_block_contents = true
csharp_indent_braces = false
csharp_indent_case_contents = true
csharp_indent_switch_labels = true
csharp_indent_labels = one_less_than_current
# Space preferences
csharp_space_after_cast = false
csharp_space_after_colon_in_inheritance_clause = true
csharp_space_after_comma = true
csharp_space_after_dot = false
csharp_space_after_keywords_in_control_flow_statements = true
csharp_space_after_semicolon_in_for_statement = true
csharp_space_around_binary_operators = before_and_after
csharp_space_around_declaration_statements = do_not_ignore
csharp_space_before_colon_in_inheritance_clause = true
csharp_space_before_comma = false
csharp_space_before_dot = false
csharp_space_before_open_square_brackets = false
csharp_space_before_semicolon_in_for_statement = false
csharp_space_between_empty_square_brackets = false
csharp_space_between_method_call_empty_parameter_list_parentheses = false
csharp_space_between_method_call_name_and_opening_parenthesis = false
csharp_space_between_method_call_parameter_list_parentheses = false
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
csharp_space_between_method_declaration_name_and_open_parenthesis = false
csharp_space_between_method_declaration_parameter_list_parentheses = false
csharp_space_between_parentheses = false
csharp_space_between_square_brackets = false
[*.{xml,csproj,targets,props,json,yml}]
indent_size = 2
indent_style = space

182
.gitlab-ci.yml Normal file
View File

@@ -0,0 +1,182 @@
image: mcr.microsoft.com/dotnet/sdk:8.0
variables:
TZ: "Europe/Berlin"
LANG: "de"
stages:
- build
- test
- deploy
default-build:
stage: build
tags:
- docker
- lnx
- 64bit
rules:
- if: $CI_COMMIT_TAG == null
script:
- dotnet build -c Debug --nologo
- mkdir ./artifacts
- shopt -s globstar
- mv ./**/*.nupkg ./artifacts/ || true
- mv ./**/*.snupkg ./artifacts/ || true
artifacts:
paths:
- artifacts/*.nupkg
- artifacts/*.snupkg
expire_in: 1 days
default-test:
stage: test
dependencies:
- default-build
tags:
- docker
- lnx
- 64bit
rules:
- if: $CI_COMMIT_TAG == null
coverage: /Branch coverage[\s\S].+%/
before_script:
- dotnet tool install dotnet-reportgenerator-globaltool --tool-path /dotnet-tools
script:
- dotnet test -c Debug --nologo /p:CoverletOutputFormat=Cobertura
- /dotnet-tools/reportgenerator "-reports:${CI_PROJECT_DIR}/**/coverage.cobertura.xml" "-targetdir:/reports" -reportType:TextSummary
- cat /reports/Summary.txt
artifacts:
when: always
reports:
coverage_report:
coverage_format: cobertura
path: ./**/coverage.cobertura.xml
default-deploy:
stage: deploy
dependencies:
- default-build
- default-test
tags:
- docker
- lnx
- 64bit
rules:
- if: $CI_COMMIT_TAG == null
script:
- dotnet nuget push -k $BAGET_APIKEY -s https://nuget.am-wd.de/v3/index.json --skip-duplicate artifacts/*.nupkg || true
core-build:
stage: build
tags:
- docker
- lnx
- 64bit
rules:
- if: $CI_COMMIT_TAG =~ /^v[0-9.]+/
script:
- dotnet build -c Release --nologo Cloudflare/Cloudflare.csproj
- mkdir ./artifacts
- shopt -s globstar
- mv ./**/*.nupkg ./artifacts/ || true
- mv ./**/*.snupkg ./artifacts/ || true
artifacts:
paths:
- artifacts/*.nupkg
- artifacts/*.snupkg
expire_in: 1 days
core-test:
stage: test
dependencies:
- core-build
tags:
- docker
- lnx
- 64bit
rules:
- if: $CI_COMMIT_TAG =~ /^v[0-9.]+/
script:
- dotnet test -c Release --nologo /p:CoverletOutputFormat=Cobertura Cloudflare.Tests/Cloudflare.Tests.csproj
artifacts:
when: always
reports:
coverage_report:
coverage_format: cobertura
path: ./**/coverage.cobertura.xml
core-deploy:
stage: deploy
dependencies:
- core-build
- core-test
tags:
- docker
- lnx
- 64bit
rules:
- if: $CI_COMMIT_TAG =~ /^v[0-9.]+/
script:
- dotnet nuget push -k $BAGET_APIKEY -s https://nuget.am-wd.de/v3/index.json --skip-duplicate artifacts/*.nupkg || true
# - dotnet nuget push -k $NUGET_APIKEY -s https://api.nuget.org/v3/index.json --skip-duplicate artifacts/*.nupkg || true
extensions-build:
stage: build
tags:
- docker
- lnx
- 64bit
rules:
- if: $CI_COMMIT_TAG =~ /^[a-z]+\/v[0-9.]+/
script:
- dotnet build -c Release --nologo
- mkdir ./artifacts
- shopt -s globstars
- mv ./**/*.nupkg ./artifacts/ || true
- mv ./**/*.snupkg ./artifacts/ || true
artifacts:
paths:
- artifacts/*.nupkg
- artifacts/*.snupkg
expire_in: 1 days
extensions-test:
stage: test
dependencies:
- extensions-build
tags:
- docker
- lnx
- 64bit
rules:
- if: $CI_COMMIT_TAG =~ /^[a-z]+\/v[0-9.]+/
script:
- dotnet test -c Release --nologo /p:CoverletOutputFormat=Cobertura
artifacts:
when: always
reports:
coverage_report:
coverage_format: cobertura
path: ./**/coverage.cobertura.xml
extensions-deploy:
stage: deploy
dependencies:
- extensions-build
- extensions-test
tags:
- docker
- lnx
- 64bit
rules:
- if: $CI_COMMIT_TAG =~ /^[a-z]+\/v[0-9.]+/
script:
- dotnet nuget push -k $BAGET_APIKEY -s https://nuget.am-wd.de/v3/index.json --skip-duplicate artifacts/*.nupkg || true
# - dotnet nuget push -k $NUGET_APIKEY -s https://api.nuget.org/v3/index.json --skip-duplicate artifacts/*.nupkg || true

1
CHANGELOG.md Normal file
View File

@@ -0,0 +1 @@
# Changelog

View File

@@ -0,0 +1,80 @@
using System;
using System.Linq;
using System.Net.Http;
using AMWD.Net.Api.Cloudflare.Auth;
namespace Cloudflare.Core.Tests.Auth
{
[TestClass]
public class ApiKeyAuthenticationTest
{
[TestMethod]
public void ShouldAddHeaders()
{
// Arrange
string emailAddress = "test@example.com";
string apiKey = "some-api-key";
var auth = new ApiKeyAuthentication(emailAddress, apiKey);
using var clt = new HttpClient();
// Act
auth.AddHeader(clt);
// Assert
Assert.IsTrue(clt.DefaultRequestHeaders.Contains("X-Auth-Email"));
Assert.IsTrue(clt.DefaultRequestHeaders.Contains("X-Auth-Key"));
Assert.AreEqual(emailAddress, clt.DefaultRequestHeaders.GetValues("X-Auth-Email").First());
Assert.AreEqual(apiKey, clt.DefaultRequestHeaders.GetValues("X-Auth-Key").First());
}
[DataTestMethod]
[DataRow(null)]
[DataRow("")]
[DataRow(" ")]
[ExpectedException(typeof(ArgumentNullException))]
public void ShouldArgumentNullExceptionForEmailAddress(string emailAddress)
{
// Arrange
string apiKey = "some-api-key";
// Act
new ApiKeyAuthentication(emailAddress, apiKey);
// Assert - ArgumentNullException
}
[DataTestMethod]
[DataRow(null)]
[DataRow("")]
[DataRow(" ")]
[ExpectedException(typeof(ArgumentNullException))]
public void ShouldArgumentNullExceptionForApiKey(string apiKey)
{
// Arrange
string emailAddress = "test@example.com";
// Act
new ApiKeyAuthentication(emailAddress, apiKey);
// Assert - ArgumentNullException
}
[DataTestMethod]
[DataRow("test")]
[DataRow("test@example")]
[DataRow("example.com")]
[ExpectedException(typeof(ArgumentException))]
public void ShouldArgumentExceptionForInvalidEmailAddress(string emailAddress)
{
// Arrange
string apiKey = "some-api-key";
// Act
new ApiKeyAuthentication(emailAddress, apiKey);
// Assert - ArgumentException
}
}
}

View File

@@ -0,0 +1,44 @@
using System;
using System.Net.Http;
using AMWD.Net.Api.Cloudflare.Auth;
namespace Cloudflare.Core.Tests.Auth
{
[TestClass]
public class ApiTokenAuthenticationTest
{
[TestMethod]
public void ShouldAddHeader()
{
// Arrange
string apiToken = "some-api-token";
var auth = new ApiTokenAuthentication(apiToken);
using var clt = new HttpClient();
// Act
auth.AddHeader(clt);
// Assert
Assert.IsTrue(clt.DefaultRequestHeaders.Contains("Authorization"));
Assert.AreEqual("Bearer", clt.DefaultRequestHeaders.Authorization.Scheme);
Assert.AreEqual(apiToken, clt.DefaultRequestHeaders.Authorization.Parameter);
}
[DataTestMethod]
[DataRow(null)]
[DataRow("")]
[DataRow(" ")]
[ExpectedException(typeof(ArgumentNullException))]
public void ShouldArgumentNullExceptionForEmailAddress(string apiToken)
{
// Arrange
// Act
new ApiTokenAuthentication(apiToken);
// Assert - ArgumentNullException
}
}
}

View File

@@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<CollectCoverage>true</CollectCoverage>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.msbuild" Version="6.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="MSTest.TestAdapter" Version="3.6.1" />
<PackageReference Include="MSTest.TestFramework" Version="3.6.1" />
</ItemGroup>
<ItemGroup>
<Using Include="Microsoft.VisualStudio.TestTools.UnitTesting" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="$(SolutionDir)\Cloudflare\Cloudflare.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,251 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using AMWD.Net.Api.Cloudflare;
using AMWD.Net.Api.Cloudflare.Auth;
using Moq;
using Moq.Protected;
namespace Cloudflare.Core.Tests
{
[TestClass]
public class CloudflareClientTest
{
private Mock<HttpMessageHandler> _httpHandlerMock;
private Mock<ClientOptions> _clientOptionsMock;
private Mock<IAuthentication> _authenticationMock;
[TestInitialize]
public void Initialize()
{
_httpHandlerMock = new Mock<HttpMessageHandler>();
_authenticationMock = new Mock<IAuthentication>();
_clientOptionsMock = new Mock<ClientOptions>();
_clientOptionsMock.Setup(o => o.BaseUrl).Returns("http://localhost/api/v4/");
_clientOptionsMock.Setup(o => o.Timeout).Returns(TimeSpan.FromSeconds(60));
_clientOptionsMock.Setup(o => o.MaxRetries).Returns(2);
_clientOptionsMock.Setup(o => o.DefaultHeaders).Returns(new Dictionary<string, string>());
_clientOptionsMock.Setup(o => o.DefaultQueryParams).Returns(new Dictionary<string, string>());
_clientOptionsMock.Setup(o => o.AllowRedirects).Returns(false);
_clientOptionsMock.Setup(o => o.UseProxy).Returns(false);
}
[TestMethod]
public void ShouldInitializeWithEmailAndKey()
{
// Arrange
string email = "test@example.com";
string apiKey = "some-api-key";
// Act
using var client = new CloudflareClient(email, apiKey);
// Assert
var httpClient = client.GetType()
.GetField("_httpClient", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(client) as HttpClient;
Assert.IsNotNull(httpClient);
Assert.AreEqual(email, httpClient.DefaultRequestHeaders.GetValues("X-Auth-Email").First());
Assert.AreEqual(apiKey, httpClient.DefaultRequestHeaders.GetValues("X-Auth-Key").First());
VerifyNoOtherCalls();
}
[TestMethod]
public void ShouldInitializeWithApiToken()
{
// Arrange
string token = "some-special-api-token";
// Act
using var client = new CloudflareClient(token);
// Assert
var httpClient = client.GetType()
.GetField("_httpClient", BindingFlags.NonPublic | BindingFlags.Instance)
.GetValue(client) as HttpClient;
Assert.IsNotNull(httpClient);
Assert.AreEqual($"Bearer {token}", httpClient.DefaultRequestHeaders.GetValues("Authorization").First());
VerifyNoOtherCalls();
}
[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void ShouldThrowArgumentNullOnMissingAuthentication()
{
// Arrange
// Act
using var client = new CloudflareClient((IAuthentication)null);
// Assert - ArgumentNullException
}
[TestMethod]
public void ShouldAddCustomDefaultHeaders()
{
// Arrange
var clientOptions = new ClientOptions();
clientOptions.DefaultHeaders.Add("SomeKey", "SomeValue");
// Act
using var client = new CloudflareClient("token", clientOptions);
// Assert
var httpClient = client.GetType()
.GetField("_httpClient", BindingFlags.NonPublic | BindingFlags.Instance)
.GetValue(client) as HttpClient;
Assert.IsNotNull(httpClient);
Assert.IsTrue(httpClient.DefaultRequestHeaders.Contains("SomeKey"));
Assert.AreEqual("SomeValue", httpClient.DefaultRequestHeaders.GetValues("SomeKey").First());
VerifyNoOtherCalls();
}
[TestMethod]
public void ShouldDisposeHttpClient()
{
// Arrange
var client = GetClient();
// Act
client.Dispose();
// Assert
_httpHandlerMock.Protected().Verify("Dispose", Times.Once(), exactParameterMatch: true, true);
VerifyDefault();
VerifyNoOtherCalls();
}
[TestMethod]
public void ShouldAllowMultipleDispose()
{
// Arrange
var client = GetClient();
// Act
client.Dispose();
client.Dispose();
// Assert
_httpHandlerMock.Protected().Verify("Dispose", Times.Once(), exactParameterMatch: true, true);
VerifyDefault();
VerifyNoOtherCalls();
}
[TestMethod]
public void ShouldAssertClientOptions()
{
// Arrange + Act
var client = GetClient();
// Assert
VerifyDefault();
VerifyNoOtherCalls();
}
[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void ShouldThrowArgumentNullForBaseUrlOnAssertClientOptions()
{
// Arrange
_clientOptionsMock
.Setup(o => o.BaseUrl)
.Returns((string)null);
// Act
var client = GetClient();
// Assert - ArgumentNullException
}
[TestMethod]
[ExpectedException(typeof(ArgumentOutOfRangeException))]
public void ShouldThrowArgumentOutOfRangeForTimeoutOnAssertClientOptions()
{
// Arrange
_clientOptionsMock
.Setup(o => o.Timeout)
.Returns(TimeSpan.Zero);
// Act
var client = GetClient();
// Assert - ArgumentOutOfRangeException
}
[DataTestMethod]
[DataRow(-1)]
[DataRow(11)]
[ExpectedException(typeof(ArgumentOutOfRangeException))]
public void ShouldThrowArgumentOutOfRangeForMaxRetriesOnAssertClientOptions(int maxRetries)
{
// Arrange
_clientOptionsMock
.Setup(o => o.MaxRetries)
.Returns(maxRetries);
// Act
var client = GetClient();
// Assert - ArgumentOutOfRangeException
}
[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void ShouldThrowArgumentNullForUseProxyOnAssertClientOptions()
{
// Arrange
_clientOptionsMock
.Setup(o => o.UseProxy)
.Returns(true);
// Act
var client = GetClient();
// Assert - ArgumentNullException
}
private void VerifyDefault()
{
_clientOptionsMock.VerifyGet(o => o.AllowRedirects, Times.Once);
_clientOptionsMock.VerifyGet(o => o.BaseUrl, Times.Exactly(2));
_clientOptionsMock.VerifyGet(o => o.DefaultHeaders, Times.Once);
_clientOptionsMock.VerifyGet(o => o.MaxRetries, Times.Exactly(2));
_clientOptionsMock.VerifyGet(o => o.Proxy, Times.Once);
_clientOptionsMock.VerifyGet(o => o.Timeout, Times.Exactly(2));
_clientOptionsMock.VerifyGet(o => o.UseProxy, Times.Exactly(2));
_authenticationMock.Verify(a => a.AddHeader(It.IsAny<HttpClient>()), Times.Once);
}
private void VerifyNoOtherCalls()
{
_httpHandlerMock.VerifyNoOtherCalls();
_clientOptionsMock.VerifyNoOtherCalls();
_authenticationMock.VerifyNoOtherCalls();
}
private CloudflareClient GetClient()
{
var httpClient = new HttpClient(_httpHandlerMock.Object);
var client = new CloudflareClient(_authenticationMock.Object, _clientOptionsMock.Object);
var httpClientField = client.GetType()
.GetField("_httpClient", BindingFlags.NonPublic | BindingFlags.Instance);
(httpClientField.GetValue(client) as HttpClient).Dispose();
httpClientField.SetValue(client, httpClient);
return client;
}
}
}

View File

@@ -0,0 +1,326 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Mime;
using System.Reflection;
using System.Security.Authentication;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using AMWD.Net.Api.Cloudflare;
using AMWD.Net.Api.Cloudflare.Auth;
using Moq;
using Moq.Protected;
namespace Cloudflare.Core.Tests.CloudflareClientTests
{
[TestClass]
public class DeleteAsyncTest
{
private const string _baseUrl = "http://localhost/api/v4/";
private HttpMessageHandlerMock _httpHandlerMock;
private Mock<ClientOptions> _clientOptionsMock;
private Mock<IAuthentication> _authenticationMock;
[TestInitialize]
public void Initialize()
{
_httpHandlerMock = new HttpMessageHandlerMock();
_authenticationMock = new Mock<IAuthentication>();
_clientOptionsMock = new Mock<ClientOptions>();
_authenticationMock
.Setup(a => a.AddHeader(It.IsAny<HttpClient>()))
.Callback<HttpClient>(c => c.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "Some-API-Token"));
_clientOptionsMock.Setup(o => o.BaseUrl).Returns(_baseUrl);
_clientOptionsMock.Setup(o => o.Timeout).Returns(TimeSpan.FromSeconds(60));
_clientOptionsMock.Setup(o => o.MaxRetries).Returns(2);
_clientOptionsMock.Setup(o => o.DefaultHeaders).Returns(new Dictionary<string, string>());
_clientOptionsMock.Setup(o => o.DefaultQueryParams).Returns(new Dictionary<string, string>());
_clientOptionsMock.Setup(o => o.AllowRedirects).Returns(false);
_clientOptionsMock.Setup(o => o.UseProxy).Returns(false);
}
[TestMethod]
[ExpectedException(typeof(ObjectDisposedException))]
public async Task ShouldThrowDisposed()
{
// Arrange
var client = GetClient() as CloudflareClient;
client.Dispose();
// Act
await client.DeleteAsync<object>("test");
// Assert - ObjectDisposedException
}
[DataTestMethod]
[DataRow(null)]
[DataRow("")]
[DataRow(" ")]
[ExpectedException(typeof(ArgumentNullException))]
public async Task ShouldThrowArgumentNullOnRequestPath(string path)
{
// Arrange
var client = GetClient();
// Act
await client.DeleteAsync<object>(path);
// Assert - ArgumentNullException
}
[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public async Task ShouldThrowArgumentOnRequestPath()
{
// Arrange
var client = GetClient();
// Act
await client.DeleteAsync<object>("foo?bar=baz");
// Assert - ArgumentException
}
[TestMethod]
public async Task ShouldDelete()
{
// Arrange
_httpHandlerMock.Responses.Enqueue(new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent(@"{""success"": true, ""errors"": [], ""messages"": [], ""result"": { ""string"": ""some-string"", ""integer"": 123 }}", Encoding.UTF8, MediaTypeNames.Application.Json),
});
var client = GetClient();
// Act
var response = await client.DeleteAsync<TestClass>("test");
// Assert
Assert.IsNotNull(response);
Assert.IsTrue(response.Success);
Assert.IsNotNull(response.Errors);
Assert.IsNotNull(response.Messages);
Assert.IsNull(response.ResultInfo);
Assert.AreEqual(0, response.Errors.Count);
Assert.AreEqual(0, response.Messages.Count);
Assert.IsNotNull(response.Result);
Assert.AreEqual("some-string", response.Result.Str);
Assert.AreEqual(123, response.Result.Int);
Assert.AreEqual(1, _httpHandlerMock.Callbacks.Count);
var callback = _httpHandlerMock.Callbacks.First();
Assert.AreEqual(HttpMethod.Delete, callback.Method);
Assert.AreEqual("http://localhost/api/v4/test", callback.Url);
Assert.IsNull(callback.Content);
Assert.AreEqual(3, callback.Headers.Count);
Assert.IsTrue(callback.Headers.ContainsKey("Accept"));
Assert.IsTrue(callback.Headers.ContainsKey("Authorization"));
Assert.IsTrue(callback.Headers.ContainsKey("User-Agent"));
Assert.AreEqual("application/json", callback.Headers["Accept"]);
Assert.AreEqual("Bearer Some-API-Token", callback.Headers["Authorization"]);
Assert.AreEqual("AMWD.CloudflareClient/1.0.0", callback.Headers["User-Agent"]);
_httpHandlerMock.Mock.Protected().Verify("SendAsync", Times.Once(), ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>());
VerifyDefaults();
VerifyNoOtherCalls();
}
[DataTestMethod]
[DataRow(HttpStatusCode.Unauthorized)]
[DataRow(HttpStatusCode.Forbidden)]
public async Task ShouldThrowAuthenticationExceptionOnStatusCode(HttpStatusCode statusCode)
{
// Arrange
_httpHandlerMock.Responses.Enqueue(new HttpResponseMessage
{
StatusCode = statusCode,
Content = new StringContent(@"{""success"": false, ""errors"": [{ ""code"": ""4711"", ""message"": ""foo & baz."" }, { ""code"": ""4712"", ""message"": ""Happy Error!"" }], ""messages"": []}", Encoding.UTF8, MediaTypeNames.Application.Json),
});
var client = GetClient();
try
{
// Act
await client.DeleteAsync<TestClass>("foo");
Assert.Fail();
}
catch (AuthenticationException ex)
{
// Assert
Assert.IsNull(ex.InnerException);
Assert.AreEqual($"4711: foo & baz.{Environment.NewLine}4712: Happy Error!", ex.Message);
}
}
[TestMethod]
public async Task ShouldReturnPlainText()
{
// Arrange
_clientOptionsMock.Setup(o => o.DefaultQueryParams).Returns(new Dictionary<string, string> { { "bar", "08/15" } });
_httpHandlerMock.Responses.Enqueue(new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent("This is an awesome text ;-)", Encoding.UTF8, MediaTypeNames.Text.Plain),
});
var client = GetClient();
// Act
var response = await client.DeleteAsync<string>("some-awesome-path");
// Assert
Assert.IsNotNull(response);
Assert.IsTrue(response.Success);
Assert.IsNotNull(response.Errors);
Assert.IsNotNull(response.Messages);
Assert.IsNotNull(response.ResultInfo);
Assert.AreEqual(0, response.Errors.Count);
Assert.AreEqual(0, response.Messages.Count);
Assert.AreEqual(0, response.ResultInfo.Count);
Assert.AreEqual(0, response.ResultInfo.Page);
Assert.AreEqual(0, response.ResultInfo.PerPage);
Assert.AreEqual(0, response.ResultInfo.TotalCount);
Assert.AreEqual("This is an awesome text ;-)", response.Result);
Assert.AreEqual(1, _httpHandlerMock.Callbacks.Count);
var callback = _httpHandlerMock.Callbacks.First();
Assert.AreEqual(HttpMethod.Delete, callback.Method);
Assert.AreEqual("http://localhost/api/v4/some-awesome-path?bar=08%2F15", callback.Url);
Assert.IsNull(callback.Content);
Assert.AreEqual(3, callback.Headers.Count);
Assert.IsTrue(callback.Headers.ContainsKey("Accept"));
Assert.IsTrue(callback.Headers.ContainsKey("Authorization"));
Assert.IsTrue(callback.Headers.ContainsKey("User-Agent"));
Assert.AreEqual("application/json", callback.Headers["Accept"]);
Assert.AreEqual("Bearer Some-API-Token", callback.Headers["Authorization"]);
Assert.AreEqual("AMWD.CloudflareClient/1.0.0", callback.Headers["User-Agent"]);
_httpHandlerMock.Mock.Protected().Verify("SendAsync", Times.Once(), ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>());
_authenticationMock.Verify(m => m.AddHeader(It.IsAny<HttpClient>()), Times.Once);
_clientOptionsMock.Verify(o => o.BaseUrl, Times.Exactly(2));
_clientOptionsMock.Verify(o => o.Timeout, Times.Exactly(2));
_clientOptionsMock.Verify(o => o.MaxRetries, Times.Exactly(2));
_clientOptionsMock.Verify(o => o.DefaultHeaders, Times.Once);
_clientOptionsMock.Verify(o => o.DefaultQueryParams, Times.Exactly(2));
_clientOptionsMock.Verify(o => o.AllowRedirects, Times.Once);
_clientOptionsMock.Verify(o => o.UseProxy, Times.Exactly(2));
_clientOptionsMock.Verify(o => o.Proxy, Times.Once);
VerifyNoOtherCalls();
}
[TestMethod]
[ExpectedException(typeof(JsonReaderException))]
public async Task ShouldThrowExceptionOnInvalidResponse()
{
// Arrange
_httpHandlerMock.Responses.Enqueue(new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent("This is a bad text :p", Encoding.UTF8, MediaTypeNames.Text.Plain),
});
var client = GetClient();
// Act
await client.DeleteAsync<TestClass>("some-path");
}
private void VerifyDefaults()
{
_authenticationMock.Verify(m => m.AddHeader(It.IsAny<HttpClient>()), Times.Once);
_clientOptionsMock.Verify(o => o.BaseUrl, Times.Exactly(2));
_clientOptionsMock.Verify(o => o.Timeout, Times.Exactly(2));
_clientOptionsMock.Verify(o => o.MaxRetries, Times.Exactly(2));
_clientOptionsMock.Verify(o => o.DefaultHeaders, Times.Once);
_clientOptionsMock.Verify(o => o.DefaultQueryParams, Times.Once);
_clientOptionsMock.Verify(o => o.AllowRedirects, Times.Once);
_clientOptionsMock.Verify(o => o.UseProxy, Times.Exactly(2));
_clientOptionsMock.Verify(o => o.Proxy, Times.Once);
}
private void VerifyNoOtherCalls()
{
_httpHandlerMock.Mock.VerifyNoOtherCalls();
_authenticationMock.VerifyNoOtherCalls();
_clientOptionsMock.VerifyNoOtherCalls();
}
private ICloudflareClient GetClient()
{
var httpClient = new HttpClient(_httpHandlerMock.Mock.Object)
{
BaseAddress = new Uri(_baseUrl),
Timeout = _clientOptionsMock.Object.Timeout,
};
httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("AMWD.CloudflareClient", "1.0.0"));
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
if (_clientOptionsMock.Object.DefaultHeaders.Count > 0)
{
foreach (var headerKvp in _clientOptionsMock.Object.DefaultHeaders)
httpClient.DefaultRequestHeaders.Add(headerKvp.Key, headerKvp.Value);
}
_authenticationMock.Object.AddHeader(httpClient);
_authenticationMock.Invocations.Clear();
_clientOptionsMock.Invocations.Clear();
var client = new CloudflareClient(_authenticationMock.Object, _clientOptionsMock.Object);
var httpClientField = client.GetType()
.GetField("_httpClient", BindingFlags.NonPublic | BindingFlags.Instance);
(httpClientField.GetValue(client) as HttpClient).Dispose();
httpClientField.SetValue(client, httpClient);
return client;
}
private class TestClass
{
[JsonProperty("string")]
public string Str { get; set; }
[JsonProperty("integer")]
public int Int { get; set; }
}
private class TestFilter : IQueryParameterFilter
{
public IDictionary<string, string> GetQueryParameters()
{
return new Dictionary<string, string>
{
{ "test", "filter-text" }
};
}
}
}
}

View File

@@ -0,0 +1,326 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Mime;
using System.Reflection;
using System.Security.Authentication;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using AMWD.Net.Api.Cloudflare;
using AMWD.Net.Api.Cloudflare.Auth;
using Moq;
using Moq.Protected;
namespace Cloudflare.Core.Tests.CloudflareClientTests
{
[TestClass]
public class GetAsyncTest
{
private const string _baseUrl = "http://localhost/api/v4/";
private HttpMessageHandlerMock _httpHandlerMock;
private Mock<ClientOptions> _clientOptionsMock;
private Mock<IAuthentication> _authenticationMock;
[TestInitialize]
public void Initialize()
{
_httpHandlerMock = new HttpMessageHandlerMock();
_authenticationMock = new Mock<IAuthentication>();
_clientOptionsMock = new Mock<ClientOptions>();
_authenticationMock
.Setup(a => a.AddHeader(It.IsAny<HttpClient>()))
.Callback<HttpClient>(c => c.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "Some-API-Token"));
_clientOptionsMock.Setup(o => o.BaseUrl).Returns(_baseUrl);
_clientOptionsMock.Setup(o => o.Timeout).Returns(TimeSpan.FromSeconds(60));
_clientOptionsMock.Setup(o => o.MaxRetries).Returns(2);
_clientOptionsMock.Setup(o => o.DefaultHeaders).Returns(new Dictionary<string, string>());
_clientOptionsMock.Setup(o => o.DefaultQueryParams).Returns(new Dictionary<string, string>());
_clientOptionsMock.Setup(o => o.AllowRedirects).Returns(false);
_clientOptionsMock.Setup(o => o.UseProxy).Returns(false);
}
[TestMethod]
[ExpectedException(typeof(ObjectDisposedException))]
public async Task ShouldThrowDisposed()
{
// Arrange
var client = GetClient() as CloudflareClient;
client.Dispose();
// Act
await client.GetAsync<object>("/test");
// Assert - ObjectDisposedException
}
[DataTestMethod]
[DataRow(null)]
[DataRow("")]
[DataRow(" ")]
[ExpectedException(typeof(ArgumentNullException))]
public async Task ShouldThrowArgumentNullOnRequestPath(string path)
{
// Arrange
var client = GetClient();
// Act
await client.GetAsync<object>(path);
// Assert - ArgumentNullException
}
[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public async Task ShouldThrowArgumentOnRequestPath()
{
// Arrange
var client = GetClient();
// Act
await client.GetAsync<object>("/foo?bar=baz");
// Assert - ArgumentException
}
[TestMethod]
public async Task ShouldGet()
{
// Arrange
_httpHandlerMock.Responses.Enqueue(new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent(@"{""success"": true, ""errors"": [], ""messages"": [], ""result"": { ""string"": ""some-string"", ""integer"": 123 }}", Encoding.UTF8, MediaTypeNames.Application.Json),
});
var client = GetClient();
// Act
var response = await client.GetAsync<TestClass>("test");
// Assert
Assert.IsNotNull(response);
Assert.IsTrue(response.Success);
Assert.IsNotNull(response.Errors);
Assert.IsNotNull(response.Messages);
Assert.IsNull(response.ResultInfo);
Assert.AreEqual(0, response.Errors.Count);
Assert.AreEqual(0, response.Messages.Count);
Assert.IsNotNull(response.Result);
Assert.AreEqual("some-string", response.Result.Str);
Assert.AreEqual(123, response.Result.Int);
Assert.AreEqual(1, _httpHandlerMock.Callbacks.Count);
var callback = _httpHandlerMock.Callbacks.First();
Assert.AreEqual(HttpMethod.Get, callback.Method);
Assert.AreEqual("http://localhost/api/v4/test", callback.Url);
Assert.IsNull(callback.Content);
Assert.AreEqual(3, callback.Headers.Count);
Assert.IsTrue(callback.Headers.ContainsKey("Accept"));
Assert.IsTrue(callback.Headers.ContainsKey("Authorization"));
Assert.IsTrue(callback.Headers.ContainsKey("User-Agent"));
Assert.AreEqual("application/json", callback.Headers["Accept"]);
Assert.AreEqual("Bearer Some-API-Token", callback.Headers["Authorization"]);
Assert.AreEqual("AMWD.CloudflareClient/1.0.0", callback.Headers["User-Agent"]);
_httpHandlerMock.Mock.Protected().Verify("SendAsync", Times.Once(), ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>());
VerifyDefaults();
VerifyNoOtherCalls();
}
[DataTestMethod]
[DataRow(HttpStatusCode.Unauthorized)]
[DataRow(HttpStatusCode.Forbidden)]
public async Task ShouldThrowAuthenticationExceptionOnStatusCode(HttpStatusCode statusCode)
{
// Arrange
_httpHandlerMock.Responses.Enqueue(new HttpResponseMessage
{
StatusCode = statusCode,
Content = new StringContent(@"{""success"": false, ""errors"": [{ ""code"": ""4711"", ""message"": ""foo & baz."" }, { ""code"": ""4712"", ""message"": ""Happy Error!"" }], ""messages"": []}", Encoding.UTF8, MediaTypeNames.Application.Json),
});
var client = GetClient();
try
{
// Act
await client.GetAsync<TestClass>("foo");
Assert.Fail();
}
catch (AuthenticationException ex)
{
// Assert
Assert.IsNull(ex.InnerException);
Assert.AreEqual($"4711: foo & baz.{Environment.NewLine}4712: Happy Error!", ex.Message);
}
}
[TestMethod]
public async Task ShouldReturnPlainText()
{
// Arrange
_clientOptionsMock.Setup(o => o.DefaultQueryParams).Returns(new Dictionary<string, string> { { "bar", "08/15" } });
_httpHandlerMock.Responses.Enqueue(new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent("This is an awesome text ;-)", Encoding.UTF8, MediaTypeNames.Text.Plain),
});
var client = GetClient();
// Act
var response = await client.GetAsync<string>("some-awesome-path", new TestFilter());
// Assert
Assert.IsNotNull(response);
Assert.IsTrue(response.Success);
Assert.IsNotNull(response.Errors);
Assert.IsNotNull(response.Messages);
Assert.IsNotNull(response.ResultInfo);
Assert.AreEqual(0, response.Errors.Count);
Assert.AreEqual(0, response.Messages.Count);
Assert.AreEqual(0, response.ResultInfo.Count);
Assert.AreEqual(0, response.ResultInfo.Page);
Assert.AreEqual(0, response.ResultInfo.PerPage);
Assert.AreEqual(0, response.ResultInfo.TotalCount);
Assert.AreEqual("This is an awesome text ;-)", response.Result);
Assert.AreEqual(1, _httpHandlerMock.Callbacks.Count);
var callback = _httpHandlerMock.Callbacks.First();
Assert.AreEqual(HttpMethod.Get, callback.Method);
Assert.AreEqual("http://localhost/api/v4/some-awesome-path?bar=08%2F15&test=filter-text", callback.Url);
Assert.IsNull(callback.Content);
Assert.AreEqual(3, callback.Headers.Count);
Assert.IsTrue(callback.Headers.ContainsKey("Accept"));
Assert.IsTrue(callback.Headers.ContainsKey("Authorization"));
Assert.IsTrue(callback.Headers.ContainsKey("User-Agent"));
Assert.AreEqual("application/json", callback.Headers["Accept"]);
Assert.AreEqual("Bearer Some-API-Token", callback.Headers["Authorization"]);
Assert.AreEqual("AMWD.CloudflareClient/1.0.0", callback.Headers["User-Agent"]);
_httpHandlerMock.Mock.Protected().Verify("SendAsync", Times.Once(), ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>());
_authenticationMock.Verify(m => m.AddHeader(It.IsAny<HttpClient>()), Times.Once);
_clientOptionsMock.Verify(o => o.BaseUrl, Times.Exactly(2));
_clientOptionsMock.Verify(o => o.Timeout, Times.Exactly(2));
_clientOptionsMock.Verify(o => o.MaxRetries, Times.Exactly(2));
_clientOptionsMock.Verify(o => o.DefaultHeaders, Times.Once);
_clientOptionsMock.Verify(o => o.DefaultQueryParams, Times.Exactly(2));
_clientOptionsMock.Verify(o => o.AllowRedirects, Times.Once);
_clientOptionsMock.Verify(o => o.UseProxy, Times.Exactly(2));
_clientOptionsMock.Verify(o => o.Proxy, Times.Once);
VerifyNoOtherCalls();
}
[TestMethod]
[ExpectedException(typeof(JsonReaderException))]
public async Task ShouldThrowExceptionOnInvalidResponse()
{
// Arrange
_httpHandlerMock.Responses.Enqueue(new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent("This is a bad text :p", Encoding.UTF8, MediaTypeNames.Text.Plain),
});
var client = GetClient();
// Act
await client.GetAsync<TestClass>("some-path");
}
private void VerifyDefaults()
{
_authenticationMock.Verify(m => m.AddHeader(It.IsAny<HttpClient>()), Times.Once);
_clientOptionsMock.Verify(o => o.BaseUrl, Times.Exactly(2));
_clientOptionsMock.Verify(o => o.Timeout, Times.Exactly(2));
_clientOptionsMock.Verify(o => o.MaxRetries, Times.Exactly(2));
_clientOptionsMock.Verify(o => o.DefaultHeaders, Times.Once);
_clientOptionsMock.Verify(o => o.DefaultQueryParams, Times.Once);
_clientOptionsMock.Verify(o => o.AllowRedirects, Times.Once);
_clientOptionsMock.Verify(o => o.UseProxy, Times.Exactly(2));
_clientOptionsMock.Verify(o => o.Proxy, Times.Once);
}
private void VerifyNoOtherCalls()
{
_httpHandlerMock.Mock.VerifyNoOtherCalls();
_authenticationMock.VerifyNoOtherCalls();
_clientOptionsMock.VerifyNoOtherCalls();
}
private ICloudflareClient GetClient()
{
var httpClient = new HttpClient(_httpHandlerMock.Mock.Object)
{
BaseAddress = new Uri(_baseUrl),
Timeout = _clientOptionsMock.Object.Timeout,
};
httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("AMWD.CloudflareClient", "1.0.0"));
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
if (_clientOptionsMock.Object.DefaultHeaders.Count > 0)
{
foreach (var headerKvp in _clientOptionsMock.Object.DefaultHeaders)
httpClient.DefaultRequestHeaders.Add(headerKvp.Key, headerKvp.Value);
}
_authenticationMock.Object.AddHeader(httpClient);
_authenticationMock.Invocations.Clear();
_clientOptionsMock.Invocations.Clear();
var client = new CloudflareClient(_authenticationMock.Object, _clientOptionsMock.Object);
var httpClientField = client.GetType()
.GetField("_httpClient", BindingFlags.NonPublic | BindingFlags.Instance);
(httpClientField.GetValue(client) as HttpClient).Dispose();
httpClientField.SetValue(client, httpClient);
return client;
}
private class TestClass
{
[JsonProperty("string")]
public string Str { get; set; }
[JsonProperty("integer")]
public int Int { get; set; }
}
private class TestFilter : IQueryParameterFilter
{
public IDictionary<string, string> GetQueryParameters()
{
return new Dictionary<string, string>
{
{ "test", "filter-text" }
};
}
}
}
}

View File

@@ -0,0 +1,386 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Mime;
using System.Reflection;
using System.Security.Authentication;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using AMWD.Net.Api.Cloudflare;
using AMWD.Net.Api.Cloudflare.Auth;
using Moq;
using Moq.Protected;
namespace Cloudflare.Core.Tests.CloudflareClientTests
{
[TestClass]
public class PatchAsyncTest
{
private const string _baseUrl = "https://localhost/api/v4/";
private HttpMessageHandlerMock _httpHandlerMock;
private Mock<ClientOptions> _clientOptionsMock;
private Mock<IAuthentication> _authenticationMock;
private TestClass _request;
[TestInitialize]
public void Initialize()
{
_httpHandlerMock = new HttpMessageHandlerMock();
_authenticationMock = new Mock<IAuthentication>();
_clientOptionsMock = new Mock<ClientOptions>();
_authenticationMock
.Setup(a => a.AddHeader(It.IsAny<HttpClient>()))
.Callback<HttpClient>(c => c.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "Some-API-Token"));
_clientOptionsMock.Setup(o => o.BaseUrl).Returns(_baseUrl);
_clientOptionsMock.Setup(o => o.Timeout).Returns(TimeSpan.FromSeconds(60));
_clientOptionsMock.Setup(o => o.MaxRetries).Returns(2);
_clientOptionsMock.Setup(o => o.DefaultHeaders).Returns(new Dictionary<string, string>());
_clientOptionsMock.Setup(o => o.DefaultQueryParams).Returns(new Dictionary<string, string>());
_clientOptionsMock.Setup(o => o.AllowRedirects).Returns(false);
_clientOptionsMock.Setup(o => o.UseProxy).Returns(false);
_request = new TestClass
{
Int = 54321,
Str = "Happy Testing!"
};
}
[TestMethod]
[ExpectedException(typeof(ObjectDisposedException))]
public async Task ShouldThrowDisposed()
{
// Arrange
var client = GetClient() as CloudflareClient;
client.Dispose();
// Act
await client.PatchAsync<object, object>("test", _request);
// Assert - ObjectDisposedException
}
[DataTestMethod]
[DataRow(null)]
[DataRow("")]
[DataRow(" ")]
[ExpectedException(typeof(ArgumentNullException))]
public async Task ShouldThrowArgumentNullOnRequestPath(string path)
{
// Arrange
var client = GetClient();
// Act
await client.PatchAsync<object, object>(path, _request);
// Assert - ArgumentNullException
}
[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public async Task ShouldThrowArgumentOnRequestPath()
{
// Arrange
var client = GetClient();
// Act
await client.PatchAsync<object, object>("foo?bar=baz", _request);
// Assert - ArgumentException
}
[TestMethod]
public async Task ShouldPatch()
{
// Arrange
_httpHandlerMock.Responses.Enqueue(new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent(@"{""success"": true, ""errors"": [], ""messages"": [], ""result"": { ""string"": ""some-string"", ""integer"": 123 }}", Encoding.UTF8, MediaTypeNames.Application.Json),
});
var client = GetClient();
// Act
var response = await client.PatchAsync<TestClass, TestClass>("test", _request);
// Assert
Assert.IsNotNull(response);
Assert.IsTrue(response.Success);
Assert.IsNotNull(response.Errors);
Assert.IsNotNull(response.Messages);
Assert.IsNull(response.ResultInfo);
Assert.AreEqual(0, response.Errors.Count);
Assert.AreEqual(0, response.Messages.Count);
Assert.IsNotNull(response.Result);
Assert.AreEqual("some-string", response.Result.Str);
Assert.AreEqual(123, response.Result.Int);
Assert.AreEqual(1, _httpHandlerMock.Callbacks.Count);
var callback = _httpHandlerMock.Callbacks.First();
Assert.AreEqual(HttpMethod.Patch, callback.Method);
Assert.AreEqual("https://localhost/api/v4/test", callback.Url);
Assert.AreEqual(@"{""string"":""Happy Testing!"",""integer"":54321}", callback.Content);
Assert.AreEqual(3, callback.Headers.Count);
Assert.IsTrue(callback.Headers.ContainsKey("Accept"));
Assert.IsTrue(callback.Headers.ContainsKey("Authorization"));
Assert.IsTrue(callback.Headers.ContainsKey("User-Agent"));
Assert.AreEqual("application/json", callback.Headers["Accept"]);
Assert.AreEqual("Bearer Some-API-Token", callback.Headers["Authorization"]);
Assert.AreEqual("AMWD.CloudflareClient/1.0.0", callback.Headers["User-Agent"]);
_httpHandlerMock.Mock.Protected().Verify("SendAsync", Times.Once(), ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>());
VerifyDefaults();
VerifyNoOtherCalls();
}
[TestMethod]
public async Task ShouldPatchHttpContentDirectly()
{
// Arrange
var stringContent = new StringContent(@"{""test"":""HERE ?""}", Encoding.UTF8, "application/json");
_httpHandlerMock.Responses.Enqueue(new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent(@"{""success"": true, ""errors"": [], ""messages"": [], ""result"": { ""string"": ""some-string"", ""integer"": 123 }}", Encoding.UTF8, MediaTypeNames.Application.Json),
});
var client = GetClient();
// Act
var response = await client.PatchAsync<TestClass, StringContent>("test", stringContent);
// Assert
Assert.IsNotNull(response);
Assert.IsTrue(response.Success);
Assert.IsNotNull(response.Errors);
Assert.IsNotNull(response.Messages);
Assert.IsNull(response.ResultInfo);
Assert.AreEqual(0, response.Errors.Count);
Assert.AreEqual(0, response.Messages.Count);
Assert.IsNotNull(response.Result);
Assert.AreEqual("some-string", response.Result.Str);
Assert.AreEqual(123, response.Result.Int);
Assert.AreEqual(1, _httpHandlerMock.Callbacks.Count);
var callback = _httpHandlerMock.Callbacks.First();
Assert.AreEqual(HttpMethod.Patch, callback.Method);
Assert.AreEqual("https://localhost/api/v4/test", callback.Url);
Assert.AreEqual(@"{""test"":""HERE ?""}", callback.Content);
Assert.AreEqual(3, callback.Headers.Count);
Assert.IsTrue(callback.Headers.ContainsKey("Accept"));
Assert.IsTrue(callback.Headers.ContainsKey("Authorization"));
Assert.IsTrue(callback.Headers.ContainsKey("User-Agent"));
Assert.AreEqual("application/json", callback.Headers["Accept"]);
Assert.AreEqual("Bearer Some-API-Token", callback.Headers["Authorization"]);
Assert.AreEqual("AMWD.CloudflareClient/1.0.0", callback.Headers["User-Agent"]);
_httpHandlerMock.Mock.Protected().Verify("SendAsync", Times.Once(), ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>());
VerifyDefaults();
VerifyNoOtherCalls();
}
[DataTestMethod]
[DataRow(HttpStatusCode.Unauthorized)]
[DataRow(HttpStatusCode.Forbidden)]
public async Task ShouldThrowAuthenticationExceptionOnStatusCode(HttpStatusCode statusCode)
{
// Arrange
_httpHandlerMock.Responses.Enqueue(new HttpResponseMessage
{
StatusCode = statusCode,
Content = new StringContent(@"{""success"": false, ""errors"": [{ ""code"": ""4711"", ""message"": ""foo & baz."" }, { ""code"": ""4712"", ""message"": ""Happy Error!"" }], ""messages"": []}", Encoding.UTF8, MediaTypeNames.Application.Json),
});
var client = GetClient();
try
{
// Act
await client.PatchAsync<object, object>("foo", _request);
Assert.Fail();
}
catch (AuthenticationException ex)
{
// Assert
Assert.IsNull(ex.InnerException);
Assert.AreEqual($"4711: foo & baz.{Environment.NewLine}4712: Happy Error!", ex.Message);
}
}
[TestMethod]
public async Task ShouldReturnPlainText()
{
// Arrange
_clientOptionsMock.Setup(o => o.DefaultQueryParams).Returns(new Dictionary<string, string> { { "bar", "08/15" } });
_httpHandlerMock.Responses.Enqueue(new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent("This is an awesome text ;-)", Encoding.UTF8, MediaTypeNames.Text.Plain),
});
var client = GetClient();
// Act
var response = await client.PatchAsync<string, TestClass>("some-awesome-path", _request);
// Assert
Assert.IsNotNull(response);
Assert.IsTrue(response.Success);
Assert.IsNotNull(response.Errors);
Assert.IsNotNull(response.Messages);
Assert.IsNotNull(response.ResultInfo);
Assert.AreEqual(0, response.Errors.Count);
Assert.AreEqual(0, response.Messages.Count);
Assert.AreEqual(0, response.ResultInfo.Count);
Assert.AreEqual(0, response.ResultInfo.Page);
Assert.AreEqual(0, response.ResultInfo.PerPage);
Assert.AreEqual(0, response.ResultInfo.TotalCount);
Assert.AreEqual("This is an awesome text ;-)", response.Result);
Assert.AreEqual(1, _httpHandlerMock.Callbacks.Count);
var callback = _httpHandlerMock.Callbacks.First();
Assert.AreEqual(HttpMethod.Patch, callback.Method);
Assert.AreEqual("https://localhost/api/v4/some-awesome-path?bar=08%2F15", callback.Url);
Assert.AreEqual(@"{""string"":""Happy Testing!"",""integer"":54321}", callback.Content);
Assert.AreEqual(3, callback.Headers.Count);
Assert.IsTrue(callback.Headers.ContainsKey("Accept"));
Assert.IsTrue(callback.Headers.ContainsKey("Authorization"));
Assert.IsTrue(callback.Headers.ContainsKey("User-Agent"));
Assert.AreEqual("application/json", callback.Headers["Accept"]);
Assert.AreEqual("Bearer Some-API-Token", callback.Headers["Authorization"]);
Assert.AreEqual("AMWD.CloudflareClient/1.0.0", callback.Headers["User-Agent"]);
_httpHandlerMock.Mock.Protected().Verify("SendAsync", Times.Once(), ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>());
_authenticationMock.Verify(m => m.AddHeader(It.IsAny<HttpClient>()), Times.Once);
_clientOptionsMock.Verify(o => o.BaseUrl, Times.Exactly(2));
_clientOptionsMock.Verify(o => o.Timeout, Times.Exactly(2));
_clientOptionsMock.Verify(o => o.MaxRetries, Times.Exactly(2));
_clientOptionsMock.Verify(o => o.DefaultHeaders, Times.Once);
_clientOptionsMock.Verify(o => o.DefaultQueryParams, Times.Exactly(2));
_clientOptionsMock.Verify(o => o.AllowRedirects, Times.Once);
_clientOptionsMock.Verify(o => o.UseProxy, Times.Exactly(2));
_clientOptionsMock.Verify(o => o.Proxy, Times.Once);
VerifyNoOtherCalls();
}
[TestMethod]
[ExpectedException(typeof(JsonReaderException))]
public async Task ShouldThrowExceptionOnInvalidResponse()
{
// Arrange
_httpHandlerMock.Responses.Enqueue(new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent("This is a bad text :p", Encoding.UTF8, MediaTypeNames.Text.Plain),
});
var client = GetClient();
// Act
await client.PatchAsync<TestClass, TestClass>("some-path", _request);
}
private void VerifyDefaults()
{
_authenticationMock.Verify(m => m.AddHeader(It.IsAny<HttpClient>()), Times.Once);
_clientOptionsMock.Verify(o => o.BaseUrl, Times.Exactly(2));
_clientOptionsMock.Verify(o => o.Timeout, Times.Exactly(2));
_clientOptionsMock.Verify(o => o.MaxRetries, Times.Exactly(2));
_clientOptionsMock.Verify(o => o.DefaultHeaders, Times.Once);
_clientOptionsMock.Verify(o => o.DefaultQueryParams, Times.Once);
_clientOptionsMock.Verify(o => o.AllowRedirects, Times.Once);
_clientOptionsMock.Verify(o => o.UseProxy, Times.Exactly(2));
_clientOptionsMock.Verify(o => o.Proxy, Times.Once);
}
private void VerifyNoOtherCalls()
{
_httpHandlerMock.Mock.VerifyNoOtherCalls();
_authenticationMock.VerifyNoOtherCalls();
_clientOptionsMock.VerifyNoOtherCalls();
}
private ICloudflareClient GetClient()
{
var httpClient = new HttpClient(_httpHandlerMock.Mock.Object)
{
BaseAddress = new Uri(_baseUrl),
Timeout = _clientOptionsMock.Object.Timeout,
};
httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("AMWD.CloudflareClient", "1.0.0"));
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
if (_clientOptionsMock.Object.DefaultHeaders.Count > 0)
{
foreach (var headerKvp in _clientOptionsMock.Object.DefaultHeaders)
httpClient.DefaultRequestHeaders.Add(headerKvp.Key, headerKvp.Value);
}
_authenticationMock.Object.AddHeader(httpClient);
_authenticationMock.Invocations.Clear();
_clientOptionsMock.Invocations.Clear();
var client = new CloudflareClient(_authenticationMock.Object, _clientOptionsMock.Object);
var httpClientField = client.GetType()
.GetField("_httpClient", BindingFlags.NonPublic | BindingFlags.Instance);
(httpClientField.GetValue(client) as HttpClient).Dispose();
httpClientField.SetValue(client, httpClient);
return client;
}
private class TestClass
{
[JsonProperty("string")]
public string Str { get; set; }
[JsonProperty("integer")]
public int Int { get; set; }
}
private class TestFilter : IQueryParameterFilter
{
public IDictionary<string, string> GetQueryParameters()
{
return new Dictionary<string, string>
{
{ "test", "filter-text" }
};
}
}
}
}

View File

@@ -0,0 +1,386 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Mime;
using System.Reflection;
using System.Security.Authentication;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using AMWD.Net.Api.Cloudflare;
using AMWD.Net.Api.Cloudflare.Auth;
using Moq;
using Moq.Protected;
namespace Cloudflare.Core.Tests.CloudflareClientTests
{
[TestClass]
public class PostAsyncTest
{
private const string _baseUrl = "https://localhost/api/v4/";
private HttpMessageHandlerMock _httpHandlerMock;
private Mock<ClientOptions> _clientOptionsMock;
private Mock<IAuthentication> _authenticationMock;
private TestClass _request;
[TestInitialize]
public void Initialize()
{
_httpHandlerMock = new HttpMessageHandlerMock();
_authenticationMock = new Mock<IAuthentication>();
_clientOptionsMock = new Mock<ClientOptions>();
_authenticationMock
.Setup(a => a.AddHeader(It.IsAny<HttpClient>()))
.Callback<HttpClient>(c => c.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "Some-API-Token"));
_clientOptionsMock.Setup(o => o.BaseUrl).Returns(_baseUrl);
_clientOptionsMock.Setup(o => o.Timeout).Returns(TimeSpan.FromSeconds(60));
_clientOptionsMock.Setup(o => o.MaxRetries).Returns(2);
_clientOptionsMock.Setup(o => o.DefaultHeaders).Returns(new Dictionary<string, string>());
_clientOptionsMock.Setup(o => o.DefaultQueryParams).Returns(new Dictionary<string, string>());
_clientOptionsMock.Setup(o => o.AllowRedirects).Returns(false);
_clientOptionsMock.Setup(o => o.UseProxy).Returns(false);
_request = new TestClass
{
Int = 54321,
Str = "Happy Testing!"
};
}
[TestMethod]
[ExpectedException(typeof(ObjectDisposedException))]
public async Task ShouldThrowDisposed()
{
// Arrange
var client = GetClient() as CloudflareClient;
client.Dispose();
// Act
await client.PostAsync<object, object>("test", _request);
// Assert - ObjectDisposedException
}
[DataTestMethod]
[DataRow(null)]
[DataRow("")]
[DataRow(" ")]
[ExpectedException(typeof(ArgumentNullException))]
public async Task ShouldThrowArgumentNullOnRequestPath(string path)
{
// Arrange
var client = GetClient();
// Act
await client.PostAsync<object, object>(path, _request);
// Assert - ArgumentNullException
}
[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public async Task ShouldThrowArgumentOnRequestPath()
{
// Arrange
var client = GetClient();
// Act
await client.PostAsync<object, object>("foo?bar=baz", _request);
// Assert - ArgumentException
}
[TestMethod]
public async Task ShouldPost()
{
// Arrange
_httpHandlerMock.Responses.Enqueue(new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent(@"{""success"": true, ""errors"": [], ""messages"": [], ""result"": { ""string"": ""some-string"", ""integer"": 123 }}", Encoding.UTF8, MediaTypeNames.Application.Json),
});
var client = GetClient();
// Act
var response = await client.PostAsync<TestClass, TestClass>("test", _request);
// Assert
Assert.IsNotNull(response);
Assert.IsTrue(response.Success);
Assert.IsNotNull(response.Errors);
Assert.IsNotNull(response.Messages);
Assert.IsNull(response.ResultInfo);
Assert.AreEqual(0, response.Errors.Count);
Assert.AreEqual(0, response.Messages.Count);
Assert.IsNotNull(response.Result);
Assert.AreEqual("some-string", response.Result.Str);
Assert.AreEqual(123, response.Result.Int);
Assert.AreEqual(1, _httpHandlerMock.Callbacks.Count);
var callback = _httpHandlerMock.Callbacks.First();
Assert.AreEqual(HttpMethod.Post, callback.Method);
Assert.AreEqual("https://localhost/api/v4/test", callback.Url);
Assert.AreEqual(@"{""string"":""Happy Testing!"",""integer"":54321}", callback.Content);
Assert.AreEqual(3, callback.Headers.Count);
Assert.IsTrue(callback.Headers.ContainsKey("Accept"));
Assert.IsTrue(callback.Headers.ContainsKey("Authorization"));
Assert.IsTrue(callback.Headers.ContainsKey("User-Agent"));
Assert.AreEqual("application/json", callback.Headers["Accept"]);
Assert.AreEqual("Bearer Some-API-Token", callback.Headers["Authorization"]);
Assert.AreEqual("AMWD.CloudflareClient/1.0.0", callback.Headers["User-Agent"]);
_httpHandlerMock.Mock.Protected().Verify("SendAsync", Times.Once(), ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>());
VerifyDefaults();
VerifyNoOtherCalls();
}
[TestMethod]
public async Task ShouldPostHttpContentDirectly()
{
// Arrange
var stringContent = new StringContent(@"{""test"":""HERE ?""}", Encoding.UTF8, "application/json");
_httpHandlerMock.Responses.Enqueue(new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent(@"{""success"": true, ""errors"": [], ""messages"": [], ""result"": { ""string"": ""some-string"", ""integer"": 123 }}", Encoding.UTF8, MediaTypeNames.Application.Json),
});
var client = GetClient();
// Act
var response = await client.PostAsync<TestClass, StringContent>("test", stringContent);
// Assert
Assert.IsNotNull(response);
Assert.IsTrue(response.Success);
Assert.IsNotNull(response.Errors);
Assert.IsNotNull(response.Messages);
Assert.IsNull(response.ResultInfo);
Assert.AreEqual(0, response.Errors.Count);
Assert.AreEqual(0, response.Messages.Count);
Assert.IsNotNull(response.Result);
Assert.AreEqual("some-string", response.Result.Str);
Assert.AreEqual(123, response.Result.Int);
Assert.AreEqual(1, _httpHandlerMock.Callbacks.Count);
var callback = _httpHandlerMock.Callbacks.First();
Assert.AreEqual(HttpMethod.Post, callback.Method);
Assert.AreEqual("https://localhost/api/v4/test", callback.Url);
Assert.AreEqual(@"{""test"":""HERE ?""}", callback.Content);
Assert.AreEqual(3, callback.Headers.Count);
Assert.IsTrue(callback.Headers.ContainsKey("Accept"));
Assert.IsTrue(callback.Headers.ContainsKey("Authorization"));
Assert.IsTrue(callback.Headers.ContainsKey("User-Agent"));
Assert.AreEqual("application/json", callback.Headers["Accept"]);
Assert.AreEqual("Bearer Some-API-Token", callback.Headers["Authorization"]);
Assert.AreEqual("AMWD.CloudflareClient/1.0.0", callback.Headers["User-Agent"]);
_httpHandlerMock.Mock.Protected().Verify("SendAsync", Times.Once(), ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>());
VerifyDefaults();
VerifyNoOtherCalls();
}
[DataTestMethod]
[DataRow(HttpStatusCode.Unauthorized)]
[DataRow(HttpStatusCode.Forbidden)]
public async Task ShouldThrowAuthenticationExceptionOnStatusCode(HttpStatusCode statusCode)
{
// Arrange
_httpHandlerMock.Responses.Enqueue(new HttpResponseMessage
{
StatusCode = statusCode,
Content = new StringContent(@"{""success"": false, ""errors"": [{ ""code"": ""4711"", ""message"": ""foo & baz."" }, { ""code"": ""4712"", ""message"": ""Happy Error!"" }], ""messages"": []}", Encoding.UTF8, MediaTypeNames.Application.Json),
});
var client = GetClient();
try
{
// Act
await client.PostAsync<object, object>("foo", _request);
Assert.Fail();
}
catch (AuthenticationException ex)
{
// Assert
Assert.IsNull(ex.InnerException);
Assert.AreEqual($"4711: foo & baz.{Environment.NewLine}4712: Happy Error!", ex.Message);
}
}
[TestMethod]
public async Task ShouldReturnPlainText()
{
// Arrange
_clientOptionsMock.Setup(o => o.DefaultQueryParams).Returns(new Dictionary<string, string> { { "bar", "08/15" } });
_httpHandlerMock.Responses.Enqueue(new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent("This is an awesome text ;-)", Encoding.UTF8, MediaTypeNames.Text.Plain),
});
var client = GetClient();
// Act
var response = await client.PostAsync<string, TestClass>("some-awesome-path", _request);
// Assert
Assert.IsNotNull(response);
Assert.IsTrue(response.Success);
Assert.IsNotNull(response.Errors);
Assert.IsNotNull(response.Messages);
Assert.IsNotNull(response.ResultInfo);
Assert.AreEqual(0, response.Errors.Count);
Assert.AreEqual(0, response.Messages.Count);
Assert.AreEqual(0, response.ResultInfo.Count);
Assert.AreEqual(0, response.ResultInfo.Page);
Assert.AreEqual(0, response.ResultInfo.PerPage);
Assert.AreEqual(0, response.ResultInfo.TotalCount);
Assert.AreEqual("This is an awesome text ;-)", response.Result);
Assert.AreEqual(1, _httpHandlerMock.Callbacks.Count);
var callback = _httpHandlerMock.Callbacks.First();
Assert.AreEqual(HttpMethod.Post, callback.Method);
Assert.AreEqual("https://localhost/api/v4/some-awesome-path?bar=08%2F15", callback.Url);
Assert.AreEqual(@"{""string"":""Happy Testing!"",""integer"":54321}", callback.Content);
Assert.AreEqual(3, callback.Headers.Count);
Assert.IsTrue(callback.Headers.ContainsKey("Accept"));
Assert.IsTrue(callback.Headers.ContainsKey("Authorization"));
Assert.IsTrue(callback.Headers.ContainsKey("User-Agent"));
Assert.AreEqual("application/json", callback.Headers["Accept"]);
Assert.AreEqual("Bearer Some-API-Token", callback.Headers["Authorization"]);
Assert.AreEqual("AMWD.CloudflareClient/1.0.0", callback.Headers["User-Agent"]);
_httpHandlerMock.Mock.Protected().Verify("SendAsync", Times.Once(), ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>());
_authenticationMock.Verify(m => m.AddHeader(It.IsAny<HttpClient>()), Times.Once);
_clientOptionsMock.Verify(o => o.BaseUrl, Times.Exactly(2));
_clientOptionsMock.Verify(o => o.Timeout, Times.Exactly(2));
_clientOptionsMock.Verify(o => o.MaxRetries, Times.Exactly(2));
_clientOptionsMock.Verify(o => o.DefaultHeaders, Times.Once);
_clientOptionsMock.Verify(o => o.DefaultQueryParams, Times.Exactly(2));
_clientOptionsMock.Verify(o => o.AllowRedirects, Times.Once);
_clientOptionsMock.Verify(o => o.UseProxy, Times.Exactly(2));
_clientOptionsMock.Verify(o => o.Proxy, Times.Once);
VerifyNoOtherCalls();
}
[TestMethod]
[ExpectedException(typeof(JsonReaderException))]
public async Task ShouldThrowExceptionOnInvalidResponse()
{
// Arrange
_httpHandlerMock.Responses.Enqueue(new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent("This is a bad text :p", Encoding.UTF8, MediaTypeNames.Text.Plain),
});
var client = GetClient();
// Act
await client.PostAsync<TestClass, TestClass>("some-path", _request);
}
private void VerifyDefaults()
{
_authenticationMock.Verify(m => m.AddHeader(It.IsAny<HttpClient>()), Times.Once);
_clientOptionsMock.Verify(o => o.BaseUrl, Times.Exactly(2));
_clientOptionsMock.Verify(o => o.Timeout, Times.Exactly(2));
_clientOptionsMock.Verify(o => o.MaxRetries, Times.Exactly(2));
_clientOptionsMock.Verify(o => o.DefaultHeaders, Times.Once);
_clientOptionsMock.Verify(o => o.DefaultQueryParams, Times.Once);
_clientOptionsMock.Verify(o => o.AllowRedirects, Times.Once);
_clientOptionsMock.Verify(o => o.UseProxy, Times.Exactly(2));
_clientOptionsMock.Verify(o => o.Proxy, Times.Once);
}
private void VerifyNoOtherCalls()
{
_httpHandlerMock.Mock.VerifyNoOtherCalls();
_authenticationMock.VerifyNoOtherCalls();
_clientOptionsMock.VerifyNoOtherCalls();
}
private ICloudflareClient GetClient()
{
var httpClient = new HttpClient(_httpHandlerMock.Mock.Object)
{
BaseAddress = new Uri(_baseUrl),
Timeout = _clientOptionsMock.Object.Timeout,
};
httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("AMWD.CloudflareClient", "1.0.0"));
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
if (_clientOptionsMock.Object.DefaultHeaders.Count > 0)
{
foreach (var headerKvp in _clientOptionsMock.Object.DefaultHeaders)
httpClient.DefaultRequestHeaders.Add(headerKvp.Key, headerKvp.Value);
}
_authenticationMock.Object.AddHeader(httpClient);
_authenticationMock.Invocations.Clear();
_clientOptionsMock.Invocations.Clear();
var client = new CloudflareClient(_authenticationMock.Object, _clientOptionsMock.Object);
var httpClientField = client.GetType()
.GetField("_httpClient", BindingFlags.NonPublic | BindingFlags.Instance);
(httpClientField.GetValue(client) as HttpClient).Dispose();
httpClientField.SetValue(client, httpClient);
return client;
}
private class TestClass
{
[JsonProperty("string")]
public string Str { get; set; }
[JsonProperty("integer")]
public int Int { get; set; }
}
private class TestFilter : IQueryParameterFilter
{
public IDictionary<string, string> GetQueryParameters()
{
return new Dictionary<string, string>
{
{ "test", "filter-text" }
};
}
}
}
}

View File

@@ -0,0 +1,386 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Mime;
using System.Reflection;
using System.Security.Authentication;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using AMWD.Net.Api.Cloudflare;
using AMWD.Net.Api.Cloudflare.Auth;
using Moq;
using Moq.Protected;
namespace Cloudflare.Core.Tests.CloudflareClientTests
{
[TestClass]
public class PutAsyncTest
{
private const string _baseUrl = "https://localhost/api/v4/";
private HttpMessageHandlerMock _httpHandlerMock;
private Mock<ClientOptions> _clientOptionsMock;
private Mock<IAuthentication> _authenticationMock;
private TestClass _request;
[TestInitialize]
public void Initialize()
{
_httpHandlerMock = new HttpMessageHandlerMock();
_authenticationMock = new Mock<IAuthentication>();
_clientOptionsMock = new Mock<ClientOptions>();
_authenticationMock
.Setup(a => a.AddHeader(It.IsAny<HttpClient>()))
.Callback<HttpClient>(c => c.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "Some-API-Token"));
_clientOptionsMock.Setup(o => o.BaseUrl).Returns(_baseUrl);
_clientOptionsMock.Setup(o => o.Timeout).Returns(TimeSpan.FromSeconds(60));
_clientOptionsMock.Setup(o => o.MaxRetries).Returns(2);
_clientOptionsMock.Setup(o => o.DefaultHeaders).Returns(new Dictionary<string, string>());
_clientOptionsMock.Setup(o => o.DefaultQueryParams).Returns(new Dictionary<string, string>());
_clientOptionsMock.Setup(o => o.AllowRedirects).Returns(false);
_clientOptionsMock.Setup(o => o.UseProxy).Returns(false);
_request = new TestClass
{
Int = 54321,
Str = "Happy Testing!"
};
}
[TestMethod]
[ExpectedException(typeof(ObjectDisposedException))]
public async Task ShouldThrowDisposed()
{
// Arrange
var client = GetClient() as CloudflareClient;
client.Dispose();
// Act
await client.PutAsync<object, object>("test", _request);
// Assert - ObjectDisposedException
}
[DataTestMethod]
[DataRow(null)]
[DataRow("")]
[DataRow(" ")]
[ExpectedException(typeof(ArgumentNullException))]
public async Task ShouldThrowArgumentNullOnRequestPath(string path)
{
// Arrange
var client = GetClient();
// Act
await client.PutAsync<object, object>(path, _request);
// Assert - ArgumentNullException
}
[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public async Task ShouldThrowArgumentOnRequestPath()
{
// Arrange
var client = GetClient();
// Act
await client.PutAsync<object, object>("foo?bar=baz", _request);
// Assert - ArgumentException
}
[TestMethod]
public async Task ShouldPut()
{
// Arrange
_httpHandlerMock.Responses.Enqueue(new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent(@"{""success"": true, ""errors"": [], ""messages"": [], ""result"": { ""string"": ""some-string"", ""integer"": 123 }}", Encoding.UTF8, MediaTypeNames.Application.Json),
});
var client = GetClient();
// Act
var response = await client.PutAsync<TestClass, TestClass>("test", _request);
// Assert
Assert.IsNotNull(response);
Assert.IsTrue(response.Success);
Assert.IsNotNull(response.Errors);
Assert.IsNotNull(response.Messages);
Assert.IsNull(response.ResultInfo);
Assert.AreEqual(0, response.Errors.Count);
Assert.AreEqual(0, response.Messages.Count);
Assert.IsNotNull(response.Result);
Assert.AreEqual("some-string", response.Result.Str);
Assert.AreEqual(123, response.Result.Int);
Assert.AreEqual(1, _httpHandlerMock.Callbacks.Count);
var callback = _httpHandlerMock.Callbacks.First();
Assert.AreEqual(HttpMethod.Put, callback.Method);
Assert.AreEqual("https://localhost/api/v4/test", callback.Url);
Assert.AreEqual(@"{""string"":""Happy Testing!"",""integer"":54321}", callback.Content);
Assert.AreEqual(3, callback.Headers.Count);
Assert.IsTrue(callback.Headers.ContainsKey("Accept"));
Assert.IsTrue(callback.Headers.ContainsKey("Authorization"));
Assert.IsTrue(callback.Headers.ContainsKey("User-Agent"));
Assert.AreEqual("application/json", callback.Headers["Accept"]);
Assert.AreEqual("Bearer Some-API-Token", callback.Headers["Authorization"]);
Assert.AreEqual("AMWD.CloudflareClient/1.0.0", callback.Headers["User-Agent"]);
_httpHandlerMock.Mock.Protected().Verify("SendAsync", Times.Once(), ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>());
VerifyDefaults();
VerifyNoOtherCalls();
}
[TestMethod]
public async Task ShouldPutHttpContentDirectly()
{
// Arrange
var stringContent = new StringContent(@"{""test"":""HERE ?""}", Encoding.UTF8, "application/json");
_httpHandlerMock.Responses.Enqueue(new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent(@"{""success"": true, ""errors"": [], ""messages"": [], ""result"": { ""string"": ""some-string"", ""integer"": 123 }}", Encoding.UTF8, MediaTypeNames.Application.Json),
});
var client = GetClient();
// Act
var response = await client.PutAsync<TestClass, StringContent>("test", stringContent);
// Assert
Assert.IsNotNull(response);
Assert.IsTrue(response.Success);
Assert.IsNotNull(response.Errors);
Assert.IsNotNull(response.Messages);
Assert.IsNull(response.ResultInfo);
Assert.AreEqual(0, response.Errors.Count);
Assert.AreEqual(0, response.Messages.Count);
Assert.IsNotNull(response.Result);
Assert.AreEqual("some-string", response.Result.Str);
Assert.AreEqual(123, response.Result.Int);
Assert.AreEqual(1, _httpHandlerMock.Callbacks.Count);
var callback = _httpHandlerMock.Callbacks.First();
Assert.AreEqual(HttpMethod.Put, callback.Method);
Assert.AreEqual("https://localhost/api/v4/test", callback.Url);
Assert.AreEqual(@"{""test"":""HERE ?""}", callback.Content);
Assert.AreEqual(3, callback.Headers.Count);
Assert.IsTrue(callback.Headers.ContainsKey("Accept"));
Assert.IsTrue(callback.Headers.ContainsKey("Authorization"));
Assert.IsTrue(callback.Headers.ContainsKey("User-Agent"));
Assert.AreEqual("application/json", callback.Headers["Accept"]);
Assert.AreEqual("Bearer Some-API-Token", callback.Headers["Authorization"]);
Assert.AreEqual("AMWD.CloudflareClient/1.0.0", callback.Headers["User-Agent"]);
_httpHandlerMock.Mock.Protected().Verify("SendAsync", Times.Once(), ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>());
VerifyDefaults();
VerifyNoOtherCalls();
}
[DataTestMethod]
[DataRow(HttpStatusCode.Unauthorized)]
[DataRow(HttpStatusCode.Forbidden)]
public async Task ShouldThrowAuthenticationExceptionOnStatusCode(HttpStatusCode statusCode)
{
// Arrange
_httpHandlerMock.Responses.Enqueue(new HttpResponseMessage
{
StatusCode = statusCode,
Content = new StringContent(@"{""success"": false, ""errors"": [{ ""code"": ""4711"", ""message"": ""foo & baz."" }, { ""code"": ""4712"", ""message"": ""Happy Error!"" }], ""messages"": []}", Encoding.UTF8, MediaTypeNames.Application.Json),
});
var client = GetClient();
try
{
// Act
await client.PutAsync<object, object>("foo", _request);
Assert.Fail();
}
catch (AuthenticationException ex)
{
// Assert
Assert.IsNull(ex.InnerException);
Assert.AreEqual($"4711: foo & baz.{Environment.NewLine}4712: Happy Error!", ex.Message);
}
}
[TestMethod]
public async Task ShouldReturnPlainText()
{
// Arrange
_clientOptionsMock.Setup(o => o.DefaultQueryParams).Returns(new Dictionary<string, string> { { "bar", "08/15" } });
_httpHandlerMock.Responses.Enqueue(new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent("This is an awesome text ;-)", Encoding.UTF8, MediaTypeNames.Text.Plain),
});
var client = GetClient();
// Act
var response = await client.PutAsync<string, TestClass>("some-awesome-path", _request);
// Assert
Assert.IsNotNull(response);
Assert.IsTrue(response.Success);
Assert.IsNotNull(response.Errors);
Assert.IsNotNull(response.Messages);
Assert.IsNotNull(response.ResultInfo);
Assert.AreEqual(0, response.Errors.Count);
Assert.AreEqual(0, response.Messages.Count);
Assert.AreEqual(0, response.ResultInfo.Count);
Assert.AreEqual(0, response.ResultInfo.Page);
Assert.AreEqual(0, response.ResultInfo.PerPage);
Assert.AreEqual(0, response.ResultInfo.TotalCount);
Assert.AreEqual("This is an awesome text ;-)", response.Result);
Assert.AreEqual(1, _httpHandlerMock.Callbacks.Count);
var callback = _httpHandlerMock.Callbacks.First();
Assert.AreEqual(HttpMethod.Put, callback.Method);
Assert.AreEqual("https://localhost/api/v4/some-awesome-path?bar=08%2F15", callback.Url);
Assert.AreEqual(@"{""string"":""Happy Testing!"",""integer"":54321}", callback.Content);
Assert.AreEqual(3, callback.Headers.Count);
Assert.IsTrue(callback.Headers.ContainsKey("Accept"));
Assert.IsTrue(callback.Headers.ContainsKey("Authorization"));
Assert.IsTrue(callback.Headers.ContainsKey("User-Agent"));
Assert.AreEqual("application/json", callback.Headers["Accept"]);
Assert.AreEqual("Bearer Some-API-Token", callback.Headers["Authorization"]);
Assert.AreEqual("AMWD.CloudflareClient/1.0.0", callback.Headers["User-Agent"]);
_httpHandlerMock.Mock.Protected().Verify("SendAsync", Times.Once(), ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>());
_authenticationMock.Verify(m => m.AddHeader(It.IsAny<HttpClient>()), Times.Once);
_clientOptionsMock.Verify(o => o.BaseUrl, Times.Exactly(2));
_clientOptionsMock.Verify(o => o.Timeout, Times.Exactly(2));
_clientOptionsMock.Verify(o => o.MaxRetries, Times.Exactly(2));
_clientOptionsMock.Verify(o => o.DefaultHeaders, Times.Once);
_clientOptionsMock.Verify(o => o.DefaultQueryParams, Times.Exactly(2));
_clientOptionsMock.Verify(o => o.AllowRedirects, Times.Once);
_clientOptionsMock.Verify(o => o.UseProxy, Times.Exactly(2));
_clientOptionsMock.Verify(o => o.Proxy, Times.Once);
VerifyNoOtherCalls();
}
[TestMethod]
[ExpectedException(typeof(JsonReaderException))]
public async Task ShouldThrowExceptionOnInvalidResponse()
{
// Arrange
_httpHandlerMock.Responses.Enqueue(new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent("This is a bad text :p", Encoding.UTF8, MediaTypeNames.Text.Plain),
});
var client = GetClient();
// Act
await client.PutAsync<TestClass, TestClass>("some-path", _request);
}
private void VerifyDefaults()
{
_authenticationMock.Verify(m => m.AddHeader(It.IsAny<HttpClient>()), Times.Once);
_clientOptionsMock.Verify(o => o.BaseUrl, Times.Exactly(2));
_clientOptionsMock.Verify(o => o.Timeout, Times.Exactly(2));
_clientOptionsMock.Verify(o => o.MaxRetries, Times.Exactly(2));
_clientOptionsMock.Verify(o => o.DefaultHeaders, Times.Once);
_clientOptionsMock.Verify(o => o.DefaultQueryParams, Times.Once);
_clientOptionsMock.Verify(o => o.AllowRedirects, Times.Once);
_clientOptionsMock.Verify(o => o.UseProxy, Times.Exactly(2));
_clientOptionsMock.Verify(o => o.Proxy, Times.Once);
}
private void VerifyNoOtherCalls()
{
_httpHandlerMock.Mock.VerifyNoOtherCalls();
_authenticationMock.VerifyNoOtherCalls();
_clientOptionsMock.VerifyNoOtherCalls();
}
private ICloudflareClient GetClient()
{
var httpClient = new HttpClient(_httpHandlerMock.Mock.Object)
{
BaseAddress = new Uri(_baseUrl),
Timeout = _clientOptionsMock.Object.Timeout,
};
httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("AMWD.CloudflareClient", "1.0.0"));
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
if (_clientOptionsMock.Object.DefaultHeaders.Count > 0)
{
foreach (var headerKvp in _clientOptionsMock.Object.DefaultHeaders)
httpClient.DefaultRequestHeaders.Add(headerKvp.Key, headerKvp.Value);
}
_authenticationMock.Object.AddHeader(httpClient);
_authenticationMock.Invocations.Clear();
_clientOptionsMock.Invocations.Clear();
var client = new CloudflareClient(_authenticationMock.Object, _clientOptionsMock.Object);
var httpClientField = client.GetType()
.GetField("_httpClient", BindingFlags.NonPublic | BindingFlags.Instance);
(httpClientField.GetValue(client) as HttpClient).Dispose();
httpClientField.SetValue(client, httpClient);
return client;
}
private class TestClass
{
[JsonProperty("string")]
public string Str { get; set; }
[JsonProperty("integer")]
public int Int { get; set; }
}
private class TestFilter : IQueryParameterFilter
{
public IDictionary<string, string> GetQueryParameters()
{
return new Dictionary<string, string>
{
{ "test", "filter-text" }
};
}
}
}
}

View File

@@ -0,0 +1,64 @@
using System.Runtime.Serialization;
using AMWD.Net.Api.Cloudflare;
namespace Cloudflare.Core.Tests.Extensions
{
[TestClass]
public class EnumExtensionsTest
{
[TestMethod]
public void ShouldReturnEnumMemberValue()
{
// Arrange
var enumValue = EnumWithAttribute.One;
// Act
string val = enumValue.GetEnumMemberValue();
// Assert
Assert.AreEqual("eins", val);
}
[TestMethod]
public void ShouldReturnStringMissingAttribute()
{
// Arrange
var enumValue = EnumWithoutAttribute.Two;
// Act
string val = enumValue.GetEnumMemberValue();
// Assert
Assert.AreEqual("Two", val);
}
[TestMethod]
public void ShouldReturnString()
{
// Arrange
EnumWithAttribute enumValue = 0;
// Act
string val = enumValue.GetEnumMemberValue();
// Assert
Assert.AreEqual("0", val);
}
public enum EnumWithAttribute
{
[EnumMember(Value = "eins")]
One = 1,
[EnumMember(Value = "zwei")]
Two = 2,
}
public enum EnumWithoutAttribute
{
One = 1,
Two = 2,
}
}
}

View File

@@ -0,0 +1,52 @@
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Moq;
using Moq.Protected;
namespace Cloudflare.Core.Tests
{
internal class HttpMessageHandlerMock
{
public HttpMessageHandlerMock()
{
Mock = new();
Mock.Protected()
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
.Callback<HttpRequestMessage, CancellationToken>(async (request, ct) =>
{
var callback = new HttpMessageRequestCallback
{
Method = request.Method,
Url = request.RequestUri.ToString(),
Headers = request.Headers.ToDictionary(h => h.Key, h => h.Value.First()),
};
if (request.Content != null)
callback.Content = await request.Content.ReadAsStringAsync();
Callbacks.Add(callback);
})
.ReturnsAsync(() => Responses.Dequeue());
}
public List<HttpMessageRequestCallback> Callbacks { get; } = [];
public Queue<HttpResponseMessage> Responses { get; } = new();
public Mock<HttpClientHandler> Mock { get; }
}
internal class HttpMessageRequestCallback
{
public HttpMethod Method { get; set; }
public string Url { get; set; }
public Dictionary<string, string> Headers { get; set; }
public string Content { get; set; }
}
}

View File

@@ -0,0 +1,43 @@
using System;
using System.Net.Http;
using System.Text.RegularExpressions;
namespace AMWD.Net.Api.Cloudflare.Auth
{
/// <summary>
/// Implements the interface to authenticate using an API key and email address.
/// </summary>
public class ApiKeyAuthentication : IAuthentication
{
private static readonly Regex _emailCheckRegex = new(@"^[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?$", RegexOptions.Compiled);
private readonly string _emailAddress;
private readonly string _apiKey;
/// <summary>
/// Initializes a new instance of the <see cref="ApiKeyAuthentication"/> class.
/// </summary>
/// <param name="emailAddress">The email address.</param>
/// <param name="apiKey">The global API key.</param>
public ApiKeyAuthentication(string emailAddress, string apiKey)
{
if (string.IsNullOrWhiteSpace(emailAddress))
throw new ArgumentNullException(nameof(emailAddress));
if (string.IsNullOrWhiteSpace(apiKey))
throw new ArgumentNullException(nameof(apiKey));
if (!_emailCheckRegex.IsMatch(emailAddress))
throw new ArgumentException("Invalid email address", nameof(emailAddress));
_emailAddress = emailAddress;
_apiKey = apiKey;
}
/// <inheritdoc />
public void AddHeader(HttpClient httpClient)
{
httpClient.DefaultRequestHeaders.Add("X-Auth-Email", _emailAddress);
httpClient.DefaultRequestHeaders.Add("X-Auth-Key", _apiKey);
}
}
}

View File

@@ -0,0 +1,32 @@
using System;
using System.Net.Http;
using System.Net.Http.Headers;
namespace AMWD.Net.Api.Cloudflare.Auth
{
/// <summary>
/// Implements the interface to authenticate using an API token.
/// </summary>
public class ApiTokenAuthentication : IAuthentication
{
private readonly string _apiToken;
/// <summary>
/// Initializes a new instance of the <see cref="ApiTokenAuthentication"/> class.
/// </summary>
/// <param name="apiToken">The API token.</param>
public ApiTokenAuthentication(string apiToken)
{
if (string.IsNullOrWhiteSpace(apiToken))
throw new ArgumentNullException(nameof(apiToken));
_apiToken = apiToken;
}
/// <inheritdoc />
public void AddHeader(HttpClient httpClient)
{
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _apiToken);
}
}
}

View File

@@ -0,0 +1,55 @@
using System;
using System.Collections.Generic;
using System.Net;
namespace AMWD.Net.Api.Cloudflare
{
/// <summary>
/// Options for the Cloudflare API.
/// </summary>
public class ClientOptions
{
/// <summary>
/// Gets or sets the default base url for the API.
/// </summary>
public virtual string BaseUrl { get; set; } = "https://api.cloudflare.com/client/v4/";
/// <summary>
/// Gets or sets the default timeout for the API.
/// </summary>
public virtual TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(60);
/// <summary>
/// Gets or sets the maximum number of retries for the API.
/// </summary>
/// <remarks>
/// The API may respond with an 5xx error and a X-Should-Retry header indicating that the request should be retried.
/// </remarks>
public virtual int MaxRetries { get; set; } = 2;
/// <summary>
/// Gets or sets additional default headers to every request.
/// </summary>
public virtual IDictionary<string, string> DefaultHeaders { get; set; } = new Dictionary<string, string>();
/// <summary>
/// Gets or sets additional default query parameters to every request.
/// </summary>
public virtual IDictionary<string, string> DefaultQueryParams { get; set; } = new Dictionary<string, string>();
/// <summary>
/// Gets or sets a value indicating whether to allow redirects.
/// </summary>
public virtual bool AllowRedirects { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to use a proxy.
/// </summary>
public virtual bool UseProxy { get; set; }
/// <summary>
/// Gets or sets the proxy information.
/// </summary>
public virtual IWebProxy Proxy { get; set; }
}
}

View File

@@ -0,0 +1,62 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks>
<NrtRevisionFormat>{semvertag:main}{!:-dev}</NrtRevisionFormat>
<AppendTargetFrameworkToOutputPath>true</AppendTargetFrameworkToOutputPath>
<CopyRefAssembliesToPublishDirectory>false</CopyRefAssembliesToPublishDirectory>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<RepositoryType>git</RepositoryType>
<RepositoryUrl>https://github.com/AM-WD/cloudflare-api.git</RepositoryUrl>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<PackageIcon>package-icon.png</PackageIcon>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageTags>cloudflare api</PackageTags>
<PackageProjectUrl>https://developers.cloudflare.com/api</PackageProjectUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<EmbedUntrackedSources>false</EmbedUntrackedSources>
<PackageId>AMWD.Net.API.Cloudflare</PackageId>
<AssemblyName>amwd-cloudflare-core</AssemblyName>
<RootNamespace>AMWD.Net.Api.Cloudflare</RootNamespace>
<Product>Cloudflare API - Core</Product>
<Description>Core features of the Cloudflare API</Description>
</PropertyGroup>
<PropertyGroup Condition="$([System.Text.RegularExpressions.Regex]::IsMatch('$(CI_COMMIT_TAG)', '^v[0-9.]+'))">
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
</PropertyGroup>
<PropertyGroup Condition="'$(GITLAB_CI)' == 'true'">
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
</PropertyGroup>
<ItemGroup Condition="'$(GITLAB_CI)' == 'true'">
<SourceLinkGitLabHost Include="$(CI_SERVER_HOST)" Version="$(CI_SERVER_VERSION)" />
<PackageReference Include="Microsoft.SourceLink.GitLab" Version="8.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<None Include="$(SolutionDir)/package-icon.png" Pack="true" PackagePath="/" />
<None Include="README.md" Pack="true" PackagePath="/" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="AMWD.NetRevisionTask" Version="1.2.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,324 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Reflection;
using System.Security.Authentication;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using AMWD.Net.Api.Cloudflare.Auth;
namespace AMWD.Net.Api.Cloudflare
{
/// <summary>
/// Implements the Core of the Cloudflare API client.
/// </summary>
public partial class CloudflareClient : ICloudflareClient, IDisposable
{
private static readonly JsonSerializerSettings _jsonSerializerSettings = new JsonSerializerSettings
{
Culture = CultureInfo.InvariantCulture,
Formatting = Formatting.None,
NullValueHandling = NullValueHandling.Ignore,
};
private readonly ClientOptions _clientOptions;
private readonly HttpClient _httpClient;
private bool _isDisposed = false;
/// <summary>
/// Initializes a new instance of the <see cref="CloudflareClient"/> class.
/// </summary>
/// <param name="emailAddress">The email address of the Cloudflare account.</param>
/// <param name="apiKey">The API key of the Cloudflare account.</param>
/// <param name="clientOptions">The client options (optional).</param>
public CloudflareClient(string emailAddress, string apiKey, ClientOptions clientOptions = null)
: this(new ApiKeyAuthentication(emailAddress, apiKey), clientOptions)
{ }
/// <summary>
/// Initializes a new instance of the <see cref="CloudflareClient"/> class.
/// </summary>
/// <param name="apiToken">The API token.</param>
/// <param name="clientOptions">The client options (optional).</param>
public CloudflareClient(string apiToken, ClientOptions clientOptions = null)
: this(new ApiTokenAuthentication(apiToken), clientOptions)
{ }
/// <summary>
/// Initializes a new instance of the <see cref="CloudflareClient"/> class.
/// </summary>
/// <param name="authentication">The authentication information.</param>
/// <param name="clientOptions">The client options (optional).</param>
public CloudflareClient(IAuthentication authentication, ClientOptions clientOptions = null)
{
if (authentication == null)
throw new ArgumentNullException(nameof(authentication));
_clientOptions = clientOptions ?? new ClientOptions();
ValidateClientOptions();
_httpClient = CreateHttpClient();
authentication.AddHeader(_httpClient);
}
/// <summary>
/// Disposes of the resources used by the <see cref="CloudflareClient"/> object.
/// </summary>
public void Dispose()
{
if (_isDisposed)
return;
_isDisposed = true;
_httpClient.Dispose();
GC.SuppressFinalize(this);
}
/// <inheritdoc/>
public async Task<CloudflareResponse<TResponse>> GetAsync<TResponse>(string requestPath, IQueryParameterFilter queryFilter = null, CancellationToken cancellationToken = default)
{
ThrowIfDisposed();
ValidateRequestPath(requestPath);
string requestUrl = BuildRequestUrl(requestPath, queryFilter);
var response = await _httpClient.GetAsync(requestUrl, cancellationToken).ConfigureAwait(false);
return await GetCloudflareResponse<TResponse>(response, cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc/>
public async Task<CloudflareResponse<TResponse>> PostAsync<TResponse, TRequest>(string requestPath, TRequest request, CancellationToken cancellationToken = default)
{
ThrowIfDisposed();
ValidateRequestPath(requestPath);
string requestUrl = BuildRequestUrl(requestPath);
HttpContent httpRequestContent;
if (request is HttpContent httpContent)
{
httpRequestContent = httpContent;
}
else
{
string json = JsonConvert.SerializeObject(request, _jsonSerializerSettings);
httpRequestContent = new StringContent(json, Encoding.UTF8, "application/json");
}
var response = await _httpClient.PostAsync(requestUrl, httpRequestContent, cancellationToken).ConfigureAwait(false);
return await GetCloudflareResponse<TResponse>(response, cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc/>
public async Task<CloudflareResponse<TResponse>> PutAsync<TResponse, TRequest>(string requestPath, TRequest request, CancellationToken cancellationToken = default)
{
ThrowIfDisposed();
ValidateRequestPath(requestPath);
string requestUrl = BuildRequestUrl(requestPath);
HttpContent httpRequestContent;
if (request is HttpContent httpContent)
{
httpRequestContent = httpContent;
}
else
{
string json = JsonConvert.SerializeObject(request, _jsonSerializerSettings);
httpRequestContent = new StringContent(json, Encoding.UTF8, "application/json");
}
var response = await _httpClient.PutAsync(requestUrl, httpRequestContent, cancellationToken).ConfigureAwait(false);
return await GetCloudflareResponse<TResponse>(response, cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc/>
public async Task<CloudflareResponse<TResponse>> DeleteAsync<TResponse>(string requestPath, CancellationToken cancellationToken = default)
{
ThrowIfDisposed();
ValidateRequestPath(requestPath);
string requestUrl = BuildRequestUrl(requestPath);
var response = await _httpClient.DeleteAsync(requestUrl, cancellationToken).ConfigureAwait(false);
return await GetCloudflareResponse<TResponse>(response, cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc/>
public async Task<CloudflareResponse<TResponse>> PatchAsync<TResponse, TRequest>(string requestPath, TRequest request, CancellationToken cancellationToken = default)
{
ThrowIfDisposed();
ValidateRequestPath(requestPath);
string requestUrl = BuildRequestUrl(requestPath);
HttpContent httpRequestContent;
if (request is HttpContent httpContent)
{
httpRequestContent = httpContent;
}
else
{
string json = JsonConvert.SerializeObject(request, _jsonSerializerSettings);
httpRequestContent = new StringContent(json, Encoding.UTF8, "application/json");
}
#if NET6_0_OR_GREATER
var response = await _httpClient.PatchAsync(requestUrl, httpRequestContent, cancellationToken).ConfigureAwait(false);
#else
var httpRequestMessage = new HttpRequestMessage
{
Version = HttpVersion.Version11,
Method = new HttpMethod("PATCH"),
RequestUri = new Uri(requestUrl),
Content = httpRequestContent,
};
var response = await _httpClient.SendAsync(httpRequestMessage, cancellationToken).ConfigureAwait(false);
#endif
return await GetCloudflareResponse<TResponse>(response, cancellationToken).ConfigureAwait(false);
}
private void ThrowIfDisposed()
{
if (_isDisposed)
throw new ObjectDisposedException(GetType().FullName);
}
private void ValidateClientOptions()
{
if (string.IsNullOrWhiteSpace(_clientOptions.BaseUrl))
throw new ArgumentNullException(nameof(_clientOptions.BaseUrl));
if (_clientOptions.Timeout <= TimeSpan.Zero)
throw new ArgumentOutOfRangeException(nameof(_clientOptions.Timeout), "Timeout must be positive.");
if (_clientOptions.MaxRetries < 0 || 10 < _clientOptions.MaxRetries)
throw new ArgumentOutOfRangeException(nameof(_clientOptions.MaxRetries), "MaxRetries should be between 0 and 10.");
if (_clientOptions.UseProxy && _clientOptions.Proxy == null)
throw new ArgumentNullException(nameof(_clientOptions.Proxy));
}
private void ValidateRequestPath(string requestPath)
{
if (string.IsNullOrWhiteSpace(requestPath))
throw new ArgumentNullException(nameof(requestPath));
if (requestPath.Contains("?"))
throw new ArgumentException("Query parameters are not allowed", nameof(requestPath));
}
private HttpClient CreateHttpClient()
{
string version = typeof(CloudflareClient).Assembly
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()
.InformationalVersion;
HttpMessageHandler handler;
try
{
handler = new HttpClientHandler
{
AllowAutoRedirect = _clientOptions.AllowRedirects,
UseProxy = _clientOptions.UseProxy,
Proxy = _clientOptions.Proxy,
};
}
catch (PlatformNotSupportedException)
{
handler = new HttpClientHandler
{
AllowAutoRedirect = _clientOptions.AllowRedirects,
};
}
var client = new HttpClient(handler, true)
{
BaseAddress = new Uri(_clientOptions.BaseUrl),
Timeout = _clientOptions.Timeout,
};
client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("AMWD.CloudflareClient", version));
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
if (_clientOptions.DefaultHeaders.Count > 0)
{
foreach (var headerKvp in _clientOptions.DefaultHeaders)
client.DefaultRequestHeaders.Add(headerKvp.Key, headerKvp.Value);
}
return client;
}
private async Task<CloudflareResponse<TRes>> GetCloudflareResponse<TRes>(HttpResponseMessage response, CancellationToken cancellationToken)
{
#if NET6_0_OR_GREATER
string content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
#else
string content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
#endif
switch (response.StatusCode)
{
case HttpStatusCode.Forbidden:
case HttpStatusCode.Unauthorized:
var errorResponse = JsonConvert.DeserializeObject<CloudflareResponse<object>>(content);
throw new AuthenticationException(string.Join(Environment.NewLine, errorResponse.Errors.Select(e => $"{e.Code}: {e.Message}")));
default:
try
{
return JsonConvert.DeserializeObject<CloudflareResponse<TRes>>(content);
}
catch
{
if (typeof(TRes) == typeof(string))
{
object cObj = content.Replace("\\n", Environment.NewLine);
return new CloudflareResponse<TRes>
{
Success = true,
ResultInfo = new ResultInfo(),
Result = (TRes)cObj,
};
}
throw;
}
}
}
private string BuildRequestUrl(string requestPath, IQueryParameterFilter queryFilter = null)
{
var dict = new Dictionary<string, string>();
if (_clientOptions.DefaultQueryParams.Count > 0)
{
foreach (var paramKvp in _clientOptions.DefaultQueryParams)
dict[paramKvp.Key] = paramKvp.Value;
}
var queryParams = queryFilter?.GetQueryParameters();
if (queryParams?.Count > 0)
{
foreach (var kvp in queryParams)
dict[kvp.Key] = kvp.Value;
}
if (dict.Count == 0)
return requestPath;
string[] param = dict.Select(kvp => $"{kvp.Key}={WebUtility.UrlEncode(kvp.Value)}").ToArray();
string query = string.Join("&", param);
return $"{requestPath}?{query}";
}
}
}

View File

@@ -0,0 +1,24 @@
using System.Runtime.Serialization;
using Newtonsoft.Json.Converters;
namespace AMWD.Net.Api.Cloudflare
{
/// <summary>
/// Whether to match all search requirements or at least one (any).
/// </summary>
[JsonConverter(typeof(StringEnumConverter))]
public enum FilterMatchType
{
/// <summary>
/// Match all search requirements.
/// </summary>
[EnumMember(Value = "all")]
All = 1,
/// <summary>
/// Match at least one search requirement.
/// </summary>
[EnumMember(Value = "any")]
Any = 2,
}
}

View File

@@ -0,0 +1,24 @@
using System.Runtime.Serialization;
using Newtonsoft.Json.Converters;
namespace AMWD.Net.Api.Cloudflare
{
/// <summary>
/// The direction to order the entity.
/// </summary>
[JsonConverter(typeof(StringEnumConverter))]
public enum OrderDirection
{
/// <summary>
/// Order in ascending order.
/// </summary>
[EnumMember(Value = "asc")]
Asc = 1,
/// <summary>
/// Order in descending order.
/// </summary>
[EnumMember(Value = "desc")]
Desc = 2
}
}

View File

@@ -0,0 +1,61 @@
using System;
using System.Runtime.Serialization;
namespace AMWD.Net.Api.Cloudflare
{
/// <summary>
/// Represents errors that occur during Cloudflare API calls.
/// </summary>
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
public class CloudflareException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="CloudflareException"/> class.
/// </summary>
public CloudflareException()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="CloudflareException"/> class with a specified error
/// message.
/// </summary>
/// <param name="message">The message that describes the error.</param>
public CloudflareException(string message)
: base(message)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="CloudflareException"/> class with a specified error
/// message and a reference to the inner exception that is the cause of this exception.
/// </summary>
/// <param name="message">The error message that explains the reason for the exception.</param>
/// <param name="innerException">
/// The exception that is the cause of the current exception, or a <see langword="null"/> reference
/// if no inner exception is specified.
/// </param>
public CloudflareException(string message, Exception innerException)
: base(message, innerException)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="CloudflareException"/> class with serialized data.
/// </summary>
/// <param name="info">
/// The <see cref="SerializationInfo"/> that holds the serialized
/// object data about the exception being thrown.
/// </param>
/// <param name="context">
/// The <see cref="StreamingContext"/> that contains contextual information
/// about the source or destination.
/// </param>
/// <exception cref="ArgumentNullException">The info parameter is null.</exception>
/// <exception cref="SerializationException">The class name is <see langword="null"/> or <see cref="Exception.HResult"/> is zero (0).</exception>
protected CloudflareException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
}
}

View File

@@ -0,0 +1,33 @@
using System;
using System.Linq;
using System.Runtime.Serialization;
namespace AMWD.Net.Api.Cloudflare
{
/// <summary>
/// Extension methods for <see cref="Enum"/>s.
/// </summary>
public static class EnumExtensions
{
/// <summary>
/// Gets the <see cref="EnumMemberAttribute.Value"/> of the <see cref="Enum"/> when available, otherwise the <see cref="Enum.ToString()"/>.
/// </summary>
/// <param name="value">The enum value.</param>
public static string GetEnumMemberValue(this Enum value)
{
var fieldInfo = value.GetType().GetField(value.ToString());
if (fieldInfo == null)
return value.ToString();
var enumMember = fieldInfo
.GetCustomAttributes(typeof(EnumMemberAttribute), inherit: false)
.Cast<EnumMemberAttribute>()
.FirstOrDefault();
if (enumMember == null)
return value.ToString();
return enumMember.Value;
}
}
}

View File

@@ -0,0 +1,16 @@
using System.Net.Http;
namespace AMWD.Net.Api.Cloudflare.Auth
{
/// <summary>
/// Defines the interface to add authentication information.
/// </summary>
public interface IAuthentication
{
/// <summary>
/// Adds authentication headers to the given <see cref="HttpClient"/>.
/// </summary>
/// <param name="httpClient">The <see cref="HttpClient"/> to add the headers to.</param>
void AddHeader(HttpClient httpClient);
}
}

View File

@@ -0,0 +1,75 @@
using System.Threading;
using System.Threading.Tasks;
namespace AMWD.Net.Api.Cloudflare
{
/// <summary>
/// Represents a client for the Cloudflare API.
/// </summary>
public interface ICloudflareClient
{
/// <summary>
/// Makes a GET request to the Cloudflare API.
/// </summary>
/// <remarks>
/// The GET method requests a representation of the specified resource.
/// Requests using GET should only retrieve data and should not contain a request content.
/// </remarks>
/// <typeparam name="TResponse">The response type.</typeparam>
/// <param name="requestPath">The request path (extending the base URL).</param>
/// <param name="queryFilter">The query parameters.</param>
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
Task<CloudflareResponse<TResponse>> GetAsync<TResponse>(string requestPath, IQueryParameterFilter queryFilter = null, CancellationToken cancellationToken = default);
/// <summary>
/// Makes a POST request to the Cloudflare API.
/// </summary>
/// <remarks>
/// The POST method submits an entity to the specified resource, often causing a change in state or side effects on the server.
/// </remarks>
/// <typeparam name="TResponse">The response type.</typeparam>
/// <typeparam name="TRequest">The request type.</typeparam>
/// <param name="requestPath">The request path (extending the base URL).</param>
/// <param name="request">The request content.</param>
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
Task<CloudflareResponse<TResponse>> PostAsync<TResponse, TRequest>(string requestPath, TRequest request, CancellationToken cancellationToken = default);
/// <summary>
/// Makes a PUT request to the Cloudflare API.
/// </summary>
/// <remarks>
/// The PUT method replaces all current representations of the target resource with the request content.
/// </remarks>
/// <typeparam name="TResponse">The response type.</typeparam>
/// <typeparam name="TRequest">The request type.</typeparam>
/// <param name="requestPath">The request path (extending the base URL).</param>
/// <param name="request">The request content.</param>
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
Task<CloudflareResponse<TResponse>> PutAsync<TResponse, TRequest>(string requestPath, TRequest request, CancellationToken cancellationToken = default);
/// <summary>
/// Makes a DELETE request to the Cloudflare API.
/// </summary>
/// <remarks>
/// The DELETE method deletes the specified resource.
/// </remarks>
/// <typeparam name="TResponse">The response type.</typeparam>
/// <param name="requestPath">The request path (extending the base URL).</param>
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
/// <returns></returns>
Task<CloudflareResponse<TResponse>> DeleteAsync<TResponse>(string requestPath, CancellationToken cancellationToken = default);
/// <summary>
/// Makes a PATCH request to the Cloudflare API.
/// </summary>
/// <remarks>
/// The PATCH method applies partial modifications to a resource.
/// </remarks>
/// <typeparam name="TResponse">The response type.</typeparam>
/// <typeparam name="TRequest">The request type.</typeparam>
/// <param name="requestPath">The request path (extending the base URL).</param>
/// <param name="request">The request content.</param>
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
Task<CloudflareResponse<TResponse>> PatchAsync<TResponse, TRequest>(string requestPath, TRequest request, CancellationToken cancellationToken = default);
}
}

View File

@@ -0,0 +1,15 @@
using System.Collections.Generic;
namespace AMWD.Net.Api.Cloudflare
{
/// <summary>
/// Represents filter options defined via query parameters.
/// </summary>
public interface IQueryParameterFilter
{
/// <summary>
/// Gets the query parameters.
/// </summary>
IDictionary<string, string> GetQueryParameters();
}
}

View File

@@ -0,0 +1,21 @@
namespace AMWD.Net.Api.Cloudflare
{
/// <summary>
/// Base implementation of an account.
/// </summary>
public class AccountBase
{
/// <summary>
/// Identifier
/// </summary>
// <= 32 characters
[JsonProperty("id")]
public string Id { get; set; }
/// <summary>
/// The name of the account.
/// </summary>
[JsonProperty("name")]
public string Name { get; set; }
}
}

View File

@@ -0,0 +1,27 @@
namespace AMWD.Net.Api.Cloudflare
{
/// <summary>
/// Base implementation of an owner.
/// </summary>
public class OwnerBase
{
/// <summary>
/// Identifier.
/// </summary>
// <= 32 characters
[JsonProperty("id")]
public string Id { get; set; }
/// <summary>
/// Name of the owner.
/// </summary>
[JsonProperty("name")]
public string Name { get; set; }
/// <summary>
/// The type of owner.
/// </summary>
[JsonProperty("type")]
public string Type { get; set; }
}
}

21
Cloudflare/README.md Normal file
View File

@@ -0,0 +1,21 @@
# Cloudflare API for .NET | Core
This is the core package for all extensions of the Cloudflare API implemented by [AM.WD].
## Contents
- The `(I)CloudflareClient` with base calls for `GET`, `POST`, `PUT`, `PATCH` and `DELETE` requests.
- Base classes to receive responses.
- `CloudflareException` to specify some errors.
- `IAuthentication` implementations to allow API-Token and API-Key (legacy) authentication.
Any specific request will be defined via extension packages.
---
Published under MIT License (see [choose a license])
[AM.WD]: https://www.nuget.org/packages?q=AMWD.&sortby=created-desc
[choose a license]: https://choosealicense.com/licenses/mit/

View File

@@ -0,0 +1,47 @@
using System.Collections.Generic;
namespace AMWD.Net.Api.Cloudflare
{
/// <summary>
/// The base Cloudflare response.
/// </summary>
public class CloudflareResponse
{
/// <summary>
/// Information about the result of the request.
/// </summary>
[JsonProperty("result_info")]
public ResultInfo ResultInfo { get; set; }
/// <summary>
/// Whether the API call was successful.
/// </summary>
[JsonProperty("success")]
public bool Success { get; set; }
/// <summary>
/// Errors returned by the API call.
/// </summary>
[JsonProperty("errors")]
public IReadOnlyList<ResponseInfo> Errors { get; set; } = [];
/// <summary>
/// Messages returned by the API call.
/// </summary>
[JsonProperty("messages")]
public IReadOnlyList<ResponseInfo> Messages { get; set; } = [];
}
/// <summary>
/// The base Cloudflare response with a result.
/// </summary>
/// <typeparam name="T"></typeparam>
public class CloudflareResponse<T> : CloudflareResponse
{
/// <summary>
/// The result of the API call.
/// </summary>
[JsonProperty("result")]
public T Result { get; set; }
}
}

View File

@@ -0,0 +1,20 @@
namespace AMWD.Net.Api.Cloudflare
{
/// <summary>
/// A Cloudflare response information.
/// </summary>
public class ResponseInfo
{
/// <summary>
/// The message code.
/// </summary>
[JsonProperty("code")]
public int Code { get; set; }
/// <summary>
/// The message.
/// </summary>
[JsonProperty("message")]
public string Message { get; set; }
}
}

View File

@@ -0,0 +1,32 @@
namespace AMWD.Net.Api.Cloudflare
{
/// <summary>
/// Cloudflare Result Information.
/// </summary>
public class ResultInfo
{
/// <summary>
/// Total number of results for the requested service.
/// </summary>
[JsonProperty("count")]
public int Count { get; set; }
/// <summary>
/// Current page within paginated list of results.
/// </summary>
[JsonProperty("page")]
public int Page { get; set; }
/// <summary>
/// Number of results per page of results.
/// </summary>
[JsonProperty("per_page")]
public int PerPage { get; set; }
/// <summary>
/// Total results available without any search parameters.
/// </summary>
[JsonProperty("total_count")]
public int TotalCount { get; set; }
}
}

337
CodeMaid.config Normal file
View File

@@ -0,0 +1,337 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<sectionGroup name="userSettings" type="System.Configuration.UserSettingsGroup, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" >
<section name="SteveCadwallader.CodeMaid.Properties.Settings" type="System.Configuration.ClientSettingsSection, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
</sectionGroup>
</configSections>
<userSettings>
<SteveCadwallader.CodeMaid.Properties.Settings>
<setting name="Cleaning_AutoCleanupOnFileSave" serializeAs="String">
<value>False</value>
</setting>
<setting name="Cleaning_ExcludeT4GeneratedCode" serializeAs="String">
<value>True</value>
</setting>
<setting name="Cleaning_ExclusionExpression" serializeAs="String">
<value>.*\.Designer\.cs||.*\.resx||packages.config||.*\.min\.js||.*\.min\.css</value>
</setting>
<setting name="Cleaning_IncludeCPlusPlus" serializeAs="String">
<value>True</value>
</setting>
<setting name="Cleaning_IncludeCSS" serializeAs="String">
<value>True</value>
</setting>
<setting name="Cleaning_IncludeCSharp" serializeAs="String">
<value>True</value>
</setting>
<setting name="Cleaning_IncludeFSharp" serializeAs="String">
<value>True</value>
</setting>
<setting name="Cleaning_IncludeHTML" serializeAs="String">
<value>True</value>
</setting>
<setting name="Cleaning_IncludeJSON" serializeAs="String">
<value>True</value>
</setting>
<setting name="Cleaning_IncludeJavaScript" serializeAs="String">
<value>True</value>
</setting>
<setting name="Cleaning_IncludeLESS" serializeAs="String">
<value>True</value>
</setting>
<setting name="Cleaning_IncludePHP" serializeAs="String">
<value>True</value>
</setting>
<setting name="Cleaning_IncludeSCSS" serializeAs="String">
<value>True</value>
</setting>
<setting name="Cleaning_IncludeTypeScript" serializeAs="String">
<value>True</value>
</setting>
<setting name="Cleaning_IncludeVB" serializeAs="String">
<value>True</value>
</setting>
<setting name="Cleaning_IncludeXAML" serializeAs="String">
<value>True</value>
</setting>
<setting name="Cleaning_IncludeXML" serializeAs="String">
<value>True</value>
</setting>
<setting name="Cleaning_InsertBlankLinePaddingAfterClasses" serializeAs="String">
<value>True</value>
</setting>
<setting name="Cleaning_InsertBlankLinePaddingAfterDelegates"
serializeAs="String">
<value>True</value>
</setting>
<setting name="Cleaning_InsertBlankLinePaddingAfterEndRegionTags"
serializeAs="String">
<value>True</value>
</setting>
<setting name="Cleaning_InsertBlankLinePaddingAfterEnumerations"
serializeAs="String">
<value>False</value>
</setting>
<setting name="Cleaning_InsertBlankLinePaddingAfterEvents" serializeAs="String">
<value>True</value>
</setting>
<setting name="Cleaning_InsertBlankLinePaddingAfterFieldsMultiLine"
serializeAs="String">
<value>False</value>
</setting>
<setting name="Cleaning_InsertBlankLinePaddingAfterInterfaces"
serializeAs="String">
<value>True</value>
</setting>
<setting name="Cleaning_InsertBlankLinePaddingAfterMethods" serializeAs="String">
<value>True</value>
</setting>
<setting name="Cleaning_InsertBlankLinePaddingAfterNamespaces"
serializeAs="String">
<value>True</value>
</setting>
<setting name="Cleaning_InsertBlankLinePaddingAfterPropertiesMultiLine"
serializeAs="String">
<value>False</value>
</setting>
<setting name="Cleaning_InsertBlankLinePaddingAfterPropertiesSingleLine"
serializeAs="String">
<value>False</value>
</setting>
<setting name="Cleaning_InsertBlankLinePaddingAfterRegionTags"
serializeAs="String">
<value>True</value>
</setting>
<setting name="Cleaning_InsertBlankLinePaddingAfterStructs" serializeAs="String">
<value>True</value>
</setting>
<setting name="Cleaning_InsertBlankLinePaddingAfterUsingStatementBlocks"
serializeAs="String">
<value>False</value>
</setting>
<setting name="Cleaning_InsertBlankLinePaddingBeforeCaseStatements"
serializeAs="String">
<value>False</value>
</setting>
<setting name="Cleaning_InsertBlankLinePaddingBeforeClasses"
serializeAs="String">
<value>True</value>
</setting>
<setting name="Cleaning_InsertBlankLinePaddingBeforeDelegates"
serializeAs="String">
<value>True</value>
</setting>
<setting name="Cleaning_InsertBlankLinePaddingBeforeEndRegionTags"
serializeAs="String">
<value>True</value>
</setting>
<setting name="Cleaning_InsertBlankLinePaddingBeforeEnumerations"
serializeAs="String">
<value>False</value>
</setting>
<setting name="Cleaning_InsertBlankLinePaddingBeforeEvents" serializeAs="String">
<value>True</value>
</setting>
<setting name="Cleaning_InsertBlankLinePaddingBeforeFieldsMultiLine"
serializeAs="String">
<value>False</value>
</setting>
<setting name="Cleaning_InsertBlankLinePaddingBeforeInterfaces"
serializeAs="String">
<value>True</value>
</setting>
<setting name="Cleaning_InsertBlankLinePaddingBeforeMethods"
serializeAs="String">
<value>True</value>
</setting>
<setting name="Cleaning_InsertBlankLinePaddingBeforeNamespaces"
serializeAs="String">
<value>True</value>
</setting>
<setting name="Cleaning_InsertBlankLinePaddingBeforePropertiesMultiLine"
serializeAs="String">
<value>False</value>
</setting>
<setting name="Cleaning_InsertBlankLinePaddingBeforePropertiesSingleLine"
serializeAs="String">
<value>False</value>
</setting>
<setting name="Cleaning_InsertBlankLinePaddingBeforeRegionTags"
serializeAs="String">
<value>True</value>
</setting>
<setting name="Cleaning_InsertBlankLinePaddingBeforeSingleLineComments"
serializeAs="String">
<value>False</value>
</setting>
<setting name="Cleaning_InsertBlankLinePaddingBeforeStructs"
serializeAs="String">
<value>True</value>
</setting>
<setting name="Cleaning_InsertBlankLinePaddingBeforeUsingStatementBlocks"
serializeAs="String">
<value>False</value>
</setting>
<setting name="Cleaning_InsertBlankLinePaddingBetweenPropertiesMultiLineAccessors"
serializeAs="String">
<value>False</value>
</setting>
<setting name="Cleaning_InsertBlankSpaceBeforeSelfClosingAngleBrackets"
serializeAs="String">
<value>False</value>
</setting>
<setting name="Cleaning_InsertEndOfFileTrailingNewLine" serializeAs="String">
<value>True</value>
</setting>
<setting name="Cleaning_InsertExplicitAccessModifiersOnClasses"
serializeAs="String">
<value>True</value>
</setting>
<setting name="Cleaning_InsertExplicitAccessModifiersOnDelegates"
serializeAs="String">
<value>True</value>
</setting>
<setting name="Cleaning_InsertExplicitAccessModifiersOnEnumerations"
serializeAs="String">
<value>True</value>
</setting>
<setting name="Cleaning_InsertExplicitAccessModifiersOnEvents"
serializeAs="String">
<value>True</value>
</setting>
<setting name="Cleaning_InsertExplicitAccessModifiersOnFields"
serializeAs="String">
<value>True</value>
</setting>
<setting name="Cleaning_InsertExplicitAccessModifiersOnInterfaces"
serializeAs="String">
<value>True</value>
</setting>
<setting name="Cleaning_InsertExplicitAccessModifiersOnMethods"
serializeAs="String">
<value>True</value>
</setting>
<setting name="Cleaning_InsertExplicitAccessModifiersOnProperties"
serializeAs="String">
<value>True</value>
</setting>
<setting name="Cleaning_InsertExplicitAccessModifiersOnStructs"
serializeAs="String">
<value>True</value>
</setting>
<setting name="Cleaning_RemoveBlankLinesAfterAttributes" serializeAs="String">
<value>True</value>
</setting>
<setting name="Cleaning_RemoveBlankLinesAfterOpeningBrace" serializeAs="String">
<value>True</value>
</setting>
<setting name="Cleaning_RemoveBlankLinesAtBottom" serializeAs="String">
<value>True</value>
</setting>
<setting name="Cleaning_RemoveBlankLinesAtTop" serializeAs="String">
<value>True</value>
</setting>
<setting name="Cleaning_RemoveBlankLinesBeforeClosingBrace" serializeAs="String">
<value>True</value>
</setting>
<setting name="Cleaning_RemoveBlankLinesBeforeClosingTags" serializeAs="String">
<value>True</value>
</setting>
<setting name="Cleaning_RemoveBlankLinesBetweenChainedStatements"
serializeAs="String">
<value>True</value>
</setting>
<setting name="Cleaning_RemoveBlankSpacesBeforeClosingAngleBrackets"
serializeAs="String">
<value>True</value>
</setting>
<setting name="Cleaning_RemoveEndOfFileTrailingNewLine" serializeAs="String">
<value>False</value>
</setting>
<setting name="Cleaning_RemoveEndOfLineWhitespace" serializeAs="String">
<value>True</value>
</setting>
<setting name="Cleaning_RemoveMultipleConsecutiveBlankLines"
serializeAs="String">
<value>True</value>
</setting>
<setting name="Cleaning_RemoveRegions" serializeAs="String">
<value>0</value>
</setting>
<setting name="Cleaning_RunVisualStudioFormatDocumentCommand"
serializeAs="String">
<value>True</value>
</setting>
<setting name="Cleaning_RunVisualStudioRemoveUnusedUsingStatements"
serializeAs="String">
<value>True</value>
</setting>
<setting name="Cleaning_RunVisualStudioSortUsingStatements" serializeAs="String">
<value>True</value>
</setting>
<setting name="Cleaning_SkipRemoveUnusedUsingStatementsDuringAutoCleanupOnSave"
serializeAs="String">
<value>True</value>
</setting>
<setting name="Cleaning_UpdateAccessorsToBothBeSingleLineOrMultiLine"
serializeAs="String">
<value>True</value>
</setting>
<setting name="Cleaning_UpdateEndRegionDirectives" serializeAs="String">
<value>True</value>
</setting>
<setting name="Cleaning_UpdateSingleLineMethods" serializeAs="String">
<value>False</value>
</setting>
<setting name="Cleaning_UsingStatementsToReinsertWhenRemovedExpression"
serializeAs="String">
<value>
</value>
</setting>
<setting name="Collapsing_CollapseSolutionWhenOpened" serializeAs="String">
<value>False</value>
</setting>
<setting name="Formatting_CommentRunDuringCleanup" serializeAs="String">
<value>False</value>
</setting>
<setting name="Formatting_CommentSkipWrapOnLastWord" serializeAs="String">
<value>True</value>
</setting>
<setting name="Formatting_CommentWrapColumn" serializeAs="String">
<value>100</value>
</setting>
<setting name="Formatting_CommentXmlAlignParamTags" serializeAs="String">
<value>False</value>
</setting>
<setting name="Formatting_CommentXmlKeepTagsTogether" serializeAs="String">
<value>False</value>
</setting>
<setting name="Formatting_CommentXmlSpaceSingleTags" serializeAs="String">
<value>False</value>
</setting>
<setting name="Formatting_CommentXmlSpaceTags" serializeAs="String">
<value>False</value>
</setting>
<setting name="Formatting_CommentXmlSplitAllTags" serializeAs="String">
<value>False</value>
</setting>
<setting name="Formatting_CommentXmlSplitSummaryTagToMultipleLines"
serializeAs="String">
<value>False</value>
</setting>
<setting name="Formatting_CommentXmlValueIndent" serializeAs="String">
<value>0</value>
</setting>
<setting name="General_ShowStartPageOnSolutionClose" serializeAs="String">
<value>False</value>
</setting>
<setting name="Progressing_HideBuildProgressOnBuildStop" serializeAs="String">
<value>False</value>
</setting>
<setting name="Progressing_ShowBuildProgressOnBuildStart" serializeAs="String">
<value>False</value>
</setting>
</SteveCadwallader.CodeMaid.Properties.Settings>
</userSettings>
</configuration>

20
Directory.Build.props Normal file
View File

@@ -0,0 +1,20 @@
<Project>
<PropertyGroup>
<LangVersion>12.0</LangVersion>
<Title>Modular Cloudflare API implementation in .NET</Title>
<Company>AM.WD</Company>
<Authors>Andreas Müller</Authors>
<Copyright>© {copyright:2024-} AM.WD</Copyright>
</PropertyGroup>
<PropertyGroup>
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>$(SolutionDir)/cloudflare-api.snk</AssemblyOriginatorKeyFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3"/>
<Using Include="Newtonsoft.Json"/>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<PackageId>AMWD.Net.API.Cloudflare.Zones</PackageId>
<PackageTags>cloudflare api zones</PackageTags>
<AssemblyName>amwd-cloudflare-zones</AssemblyName>
<RootNamespace>AMWD.Net.Api.Cloudflare.Zones</RootNamespace>
<Product>Cloudflare API - Zones</Product>
<Description>Zone management features of the Cloudflare API</Description>
</PropertyGroup>
<PropertyGroup Condition="$([System.Text.RegularExpressions.Regex]::IsMatch('$(CI_COMMIT_TAG)', '^zones\/v[0-9.]+'))">
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,20 @@
# Cloudflare API for .NET | Zones
With this extension package, you'll get all features available to manage a Zone on Cloudflare.
## Methods
- [ListZones](https://developers.cloudflare.com/api/operations/zones-get)
- [ZoneDetails](https://developers.cloudflare.com/api/operations/zones-0-get)
- [CreateZone](https://developers.cloudflare.com/api/operations/zones-post)
- [DeleteZone](https://developers.cloudflare.com/api/operations/zones-0-delete)
- [EditZone](https://developers.cloudflare.com/api/operations/zones-0-patch)
- TBD
---
Published under MIT License (see [choose a license])
[choose a license]: https://choosealicense.com/licenses/mit/

View File

@@ -0,0 +1,56 @@
<Project>
<PropertyGroup>
<NrtRevisionFormat>{semvertag:main}{!:-dev}</NrtRevisionFormat>
<AppendTargetFrameworkToOutputPath>true</AppendTargetFrameworkToOutputPath>
<CopyRefAssembliesToPublishDirectory>false</CopyRefAssembliesToPublishDirectory>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<RepositoryType>git</RepositoryType>
<RepositoryUrl>https://github.com/AM-WD/cloudflare-api.git</RepositoryUrl>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<PackageIcon>package-icon.png</PackageIcon>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageProjectUrl>https://developers.cloudflare.com/api</PackageProjectUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<EmbedUntrackedSources>false</EmbedUntrackedSources>
</PropertyGroup>
<PropertyGroup Condition="'$(GITLAB_CI)' == 'true'">
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
</PropertyGroup>
<ItemGroup Condition="'$(GITLAB_CI)' == 'true'">
<SourceLinkGitLabHost Include="$(CI_SERVER_HOST)" Version="$(CI_SERVER_VERSION)" />
<PackageReference Include="Microsoft.SourceLink.GitLab" Version="8.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<None Include="$(SolutionDir)/package-icon.png" Pack="true" PackagePath="/" />
<None Include="README.md" Pack="true" PackagePath="/" />
</ItemGroup>
<ItemGroup Condition="'$(Configuration)' == 'Release'">
<PackageReference Include="AMWD.Net.API.Cloudflare" Version="0.0.1" />
</ItemGroup>
<ItemGroup Condition="'$(Configuration)' == 'Debug'">
<ProjectReference Include="$(SolutionDir)\Cloudflare\Cloudflare.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="AMWD.NetRevisionTask" Version="1.2.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" />
</Project>

View File

@@ -9,8 +9,9 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
The above copyright notice and this permission notice (including the next
paragraph) shall be included in all copies or substantial portions of
the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,

34
README.md Normal file
View File

@@ -0,0 +1,34 @@
# Cloudflare API for .NET
This project aims to implement the [Cloudflare API] in an extensible way.
## Overview
There should be a package for each API section as defined by Cloudflare.
### [Cloudflare]
This is the base client, that will perform the request itself. It has the base url and holds the credentials.
### [Cloudflare.Zones]
If you install this package, you will get all methods to handle a DNS zone.
---
Published under [MIT License] (see [choose a license])
[![Buy me a Coffee](https://shields.am-wd.de/badge/PayPal-Buy_me_a_Coffee-yellow?style=flat&logo=paypal)](https://link.am-wd.de/donate)
[![built with Codeium](https://codeium.com/badges/main)](https://link.am-wd.de/codeium)
[Cloudflare]: Cloudflare/README.md
[Cloudflare.Zones]: Extensions/Cloudflare.Zones/README.md
[Cloudflare API]: https://developers.cloudflare.com/api/
[MIT License]: LICENSE.txt
[choose a license]: https://choosealicense.com/licenses/mit/

View File

@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="$(SolutionDir)\Extensions\Cloudflare.Zones\Cloudflare.Zones.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,28 @@
<Project>
<PropertyGroup>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<CollectCoverage>true</CollectCoverage>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.msbuild" Version="6.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="MSTest.TestAdapter" Version="3.6.1" />
<PackageReference Include="MSTest.TestFramework" Version="3.6.1" />
</ItemGroup>
<ItemGroup>
<Using Include="Microsoft.VisualStudio.TestTools.UnitTesting" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="$(SolutionDir)\Cloudflare\Cloudflare.csproj" />
</ItemGroup>
<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" />
</Project>

84
cloudflare-api.sln Normal file
View File

@@ -0,0 +1,84 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.10.35013.160
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cloudflare", "Cloudflare\Cloudflare.csproj", "{9D98650A-01CC-44B1-AC1E-D6323E1777C5}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{5D69F102-CF03-4175-8C59-D457450B28E0}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Extensions", "Extensions", "{EE760850-ED97-4493-B0AE-326289A60145}"
ProjectSection(SolutionItems) = preProject
Extensions\Directory.Build.props = Extensions\Directory.Build.props
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{BA35336B-7640-4C0C-B93E-06BDC1EE1872}"
ProjectSection(SolutionItems) = preProject
.gitlab-ci.yml = .gitlab-ci.yml
Directory.Build.props = Directory.Build.props
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "config", "config", "{E72A0B89-A37E-4BB3-B2EF-26AB24D3D716}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
.gitignore = .gitignore
CodeMaid.config = CodeMaid.config
nuget.config = nuget.config
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{5AF54083-1A93-4C43-B36C-EDD9E5DE0695}"
ProjectSection(SolutionItems) = preProject
CHANGELOG.md = CHANGELOG.md
LICENSE.txt = LICENSE.txt
package-icon.png = package-icon.png
README.md = README.md
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cloudflare.Zones", "Extensions\Cloudflare.Zones\Cloudflare.Zones.csproj", "{290D0987-D295-4DBD-9090-14D3DED63281}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "UnitTests", "UnitTests", "{A31B4929-190B-4AB8-984B-E284BB159F04}"
ProjectSection(SolutionItems) = preProject
UnitTests\Directory.Build.props = UnitTests\Directory.Build.props
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cloudflare.Tests", "Cloudflare.Tests\Cloudflare.Tests.csproj", "{2491D707-E845-49DF-8D94-0154AAD36E42}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cloudflare.Zones.Tests", "UnitTests\Cloudflare.Zones.Tests\Cloudflare.Zones.Tests.csproj", "{835705E5-D9F9-4236-9E25-898A851C8165}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{9D98650A-01CC-44B1-AC1E-D6323E1777C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9D98650A-01CC-44B1-AC1E-D6323E1777C5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9D98650A-01CC-44B1-AC1E-D6323E1777C5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9D98650A-01CC-44B1-AC1E-D6323E1777C5}.Release|Any CPU.Build.0 = Release|Any CPU
{290D0987-D295-4DBD-9090-14D3DED63281}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{290D0987-D295-4DBD-9090-14D3DED63281}.Debug|Any CPU.Build.0 = Debug|Any CPU
{290D0987-D295-4DBD-9090-14D3DED63281}.Release|Any CPU.ActiveCfg = Release|Any CPU
{290D0987-D295-4DBD-9090-14D3DED63281}.Release|Any CPU.Build.0 = Release|Any CPU
{2491D707-E845-49DF-8D94-0154AAD36E42}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2491D707-E845-49DF-8D94-0154AAD36E42}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2491D707-E845-49DF-8D94-0154AAD36E42}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2491D707-E845-49DF-8D94-0154AAD36E42}.Release|Any CPU.Build.0 = Release|Any CPU
{835705E5-D9F9-4236-9E25-898A851C8165}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{835705E5-D9F9-4236-9E25-898A851C8165}.Debug|Any CPU.Build.0 = Debug|Any CPU
{835705E5-D9F9-4236-9E25-898A851C8165}.Release|Any CPU.ActiveCfg = Release|Any CPU
{835705E5-D9F9-4236-9E25-898A851C8165}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{BA35336B-7640-4C0C-B93E-06BDC1EE1872} = {5D69F102-CF03-4175-8C59-D457450B28E0}
{E72A0B89-A37E-4BB3-B2EF-26AB24D3D716} = {5D69F102-CF03-4175-8C59-D457450B28E0}
{5AF54083-1A93-4C43-B36C-EDD9E5DE0695} = {5D69F102-CF03-4175-8C59-D457450B28E0}
{290D0987-D295-4DBD-9090-14D3DED63281} = {EE760850-ED97-4493-B0AE-326289A60145}
{835705E5-D9F9-4236-9E25-898A851C8165} = {A31B4929-190B-4AB8-984B-E284BB159F04}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {A465B60D-C946-4381-835C-29303EA4FAD1}
EndGlobalSection
EndGlobal

BIN
cloudflare-api.snk Normal file

Binary file not shown.