From acf23bd06f6abda2a9c20b0849b6c915c3f82290 Mon Sep 17 00:00:00 2001 From: Mattias Karlsson Date: Wed, 14 Oct 2026 16:59:20 +0200 Subject: [PATCH] feat: enrich inspect with request bodies, route JSON, and favicon Show a two-column request/response exchange with decoded form bodies, syntax highlighting, Enable/Disable, a copyable Routes.json object (generated on 404), and the package favicon plus NuGet version link. --- ...AfterGetIncludesHitAndHeaders.verified.txt | 20 ++- .../InspectTests.cs | 70 ++++++++ .../Devlead.Testing.MockHttp.Tool.csproj | 1 + .../Hosting/MockHttpWebHost.Inspect.cs | 15 ++ .../Hosting/MockHttpWebHost.cs | 4 +- .../Inspect/Models/ActivityEntry.cs | 8 +- .../Inspect/inspect.html | 164 ++++++++++++++++-- src/Devlead.Testing.MockHttp.Tool/README.md | 2 +- .../Routing/RouteTable.Entry.cs | 6 +- .../Routing/RouteTable.Headers.cs | 86 ++++++++- .../Routing/RouteTable.Invoke.cs | 16 +- .../Routing/RouteTable.cs | 15 +- 12 files changed, 371 insertions(+), 36 deletions(-) diff --git a/src/Devlead.Testing.MockHttp.Tool.Tests/InspectTests.InspectApiAfterGetIncludesHitAndHeaders.verified.txt b/src/Devlead.Testing.MockHttp.Tool.Tests/InspectTests.InspectApiAfterGetIncludesHitAndHeaders.verified.txt index a5c1f61..2bc5f63 100644 --- a/src/Devlead.Testing.MockHttp.Tool.Tests/InspectTests.InspectApiAfterGetIncludesHitAndHeaders.verified.txt +++ b/src/Devlead.Testing.MockHttp.Tool.Tests/InspectTests.InspectApiAfterGetIncludesHitAndHeaders.verified.txt @@ -71,6 +71,7 @@ localhost ] }, + requestContentLength: 0, responseHeaders: { Content-Type: [ text/plain @@ -81,7 +82,24 @@ }, contentType: text/plain, contentLength: 13, - bodyPreview: Hello, World! + bodyPreview: Hello, World!, + route: { + Request: { + Methods: [ + { + Method: GET + } + ], + AbsoluteUri: https://example.com/index.txt + }, + Responses: [ + { + ContentResource: Example.Index.txt, + ContentType: text/plain, + StatusCode: 200 + } + ] + } } ] } \ No newline at end of file diff --git a/src/Devlead.Testing.MockHttp.Tool.Tests/InspectTests.cs b/src/Devlead.Testing.MockHttp.Tool.Tests/InspectTests.cs index 3fbbd86..769417d 100644 --- a/src/Devlead.Testing.MockHttp.Tool.Tests/InspectTests.cs +++ b/src/Devlead.Testing.MockHttp.Tool.Tests/InspectTests.cs @@ -19,6 +19,62 @@ public async Task InspectPageReturnsHtml() Assert.That(html, Does.Contain("MockHttp inspect")); Assert.That(html, Does.Contain("Request headers")); Assert.That(html, Does.Contain("Response headers")); + Assert.That(html, Does.Contain("exchange")); + Assert.That(html, Does.Contain("highlightJson")); + Assert.That(html, Does.Contain("/favicon.ico")); + Assert.That(html, Does.Contain("https://www.nuget.org/packages/Devlead.Testing.MockHttp.Tool")); + Assert.That(html, Does.Contain("id=\"reqRoute\"")); + Assert.That(html, Does.Contain("id=\"copyRoute\"")); + } + + [Test] + public async Task FaviconReturnsPng() + { + await using var host = await MockHttpTestHost.StartAsync(); + + var response = await host.Client.GetAsync("/favicon.ico"); + + Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK)); + Assert.That(response.Content.Headers.ContentType?.MediaType, Is.EqualTo("image/png")); + var bytes = await response.Content.ReadAsByteArrayAsync(); + Assert.That(bytes[0], Is.EqualTo(0x89)); + Assert.That(bytes[1], Is.EqualTo((byte)'P')); + Assert.That(bytes[2], Is.EqualTo((byte)'N')); + Assert.That(bytes[3], Is.EqualTo((byte)'G')); + } + + [Test] + public async Task InspectApiAfterPostIncludesRequestBody() + { + await using var host = await MockHttpTestHost.StartAsync(); + using var content = new StringContent( + """{"requestId":"dev-send","responses":[]}""", + System.Text.Encoding.UTF8, + "application/json"); + + await host.Client.PostAsync("/messaging/send", content); + var snapshot = await host.Client.GetFromJsonAsync("/inspect/api"); + var activity = snapshot.GetProperty("activity")[0]; + + Assert.That(activity.GetProperty("method").GetString(), Is.EqualTo("POST")); + Assert.That(activity.GetProperty("requestContentType").GetString(), Does.Contain("application/json")); + Assert.That(activity.GetProperty("requestBodyPreview").GetString(), Does.Contain("dev-send")); + Assert.That(activity.GetProperty("requestContentLength").GetInt32(), Is.GreaterThan(0)); + } + + [Test] + public async Task InspectApiAfterFormPostIncludesRequestBody() + { + await using var host = await MockHttpTestHost.StartAsync(); + using var content = new StringContent("q=a+b", System.Text.Encoding.ASCII, "application/x-www-form-urlencoded"); + + await host.Client.PostAsync("/form", content); + var snapshot = await host.Client.GetFromJsonAsync("/inspect/api"); + var activity = snapshot.GetProperty("activity")[0]; + + Assert.That(activity.GetProperty("requestContentType").GetString(), Does.Contain("application/x-www-form-urlencoded")); + Assert.That(activity.GetProperty("requestBodyPreview").GetString(), Is.EqualTo("q=a+b")); + Assert.That(activity.GetProperty("requestContentLength").GetInt32(), Is.EqualTo(5)); } [Test] @@ -37,6 +93,20 @@ await VerifyJson(json) .ScrubMember("utc"); } + [Test] + public async Task InspectApiAfterNotFoundGeneratesRoute() + { + await using var host = await MockHttpTestHost.StartAsync(); + + await host.Client.GetAsync("/missing.txt"); + var snapshot = await host.Client.GetFromJsonAsync("/inspect/api"); + var route = snapshot.GetProperty("activity")[0].GetProperty("route"); + + Assert.That(route.GetProperty("Request").GetProperty("Methods")[0].GetProperty("Method").GetString(), Is.EqualTo("GET")); + Assert.That(route.GetProperty("Request").GetProperty("AbsoluteUri").GetString(), Does.Contain("/missing.txt")); + Assert.That(route.GetProperty("Responses")[0].GetProperty("StatusCode").GetInt32(), Is.EqualTo(404)); + } + [Test] public async Task DisabledRouteGetDoesNotIncrementHits() { diff --git a/src/Devlead.Testing.MockHttp.Tool/Devlead.Testing.MockHttp.Tool.csproj b/src/Devlead.Testing.MockHttp.Tool/Devlead.Testing.MockHttp.Tool.csproj index fc899df..b1ac980 100644 --- a/src/Devlead.Testing.MockHttp.Tool/Devlead.Testing.MockHttp.Tool.csproj +++ b/src/Devlead.Testing.MockHttp.Tool/Devlead.Testing.MockHttp.Tool.csproj @@ -37,6 +37,7 @@ + diff --git a/src/Devlead.Testing.MockHttp.Tool/Hosting/MockHttpWebHost.Inspect.cs b/src/Devlead.Testing.MockHttp.Tool/Hosting/MockHttpWebHost.Inspect.cs index a2e5fee..7d06214 100644 --- a/src/Devlead.Testing.MockHttp.Tool/Hosting/MockHttpWebHost.Inspect.cs +++ b/src/Devlead.Testing.MockHttp.Tool/Hosting/MockHttpWebHost.Inspect.cs @@ -20,6 +20,12 @@ internal static partial class MockHttpWebHost ? informational.Split('+', 2)[0] : typeof(MockHttpWebHost).Assembly.GetName().Version?.ToString() ?? string.Empty; + private static readonly byte[] FaviconPng = LoadEmbedded( + "Devlead.Testing.MockHttp.Tool.Inspect.favicon.png", + "The favicon resource is missing."); + + private static IResult Favicon() => Results.File(FaviconPng, "image/png"); + private static IResult InspectPage() { using var stream = typeof(MockHttpWebHost).Assembly @@ -29,6 +35,15 @@ private static IResult InspectPage() return Results.Content(reader.ReadToEnd(), "text/html", Encoding.UTF8); } + private static byte[] LoadEmbedded(string name, string missing) + { + using var stream = typeof(MockHttpWebHost).Assembly.GetManifestResourceStream(name) + ?? throw new InvalidOperationException(missing); + using var memory = new MemoryStream(); + stream.CopyTo(memory); + return memory.ToArray(); + } + private static IResult InspectState(RouteTable table, MockHttpListenUrl listen) => Results.Json(table.Snapshot(listen.Value, ApplicationVersion), JsonOptions); diff --git a/src/Devlead.Testing.MockHttp.Tool/Hosting/MockHttpWebHost.cs b/src/Devlead.Testing.MockHttp.Tool/Hosting/MockHttpWebHost.cs index d0aa83d..ad6cd2e 100644 --- a/src/Devlead.Testing.MockHttp.Tool/Hosting/MockHttpWebHost.cs +++ b/src/Devlead.Testing.MockHttp.Tool/Hosting/MockHttpWebHost.cs @@ -26,6 +26,7 @@ public static WebApplicationBuilder CreateBuilder(RouteTable table, string liste /// The web application. public static void Map(WebApplication app) { + app.MapGet("/favicon.ico", Favicon); app.MapGet("/inspect", InspectPage); app.MapGet("/inspect/api", InspectState); app.MapPost("/inspect/api/routes/toggle", InspectToggle); @@ -33,7 +34,8 @@ public static void Map(WebApplication app) var table = app.Services.GetRequiredService(); app.UseWhen( - context => !context.Request.Path.StartsWithSegments("/inspect"), + context => !context.Request.Path.StartsWithSegments("/inspect") + && !context.Request.Path.Equals("/favicon.ico", StringComparison.OrdinalIgnoreCase), branch => branch.Run(table.InvokeAsync)); } } diff --git a/src/Devlead.Testing.MockHttp.Tool/Inspect/Models/ActivityEntry.cs b/src/Devlead.Testing.MockHttp.Tool/Inspect/Models/ActivityEntry.cs index da6dd0e..6332520 100644 --- a/src/Devlead.Testing.MockHttp.Tool/Inspect/Models/ActivityEntry.cs +++ b/src/Devlead.Testing.MockHttp.Tool/Inspect/Models/ActivityEntry.cs @@ -1,3 +1,5 @@ +using System.Text.Json; + namespace Devlead.Testing.MockHttp.Tool.Inspect.Models; internal sealed record ActivityEntry( @@ -10,7 +12,11 @@ internal sealed record ActivityEntry( string MatchedAbsoluteUri, bool EnableDisable, IReadOnlyDictionary RequestHeaders, + string? RequestContentType, + int RequestContentLength, + string? RequestBodyPreview, IReadOnlyDictionary ResponseHeaders, string? ContentType, int ContentLength, - string? BodyPreview); + string? BodyPreview, + JsonElement Route); diff --git a/src/Devlead.Testing.MockHttp.Tool/Inspect/inspect.html b/src/Devlead.Testing.MockHttp.Tool/Inspect/inspect.html index f6356c4..4ab9dda 100644 --- a/src/Devlead.Testing.MockHttp.Tool/Inspect/inspect.html +++ b/src/Devlead.Testing.MockHttp.Tool/Inspect/inspect.html @@ -3,6 +3,7 @@ MockHttp inspect +

MockHttp inspect

-
@@ -63,7 +76,7 @@

Routes

Route

- +
Method
Select a route.
@@ -73,7 +86,7 @@

Route

-
+

Activity

@@ -90,17 +103,30 @@

Activity

-

Headers

+

Exchange

General
Select a request.
Matched
-

Request headers

-
-

Response headers

-
-

Body preview

-

+        
+
+

Request headers

+
+

Body preview

+

+            
+

Route

+ +
+

+          
+
+

Response headers

+
+

Body preview

+

+          
+
@@ -110,6 +136,7 @@

Body preview

Inspect +