Skip to content
This repository was archived by the owner on Feb 26, 2026. It is now read-only.
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions src/Common/Middleware/ExceptionHandlingMiddleware.cs
Original file line number Diff line number Diff line change
@@ -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<ExceptionHandlingMiddleware> _logger;
private readonly IHostEnvironment _environment;

public ExceptionHandlingMiddleware(
RequestDelegate next,
ILogger<ExceptionHandlingMiddleware> 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));
}
}
13 changes: 12 additions & 1 deletion src/Endpoints/v1/Gods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,23 @@ 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);
}

public static Task<List<God>> AddOrUpdateGods(List<GodInput> gods, IGodRepository repository) => repository.AddOrUpdateGods(gods);

public static Task<IList<God>> GetAlllGods(IGodRepository repository) => repository.GetAllGodsAsync();

public static async Task<IResult> 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);
}
}
6 changes: 4 additions & 2 deletions src/Gods/DBRepositories/GodRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,11 @@ public async Task<IList<God>> GetAllGodsAsync()
return gods;
}

public async Task<God> GetGodAsync(GodParameter parameter)
public async Task<God?> 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<List<God>> GetGodByNameAsync(GodByNameParameter parameter)
Expand Down
2 changes: 1 addition & 1 deletion src/Gods/Interfaces/IGodRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace MythApi.Gods.Interfaces;
public interface IGodRepository{
public Task<IList<God>> GetAllGodsAsync();

public Task<God> GetGodAsync(GodParameter parameter);
public Task<God?> GetGodAsync(GodParameter parameter);

public Task<List<God>> GetGodByNameAsync(GodByNameParameter parameter);

Expand Down
6 changes: 4 additions & 2 deletions src/Gods/Mocks/GodRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,11 @@ public Task<IList<God>> GetAllGodsAsync()
return Task.FromResult(gods as IList<God>);
}

public Task<God> GetGodAsync(GodParameter parameter)
public Task<God?> GetGodAsync(GodParameter parameter)
{
return Task.FromResult(gods[parameter.Id]);
if (parameter.Id >= 0 && parameter.Id < gods.Count)
return Task.FromResult<God?>(gods[parameter.Id]);
return Task.FromResult<God?>(null);
}

public Task<List<God>> GetGodByNameAsync(GodByNameParameter parameter)
Expand Down
3 changes: 3 additions & 0 deletions src/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -110,6 +111,8 @@
initializer.InitializeDatabase();
}

app.UseMiddleware<ExceptionHandlingMiddleware>();

app.RegisterGodEndpoints();
app.RegisterMythologiesEndpoints();
app.UseSwagger();
Expand Down
55 changes: 55 additions & 0 deletions tests/IntegrationTests/GodsEndpointTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<List<God>>("/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<God>();
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()
{
Expand Down
38 changes: 38 additions & 0 deletions tests/UnitTests/GodEndpointsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;

namespace UnitTests
{
Expand Down Expand Up @@ -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<GodParameter>())).ReturnsAsync(god);

var result = await Gods.GetGodById(1, _mockRepository.Object);

Assert.That(result, Is.InstanceOf<IResult>());
}

[Test]
public async Task GetGodById_NonExistentId_ShouldReturnNotFound()
{
_mockRepository.Setup(repo => repo.GetGodAsync(It.IsAny<GodParameter>())).ReturnsAsync((God?)null);

var result = await Gods.GetGodById(99999, _mockRepository.Object);

Assert.That(result, Is.InstanceOf<IResult>());
}

[Test]
public async Task GetGodById_NegativeId_ShouldReturnBadRequest()
{
var result = await Gods.GetGodById(-1, _mockRepository.Object);

Assert.That(result, Is.InstanceOf<IResult>());
}

[Test]
public async Task GetGodById_ZeroId_ShouldReturnBadRequest()
{
var result = await Gods.GetGodById(0, _mockRepository.Object);

Assert.That(result, Is.InstanceOf<IResult>());
}

[Test]
public async Task AddOrUpdateGods_ShouldAddNewGod()
{
Expand Down