feat(api): List + GetById usuarios — handlers, repo, endpoints [UDT-008]

This commit is contained in:
2026-04-15 17:46:23 -03:00
parent 9dcd63543e
commit 2925336783
29 changed files with 1210 additions and 6 deletions

View File

@@ -0,0 +1,61 @@
using NSubstitute;
using SIGCM2.Application.Abstractions.Persistence;
using SIGCM2.Application.Usuarios.GetById;
using SIGCM2.Domain.Entities;
using SIGCM2.Domain.Exceptions;
namespace SIGCM2.Application.Tests.Usuarios;
public class GetUsuarioByIdQueryHandlerTests
{
private readonly IUsuarioRepository _repo = Substitute.For<IUsuarioRepository>();
private readonly GetUsuarioByIdQueryHandler _handler;
public GetUsuarioByIdQueryHandlerTests()
{
_handler = new GetUsuarioByIdQueryHandler(_repo);
}
[Fact]
public async Task Handle_Returns_UsuarioDetailDto_When_Found()
{
var usuario = new Usuario(5, "jperez", "$2a$12$hash", "Juan", "Pérez", "j@x.com", "cajero", "[]", true,
fechaModificacion: null, ultimoLogin: null, mustChangePassword: false);
_repo.GetDetailAsync(5, Arg.Any<CancellationToken>()).Returns(usuario);
var result = await _handler.Handle(new GetUsuarioByIdQuery(5));
Assert.Equal(5, result.Id);
Assert.Equal("jperez", result.Username);
Assert.Equal("Juan", result.Nombre);
Assert.Equal("Pérez", result.Apellido);
Assert.Equal("j@x.com", result.Email);
Assert.Equal("cajero", result.Rol);
Assert.True(result.Activo);
Assert.False(result.MustChangePassword);
}
[Fact]
public async Task Handle_DoesNotReturn_PasswordHash_In_Dto()
{
var usuario = new Usuario(5, "jperez", "$2a$12$SECRETHASH", "Juan", "Pérez", null, "cajero", "[]", true);
_repo.GetDetailAsync(5, Arg.Any<CancellationToken>()).Returns(usuario);
var result = await _handler.Handle(new GetUsuarioByIdQuery(5));
// UsuarioDetailDto must not expose PasswordHash
var props = typeof(UsuarioDetailDto).GetProperties().Select(p => p.Name);
Assert.DoesNotContain("PasswordHash", props);
Assert.DoesNotContain("PermisosJson", props);
}
[Fact]
public async Task Handle_Throws_UsuarioNotFoundException_When_Not_Found()
{
_repo.GetDetailAsync(9999, Arg.Any<CancellationToken>()).Returns((Usuario?)null);
await Assert.ThrowsAsync<UsuarioNotFoundException>(
() => _handler.Handle(new GetUsuarioByIdQuery(9999)));
}
}

View File

@@ -0,0 +1,119 @@
using NSubstitute;
using SIGCM2.Application.Abstractions.Persistence;
using SIGCM2.Application.Common;
using SIGCM2.Application.Usuarios.List;
using SIGCM2.Domain.Exceptions;
namespace SIGCM2.Application.Tests.Usuarios;
public class ListUsuariosQueryHandlerTests
{
private readonly IUsuarioRepository _repo = Substitute.For<IUsuarioRepository>();
private readonly ListUsuariosQueryHandler _handler;
public ListUsuariosQueryHandlerTests()
{
_handler = new ListUsuariosQueryHandler(_repo);
}
[Fact]
public async Task Handle_Returns_PagedResult_With_Items()
{
var items = new List<UsuarioListItem>
{
new(1, "admin", "Admin", "Sys", null, "admin", true, null, null)
};
var paged = new PagedResult<UsuarioListItem>(items, 1, 20, 1);
_repo.GetPagedAsync(Arg.Any<UsuariosQuery>(), Arg.Any<CancellationToken>())
.Returns(paged);
var query = new ListUsuariosQuery(1, 20, null, null, null);
var result = await _handler.Handle(query);
Assert.Equal(1, result.Total);
Assert.Single(result.Items);
}
[Fact]
public async Task Handle_Clamps_PageSize_Above_100_To_100()
{
_repo.GetPagedAsync(Arg.Any<UsuariosQuery>(), Arg.Any<CancellationToken>())
.Returns(new PagedResult<UsuarioListItem>([], 1, 100, 0));
var query = new ListUsuariosQuery(1, 200, null, null, null);
await _handler.Handle(query);
await _repo.Received(1).GetPagedAsync(
Arg.Is<UsuariosQuery>(q => q.PageSize == 100),
Arg.Any<CancellationToken>());
}
[Fact]
public async Task Handle_Clamps_Page_Below_1_To_1()
{
_repo.GetPagedAsync(Arg.Any<UsuariosQuery>(), Arg.Any<CancellationToken>())
.Returns(new PagedResult<UsuarioListItem>([], 1, 20, 0));
var query = new ListUsuariosQuery(0, 20, null, null, null);
await _handler.Handle(query);
await _repo.Received(1).GetPagedAsync(
Arg.Is<UsuariosQuery>(q => q.Page == 1),
Arg.Any<CancellationToken>());
}
[Fact]
public async Task Handle_Passes_Rol_Filter()
{
_repo.GetPagedAsync(Arg.Any<UsuariosQuery>(), Arg.Any<CancellationToken>())
.Returns(new PagedResult<UsuarioListItem>([], 1, 20, 0));
var query = new ListUsuariosQuery(1, 20, "admin", null, null);
await _handler.Handle(query);
await _repo.Received(1).GetPagedAsync(
Arg.Is<UsuariosQuery>(q => q.Rol == "admin"),
Arg.Any<CancellationToken>());
}
[Fact]
public async Task Handle_Passes_Activo_Filter()
{
_repo.GetPagedAsync(Arg.Any<UsuariosQuery>(), Arg.Any<CancellationToken>())
.Returns(new PagedResult<UsuarioListItem>([], 1, 20, 0));
var query = new ListUsuariosQuery(1, 20, null, false, null);
await _handler.Handle(query);
await _repo.Received(1).GetPagedAsync(
Arg.Is<UsuariosQuery>(q => q.Activo == false),
Arg.Any<CancellationToken>());
}
[Fact]
public async Task Handle_Passes_Search_Filter()
{
_repo.GetPagedAsync(Arg.Any<UsuariosQuery>(), Arg.Any<CancellationToken>())
.Returns(new PagedResult<UsuarioListItem>([], 1, 20, 0));
var query = new ListUsuariosQuery(1, 20, null, null, "juan");
await _handler.Handle(query);
await _repo.Received(1).GetPagedAsync(
Arg.Is<UsuariosQuery>(q => q.Search == "juan"),
Arg.Any<CancellationToken>());
}
[Fact]
public async Task Handle_Returns_Empty_When_No_Items()
{
_repo.GetPagedAsync(Arg.Any<UsuariosQuery>(), Arg.Any<CancellationToken>())
.Returns(new PagedResult<UsuarioListItem>([], 1, 20, 0));
var result = await _handler.Handle(new ListUsuariosQuery(1, 20, null, null, null));
Assert.Equal(0, result.Total);
Assert.Empty(result.Items);
}
}