Add ValidatePDF feature with API, Swagger, and tests
Implemented the ValidatePDF feature end-to-end: - Added `/api/v1/documents/validate` Minimal API endpoint. - Introduced centralized ExceptionHandlingMiddleware. - Configured Swagger with `AddSwaggerDocumentation` extension. - Enabled XML comments in `DocumentOperator.API.csproj`. - Updated DTOs with XML comments and added `FileSizeMB`. - Added integration tests for the ValidatePDF endpoint (3 tests). - Registered infrastructure services (e.g., `IPdfProcessor`). - Refactored `Program.cs` to include middleware and endpoints. - Updated PHASENPLAN.md and ROADMAP.md to reflect progress. - Cleaned up code and made `Program` accessible for tests.
This commit is contained in:
@@ -1,6 +1,124 @@
|
||||
namespace DocumentOperator.API.Middleware
|
||||
using DocumentOperator.Domain.Common.Exceptions;
|
||||
using FluentValidation;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace DocumentOperator.API.Middleware;
|
||||
|
||||
/// <summary>
|
||||
/// Central exception handling middleware
|
||||
/// Maps exceptions to HTTP status codes and RFC 7807 Problem Details
|
||||
/// </summary>
|
||||
public class ExceptionHandlingMiddleware
|
||||
{
|
||||
public class ExceptionHandlingMiddleware
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly ILogger<ExceptionHandlingMiddleware> _logger;
|
||||
|
||||
public ExceptionHandlingMiddleware(RequestDelegate next, ILogger<ExceptionHandlingMiddleware> logger)
|
||||
{
|
||||
_next = next;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _next(context);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Unhandled exception: {ExceptionMessage}", ex.Message);
|
||||
await HandleExceptionAsync(context, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task HandleExceptionAsync(HttpContext context, Exception exception)
|
||||
{
|
||||
var (statusCode, problemDetails) = MapExceptionToProblemDetails(exception, context);
|
||||
|
||||
context.Response.StatusCode = (int)statusCode;
|
||||
context.Response.ContentType = "application/problem+json";
|
||||
|
||||
var options = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
};
|
||||
|
||||
await context.Response.WriteAsync(JsonSerializer.Serialize(problemDetails, options));
|
||||
}
|
||||
|
||||
private static (HttpStatusCode StatusCode, ProblemDetails ProblemDetails) MapExceptionToProblemDetails(
|
||||
Exception exception,
|
||||
HttpContext context)
|
||||
{
|
||||
return exception switch
|
||||
{
|
||||
// FluentValidation (400 Bad Request)
|
||||
ValidationException validationEx => (
|
||||
HttpStatusCode.BadRequest,
|
||||
new ProblemDetails
|
||||
{
|
||||
Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.1",
|
||||
Title = "Validation Error",
|
||||
Status = (int)HttpStatusCode.BadRequest,
|
||||
Detail = string.Join("; ", validationEx.Errors.Select(e => e.ErrorMessage)),
|
||||
Instance = context.Request.Path
|
||||
}
|
||||
),
|
||||
|
||||
// Domain Validation Exception (400 Bad Request)
|
||||
DomainValidationException domainEx => (
|
||||
HttpStatusCode.BadRequest,
|
||||
new ProblemDetails
|
||||
{
|
||||
Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.1",
|
||||
Title = "Domain Validation Error",
|
||||
Status = (int)HttpStatusCode.BadRequest,
|
||||
Detail = domainEx.Message,
|
||||
Instance = context.Request.Path
|
||||
}
|
||||
),
|
||||
|
||||
// Not Found Exception (404 Not Found)
|
||||
NotFoundException notFoundEx => (
|
||||
HttpStatusCode.NotFound,
|
||||
new ProblemDetails
|
||||
{
|
||||
Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.4",
|
||||
Title = "Resource Not Found",
|
||||
Status = (int)HttpStatusCode.NotFound,
|
||||
Detail = notFoundEx.Message,
|
||||
Instance = context.Request.Path
|
||||
}
|
||||
),
|
||||
|
||||
// PDF Processing Exception (500 Internal Server Error)
|
||||
PdfProcessingException pdfEx => (
|
||||
HttpStatusCode.InternalServerError,
|
||||
new ProblemDetails
|
||||
{
|
||||
Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.6.1",
|
||||
Title = "PDF Processing Error",
|
||||
Status = (int)HttpStatusCode.InternalServerError,
|
||||
Detail = pdfEx.Message,
|
||||
Instance = context.Request.Path
|
||||
}
|
||||
),
|
||||
|
||||
// Generic Exception (500 Internal Server Error)
|
||||
_ => (
|
||||
HttpStatusCode.InternalServerError,
|
||||
new ProblemDetails
|
||||
{
|
||||
Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.6.1",
|
||||
Title = "Internal Server Error",
|
||||
Status = (int)HttpStatusCode.InternalServerError,
|
||||
Detail = "An unexpected error occurred. Please contact support.",
|
||||
Instance = context.Request.Path
|
||||
}
|
||||
)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user