Ввести доменные исключения и ресурсы текстов ошибок
DealException/NotFoundException/Validation/Conflict/ServiceUnavailable с кодом ошибки, тексты в ErrorMessages.resx. Общий HTTP-обработчик (DealExceptionHandler) и маппинг в gRPC-интерцепторе: доменные ошибки → статус, прочие → обобщённый текст без стектрейса. Правила закреплены в код-стайле §10.
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
using System.Text.Json;
|
||||
using Deal.Api.Middleware;
|
||||
using Deal.SharedKernel.Errors;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
|
||||
namespace Deal.Tests.Unit.Api;
|
||||
|
||||
/// <summary>
|
||||
/// Тесты обработчика необработанных исключений HTTP.
|
||||
/// </summary>
|
||||
public sealed class DealExceptionHandlerTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task NotFound_MapsTo404WithCodeAndRussianDetail()
|
||||
{
|
||||
DefaultHttpContext context = CreateContext();
|
||||
DealExceptionHandler handler = new(Substitute.For<ILogger<DealExceptionHandler>>());
|
||||
|
||||
bool handled = await handler.TryHandleAsync(
|
||||
context,
|
||||
new NotFoundException("Карточка", "c_1"),
|
||||
CancellationToken.None);
|
||||
|
||||
Assert.True(handled);
|
||||
Assert.Equal(StatusCodes.Status404NotFound, context.Response.StatusCode);
|
||||
(string detail, string code) = await ReadBodyAsync(context);
|
||||
Assert.Equal(DealErrorCodes.NotFound, code);
|
||||
Assert.Contains("Карточка", detail);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Unavailable_MapsTo503()
|
||||
{
|
||||
DefaultHttpContext context = CreateContext();
|
||||
DealExceptionHandler handler = new(Substitute.For<ILogger<DealExceptionHandler>>());
|
||||
|
||||
await handler.TryHandleAsync(
|
||||
context,
|
||||
new ServiceUnavailableException("ИИ"),
|
||||
CancellationToken.None);
|
||||
|
||||
Assert.Equal(StatusCodes.Status503ServiceUnavailable, context.Response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UnexpectedException_MapsToGeneric500WithoutStackOrDetails()
|
||||
{
|
||||
DefaultHttpContext context = CreateContext();
|
||||
DealExceptionHandler handler = new(Substitute.For<ILogger<DealExceptionHandler>>());
|
||||
|
||||
bool handled = await handler.TryHandleAsync(
|
||||
context,
|
||||
new InvalidOperationException("секретная внутренняя деталь"),
|
||||
CancellationToken.None);
|
||||
|
||||
Assert.True(handled);
|
||||
Assert.Equal(StatusCodes.Status500InternalServerError, context.Response.StatusCode);
|
||||
(string detail, string code) = await ReadBodyAsync(context);
|
||||
Assert.Equal(DealErrorCodes.Internal, code);
|
||||
Assert.DoesNotContain("секретная внутренняя деталь", detail);
|
||||
Assert.DoesNotContain("at ", detail);
|
||||
}
|
||||
|
||||
private static DefaultHttpContext CreateContext()
|
||||
{
|
||||
var context = new DefaultHttpContext();
|
||||
context.Response.Body = new MemoryStream();
|
||||
return context;
|
||||
}
|
||||
|
||||
private static async Task<(string Detail, string Code)> ReadBodyAsync(HttpContext context)
|
||||
{
|
||||
context.Response.Body.Seek(0, SeekOrigin.Begin);
|
||||
using var reader = new StreamReader(context.Response.Body);
|
||||
string json = await reader.ReadToEndAsync();
|
||||
using JsonDocument document = JsonDocument.Parse(json);
|
||||
return (
|
||||
document.RootElement.GetProperty("detail").GetString() ?? string.Empty,
|
||||
document.RootElement.GetProperty("code").GetString() ?? string.Empty);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user