Skip to content
Open
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
26 changes: 25 additions & 1 deletion src/Endpoints/v1/Gods.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc;
using MythApi.Gods.Interfaces;
using MythApi.Common.Database.Models;
Expand All @@ -16,7 +17,30 @@ public static void RegisterGodEndpoints(this IEndpointRouteBuilder endpoints) {
gods.MapPost("", AddOrUpdateGods);
}

public static Task<List<God>> AddOrUpdateGods(List<GodInput> gods, IGodRepository repository) => repository.AddOrUpdateGods(gods);
public static async Task<IResult> AddOrUpdateGods(List<GodInput> gods, IGodRepository repository)
{
var errors = new Dictionary<string, string[]>();

for (var i = 0; i < gods.Count; i++)
{
Comment on lines +23 to +25
var validationResults = new List<ValidationResult>();
var context = new ValidationContext(gods[i]);
if (!Validator.TryValidateObject(gods[i], context, validationResults, validateAllProperties: true))
{
foreach (var result in validationResults)
{
var key = $"[{i}].{result.MemberNames.FirstOrDefault() ?? "Unknown"}";
errors[key] = [result.ErrorMessage ?? "Invalid value."];
}
Comment on lines +30 to +34
}
}

if (errors.Count > 0)
return Results.ValidationProblem(errors);

var godsResult = await repository.AddOrUpdateGods(gods);
return Results.Ok(godsResult);
}

public static Task<IList<God>> GetAlllGods(IGodRepository repository) => repository.GetAllGodsAsync();
}
10 changes: 10 additions & 0 deletions src/Gods/Models/God.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
using System.ComponentModel.DataAnnotations;

namespace MythApi.Gods.Models;

public class GodInput {
public int? Id { get; set; }

[Required(ErrorMessage = "Name is required.")]
[MinLength(1, ErrorMessage = "Name must not be empty.")]
[MaxLength(100, ErrorMessage = "Name must not exceed 100 characters.")]
[RegularExpression(@"^[\p{L}\s'\-\.]+$", ErrorMessage = "Name may only contain letters, spaces, hyphens, apostrophes, and dots.")]
public string Name { get; set; } = null!;
Comment on lines +8 to 12

[Required(ErrorMessage = "Description is required.")]
[MinLength(1, ErrorMessage = "Description must not be empty.")]
[MaxLength(500, ErrorMessage = "Description must not exceed 500 characters.")]
public string Description { get; set; } = null!;

[Range(1, int.MaxValue, ErrorMessage = "MythologyId must be a positive integer.")]
public int MythologyId { get; set; }
}

68 changes: 67 additions & 1 deletion tests/IntegrationTests/GodsEndpointTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System.Net;
using System.Net.Http.Json;
using MythApi.Common.Database.Models;
using MythApi.Gods.Models;

namespace IntegrationTests;

Expand Down Expand Up @@ -71,4 +73,68 @@ public async Task GetAllGods_ConcurrentRequests_ShouldRespectRateLim()
// Assert.That(rateLimitedRequests, Is.GreaterThan(0), "Some requests should be rate limited");
Assert.That(successfulRequests + rateLimitedRequests, Is.EqualTo(numberOfRequests), "All requests should be either successful or rate limited");
}
}

[Test]
public async Task AddOrUpdateGods_ValidGod_ShouldReturnOk()
{
// Arrange
var godInputs = new List<GodInput>
{
new GodInput { Name = "Hermes", MythologyId = 2, Description = "God of travel and commerce." }
};

// Act
var response = await _httpClient.PostAsJsonAsync("/api/v1/gods", godInputs);

// Assert
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
}

[Test]
public async Task AddOrUpdateGods_EmptyName_ShouldReturnBadRequest()
{
// Arrange
var godInputs = new List<GodInput>
{
new GodInput { Name = "", MythologyId = 1, Description = "Some description." }
};

// Act
var response = await _httpClient.PostAsJsonAsync("/api/v1/gods", godInputs);

// Assert
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest));
}
Comment on lines +93 to +107

[Test]
public async Task AddOrUpdateGods_NameWithNumbers_ShouldReturnBadRequest()
{
// Arrange
var godInputs = new List<GodInput>
{
new GodInput { Name = "Zeus123", MythologyId = 1, Description = "Invalid name." }
};

// Act
var response = await _httpClient.PostAsJsonAsync("/api/v1/gods", godInputs);

// Assert
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest));
}

[Test]
public async Task AddOrUpdateGods_NameTooLong_ShouldReturnBadRequest()
{
// Arrange
var godInputs = new List<GodInput>
{
new GodInput { Name = new string('A', 101), MythologyId = 1, Description = "Too long name." }
};

// Act
var response = await _httpClient.PostAsJsonAsync("/api/v1/gods", godInputs);

// Assert
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest));
}
}
101 changes: 99 additions & 2 deletions tests/UnitTests/GodEndpointsTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Http;
using Moq;
using MythApi.Common.Database.Models;
using MythApi.Endpoints.v1;
Expand Down Expand Up @@ -51,8 +52,104 @@ public async Task AddOrUpdateGods_ShouldAddNewGod()

var result = await Gods.AddOrUpdateGods(godInputs, _mockRepository.Object);

Assert.That(result.Count, Is.EqualTo(1));
Assert.That(result.First().Name, Is.EqualTo("Zeus"));
var okResult = result as IValueHttpResult;
Assert.That(okResult, Is.Not.Null);
var godList = okResult!.Value as List<God>;
Assert.That(godList, Is.Not.Null);
Assert.That(godList!.Count, Is.EqualTo(1));
Assert.That(godList.First().Name, Is.EqualTo("Zeus"));
}

[Test]
public async Task AddOrUpdateGods_EmptyName_ShouldReturnValidationProblem()
{
var godInputs = new List<GodInput>
{
new GodInput { Name = "", MythologyId = 1, Description = "Some description" }
};

var result = await Gods.AddOrUpdateGods(godInputs, _mockRepository.Object);

Assert.That(result, Is.InstanceOf<IResult>());
// Repository should not be called when validation fails
_mockRepository.Verify(repo => repo.AddOrUpdateGods(It.IsAny<List<GodInput>>()), Times.Never);
Comment on lines +71 to +75
}

[Test]
public async Task AddOrUpdateGods_NameTooLong_ShouldReturnValidationProblem()
{
var longName = new string('A', 101);
var godInputs = new List<GodInput>
{
new GodInput { Name = longName, MythologyId = 1, Description = "Some description" }
};

var result = await Gods.AddOrUpdateGods(godInputs, _mockRepository.Object);

Assert.That(result, Is.InstanceOf<IResult>());
_mockRepository.Verify(repo => repo.AddOrUpdateGods(It.IsAny<List<GodInput>>()), Times.Never);
}

[Test]
public async Task AddOrUpdateGods_InvalidCharactersInName_ShouldReturnValidationProblem()
{
var godInputs = new List<GodInput>
{
new GodInput { Name = "Zeus123", MythologyId = 1, Description = "Some description" }
};

var result = await Gods.AddOrUpdateGods(godInputs, _mockRepository.Object);

Assert.That(result, Is.InstanceOf<IResult>());
_mockRepository.Verify(repo => repo.AddOrUpdateGods(It.IsAny<List<GodInput>>()), Times.Never);
}

[Test]
public async Task AddOrUpdateGods_ValidNameWithHyphenAndApostrophe_ShouldSucceed()
{
var godInputs = new List<GodInput>
{
new GodInput { Name = "O'Brien-Thor", MythologyId = 1, Description = "A valid god name with hyphen and apostrophe" }
};
var gods = new List<God>
{
new God { Name = "O'Brien-Thor", MythologyId = 1, Description = "A valid god name with hyphen and apostrophe" }
};
_mockRepository.Setup(repo => repo.AddOrUpdateGods(It.IsAny<List<GodInput>>())).ReturnsAsync(gods);

var result = await Gods.AddOrUpdateGods(godInputs, _mockRepository.Object);

var okResult = result as IValueHttpResult;
Assert.That(okResult, Is.Not.Null);
_mockRepository.Verify(repo => repo.AddOrUpdateGods(It.IsAny<List<GodInput>>()), Times.Once);
}

[Test]
public async Task AddOrUpdateGods_InvalidMythologyId_ShouldReturnValidationProblem()
{
var godInputs = new List<GodInput>
{
new GodInput { Name = "Zeus", MythologyId = 0, Description = "God of the sky" }
};

var result = await Gods.AddOrUpdateGods(godInputs, _mockRepository.Object);

Assert.That(result, Is.InstanceOf<IResult>());
_mockRepository.Verify(repo => repo.AddOrUpdateGods(It.IsAny<List<GodInput>>()), Times.Never);
}

[Test]
public async Task AddOrUpdateGods_EmptyDescription_ShouldReturnValidationProblem()
{
var godInputs = new List<GodInput>
{
new GodInput { Name = "Zeus", MythologyId = 1, Description = "" }
};

var result = await Gods.AddOrUpdateGods(godInputs, _mockRepository.Object);

Assert.That(result, Is.InstanceOf<IResult>());
_mockRepository.Verify(repo => repo.AddOrUpdateGods(It.IsAny<List<GodInput>>()), Times.Never);
}
}
}