feat(backend): Implement Story 5.4 - MCP Error Handling & Logging
Implement comprehensive error handling and structured logging for MCP module. **Exception Hierarchy**: - Created McpException base class with JSON-RPC error mapping - Implemented 8 specific exception types (Parse, InvalidRequest, MethodNotFound, etc.) - Each exception maps to correct HTTP status code (401, 403, 404, 422, 400, 500) **Middleware**: - McpCorrelationIdMiddleware: Generates/extracts correlation ID for request tracking - McpExceptionHandlerMiddleware: Global exception handler with JSON-RPC error responses - McpLoggingMiddleware: Request/response logging with sensitive data sanitization **Serilog Integration**: - Configured structured logging with Console and File sinks - Log rotation (daily, 30-day retention) - Correlation ID enrichment in all log entries **Features**: - Correlation ID propagation across request chain - Structured logging with TenantId, UserId, ApiKeyId - Sensitive data sanitization (API keys, passwords) - Performance metrics (request duration, slow request warnings) - JSON-RPC 2.0 compliant error responses **Testing**: - 174 tests passing (all MCP module tests) - Unit tests for all exception classes - Unit tests for all middleware components - 100% coverage of error mapping and HTTP status codes **Files Added**: - 9 exception classes in Domain/Exceptions/ - 3 middleware classes in Infrastructure/Middleware/ - 4 test files with comprehensive coverage **Files Modified**: - Program.cs: Serilog configuration - McpServiceExtensions.cs: Middleware pipeline registration - JsonRpcError.cs: Added parameterless constructor for deserialization - MCP Infrastructure .csproj: Added Serilog package reference **Verification**: ✅ All 174 MCP module tests passing ✅ Build successful with no errors ✅ Exception-to-HTTP-status mapping verified ✅ Correlation ID propagation tested ✅ Sensitive data sanitization verified Story: docs/stories/sprint_5/story_5_4.md 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
using ColaFlow.Modules.Mcp.Contracts.JsonRpc;
|
||||
using ColaFlow.Modules.Mcp.Domain.Exceptions;
|
||||
using FluentAssertions;
|
||||
|
||||
namespace ColaFlow.Modules.Mcp.Tests.Domain.Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for MCP exception classes
|
||||
/// </summary>
|
||||
public class McpExceptionTests
|
||||
{
|
||||
[Fact]
|
||||
public void McpParseException_ShouldHaveCorrectErrorCode()
|
||||
{
|
||||
// Arrange & Act
|
||||
var exception = new McpParseException("Invalid JSON");
|
||||
|
||||
// Assert
|
||||
exception.ErrorCode.Should().Be(JsonRpcErrorCode.ParseError);
|
||||
exception.Message.Should().Be("Invalid JSON");
|
||||
exception.GetHttpStatusCode().Should().Be(400);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void McpInvalidRequestException_ShouldHaveCorrectErrorCode()
|
||||
{
|
||||
// Arrange & Act
|
||||
var exception = new McpInvalidRequestException("Missing required field");
|
||||
|
||||
// Assert
|
||||
exception.ErrorCode.Should().Be(JsonRpcErrorCode.InvalidRequest);
|
||||
exception.Message.Should().Be("Missing required field");
|
||||
exception.GetHttpStatusCode().Should().Be(400);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void McpMethodNotFoundException_ShouldHaveCorrectErrorCode()
|
||||
{
|
||||
// Arrange & Act
|
||||
var exception = new McpMethodNotFoundException("unknown_method");
|
||||
|
||||
// Assert
|
||||
exception.ErrorCode.Should().Be(JsonRpcErrorCode.MethodNotFound);
|
||||
exception.Message.Should().Be("Method not found: unknown_method");
|
||||
exception.GetHttpStatusCode().Should().Be(404);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void McpInvalidParamsException_ShouldHaveCorrectErrorCode()
|
||||
{
|
||||
// Arrange & Act
|
||||
var exception = new McpInvalidParamsException("Invalid parameter type");
|
||||
|
||||
// Assert
|
||||
exception.ErrorCode.Should().Be(JsonRpcErrorCode.InvalidParams);
|
||||
exception.Message.Should().Be("Invalid parameter type");
|
||||
exception.GetHttpStatusCode().Should().Be(400);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void McpUnauthorizedException_ShouldHaveCorrectErrorCode()
|
||||
{
|
||||
// Arrange & Act
|
||||
var exception = new McpUnauthorizedException("Invalid API key");
|
||||
|
||||
// Assert
|
||||
exception.ErrorCode.Should().Be(JsonRpcErrorCode.Unauthorized);
|
||||
exception.Message.Should().Be("Invalid API key");
|
||||
exception.GetHttpStatusCode().Should().Be(401);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void McpForbiddenException_ShouldHaveCorrectErrorCode()
|
||||
{
|
||||
// Arrange & Act
|
||||
var exception = new McpForbiddenException("Insufficient permissions");
|
||||
|
||||
// Assert
|
||||
exception.ErrorCode.Should().Be(JsonRpcErrorCode.Forbidden);
|
||||
exception.Message.Should().Be("Insufficient permissions");
|
||||
exception.GetHttpStatusCode().Should().Be(403);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void McpNotFoundException_ShouldHaveCorrectErrorCode()
|
||||
{
|
||||
// Arrange & Act
|
||||
var exception = new McpNotFoundException("Task", "task-123");
|
||||
|
||||
// Assert
|
||||
exception.ErrorCode.Should().Be(JsonRpcErrorCode.NotFound);
|
||||
exception.Message.Should().Be("Task not found: task-123");
|
||||
exception.GetHttpStatusCode().Should().Be(404);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void McpValidationException_ShouldHaveCorrectErrorCode()
|
||||
{
|
||||
// Arrange & Act
|
||||
var errorData = new { field = "name", error = "required" };
|
||||
var exception = new McpValidationException("Validation failed", errorData);
|
||||
|
||||
// Assert
|
||||
exception.ErrorCode.Should().Be(JsonRpcErrorCode.ValidationFailed);
|
||||
exception.Message.Should().Be("Validation failed");
|
||||
exception.ErrorData.Should().Be(errorData);
|
||||
exception.GetHttpStatusCode().Should().Be(422);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToJsonRpcError_ShouldConvertToJsonRpcError()
|
||||
{
|
||||
// Arrange
|
||||
var errorData = new { detail = "test" };
|
||||
var exception = new McpValidationException("Test error", errorData);
|
||||
|
||||
// Act
|
||||
var jsonRpcError = exception.ToJsonRpcError();
|
||||
|
||||
// Assert
|
||||
jsonRpcError.Code.Should().Be((int)JsonRpcErrorCode.ValidationFailed);
|
||||
jsonRpcError.Message.Should().Be("Test error");
|
||||
jsonRpcError.Data.Should().Be(errorData);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void McpParseException_WithInnerException_ShouldPreserveInnerException()
|
||||
{
|
||||
// Arrange
|
||||
var innerException = new InvalidOperationException("Root cause");
|
||||
|
||||
// Act
|
||||
var exception = new McpParseException("Parse failed", innerException);
|
||||
|
||||
// Assert
|
||||
exception.InnerException.Should().Be(innerException);
|
||||
exception.InnerException.Message.Should().Be("Root cause");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void McpException_WithErrorData_ShouldPreserveErrorData()
|
||||
{
|
||||
// Arrange
|
||||
var errorData = new
|
||||
{
|
||||
field = "email",
|
||||
error = "invalid format",
|
||||
value = "not-an-email"
|
||||
};
|
||||
|
||||
// Act
|
||||
var exception = new McpValidationException("Validation failed", errorData);
|
||||
|
||||
// Assert
|
||||
exception.ErrorData.Should().NotBeNull();
|
||||
exception.ErrorData.Should().Be(errorData);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user