diff --git a/colaflow-api/src/ColaFlow.API/Handlers/GlobalExceptionHandler.cs b/colaflow-api/src/ColaFlow.API/Handlers/GlobalExceptionHandler.cs index f3532f2..b437fc1 100644 --- a/colaflow-api/src/ColaFlow.API/Handlers/GlobalExceptionHandler.cs +++ b/colaflow-api/src/ColaFlow.API/Handlers/GlobalExceptionHandler.cs @@ -2,6 +2,7 @@ using System.Diagnostics; using FluentValidation; using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; using ColaFlow.Modules.ProjectManagement.Domain.Exceptions; namespace ColaFlow.API.Handlers; @@ -25,7 +26,7 @@ public sealed class GlobalExceptionHandler : IExceptionHandler CancellationToken cancellationToken) { // Log with appropriate level based on exception type - if (exception is ValidationException or DomainException or NotFoundException) + if (exception is ValidationException or DomainException or NotFoundException or UnauthorizedAccessException or ArgumentException) { _logger.LogWarning(exception, "Client error occurred: {Message}", exception.Message); } @@ -39,6 +40,10 @@ public sealed class GlobalExceptionHandler : IExceptionHandler ValidationException validationEx => CreateValidationProblemDetails(httpContext, validationEx), DomainException domainEx => CreateDomainProblemDetails(httpContext, domainEx), NotFoundException notFoundEx => CreateNotFoundProblemDetails(httpContext, notFoundEx), + UnauthorizedAccessException unauthorizedEx => CreateUnauthorizedProblemDetails(httpContext, unauthorizedEx), + ArgumentException argumentEx => CreateBadRequestProblemDetails(httpContext, argumentEx), + InvalidOperationException invalidOpEx => CreateBadRequestProblemDetails(httpContext, invalidOpEx), + DbUpdateException dbUpdateEx when IsDuplicateKeyViolation(dbUpdateEx) => CreateConflictProblemDetails(httpContext, dbUpdateEx), _ => CreateInternalServerErrorProblemDetails(httpContext, exception) }; @@ -48,6 +53,15 @@ public sealed class GlobalExceptionHandler : IExceptionHandler return true; // Exception handled } + private static bool IsDuplicateKeyViolation(DbUpdateException exception) + { + // Check for duplicate key violation in SQL Server or PostgreSQL + var innerException = exception.InnerException?.Message ?? string.Empty; + return innerException.Contains("duplicate key", StringComparison.OrdinalIgnoreCase) || + innerException.Contains("unique constraint", StringComparison.OrdinalIgnoreCase) || + innerException.Contains("IX_", StringComparison.OrdinalIgnoreCase); + } + private static ProblemDetails CreateValidationProblemDetails( HttpContext context, ValidationException exception) @@ -110,6 +124,60 @@ public sealed class GlobalExceptionHandler : IExceptionHandler }; } + private static ProblemDetails CreateUnauthorizedProblemDetails( + HttpContext context, + UnauthorizedAccessException exception) + { + return new ProblemDetails + { + Type = "https://tools.ietf.org/html/rfc7235#section-3.1", + Title = "Unauthorized", + Status = StatusCodes.Status401Unauthorized, + Detail = exception.Message, + Instance = context.Request.Path, + Extensions = + { + ["traceId"] = Activity.Current?.Id ?? context.TraceIdentifier + } + }; + } + + private static ProblemDetails CreateBadRequestProblemDetails( + HttpContext context, + Exception exception) + { + return new ProblemDetails + { + Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1", + Title = "Bad Request", + Status = StatusCodes.Status400BadRequest, + Detail = exception.Message, + Instance = context.Request.Path, + Extensions = + { + ["traceId"] = Activity.Current?.Id ?? context.TraceIdentifier + } + }; + } + + private static ProblemDetails CreateConflictProblemDetails( + HttpContext context, + DbUpdateException exception) + { + return new ProblemDetails + { + Type = "https://tools.ietf.org/html/rfc7231#section-6.5.8", + Title = "Conflict", + Status = StatusCodes.Status409Conflict, + Detail = "A resource with the same identifier already exists.", + Instance = context.Request.Path, + Extensions = + { + ["traceId"] = Activity.Current?.Id ?? context.TraceIdentifier + } + }; + } + private static ProblemDetails CreateInternalServerErrorProblemDetails( HttpContext context, Exception exception) diff --git a/colaflow-api/tests/Modules/Identity/ColaFlow.Modules.Identity.IntegrationTests/Infrastructure/DatabaseFixture.cs b/colaflow-api/tests/Modules/Identity/ColaFlow.Modules.Identity.IntegrationTests/Infrastructure/DatabaseFixture.cs index 300d018..9a7986f 100644 --- a/colaflow-api/tests/Modules/Identity/ColaFlow.Modules.Identity.IntegrationTests/Infrastructure/DatabaseFixture.cs +++ b/colaflow-api/tests/Modules/Identity/ColaFlow.Modules.Identity.IntegrationTests/Infrastructure/DatabaseFixture.cs @@ -8,18 +8,28 @@ namespace ColaFlow.Modules.Identity.IntegrationTests.Infrastructure; public class DatabaseFixture : IDisposable { public ColaFlowWebApplicationFactory Factory { get; } - public HttpClient Client { get; } + + // Note: Client property is kept for backward compatibility but creates new instances + // Tests should call CreateClient() for isolation to avoid shared state issues + public HttpClient Client => CreateClient(); public DatabaseFixture() { // Use In-Memory Database for fast, isolated tests Factory = new ColaFlowWebApplicationFactory(useInMemoryDatabase: true); - Client = Factory.CreateClient(); + } + + /// + /// Creates a new HttpClient for each test to ensure test isolation + /// Prevents Authorization header sharing between tests + /// + public HttpClient CreateClient() + { + return Factory.CreateClient(); } public void Dispose() { - Client?.Dispose(); Factory?.Dispose(); GC.SuppressFinalize(this); }