feat(application): guard avisos en MoveRubroCommandHandler (CAT-002)
This commit is contained in:
@@ -17,6 +17,7 @@ public class MoveRubroCommandHandlerTests
|
||||
private readonly IAuditLogger _audit = Substitute.For<IAuditLogger>();
|
||||
private readonly FakeTimeProvider _timeProvider = new(new DateTimeOffset(2026, 4, 18, 12, 0, 0, TimeSpan.Zero));
|
||||
private readonly IOptions<RubrosOptions> _options = Options.Create(new RubrosOptions { MaxDepth = 10 });
|
||||
private readonly IAvisoQueryRepository _avisoQuery = Substitute.For<IAvisoQueryRepository>();
|
||||
private readonly MoveRubroCommandHandler _handler;
|
||||
|
||||
private static Rubro MakeRubro(int id, int? parentId = null, bool activo = true)
|
||||
@@ -32,8 +33,11 @@ public class MoveRubroCommandHandlerTests
|
||||
.Returns(0);
|
||||
_repo.GetMaxOrdenAsync(Arg.Any<int?>(), Arg.Any<CancellationToken>())
|
||||
.Returns(0);
|
||||
// Default: no avisos
|
||||
_avisoQuery.CountAvisosEnRubroAsync(Arg.Any<int>(), Arg.Any<CancellationToken>())
|
||||
.Returns(0);
|
||||
|
||||
_handler = new MoveRubroCommandHandler(_repo, _audit, _timeProvider, _options);
|
||||
_handler = new MoveRubroCommandHandler(_repo, _audit, _timeProvider, _options, _avisoQuery);
|
||||
}
|
||||
|
||||
// ── Happy path: move to other parent ────────────────────────────────────
|
||||
@@ -173,4 +177,66 @@ public class MoveRubroCommandHandlerTests
|
||||
|
||||
await act.Should().ThrowAsync<RubroMaxDepthExceededException>();
|
||||
}
|
||||
|
||||
// ── CAT-002: Guard nuevo padre sin avisos ───────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_NuevoParentTieneAvisos_Throws_RubroPadreEsHojaConAvisosException()
|
||||
{
|
||||
const int nuevoParentId = 20;
|
||||
var rubro = MakeRubro(8, parentId: 2);
|
||||
var newParent = MakeRubro(nuevoParentId);
|
||||
_repo.GetByIdAsync(8, Arg.Any<CancellationToken>()).Returns(rubro);
|
||||
_repo.GetByIdAsync(nuevoParentId, Arg.Any<CancellationToken>()).Returns(newParent);
|
||||
_avisoQuery.CountAvisosEnRubroAsync(nuevoParentId, Arg.Any<CancellationToken>()).Returns(2);
|
||||
|
||||
var act = () => _handler.Handle(new MoveRubroCommand(Id: 8, NuevoParentId: nuevoParentId, NuevoOrden: 0));
|
||||
|
||||
await act.Should().ThrowAsync<RubroPadreEsHojaConAvisosException>()
|
||||
.Where(ex => ex.ParentId == nuevoParentId && ex.CantidadAvisos == 2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_NuevoParentTieneCeroAvisos_DoesNotThrow()
|
||||
{
|
||||
const int nuevoParentId = 20;
|
||||
var rubro = MakeRubro(8, parentId: 2);
|
||||
var newParent = MakeRubro(nuevoParentId);
|
||||
_repo.GetByIdAsync(8, Arg.Any<CancellationToken>()).Returns(rubro);
|
||||
_repo.GetByIdAsync(nuevoParentId, Arg.Any<CancellationToken>()).Returns(newParent);
|
||||
_avisoQuery.CountAvisosEnRubroAsync(nuevoParentId, Arg.Any<CancellationToken>()).Returns(0);
|
||||
|
||||
var result = await _handler.Handle(new MoveRubroCommand(Id: 8, NuevoParentId: nuevoParentId, NuevoOrden: 0));
|
||||
|
||||
result.Id.Should().Be(8);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_NuevoParentEsNull_SkipsAvisosGuard()
|
||||
{
|
||||
// Move to root — no parent to check avisos for
|
||||
var rubro = MakeRubro(8, parentId: 2);
|
||||
_repo.GetByIdAsync(8, Arg.Any<CancellationToken>()).Returns(rubro);
|
||||
|
||||
await _handler.Handle(new MoveRubroCommand(Id: 8, NuevoParentId: null, NuevoOrden: 0));
|
||||
|
||||
await _avisoQuery.DidNotReceive().CountAvisosEnRubroAsync(Arg.Any<int>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_CycleCheck_Wins_OverAvisosGuard()
|
||||
{
|
||||
// Cycle check fires before avisos guard
|
||||
const int nuevoParentId = 10;
|
||||
var rubro = MakeRubro(5, parentId: null);
|
||||
_repo.GetByIdAsync(5, Arg.Any<CancellationToken>()).Returns(rubro);
|
||||
// nuevoParentId IS a descendant (cycle)
|
||||
_repo.GetDescendantsAsync(5, Arg.Any<CancellationToken>())
|
||||
.Returns(new[] { MakeRubro(nuevoParentId, parentId: 5) });
|
||||
_avisoQuery.CountAvisosEnRubroAsync(nuevoParentId, Arg.Any<CancellationToken>()).Returns(3);
|
||||
|
||||
var act = () => _handler.Handle(new MoveRubroCommand(Id: 5, NuevoParentId: nuevoParentId, NuevoOrden: 0));
|
||||
|
||||
await act.Should().ThrowAsync<RubroCycleDetectedException>();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user