feat(application): IProductTypeRepository + IProductQueryRepository stub + queries (PRD-001)

This commit is contained in:
2026-04-19 09:38:51 -03:00
parent 132d17c99f
commit 3c9e852379
13 changed files with 375 additions and 0 deletions

View File

@@ -0,0 +1,70 @@
using FluentAssertions;
using NSubstitute;
using SIGCM2.Application.Abstractions.Persistence;
using SIGCM2.Application.ProductTypes.GetById;
using SIGCM2.Domain.Entities;
using SIGCM2.Domain.Exceptions;
namespace SIGCM2.Application.Tests.ProductTypes.GetById;
public class GetProductTypeByIdQueryHandlerTests
{
private readonly IProductTypeRepository _repo = Substitute.For<IProductTypeRepository>();
private readonly GetProductTypeByIdQueryHandler _handler;
public GetProductTypeByIdQueryHandlerTests()
{
_handler = new GetProductTypeByIdQueryHandler(_repo);
}
private static ProductType FullProductType(int id = 5) =>
new(id, "Clasificados",
hasDuration: true, requiresText: true, requiresCategory: false, isBundle: false,
allowImages: true, maxImages: 10, maxImageSizeMB: 2.5m, maxImageWidth: 1920, maxImageHeight: 1080,
isActive: true,
fechaCreacion: new DateTime(2026, 1, 10, 0, 0, 0, DateTimeKind.Utc),
fechaModificacion: new DateTime(2026, 3, 5, 0, 0, 0, DateTimeKind.Utc));
[Fact]
public async Task Handle_Found_ReturnsDetailDto()
{
_repo.GetByIdAsync(5, Arg.Any<CancellationToken>()).Returns(FullProductType(5));
var result = await _handler.Handle(new GetProductTypeByIdQuery(5));
result.Id.Should().Be(5);
result.Nombre.Should().Be("Clasificados");
}
[Fact]
public async Task Handle_NotFound_ThrowsProductTypeNotFoundException()
{
_repo.GetByIdAsync(999, Arg.Any<CancellationToken>()).Returns((ProductType?)null);
var act = async () => await _handler.Handle(new GetProductTypeByIdQuery(999));
await act.Should().ThrowAsync<ProductTypeNotFoundException>()
.Where(e => e.ProductTypeId == 999);
}
[Fact]
public async Task Handle_DetailContainsAllFields()
{
_repo.GetByIdAsync(5, Arg.Any<CancellationToken>()).Returns(FullProductType(5));
var result = await _handler.Handle(new GetProductTypeByIdQuery(5));
result.HasDuration.Should().BeTrue();
result.RequiresText.Should().BeTrue();
result.RequiresCategory.Should().BeFalse();
result.IsBundle.Should().BeFalse();
result.AllowImages.Should().BeTrue();
result.MaxImages.Should().Be(10);
result.MaxImageSizeMB.Should().Be(2.5m);
result.MaxImageWidth.Should().Be(1920);
result.MaxImageHeight.Should().Be(1080);
result.IsActive.Should().BeTrue();
result.FechaCreacion.Should().Be(new DateTime(2026, 1, 10, 0, 0, 0, DateTimeKind.Utc));
result.FechaModificacion.Should().Be(new DateTime(2026, 3, 5, 0, 0, 0, DateTimeKind.Utc));
}
}

View File

@@ -0,0 +1,109 @@
using FluentAssertions;
using NSubstitute;
using SIGCM2.Application.Abstractions.Persistence;
using SIGCM2.Application.Common;
using SIGCM2.Application.ProductTypes.List;
using SIGCM2.Domain.Entities;
namespace SIGCM2.Application.Tests.ProductTypes.List;
public class ListProductTypesQueryHandlerTests
{
private readonly IProductTypeRepository _repo = Substitute.For<IProductTypeRepository>();
private readonly ListProductTypesQueryHandler _handler;
public ListProductTypesQueryHandlerTests()
{
_handler = new ListProductTypesQueryHandler(_repo);
}
private static ProductType MakeProductType(int id, string nombre, bool isActive = true) =>
new(id, nombre, false, false, false, false, false, null, null, null, null,
isActive, DateTime.UtcNow, null);
private static PagedResult<ProductType> PagedOf(List<ProductType> items, int page = 1, int pageSize = 20, int? total = null) =>
new(items, page, pageSize, total ?? items.Count);
// ── Happy path ───────────────────────────────────────────────────────────
[Fact]
public async Task Handle_DefaultQuery_Returns20Active()
{
var items = Enumerable.Range(1, 5).Select(i => MakeProductType(i, $"Tipo{i}")).ToList();
_repo.GetPagedAsync(Arg.Any<ProductTypesQuery>(), Arg.Any<CancellationToken>())
.Returns(PagedOf(items));
var result = await _handler.Handle(new ListProductTypesQuery());
result.Items.Should().HaveCount(5);
}
[Fact]
public async Task Handle_PageLessThan1_ClampsToOne()
{
_repo.GetPagedAsync(Arg.Any<ProductTypesQuery>(), Arg.Any<CancellationToken>())
.Returns(PagedOf([]));
await _handler.Handle(new ListProductTypesQuery(Page: 0));
await _repo.Received(1).GetPagedAsync(
Arg.Is<ProductTypesQuery>(q => q.Page == 1),
Arg.Any<CancellationToken>());
}
[Fact]
public async Task Handle_PageSizeOver100_ClampsTo100()
{
_repo.GetPagedAsync(Arg.Any<ProductTypesQuery>(), Arg.Any<CancellationToken>())
.Returns(PagedOf([]));
await _handler.Handle(new ListProductTypesQuery(PageSize: 200));
await _repo.Received(1).GetPagedAsync(
Arg.Is<ProductTypesQuery>(q => q.PageSize == 100),
Arg.Any<CancellationToken>());
}
[Fact]
public async Task Handle_ActivoFalse_ReturnsInactives()
{
var inactive = new List<ProductType> { MakeProductType(99, "Inactivo", false) };
_repo.GetPagedAsync(Arg.Any<ProductTypesQuery>(), Arg.Any<CancellationToken>())
.Returns(PagedOf(inactive));
var result = await _handler.Handle(new ListProductTypesQuery(Activo: false));
await _repo.Received(1).GetPagedAsync(
Arg.Is<ProductTypesQuery>(q => q.Activo == false),
Arg.Any<CancellationToken>());
result.Items.Should().HaveCount(1);
}
[Fact]
public async Task Handle_SearchFilter_PassesThroughToRepo()
{
_repo.GetPagedAsync(Arg.Any<ProductTypesQuery>(), Arg.Any<CancellationToken>())
.Returns(PagedOf([]));
await _handler.Handle(new ListProductTypesQuery(Search: "clasif"));
await _repo.Received(1).GetPagedAsync(
Arg.Is<ProductTypesQuery>(q => q.Search == "clasif"),
Arg.Any<CancellationToken>());
}
[Fact]
public async Task Handle_PagedResult_ReturnsItemsAndTotal()
{
var items = Enumerable.Range(1, 5).Select(i => MakeProductType(i, $"Tipo{i}")).ToList();
_repo.GetPagedAsync(Arg.Any<ProductTypesQuery>(), Arg.Any<CancellationToken>())
.Returns(new PagedResult<ProductType>(items, 2, 5, 15));
var result = await _handler.Handle(new ListProductTypesQuery(Page: 2, PageSize: 5));
result.Total.Should().Be(15);
result.Page.Should().Be(2);
result.PageSize.Should().Be(5);
result.Items.Should().HaveCount(5);
}
}

View File

@@ -0,0 +1,26 @@
using FluentAssertions;
using SIGCM2.Application.Products;
namespace SIGCM2.Application.Tests.ProductTypes;
public class NullProductQueryRepositoryTests
{
private readonly NullProductQueryRepository _sut = new();
[Fact]
public async Task ExistsActiveByProductTypeAsync_AlwaysReturnsFalse()
{
var result = await _sut.ExistsActiveByProductTypeAsync(productTypeId: 1);
result.Should().BeFalse();
}
[Fact]
public async Task ExistsActiveByProductTypeAsync_WithCancellationToken_DoesNotThrow()
{
using var cts = new CancellationTokenSource();
var act = async () => await _sut.ExistsActiveByProductTypeAsync(productTypeId: 999, ct: cts.Token);
await act.Should().NotThrowAsync();
}
}