using FluentValidation; using FluentValidation.Results; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Json; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Microsoft.EntityFrameworkCore; using Prefab.Handler; using Prefab.Catalog.Domain.Exceptions; using Prefab.Web.Shared; using Shouldly; using System.Text.Json; namespace Prefab.Tests.Unit.Web.Shared; public sealed class ExceptionHandlerShould { [Fact] public async Task ReturnBadRequestForValidationFailures() { var failure = new ValidationFailure("productId", "Either productId or sku must be provided."); var exception = new ValidationException(new[] { failure }); var context = new DefaultHttpContext(); context.Response.Body = new MemoryStream(); var services = new ServiceCollection(); services.AddSingleton(); using var provider = services.BuildServiceProvider(); context.RequestServices = provider; var middleware = new ExceptionHandler( _ => throw exception, NullLogger.Instance, Options.Create(new JsonOptions())); await middleware.InvokeAsync(context); context.Response.StatusCode.ShouldBe(StatusCodes.Status400BadRequest); context.Response.Body.Seek(0, SeekOrigin.Begin); using var reader = new StreamReader(context.Response.Body); var payload = await reader.ReadToEndAsync(TestContext.Current.CancellationToken); using var document = JsonDocument.Parse(payload); var root = document.RootElement; var messages = new List(); if (root.TryGetProperty("errors", out var errorsElement)) { messages.AddRange( errorsElement .EnumerateObject() .SelectMany(property => property.Value.EnumerateArray()) .Select(element => element.GetString()) .Where(message => !string.IsNullOrWhiteSpace(message))! .Cast()); } if (messages.Count == 0 && root.TryGetProperty("detail", out var detailElement)) { messages.Add(detailElement.GetString() ?? string.Empty); } messages.ShouldNotBeEmpty(payload); messages.ShouldContain(message => message.Equals("Either productId or sku must be provided.", StringComparison.OrdinalIgnoreCase)); } [Fact] public async Task ReturnBadRequestForDomainExceptions() { var exception = new DomainValidationException("Demo domain failure."); var context = new DefaultHttpContext(); context.Response.Body = new MemoryStream(); var services = new ServiceCollection(); services.AddSingleton(); using var provider = services.BuildServiceProvider(); context.RequestServices = provider; var middleware = new ExceptionHandler( _ => throw exception, NullLogger.Instance, Options.Create(new JsonOptions())); await middleware.InvokeAsync(context); context.Response.StatusCode.ShouldBe(StatusCodes.Status400BadRequest); context.Response.Body.Seek(0, SeekOrigin.Begin); using var reader = new StreamReader(context.Response.Body); var payload = await reader.ReadToEndAsync(TestContext.Current.CancellationToken); using var document = JsonDocument.Parse(payload); var detail = document.RootElement.GetProperty("detail").GetString(); document.RootElement.GetProperty("type").GetString().ShouldBe("https://prefab.dev/problems/domain-error"); detail.ShouldNotBeNull(); detail!.ShouldContain("Demo domain failure."); } [Fact] public async Task ReturnConflictForConcurrencyExceptions() { var exception = new DbUpdateConcurrencyException("Concurrency conflict"); var context = new DefaultHttpContext(); context.Response.Body = new MemoryStream(); var services = new ServiceCollection(); services.AddSingleton(); using var provider = services.BuildServiceProvider(); context.RequestServices = provider; var middleware = new ExceptionHandler( _ => throw exception, NullLogger.Instance, Options.Create(new JsonOptions())); await middleware.InvokeAsync(context); context.Response.StatusCode.ShouldBe(StatusCodes.Status409Conflict); context.Response.Body.Seek(0, SeekOrigin.Begin); using var reader = new StreamReader(context.Response.Body); var payload = await reader.ReadToEndAsync(TestContext.Current.CancellationToken); using var document = JsonDocument.Parse(payload); document.RootElement.GetProperty("type").GetString().ShouldBe("https://prefab.dev/problems/concurrency-conflict"); } }