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

@@ -56,6 +56,44 @@ public sealed class UsuarioRepository : IUsuarioRepository
return MapRow(row);
}
public async Task<bool> ExistsByUsernameAsync(string username, CancellationToken ct = default)
{
const string sql = """
SELECT COUNT(1) FROM dbo.Usuario WHERE Username = @Username
""";
await using var connection = _connectionFactory.CreateConnection();
await connection.OpenAsync(ct);
var count = await connection.ExecuteScalarAsync<int>(sql, new { Username = username });
return count > 0;
}
public async Task<int> AddAsync(Usuario usuario, CancellationToken ct = default)
{
// DF handles: Activo (1), PermisosJson ('[]'), FechaCreacion (GETDATE())
const string sql = """
INSERT INTO dbo.Usuario (Username, PasswordHash, Nombre, Apellido, Email, Rol)
OUTPUT INSERTED.Id
VALUES (@Username, @PasswordHash, @Nombre, @Apellido, @Email, @Rol)
""";
await using var connection = _connectionFactory.CreateConnection();
await connection.OpenAsync(ct);
var id = await connection.ExecuteScalarAsync<int>(sql, new
{
usuario.Username,
usuario.PasswordHash,
usuario.Nombre,
usuario.Apellido,
usuario.Email,
usuario.Rol
});
return id;
}
private static Usuario MapRow(UsuarioRow row)
=> new(
id: row.Id,