diff --git a/src/api/SIGCM2.Application/Rubros/Create/CreateRubroCommandHandler.cs b/src/api/SIGCM2.Application/Rubros/Create/CreateRubroCommandHandler.cs index 7332524..f142877 100644 --- a/src/api/SIGCM2.Application/Rubros/Create/CreateRubroCommandHandler.cs +++ b/src/api/SIGCM2.Application/Rubros/Create/CreateRubroCommandHandler.cs @@ -14,17 +14,20 @@ public sealed class CreateRubroCommandHandler : ICommandHandler options) + IOptions options, + IAvisoQueryRepository avisoQuery) { _repo = repo; _audit = audit; _timeProvider = timeProvider; _options = options.Value; + _avisoQuery = avisoQuery; } public async Task Handle(CreateRubroCommand command) @@ -38,6 +41,12 @@ public sealed class CreateRubroCommandHandler : ICommandHandler 0) + throw new RubroPadreEsHojaConAvisosException(command.ParentId.Value, avisosCount); + // Depth check: parent's depth + 1 must not exceed MaxDepth var parentDepth = await _repo.GetDepthAsync(command.ParentId); var newDepth = parentDepth + 1; diff --git a/tests/SIGCM2.Application.Tests/Rubros/Create/CreateRubroCommandHandlerTests.cs b/tests/SIGCM2.Application.Tests/Rubros/Create/CreateRubroCommandHandlerTests.cs index f4b4798..6181380 100644 --- a/tests/SIGCM2.Application.Tests/Rubros/Create/CreateRubroCommandHandlerTests.cs +++ b/tests/SIGCM2.Application.Tests/Rubros/Create/CreateRubroCommandHandlerTests.cs @@ -18,6 +18,7 @@ public class CreateRubroCommandHandlerTests private readonly IAuditLogger _audit = Substitute.For(); private readonly FakeTimeProvider _timeProvider = new(new DateTimeOffset(2026, 4, 18, 12, 0, 0, TimeSpan.Zero)); private readonly IOptions _options = Options.Create(new RubrosOptions { MaxDepth = 10 }); + private readonly IAvisoQueryRepository _avisoQuery = Substitute.For(); private readonly CreateRubroCommandHandler _handler; public CreateRubroCommandHandlerTests() @@ -30,8 +31,11 @@ public class CreateRubroCommandHandlerTests .Returns(1); _repo.GetDepthAsync(Arg.Any(), Arg.Any()) .Returns(0); + // Default: no avisos (stub behavior) + _avisoQuery.CountAvisosEnRubroAsync(Arg.Any(), Arg.Any()) + .Returns(0); - _handler = new CreateRubroCommandHandler(_repo, _audit, _timeProvider, _options); + _handler = new CreateRubroCommandHandler(_repo, _audit, _timeProvider, _options, _avisoQuery); } private static CreateRubroCommand RootCommand() => new("Autos", ParentId: null, TarifarioBaseId: null); @@ -173,4 +177,76 @@ public class CreateRubroCommandHandlerTests result.Id.Should().Be(99); } + + // ── CAT-002: Guard padre sin avisos ────────────────────────────────────── + + [Fact] + public async Task Handle_ParentTieneAvisos_Throws_RubroPadreEsHojaConAvisosException() + { + const int parentId = 5; + var parent = new Rubro(parentId, null, "ParentConAvisos", 0, activo: true, tarifarioBaseId: null, + fechaCreacion: DateTime.UtcNow, fechaModificacion: null); + _repo.GetByIdAsync(parentId, Arg.Any()).Returns(parent); + _avisoQuery.CountAvisosEnRubroAsync(parentId, Arg.Any()).Returns(3); + + var act = () => _handler.Handle(ChildCommand(parentId: parentId)); + + await act.Should().ThrowAsync() + .Where(ex => ex.ParentId == parentId && ex.CantidadAvisos == 3); + } + + [Fact] + public async Task Handle_ParentTieneCeroAvisos_DoesNotThrow() + { + const int parentId = 5; + var parent = new Rubro(parentId, null, "ParentSinAvisos", 0, activo: true, tarifarioBaseId: null, + fechaCreacion: DateTime.UtcNow, fechaModificacion: null); + _repo.GetByIdAsync(parentId, Arg.Any()).Returns(parent); + _avisoQuery.CountAvisosEnRubroAsync(parentId, Arg.Any()).Returns(0); + _repo.AddAsync(Arg.Any(), Arg.Any()).Returns(10); + + var result = await _handler.Handle(ChildCommand(parentId: parentId)); + + result.Id.Should().Be(10); + } + + [Fact] + public async Task Handle_ParentNull_SkipsAvisosGuard() + { + // Root creation — no parent → CountAvisosEnRubroAsync should NOT be called + await _handler.Handle(RootCommand()); + + await _avisoQuery.DidNotReceive().CountAvisosEnRubroAsync(Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task Handle_GuardOrder_ParentInactivo_Wins_OverAvisos() + { + // Inactive parent with avisos → RubroPadreInactivoException (not avisos exception) + const int parentId = 7; + var inactiveParent = new Rubro(parentId, null, "InactivoConAvisos", 0, activo: false, tarifarioBaseId: null, + fechaCreacion: DateTime.UtcNow, fechaModificacion: null); + _repo.GetByIdAsync(parentId, Arg.Any()).Returns(inactiveParent); + _avisoQuery.CountAvisosEnRubroAsync(parentId, Arg.Any()).Returns(3); + + var act = () => _handler.Handle(ChildCommand(parentId: parentId)); + + await act.Should().ThrowAsync(); + } + + [Fact] + public async Task Handle_GuardOrder_Avisos_Wins_OverDepth() + { + // Parent at MAX_DEPTH AND has avisos → RubroPadreEsHojaConAvisosException (avisos guard fires first) + const int parentId = 5; + var parent = new Rubro(parentId, null, "ParentAtMaxDepth", 0, activo: true, tarifarioBaseId: null, + fechaCreacion: DateTime.UtcNow, fechaModificacion: null); + _repo.GetByIdAsync(parentId, Arg.Any()).Returns(parent); + _avisoQuery.CountAvisosEnRubroAsync(parentId, Arg.Any()).Returns(2); + _repo.GetDepthAsync(parentId, Arg.Any()).Returns(10); // at MaxDepth + + var act = () => _handler.Handle(ChildCommand(parentId: parentId)); + + await act.Should().ThrowAsync(); + } }