Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 11 additions & 10 deletions src/HttpMock.Unit.Tests/RequestProcessorTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Moq;
using NUnit.Framework;

Expand Down Expand Up @@ -65,21 +66,21 @@ public void Custom_verb_should_return_handler_with_custom_method_set() {
}

[Test]
public void If_no_handlers_found_should_fire_onresponse_with_a_404() {
public async Task If_no_handlers_found_should_fire_onresponse_with_a_404() {
_processor = new RequestProcessor(_ruleThatReturnsNoHandlers.Object, new RequestHandlerList());

_processor.Add(_requestHandlerFactory.Get("test"));

HttpMockResponseHead capturedHead = null;
byte[] capturedBody = null;
_processor.OnRequest(new HttpRequestHead(), null, (h, b) => { capturedHead = h; capturedBody = b; });
await _processor.OnRequest(new HttpRequestHead(), null, (h, b) => { capturedHead = h; capturedBody = b; });

Assert.That(capturedHead.Status, Is.EqualTo("404 NotFound"));
Assert.That(capturedBody, Is.Null);
}

[Test]
public void If_a_handler_found_should_fire_onresponse_with_that_repsonse() {
public async Task If_a_handler_found_should_fire_onresponse_with_that_repsonse() {
_processor = new RequestProcessor(_ruleThatReturnsFirstHandler.Object, new RequestHandlerList());

RequestHandler requestHandler = _requestHandlerFactory.Get("test");
Expand All @@ -88,15 +89,15 @@ public void If_a_handler_found_should_fire_onresponse_with_that_repsonse() {

HttpMockResponseHead capturedHead = null;
byte[] capturedBody = null;
_processor.OnRequest(new HttpRequestHead { Headers = headers }, null,
await _processor.OnRequest(new HttpRequestHead { Headers = headers }, null,
(h, b) => { capturedHead = h; capturedBody = b; });

Assert.That(capturedHead.Status, Is.EqualTo(requestHandler.ResponseBuilder.BuildHeaders().Status));
Assert.That(capturedBody, Is.EqualTo(requestHandler.ResponseBuilder.BuildBody(headers)));
}

[Test]
public void Matching_HEAD_handler_should_output_handlers_expected_response_with_null_body() {
public async Task Matching_HEAD_handler_should_output_handlers_expected_response_with_null_body() {

_processor = new RequestProcessor(_ruleThatReturnsFirstHandler.Object, new RequestHandlerList());

Expand All @@ -106,7 +107,7 @@ public void Matching_HEAD_handler_should_output_handlers_expected_response_with_

HttpMockResponseHead capturedHead = null;
byte[] capturedBody = null;
_processor.OnRequest(httpRequestHead, null, (h, b) => { capturedHead = h; capturedBody = b; });
await _processor.OnRequest(httpRequestHead, null, (h, b) => { capturedHead = h; capturedBody = b; });

Assert.That(capturedHead, Is.Not.Null);
Assert.That(capturedBody, Is.Null);
Expand All @@ -129,7 +130,7 @@ public void When_a_handler_is_added_should_be_able_to_find_it() {
}

[Test]
public void When_a_handler_is_hit_handlers_request_count_is_incremented() {
public async Task When_a_handler_is_hit_handlers_request_count_is_incremented() {

string expectedPath = "/blah/test";
string expectedMethod = "GET";
Expand All @@ -138,14 +139,14 @@ public void When_a_handler_is_hit_handlers_request_count_is_incremented() {

requestProcessor.Add(_requestHandlerFactory.Get(expectedPath));
var httpRequestHead = new HttpRequestHead { Headers = new Dictionary<string, string>(), Uri = expectedPath, Method = expectedPath };
requestProcessor.OnRequest(httpRequestHead, null, (h, b) => { });
await requestProcessor.OnRequest(httpRequestHead, null, (h, b) => { });

var handler = requestProcessor.FindHandler(expectedMethod, expectedPath);
Assert.That(handler.RequestCount(), Is.EqualTo(1));
}

[Test]
public void Returns_mock_not_found_when_handler_constraints_cannot_be_verified()
public async Task Returns_mock_not_found_when_handler_constraints_cannot_be_verified()
{
var excludePhrase = "OhMyDaysssss";

Expand All @@ -160,7 +161,7 @@ public void Returns_mock_not_found_when_handler_constraints_cannot_be_verified()
var p = new RequestProcessor(matchingRule.Object, new RequestHandlerList { handlerWithConstraints });

HttpMockResponseHead capturedHead = null;
p.OnRequest(new HttpRequestHead { Uri = "http://blah.com/cheese/" + excludePhrase }, null,
await p.OnRequest(new HttpRequestHead { Uri = "http://blah.com/cheese/" + excludePhrase }, null,
(h, b) => capturedHead = h);

Assert.That(capturedHead.Status, Is.EqualTo("404 NotFound"));
Expand Down
4 changes: 2 additions & 2 deletions src/HttpMock/BufferedBody.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public BufferedBody(Func<byte[]> data)
_dataFunc = data;
}

public Func<int> Length => () => _dataFunc().Length;
public Func<int> Length => () => GetBytes().Length;

public byte[] GetBytes()
{
Expand All @@ -29,7 +29,7 @@ public void SetRequestHeaders(IDictionary<string, string> requestHeaders) { }

class NoBody : IResponse
{
public byte[] GetBytes() => new byte[0];
public byte[] GetBytes() => Array.Empty<byte>();

public void SetRequestHeaders(IDictionary<string, string> requestHeaders) { }
}
Expand Down
26 changes: 15 additions & 11 deletions src/HttpMock/EndpointMatchingRule.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
Expand All @@ -9,6 +10,7 @@ namespace HttpMock
{
public class EndpointMatchingRule : IMatchingRule
{
private static readonly ConcurrentDictionary<string, Regex> PathRegexCache = new();
private readonly HeaderMatch _headerMatch;
private readonly QueryParamMatch _queryParamMatch;

Expand All @@ -22,29 +24,30 @@ public bool IsEndpointMatch(IRequestHandler requestHandler, IHttpRequestHead req
if (requestHandler.QueryParams == null)
throw new ArgumentException("requestHandler QueryParams cannot be null");

var requestQueryParams = GetQueryParams(request);
var requestHeaders = GetHeaders(request);
// Check cheapest conditions first before parsing query/headers
bool httpMethodsMatch = requestHandler.Method == request.Method;
if (!httpMethodsMatch) return false;

bool uriStartsWith = MatchPath(requestHandler, request);
if (!uriStartsWith) return false;

bool httpMethodsMatch = requestHandler.Method == request.Method;

bool queryParamMatch = true;
bool shouldMatchQueryParams = (requestHandler.QueryParams.Count > 0);

if (shouldMatchQueryParams) {
queryParamMatch = _queryParamMatch.MatchQueryParams(requestHandler, requestQueryParams);
var requestQueryParams = GetQueryParams(request);
if (!_queryParamMatch.MatchQueryParams(requestHandler, requestQueryParams))
return false;
}

bool headerMatch = true;
bool shouldMatchHeaders = requestHandler.RequestHeaders != null
&& requestHandler.RequestHeaders.Count > 0;

if (shouldMatchHeaders) {
headerMatch = _headerMatch.MatchHeaders(requestHandler, requestHeaders);
var requestHeaders = GetHeaders(request);
if (!_headerMatch.MatchHeaders(requestHandler, requestHeaders))
return false;
}

return uriStartsWith && httpMethodsMatch && queryParamMatch && headerMatch;
return true;
}

private static bool MatchPath(IRequestHandler requestHandler, IHttpRequestHead request)
Expand All @@ -55,7 +58,8 @@ private static bool MatchPath(IRequestHandler requestHandler, IHttpRequestHead r
{
pathToMatch = request.Uri.Substring(0, positionOfQueryStart);
}
var pathMatch = new Regex(string.Format(@"^{0}\/*$", Regex.Escape(requestHandler.Path)));
var pathMatch = PathRegexCache.GetOrAdd(requestHandler.Path,
static path => new Regex($@"^{Regex.Escape(path)}\/*$", RegexOptions.Compiled));
return pathMatch.IsMatch(pathToMatch);
}

Expand Down
18 changes: 11 additions & 7 deletions src/HttpMock/FileResponseBody.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ namespace HttpMock
{
class FileResponseBody : IResponse
{
private static readonly Regex RangeRegex = new(@"bytes=([\d]*)-([\d]*)", RegexOptions.Compiled);
private readonly string _filepath;
private readonly ILogger<FileResponseBody> _log;
private IDictionary<string, string> _requestHeaders;
Expand All @@ -23,26 +24,29 @@ public byte[] GetBytes()
var fileInfo = new FileInfo(_filepath);
using (FileStream fileStream = fileInfo.Open(FileMode.Open, FileAccess.Read))
{
var buffer = new byte[fileInfo.Length];
fileStream.ReadExactly(buffer, 0, (int)fileInfo.Length);
int length = (int)fileInfo.Length;
int offset = 0;

if (_requestHeaders != null && _requestHeaders.ContainsKey(HttpRequestHeader.Range.ToString()))
{
string range = _requestHeaders[HttpRequestHeader.Range.ToString()];
Regex rangeEx = new Regex(@"bytes=([\d]*)-([\d]*)");
if (rangeEx.IsMatch(range))
var match = RangeRegex.Match(range);
if (match.Success)
{
int from = Convert.ToInt32(rangeEx.Match(range).Groups[1].Value);
int to = Convert.ToInt32(rangeEx.Match(range).Groups[2].Value);
int from = Convert.ToInt32(match.Groups[1].Value);
int to = Convert.ToInt32(match.Groups[2].Value);
offset = from;
length = (to - from) + 1;
}
}

if (offset > 0)
{
fileStream.Seek(offset, SeekOrigin.Begin);
}

var result = new byte[length];
Array.Copy(buffer, offset, result, 0, length);
fileStream.ReadExactly(result, 0, length);
_log.LogDebug("Wrote {Length} bytes to buffer", length);
return result;
}
Expand Down
30 changes: 21 additions & 9 deletions src/HttpMock/HeaderMatch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,29 @@ namespace HttpMock
public class HeaderMatch {
internal bool MatchHeaders(IRequestHandler requestHandler, IDictionary<string, string> requestHeaders)
{
return requestHandler.RequestHeaders.All(
expectedHeader => requestHeaders.Any(header => HeadersMatch(expectedHeader, header)));
}

private static bool HeadersMatch(KeyValuePair<string, string> expectedHeader, KeyValuePair<string, string> header)
{
if (!string.Equals(expectedHeader.Key, header.Key, StringComparison.OrdinalIgnoreCase))
foreach (var expectedHeader in requestHandler.RequestHeaders)
{
return false;
if (!requestHeaders.TryGetValue(expectedHeader.Key, out var actualValue))
{
// Try case-insensitive key lookup as a fallback
bool found = false;
foreach (var header in requestHeaders)
{
if (string.Equals(header.Key, expectedHeader.Key, StringComparison.OrdinalIgnoreCase)
&& string.Equals(header.Value, expectedHeader.Value, StringComparison.OrdinalIgnoreCase))
{
found = true;
break;
}
}
if (!found) return false;
}
else if (!string.Equals(actualValue, expectedHeader.Value, StringComparison.OrdinalIgnoreCase))
{
return false;
}
}
return string.Equals(expectedHeader.Value, header.Value, StringComparison.OrdinalIgnoreCase);
return true;
}
}
}
92 changes: 46 additions & 46 deletions src/HttpMock/HttpMockRepository.cs
Original file line number Diff line number Diff line change
@@ -1,46 +1,46 @@
using System;
using System.Net;
using System.Net.Sockets;
using Microsoft.Extensions.Logging;

namespace HttpMock
{
public static class HttpMockRepository
{
private static readonly HttpServerFactory _httpServerFactory = new HttpServerFactory();

public static IHttpServer At(string uri, ILoggerFactory loggerFactory = null)
{
if (uri.Trim().EndsWith("/"))
{
throw new ArgumentException(
String.Format("Do not use a trailing slash for the server URI please: {0}", uri), "uri");
}

return At(new Uri(uri), loggerFactory);
}

public static IHttpServer At(Uri uri, ILoggerFactory loggerFactory = null)
{
return _httpServerFactory.Get(uri, loggerFactory).WithNewContext();
}

/// <summary>
/// Asks the operating system for a free TCP port on the loopback interface.
/// </summary>
/// <remarks>
/// The port is reserved by <see cref="TcpListener"/> on port 0 and released
/// immediately. There is a small TOCTOU window between the release and the
/// caller binding to the port; this is an accepted limitation for test/mock usage.
/// </remarks>
/// <returns>An available port number.</returns>
public static int FindFreePort()
{
using var listener = new TcpListener(IPAddress.Loopback, 0);
listener.Start();
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
listener.Stop();
return port;
}
}
}
using System;
using System.Net;
using System.Net.Sockets;
using Microsoft.Extensions.Logging;
namespace HttpMock
{
public static class HttpMockRepository
{
private static readonly HttpServerFactory _httpServerFactory = new HttpServerFactory();
public static IHttpServer At(string uri, ILoggerFactory loggerFactory = null)
{
if (uri.Trim().EndsWith("/"))
{
throw new ArgumentException(
String.Format("Do not use a trailing slash for the server URI please: {0}", uri), "uri");
}
return At(new Uri(uri), loggerFactory);
}
public static IHttpServer At(Uri uri, ILoggerFactory loggerFactory = null)
{
return _httpServerFactory.Get(uri, loggerFactory).WithNewContext();
}
/// <summary>
/// Asks the operating system for a free TCP port on the loopback interface.
/// </summary>
/// <remarks>
/// The port is reserved by <see cref="TcpListener"/> on port 0 and released
/// immediately. There is a small TOCTOU window between the release and the
/// caller binding to the port; this is an accepted limitation for test/mock usage.
/// </remarks>
/// <returns>An available port number.</returns>
public static int FindFreePort()
{
using var listener = new TcpListener(IPAddress.Loopback, 0);
listener.Start();
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
listener.Stop();
return port;
}
}
}
Loading
Loading