From f861dfa826da8d5af84142823074c1d4d2477182 Mon Sep 17 00:00:00 2001 From: dmolinari Date: Sun, 19 Apr 2026 08:25:13 -0300 Subject: [PATCH] feat(application): RubroTreeBuilder + GetRubroTree con tieneAvisos (CAT-002) --- .../GetTree/GetRubroTreeQueryHandler.cs | 10 ++- .../GetTree/GetRubroTreeQueryHandlerTests.cs | 76 ++++++++++++++++++- 2 files changed, 80 insertions(+), 6 deletions(-) diff --git a/src/api/SIGCM2.Application/Rubros/GetTree/GetRubroTreeQueryHandler.cs b/src/api/SIGCM2.Application/Rubros/GetTree/GetRubroTreeQueryHandler.cs index 884b869..26e0de1 100644 --- a/src/api/SIGCM2.Application/Rubros/GetTree/GetRubroTreeQueryHandler.cs +++ b/src/api/SIGCM2.Application/Rubros/GetTree/GetRubroTreeQueryHandler.cs @@ -8,16 +8,20 @@ namespace SIGCM2.Application.Rubros.GetTree; public sealed class GetRubroTreeQueryHandler : ICommandHandler> { private readonly IRubroRepository _repo; + private readonly IAvisoQueryRepository _avisoQuery; - public GetRubroTreeQueryHandler(IRubroRepository repo) + public GetRubroTreeQueryHandler(IRubroRepository repo, IAvisoQueryRepository avisoQuery) { _repo = repo; + _avisoQuery = avisoQuery; } public async Task> Handle(GetRubroTreeQuery query) { var all = await _repo.GetAllAsync(query.IncluirInactivos); - // CAT-002: avisoCounts injected via IAvisoQueryRepository (wired in Batch 6) - return RubroTreeBuilder.Build(all, query.IncluirInactivos, new Dictionary()); + var ids = all.Select(r => r.Id).ToList(); + // CAT-002: single batch call — avoids N+1 when PRD-002 activates the real implementation + var avisoCounts = await _avisoQuery.CountAvisosBatchAsync(ids); + return RubroTreeBuilder.Build(all, query.IncluirInactivos, avisoCounts); } } diff --git a/tests/SIGCM2.Application.Tests/Rubros/GetTree/GetRubroTreeQueryHandlerTests.cs b/tests/SIGCM2.Application.Tests/Rubros/GetTree/GetRubroTreeQueryHandlerTests.cs index e79edbb..00e4150 100644 --- a/tests/SIGCM2.Application.Tests/Rubros/GetTree/GetRubroTreeQueryHandlerTests.cs +++ b/tests/SIGCM2.Application.Tests/Rubros/GetTree/GetRubroTreeQueryHandlerTests.cs @@ -2,6 +2,7 @@ using FluentAssertions; using Microsoft.Extensions.Time.Testing; using NSubstitute; using SIGCM2.Application.Abstractions.Persistence; +using SIGCM2.Application.Avisos; using SIGCM2.Application.Rubros.GetById; using SIGCM2.Application.Rubros.GetTree; using SIGCM2.Domain.Entities; @@ -13,6 +14,7 @@ public class GetRubroTreeQueryHandlerTests { private static readonly FakeTimeProvider FakeTime = new(new DateTimeOffset(2026, 4, 18, 12, 0, 0, TimeSpan.Zero)); private readonly IRubroRepository _repo = Substitute.For(); + private readonly IAvisoQueryRepository _avisoQuery = Substitute.For(); private static Rubro MakeRubro(int id, int? parentId = null, bool activo = true) => new(id, parentId, $"Rubro{id}", 0, activo, null, FakeTime.GetUtcNow().UtcDateTime, null); @@ -24,8 +26,10 @@ public class GetRubroTreeQueryHandlerTests { _repo.GetAllAsync(false, Arg.Any()) .Returns(new[] { MakeRubro(1), MakeRubro(2) }); + _avisoQuery.CountAvisosBatchAsync(Arg.Any>(), Arg.Any()) + .Returns(new Dictionary()); - var handler = new GetRubroTreeQueryHandler(_repo); + var handler = new GetRubroTreeQueryHandler(_repo, _avisoQuery); var result = await handler.Handle(new GetRubroTreeQuery(IncluirInactivos: false)); result.Should().HaveCount(2); @@ -36,8 +40,10 @@ public class GetRubroTreeQueryHandlerTests { _repo.GetAllAsync(true, Arg.Any()) .Returns(new[] { MakeRubro(1), MakeRubro(2, activo: false) }); + _avisoQuery.CountAvisosBatchAsync(Arg.Any>(), Arg.Any()) + .Returns(new Dictionary()); - var handler = new GetRubroTreeQueryHandler(_repo); + var handler = new GetRubroTreeQueryHandler(_repo, _avisoQuery); var result = await handler.Handle(new GetRubroTreeQuery(IncluirInactivos: true)); await _repo.Received(1).GetAllAsync(true, Arg.Any()); @@ -49,13 +55,77 @@ public class GetRubroTreeQueryHandlerTests { _repo.GetAllAsync(Arg.Any(), Arg.Any()) .Returns(Array.Empty()); + _avisoQuery.CountAvisosBatchAsync(Arg.Any>(), Arg.Any()) + .Returns(new Dictionary()); - var handler = new GetRubroTreeQueryHandler(_repo); + var handler = new GetRubroTreeQueryHandler(_repo, _avisoQuery); var result = await handler.Handle(new GetRubroTreeQuery(IncluirInactivos: false)); result.Should().BeEmpty(); } + // ── CAT-002: TieneAvisos populated via batch ───────────────────────────── + + [Fact] + public async Task Handle_PopulatesTieneAvisos_True_WhenBatchResultContainsCount() + { + _repo.GetAllAsync(false, Arg.Any()) + .Returns(new[] { MakeRubro(1), MakeRubro(2), MakeRubro(3) }); + _avisoQuery.CountAvisosBatchAsync(Arg.Any>(), Arg.Any()) + .Returns(new Dictionary { { 1, 2 } }); + + var handler = new GetRubroTreeQueryHandler(_repo, _avisoQuery); + var result = await handler.Handle(new GetRubroTreeQuery(IncluirInactivos: false)); + + result.Single(n => n.Id == 1).TieneAvisos.Should().BeTrue(); + result.Single(n => n.Id == 2).TieneAvisos.Should().BeFalse(); + result.Single(n => n.Id == 3).TieneAvisos.Should().BeFalse(); + } + + [Fact] + public async Task Handle_CallsBatchExactlyOnce_WithAllRubroIds() + { + _repo.GetAllAsync(false, Arg.Any()) + .Returns(new[] { MakeRubro(1), MakeRubro(2), MakeRubro(3) }); + _avisoQuery.CountAvisosBatchAsync(Arg.Any>(), Arg.Any()) + .Returns(new Dictionary()); + + var handler = new GetRubroTreeQueryHandler(_repo, _avisoQuery); + await handler.Handle(new GetRubroTreeQuery(IncluirInactivos: false)); + + await _avisoQuery.Received(1).CountAvisosBatchAsync( + Arg.Any>(), + Arg.Any()); + } + + [Fact] + public async Task Handle_EmptyTree_CallsBatchWithEmptyList() + { + _repo.GetAllAsync(Arg.Any(), Arg.Any()) + .Returns(Array.Empty()); + _avisoQuery.CountAvisosBatchAsync(Arg.Any>(), Arg.Any()) + .Returns(new Dictionary()); + + var handler = new GetRubroTreeQueryHandler(_repo, _avisoQuery); + await handler.Handle(new GetRubroTreeQuery(IncluirInactivos: false)); + + await _avisoQuery.Received(1).CountAvisosBatchAsync( + Arg.Is>(ids => ids.Count == 0), + Arg.Any()); + } + + [Fact] + public async Task Handle_StubBehavior_AllNodesTieneAvisosFalse() + { + _repo.GetAllAsync(false, Arg.Any()) + .Returns(new[] { MakeRubro(1), MakeRubro(2) }); + + var handler = new GetRubroTreeQueryHandler(_repo, new NullAvisoQueryRepository()); + var result = await handler.Handle(new GetRubroTreeQuery(IncluirInactivos: false)); + + result.Should().AllSatisfy(n => n.TieneAvisos.Should().BeFalse()); + } + // ── GetRubroByIdQueryHandler ───────────────────────────────────────────── [Fact]