diff --git a/src/Common/Middleware/ExceptionHandlingMiddleware.cs b/src/Common/Middleware/ExceptionHandlingMiddleware.cs new file mode 100644 index 0000000..62aa585 --- /dev/null +++ b/src/Common/Middleware/ExceptionHandlingMiddleware.cs @@ -0,0 +1,76 @@ +using System.Net; +using System.Text.Json; + +namespace MythApi.Common.Middleware; + +public class ExceptionHandlingMiddleware +{ + private readonly RequestDelegate _next; + private readonly ILogger _logger; + private readonly IHostEnvironment _environment; + + public ExceptionHandlingMiddleware( + RequestDelegate next, + ILogger logger, + IHostEnvironment environment) + { + _next = next; + _logger = logger; + _environment = environment; + } + + public async Task InvokeAsync(HttpContext context) + { + try + { + await _next(context); + } + catch (Exception ex) + { + _logger.LogError(ex, "Unhandled exception occurred: {Message}", ex.Message); + await HandleExceptionAsync(context, ex); + } + } + + private async Task HandleExceptionAsync(HttpContext context, Exception exception) + { + context.Response.ContentType = "application/json"; + + object response; + + switch (exception) + { + case ArgumentException: + case InvalidOperationException: + context.Response.StatusCode = (int)HttpStatusCode.BadRequest; + break; + case UnauthorizedAccessException: + context.Response.StatusCode = (int)HttpStatusCode.Unauthorized; + break; + default: + context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; + break; + } + + // Only include stack trace in development + if (_environment.IsDevelopment()) + { + response = new + { + error = exception.Message, + stackTrace = exception.StackTrace, + traceId = context.TraceIdentifier + }; + } + else + { + response = new + { + error = "An error occurred processing your request.", + traceId = context.TraceIdentifier + }; + } + + await context.Response.WriteAsync(JsonSerializer.Serialize(response)); + } +} diff --git a/src/Endpoints/v1/Gods.cs b/src/Endpoints/v1/Gods.cs index a0352e2..b349293 100644 --- a/src/Endpoints/v1/Gods.cs +++ b/src/Endpoints/v1/Gods.cs @@ -11,7 +11,7 @@ public static void RegisterGodEndpoints(this IEndpointRouteBuilder endpoints) { gods.MapGet("", GetAlllGods); - gods.MapGet("{id}", (int id, IGodRepository repository) => repository.GetGodAsync(new GodParameter(id))); + gods.MapGet("{id}", GetGodById); gods.MapGet("search/{name}", (string name, IGodRepository repository, [FromQuery] bool includeAliases = false) => repository.GetGodByNameAsync(new GodByNameParameter(name, includeAliases))); gods.MapPost("", AddOrUpdateGods); } @@ -19,4 +19,15 @@ public static void RegisterGodEndpoints(this IEndpointRouteBuilder endpoints) { public static Task> AddOrUpdateGods(List gods, IGodRepository repository) => repository.AddOrUpdateGods(gods); public static Task> GetAlllGods(IGodRepository repository) => repository.GetAllGodsAsync(); + + public static async Task GetGodById(int id, IGodRepository repository) + { + if (id <= 0) + return Results.BadRequest(new { error = "ID must be a positive integer" }); + + var god = await repository.GetGodAsync(new GodParameter(id)); + return god is null + ? Results.NotFound(new { error = $"God with ID {id} not found" }) + : Results.Ok(god); + } } \ No newline at end of file diff --git a/src/Gods/DBRepositories/GodRepository.cs b/src/Gods/DBRepositories/GodRepository.cs index d2bef4e..eb342c1 100644 --- a/src/Gods/DBRepositories/GodRepository.cs +++ b/src/Gods/DBRepositories/GodRepository.cs @@ -52,9 +52,11 @@ public async Task> GetAllGodsAsync() return gods; } - public async Task GetGodAsync(GodParameter parameter) + public async Task GetGodAsync(GodParameter parameter) { - return await _context.Gods.FirstAsync(x => x.Id == parameter.Id); + return await _context.Gods + .Include(g => g.Aliases) + .FirstOrDefaultAsync(x => x.Id == parameter.Id); } public Task> GetGodByNameAsync(GodByNameParameter parameter) diff --git a/src/Gods/Interfaces/IGodRepository.cs b/src/Gods/Interfaces/IGodRepository.cs index 4e25ac7..31d00cf 100644 --- a/src/Gods/Interfaces/IGodRepository.cs +++ b/src/Gods/Interfaces/IGodRepository.cs @@ -6,7 +6,7 @@ namespace MythApi.Gods.Interfaces; public interface IGodRepository{ public Task> GetAllGodsAsync(); - public Task GetGodAsync(GodParameter parameter); + public Task GetGodAsync(GodParameter parameter); public Task> GetGodByNameAsync(GodByNameParameter parameter); diff --git a/src/Gods/Mocks/GodRepository.cs b/src/Gods/Mocks/GodRepository.cs index 1ebaf63..0ef6469 100644 --- a/src/Gods/Mocks/GodRepository.cs +++ b/src/Gods/Mocks/GodRepository.cs @@ -40,9 +40,11 @@ public Task> GetAllGodsAsync() return Task.FromResult(gods as IList); } - public Task GetGodAsync(GodParameter parameter) + public Task GetGodAsync(GodParameter parameter) { - return Task.FromResult(gods[parameter.Id]); + if (parameter.Id >= 0 && parameter.Id < gods.Count) + return Task.FromResult(gods[parameter.Id]); + return Task.FromResult(null); } public Task> GetGodByNameAsync(GodByNameParameter parameter) diff --git a/src/Program.cs b/src/Program.cs index 57d4f69..4cbe470 100644 --- a/src/Program.cs +++ b/src/Program.cs @@ -5,6 +5,7 @@ using MythApi.Endpoints.v1; using MythApi.Mythologies.DBRepositories; using MythApi.Mythologies.Interfaces; +using MythApi.Common.Middleware; using Azure.Identity; using Serilog; using System.Runtime.CompilerServices; @@ -110,6 +111,8 @@ initializer.InitializeDatabase(); } + app.UseMiddleware(); + app.RegisterGodEndpoints(); app.RegisterMythologiesEndpoints(); app.UseSwagger(); diff --git a/tests/IntegrationTests/GodsEndpointTests.cs b/tests/IntegrationTests/GodsEndpointTests.cs index 1762286..97d116e 100644 --- a/tests/IntegrationTests/GodsEndpointTests.cs +++ b/tests/IntegrationTests/GodsEndpointTests.cs @@ -45,6 +45,61 @@ public async Task GetAllGods_ShouldReturnGodsList() // The test database should be initialized with some gods by DatabaseInitializer } + [Test] + public async Task GetGodById_ValidId_ShouldReturnGod() + { + // Arrange - Get a list of gods first to find a valid ID + var gods = await _httpClient.GetFromJsonAsync>("/api/v1/gods"); + Assert.That(gods, Is.Not.Null); + Assert.That(gods!.Count, Is.GreaterThan(0)); + var validId = gods[0].Id; + + // Act + var response = await _httpClient.GetAsync($"/api/v1/gods/{validId}"); + + // Assert + Assert.That(response.IsSuccessStatusCode, Is.True); + var god = await response.Content.ReadFromJsonAsync(); + Assert.That(god, Is.Not.Null); + Assert.That(god!.Id, Is.EqualTo(validId)); + } + + [Test] + public async Task GetGodById_NonExistentId_ShouldReturn404() + { + // Act + var response = await _httpClient.GetAsync("/api/v1/gods/99999"); + + // Assert + Assert.That(response.StatusCode, Is.EqualTo(System.Net.HttpStatusCode.NotFound)); + var content = await response.Content.ReadAsStringAsync(); + Assert.That(content, Does.Contain("not found")); + } + + [Test] + public async Task GetGodById_NegativeId_ShouldReturn400() + { + // Act + var response = await _httpClient.GetAsync("/api/v1/gods/-1"); + + // Assert + Assert.That(response.StatusCode, Is.EqualTo(System.Net.HttpStatusCode.BadRequest)); + var content = await response.Content.ReadAsStringAsync(); + Assert.That(content, Does.Contain("positive integer")); + } + + [Test] + public async Task GetGodById_ZeroId_ShouldReturn400() + { + // Act + var response = await _httpClient.GetAsync("/api/v1/gods/0"); + + // Assert + Assert.That(response.StatusCode, Is.EqualTo(System.Net.HttpStatusCode.BadRequest)); + var content = await response.Content.ReadAsStringAsync(); + Assert.That(content, Does.Contain("positive integer")); + } + [Test] public async Task GetAllGods_ConcurrentRequests_ShouldRespectRateLim() { diff --git a/tests/UnitTests/GodEndpointsTests.cs b/tests/UnitTests/GodEndpointsTests.cs index 595ff47..3551aab 100644 --- a/tests/UnitTests/GodEndpointsTests.cs +++ b/tests/UnitTests/GodEndpointsTests.cs @@ -8,6 +8,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; namespace UnitTests { @@ -36,6 +37,43 @@ public async Task GetAllGods_ShouldReturnAllGods() Assert.That(result.Count, Is.EqualTo(2)); } + [Test] + public async Task GetGodById_ValidId_ShouldReturnGod() + { + var god = new God { Id = 1, Name = "Zeus", MythologyId = 1, Description = "God of the sky" }; + _mockRepository.Setup(repo => repo.GetGodAsync(It.IsAny())).ReturnsAsync(god); + + var result = await Gods.GetGodById(1, _mockRepository.Object); + + Assert.That(result, Is.InstanceOf()); + } + + [Test] + public async Task GetGodById_NonExistentId_ShouldReturnNotFound() + { + _mockRepository.Setup(repo => repo.GetGodAsync(It.IsAny())).ReturnsAsync((God?)null); + + var result = await Gods.GetGodById(99999, _mockRepository.Object); + + Assert.That(result, Is.InstanceOf()); + } + + [Test] + public async Task GetGodById_NegativeId_ShouldReturnBadRequest() + { + var result = await Gods.GetGodById(-1, _mockRepository.Object); + + Assert.That(result, Is.InstanceOf()); + } + + [Test] + public async Task GetGodById_ZeroId_ShouldReturnBadRequest() + { + var result = await Gods.GetGodById(0, _mockRepository.Object); + + Assert.That(result, Is.InstanceOf()); + } + [Test] public async Task AddOrUpdateGods_ShouldAddNewGod() {