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.
This commit is contained in:
2026-04-15 15:56:49 -03:00
parent 885a8cef17
commit 1a864e9f8b
4 changed files with 72 additions and 3 deletions

View File

@@ -16,13 +16,16 @@ public sealed class PermisosController : ControllerBase
{ {
private readonly IDispatcher _dispatcher; private readonly IDispatcher _dispatcher;
private readonly IValidator<AssignPermisosToRolCommand> _assignValidator; private readonly IValidator<AssignPermisosToRolCommand> _assignValidator;
private readonly IValidator<GetRolPermisosQuery> _getRolPermisosValidator;
public PermisosController( public PermisosController(
IDispatcher dispatcher, IDispatcher dispatcher,
IValidator<AssignPermisosToRolCommand> assignValidator) IValidator<AssignPermisosToRolCommand> assignValidator,
IValidator<GetRolPermisosQuery> getRolPermisosValidator)
{ {
_dispatcher = dispatcher; _dispatcher = dispatcher;
_assignValidator = assignValidator; _assignValidator = assignValidator;
_getRolPermisosValidator = getRolPermisosValidator;
} }
/// <summary>Lists all permisos in the canonical catalog. Requires admin role.</summary> /// <summary>Lists all permisos in the canonical catalog. Requires admin role.</summary>
@@ -39,13 +42,23 @@ public sealed class PermisosController : ControllerBase
/// <summary>Gets all permisos assigned to a rol. Requires admin role.</summary> /// <summary>Gets all permisos assigned to a rol. Requires admin role.</summary>
[HttpGet("roles/{codigo}/permisos")] [HttpGet("roles/{codigo}/permisos")]
[ProducesResponseType(typeof(IReadOnlyList<PermisoDto>), StatusCodes.Status200OK)] [ProducesResponseType(typeof(IReadOnlyList<PermisoDto>), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> GetRolPermisos(string codigo) public async Task<IActionResult> GetRolPermisos(string codigo)
{ {
var result = await _dispatcher.Send<GetRolPermisosQuery, IReadOnlyList<PermisoDto>>( var query = new GetRolPermisosQuery(codigo);
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<GetRolPermisosQuery, IReadOnlyList<PermisoDto>>(query);
return Ok(result); return Ok(result);
} }

View File

@@ -0,0 +1,14 @@
using FluentValidation;
namespace SIGCM2.Application.Permisos.GetByRol;
public sealed class GetRolPermisosQueryValidator : AbstractValidator<GetRolPermisosQuery>
{
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.");
}
}

View File

@@ -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 ────────────────────────────────── // ── PUT /api/v1/roles/{codigo}/permisos ──────────────────────────────────
[Fact] [Fact]

View File

@@ -1,3 +1,4 @@
using FluentValidation;
using NSubstitute; using NSubstitute;
using SIGCM2.Application.Abstractions.Persistence; using SIGCM2.Application.Abstractions.Persistence;
using SIGCM2.Application.Permisos.GetByRol; using SIGCM2.Application.Permisos.GetByRol;
@@ -89,3 +90,33 @@ public class GetRolPermisosQueryHandlerTests
Assert.Equal(18, result.Count); Assert.Equal(18, result.Count);
} }
} }
public class GetRolPermisosQueryValidatorTests
{
private readonly IValidator<GetRolPermisosQuery> _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);
}
}