feat(api): UDT-004 dominio + repositorio + application roles (tdd)
- Migraciones V003 (tabla Rol + 8 seeds canonicos) y V004 (drop CK + FK Usuario.Rol) - Dominio: Rol entity + 3 excepciones (RolNotFound/AlreadyExists/InUse) - Infraestructura: RolRepository (Dapper) con List/Get/ExistsActive/Add/Update/HasActiveUsuarios - Application: CRUD queries y commands (List, Get, Create, Update, Deactivate) + validators (codigo regex ^[a-z][a-z0-9_]*$) - Validator UDT-003: whitelist alineada a codigos canonicos (full IRolRepository lookup diferido a Phase 5.1) - Tests: 169 application + 15 api (todos verdes). Respawn configurado para re-seedear Rol canonical post-reset. - Estricto TDD: RED/GREEN/TRIANGULATE en todos los handlers nuevos.
This commit is contained in:
82
tests/SIGCM2.Application.Tests/Domain/RolTests.cs
Normal file
82
tests/SIGCM2.Application.Tests/Domain/RolTests.cs
Normal file
@@ -0,0 +1,82 @@
|
||||
using SIGCM2.Domain.Entities;
|
||||
|
||||
namespace SIGCM2.Application.Tests.Domain;
|
||||
|
||||
public class RolTests
|
||||
{
|
||||
// Happy path: full constructor sets all properties.
|
||||
[Fact]
|
||||
public void Constructor_SetsAllProperties()
|
||||
{
|
||||
var created = new DateTime(2026, 4, 15, 10, 0, 0, DateTimeKind.Utc);
|
||||
var modified = new DateTime(2026, 4, 15, 11, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
var rol = new Rol(
|
||||
id: 1,
|
||||
codigo: "cajero",
|
||||
nombre: "Cajero",
|
||||
descripcion: "Atención de mostrador",
|
||||
activo: true,
|
||||
fechaCreacion: created,
|
||||
fechaModificacion: modified
|
||||
);
|
||||
|
||||
Assert.Equal(1, rol.Id);
|
||||
Assert.Equal("cajero", rol.Codigo);
|
||||
Assert.Equal("Cajero", rol.Nombre);
|
||||
Assert.Equal("Atención de mostrador", rol.Descripcion);
|
||||
Assert.True(rol.Activo);
|
||||
Assert.Equal(created, rol.FechaCreacion);
|
||||
Assert.Equal(modified, rol.FechaModificacion);
|
||||
}
|
||||
|
||||
// Triangulation: descripcion is nullable, fechaModificacion is nullable.
|
||||
[Fact]
|
||||
public void Constructor_WithNullOptionals_SetsNulls()
|
||||
{
|
||||
var created = new DateTime(2026, 4, 15, 10, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
var rol = new Rol(
|
||||
id: 2,
|
||||
codigo: "reportes",
|
||||
nombre: "Reportes",
|
||||
descripcion: null,
|
||||
activo: false,
|
||||
fechaCreacion: created,
|
||||
fechaModificacion: null
|
||||
);
|
||||
|
||||
Assert.Null(rol.Descripcion);
|
||||
Assert.Null(rol.FechaModificacion);
|
||||
Assert.False(rol.Activo);
|
||||
}
|
||||
|
||||
// ForCreation: Id=0 (IDENTITY assigned by DB), Activo=true, FechaCreacion=SYSUTCDATETIME-ish (not set here), FechaModificacion=null.
|
||||
[Fact]
|
||||
public void ForCreation_ReturnsNewInstanceWithDefaults()
|
||||
{
|
||||
var rol = Rol.ForCreation(
|
||||
codigo: "picadora",
|
||||
nombre: "Picadora/Correctora",
|
||||
descripcion: "Edición de textos"
|
||||
);
|
||||
|
||||
Assert.Equal(0, rol.Id);
|
||||
Assert.Equal("picadora", rol.Codigo);
|
||||
Assert.Equal("Picadora/Correctora", rol.Nombre);
|
||||
Assert.Equal("Edición de textos", rol.Descripcion);
|
||||
Assert.True(rol.Activo);
|
||||
Assert.Null(rol.FechaModificacion);
|
||||
}
|
||||
|
||||
// Triangulation: ForCreation accepts null descripcion.
|
||||
[Fact]
|
||||
public void ForCreation_WithNullDescripcion_AllowsNull()
|
||||
{
|
||||
var rol = Rol.ForCreation(codigo: "admin", nombre: "Administrador", descripcion: null);
|
||||
|
||||
Assert.Null(rol.Descripcion);
|
||||
Assert.Equal("admin", rol.Codigo);
|
||||
Assert.True(rol.Activo);
|
||||
}
|
||||
}
|
||||
@@ -29,10 +29,13 @@ public class RefreshTokenRepositoryTests : IAsyncLifetime
|
||||
|
||||
_respawner = await Respawner.CreateAsync(_connection, new RespawnerOptions
|
||||
{
|
||||
DbAdapter = DbAdapter.SqlServer
|
||||
DbAdapter = DbAdapter.SqlServer,
|
||||
// Rol is a lookup table seeded by migration V003 — never wipe or Usuario FK breaks.
|
||||
TablesToIgnore = [new Respawn.Graph.Table("dbo", "Rol")]
|
||||
});
|
||||
|
||||
await _respawner.ResetAsync(_connection);
|
||||
await SeedRolCanonicalAsync();
|
||||
await SeedTestUserAsync();
|
||||
|
||||
_testUserId = await _connection.QuerySingleAsync<int>(
|
||||
@@ -49,6 +52,29 @@ public class RefreshTokenRepositoryTests : IAsyncLifetime
|
||||
await _connection.DisposeAsync();
|
||||
}
|
||||
|
||||
private async Task SeedRolCanonicalAsync()
|
||||
{
|
||||
const string sql = """
|
||||
SET QUOTED_IDENTIFIER ON;
|
||||
MERGE dbo.Rol AS t
|
||||
USING (VALUES
|
||||
('admin', N'Administrador', N'Supervisor total'),
|
||||
('cajero', N'Cajero', N'Mostrador contado'),
|
||||
('operador_ctacte', N'Operador Cta Cte', N'Cuenta corriente'),
|
||||
('picadora', N'Picadora/Correctora', N'Edición de textos'),
|
||||
('jefe_publicidad', N'Jefe de Publicidad', N'Supervisión de pauta'),
|
||||
('productor', N'Productor', N'Carga restringida'),
|
||||
('diagramacion', N'Diagramación/Taller', N'Solo lectura pauta'),
|
||||
('reportes', N'Reportes', N'Solo lectura reportes')
|
||||
) AS s (Codigo, Nombre, Descripcion)
|
||||
ON t.Codigo = s.Codigo
|
||||
WHEN NOT MATCHED BY TARGET THEN
|
||||
INSERT (Codigo, Nombre, Descripcion, Activo)
|
||||
VALUES (s.Codigo, s.Nombre, s.Descripcion, 1);
|
||||
""";
|
||||
await _connection.ExecuteAsync(sql);
|
||||
}
|
||||
|
||||
private async Task SeedTestUserAsync()
|
||||
{
|
||||
await _connection.ExecuteAsync("""
|
||||
|
||||
240
tests/SIGCM2.Application.Tests/Integration/RolRepositoryTests.cs
Normal file
240
tests/SIGCM2.Application.Tests/Integration/RolRepositoryTests.cs
Normal file
@@ -0,0 +1,240 @@
|
||||
using Dapper;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using SIGCM2.Domain.Entities;
|
||||
using SIGCM2.Infrastructure.Persistence;
|
||||
|
||||
namespace SIGCM2.Application.Tests.Integration;
|
||||
|
||||
[Collection("Database")]
|
||||
public class RolRepositoryTests : IAsyncLifetime
|
||||
{
|
||||
private const string ConnectionString =
|
||||
"Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;";
|
||||
|
||||
private SqlConnection _connection = null!;
|
||||
private RolRepository _repository = null!;
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
_connection = new SqlConnection(ConnectionString);
|
||||
await _connection.OpenAsync();
|
||||
|
||||
// Clean Usuario first (FK), then custom Rol codes created by tests.
|
||||
await _connection.ExecuteAsync("DELETE FROM dbo.Usuario;");
|
||||
await _connection.ExecuteAsync("DELETE FROM dbo.Rol WHERE Codigo NOT IN ('admin','cajero','operador_ctacte','picadora','jefe_publicidad','productor','diagramacion','reportes');");
|
||||
// Ensure canonical Rol seeds exist (idempotent — previous test classes may have wiped them via Respawn).
|
||||
await SeedRolCanonicalAsync();
|
||||
// Reset any mutations applied to canonical seeds during prior tests.
|
||||
await _connection.ExecuteAsync("UPDATE dbo.Rol SET Activo = 1, FechaModificacion = NULL WHERE Codigo IN ('admin','cajero','operador_ctacte','picadora','jefe_publicidad','productor','diagramacion','reportes');");
|
||||
// Seed admin usuario (needed by HasActiveUsuariosAsync test expecting admin active).
|
||||
await _connection.ExecuteAsync(
|
||||
"INSERT INTO dbo.Usuario (Username, PasswordHash, Nombre, Apellido, Rol, PermisosJson, Activo) " +
|
||||
"VALUES ('admin', '$2a$12$hash', 'Administrador', 'Sistema', 'admin', '[\"*\"]', 1);");
|
||||
|
||||
var factory = new SqlConnectionFactory(ConnectionString);
|
||||
_repository = new RolRepository(factory);
|
||||
}
|
||||
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
await _connection.CloseAsync();
|
||||
await _connection.DisposeAsync();
|
||||
}
|
||||
|
||||
private async Task SeedRolCanonicalAsync()
|
||||
{
|
||||
const string sql = """
|
||||
SET QUOTED_IDENTIFIER ON;
|
||||
MERGE dbo.Rol AS t
|
||||
USING (VALUES
|
||||
('admin', N'Administrador', N'Supervisor total'),
|
||||
('cajero', N'Cajero', N'Mostrador contado'),
|
||||
('operador_ctacte', N'Operador Cta Cte', N'Cuenta corriente'),
|
||||
('picadora', N'Picadora/Correctora', N'Edición de textos'),
|
||||
('jefe_publicidad', N'Jefe de Publicidad', N'Supervisión de pauta'),
|
||||
('productor', N'Productor', N'Carga restringida'),
|
||||
('diagramacion', N'Diagramación/Taller', N'Solo lectura pauta'),
|
||||
('reportes', N'Reportes', N'Solo lectura reportes')
|
||||
) AS s (Codigo, Nombre, Descripcion)
|
||||
ON t.Codigo = s.Codigo
|
||||
WHEN NOT MATCHED BY TARGET THEN
|
||||
INSERT (Codigo, Nombre, Descripcion, Activo)
|
||||
VALUES (s.Codigo, s.Nombre, s.Descripcion, 1);
|
||||
""";
|
||||
await _connection.ExecuteAsync(sql);
|
||||
}
|
||||
|
||||
// ── ListAsync ────────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task ListAsync_ReturnsAllCanonicalSeeds()
|
||||
{
|
||||
var list = await _repository.ListAsync();
|
||||
|
||||
var codes = list.Select(r => r.Codigo).ToHashSet();
|
||||
Assert.Contains("admin", codes);
|
||||
Assert.Contains("cajero", codes);
|
||||
Assert.Contains("operador_ctacte", codes);
|
||||
Assert.Contains("picadora", codes);
|
||||
Assert.Contains("jefe_publicidad", codes);
|
||||
Assert.Contains("productor", codes);
|
||||
Assert.Contains("diagramacion", codes);
|
||||
Assert.Contains("reportes", codes);
|
||||
Assert.True(list.Count >= 8);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ListAsync_IncludesInactiveRoles()
|
||||
{
|
||||
// Triangulation: list must include deactivated rows too.
|
||||
await _connection.ExecuteAsync("INSERT INTO dbo.Rol (Codigo, Nombre, Activo) VALUES ('listtest_inactive', N'Inactivo de test', 0);");
|
||||
|
||||
var list = await _repository.ListAsync();
|
||||
|
||||
var inactive = list.Single(r => r.Codigo == "listtest_inactive");
|
||||
Assert.False(inactive.Activo);
|
||||
}
|
||||
|
||||
// ── GetByCodigoAsync ────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task GetByCodigoAsync_ExistingCodigo_ReturnsRol()
|
||||
{
|
||||
var rol = await _repository.GetByCodigoAsync("cajero");
|
||||
|
||||
Assert.NotNull(rol);
|
||||
Assert.Equal("cajero", rol!.Codigo);
|
||||
Assert.Equal("Cajero", rol.Nombre);
|
||||
Assert.True(rol.Activo);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetByCodigoAsync_NonExistentCodigo_ReturnsNull()
|
||||
{
|
||||
var rol = await _repository.GetByCodigoAsync("no_existe");
|
||||
Assert.Null(rol);
|
||||
}
|
||||
|
||||
// ── ExistsActiveByCodigoAsync ───────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task ExistsActiveByCodigoAsync_ActiveCodigo_ReturnsTrue()
|
||||
{
|
||||
Assert.True(await _repository.ExistsActiveByCodigoAsync("admin"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExistsActiveByCodigoAsync_InactiveCodigo_ReturnsFalse()
|
||||
{
|
||||
await _connection.ExecuteAsync("INSERT INTO dbo.Rol (Codigo, Nombre, Activo) VALUES ('exists_inactive', N'Test inactivo', 0);");
|
||||
|
||||
Assert.False(await _repository.ExistsActiveByCodigoAsync("exists_inactive"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExistsActiveByCodigoAsync_MissingCodigo_ReturnsFalse()
|
||||
{
|
||||
Assert.False(await _repository.ExistsActiveByCodigoAsync("missing_codigo_xyz"));
|
||||
}
|
||||
|
||||
// ── AddAsync ────────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task AddAsync_NewRol_PersistsAndReturnsId()
|
||||
{
|
||||
var rol = Rol.ForCreation("addtest_new", "Add Test", "Rol de prueba add");
|
||||
|
||||
var newId = await _repository.AddAsync(rol);
|
||||
|
||||
Assert.True(newId > 0);
|
||||
|
||||
var persisted = await _connection.QuerySingleAsync<(string Codigo, string Nombre, string? Descripcion, bool Activo)>(
|
||||
"SELECT Codigo, Nombre, Descripcion, Activo FROM dbo.Rol WHERE Id = @Id",
|
||||
new { Id = newId });
|
||||
|
||||
Assert.Equal("addtest_new", persisted.Codigo);
|
||||
Assert.Equal("Add Test", persisted.Nombre);
|
||||
Assert.Equal("Rol de prueba add", persisted.Descripcion);
|
||||
Assert.True(persisted.Activo);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddAsync_WithNullDescripcion_PersistsNull()
|
||||
{
|
||||
var rol = Rol.ForCreation("addtest_nulldesc", "Null Desc", null);
|
||||
|
||||
var newId = await _repository.AddAsync(rol);
|
||||
|
||||
var desc = await _connection.ExecuteScalarAsync<string?>(
|
||||
"SELECT Descripcion FROM dbo.Rol WHERE Id = @Id", new { Id = newId });
|
||||
Assert.Null(desc);
|
||||
}
|
||||
|
||||
// ── UpdateAsync ─────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateAsync_ExistingCodigo_UpdatesMutableFieldsAndSetsFechaModificacion()
|
||||
{
|
||||
await _connection.ExecuteAsync(
|
||||
"INSERT INTO dbo.Rol (Codigo, Nombre, Descripcion, Activo) VALUES ('updtest_one', N'Nombre Viejo', N'Desc vieja', 1);");
|
||||
|
||||
var updated = await _repository.UpdateAsync("updtest_one", "Nombre Nuevo", "Desc nueva", activo: true);
|
||||
|
||||
Assert.True(updated);
|
||||
|
||||
var row = await _connection.QuerySingleAsync<(string Nombre, string? Descripcion, bool Activo, DateTime? FechaModificacion)>(
|
||||
"SELECT Nombre, Descripcion, Activo, FechaModificacion FROM dbo.Rol WHERE Codigo = 'updtest_one'");
|
||||
|
||||
Assert.Equal("Nombre Nuevo", row.Nombre);
|
||||
Assert.Equal("Desc nueva", row.Descripcion);
|
||||
Assert.True(row.Activo);
|
||||
Assert.NotNull(row.FechaModificacion);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateAsync_NonExistentCodigo_ReturnsFalse()
|
||||
{
|
||||
var updated = await _repository.UpdateAsync("updtest_missing", "X", null, true);
|
||||
Assert.False(updated);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateAsync_DoesNotChangeCodigo()
|
||||
{
|
||||
await _connection.ExecuteAsync(
|
||||
"INSERT INTO dbo.Rol (Codigo, Nombre, Activo) VALUES ('updtest_codigo', N'Test Codigo', 1);");
|
||||
|
||||
await _repository.UpdateAsync("updtest_codigo", "Nombre Cambiado", null, true);
|
||||
|
||||
var stillExists = await _connection.ExecuteScalarAsync<int>(
|
||||
"SELECT COUNT(*) FROM dbo.Rol WHERE Codigo = 'updtest_codigo';");
|
||||
Assert.Equal(1, stillExists);
|
||||
}
|
||||
|
||||
// ── HasActiveUsuariosAsync ──────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task HasActiveUsuariosAsync_WithActiveUsuario_ReturnsTrue()
|
||||
{
|
||||
// 'admin' Usuario is seeded active and references Rol.admin.
|
||||
Assert.True(await _repository.HasActiveUsuariosAsync("admin"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HasActiveUsuariosAsync_NoUsuariosReferencing_ReturnsFalse()
|
||||
{
|
||||
// 'reportes' seed has no Usuario referencing it in a clean test DB.
|
||||
Assert.False(await _repository.HasActiveUsuariosAsync("reportes"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HasActiveUsuariosAsync_OnlyInactiveUsuarioReferencing_ReturnsFalse()
|
||||
{
|
||||
// Insert an INACTIVE usuario referencing 'cajero'.
|
||||
await _connection.ExecuteAsync(
|
||||
"INSERT INTO dbo.Usuario (Username, PasswordHash, Nombre, Apellido, Rol, PermisosJson, Activo) " +
|
||||
"VALUES ('inactivo1', '$2a$12$hash', 'Test', 'Inactivo', 'cajero', '[]', 0);");
|
||||
|
||||
Assert.False(await _repository.HasActiveUsuariosAsync("cajero"));
|
||||
}
|
||||
}
|
||||
@@ -21,11 +21,14 @@ public class UsuarioRepositoryTests : IAsyncLifetime
|
||||
|
||||
_respawner = await Respawner.CreateAsync(_connection, new RespawnerOptions
|
||||
{
|
||||
DbAdapter = DbAdapter.SqlServer
|
||||
DbAdapter = DbAdapter.SqlServer,
|
||||
// Rol is a lookup table seeded by migration V003 — never wipe or Usuario FK breaks.
|
||||
TablesToIgnore = [new Respawn.Graph.Table("dbo", "Rol")]
|
||||
});
|
||||
|
||||
// Reset DB and seed admin user for each test class run
|
||||
// Reset DB, re-seed Rol canonical table (lookup) and admin user for each test class run.
|
||||
await _respawner.ResetAsync(_connection);
|
||||
await SeedRolCanonicalAsync();
|
||||
await SeedAdminAsync();
|
||||
|
||||
var factory = new SqlConnectionFactory(ConnectionString);
|
||||
@@ -62,21 +65,44 @@ public class UsuarioRepositoryTests : IAsyncLifetime
|
||||
|
||||
// Triangulation: case-sensitive username lookup (SQL Server UNIQUE constraint is case-insensitive by default)
|
||||
[Fact]
|
||||
public async Task GetByUsernameAsync_DifferentUser_ReturnsCorrectUser()
|
||||
public async Task GetByUsernameAsync_DifferentUser_ReturnsCorrectUser_Cajero()
|
||||
{
|
||||
// Insert a second user
|
||||
// Insert a second user with canonical rol 'cajero' (post-UDT-004 FK requires Rol.Codigo to exist).
|
||||
await _connection.ExecuteAsync(
|
||||
"INSERT INTO dbo.Usuario (Username, PasswordHash, Nombre, Apellido, Rol, PermisosJson) " +
|
||||
"VALUES ('vendedor1', '$2a$12$hash2', 'Juan', 'Pérez', 'vendedor', '[]')");
|
||||
"VALUES ('cajero1', '$2a$12$hash2', 'Juan', 'Pérez', 'cajero', '[]')");
|
||||
|
||||
var admin = await _repository.GetByUsernameAsync("admin");
|
||||
var vendedor = await _repository.GetByUsernameAsync("vendedor1");
|
||||
var cajero = await _repository.GetByUsernameAsync("cajero1");
|
||||
|
||||
Assert.NotNull(admin);
|
||||
Assert.NotNull(vendedor);
|
||||
Assert.NotEqual(admin.Id, vendedor.Id);
|
||||
Assert.NotNull(cajero);
|
||||
Assert.NotEqual(admin.Id, cajero.Id);
|
||||
Assert.Equal("admin", admin.Rol);
|
||||
Assert.Equal("vendedor", vendedor.Rol);
|
||||
Assert.Equal("cajero", cajero.Rol);
|
||||
}
|
||||
|
||||
private async Task SeedRolCanonicalAsync()
|
||||
{
|
||||
const string sql = """
|
||||
SET QUOTED_IDENTIFIER ON;
|
||||
MERGE dbo.Rol AS t
|
||||
USING (VALUES
|
||||
('admin', N'Administrador', N'Supervisor total'),
|
||||
('cajero', N'Cajero', N'Mostrador contado'),
|
||||
('operador_ctacte', N'Operador Cta Cte', N'Cuenta corriente'),
|
||||
('picadora', N'Picadora/Correctora', N'Edición de textos'),
|
||||
('jefe_publicidad', N'Jefe de Publicidad', N'Supervisión de pauta'),
|
||||
('productor', N'Productor', N'Carga restringida'),
|
||||
('diagramacion', N'Diagramación/Taller', N'Solo lectura pauta'),
|
||||
('reportes', N'Reportes', N'Solo lectura reportes')
|
||||
) AS s (Codigo, Nombre, Descripcion)
|
||||
ON t.Codigo = s.Codigo
|
||||
WHEN NOT MATCHED BY TARGET THEN
|
||||
INSERT (Codigo, Nombre, Descripcion, Activo)
|
||||
VALUES (s.Codigo, s.Nombre, s.Descripcion, 1);
|
||||
""";
|
||||
await _connection.ExecuteAsync(sql);
|
||||
}
|
||||
|
||||
private async Task SeedAdminAsync()
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
using NSubstitute;
|
||||
using SIGCM2.Application.Abstractions.Persistence;
|
||||
using SIGCM2.Application.Roles.Create;
|
||||
using SIGCM2.Domain.Entities;
|
||||
using SIGCM2.Domain.Exceptions;
|
||||
|
||||
namespace SIGCM2.Application.Tests.Roles.Create;
|
||||
|
||||
public class CreateRolCommandHandlerTests
|
||||
{
|
||||
private readonly IRolRepository _repository = Substitute.For<IRolRepository>();
|
||||
private readonly CreateRolCommandHandler _handler;
|
||||
|
||||
private static CreateRolCommand ValidCommand() => new("cajero_senior", "Cajero Senior", "Con más permisos");
|
||||
|
||||
public CreateRolCommandHandlerTests()
|
||||
{
|
||||
_handler = new CreateRolCommandHandler(_repository);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_CodigoDuplicado_ThrowsRolAlreadyExistsException()
|
||||
{
|
||||
var now = new DateTime(2026, 4, 15, 10, 0, 0, DateTimeKind.Utc);
|
||||
_repository.GetByCodigoAsync("cajero_senior")
|
||||
.Returns(new Rol(99, "cajero_senior", "Cajero Senior", null, true, now, null));
|
||||
|
||||
var ex = await Assert.ThrowsAsync<RolAlreadyExistsException>(
|
||||
() => _handler.Handle(ValidCommand()));
|
||||
|
||||
Assert.Equal("cajero_senior", ex.Codigo);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_CodigoDuplicado_DoesNotCallAddAsync()
|
||||
{
|
||||
var now = new DateTime(2026, 4, 15, 10, 0, 0, DateTimeKind.Utc);
|
||||
_repository.GetByCodigoAsync(Arg.Any<string>())
|
||||
.Returns(new Rol(1, "cajero_senior", "X", null, true, now, null));
|
||||
|
||||
try { await _handler.Handle(ValidCommand()); } catch (RolAlreadyExistsException) { }
|
||||
|
||||
await _repository.DidNotReceive().AddAsync(Arg.Any<Rol>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_Happy_AddsAndReturnsDtoWithId()
|
||||
{
|
||||
_repository.GetByCodigoAsync(Arg.Any<string>()).Returns((Rol?)null);
|
||||
_repository.AddAsync(Arg.Any<Rol>(), Arg.Any<CancellationToken>()).Returns(42);
|
||||
|
||||
var result = await _handler.Handle(ValidCommand());
|
||||
|
||||
Assert.Equal(42, result.Id);
|
||||
Assert.Equal("cajero_senior", result.Codigo);
|
||||
Assert.Equal("Cajero Senior", result.Nombre);
|
||||
Assert.Equal("Con más permisos", result.Descripcion);
|
||||
Assert.True(result.Activo);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_Happy_CallsAddAsyncOnce()
|
||||
{
|
||||
_repository.GetByCodigoAsync(Arg.Any<string>()).Returns((Rol?)null);
|
||||
_repository.AddAsync(Arg.Any<Rol>(), Arg.Any<CancellationToken>()).Returns(5);
|
||||
|
||||
await _handler.Handle(ValidCommand());
|
||||
|
||||
await _repository.Received(1).AddAsync(
|
||||
Arg.Is<Rol>(r => r.Codigo == "cajero_senior" && r.Nombre == "Cajero Senior" && r.Activo),
|
||||
Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_Happy_WithNullDescripcion_PassesNullToRepository()
|
||||
{
|
||||
_repository.GetByCodigoAsync(Arg.Any<string>()).Returns((Rol?)null);
|
||||
_repository.AddAsync(Arg.Any<Rol>(), Arg.Any<CancellationToken>()).Returns(1);
|
||||
|
||||
await _handler.Handle(new CreateRolCommand("nuevo_rol", "Nuevo", null));
|
||||
|
||||
await _repository.Received(1).AddAsync(
|
||||
Arg.Is<Rol>(r => r.Descripcion == null),
|
||||
Arg.Any<CancellationToken>());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
using FluentValidation.TestHelper;
|
||||
using SIGCM2.Application.Roles.Create;
|
||||
|
||||
namespace SIGCM2.Application.Tests.Roles.Create;
|
||||
|
||||
public class CreateRolCommandValidatorTests
|
||||
{
|
||||
private static CreateRolCommandValidator BuildValidator() => new();
|
||||
private static CreateRolCommand Valid() => new("cajero_senior", "Cajero Senior", "Cajero con permisos extendidos");
|
||||
|
||||
// ── Happy path ─────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Validate_Valid_NoErrors()
|
||||
{
|
||||
BuildValidator().TestValidate(Valid()).ShouldNotHaveAnyValidationErrors();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_NullDescripcion_IsValid()
|
||||
{
|
||||
BuildValidator().TestValidate(Valid() with { Descripcion = null }).ShouldNotHaveAnyValidationErrors();
|
||||
}
|
||||
|
||||
// ── Codigo ─────────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Validate_EmptyCodigo_HasError()
|
||||
{
|
||||
BuildValidator().TestValidate(Valid() with { Codigo = "" })
|
||||
.ShouldHaveValidationErrorFor(c => c.Codigo);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_CodigoTooShort_HasError()
|
||||
{
|
||||
BuildValidator().TestValidate(Valid() with { Codigo = "ab" })
|
||||
.ShouldHaveValidationErrorFor(c => c.Codigo);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_CodigoTooLong_HasError()
|
||||
{
|
||||
BuildValidator().TestValidate(Valid() with { Codigo = new string('a', 31) })
|
||||
.ShouldHaveValidationErrorFor(c => c.Codigo);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("abc")] // boundary short
|
||||
[InlineData("cajero")]
|
||||
[InlineData("operador_ctacte")]
|
||||
[InlineData("jefe_publicidad")]
|
||||
[InlineData("a1b2")]
|
||||
public void Validate_CodigoValidFormats_NoError(string codigo)
|
||||
{
|
||||
BuildValidator().TestValidate(Valid() with { Codigo = codigo })
|
||||
.ShouldNotHaveValidationErrorFor(c => c.Codigo);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Cajero")] // uppercase
|
||||
[InlineData("1cajero")] // starts with digit
|
||||
[InlineData("_cajero")] // starts with underscore
|
||||
[InlineData("cajero senior")] // space
|
||||
[InlineData("cajero-senior")] // dash
|
||||
[InlineData("cajero.senior")] // dot
|
||||
public void Validate_CodigoInvalidFormats_HasError(string codigo)
|
||||
{
|
||||
BuildValidator().TestValidate(Valid() with { Codigo = codigo })
|
||||
.ShouldHaveValidationErrorFor(c => c.Codigo);
|
||||
}
|
||||
|
||||
// ── Nombre ─────────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Validate_EmptyNombre_HasError()
|
||||
{
|
||||
BuildValidator().TestValidate(Valid() with { Nombre = "" })
|
||||
.ShouldHaveValidationErrorFor(c => c.Nombre);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_NombreTooLong_HasError()
|
||||
{
|
||||
BuildValidator().TestValidate(Valid() with { Nombre = new string('a', 61) })
|
||||
.ShouldHaveValidationErrorFor(c => c.Nombre);
|
||||
}
|
||||
|
||||
// ── Descripcion ────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Validate_DescripcionTooLong_HasError()
|
||||
{
|
||||
BuildValidator().TestValidate(Valid() with { Descripcion = new string('a', 251) })
|
||||
.ShouldHaveValidationErrorFor(c => c.Descripcion);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
using NSubstitute;
|
||||
using SIGCM2.Application.Abstractions.Persistence;
|
||||
using SIGCM2.Application.Roles.Deactivate;
|
||||
using SIGCM2.Domain.Entities;
|
||||
using SIGCM2.Domain.Exceptions;
|
||||
|
||||
namespace SIGCM2.Application.Tests.Roles.Deactivate;
|
||||
|
||||
public class DeactivateRolCommandHandlerTests
|
||||
{
|
||||
private readonly IRolRepository _repository = Substitute.For<IRolRepository>();
|
||||
private readonly DeactivateRolCommandHandler _handler;
|
||||
|
||||
private static Rol RolActive(string codigo, int id = 10)
|
||||
{
|
||||
var now = new DateTime(2026, 4, 15, 10, 0, 0, DateTimeKind.Utc);
|
||||
return new Rol(id, codigo, "Nombre", "Desc", true, now, null);
|
||||
}
|
||||
|
||||
public DeactivateRolCommandHandlerTests()
|
||||
{
|
||||
_handler = new DeactivateRolCommandHandler(_repository);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_NonExistentCodigo_ThrowsRolNotFoundException()
|
||||
{
|
||||
_repository.GetByCodigoAsync("missing").Returns((Rol?)null);
|
||||
|
||||
var ex = await Assert.ThrowsAsync<RolNotFoundException>(
|
||||
() => _handler.Handle(new DeactivateRolCommand("missing")));
|
||||
|
||||
Assert.Equal("missing", ex.Codigo);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_CodigoConUsuariosActivos_ThrowsRolInUseException()
|
||||
{
|
||||
_repository.GetByCodigoAsync("cajero").Returns(RolActive("cajero"));
|
||||
_repository.HasActiveUsuariosAsync("cajero").Returns(true);
|
||||
|
||||
var ex = await Assert.ThrowsAsync<RolInUseException>(
|
||||
() => _handler.Handle(new DeactivateRolCommand("cajero")));
|
||||
|
||||
Assert.Equal("cajero", ex.Codigo);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_RolInUse_DoesNotCallUpdateAsync()
|
||||
{
|
||||
_repository.GetByCodigoAsync("cajero").Returns(RolActive("cajero"));
|
||||
_repository.HasActiveUsuariosAsync("cajero").Returns(true);
|
||||
|
||||
try { await _handler.Handle(new DeactivateRolCommand("cajero")); } catch (RolInUseException) { }
|
||||
|
||||
await _repository.DidNotReceive().UpdateAsync(
|
||||
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string?>(), Arg.Any<bool>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_Happy_SetsActivoFalseAndReturnsDto()
|
||||
{
|
||||
var now = new DateTime(2026, 4, 15, 10, 0, 0, DateTimeKind.Utc);
|
||||
var afterDeactivation = new DateTime(2026, 4, 15, 13, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
_repository.GetByCodigoAsync("reportes")
|
||||
.Returns(
|
||||
RolActive("reportes", 20),
|
||||
new Rol(20, "reportes", "Nombre", "Desc", false, now, afterDeactivation));
|
||||
_repository.HasActiveUsuariosAsync("reportes").Returns(false);
|
||||
_repository.UpdateAsync("reportes", "Nombre", "Desc", false, Arg.Any<CancellationToken>())
|
||||
.Returns(true);
|
||||
|
||||
var dto = await _handler.Handle(new DeactivateRolCommand("reportes"));
|
||||
|
||||
Assert.Equal(20, dto.Id);
|
||||
Assert.False(dto.Activo);
|
||||
Assert.Equal(afterDeactivation, dto.FechaModificacion);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_Happy_CallsUpdateAsyncWithActivoFalse()
|
||||
{
|
||||
var now = new DateTime(2026, 4, 15, 10, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
_repository.GetByCodigoAsync("reportes")
|
||||
.Returns(
|
||||
RolActive("reportes"),
|
||||
new Rol(10, "reportes", "Nombre", "Desc", false, now, now));
|
||||
_repository.HasActiveUsuariosAsync("reportes").Returns(false);
|
||||
_repository.UpdateAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string?>(), Arg.Any<bool>(), Arg.Any<CancellationToken>())
|
||||
.Returns(true);
|
||||
|
||||
await _handler.Handle(new DeactivateRolCommand("reportes"));
|
||||
|
||||
await _repository.Received(1).UpdateAsync("reportes", "Nombre", "Desc", false, Arg.Any<CancellationToken>());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using NSubstitute;
|
||||
using SIGCM2.Application.Abstractions.Persistence;
|
||||
using SIGCM2.Application.Roles.Get;
|
||||
using SIGCM2.Domain.Entities;
|
||||
using SIGCM2.Domain.Exceptions;
|
||||
|
||||
namespace SIGCM2.Application.Tests.Roles.Get;
|
||||
|
||||
public class GetRolByCodigoQueryHandlerTests
|
||||
{
|
||||
private readonly IRolRepository _repository = Substitute.For<IRolRepository>();
|
||||
private readonly GetRolByCodigoQueryHandler _handler;
|
||||
|
||||
public GetRolByCodigoQueryHandlerTests()
|
||||
{
|
||||
_handler = new GetRolByCodigoQueryHandler(_repository);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ExistingCodigo_ReturnsDto()
|
||||
{
|
||||
var now = new DateTime(2026, 4, 15, 10, 0, 0, DateTimeKind.Utc);
|
||||
_repository.GetByCodigoAsync("cajero").Returns(new Rol(5, "cajero", "Cajero", "Desc", true, now, null));
|
||||
|
||||
var dto = await _handler.Handle(new GetRolByCodigoQuery("cajero"));
|
||||
|
||||
Assert.Equal(5, dto.Id);
|
||||
Assert.Equal("cajero", dto.Codigo);
|
||||
Assert.Equal("Cajero", dto.Nombre);
|
||||
Assert.True(dto.Activo);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_NonExistentCodigo_ThrowsRolNotFoundException()
|
||||
{
|
||||
_repository.GetByCodigoAsync("missing").Returns((Rol?)null);
|
||||
|
||||
var ex = await Assert.ThrowsAsync<RolNotFoundException>(
|
||||
() => _handler.Handle(new GetRolByCodigoQuery("missing")));
|
||||
|
||||
Assert.Equal("missing", ex.Codigo);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
using NSubstitute;
|
||||
using SIGCM2.Application.Abstractions.Persistence;
|
||||
using SIGCM2.Application.Roles.List;
|
||||
using SIGCM2.Domain.Entities;
|
||||
|
||||
namespace SIGCM2.Application.Tests.Roles.List;
|
||||
|
||||
public class ListRolesQueryHandlerTests
|
||||
{
|
||||
private readonly IRolRepository _repository = Substitute.For<IRolRepository>();
|
||||
private readonly ListRolesQueryHandler _handler;
|
||||
|
||||
public ListRolesQueryHandlerTests()
|
||||
{
|
||||
_handler = new ListRolesQueryHandler(_repository);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ReturnsAllRolesFromRepositoryAsDtos()
|
||||
{
|
||||
var now = new DateTime(2026, 4, 15, 10, 0, 0, DateTimeKind.Utc);
|
||||
_repository.ListAsync().Returns(new List<Rol>
|
||||
{
|
||||
new(1, "admin", "Administrador", "Desc admin", true, now, null),
|
||||
new(2, "cajero", "Cajero", "Desc cajero", true, now, null),
|
||||
});
|
||||
|
||||
var result = await _handler.Handle(new ListRolesQuery());
|
||||
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.Equal("admin", result[0].Codigo);
|
||||
Assert.Equal("Administrador", result[0].Nombre);
|
||||
Assert.Equal("cajero", result[1].Codigo);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_IncludesInactiveRoles()
|
||||
{
|
||||
var now = new DateTime(2026, 4, 15, 10, 0, 0, DateTimeKind.Utc);
|
||||
_repository.ListAsync().Returns(new List<Rol>
|
||||
{
|
||||
new(1, "active_code", "Activo", null, true, now, null),
|
||||
new(2, "inactive_code", "Inactivo", null, false, now, null),
|
||||
});
|
||||
|
||||
var result = await _handler.Handle(new ListRolesQuery());
|
||||
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.True(result[0].Activo);
|
||||
Assert.False(result[1].Activo);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_EmptyRepository_ReturnsEmptyList()
|
||||
{
|
||||
_repository.ListAsync().Returns(new List<Rol>());
|
||||
|
||||
var result = await _handler.Handle(new ListRolesQuery());
|
||||
|
||||
Assert.Empty(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
using NSubstitute;
|
||||
using SIGCM2.Application.Abstractions.Persistence;
|
||||
using SIGCM2.Application.Roles.Update;
|
||||
using SIGCM2.Domain.Entities;
|
||||
using SIGCM2.Domain.Exceptions;
|
||||
|
||||
namespace SIGCM2.Application.Tests.Roles.Update;
|
||||
|
||||
public class UpdateRolCommandHandlerTests
|
||||
{
|
||||
private readonly IRolRepository _repository = Substitute.For<IRolRepository>();
|
||||
private readonly UpdateRolCommandHandler _handler;
|
||||
|
||||
public UpdateRolCommandHandlerTests()
|
||||
{
|
||||
_handler = new UpdateRolCommandHandler(_repository);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_NonExistentCodigo_ThrowsRolNotFoundException()
|
||||
{
|
||||
_repository.UpdateAsync("missing", Arg.Any<string>(), Arg.Any<string?>(), Arg.Any<bool>(), Arg.Any<CancellationToken>())
|
||||
.Returns(false);
|
||||
|
||||
var ex = await Assert.ThrowsAsync<RolNotFoundException>(
|
||||
() => _handler.Handle(new UpdateRolCommand("missing", "X", null, true)));
|
||||
|
||||
Assert.Equal("missing", ex.Codigo);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_Happy_ReturnsDtoWithUpdatedFields()
|
||||
{
|
||||
var fechaCreacion = new DateTime(2026, 4, 10, 9, 0, 0, DateTimeKind.Utc);
|
||||
var fechaModificacion = new DateTime(2026, 4, 15, 12, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
_repository.UpdateAsync("cajero", "Cajero V2", "Desc V2", true, Arg.Any<CancellationToken>())
|
||||
.Returns(true);
|
||||
_repository.GetByCodigoAsync("cajero")
|
||||
.Returns(new Rol(10, "cajero", "Cajero V2", "Desc V2", true, fechaCreacion, fechaModificacion));
|
||||
|
||||
var dto = await _handler.Handle(new UpdateRolCommand("cajero", "Cajero V2", "Desc V2", true));
|
||||
|
||||
Assert.Equal(10, dto.Id);
|
||||
Assert.Equal("Cajero V2", dto.Nombre);
|
||||
Assert.Equal("Desc V2", dto.Descripcion);
|
||||
Assert.True(dto.Activo);
|
||||
Assert.Equal(fechaModificacion, dto.FechaModificacion);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_Happy_CallsUpdateAsyncWithExactFields()
|
||||
{
|
||||
var now = new DateTime(2026, 4, 15, 12, 0, 0, DateTimeKind.Utc);
|
||||
_repository.UpdateAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string?>(), Arg.Any<bool>(), Arg.Any<CancellationToken>())
|
||||
.Returns(true);
|
||||
_repository.GetByCodigoAsync("cajero")
|
||||
.Returns(new Rol(1, "cajero", "X", null, false, now, now));
|
||||
|
||||
await _handler.Handle(new UpdateRolCommand("cajero", "X", null, false));
|
||||
|
||||
await _repository.Received(1).UpdateAsync("cajero", "X", null, false, Arg.Any<CancellationToken>());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using FluentValidation.TestHelper;
|
||||
using SIGCM2.Application.Roles.Update;
|
||||
|
||||
namespace SIGCM2.Application.Tests.Roles.Update;
|
||||
|
||||
public class UpdateRolCommandValidatorTests
|
||||
{
|
||||
private static UpdateRolCommandValidator BuildValidator() => new();
|
||||
private static UpdateRolCommand Valid() => new("cajero", "Cajero Updated", "Desc updated", true);
|
||||
|
||||
[Fact]
|
||||
public void Validate_Valid_NoErrors()
|
||||
{
|
||||
BuildValidator().TestValidate(Valid()).ShouldNotHaveAnyValidationErrors();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_EmptyCodigo_HasError()
|
||||
{
|
||||
BuildValidator().TestValidate(Valid() with { Codigo = "" })
|
||||
.ShouldHaveValidationErrorFor(c => c.Codigo);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_EmptyNombre_HasError()
|
||||
{
|
||||
BuildValidator().TestValidate(Valid() with { Nombre = "" })
|
||||
.ShouldHaveValidationErrorFor(c => c.Nombre);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_NombreTooLong_HasError()
|
||||
{
|
||||
BuildValidator().TestValidate(Valid() with { Nombre = new string('a', 61) })
|
||||
.ShouldHaveValidationErrorFor(c => c.Nombre);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_NullDescripcion_Allowed()
|
||||
{
|
||||
BuildValidator().TestValidate(Valid() with { Descripcion = null })
|
||||
.ShouldNotHaveValidationErrorFor(c => c.Descripcion);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_DescripcionTooLong_HasError()
|
||||
{
|
||||
BuildValidator().TestValidate(Valid() with { Descripcion = new string('a', 251) })
|
||||
.ShouldHaveValidationErrorFor(c => c.Descripcion);
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ public class CreateUsuarioCommandValidatorTests
|
||||
Nombre: "Juan",
|
||||
Apellido: "Pérez",
|
||||
Email: null,
|
||||
Rol: "vendedor");
|
||||
Rol: "cajero");
|
||||
|
||||
// ── Happy paths ──────────────────────────────────────────────────────────
|
||||
|
||||
@@ -156,9 +156,13 @@ public class CreateUsuarioCommandValidatorTests
|
||||
|
||||
[Theory]
|
||||
[InlineData("admin")]
|
||||
[InlineData("vendedor")]
|
||||
[InlineData("tasador")]
|
||||
[InlineData("consulta")]
|
||||
[InlineData("cajero")]
|
||||
[InlineData("operador_ctacte")]
|
||||
[InlineData("picadora")]
|
||||
[InlineData("jefe_publicidad")]
|
||||
[InlineData("productor")]
|
||||
[InlineData("diagramacion")]
|
||||
[InlineData("reportes")]
|
||||
public void Validate_ValidRoles_NoError(string rol)
|
||||
{
|
||||
var cmd = ValidCommand() with { Rol = rol };
|
||||
|
||||
Reference in New Issue
Block a user