feat(application): Product handlers + DI registration, fix permiso count to 27 (PRD-002)
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ExceptionExtensions;
|
||||
using SIGCM2.Application.Abstractions.Persistence;
|
||||
using SIGCM2.Application.Audit;
|
||||
using SIGCM2.Application.Products.Deactivate;
|
||||
using SIGCM2.Domain.Entities;
|
||||
using SIGCM2.Domain.Exceptions;
|
||||
|
||||
namespace SIGCM2.Application.Tests.Products.Deactivate;
|
||||
|
||||
/// <summary>
|
||||
/// PRD-002 — DeactivateProductCommandHandler tests.
|
||||
/// </summary>
|
||||
public class DeactivateProductCommandHandlerTests
|
||||
{
|
||||
private readonly IProductRepository _repo = Substitute.For<IProductRepository>();
|
||||
private readonly IAuditLogger _audit = Substitute.For<IAuditLogger>();
|
||||
private readonly FakeTimeProvider _time = new(new DateTimeOffset(2026, 4, 19, 14, 0, 0, TimeSpan.Zero));
|
||||
private readonly DeactivateProductCommandHandler _handler;
|
||||
|
||||
public DeactivateProductCommandHandlerTests()
|
||||
{
|
||||
_handler = new DeactivateProductCommandHandler(_repo, _audit, _time);
|
||||
}
|
||||
|
||||
private static Product ActiveProduct(int id = 1) => new(
|
||||
id: id, nombre: "Clasificado Estándar",
|
||||
medioId: 1, productTypeId: 2, rubroId: null,
|
||||
basePrice: 100.50m, priceDurationDays: null,
|
||||
isActive: true,
|
||||
fechaCreacion: new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc),
|
||||
fechaModificacion: null);
|
||||
|
||||
private static Product InactiveProduct(int id = 1) => new(
|
||||
id: id, nombre: "Clasificado Estándar",
|
||||
medioId: 1, productTypeId: 2, rubroId: null,
|
||||
basePrice: 100.50m, priceDurationDays: null,
|
||||
isActive: false,
|
||||
fechaCreacion: new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc),
|
||||
fechaModificacion: null);
|
||||
|
||||
// ── Not found ────────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_NotFound_ThrowsProductNotFoundException()
|
||||
{
|
||||
_repo.GetByIdAsync(99, Arg.Any<CancellationToken>()).Returns((Product?)null);
|
||||
|
||||
var act = async () => await _handler.Handle(new DeactivateProductCommand(99));
|
||||
|
||||
await act.Should().ThrowAsync<ProductNotFoundException>()
|
||||
.Where(e => e.ProductId == 99);
|
||||
}
|
||||
|
||||
// ── Already inactive (idempotent) ─────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_AlreadyInactive_ReturnsDto_NoAudit_NoRepoUpdate()
|
||||
{
|
||||
_repo.GetByIdAsync(1, Arg.Any<CancellationToken>()).Returns(InactiveProduct());
|
||||
|
||||
var result = await _handler.Handle(new DeactivateProductCommand(1));
|
||||
|
||||
result.Id.Should().Be(1);
|
||||
result.IsActive.Should().BeFalse();
|
||||
await _repo.DidNotReceive().UpdateAsync(Arg.Any<Product>(), Arg.Any<CancellationToken>());
|
||||
await _audit.DidNotReceive().LogAsync(
|
||||
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(),
|
||||
Arg.Any<object?>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
// ── Happy path ───────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ActiveProduct_DeactivatesAndAudits()
|
||||
{
|
||||
_repo.GetByIdAsync(1, Arg.Any<CancellationToken>()).Returns(ActiveProduct());
|
||||
|
||||
await _handler.Handle(new DeactivateProductCommand(1));
|
||||
|
||||
await _repo.Received(1).UpdateAsync(
|
||||
Arg.Is<Product>(p => !p.IsActive),
|
||||
Arg.Any<CancellationToken>());
|
||||
await _audit.Received(1).LogAsync(
|
||||
action: "producto.deactivated",
|
||||
targetType: "Product",
|
||||
targetId: "1",
|
||||
metadata: Arg.Any<object?>(),
|
||||
ct: Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_UsesTimeProviderInDeactivate()
|
||||
{
|
||||
_repo.GetByIdAsync(1, Arg.Any<CancellationToken>()).Returns(ActiveProduct());
|
||||
var expectedDate = _time.GetUtcNow().UtcDateTime;
|
||||
|
||||
await _handler.Handle(new DeactivateProductCommand(1));
|
||||
|
||||
await _repo.Received(1).UpdateAsync(
|
||||
Arg.Is<Product>(p => p.FechaModificacion == expectedDate),
|
||||
Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ReturnsDtoWithIsActiveFalse()
|
||||
{
|
||||
_repo.GetByIdAsync(1, Arg.Any<CancellationToken>()).Returns(ActiveProduct());
|
||||
|
||||
var result = await _handler.Handle(new DeactivateProductCommand(1));
|
||||
|
||||
result.IsActive.Should().BeFalse();
|
||||
}
|
||||
|
||||
// ── Rollback ──────────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_RepoThrows_AuditNotCalled()
|
||||
{
|
||||
_repo.GetByIdAsync(1, Arg.Any<CancellationToken>()).Returns(ActiveProduct());
|
||||
_repo.UpdateAsync(Arg.Any<Product>(), Arg.Any<CancellationToken>())
|
||||
.ThrowsAsync(new InvalidOperationException("DB error"));
|
||||
|
||||
var act = async () => await _handler.Handle(new DeactivateProductCommand(1));
|
||||
await act.Should().ThrowAsync<InvalidOperationException>();
|
||||
|
||||
await _audit.DidNotReceive().LogAsync(
|
||||
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(),
|
||||
Arg.Any<object?>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user