From 7f17b81bc31787d7cb846a52498fd89b744776e4 Mon Sep 17 00:00:00 2001 From: Pure Krome Date: Wed, 15 Feb 2017 12:51:32 +1100 Subject: [PATCH] Add Request Header support to HttpOptions. (#17) Fixes #16 --- ReleaseNotes.md | 9 ++ src/HttpClient.Helpers/FakeMessageHandler.cs | 60 ++++++++- .../HttpClient.Helpers.nuspec | 1 + src/HttpClient.Helpers/HttpMessageOptions.cs | 15 ++- .../HttpClient.Helpers.Tests/GetAsyncTests.cs | 125 +++++++++++++++++- 5 files changed, 200 insertions(+), 10 deletions(-) create mode 100644 ReleaseNotes.md diff --git a/ReleaseNotes.md b/ReleaseNotes.md new file mode 100644 index 0000000..5ef1eea --- /dev/null +++ b/ReleaseNotes.md @@ -0,0 +1,9 @@ +# Release Notes + +### v5.1.0 (2017-02-06) +**Features** +- Added support for `Headers` in `HttpMessageOptions`. + + +### v1.0.0 -> v5.0.0 +- Inital and subsequent releases in supporting faking an `HttpClient` request/response. \ No newline at end of file diff --git a/src/HttpClient.Helpers/FakeMessageHandler.cs b/src/HttpClient.Helpers/FakeMessageHandler.cs index be8196b..09f9f55 100644 --- a/src/HttpClient.Helpers/FakeMessageHandler.cs +++ b/src/HttpClient.Helpers/FakeMessageHandler.cs @@ -20,9 +20,9 @@ public class FakeHttpMessageHandler : HttpClientHandler /// /// TIP: If you have a requestUri = "*", this is a catch-all ... so if none of the other requestUri's match, then it will fall back to this dictionary item. public FakeHttpMessageHandler(HttpMessageOptions options) : this(new List - { - options - }) + { + options + }) { } @@ -61,7 +61,8 @@ protected override Task SendAsync(HttpRequestMessage reques { RequestUri = requestUri, HttpMethod = request.Method, - HttpContent = request.Content + HttpContent = request.Content, + Headers = request.Headers.ToDictionary(kv => kv.Key, kv => kv.Value) }; var expectedOption = GetExpectedOption(option); @@ -134,7 +135,11 @@ private HttpMessageOptions GetExpectedOption(HttpMessageOptions option) (x.HttpMethod == option.HttpMethod || x.HttpMethod == null) && (x.HttpContent == option.HttpContent || - x.HttpContent == null)); + x.HttpContent == null) && + (x.Headers == null || + x.Headers.Count == 0) || + (x.Headers != null && + HeaderExists(x.Headers, option.Headers))); } private static void IncrementCalls(HttpMessageOptions options) @@ -154,5 +159,50 @@ private static void IncrementCalls(HttpMessageOptions options) var existingValue = (int) propertyInfo.GetValue(options); propertyInfo.SetValue(options, ++existingValue); } + + private static bool HeaderExists(IDictionary> source, + IDictionary> destination) + { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (destination == null) + { + throw new ArgumentNullException(nameof(destination)); + } + + // Both sides are not the same size. + if (source.Count != destination.Count) + { + return false; + } + + foreach (var key in source.Keys) + { + if (!destination.ContainsKey(key)) + { + // Key is missing from the destination. + return false; + } + + if (source[key].Count() != destination[key].Count()) + { + // The destination now doesn't have the same size of 'values'. + return false; + } + + foreach (var value in source[key]) + { + if (!destination[key].Contains(value)) + { + return false; + } + } + } + + return true; + } } } \ No newline at end of file diff --git a/src/HttpClient.Helpers/HttpClient.Helpers.nuspec b/src/HttpClient.Helpers/HttpClient.Helpers.nuspec index 0ebd654..ed76790 100644 --- a/src/HttpClient.Helpers/HttpClient.Helpers.nuspec +++ b/src/HttpClient.Helpers/HttpClient.Helpers.nuspec @@ -18,6 +18,7 @@ + Added support for Headers in HttpMessageOptions. diff --git a/src/HttpClient.Helpers/HttpMessageOptions.cs b/src/HttpClient.Helpers/HttpMessageOptions.cs index e7a4647..a878bcf 100644 --- a/src/HttpClient.Helpers/HttpMessageOptions.cs +++ b/src/HttpClient.Helpers/HttpMessageOptions.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Net.Http; namespace WorldDomination.Net.Http @@ -51,6 +53,11 @@ public HttpContent HttpContent } } + /// + /// Optional: If not provided, then assumed to have *no* headers. + /// + public IDictionary> Headers { get; set; } + // Note: I'm using reflection to set the value in here because I want this value to be _read-only_. // Secondly, this occurs during a UNIT TEST, so I consider the expensive reflection costs to be // acceptable in this situation. @@ -59,8 +66,12 @@ public HttpContent HttpContent public override string ToString() { var httpMethodText = HttpMethod?.ToString() ?? NoValue; - return - $"{httpMethodText} {RequestUri}{(HttpContent != null ? $" body/content: {_httpContentSerialized}" : "")}"; + + var headers = Headers != null && + Headers.Any() + ? " " + string.Join(":", Headers.Select(x => $"{x.Key}|{string.Join(",", x.Value)}")) + : ""; + return $"{httpMethodText} {RequestUri}{(HttpContent != null ? $" body/content: {_httpContentSerialized}" : "")}{headers}"; } } } \ No newline at end of file diff --git a/tests/HttpClient.Helpers.Tests/GetAsyncTests.cs b/tests/HttpClient.Helpers.Tests/GetAsyncTests.cs index 9eaa6b7..2efdff5 100644 --- a/tests/HttpClient.Helpers.Tests/GetAsyncTests.cs +++ b/tests/HttpClient.Helpers.Tests/GetAsyncTests.cs @@ -76,6 +76,25 @@ public static IEnumerable ValidHttpMessageOptions HttpResponseMessage = SomeFakeResponse } }; + + // Has to match GET + URI + Header + yield return new object[] + { + new HttpMessageOptions + { + HttpMethod = HttpMethod.Get, + RequestUri = RequestUri, + Headers = new Dictionary> + { + {"Bearer", new[] + { + "pewpew" + } + } + }, + HttpResponseMessage = SomeFakeResponse + } + }; } } @@ -118,6 +137,65 @@ public static IEnumerable ValidSomeHttpMessageOptions } } + + public static IEnumerable DifferentHttpMessageOptions + { + get + { + yield return new object[] + { + // Different uri. + new HttpMessageOptions + { + RequestUri = "http://this.is.a.different.website" + } + }; + + yield return new object[] + { + // Different Method. + new HttpMessageOptions + { + HttpMethod = HttpMethod.Head + } + }; + + yield return new object[] + { + // Different header (different key). + new HttpMessageOptions + { + Headers = new Dictionary> + { + { + "xxxx", new[] + { + "pewpew" + } + } + } + } + }; + + yield return new object[] + { + // Different header (found key, different content). + new HttpMessageOptions + { + Headers = new Dictionary> + { + { + "Bearer", new[] + { + "pewpew" + } + } + } + } + }; + } + } + [Theory] [MemberData(nameof(ValidHttpMessageOptions))] public async Task GivenAnHttpMessageOptions_GetAsync_ReturnsAFakeResponse(HttpMessageOptions options) @@ -125,10 +203,13 @@ public async Task GivenAnHttpMessageOptions_GetAsync_ReturnsAFakeResponse(HttpMe // Arrange. var fakeHttpMessageHandler = new FakeHttpMessageHandler(options); - // Act & Assert. + // Act. await DoGetAsync(RequestUri, ExpectedContent, - fakeHttpMessageHandler); + fakeHttpMessageHandler, + options.Headers); + + // Assert. options.NumberOfTimesCalled.ShouldBe(1); } @@ -274,9 +355,37 @@ await DoGetAsync(RequestUri, options.NumberOfTimesCalled.ShouldBe(3); } + [Theory] + [MemberData(nameof(DifferentHttpMessageOptions))] + public async Task GivenSomeDifferentHttpMessageOptions_GetAsync_ShouldThrowAnException(HttpMessageOptions options) + { + // Arrange. + var fakeHttpMessageHandler = new FakeHttpMessageHandler(options); + var headers = new Dictionary> + { + { + "hi", new[] + { + "there" + } + } + }; + + // Act. + var exception = await Should.ThrowAsync(() => DoGetAsync(RequestUri, + ExpectedContent, + fakeHttpMessageHandler, + headers)); + + // Assert. + exception.Message.ShouldStartWith("No HttpResponseMessage found for the Request Uri:"); + options.NumberOfTimesCalled.ShouldBe(0); + } + private static async Task DoGetAsync(string requestUri, string expectedResponseContent, - FakeHttpMessageHandler fakeHttpMessageHandler) + FakeHttpMessageHandler fakeHttpMessageHandler, + IDictionary> optionalHeaders =null) { requestUri.ShouldNotBeNullOrWhiteSpace(); expectedResponseContent.ShouldNotBeNullOrWhiteSpace(); @@ -286,6 +395,16 @@ private static async Task DoGetAsync(string requestUri, string content; using (var httpClient = new System.Net.Http.HttpClient(fakeHttpMessageHandler)) { + // Do we have any Headers? + if (optionalHeaders != null && + optionalHeaders.Any()) + { + foreach (var keyValue in optionalHeaders) + { + httpClient.DefaultRequestHeaders.Add(keyValue.Key, keyValue.Value); + } + } + // Act. message = await httpClient.GetAsync(requestUri); content = await message.Content.ReadAsStringAsync();