diff --git a/Website.Tests/Data/ApiClientTests.cs b/Website.Tests/Data/ApiClientTests.cs new file mode 100644 index 0000000..fdd56b9 --- /dev/null +++ b/Website.Tests/Data/ApiClientTests.cs @@ -0,0 +1,83 @@ +using System; +using System.Net.Http; +using System.Threading.Tasks; +using FluentAssertions; +using RichardSzalay.MockHttp; +using Website.Data; +using Xunit; + +namespace Website.Tests.Data { + public class ApiClientTests { + private class TestApiClient : ApiClient { + public TestApiClient(HttpClient client) : base(client) { + } + + public async Task Post(string url, object value, object query = null) => await base.Post(url, value, query); + + public async Task Get(string url, object query = null) => await base.Get(url, query); + } + + private class TestObject { + public int TestProperty { get; set; } + } + + private static HttpClient MakeHttpClient(HttpMethod method, string url, string response) { + var httpMessageHandler = new MockHttpMessageHandler(); + httpMessageHandler.When(method, url).Respond("application/json", response); + + var httpClient = httpMessageHandler.ToHttpClient(); + httpClient.BaseAddress = new Uri("http://example.com"); + return httpClient; + } + + [Fact] + public async Task Get_WithUrl_ReturnsResult() { + var httpClient = MakeHttpClient(HttpMethod.Get, "/test", @"{""TestProperty"":1}"); + + var expected = new TestObject {TestProperty = 1}; + + var client = new TestApiClient(httpClient); + (await client.Get("/test")).Should().BeEquivalentTo(expected); + } + + [Fact] + public async Task Get_WithUrlAndQuery_ReturnsResult() { + var httpClient = MakeHttpClient(HttpMethod.Get, "/test?query1=1&query2=2", @"{""TestProperty"":1}"); + + var expected = new TestObject {TestProperty = 1}; + + var client = new TestApiClient(httpClient); + (await client.Get("/test", new {query1 = 1, query2 = 2})).Should().BeEquivalentTo(expected); + } + + [Fact] + public void Get_WithUrlThatReturns404_ThrowsException() { + var httpClient = MakeHttpClient(HttpMethod.Get, "/test", ""); + + var client = new TestApiClient(httpClient); + + client.Invoking(apiClient => apiClient.Get("/404")).Should().Throw() + .WithMessage("Error calling API http://example.com/404: 404, No matching mock handler for \"GET http://example.com/404\""); + } + + [Fact] + public async Task Post_WithUrlAndValue_ReturnsResult() { + var httpClient = MakeHttpClient(HttpMethod.Post, "/test", @"{""TestProperty"":1}"); + + var expected = new TestObject {TestProperty = 1}; + + var client = new TestApiClient(httpClient); + (await client.Post("/test", "value")).Should().BeEquivalentTo(expected); + } + + [Fact] + public async Task Post_WithUrlAndValueAndQuery_ReturnsResult() { + var httpClient = MakeHttpClient(HttpMethod.Post, "/test?query1=1&query2=2", @"{""TestProperty"":1}"); + + var expected = new TestObject {TestProperty = 1}; + + var client = new TestApiClient(httpClient); + (await client.Post("/test", "value", new {query1 = 1, query2 = 2})).Should().BeEquivalentTo(expected); + } + } +} diff --git a/Website.Tests/Website.Tests.csproj b/Website.Tests/Website.Tests.csproj index 9bfd06a..3a4ffb3 100644 --- a/Website.Tests/Website.Tests.csproj +++ b/Website.Tests/Website.Tests.csproj @@ -16,6 +16,7 @@ + all diff --git a/Website/Data/ApiCallException.cs b/Website/Data/ApiCallException.cs new file mode 100644 index 0000000..9c66f99 --- /dev/null +++ b/Website/Data/ApiCallException.cs @@ -0,0 +1,12 @@ +using System; +using System.Net.Http; + +namespace Website.Data { + public class ApiCallException : Exception { + private readonly HttpResponseMessage Response; + + public ApiCallException(HttpResponseMessage response):base($"Error calling API {response.RequestMessage.RequestUri}: {(int)response.StatusCode}, {response.ReasonPhrase}") { + Response = response; + } + } +} diff --git a/Website/Data/ApiClient.cs b/Website/Data/ApiClient.cs new file mode 100644 index 0000000..08f831a --- /dev/null +++ b/Website/Data/ApiClient.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.WebUtilities; +using Newtonsoft.Json; + +namespace Website.Data { + public abstract class ApiClient { + private readonly HttpClient _client; + + protected ApiClient(HttpClient client) { + _client = client; + } + + private IDictionary ParseQueryParameters(object query) { + var type = query.GetType(); + var props = type.GetProperties(); + return props.ToDictionary(info => info.Name, info => info.GetValue(query, null).ToString()); + } + + private async Task Send(HttpMethod method, string url, object query, HttpContent content = null) { + if (query != null) + url = QueryHelpers.AddQueryString(url, ParseQueryParameters(query)); + + using var httpRequest = new HttpRequestMessage(method, url) { Content = content }; + var response = await _client.SendAsync(httpRequest); + + if (!response.IsSuccessStatusCode) + throw new ApiCallException(response); + + var json = await response.Content.ReadAsStringAsync(); + return JsonConvert.DeserializeObject(json); + } + + protected async Task Post(string url, object value, object query = null) { + var json = JsonConvert.SerializeObject(value); + using var requestBody = new StringContent(json, Encoding.UTF8, "application/json"); + return await Send(HttpMethod.Post, url, query, requestBody); + } + + protected async Task Get(string url, object query = null) { + return await Send(HttpMethod.Get, url, query); + } + + } +}