diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..92ed11e
--- /dev/null
+++ b/.editorconfig
@@ -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
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000..0778d47
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -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
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..1021240
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1 @@
+# Changelog
diff --git a/Cloudflare.Tests/Auth/ApiKeyAuthenticationTest.cs b/Cloudflare.Tests/Auth/ApiKeyAuthenticationTest.cs
new file mode 100644
index 0000000..e676c55
--- /dev/null
+++ b/Cloudflare.Tests/Auth/ApiKeyAuthenticationTest.cs
@@ -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
+ }
+ }
+}
diff --git a/Cloudflare.Tests/Auth/ApiTokenAuthenticationTest.cs b/Cloudflare.Tests/Auth/ApiTokenAuthenticationTest.cs
new file mode 100644
index 0000000..1b88408
--- /dev/null
+++ b/Cloudflare.Tests/Auth/ApiTokenAuthenticationTest.cs
@@ -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
+ }
+ }
+}
diff --git a/Cloudflare.Tests/Cloudflare.Tests.csproj b/Cloudflare.Tests/Cloudflare.Tests.csproj
new file mode 100644
index 0000000..0ef22b7
--- /dev/null
+++ b/Cloudflare.Tests/Cloudflare.Tests.csproj
@@ -0,0 +1,30 @@
+
+
+
+ net8.0
+
+ false
+ true
+ true
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Cloudflare.Tests/CloudflareClientTest.cs b/Cloudflare.Tests/CloudflareClientTest.cs
new file mode 100644
index 0000000..385f084
--- /dev/null
+++ b/Cloudflare.Tests/CloudflareClientTest.cs
@@ -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 _httpHandlerMock;
+ private Mock _clientOptionsMock;
+ private Mock _authenticationMock;
+
+ [TestInitialize]
+ public void Initialize()
+ {
+ _httpHandlerMock = new Mock();
+ _authenticationMock = new Mock();
+ _clientOptionsMock = new Mock();
+
+ _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());
+ _clientOptionsMock.Setup(o => o.DefaultQueryParams).Returns(new Dictionary());
+ _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()), 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;
+ }
+ }
+}
diff --git a/Cloudflare.Tests/CloudflareClientTests/DeleteAsyncTest.cs b/Cloudflare.Tests/CloudflareClientTests/DeleteAsyncTest.cs
new file mode 100644
index 0000000..4b77b1c
--- /dev/null
+++ b/Cloudflare.Tests/CloudflareClientTests/DeleteAsyncTest.cs
@@ -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 _clientOptionsMock;
+ private Mock _authenticationMock;
+
+ [TestInitialize]
+ public void Initialize()
+ {
+ _httpHandlerMock = new HttpMessageHandlerMock();
+ _authenticationMock = new Mock();
+ _clientOptionsMock = new Mock();
+
+ _authenticationMock
+ .Setup(a => a.AddHeader(It.IsAny()))
+ .Callback(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());
+ _clientOptionsMock.Setup(o => o.DefaultQueryParams).Returns(new Dictionary());
+ _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