Skip to content

Instantly share code, notes, and snippets.

@deMD
Created March 21, 2025 08:33
Show Gist options
  • Select an option

  • Save deMD/1a9ba0e4e0f49895b5a15fe4768c1879 to your computer and use it in GitHub Desktop.

Select an option

Save deMD/1a9ba0e4e0f49895b5a15fe4768c1879 to your computer and use it in GitHub Desktop.
Spyserver to use in testing; allows sut to send real http request to a managed local server and tracks requests for easy assertions; useful in scenarios where normal test doubles can't be used or are harder to set up.
public class SpyServer(string baseUri = "http://localhost:5000/") : IDisposable, IAsyncDisposable
{
private static readonly Response _defaultResponse = new() { StatusCode = HttpStatusCode.InternalServerError };
private readonly Lock _lock = new();
private Response _nextResponse = _defaultResponse;
private HttpListener _server = null!;
public Request? LastRequest { get; private set; }
public async ValueTask DisposeAsync()
{
await StopAsync();
Dispose();
}
public void Dispose() => ((IDisposable)_server).Dispose();
public Task StartAsync() =>
Task.Run(() =>
{
_server = new HttpListener();
_server.Prefixes.Add(baseUri);
_server.Start();
_server.BeginGetContext(HandleRequest, null);
});
private Task StopAsync() =>
Task.Run(() =>
{
_server.Stop();
});
public void SetResponse(Response response)
{
lock (_lock)
{
_nextResponse = response;
}
}
public void SetJsonResponse<T>(T responseObject)
{
lock (_lock)
{
_nextResponse = new Response
{
StatusCode = HttpStatusCode.OK,
Body = JsonConvert.SerializeObject(responseObject)
};
}
}
private void HandleRequest(IAsyncResult ar)
{
try
{
var context = _server.EndGetContext(ar);
var request = context.Request;
var response = context.Response;
using var reader = new StreamReader(request.InputStream);
var body = reader.ReadToEnd();
StoreRequest(request, body);
SendResponse(response);
_server.BeginGetContext(HandleRequest, null);
}
catch
{
// swallow exception
}
}
private void StoreRequest(HttpListenerRequest request, string body)
{
var headers = new Dictionary<string, string>();
foreach (var key in request.Headers.AllKeys)
{
if (key != null && key != "Connection" && key != "Content-Length" && key != "Host")
{
headers[key] = request.Headers[key]!;
}
}
LastRequest = new Request
{
Method = request.HttpMethod,
Path = request.Url!.AbsolutePath,
Headers = headers,
Body = body
};
}
private void SendResponse(HttpListenerResponse response)
{
lock (_lock)
{
response.StatusCode = (int)_nextResponse.StatusCode;
if (_nextResponse.Headers != null)
{
foreach (var header in _nextResponse.Headers)
{
response.Headers[header.Key] = header.Value;
}
}
if (_nextResponse.Body != null)
{
var buffer = Encoding.UTF8.GetBytes(_nextResponse.Body);
response.OutputStream.Write(buffer, 0, buffer.Length);
}
response.OutputStream.Close();
}
}
public class Request
{
public string Method { get; set; } = null!;
public string Path { get; set; } = null!;
public Dictionary<string, string> Headers { get; set; } = null!;
public string Body { get; set; } = null!;
}
public class Response
{
public HttpStatusCode StatusCode { get; set; } = HttpStatusCode.InternalServerError;
public Dictionary<string, string>? Headers { get; set; }
public string? Body { get; set; }
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment