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:
Yaojia Wang
2025-11-03 17:38:40 +01:00
parent 4183b10b39
commit dbbb49e5b6
2 changed files with 82 additions and 4 deletions

View File

@@ -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)

View File

@@ -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();
}
/// <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()
{
Client?.Dispose();
Factory?.Dispose();
GC.SuppressFinalize(this);
}