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(); 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()).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()).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()).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()).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); } }