Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions DebugProbe.AspNetCore.Tests/Compare/CompareUrlValidatorTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using DebugProbe.AspNetCore.Internal.Compare;
using DebugProbe.AspNetCore.Options;

namespace DebugProbe.AspNetCore.Tests.Compare;

public class CompareUrlValidatorTests
{
[Theory]
[InlineData("https://example.com")]
[InlineData("http://example.com:8080/path")]
public async Task Validates_valid_http_and_https_urls(string url)
{
var result = await CompareUrlValidator.ValidateCompareBaseUrlAsync(url, new DebugProbeOptions());

Assert.True(result.IsValid);
Assert.NotNull(result.BaseUri);
Assert.Equal(new Uri(url).GetLeftPart(UriPartial.Authority), result.BaseUri.ToString().TrimEnd('/'));
}

[Theory]
[InlineData("ftp://example.com")]
[InlineData("file:///tmp/test")]
public async Task Rejects_invalid_schemes(string url)
{
var result = await CompareUrlValidator.ValidateCompareBaseUrlAsync(url, new DebugProbeOptions());

Assert.False(result.IsValid);
Assert.Equal("Compare server URL must use http or https", result.Error);
}

[Fact]
public async Task Rejects_localhost_by_default()
{
var result = await CompareUrlValidator.ValidateCompareBaseUrlAsync("http://localhost:5000", new DebugProbeOptions());

Assert.False(result.IsValid);
Assert.Equal("Compare server URL cannot target localhost", result.Error);
}

[Fact]
public async Task Allows_localhost_when_local_compare_targets_are_enabled()
{
var result = await CompareUrlValidator.ValidateCompareBaseUrlAsync(
"http://localhost:5000/debug",
new DebugProbeOptions { AllowLocalCompareTargets = true });

Assert.True(result.IsValid);
Assert.Equal("http://localhost:5000", result.BaseUri?.ToString().TrimEnd('/'));
}

[Fact]
public async Task Validates_allow_local_compare_targets_for_private_addresses()
{
var blocked = await CompareUrlValidator.ValidateCompareBaseUrlAsync(
"http://127.0.0.1:5000",
new DebugProbeOptions());
var allowed = await CompareUrlValidator.ValidateCompareBaseUrlAsync(
"http://127.0.0.1:5000",
new DebugProbeOptions { AllowLocalCompareTargets = true });

Assert.False(blocked.IsValid);
Assert.True(allowed.IsValid);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using DebugProbe.AspNetCore.Extensions;
using DebugProbe.AspNetCore.Options;
using DebugProbe.AspNetCore.Storage;
using Microsoft.Extensions.DependencyInjection;

namespace DebugProbe.AspNetCore.Tests.Configuration;

public class DebugProbeOptionsTests
{
[Fact]
public void Defaults_work_correctly()
{
var options = new DebugProbeOptions();

Assert.Equal(20, options.MaxEntries);
Assert.Equal(256, options.MaxBodyCaptureSizeKb);
Assert.False(options.AllowLocalCompareTargets);
Assert.Empty(options.IgnorePaths);
}

[Fact]
public void Custom_options_are_registered_and_used()
{
var services = new ServiceCollection();

services.AddDebugProbe(options =>
{
options.MaxEntries = 2;
options.MaxBodyCaptureSizeKb = 4;
options.AllowLocalCompareTargets = true;
options.IgnorePaths = ["/health"];
});

using var provider = services.BuildServiceProvider();
var options = provider.GetRequiredService<DebugProbeOptions>();
var store = provider.GetRequiredService<DebugEntryStore>();

Assert.Equal(2, options.MaxEntries);
Assert.Equal(4, options.MaxBodyCaptureSizeKb);
Assert.True(options.AllowLocalCompareTargets);
Assert.Equal(["/health"], options.IgnorePaths);
Assert.NotNull(store.Environment);
}
}
28 changes: 28 additions & 0 deletions DebugProbe.AspNetCore.Tests/DebugProbe.AspNetCore.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0" />
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="8.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="xunit" Version="2.5.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\DebugProbe.AspNetCore\DebugProbe.AspNetCore.csproj" />
</ItemGroup>

<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>

</Project>
2 changes: 2 additions & 0 deletions DebugProbe.AspNetCore.Tests/GlobalUsings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
global using Microsoft.AspNetCore.Builder;
global using Microsoft.AspNetCore.Http;
65 changes: 65 additions & 0 deletions DebugProbe.AspNetCore.Tests/Infrastructure/DebugProbeTestApp.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using DebugProbe.AspNetCore.Extensions;
using DebugProbe.AspNetCore.Models;
using DebugProbe.AspNetCore.Options;
using DebugProbe.AspNetCore.Storage;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace DebugProbe.AspNetCore.Tests.Infrastructure;

internal sealed class DebugProbeTestApp : IAsyncDisposable
{
private readonly IHost _host;

private DebugProbeTestApp(IHost host)
{
_host = host;
Client = host.GetTestClient();
Store = host.Services.GetRequiredService<DebugEntryStore>();
}

public HttpClient Client { get; }

public DebugEntryStore Store { get; }

public DebugEntry SingleEntry => Assert.Single(Store.GetAll());

public static async Task<DebugProbeTestApp> CreateAsync(
Action<IEndpointRouteBuilder> mapEndpoints,
Action<DebugProbeOptions>? configureOptions = null,
Action<IApplicationBuilder>? configureAfterDebugProbe = null)
{
var host = await new HostBuilder()
.ConfigureWebHost(webHost =>
{
webHost.UseTestServer();
webHost.ConfigureServices(services =>
{
services.AddRouting();
services.AddDebugProbe(configureOptions);
});
webHost.Configure(app =>
{
app.UseRouting();
app.UseDebugProbe();
configureAfterDebugProbe?.Invoke(app);
app.UseEndpoints(mapEndpoints);
});
})
.StartAsync();

return new DebugProbeTestApp(host);
}

public async ValueTask DisposeAsync()
{
Client.Dispose();
await _host.StopAsync();
_host.Dispose();
}
}
92 changes: 92 additions & 0 deletions DebugProbe.AspNetCore.Tests/Middleware/ExceptionHandlingTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
using System.Net;
using System.Text;
using DebugProbe.AspNetCore.Tests.Infrastructure;
using Microsoft.AspNetCore.Diagnostics;

namespace DebugProbe.AspNetCore.Tests.Middleware;

public class ExceptionHandlingTests
{
[Fact]
public async Task Captures_exception_response_text()
{
await using var app = await CreateExceptionAppAsync("handled error");

var response = await app.Client.PostAsync("/throw", JsonContent("{\"id\":42}"));

Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
Assert.Equal("handled error", await response.Content.ReadAsStringAsync());
Assert.Equal("handled error", app.SingleEntry.ResponseBody);
}

[Fact]
public async Task Stores_exception_information_when_exception_is_not_handled()
{
await using var app = await DebugProbeTestApp.CreateAsync(endpoints =>
{
endpoints.MapGet("/throw", (HttpContext _) => throw new InvalidOperationException("unhandled failure"));
});

await Assert.ThrowsAsync<InvalidOperationException>(() => app.Client.GetAsync("/throw"));

var entry = app.SingleEntry;
Assert.Equal(500, entry.StatusCode);
Assert.Contains("InvalidOperationException", entry.ResponseBody);
Assert.Contains("unhandled failure", entry.ResponseBody);
}

[Fact]
public async Task Handles_empty_error_responses()
{
await using var app = await CreateExceptionAppAsync(string.Empty);

var response = await app.Client.GetAsync("/throw");

Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
Assert.Equal(string.Empty, app.SingleEntry.ResponseBody);
}

[Fact]
public async Task Issue_38_exception_endpoint_captures_status_request_body_and_response_body()
{
await using var app = await CreateExceptionAppAsync("{\"error\":\"boom\"}", "application/json");

var response = await app.Client.PostAsync("/throw", JsonContent("{\"name\":\"Ada\"}"));

Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
var entry = app.SingleEntry;
Assert.Equal(500, entry.StatusCode);
Assert.Equal("{\"name\":\"Ada\"}", entry.RequestBody);
Assert.Equal("{\"error\":\"boom\"}", entry.ResponseBody);
}

private static Task<DebugProbeTestApp> CreateExceptionAppAsync(
string errorBody,
string contentType = "text/plain")
{
return DebugProbeTestApp.CreateAsync(
endpoints =>
{
endpoints.MapPost("/throw", (HttpContext _) => throw new InvalidOperationException("boom"));
endpoints.MapGet("/throw", (HttpContext _) => throw new InvalidOperationException("boom"));
},
configureAfterDebugProbe: builder =>
{
builder.UseExceptionHandler(errorApp =>
{
errorApp.Run(async context =>
{
_ = context.Features.Get<IExceptionHandlerFeature>();
context.Response.StatusCode = 500;
context.Response.ContentType = contentType;
await context.Response.WriteAsync(errorBody);
});
});
});
}

private static StringContent JsonContent(string value)
{
return new StringContent(value, Encoding.UTF8, "application/json");
}
}
Loading