From a3f01bc6c97c9ee9b3ae10908186728dc7363f46 Mon Sep 17 00:00:00 2001 From: dmolinari Date: Thu, 16 Apr 2026 13:54:47 -0300 Subject: [PATCH] feat(audit): enchufar audit en handlers de Rol (UDT-010 B8) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 4 command handlers del módulo Roles + Permisos ahora auditan: | Handler | Action | |--------------------------------------|------------------------| | CreateRolCommandHandler | rol.create | | UpdateRolCommandHandler | rol.update | | DeactivateRolCommandHandler | rol.deactivate | | AssignPermisosToRolCommandHandler | rol.permisos_update | Mismo patrón que B7 (using block + post-commit reads outside scope). Metadata: - rol.create: after={Codigo, Nombre, Descripcion} - rol.update: {before, after} diff - rol.permisos_update: {before, after} con arrays de codigos ordenados AssignPermisosToRolCommandHandler captura 'before' leyendo GetByRolCodigoAsync antes del TransactionScope para poder emitir el diff. 4 test classes actualizados con mock de IAuditLogger. Suite: 378/378 Application.Tests + 141/141 Api.Tests = 519/519 passing. Refs: sdd/udt-010-auditoria-trazabilidad/{spec#REQ-RM-AUD, design, tasks#B8} --- .../AssignPermisosToRolCommandHandler.cs | 32 ++++++++++++++--- .../Roles/Create/CreateRolCommandHandler.cs | 24 +++++++++++-- .../Deactivate/DeactivateRolCommandHandler.cs | 27 +++++++++++--- .../Roles/Update/UpdateRolCommandHandler.cs | 35 ++++++++++++++++--- .../AssignPermisosToRolCommandHandlerTests.cs | 4 ++- .../Create/CreateRolCommandHandlerTests.cs | 4 ++- .../DeactivateRolCommandHandlerTests.cs | 4 ++- .../Update/UpdateRolCommandHandlerTests.cs | 4 ++- 8 files changed, 114 insertions(+), 20 deletions(-) diff --git a/src/api/SIGCM2.Application/Permisos/Assign/AssignPermisosToRolCommandHandler.cs b/src/api/SIGCM2.Application/Permisos/Assign/AssignPermisosToRolCommandHandler.cs index 5f705f7..87bab38 100644 --- a/src/api/SIGCM2.Application/Permisos/Assign/AssignPermisosToRolCommandHandler.cs +++ b/src/api/SIGCM2.Application/Permisos/Assign/AssignPermisosToRolCommandHandler.cs @@ -1,5 +1,7 @@ +using System.Transactions; using SIGCM2.Application.Abstractions; using SIGCM2.Application.Abstractions.Persistence; +using SIGCM2.Application.Audit; using SIGCM2.Application.Permisos.Dtos; using SIGCM2.Domain.Exceptions; @@ -10,15 +12,18 @@ public sealed class AssignPermisosToRolCommandHandler : ICommandHandler> Handle(AssignPermisosToRolCommand command) @@ -40,9 +45,28 @@ public sealed class AssignPermisosToRolCommandHandler : ICommandHandler p.Id); - await _rolPermisoRepository.ReplaceForRolAsync(rol.Id, permisoIds); + // Capture "before" snapshot for audit diff + var previousPermisos = await _rolPermisoRepository.GetByRolCodigoAsync(rol.Codigo); + var beforeCodigos = previousPermisos.Select(p => p.Codigo).OrderBy(c => c, StringComparer.Ordinal).ToArray(); + var afterCodigos = permisos.Select(p => p.Codigo).OrderBy(c => c, StringComparer.Ordinal).ToArray(); + + using (var tx = new TransactionScope( + TransactionScopeOption.Required, + new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted }, + TransactionScopeAsyncFlowOption.Enabled)) + { + // 3. Reemplazar el set (DELETE+INSERT en transacción dentro del repo) + var permisoIds = permisos.Select(p => p.Id); + await _rolPermisoRepository.ReplaceForRolAsync(rol.Id, permisoIds); + + await _audit.LogAsync( + action: "rol.permisos_update", + targetType: "Rol", + targetId: rol.Id.ToString(), + metadata: new { before = beforeCodigos, after = afterCodigos }); + + tx.Complete(); + } // 4. Retornar el nuevo set asignado return permisos diff --git a/src/api/SIGCM2.Application/Roles/Create/CreateRolCommandHandler.cs b/src/api/SIGCM2.Application/Roles/Create/CreateRolCommandHandler.cs index 072dab6..508fb8a 100644 --- a/src/api/SIGCM2.Application/Roles/Create/CreateRolCommandHandler.cs +++ b/src/api/SIGCM2.Application/Roles/Create/CreateRolCommandHandler.cs @@ -1,5 +1,7 @@ +using System.Transactions; using SIGCM2.Application.Abstractions; using SIGCM2.Application.Abstractions.Persistence; +using SIGCM2.Application.Audit; using SIGCM2.Application.Roles.Dtos; using SIGCM2.Domain.Entities; using SIGCM2.Domain.Exceptions; @@ -9,10 +11,12 @@ namespace SIGCM2.Application.Roles.Create; public sealed class CreateRolCommandHandler : ICommandHandler { private readonly IRolRepository _repository; + private readonly IAuditLogger _audit; - public CreateRolCommandHandler(IRolRepository repository) + public CreateRolCommandHandler(IRolRepository repository, IAuditLogger audit) { _repository = repository; + _audit = audit; } public async Task Handle(CreateRolCommand command) @@ -24,7 +28,23 @@ public sealed class CreateRolCommandHandler : ICommandHandler { private readonly IRolRepository _repository; + private readonly IAuditLogger _audit; - public DeactivateRolCommandHandler(IRolRepository repository) + public DeactivateRolCommandHandler(IRolRepository repository, IAuditLogger audit) { _repository = repository; + _audit = audit; } public async Task Handle(DeactivateRolCommand command) @@ -23,10 +27,23 @@ public sealed class DeactivateRolCommandHandler : ICommandHandler { private readonly IRolRepository _repository; + private readonly IAuditLogger _audit; - public UpdateRolCommandHandler(IRolRepository repository) + public UpdateRolCommandHandler(IRolRepository repository, IAuditLogger audit) { _repository = repository; + _audit = audit; } public async Task Handle(UpdateRolCommand command) { - var updated = await _repository.UpdateAsync( - command.Codigo, command.Nombre, command.Descripcion, command.Activo); + var before = await _repository.GetByCodigoAsync(command.Codigo) + ?? throw new RolNotFoundException(command.Codigo); - if (!updated) - throw new RolNotFoundException(command.Codigo); + using (var tx = new TransactionScope( + TransactionScopeOption.Required, + new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted }, + TransactionScopeAsyncFlowOption.Enabled)) + { + var updated = await _repository.UpdateAsync( + command.Codigo, command.Nombre, command.Descripcion, command.Activo); + + if (!updated) + throw new RolNotFoundException(command.Codigo); + + await _audit.LogAsync( + action: "rol.update", + targetType: "Rol", + targetId: before.Id.ToString(), + metadata: new + { + before = new { before.Nombre, before.Descripcion, before.Activo }, + after = new { command.Nombre, command.Descripcion, command.Activo }, + }); + + tx.Complete(); + } var rol = await _repository.GetByCodigoAsync(command.Codigo) ?? throw new RolNotFoundException(command.Codigo); diff --git a/tests/SIGCM2.Application.Tests/Permisos/Assign/AssignPermisosToRolCommandHandlerTests.cs b/tests/SIGCM2.Application.Tests/Permisos/Assign/AssignPermisosToRolCommandHandlerTests.cs index 54a9818..14fb6f8 100644 --- a/tests/SIGCM2.Application.Tests/Permisos/Assign/AssignPermisosToRolCommandHandlerTests.cs +++ b/tests/SIGCM2.Application.Tests/Permisos/Assign/AssignPermisosToRolCommandHandlerTests.cs @@ -1,5 +1,6 @@ using NSubstitute; using SIGCM2.Application.Abstractions.Persistence; +using SIGCM2.Application.Audit; using SIGCM2.Application.Permisos.Assign; using SIGCM2.Domain.Entities; using SIGCM2.Domain.Exceptions; @@ -11,11 +12,12 @@ public class AssignPermisosToRolCommandHandlerTests private readonly IRolRepository _rolRepository = Substitute.For(); private readonly IPermisoRepository _permisoRepository = Substitute.For(); private readonly IRolPermisoRepository _rolPermisoRepository = Substitute.For(); + private readonly IAuditLogger _audit = Substitute.For(); private readonly AssignPermisosToRolCommandHandler _handler; public AssignPermisosToRolCommandHandlerTests() { - _handler = new AssignPermisosToRolCommandHandler(_rolRepository, _permisoRepository, _rolPermisoRepository); + _handler = new AssignPermisosToRolCommandHandler(_rolRepository, _permisoRepository, _rolPermisoRepository, _audit); } private static Rol MakeRol(int id, string codigo) => diff --git a/tests/SIGCM2.Application.Tests/Roles/Create/CreateRolCommandHandlerTests.cs b/tests/SIGCM2.Application.Tests/Roles/Create/CreateRolCommandHandlerTests.cs index 04b47f2..8a1a5f9 100644 --- a/tests/SIGCM2.Application.Tests/Roles/Create/CreateRolCommandHandlerTests.cs +++ b/tests/SIGCM2.Application.Tests/Roles/Create/CreateRolCommandHandlerTests.cs @@ -1,5 +1,6 @@ using NSubstitute; using SIGCM2.Application.Abstractions.Persistence; +using SIGCM2.Application.Audit; using SIGCM2.Application.Roles.Create; using SIGCM2.Domain.Entities; using SIGCM2.Domain.Exceptions; @@ -9,13 +10,14 @@ namespace SIGCM2.Application.Tests.Roles.Create; public class CreateRolCommandHandlerTests { private readonly IRolRepository _repository = Substitute.For(); + private readonly IAuditLogger _audit = Substitute.For(); private readonly CreateRolCommandHandler _handler; private static CreateRolCommand ValidCommand() => new("cajero_senior", "Cajero Senior", "Con más permisos"); public CreateRolCommandHandlerTests() { - _handler = new CreateRolCommandHandler(_repository); + _handler = new CreateRolCommandHandler(_repository, _audit); } [Fact] diff --git a/tests/SIGCM2.Application.Tests/Roles/Deactivate/DeactivateRolCommandHandlerTests.cs b/tests/SIGCM2.Application.Tests/Roles/Deactivate/DeactivateRolCommandHandlerTests.cs index 71af502..a760006 100644 --- a/tests/SIGCM2.Application.Tests/Roles/Deactivate/DeactivateRolCommandHandlerTests.cs +++ b/tests/SIGCM2.Application.Tests/Roles/Deactivate/DeactivateRolCommandHandlerTests.cs @@ -1,5 +1,6 @@ using NSubstitute; using SIGCM2.Application.Abstractions.Persistence; +using SIGCM2.Application.Audit; using SIGCM2.Application.Roles.Deactivate; using SIGCM2.Domain.Entities; using SIGCM2.Domain.Exceptions; @@ -9,6 +10,7 @@ namespace SIGCM2.Application.Tests.Roles.Deactivate; public class DeactivateRolCommandHandlerTests { private readonly IRolRepository _repository = Substitute.For(); + private readonly IAuditLogger _audit = Substitute.For(); private readonly DeactivateRolCommandHandler _handler; private static Rol RolActive(string codigo, int id = 10) @@ -19,7 +21,7 @@ public class DeactivateRolCommandHandlerTests public DeactivateRolCommandHandlerTests() { - _handler = new DeactivateRolCommandHandler(_repository); + _handler = new DeactivateRolCommandHandler(_repository, _audit); } [Fact] diff --git a/tests/SIGCM2.Application.Tests/Roles/Update/UpdateRolCommandHandlerTests.cs b/tests/SIGCM2.Application.Tests/Roles/Update/UpdateRolCommandHandlerTests.cs index 9edb511..12a0014 100644 --- a/tests/SIGCM2.Application.Tests/Roles/Update/UpdateRolCommandHandlerTests.cs +++ b/tests/SIGCM2.Application.Tests/Roles/Update/UpdateRolCommandHandlerTests.cs @@ -1,5 +1,6 @@ using NSubstitute; using SIGCM2.Application.Abstractions.Persistence; +using SIGCM2.Application.Audit; using SIGCM2.Application.Roles.Update; using SIGCM2.Domain.Entities; using SIGCM2.Domain.Exceptions; @@ -9,11 +10,12 @@ namespace SIGCM2.Application.Tests.Roles.Update; public class UpdateRolCommandHandlerTests { private readonly IRolRepository _repository = Substitute.For(); + private readonly IAuditLogger _audit = Substitute.For(); private readonly UpdateRolCommandHandler _handler; public UpdateRolCommandHandlerTests() { - _handler = new UpdateRolCommandHandler(_repository); + _handler = new UpdateRolCommandHandler(_repository, _audit); } [Fact]