Leightweight Http Server.
- Created to be used in internal load-tests to mock some http services.
Developed with these requirements in mind:
- Support for concurrent requests.
- No admin-rights necessary (unless binding address requires elevated permissions).
- It's a real http server.
- Easy to mock responses with funcs or with custom IHttpHandlerMock.
Cons:
- It seems that recently microsoft created their version of host to solve same problems as this nuget is attempting to solve: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis?view=aspnetcore-7.0 im yet to checkout how good it is.. but i think its fair to assume that because of it HttpMockSlim might become obsolete soon x]
- It is small, so only simplified handler exists - which supports mocking StatusCode/ContentType/ResponseBody.
- Advanced features require implementing IHttpHandlerMock and handling native c# HttpListenerContext.
https://www.nuget.org/packages/Viki.HttpMockSlim
Install-Package Viki.HttpMockSlim
- Don't forget .NET connection limiting features if want more concurrency
<system.net>
<connectionManagement>
<add address="*" maxconnection="100" /> <!-- By default afaik its 2 -->
</connectionManagement>
</system.net>
// ### Starting the server ###
HttpMock httpMock = new HttpMock();
httpMock.Start("http://localhost:8080/");
// ### Seting up handlers ###
// ! Server will try to check requests against each handler in registered order, until first handler processes it.
// --- Mocked swift storage ---
httpMock.Add(new SwiftAuthMock("/fake-swift/"));
httpMock.Add(new FakeStorageHandler("/fake-swift/"));
// --- Simplified & filtered (by HTTP method & path) func handlers ---
httpMock.Add("GET", "/sleep", (request, response) =>
{
Thread.Sleep(2000);
response.SetBody($"{request}\r\nSlept like a baby!");
});
httpMock.Add("GET", "/", (request, response) => response.SetBody($"{request}\r\nThe root is strong with this one!"));
// --- Other Random examples ---
// * Another low level example
httpMock.Add(new LowLevelExample());
// * This handler will handle all requests, since it doesn't have a filter
httpMock.Add((request, response) => response.SetBody($"{request}\r\rGotta catch them all!"));
// * This one will never be activated, because "Gotta catch them all!" will always handle it.
httpMock.Add("GET", "/will-not-work", (request, response) => response.SetBody($"{request}\r\nBOOO!"));
Console.WriteLine("Enter to exit...");
Console.ReadLine();
public class LowLevelExample : IHttpHandlerMock
{
// Lets handle all DELETE's
public bool Handle(HttpListenerContext context)
{
if (context.Request.HttpMethod != "DELETE")
return false;
context.Response.StatusCode = 200;
context.Response.ContentType = "text/plain";
new StreamGenerator(256, 'A').CopyTo(context.Response.OutputStream);
// One must not forget to close it, as .NET can decide to send the data only after Close().
context.Response.Close();
return true;
}
}
public class SwiftAuthMock : FilteredHandlerBase
{
private readonly string _fakeSwiftPath;
public SwiftAuthMock(string fakeSwiftPath) : base("GET", "/auth")
{
if (fakeSwiftPath == null)
throw new ArgumentNullException(nameof(fakeSwiftPath));
_fakeSwiftPath = fakeSwiftPath;
}
protected override bool HandleInner(HttpListenerContext context)
{
// No idea if this response is valid for Swift RFC. but it works :)
context.Response.StatusCode = 200;
context.Response.ContentType = "text/plain";
context.Response.SendChunked = true;
context.Response.AddHeader("X-Auth-Token", "fifty_shades_of_mocked_passkey");
context.Response.AddHeader("X-Storage-Url", $"{context.Request.Url.Scheme}://{context.Request.Url.Authority}{_fakeSwiftPath}");
context.Response.Close();
return true;
}
}
public class FakeStorageHandler : IHttpHandlerMock
{
protected readonly string PathBase;
public FakeStorageHandler(string pathBase)
{
if (pathBase == null)
throw new ArgumentNullException(nameof(pathBase));
PathBase = pathBase;
}
public bool Handle(HttpListenerContext context)
{
bool handled = false;
HttpListenerRequest request = context.Request;
if (request.RawUrl.StartsWith(PathBase, StringComparison.InvariantCulture))
{
handled = true;
switch (request.HttpMethod)
{
case "GET":
HandleGet(context);
break;
case "PUT":
HandlePut(context);
break;
case "DELETE":
HandleDelete(context);
break;
default:
handled = false;
break;
}
}
return handled;
}
protected virtual void HandleGet(HttpListenerContext context)
{
WriteResponse(context, new StreamGenerator(16, '#'));
}
protected virtual void HandlePut(HttpListenerContext context)
{
// Don't forget to read all what was sent anyway
var result = context.Request.InputStream.ReadAllBytes();
WriteResponse(context, new StreamGenerator(0, 0));
}
protected virtual void HandleDelete(HttpListenerContext context)
{
WriteResponse(context, new StreamGenerator(0, 0));
}
protected static void WriteResponse(HttpListenerContext context, int statusCode)
{
HttpListenerResponse response = context.Response;
response.StatusCode = statusCode;
response.ContentType = "text/plain";
response.SendChunked = true;
response.Close();
}
protected static void WriteResponse(HttpListenerContext context, Stream body)
{
HttpListenerResponse response = context.Response;
response.StatusCode = 200;
response.ContentType = "text/plain";
response.SendChunked = true;
body.CopyTo(response.OutputStream);
response.Close();
}
}