- RolesController /api/v1/roles CRUD admin-only: GET list, GET {codigo}, POST, PUT, DELETE (soft-delete con guard 409)
- ExceptionFilter: mapea RolNotFound (404), RolAlreadyExists (409), RolInUse (409)
- DI: registra 5 handlers de Roles (Application) y IRolRepository/RolRepository (Infrastructure)
- CreateUsuarioCommandValidator: reemplaza whitelist hardcoded por IRolRepository.ExistsActiveByCodigoAsync via MustAsync; constructor recibe (AuthOptions, IRolRepository)
- Tests: 202 verdes (173 application + 29 api). Nuevas: RolesEndpointTests (13 integration), CreateUsuarioCommandValidatorTests reescrito con NSubstitute mock, CreateUsuario_WithInactiveRol_Returns400 en Api.Tests
- Fix: ApiIntegration pasa de IClassFixture (N factories) a ICollectionFixture (1 factory shared) — evitaba ObjectDisposedException sobre RSABCrypt al compartir coleccion con multiples test classes
- tests/tests.runsettings: MaxCpuCount=1 para evitar race entre assemblies sobre SIGCM2_Test
239 lines
9.1 KiB
C#
239 lines
9.1 KiB
C#
using FluentValidation.TestHelper;
|
|
using NSubstitute;
|
|
using SIGCM2.Application.Abstractions.Persistence;
|
|
using SIGCM2.Application.Auth;
|
|
using SIGCM2.Application.Usuarios.Create;
|
|
|
|
namespace SIGCM2.Application.Tests.Usuarios.Create;
|
|
|
|
public class CreateUsuarioCommandValidatorTests
|
|
{
|
|
private readonly IRolRepository _roles = Substitute.For<IRolRepository>();
|
|
|
|
public CreateUsuarioCommandValidatorTests()
|
|
{
|
|
// Default mock behavior: canonical seeds are active; unknown codes are not.
|
|
var canonical = new[] { "admin", "cajero", "operador_ctacte", "picadora",
|
|
"jefe_publicidad", "productor", "diagramacion", "reportes" };
|
|
foreach (var code in canonical)
|
|
_roles.ExistsActiveByCodigoAsync(code, Arg.Any<CancellationToken>()).Returns(true);
|
|
}
|
|
|
|
private CreateUsuarioCommandValidator BuildValidator(AuthOptions? opts = null) =>
|
|
new(opts ?? new AuthOptions(), _roles);
|
|
|
|
private static CreateUsuarioCommand ValidCommand() => new(
|
|
Username: "operador1",
|
|
Password: "Secreto123",
|
|
Nombre: "Juan",
|
|
Apellido: "Pérez",
|
|
Email: null,
|
|
Rol: "cajero");
|
|
|
|
// ── Happy paths ──────────────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public async Task Validate_ValidCommand_NoErrors()
|
|
{
|
|
var result = await BuildValidator().TestValidateAsync(ValidCommand());
|
|
result.ShouldNotHaveAnyValidationErrors();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Validate_NullEmail_IsValid()
|
|
{
|
|
var cmd = ValidCommand() with { Email = null };
|
|
var result = await BuildValidator().TestValidateAsync(cmd);
|
|
result.ShouldNotHaveAnyValidationErrors();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Validate_ValidEmailPresent_NoErrors()
|
|
{
|
|
var cmd = ValidCommand() with { Email = "juan@example.com" };
|
|
var result = await BuildValidator().TestValidateAsync(cmd);
|
|
result.ShouldNotHaveAnyValidationErrors();
|
|
}
|
|
|
|
// ── Username ─────────────────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public async Task Validate_EmptyUsername_HasError()
|
|
{
|
|
var cmd = ValidCommand() with { Username = "" };
|
|
(await BuildValidator().TestValidateAsync(cmd)).ShouldHaveValidationErrorFor(c => c.Username);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Validate_UsernameTooShort_HasError()
|
|
{
|
|
var cmd = ValidCommand() with { Username = "ab" };
|
|
(await BuildValidator().TestValidateAsync(cmd)).ShouldHaveValidationErrorFor(c => c.Username);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Validate_UsernameTooLong_HasError()
|
|
{
|
|
var cmd = ValidCommand() with { Username = new string('a', 51) };
|
|
(await BuildValidator().TestValidateAsync(cmd)).ShouldHaveValidationErrorFor(c => c.Username);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("abc")]
|
|
[InlineData("user.name")]
|
|
[InlineData("user-name")]
|
|
[InlineData("user_name")]
|
|
[InlineData("user123")]
|
|
public async Task Validate_UsernameValidFormats_NoError(string username)
|
|
{
|
|
var cmd = ValidCommand() with { Username = username };
|
|
(await BuildValidator().TestValidateAsync(cmd)).ShouldNotHaveValidationErrorFor(c => c.Username);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("user name")]
|
|
[InlineData("user@name")]
|
|
[InlineData("user#1")]
|
|
public async Task Validate_UsernameInvalidChars_HasError(string username)
|
|
{
|
|
var cmd = ValidCommand() with { Username = username };
|
|
(await BuildValidator().TestValidateAsync(cmd)).ShouldHaveValidationErrorFor(c => c.Username);
|
|
}
|
|
|
|
// ── Password ─────────────────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public async Task Validate_EmptyPassword_HasError()
|
|
{
|
|
var cmd = ValidCommand() with { Password = "" };
|
|
(await BuildValidator().TestValidateAsync(cmd)).ShouldHaveValidationErrorFor(c => c.Password);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Validate_PasswordTooShort_HasError()
|
|
{
|
|
var cmd = ValidCommand() with { Password = "Ab1cd5" };
|
|
(await BuildValidator().TestValidateAsync(cmd)).ShouldHaveValidationErrorFor(c => c.Password);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Validate_PasswordNoLetter_HasError()
|
|
{
|
|
var cmd = ValidCommand() with { Password = "12345678" };
|
|
(await BuildValidator().TestValidateAsync(cmd)).ShouldHaveValidationErrorFor(c => c.Password);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Validate_PasswordNoDigit_HasError()
|
|
{
|
|
var cmd = ValidCommand() with { Password = "abcdefgh" };
|
|
(await BuildValidator().TestValidateAsync(cmd)).ShouldHaveValidationErrorFor(c => c.Password);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Validate_PasswordExactMinLength_NoError()
|
|
{
|
|
var cmd = ValidCommand() with { Password = "Secre123" };
|
|
(await BuildValidator().TestValidateAsync(cmd)).ShouldNotHaveValidationErrorFor(c => c.Password);
|
|
}
|
|
|
|
// ── Nombre / Apellido ────────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public async Task Validate_EmptyNombre_HasError()
|
|
{
|
|
var cmd = ValidCommand() with { Nombre = "" };
|
|
(await BuildValidator().TestValidateAsync(cmd)).ShouldHaveValidationErrorFor(c => c.Nombre);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Validate_EmptyApellido_HasError()
|
|
{
|
|
var cmd = ValidCommand() with { Apellido = "" };
|
|
(await BuildValidator().TestValidateAsync(cmd)).ShouldHaveValidationErrorFor(c => c.Apellido);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Validate_NombreTooLong_HasError()
|
|
{
|
|
var cmd = ValidCommand() with { Nombre = new string('a', 101) };
|
|
(await BuildValidator().TestValidateAsync(cmd)).ShouldHaveValidationErrorFor(c => c.Nombre);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Validate_ApellidoTooLong_HasError()
|
|
{
|
|
var cmd = ValidCommand() with { Apellido = new string('a', 101) };
|
|
(await BuildValidator().TestValidateAsync(cmd)).ShouldHaveValidationErrorFor(c => c.Apellido);
|
|
}
|
|
|
|
// ── Rol ──────────────────────────────────────────────────────────────────
|
|
|
|
[Theory]
|
|
[InlineData("admin")]
|
|
[InlineData("cajero")]
|
|
[InlineData("operador_ctacte")]
|
|
[InlineData("picadora")]
|
|
[InlineData("jefe_publicidad")]
|
|
[InlineData("productor")]
|
|
[InlineData("diagramacion")]
|
|
[InlineData("reportes")]
|
|
public async Task Validate_CanonicalActiveRoles_NoError(string rol)
|
|
{
|
|
var cmd = ValidCommand() with { Rol = rol };
|
|
(await BuildValidator().TestValidateAsync(cmd)).ShouldNotHaveValidationErrorFor(c => c.Rol);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Validate_RolInexistente_HasError()
|
|
{
|
|
_roles.ExistsActiveByCodigoAsync("superuser", Arg.Any<CancellationToken>()).Returns(false);
|
|
|
|
var cmd = ValidCommand() with { Rol = "superuser" };
|
|
(await BuildValidator().TestValidateAsync(cmd)).ShouldHaveValidationErrorFor(c => c.Rol);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Validate_RolInactivo_HasError()
|
|
{
|
|
// The repository reports NOT active (soft-deleted rol) → validator rejects.
|
|
_roles.ExistsActiveByCodigoAsync("picadora", Arg.Any<CancellationToken>()).Returns(false);
|
|
|
|
var cmd = ValidCommand() with { Rol = "picadora" };
|
|
(await BuildValidator().TestValidateAsync(cmd)).ShouldHaveValidationErrorFor(c => c.Rol);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Validate_RolEmptyString_HasError()
|
|
{
|
|
var cmd = ValidCommand() with { Rol = "" };
|
|
(await BuildValidator().TestValidateAsync(cmd)).ShouldHaveValidationErrorFor(c => c.Rol);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Validate_RolCaseSensitive_HasError()
|
|
{
|
|
// 'ADMIN' uppercase is not a canonical code; mock returns false by default.
|
|
_roles.ExistsActiveByCodigoAsync("ADMIN", Arg.Any<CancellationToken>()).Returns(false);
|
|
|
|
var cmd = ValidCommand() with { Rol = "ADMIN" };
|
|
(await BuildValidator().TestValidateAsync(cmd)).ShouldHaveValidationErrorFor(c => c.Rol);
|
|
}
|
|
|
|
// ── Email ────────────────────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public async Task Validate_InvalidEmail_HasError()
|
|
{
|
|
var cmd = ValidCommand() with { Email = "not-an-email" };
|
|
(await BuildValidator().TestValidateAsync(cmd)).ShouldHaveValidationErrorFor(c => c.Email);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Validate_EmailTooLong_HasError()
|
|
{
|
|
var cmd = ValidCommand() with { Email = new string('a', 145) + "@b.com" };
|
|
(await BuildValidator().TestValidateAsync(cmd)).ShouldHaveValidationErrorFor(c => c.Email);
|
|
}
|
|
}
|