Created
March 21, 2025 08:33
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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