diff --git a/GreenArrow.Engine.Test/DKIMKeysApi/DKIMKeysApiTest.cs b/GreenArrow.Engine.Test/DKIMKeysApi/DKIMKeysApiTest.cs new file mode 100644 index 0000000..a2efff8 --- /dev/null +++ b/GreenArrow.Engine.Test/DKIMKeysApi/DKIMKeysApiTest.cs @@ -0,0 +1,156 @@ +using GreenArrow.Engine.DKIMKeysApi; +using GreenArrow.Engine.Extensions; +using GreenArrow.Engine.HttpSubmissionApi; +using GreenArrow.Engine.RestApi; +using Microsoft.Extensions.Options; +using Moq.Protected; +using System.Net; + +namespace GreenArrow.Engine.Test.HttpSubmissionApi +{ + public class DKIMKeysApiTest + { + private static readonly Fixture specimens = new(); + + private static IDKIMKeysApi CreateSut( + GreenArrowEngineSettings? settings = null, + IHttpClientFactory? httpClientFactory = null + ) + { + settings ??= new GreenArrowEngineSettings { ServerUri = $"https://localhost/" }; + + return new DKIMKeysApiClient( + options: Options.Create(settings), + httpFactory: httpClientFactory ?? Mock.Of() + ); + } + + private static Mock CreateHttpMessageHandlerMock(HttpResponseMessage httpResponseMessage) + { + var httpMessageHandlerMock = new Mock(); + httpMessageHandlerMock.Protected() + .Setup>(nameof(HttpClient.SendAsync), ItExpr.IsAny(), ItExpr.IsAny()) + .ReturnsAsync(httpResponseMessage); + + return httpMessageHandlerMock; + } + + private static Mock CreateHttpMessageHandlerMock(Exception exception) + { + var httpMessageHandlerMock = new Mock(); + httpMessageHandlerMock.Protected() + .Setup>(nameof(HttpClient.SendAsync), ItExpr.IsAny(), ItExpr.IsAny()) + .ThrowsAsync(exception); + + return httpMessageHandlerMock; + } + + private static Mock CreateHttpClientFactoryMock(HttpMessageHandler httpMessageHandler) + { + var httpClient = new HttpClient(httpMessageHandler); + + var httpClientFactoryMock = new Mock(); + httpClientFactoryMock + .Setup(_ => _.CreateClient(It.IsAny())) + .Returns(httpClient); + + return httpClientFactoryMock; + } + + [Fact] + public async Task Should_request_to_the_configured_endpoint_name() + { + // Arrange + var settings = new GreenArrowEngineSettings { ServerUri = $"https://localhost/", DKIMKeysAPIEndpoint = "/api/v3/eng/dkim_keys" }; + var httpResponseMessage = new HttpResponseMessage + { + StatusCode = specimens.Create(), + }; + + var httpMessageHandlerMock = CreateHttpMessageHandlerMock(httpResponseMessage); + var httpClientFactoryMock = CreateHttpClientFactoryMock(httpMessageHandlerMock.Object); + var sut = CreateSut(settings, httpClientFactory: httpClientFactoryMock.Object); + + var request = specimens.Create(); + + // Act + await sut.PostAsync(request, CancellationToken.None); + + // Assert + httpMessageHandlerMock.Protected().Verify( + nameof(HttpClient.SendAsync), + Times.Exactly(1), + ItExpr.Is(request => request.RequestUri.AbsolutePath.EndsWith(settings.DKIMKeysAPIEndpoint)), + ItExpr.IsAny() + ); + } + + [Fact] + public async Task Post_should_return_http_status_code_on_sucessfull() + { + // Arrange + var httpResponseMessage = new HttpResponseMessage + { + StatusCode = specimens.Create(), + }; + + var httpMessageHandlerMock = CreateHttpMessageHandlerMock(httpResponseMessage); + var httpClientFactoryMock = CreateHttpClientFactoryMock(httpMessageHandlerMock.Object); + var sut = CreateSut(httpClientFactory: httpClientFactoryMock.Object); + + var request = specimens.Create(); + + // Act + var result = await sut.PostAsync(request, CancellationToken.None); + + // Assert + Assert.Equal(httpResponseMessage.StatusCode, result.HttpStatusCode); + } + + [Fact] + public async Task Post_should_throw_RestApiException_upon_an_exception_in_the_implementation() + { + // Arrange + var exception = specimens.Create(); + + var httpMessageHandlerMock = CreateHttpMessageHandlerMock(exception); + var httpClientFactoryMock = CreateHttpClientFactoryMock(httpMessageHandlerMock.Object); + var sut = CreateSut(httpClientFactory: httpClientFactoryMock.Object); + + var request = specimens.Create(); + var cancellationToken = specimens.Create(); + + // Act + var result = await Assert.ThrowsAsync(() => sut.PostAsync(request, cancellationToken)); + + // Assert + Assert.Equal(exception, result.InnerException); + } + + [Fact] + public async Task Post_should_return_deserialized_response_content_on_sucessfull() + { + // Arrange + var DKIMKeysResponse = specimens.Create(); + var httpContent = new StringContent(DKIMKeysResponse.ToJson(true)); + + var httpResponseMessage = new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = httpContent, + }; + + var httpMessageHandlerMock = CreateHttpMessageHandlerMock(httpResponseMessage); + var httpClientFactoryMock = CreateHttpClientFactoryMock(httpMessageHandlerMock.Object); + var sut = CreateSut(httpClientFactory: httpClientFactoryMock.Object); + + var request = specimens.Create(); + + // Act + var result = await sut.PostAsync(request, CancellationToken.None); + + // Assert + Assert.Equal(DKIMKeysResponse.Success, result.Content.Success); + } + } +} diff --git a/GreenArrow.Engine/DKIMKeysApi/DKIMKeysApiClient.cs b/GreenArrow.Engine/DKIMKeysApi/DKIMKeysApiClient.cs new file mode 100644 index 0000000..a2e3eed --- /dev/null +++ b/GreenArrow.Engine/DKIMKeysApi/DKIMKeysApiClient.cs @@ -0,0 +1,79 @@ +using GreenArrow.Engine.Extensions; +using GreenArrow.Engine.RestApi; +using Microsoft.Extensions.Options; +using System.Net; +using System.Net.Http.Headers; + +namespace GreenArrow.Engine.DKIMKeysApi +{ + /// + /// DKIM Keys API client implementation + /// + public class DKIMKeysApiClient : IDKIMKeysApi + { + private readonly GreenArrowEngineSettings _settings; + private readonly IHttpClientFactory _httpFactory; + + private readonly string _endpoint; + + /// + /// Initializes a new instance of Green Arrow DKIM Keys API Client + /// + /// Green Arrow settings with API Url and Authorization Token + /// HttpClienFactory for create HttpClient objects + public DKIMKeysApiClient( + IOptions options, + IHttpClientFactory httpFactory) + { + _settings = options.Value; + _httpFactory = httpFactory; + _endpoint = GetEndPoint(); + } + + private HttpClient CreateHttpClient() + { + var client = _httpFactory.CreateClient(); + return client; + } + + private string GetEndPoint() + { + var baseUri = new Uri(_settings.ServerUri); + var endpointUri = new Uri(baseUri, _settings.DKIMKeysAPIEndpoint); + return endpointUri.ToString(); + } + + /// + public async Task> PostAsync(DKIMKeysRequest request, CancellationToken cancellationToken = default) + { + try + { + var jsonContent = request.ToJson(); + + var client = CreateHttpClient(); + + var authenticationString = $"{request.Username}:{request.Password}"; + var base64EncodedAuthenticationString = Convert.ToBase64String(System.Text.ASCIIEncoding.ASCII.GetBytes(authenticationString)); + + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", base64EncodedAuthenticationString); + + var httpContent = new StringContent(jsonContent, encoding: default, mediaType: "application/json"); + var httpResponse = await client.PostAsync(_endpoint, httpContent, cancellationToken); + + if (httpResponse.StatusCode == HttpStatusCode.OK) + { + var result = await httpResponse.Content.ReadAsStringAsync(cancellationToken); + var content = result.ToObject(); + return new RestApiResponse(httpResponse.StatusCode, content); + } + + return new RestApiResponse(httpResponse.StatusCode); + + } + catch (Exception exception) + { + throw new RestApiException("Unexpected exception", exception); + } + } + } +} diff --git a/GreenArrow.Engine/DKIMKeysApi/DKIMKeysRequest.cs b/GreenArrow.Engine/DKIMKeysApi/DKIMKeysRequest.cs new file mode 100644 index 0000000..d910fcb --- /dev/null +++ b/GreenArrow.Engine/DKIMKeysApi/DKIMKeysRequest.cs @@ -0,0 +1,29 @@ +using GreenArrow.Engine.Model; +using GreenArrow.Engine.RestApi; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace GreenArrow.Engine.DKIMKeysApi +{ + /// + /// Create a DKIM Key Request + /// + [JsonObject(NamingStrategyType = typeof(SnakeCaseNamingStrategy), ItemNullValueHandling = NullValueHandling.Ignore)] + public class DKIMKeysRequest : IRestApiModel + { + /// + /// Username that is authorized to log in to GreenArrow Engine’s web interface. + /// + public string Username { get; set; } + + /// + /// Password that is authorized to log in to GreenArrow Engine’s web interface. + /// + public string Password { get; set; } + + /// + /// To create the private key + /// + public DkimKey DkimKey { get; set; } + } +} diff --git a/GreenArrow.Engine/DKIMKeysApi/DKIMKeysResponse.cs b/GreenArrow.Engine/DKIMKeysApi/DKIMKeysResponse.cs new file mode 100644 index 0000000..b3ba93f --- /dev/null +++ b/GreenArrow.Engine/DKIMKeysApi/DKIMKeysResponse.cs @@ -0,0 +1,42 @@ +using GreenArrow.Engine.Model; +using GreenArrow.Engine.RestApi; + +namespace GreenArrow.Engine.DKIMKeysApi +{ + /// + /// GreenArrow Response is the full DKIM Key record data + /// + public class DKIMKeysResponse : IRestApiModel + { + /// + /// When request was succesful created + /// + public bool Success { get; init; } + + /// + /// Full DKIM Key record data + /// + public DKIMKeysResponseData Data { get; init; } + + /// + /// Error code when request was not accepted + /// + public string ErrorCode { get; init; } + + /// + /// Error message when request was not accepted + /// + public string ErrorMessages { get; init; } + } + + /// + /// Full DKIM Key record data + /// + public class DKIMKeysResponseData + { + /// + /// Full DKIM Key record data + /// + public DkimKey DkimKey { get; init; } + } +} diff --git a/GreenArrow.Engine/DKIMKeysApi/IDKIMKeysApi.cs b/GreenArrow.Engine/DKIMKeysApi/IDKIMKeysApi.cs new file mode 100644 index 0000000..e31f90d --- /dev/null +++ b/GreenArrow.Engine/DKIMKeysApi/IDKIMKeysApi.cs @@ -0,0 +1,19 @@ +using GreenArrow.Engine.RestApi; + +namespace GreenArrow.Engine.DKIMKeysApi +{ + /// + /// Represent the actions available in Green Arrow Engine DKIM Keys API + /// + /// + public interface IDKIMKeysApi + { + /// + /// Create a DKIM Key + /// + /// + /// The cancellation token + /// A generic rest api response with the deserialized DKIM Keys API response when success + Task> PostAsync(DKIMKeysRequest request, CancellationToken cancellationToken = default); + } +} diff --git a/GreenArrow.Engine/GreenArrowEngineSettings.cs b/GreenArrow.Engine/GreenArrowEngineSettings.cs index 48d477f..0237531 100644 --- a/GreenArrow.Engine/GreenArrowEngineSettings.cs +++ b/GreenArrow.Engine/GreenArrowEngineSettings.cs @@ -14,5 +14,10 @@ public class GreenArrowEngineSettings /// The HTTP Submission API Endpoint /// public string HTTPSubmissionAPIEndpoint { get; set; } = "/api/v1/send.json"; + + /// + /// The DKIM API Endpoint + /// + public string DKIMKeysAPIEndpoint { get; set; } = "/api/v3/eng/dkim_keys"; } } diff --git a/GreenArrow.Engine/HttpSubmissionApi/IHttpSubmissionApi.cs b/GreenArrow.Engine/HttpSubmissionApi/IHttpSubmissionApi.cs index dba0f44..a76dfbe 100755 --- a/GreenArrow.Engine/HttpSubmissionApi/IHttpSubmissionApi.cs +++ b/GreenArrow.Engine/HttpSubmissionApi/IHttpSubmissionApi.cs @@ -12,7 +12,7 @@ public interface IHttpSubmissionApi /// Submit messages for delivery /// /// - /// The cancellation toekn + /// The cancellation token /// A generic rest api response with the deserialized Http Submission API response when success Task> PostAsync(HttpSubmissionRequest request, CancellationToken cancellationToken = default); @@ -20,7 +20,7 @@ public interface IHttpSubmissionApi /// Submit messages for delivery /// /// - /// The cancellation toekn + /// The cancellation token /// A generic rest api response with the deserialized Http Submission API response when success Task> PutAsync(HttpSubmissionRequest request, CancellationToken cancellationToken = default); } diff --git a/GreenArrow.Engine/Model/DkimKey.cs b/GreenArrow.Engine/Model/DkimKey.cs new file mode 100644 index 0000000..162dedd --- /dev/null +++ b/GreenArrow.Engine/Model/DkimKey.cs @@ -0,0 +1,53 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace GreenArrow.Engine.Model +{ + /// + /// Dkim Key Data + /// + [JsonObject(NamingStrategyType = typeof(SnakeCaseNamingStrategy), ItemNullValueHandling = NullValueHandling.Ignore)] + public class DkimKey + { + /// + /// The domain name associated with this DKIM Key. + /// + public string Domain { get; init; } + + /// + /// The selector used to identify this DKIM Key. + /// + public string Selector { get; init; } + + /// + /// Whether this is the default DKIM key for this domain. + /// + public string DefaultForDomain { get; init; } + + /// + /// The key data + /// + public Key Key { get; init; } + } + + /// + /// The key data + /// + public class Key + { + /// + /// The number of bits used to generate this key. + /// + public int Bits { get; init; } + + /// + /// The PEM-encoded private key. + /// + public string Private { get; init; } + + /// + /// The public key, derived from the private key. This is PEM-encoded with the header line, footer line, and line-breaks stripped. + /// + public string Public { get; init; } + } +}