diff --git a/src/Endpoints/v1/Gods.cs b/src/Endpoints/v1/Gods.cs index a0352e2..eb9b8cd 100644 --- a/src/Endpoints/v1/Gods.cs +++ b/src/Endpoints/v1/Gods.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using Microsoft.AspNetCore.Mvc; using MythApi.Gods.Interfaces; using MythApi.Common.Database.Models; @@ -16,7 +17,30 @@ public static void RegisterGodEndpoints(this IEndpointRouteBuilder endpoints) { gods.MapPost("", AddOrUpdateGods); } - public static Task> AddOrUpdateGods(List gods, IGodRepository repository) => repository.AddOrUpdateGods(gods); + public static async Task AddOrUpdateGods(List gods, IGodRepository repository) + { + var errors = new Dictionary(); + + for (var i = 0; i < gods.Count; i++) + { + var validationResults = new List(); + 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."]; + } + } + } + + if (errors.Count > 0) + return Results.ValidationProblem(errors); + + var godsResult = await repository.AddOrUpdateGods(gods); + return Results.Ok(godsResult); + } public static Task> GetAlllGods(IGodRepository repository) => repository.GetAllGodsAsync(); } \ No newline at end of file diff --git a/src/Gods/Models/God.cs b/src/Gods/Models/God.cs index c738e62..840901d 100644 --- a/src/Gods/Models/God.cs +++ b/src/Gods/Models/God.cs @@ -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!; + [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; } } diff --git a/tests/IntegrationTests/GodsEndpointTests.cs b/tests/IntegrationTests/GodsEndpointTests.cs index 1762286..c7a58ce 100644 --- a/tests/IntegrationTests/GodsEndpointTests.cs +++ b/tests/IntegrationTests/GodsEndpointTests.cs @@ -1,5 +1,7 @@ +using System.Net; using System.Net.Http.Json; using MythApi.Common.Database.Models; +using MythApi.Gods.Models; namespace IntegrationTests; @@ -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"); } -} \ No newline at end of file + + [Test] + public async Task AddOrUpdateGods_ValidGod_ShouldReturnOk() + { + // Arrange + var godInputs = new List + { + 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 + { + 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)); + } + + [Test] + public async Task AddOrUpdateGods_NameWithNumbers_ShouldReturnBadRequest() + { + // Arrange + var godInputs = new List + { + 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 + { + 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)); + } +} diff --git a/tests/UnitTests/GodEndpointsTests.cs b/tests/UnitTests/GodEndpointsTests.cs index 595ff47..729e7e4 100644 --- a/tests/UnitTests/GodEndpointsTests.cs +++ b/tests/UnitTests/GodEndpointsTests.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Http; using Moq; using MythApi.Common.Database.Models; using MythApi.Endpoints.v1; @@ -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; + 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 + { + new GodInput { Name = "", MythologyId = 1, Description = "Some description" } + }; + + var result = await Gods.AddOrUpdateGods(godInputs, _mockRepository.Object); + + Assert.That(result, Is.InstanceOf()); + // Repository should not be called when validation fails + _mockRepository.Verify(repo => repo.AddOrUpdateGods(It.IsAny>()), Times.Never); + } + + [Test] + public async Task AddOrUpdateGods_NameTooLong_ShouldReturnValidationProblem() + { + var longName = new string('A', 101); + var godInputs = new List + { + new GodInput { Name = longName, MythologyId = 1, Description = "Some description" } + }; + + var result = await Gods.AddOrUpdateGods(godInputs, _mockRepository.Object); + + Assert.That(result, Is.InstanceOf()); + _mockRepository.Verify(repo => repo.AddOrUpdateGods(It.IsAny>()), Times.Never); + } + + [Test] + public async Task AddOrUpdateGods_InvalidCharactersInName_ShouldReturnValidationProblem() + { + var godInputs = new List + { + new GodInput { Name = "Zeus123", MythologyId = 1, Description = "Some description" } + }; + + var result = await Gods.AddOrUpdateGods(godInputs, _mockRepository.Object); + + Assert.That(result, Is.InstanceOf()); + _mockRepository.Verify(repo => repo.AddOrUpdateGods(It.IsAny>()), Times.Never); + } + + [Test] + public async Task AddOrUpdateGods_ValidNameWithHyphenAndApostrophe_ShouldSucceed() + { + var godInputs = new List + { + new GodInput { Name = "O'Brien-Thor", MythologyId = 1, Description = "A valid god name with hyphen and apostrophe" } + }; + var gods = new List + { + new God { Name = "O'Brien-Thor", MythologyId = 1, Description = "A valid god name with hyphen and apostrophe" } + }; + _mockRepository.Setup(repo => repo.AddOrUpdateGods(It.IsAny>())).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>()), Times.Once); + } + + [Test] + public async Task AddOrUpdateGods_InvalidMythologyId_ShouldReturnValidationProblem() + { + var godInputs = new List + { + new GodInput { Name = "Zeus", MythologyId = 0, Description = "God of the sky" } + }; + + var result = await Gods.AddOrUpdateGods(godInputs, _mockRepository.Object); + + Assert.That(result, Is.InstanceOf()); + _mockRepository.Verify(repo => repo.AddOrUpdateGods(It.IsAny>()), Times.Never); + } + + [Test] + public async Task AddOrUpdateGods_EmptyDescription_ShouldReturnValidationProblem() + { + var godInputs = new List + { + new GodInput { Name = "Zeus", MythologyId = 1, Description = "" } + }; + + var result = await Gods.AddOrUpdateGods(godInputs, _mockRepository.Object); + + Assert.That(result, Is.InstanceOf()); + _mockRepository.Verify(repo => repo.AddOrUpdateGods(It.IsAny>()), Times.Never); } } }