feat(api): UDT-003 registro de usuarios — backend completo (Phases 1-6)

- Domain: Usuario.ForCreation factory, UsernameAlreadyExistsException, IUsuarioRepository extendido
- Application: CreateUsuarioCommand/Validator/Handler, UsuarioCreatedDto, AuthOptions password policy
- Infrastructure: UsuarioRepository.ExistsByUsernameAsync + AddAsync (INSERT OUTPUT INSERTED.Id), RoleClaimType="rol" en TokenValidationParameters
- Api: UsuariosController POST api/v1/users [Authorize(Roles="admin")], ExceptionFilter mapea UsernameAlreadyExistsException + SqlException 2627 → 409
- Tests (unit): 43 tests — 33 validator + 10 handler (107 total, green)
- Tests (integration): 7 tests CreateUsuarioEndpoint — 401/403/400/201/409/race/e2e (green)
- Fix: TestWebAppFactory.ConfigureTestServices reemplaza SqlConnectionFactory singleton con CS de test correcto
This commit is contained in:
2026-04-15 10:47:48 -03:00
parent 023d30fce4
commit 3d598faffc
19 changed files with 1079 additions and 1 deletions

View File

@@ -0,0 +1,62 @@
using FluentValidation;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using SIGCM2.Application.Abstractions;
using SIGCM2.Application.Usuarios.Create;
namespace SIGCM2.Api.Controllers;
[ApiController]
[Route("api/v1/users")]
[Authorize(Roles = "admin")]
public sealed class UsuariosController : ControllerBase
{
private readonly IDispatcher _dispatcher;
private readonly IValidator<CreateUsuarioCommand> _validator;
public UsuariosController(IDispatcher dispatcher, IValidator<CreateUsuarioCommand> validator)
{
_dispatcher = dispatcher;
_validator = validator;
}
/// <summary>Creates a new user. Requires admin role.</summary>
[HttpPost]
[ProducesResponseType(typeof(UsuarioCreatedDto), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
public async Task<IActionResult> CreateUsuario([FromBody] CreateUsuarioRequest request)
{
var command = new CreateUsuarioCommand(
Username: request.Username ?? string.Empty,
Password: request.Password ?? string.Empty,
Nombre: request.Nombre ?? string.Empty,
Apellido: request.Apellido ?? string.Empty,
Email: request.Email,
Rol: request.Rol ?? string.Empty);
var validation = await _validator.ValidateAsync(command);
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<CreateUsuarioCommand, UsuarioCreatedDto>(command);
return CreatedAtAction(nameof(CreateUsuario), new { id = result.Id }, result);
}
}
/// <summary>Create user request body — nullable to catch missing field scenarios.</summary>
public sealed record CreateUsuarioRequest(
string? Username,
string? Password,
string? Nombre,
string? Apellido,
string? Email,
string? Rol);