Skip to content

Commit 266f8df

Browse files
test: add automated coverage for middleware, rendering, and compare validation
Add automated test coverage
2 parents 7894ff2 + 528bf32 commit 266f8df

12 files changed

Lines changed: 708 additions & 1 deletion
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
using DebugProbe.AspNetCore.Internal.Compare;
2+
using DebugProbe.AspNetCore.Options;
3+
4+
namespace DebugProbe.AspNetCore.Tests.Compare;
5+
6+
public class CompareUrlValidatorTests
7+
{
8+
[Theory]
9+
[InlineData("https://example.com")]
10+
[InlineData("http://example.com:8080/path")]
11+
public async Task Validates_valid_http_and_https_urls(string url)
12+
{
13+
var result = await CompareUrlValidator.ValidateCompareBaseUrlAsync(url, new DebugProbeOptions());
14+
15+
Assert.True(result.IsValid);
16+
Assert.NotNull(result.BaseUri);
17+
Assert.Equal(new Uri(url).GetLeftPart(UriPartial.Authority), result.BaseUri.ToString().TrimEnd('/'));
18+
}
19+
20+
[Theory]
21+
[InlineData("ftp://example.com")]
22+
[InlineData("file:///tmp/test")]
23+
public async Task Rejects_invalid_schemes(string url)
24+
{
25+
var result = await CompareUrlValidator.ValidateCompareBaseUrlAsync(url, new DebugProbeOptions());
26+
27+
Assert.False(result.IsValid);
28+
Assert.Equal("Compare server URL must use http or https", result.Error);
29+
}
30+
31+
[Fact]
32+
public async Task Rejects_localhost_by_default()
33+
{
34+
var result = await CompareUrlValidator.ValidateCompareBaseUrlAsync("http://localhost:5000", new DebugProbeOptions());
35+
36+
Assert.False(result.IsValid);
37+
Assert.Equal("Compare server URL cannot target localhost", result.Error);
38+
}
39+
40+
[Fact]
41+
public async Task Allows_localhost_when_local_compare_targets_are_enabled()
42+
{
43+
var result = await CompareUrlValidator.ValidateCompareBaseUrlAsync(
44+
"http://localhost:5000/debug",
45+
new DebugProbeOptions { AllowLocalCompareTargets = true });
46+
47+
Assert.True(result.IsValid);
48+
Assert.Equal("http://localhost:5000", result.BaseUri?.ToString().TrimEnd('/'));
49+
}
50+
51+
[Fact]
52+
public async Task Validates_allow_local_compare_targets_for_private_addresses()
53+
{
54+
var blocked = await CompareUrlValidator.ValidateCompareBaseUrlAsync(
55+
"http://127.0.0.1:5000",
56+
new DebugProbeOptions());
57+
var allowed = await CompareUrlValidator.ValidateCompareBaseUrlAsync(
58+
"http://127.0.0.1:5000",
59+
new DebugProbeOptions { AllowLocalCompareTargets = true });
60+
61+
Assert.False(blocked.IsValid);
62+
Assert.True(allowed.IsValid);
63+
}
64+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
using DebugProbe.AspNetCore.Extensions;
2+
using DebugProbe.AspNetCore.Options;
3+
using DebugProbe.AspNetCore.Storage;
4+
using Microsoft.Extensions.DependencyInjection;
5+
6+
namespace DebugProbe.AspNetCore.Tests.Configuration;
7+
8+
public class DebugProbeOptionsTests
9+
{
10+
[Fact]
11+
public void Defaults_work_correctly()
12+
{
13+
var options = new DebugProbeOptions();
14+
15+
Assert.Equal(20, options.MaxEntries);
16+
Assert.Equal(256, options.MaxBodyCaptureSizeKb);
17+
Assert.False(options.AllowLocalCompareTargets);
18+
Assert.Empty(options.IgnorePaths);
19+
}
20+
21+
[Fact]
22+
public void Custom_options_are_registered_and_used()
23+
{
24+
var services = new ServiceCollection();
25+
26+
services.AddDebugProbe(options =>
27+
{
28+
options.MaxEntries = 2;
29+
options.MaxBodyCaptureSizeKb = 4;
30+
options.AllowLocalCompareTargets = true;
31+
options.IgnorePaths = ["/health"];
32+
});
33+
34+
using var provider = services.BuildServiceProvider();
35+
var options = provider.GetRequiredService<DebugProbeOptions>();
36+
var store = provider.GetRequiredService<DebugEntryStore>();
37+
38+
Assert.Equal(2, options.MaxEntries);
39+
Assert.Equal(4, options.MaxBodyCaptureSizeKb);
40+
Assert.True(options.AllowLocalCompareTargets);
41+
Assert.Equal(["/health"], options.IgnorePaths);
42+
Assert.NotNull(store.Environment);
43+
}
44+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net8.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
8+
<IsPackable>false</IsPackable>
9+
<IsTestProject>true</IsTestProject>
10+
</PropertyGroup>
11+
12+
<ItemGroup>
13+
<PackageReference Include="coverlet.collector" Version="6.0.0" />
14+
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="8.0.0" />
15+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
16+
<PackageReference Include="xunit" Version="2.5.3" />
17+
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
18+
</ItemGroup>
19+
20+
<ItemGroup>
21+
<ProjectReference Include="..\DebugProbe.AspNetCore\DebugProbe.AspNetCore.csproj" />
22+
</ItemGroup>
23+
24+
<ItemGroup>
25+
<Using Include="Xunit" />
26+
</ItemGroup>
27+
28+
</Project>
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
global using Microsoft.AspNetCore.Builder;
2+
global using Microsoft.AspNetCore.Http;
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
using DebugProbe.AspNetCore.Extensions;
2+
using DebugProbe.AspNetCore.Models;
3+
using DebugProbe.AspNetCore.Options;
4+
using DebugProbe.AspNetCore.Storage;
5+
using Microsoft.AspNetCore.Builder;
6+
using Microsoft.AspNetCore.Hosting;
7+
using Microsoft.AspNetCore.Http;
8+
using Microsoft.AspNetCore.Routing;
9+
using Microsoft.AspNetCore.TestHost;
10+
using Microsoft.Extensions.DependencyInjection;
11+
using Microsoft.Extensions.Hosting;
12+
13+
namespace DebugProbe.AspNetCore.Tests.Infrastructure;
14+
15+
internal sealed class DebugProbeTestApp : IAsyncDisposable
16+
{
17+
private readonly IHost _host;
18+
19+
private DebugProbeTestApp(IHost host)
20+
{
21+
_host = host;
22+
Client = host.GetTestClient();
23+
Store = host.Services.GetRequiredService<DebugEntryStore>();
24+
}
25+
26+
public HttpClient Client { get; }
27+
28+
public DebugEntryStore Store { get; }
29+
30+
public DebugEntry SingleEntry => Assert.Single(Store.GetAll());
31+
32+
public static async Task<DebugProbeTestApp> CreateAsync(
33+
Action<IEndpointRouteBuilder> mapEndpoints,
34+
Action<DebugProbeOptions>? configureOptions = null,
35+
Action<IApplicationBuilder>? configureAfterDebugProbe = null)
36+
{
37+
var host = await new HostBuilder()
38+
.ConfigureWebHost(webHost =>
39+
{
40+
webHost.UseTestServer();
41+
webHost.ConfigureServices(services =>
42+
{
43+
services.AddRouting();
44+
services.AddDebugProbe(configureOptions);
45+
});
46+
webHost.Configure(app =>
47+
{
48+
app.UseRouting();
49+
app.UseDebugProbe();
50+
configureAfterDebugProbe?.Invoke(app);
51+
app.UseEndpoints(mapEndpoints);
52+
});
53+
})
54+
.StartAsync();
55+
56+
return new DebugProbeTestApp(host);
57+
}
58+
59+
public async ValueTask DisposeAsync()
60+
{
61+
Client.Dispose();
62+
await _host.StopAsync();
63+
_host.Dispose();
64+
}
65+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
using System.Net;
2+
using System.Text;
3+
using DebugProbe.AspNetCore.Tests.Infrastructure;
4+
using Microsoft.AspNetCore.Diagnostics;
5+
6+
namespace DebugProbe.AspNetCore.Tests.Middleware;
7+
8+
public class ExceptionHandlingTests
9+
{
10+
[Fact]
11+
public async Task Captures_exception_response_text()
12+
{
13+
await using var app = await CreateExceptionAppAsync("handled error");
14+
15+
var response = await app.Client.PostAsync("/throw", JsonContent("{\"id\":42}"));
16+
17+
Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
18+
Assert.Equal("handled error", await response.Content.ReadAsStringAsync());
19+
Assert.Equal("handled error", app.SingleEntry.ResponseBody);
20+
}
21+
22+
[Fact]
23+
public async Task Stores_exception_information_when_exception_is_not_handled()
24+
{
25+
await using var app = await DebugProbeTestApp.CreateAsync(endpoints =>
26+
{
27+
endpoints.MapGet("/throw", (HttpContext _) => throw new InvalidOperationException("unhandled failure"));
28+
});
29+
30+
await Assert.ThrowsAsync<InvalidOperationException>(() => app.Client.GetAsync("/throw"));
31+
32+
var entry = app.SingleEntry;
33+
Assert.Equal(500, entry.StatusCode);
34+
Assert.Contains("InvalidOperationException", entry.ResponseBody);
35+
Assert.Contains("unhandled failure", entry.ResponseBody);
36+
}
37+
38+
[Fact]
39+
public async Task Handles_empty_error_responses()
40+
{
41+
await using var app = await CreateExceptionAppAsync(string.Empty);
42+
43+
var response = await app.Client.GetAsync("/throw");
44+
45+
Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
46+
Assert.Equal(string.Empty, app.SingleEntry.ResponseBody);
47+
}
48+
49+
[Fact]
50+
public async Task Issue_38_exception_endpoint_captures_status_request_body_and_response_body()
51+
{
52+
await using var app = await CreateExceptionAppAsync("{\"error\":\"boom\"}", "application/json");
53+
54+
var response = await app.Client.PostAsync("/throw", JsonContent("{\"name\":\"Ada\"}"));
55+
56+
Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
57+
var entry = app.SingleEntry;
58+
Assert.Equal(500, entry.StatusCode);
59+
Assert.Equal("{\"name\":\"Ada\"}", entry.RequestBody);
60+
Assert.Equal("{\"error\":\"boom\"}", entry.ResponseBody);
61+
}
62+
63+
private static Task<DebugProbeTestApp> CreateExceptionAppAsync(
64+
string errorBody,
65+
string contentType = "text/plain")
66+
{
67+
return DebugProbeTestApp.CreateAsync(
68+
endpoints =>
69+
{
70+
endpoints.MapPost("/throw", (HttpContext _) => throw new InvalidOperationException("boom"));
71+
endpoints.MapGet("/throw", (HttpContext _) => throw new InvalidOperationException("boom"));
72+
},
73+
configureAfterDebugProbe: builder =>
74+
{
75+
builder.UseExceptionHandler(errorApp =>
76+
{
77+
errorApp.Run(async context =>
78+
{
79+
_ = context.Features.Get<IExceptionHandlerFeature>();
80+
context.Response.StatusCode = 500;
81+
context.Response.ContentType = contentType;
82+
await context.Response.WriteAsync(errorBody);
83+
});
84+
});
85+
});
86+
}
87+
88+
private static StringContent JsonContent(string value)
89+
{
90+
return new StringContent(value, Encoding.UTF8, "application/json");
91+
}
92+
}

0 commit comments

Comments
 (0)