diff --git a/README.md b/README.md index f2a7add..2b51aae 100644 --- a/README.md +++ b/README.md @@ -372,6 +372,24 @@ await httpClient.PostAsync("/api/infos", new StringContent("test", Encoding.ASCI the response returned from the handler will be `415 Unsupported Media Type` + +### Handle requests only if they match the authorization header + +For some tests you might want to configure the handler to return when the correct authorization token is provided. + +To do this you can configure the handler like so: + +```csharp +handler + .RespondTo() + .Post() + .ForUrl("/search") + .AndAuthorization(new AuthenticationHeaderValue("Scheme", "Value")) + .With(HttpStatusCode.OK); +``` + +This allows you to configure multiple responses to a url depending on the authorization of a request. + ### Handle requests only if they match the request body For some tests you might want to configure the handler to return certain responses based on the request content. diff --git a/src/Codenizer.HttpClient.Testable/IRequestBuilder.cs b/src/Codenizer.HttpClient.Testable/IRequestBuilder.cs index da20b88..6f2e87e 100644 --- a/src/Codenizer.HttpClient.Testable/IRequestBuilder.cs +++ b/src/Codenizer.HttpClient.Testable/IRequestBuilder.cs @@ -1,5 +1,6 @@ using System; using System.Net; +using System.Net.Http.Headers; namespace Codenizer.HttpClient.Testable { @@ -87,6 +88,13 @@ public interface IRequestBuilder /// A MIME content type (for example text/plain) /// The current instance IRequestBuilder AndContentType(string contentType); + + /// + /// Respond to a request that matches the authorization header + /// + /// A authorization header value + /// The current instance + IRequestBuilder AndAuthorization(AuthenticationHeaderValue authenticationHeader); /// /// Respond to a request that matches the accept header diff --git a/src/Codenizer.HttpClient.Testable/RequestBuilder.cs b/src/Codenizer.HttpClient.Testable/RequestBuilder.cs index 4a72861..10264d9 100644 --- a/src/Codenizer.HttpClient.Testable/RequestBuilder.cs +++ b/src/Codenizer.HttpClient.Testable/RequestBuilder.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Net; using System.Net.Http; +using System.Net.Http.Headers; using System.Threading.Tasks; using Newtonsoft.Json; @@ -118,6 +119,12 @@ internal RequestBuilder(HttpMethod method, string pathAndQuery, string? contentT /// Optional. The value of the Accept header to match. /// public string? Accept { get; private set; } + + + /// + /// Optional. The value of the Accept header to match. + /// + public AuthenticationHeaderValue? AuthorizationHeader { get; private set; } /// /// Optional. The expected content of the request. @@ -245,6 +252,11 @@ public IRequestBuilder AndContentType(string contentType) return this; } + public IRequestBuilder AndAuthorization(AuthenticationHeaderValue authenticationHeader) { + AuthorizationHeader = authenticationHeader; + return this; + } + /// public IRequestBuilder Accepting(string mimeType) { @@ -396,6 +408,11 @@ internal Dictionary BuildRequestHeaders() { var headers = new Dictionary(); + if (AuthorizationHeader is not null) + { + headers.Add("Authorization", AuthorizationHeader.ToString()); + } + if (!string.IsNullOrEmpty(Accept)) { headers.Add("Accept", Accept!); diff --git a/test/Codenizer.HttpClient.Testable.Tests.Unit/WhenMatchingRoutes.cs b/test/Codenizer.HttpClient.Testable.Tests.Unit/WhenMatchingRoutes.cs index b0b0d56..af13720 100644 --- a/test/Codenizer.HttpClient.Testable.Tests.Unit/WhenMatchingRoutes.cs +++ b/test/Codenizer.HttpClient.Testable.Tests.Unit/WhenMatchingRoutes.cs @@ -1,6 +1,10 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; using System.Net.Http; using System.Net.Http.Headers; +using System.Runtime.CompilerServices; using FluentAssertions; using Xunit; @@ -9,7 +13,7 @@ namespace Codenizer.HttpClient.Testable.Tests.Unit public static class ConfiguredRequestsExtensions { internal static RequestBuilder? Match(this ConfiguredRequests configuredRequests, HttpMethod method, string uri, - string? accept) + string? accept, AuthenticationHeaderValue? authorization) { var requestMessage = new HttpRequestMessage(method, uri); if (!string.IsNullOrEmpty(accept)) @@ -17,6 +21,10 @@ public static class ConfiguredRequestsExtensions requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(accept)); } + if (authorization != null) { + requestMessage.Headers.Authorization = authorization; + } + return configuredRequests.Match(requestMessage); } } @@ -39,7 +47,7 @@ public void GivenPathWithQueryParameters_ReturnedRequestBuilderMatches() .Match( HttpMethod.Get, "/api/foo/bar?blah=blurb", - null) + null, null) .Should() .Be(requestBuilder); } @@ -60,10 +68,57 @@ public void GivenNonConfigured_NoResultIsReturned() .Match( HttpMethod.Get, "/api/baz", - null) + null, null) .Should() .BeNull(); } + + + [Fact] + public void GivenRouteWithAuthorizationHeader_RequestBuilderIsReturned() + { + var requestBuilder = new RequestBuilder(HttpMethod.Get, "/api/foos/{id}", null); + requestBuilder.AndAuthorization(new AuthenticationHeaderValue("Bearer")); + + var routes = new List + { + requestBuilder + }; + + var dictionary = ConfiguredRequests.FromRequestBuilders(routes); + + dictionary + .Match( + HttpMethod.Get, + "/api/foos/1234", + null, + new AuthenticationHeaderValue("Bearer")) + .Should() + .Be(requestBuilder); + } + + + [Fact] + public void GivenRouteAuthorizationHeaderAndEmptyRequestAuthorization_RequestBuilderIsNotReturned() + { + var requestBuilder = new RequestBuilder(HttpMethod.Get, "/api/foos/{id}", null); + requestBuilder.AndAuthorization(new AuthenticationHeaderValue("Bearer")); + + var routes = new List + { + requestBuilder + }; + + var dictionary = ConfiguredRequests.FromRequestBuilders(routes); + + dictionary + .Match( + HttpMethod.Get, + "/api/foos/1234", + null, null) + .Should() + .Be(null); + } [Fact] public void GivenRouteWithParameter_RequestBuilderIsReturned() @@ -81,7 +136,7 @@ public void GivenRouteWithParameter_RequestBuilderIsReturned() .Match( HttpMethod.Get, "/api/foos/1234", - null) + null, null) .Should() .Be(requestBuilder); } @@ -102,7 +157,7 @@ public void GivenRouteWithParameterAndQueryString_RequestBuilderIsReturned() .Match( HttpMethod.Get, "/api/foos/1234/?blah=baz", - null) + null, null) .Should() .Be(requestBuilder); } @@ -123,7 +178,7 @@ public void GivenRouteWithParameterAndQueryStringWithoutSeparator_RequestBuilder .Match( HttpMethod.Get, "/api/foos/1234?blah=baz", - null) + null, null) .Should() .Be(requestBuilder); } @@ -143,7 +198,7 @@ public void GivenRouteWithParameterAndQueryStringWithoutSeparatorX_RequestBuilde .Match( HttpMethod.Get, "/api/foos/1234?blah=qux", - null) + null, null) .Should() .Be(routes[1]); } @@ -165,7 +220,7 @@ public void GivenResponseWithAcceptHeaderAndNoAcceptHeaderInRequest_RequestDoesN .Match( HttpMethod.Get, "/api/foo", - null) + null, null) .Should() .BeNull(); } @@ -187,7 +242,7 @@ public void GivenResponseWithAcceptHeaderAndAcceptHeaderInRequestMatches_Respons .Match( HttpMethod.Get, "/api/foo", - "foo/bar") + "foo/bar", null) .Should() .NotBeNull(); } @@ -209,7 +264,7 @@ public void GivenResponseWithAcceptHeaderAndAcceptHeaderInRequestDoesNotMatch_Re .Match( HttpMethod.Get, "/api/foo", - "derp/derp") + "derp/derp", null) .Should() .BeNull(); } @@ -234,7 +289,7 @@ public void GivenTwoResponsesWithDifferentAcceptHeaderAndAcceptHeaderInRequestMa .Match( HttpMethod.Get, "/api/foo", - "baz/quux") + "baz/quux", null) .Should() .BeOfType() .Which @@ -263,7 +318,7 @@ public void GivenTwoResponsesWithDifferentAcceptHeaderAndAcceptHeaderInRequestMa .Match( HttpMethod.Get, "/api/foo?bar=baz", - "baz/quux") + "baz/quux", null) .Should() .BeOfType() .Which @@ -272,6 +327,41 @@ public void GivenTwoResponsesWithDifferentAcceptHeaderAndAcceptHeaderInRequestMa .Be("baz/quux"); } + [Fact] + public void GivenTwoResponsesWithDifferentAuthorizationInRequest_ResponseBuilderIsReturned() { + var requestBuilderOne = new RequestBuilder(HttpMethod.Get, "/api/foo?bar=baz", null) + .AndAuthorization(new AuthenticationHeaderValue("BEARER", "Value")); + var requestBuilderTwo = new RequestBuilder(HttpMethod.Get, "/api/foo?bar=baz", null) + .AndAuthorization(new AuthenticationHeaderValue("BEARER")); + + var routes = new List + { + (RequestBuilder)requestBuilderOne, + (RequestBuilder)requestBuilderTwo + }; + + var dictionary = ConfiguredRequests.FromRequestBuilders(routes); + + dictionary + .Match( + HttpMethod.Get, + "/api/foo?bar=baz", + "baz/quux", + new AuthenticationHeaderValue("BEARER", "Value")) + .Should() + .Be(requestBuilderOne); + + + dictionary + .Match( + HttpMethod.Get, + "/api/foo?bar=baz", + "baz/quux", + new AuthenticationHeaderValue("BEARER")) + .Should() + .Be(requestBuilderTwo); + } + [Fact] public void GivenRouteHasExtraPartInPath_ShouldNotReturnAMatch() { @@ -282,7 +372,7 @@ public void GivenRouteHasExtraPartInPath_ShouldNotReturnAMatch() var dictionary = ConfiguredRequests.FromRequestBuilders(routes); - dictionary.Match(HttpMethod.Post, "/api/v2/foos/1/bla-bla", "application/json") + dictionary.Match(HttpMethod.Post, "/api/v2/foos/1/bla-bla", "application/json", null) .Should() .BeNull(); }