feat(application): IProductTypeRepository + IProductQueryRepository stub + queries (PRD-001)
This commit is contained in:
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user