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
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
using System.Diagnostics;
using System.Text.Json;

namespace CR.Exceptions.AspNet.UnitTests;
namespace CR.Exceptions.AspNet.Tests.Component;

public sealed class CrExceptionHandlerTests
{
Expand All @@ -31,7 +31,7 @@ public Task Should_Return_404_For_NotFoundException()
public Task Should_Return_500_For_UnhandledException()
{
return AssertHandlerResult(
new Exception("Unknown exception"),
new InvalidOperationException(),
StatusCodes.Status500InternalServerError,
canCreateActivity: true);
}
Expand All @@ -40,7 +40,7 @@ public Task Should_Return_500_For_UnhandledException()
public Task Should_Return_500_For_UnhandledException_When_Activity_Is_Missing()
{
return AssertHandlerResult(
new Exception("Unknown exception"),
new InvalidOperationException(),
StatusCodes.Status500InternalServerError,
canCreateActivity: false);
}
Expand All @@ -57,61 +57,61 @@ private async Task AssertHandlerResult(Exception exception, int expectedStatusCo
using var responseStream = new MemoryStream();
var context = CreateContext(responseStream);

var isHandled = await handler.TryHandleAsync(context, exception, CancellationToken.None);

Assert.True(isHandled);
Assert.Equal(expectedStatusCode, context.Response.StatusCode);
Assert.Contains("application/problem+json", context.Response.ContentType);
Assert.True(await handler.TryHandleAsync(context, exception, CancellationToken.None));
AssertHttpContext(context, expectedStatusCode);

responseStream.Position = 0;

var problem = await JsonSerializer.DeserializeAsync<CustomProblemDetails>(responseStream, JsonSerializerOptions.Web);
var problem = await JsonSerializer.DeserializeAsync<TestProblemDetails>(responseStream, JsonSerializerOptions.Web);
var expectedTraceId = activity?.TraceId.ToHexString() ?? context.TraceIdentifier;

AssertProblemDetails(problem, context, expectedStatusCode, expectedTraceId);
}

private static ServiceProvider CreateServiceProvider()
{
return new ServiceCollection()
.AddLogging()
.AddCrExceptionHandler()
.BuildServiceProvider();
_output.WriteLine(JsonSerializer.Serialize(problem, options: _prettyJsonOptions));
}

private static DefaultHttpContext CreateContext(MemoryStream responseStream)
private static void AssertHttpContext(HttpContext context, int expectedStatusCode)
{
return new DefaultHttpContext
{
Request = { Path = "/api/test" },
Response = { Body = responseStream }
};
Assert.Equal(expectedStatusCode, context.Response.StatusCode);
Assert.Contains("application/problem+json", context.Response.ContentType);
}

private void AssertProblemDetails(CustomProblemDetails? problem, HttpContext context, int expectedStatusCode, string? expectedTraceId)
private static void AssertProblemDetails(TestProblemDetails? problem, HttpContext context, int expectedStatusCode, string? expectedTraceId)
{
Assert.NotNull(problem);

_output.WriteLine(JsonSerializer.Serialize(problem, options: _prettyJsonOptions));

Assert.False(string.IsNullOrEmpty(problem.Type));
Assert.False(string.IsNullOrEmpty(problem.Title));
Assert.False(string.IsNullOrEmpty(problem.Detail));

Assert.Equal(expectedStatusCode, problem.Status);
Assert.Equal(context.Request.Path, problem.Instance);

Assert.True(problem.Extensions.TryGetValue(
ProblemDetailsExtensionNames.TraceId,
out var traceId));

Assert.Equal(expectedTraceId, traceId?.ToString());
Assert.True(problem.Extensions.TryGetValue(ProblemDetailsExtensionNames.TraceId, out var traceId));
Assert.Equal(expectedTraceId, traceId!.ToString());

Assert.NotNull(problem.Errors);
Assert.NotEmpty(problem.Errors);
}

private sealed class CustomProblemDetails : ProblemDetails
private static ServiceProvider CreateServiceProvider()
{
return new ServiceCollection()
.AddLogging()
.AddCrExceptions()
.BuildServiceProvider();
}

private static DefaultHttpContext CreateContext(MemoryStream responseStream)
{
return new DefaultHttpContext
{
Request = { Path = "/api/test" },
Response = { Body = responseStream }
};
}

private sealed class TestProblemDetails : ProblemDetails
{
public CrError[]? Errors { get; set; }
}
Expand Down
35 changes: 35 additions & 0 deletions CR.Exceptions.AspNet.UnitTests/Component/LogLevelMapTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using CR.Exceptions.AspNet.Mapping;
using Microsoft.Extensions.Logging;

namespace CR.Exceptions.AspNet.Tests.Component;

public sealed class LogLevelMapTests
{
[Fact]
public void TryFind_ShouldReturn_Level_For_NotFoundException()
{
var level = LogLevel.Warning;
var map = CreateMap(builder => builder.Map<NotFoundException>(level));

var result = map.TryFind(new TestNotFoundException(), out var actualLevel);

Assert.True(result);
Assert.Equal(level, actualLevel);
}

[Fact]
public void TryFind_ShouldReturn_False_For_UnregisteredException()
{
var map = CreateMap();

Assert.False(map.TryFind(new TestUnregisteredException(), out var _));
}

private static LogLevelMap CreateMap(Action<LogLevelMapBuilder>? configurator = null)
{
var builder = new LogLevelMapBuilder();
configurator?.Invoke(builder);

return builder.Build();
}
}
35 changes: 35 additions & 0 deletions CR.Exceptions.AspNet.UnitTests/Component/StatusCodeMapTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using CR.Exceptions.AspNet.Mapping;
using Microsoft.AspNetCore.Http;

namespace CR.Exceptions.AspNet.Tests.Component;

public sealed class StatusCodeMapTests
{
[Fact]
public void TryFind_ShouldReturn_404_For_NotFoundException()
{
var code = StatusCodes.Status404NotFound;
var map = CreateMap(builder => builder.Map<NotFoundException>(code));

var result = map.TryFind(new TestNotFoundException(), out var actualCode);

Assert.True(result);
Assert.Equal(code, actualCode);
}

[Fact]
public void TryFind_ShouldReturn_False_For_UnregisteredException()
{
var map = CreateMap();

Assert.False(map.TryFind(new TestUnregisteredException(), out var _));
}

private static StatusCodeMap CreateMap(Action<StatusCodeMapBuilder>? configurator = null)
{
var builder = new StatusCodeMapBuilder();
configurator?.Invoke(builder);

return builder.Build();
}
}
34 changes: 0 additions & 34 deletions CR.Exceptions.AspNet.UnitTests/ExceptionStatusCodeOptionsTests.cs

This file was deleted.

4 changes: 2 additions & 2 deletions CR.Exceptions.AspNet.UnitTests/TestNotFoundException.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
namespace CR.Exceptions.AspNet.UnitTests;
namespace CR.Exceptions.AspNet.Tests;

internal sealed class TestNotFoundException : NotFoundException
{
public TestNotFoundException() : base([new("TestNotFound", "Test Entity not found")])
public TestNotFoundException() : base([new("TestNotFound", "Test entity not found error message")])
{
}
}
8 changes: 8 additions & 0 deletions CR.Exceptions.AspNet.UnitTests/TestUnregisteredException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace CR.Exceptions.AspNet.Tests;

internal sealed class TestUnregisteredException : CrException
{
public TestUnregisteredException() : base([new("TestUnregistered", "Test error message")], "Unregistered detail")
{
}
}
43 changes: 43 additions & 0 deletions CR.Exceptions.AspNet.UnitTests/Unit/LogLevelMapBuilderTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using CR.Exceptions.AspNet.Mapping;
using Microsoft.Extensions.Logging;

namespace CR.Exceptions.AspNet.Tests.Unit;

public sealed class LogLevelMapBuilderTests
{
[Fact]
public void Map_ShouldThrow_When_DuplicateLevelRegistered()
{
var builder = CreateBuilder()
.Map<InternalException>(LogLevel.Error);

Assert.ThrowsAny<ArgumentException>(() => builder.Map<InternalException>(LogLevel.Warning));
}

[Fact]
public void Map_ShouldThrow_When_InvalidLevelRegistered()
{
var builder = CreateBuilder();

Assert.ThrowsAny<ArgumentException>(() => builder.Map<ConflictException>((LogLevel)4000));
}

[Fact]
public void Map_ShouldThrow_When_NoneLevelRegistered()
{
var builder = CreateBuilder();

Assert.ThrowsAny<ArgumentException>(() => builder.Map<ConflictException>(LogLevel.None));
}

[Fact]
public void Build_ShouldReturn_Map_WithDefaultMappings()
{
var builder = CreateBuilder()
.AddDefaultMappings();

Assert.NotNull(() => builder.Build());
}

private static LogLevelMapBuilder CreateBuilder() => new();
}
35 changes: 35 additions & 0 deletions CR.Exceptions.AspNet.UnitTests/Unit/StatusCodeMapBuilderTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using CR.Exceptions.AspNet.Mapping;
using Microsoft.AspNetCore.Http;

namespace CR.Exceptions.AspNet.Tests.Unit;

public sealed class StatusCodeMapBuilderTests
{
[Fact]
public void Map_ShouldThrow_When_DuplicateCodeRegistered()
{
var builder = CreateBuilder()
.Map<ValidationException>(StatusCodes.Status400BadRequest);

Assert.ThrowsAny<ArgumentException>(() => builder.Map<ValidationException>(StatusCodes.Status404NotFound));
}

[Fact]
public void Map_ShouldThrow_When_InvalidCodeRegistered()
{
var builder = CreateBuilder();

Assert.ThrowsAny<ArgumentException>(() => builder.Map<ValidationException>(4000));
}

[Fact]
public void Build_ShouldReturn_Map_WithDefaultMappings()
{
var builder = CreateBuilder()
.AddDefaultMappings();

Assert.NotNull(() => builder.Build());
}

private static StatusCodeMapBuilder CreateBuilder() => new();
}
10 changes: 9 additions & 1 deletion CR.Exceptions.AspNet/CR.Exceptions.AspNet.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@

<PropertyGroup>
<Description>ASP.NET Core integration for CR.Exceptions.</Description>

<PackageId>$(NuGetPackagePrefix).Exceptions.AspNet</PackageId>
<PackageTags>exceptions;aspnetcore;webapi;problem-details;rfc7807;middleware;exception-handler</PackageTags>

<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup>

<ItemGroup>
Expand All @@ -19,4 +23,8 @@
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>

</Project>
<ItemGroup>
<None Include="README.md" Pack="true" PackagePath="\" />
</ItemGroup>

</Project>
36 changes: 0 additions & 36 deletions CR.Exceptions.AspNet/CrExceptionHandler.Logger.cs

This file was deleted.

Loading
Loading