From 1a864e9f8bdd43255331344bac1d7a004c45ea7d Mon Sep 17 00:00:00 2001 From: dmolinari Date: Wed, 15 Apr 2026 15:56:49 -0300 Subject: [PATCH] fix(app): validar formato codigo rol en GetRolPermisos [UDT-005] Agrega GetRolPermisosQueryValidator con regex ^[a-z][a-z0-9_]*$ para rechazar codigos invalidos con 400 en GET /api/v1/roles/{codigo}/permisos. --- .../Controllers/PermisosController.cs | 19 ++++++++++-- .../GetByRol/GetRolPermisosQueryValidator.cs | 14 +++++++++ .../Permisos/PermisosEndpointTests.cs | 11 +++++++ .../GetRolPermisosQueryHandlerTests.cs | 31 +++++++++++++++++++ 4 files changed, 72 insertions(+), 3 deletions(-) create mode 100644 src/api/SIGCM2.Application/Permisos/GetByRol/GetRolPermisosQueryValidator.cs diff --git a/src/api/SIGCM2.Api/Controllers/PermisosController.cs b/src/api/SIGCM2.Api/Controllers/PermisosController.cs index 77ed8de..13bc6c4 100644 --- a/src/api/SIGCM2.Api/Controllers/PermisosController.cs +++ b/src/api/SIGCM2.Api/Controllers/PermisosController.cs @@ -16,13 +16,16 @@ public sealed class PermisosController : ControllerBase { private readonly IDispatcher _dispatcher; private readonly IValidator _assignValidator; + private readonly IValidator _getRolPermisosValidator; public PermisosController( IDispatcher dispatcher, - IValidator assignValidator) + IValidator assignValidator, + IValidator getRolPermisosValidator) { _dispatcher = dispatcher; _assignValidator = assignValidator; + _getRolPermisosValidator = getRolPermisosValidator; } /// Lists all permisos in the canonical catalog. Requires admin role. @@ -39,13 +42,23 @@ public sealed class PermisosController : ControllerBase /// Gets all permisos assigned to a rol. Requires admin role. [HttpGet("roles/{codigo}/permisos")] [ProducesResponseType(typeof(IReadOnlyList), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task GetRolPermisos(string codigo) { - var result = await _dispatcher.Send>( - new GetRolPermisosQuery(codigo)); + var query = new GetRolPermisosQuery(codigo); + var validation = await _getRolPermisosValidator.ValidateAsync(query); + if (!validation.IsValid) + { + var errors = validation.Errors + .GroupBy(e => e.PropertyName) + .ToDictionary(g => g.Key, g => g.Select(e => e.ErrorMessage).ToArray()); + return BadRequest(new { errors }); + } + + var result = await _dispatcher.Send>(query); return Ok(result); } diff --git a/src/api/SIGCM2.Application/Permisos/GetByRol/GetRolPermisosQueryValidator.cs b/src/api/SIGCM2.Application/Permisos/GetByRol/GetRolPermisosQueryValidator.cs new file mode 100644 index 0000000..0768656 --- /dev/null +++ b/src/api/SIGCM2.Application/Permisos/GetByRol/GetRolPermisosQueryValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; + +namespace SIGCM2.Application.Permisos.GetByRol; + +public sealed class GetRolPermisosQueryValidator : AbstractValidator +{ + public GetRolPermisosQueryValidator() + { + RuleFor(x => x.RolCodigo) + .NotEmpty().WithMessage("El código del rol es requerido.") + .Matches(@"^[a-z][a-z0-9_]*$") + .WithMessage("El código del rol debe empezar con una letra minúscula y contener solo minúsculas, dígitos o guion bajo."); + } +} diff --git a/tests/SIGCM2.Api.Tests/Permisos/PermisosEndpointTests.cs b/tests/SIGCM2.Api.Tests/Permisos/PermisosEndpointTests.cs index 098a6f5..ac2fd0c 100644 --- a/tests/SIGCM2.Api.Tests/Permisos/PermisosEndpointTests.cs +++ b/tests/SIGCM2.Api.Tests/Permisos/PermisosEndpointTests.cs @@ -250,6 +250,17 @@ public sealed class PermisosEndpointTests : IAsyncLifetime } } + [Fact] + public async Task GetRolPermisos_InvalidCodigoFormat_Returns400() + { + var token = await GetBearerTokenAsync(AdminUsername, AdminPassword); + // "ROL-INVALIDO" no matchea ^[a-z][a-z0-9_]*$ (tiene guion y mayúsculas) + using var req = BuildRequest(HttpMethod.Get, "/api/v1/roles/ROL-INVALIDO/permisos", bearerToken: token); + var resp = await _client.SendAsync(req); + + Assert.Equal(HttpStatusCode.BadRequest, resp.StatusCode); + } + // ── PUT /api/v1/roles/{codigo}/permisos ────────────────────────────────── [Fact] diff --git a/tests/SIGCM2.Application.Tests/Permisos/GetByRol/GetRolPermisosQueryHandlerTests.cs b/tests/SIGCM2.Application.Tests/Permisos/GetByRol/GetRolPermisosQueryHandlerTests.cs index 2021183..0b459c9 100644 --- a/tests/SIGCM2.Application.Tests/Permisos/GetByRol/GetRolPermisosQueryHandlerTests.cs +++ b/tests/SIGCM2.Application.Tests/Permisos/GetByRol/GetRolPermisosQueryHandlerTests.cs @@ -1,3 +1,4 @@ +using FluentValidation; using NSubstitute; using SIGCM2.Application.Abstractions.Persistence; using SIGCM2.Application.Permisos.GetByRol; @@ -89,3 +90,33 @@ public class GetRolPermisosQueryHandlerTests Assert.Equal(18, result.Count); } } + +public class GetRolPermisosQueryValidatorTests +{ + private readonly IValidator _validator = + new GetRolPermisosQueryValidator(); + + [Theory] + [InlineData("ROL-INVALIDO")] + [InlineData("ROL:INVALIDO")] + [InlineData("123abc")] + [InlineData("UPPER")] + [InlineData("con espacio")] + [InlineData("")] + public async Task Validate_InvalidCodigoFormat_ReturnsInvalid(string codigo) + { + var result = await _validator.ValidateAsync(new GetRolPermisosQuery(codigo)); + Assert.False(result.IsValid); + } + + [Theory] + [InlineData("admin")] + [InlineData("cajero")] + [InlineData("rol_valido")] + [InlineData("abc123")] + public async Task Validate_ValidCodigoFormat_ReturnsValid(string codigo) + { + var result = await _validator.ValidateAsync(new GetRolPermisosQuery(codigo)); + Assert.True(result.IsValid); + } +}