fix(backend): Fix integration test failures - GlobalExceptionHandler and test isolation
Fixed 8 failing integration tests by addressing two root causes: 1. GlobalExceptionHandler returning incorrect HTTP status codes - Added handling for UnauthorizedAccessException → 401 - Added handling for ArgumentException/InvalidOperationException → 400 - Added handling for DbUpdateException (duplicate key) → 409 - Now correctly maps exception types to HTTP status codes 2. Test isolation issue with shared HttpClient - Modified DatabaseFixture to create new HttpClient for each test - Prevents Authorization header pollution between tests - Ensures clean test state for authentication tests Test Results: - Before: 23/31 passed (8 failed) - After: 31/31 passed (0 failed) Changes: - Enhanced GlobalExceptionHandler with proper status code mapping - Fixed DatabaseFixture.Client to create isolated instances - All authentication and RBAC tests now pass 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@ using System.Diagnostics;
|
|||||||
using FluentValidation;
|
using FluentValidation;
|
||||||
using Microsoft.AspNetCore.Diagnostics;
|
using Microsoft.AspNetCore.Diagnostics;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
using ColaFlow.Modules.ProjectManagement.Domain.Exceptions;
|
using ColaFlow.Modules.ProjectManagement.Domain.Exceptions;
|
||||||
|
|
||||||
namespace ColaFlow.API.Handlers;
|
namespace ColaFlow.API.Handlers;
|
||||||
@@ -25,7 +26,7 @@ public sealed class GlobalExceptionHandler : IExceptionHandler
|
|||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
// Log with appropriate level based on exception type
|
// 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);
|
_logger.LogWarning(exception, "Client error occurred: {Message}", exception.Message);
|
||||||
}
|
}
|
||||||
@@ -39,6 +40,10 @@ public sealed class GlobalExceptionHandler : IExceptionHandler
|
|||||||
ValidationException validationEx => CreateValidationProblemDetails(httpContext, validationEx),
|
ValidationException validationEx => CreateValidationProblemDetails(httpContext, validationEx),
|
||||||
DomainException domainEx => CreateDomainProblemDetails(httpContext, domainEx),
|
DomainException domainEx => CreateDomainProblemDetails(httpContext, domainEx),
|
||||||
NotFoundException notFoundEx => CreateNotFoundProblemDetails(httpContext, notFoundEx),
|
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)
|
_ => CreateInternalServerErrorProblemDetails(httpContext, exception)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -48,6 +53,15 @@ public sealed class GlobalExceptionHandler : IExceptionHandler
|
|||||||
return true; // Exception handled
|
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(
|
private static ProblemDetails CreateValidationProblemDetails(
|
||||||
HttpContext context,
|
HttpContext context,
|
||||||
ValidationException exception)
|
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(
|
private static ProblemDetails CreateInternalServerErrorProblemDetails(
|
||||||
HttpContext context,
|
HttpContext context,
|
||||||
Exception exception)
|
Exception exception)
|
||||||
|
|||||||
@@ -8,18 +8,28 @@ namespace ColaFlow.Modules.Identity.IntegrationTests.Infrastructure;
|
|||||||
public class DatabaseFixture : IDisposable
|
public class DatabaseFixture : IDisposable
|
||||||
{
|
{
|
||||||
public ColaFlowWebApplicationFactory Factory { get; }
|
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()
|
public DatabaseFixture()
|
||||||
{
|
{
|
||||||
// Use In-Memory Database for fast, isolated tests
|
// Use In-Memory Database for fast, isolated tests
|
||||||
Factory = new ColaFlowWebApplicationFactory(useInMemoryDatabase: true);
|
Factory = new ColaFlowWebApplicationFactory(useInMemoryDatabase: true);
|
||||||
Client = Factory.CreateClient();
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new HttpClient for each test to ensure test isolation
|
||||||
|
/// Prevents Authorization header sharing between tests
|
||||||
|
/// </summary>
|
||||||
|
public HttpClient CreateClient()
|
||||||
|
{
|
||||||
|
return Factory.CreateClient();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
Client?.Dispose();
|
|
||||||
Factory?.Dispose();
|
Factory?.Dispose();
|
||||||
GC.SuppressFinalize(this);
|
GC.SuppressFinalize(this);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user