test(adm-009): TipoDeIva + IngresosBrutos handler tests (Red)
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using SIGCM2.Domain.Entities;
|
|
||||||
using SIGCM2.Domain.Fiscal;
|
using SIGCM2.Domain.Fiscal;
|
||||||
|
using IibbDomain = SIGCM2.Domain.Entities.IngresosBrutos;
|
||||||
|
|
||||||
namespace SIGCM2.Application.Tests.Domain.Fiscal;
|
namespace SIGCM2.Application.Tests.Domain.Fiscal;
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@ public class IngresosBrutosTests
|
|||||||
private static readonly DateOnly Desde2020 = new(2020, 1, 1);
|
private static readonly DateOnly Desde2020 = new(2020, 1, 1);
|
||||||
private static readonly DateOnly Desde2026 = new(2026, 6, 1);
|
private static readonly DateOnly Desde2026 = new(2026, 6, 1);
|
||||||
|
|
||||||
private static IngresosBrutos MakeIIBB(
|
private static IibbDomain MakeIIBB(
|
||||||
int id = 1,
|
int id = 1,
|
||||||
ProvinciaArgentina provincia = ProvinciaArgentina.BuenosAires,
|
ProvinciaArgentina provincia = ProvinciaArgentina.BuenosAires,
|
||||||
string descripcion = "IIBB Buenos Aires",
|
string descripcion = "IIBB Buenos Aires",
|
||||||
@@ -19,7 +19,7 @@ public class IngresosBrutosTests
|
|||||||
DateOnly? vigenciaDesde = null,
|
DateOnly? vigenciaDesde = null,
|
||||||
DateOnly? vigenciaHasta = null,
|
DateOnly? vigenciaHasta = null,
|
||||||
int? predecesorId = null)
|
int? predecesorId = null)
|
||||||
=> IngresosBrutos.FromDb(
|
=> IibbDomain.FromDb(
|
||||||
id: id,
|
id: id,
|
||||||
provincia: provincia,
|
provincia: provincia,
|
||||||
descripcion: descripcion,
|
descripcion: descripcion,
|
||||||
@@ -36,7 +36,7 @@ public class IngresosBrutosTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void ForCreation_ValidArgs_ReturnsEntity()
|
public void ForCreation_ValidArgs_ReturnsEntity()
|
||||||
{
|
{
|
||||||
var iibb = IngresosBrutos.ForCreation(ProvinciaArgentina.Cordoba, "IIBB Córdoba", 2.5m, Desde2020);
|
var iibb = IibbDomain.ForCreation(ProvinciaArgentina.Cordoba, "IIBB Córdoba", 2.5m, Desde2020);
|
||||||
|
|
||||||
iibb.Provincia.Should().Be(ProvinciaArgentina.Cordoba);
|
iibb.Provincia.Should().Be(ProvinciaArgentina.Cordoba);
|
||||||
iibb.Descripcion.Should().Be("IIBB Córdoba");
|
iibb.Descripcion.Should().Be("IIBB Córdoba");
|
||||||
@@ -50,7 +50,7 @@ public class IngresosBrutosTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void ForCreation_AlicuotaNegativa_ThrowsArgumentException()
|
public void ForCreation_AlicuotaNegativa_ThrowsArgumentException()
|
||||||
{
|
{
|
||||||
var act = () => IngresosBrutos.ForCreation(ProvinciaArgentina.BuenosAires, "desc", -1m, Desde2020);
|
var act = () => IibbDomain.ForCreation(ProvinciaArgentina.BuenosAires, "desc", -1m, Desde2020);
|
||||||
|
|
||||||
act.Should().Throw<ArgumentException>()
|
act.Should().Throw<ArgumentException>()
|
||||||
.WithParameterName("alicuota");
|
.WithParameterName("alicuota");
|
||||||
@@ -59,7 +59,7 @@ public class IngresosBrutosTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void ForCreation_AlicuotaMayorA100_ThrowsArgumentException()
|
public void ForCreation_AlicuotaMayorA100_ThrowsArgumentException()
|
||||||
{
|
{
|
||||||
var act = () => IngresosBrutos.ForCreation(ProvinciaArgentina.BuenosAires, "desc", 101m, Desde2020);
|
var act = () => IibbDomain.ForCreation(ProvinciaArgentina.BuenosAires, "desc", 101m, Desde2020);
|
||||||
|
|
||||||
act.Should().Throw<ArgumentException>()
|
act.Should().Throw<ArgumentException>()
|
||||||
.WithParameterName("alicuota");
|
.WithParameterName("alicuota");
|
||||||
@@ -71,7 +71,7 @@ public class IngresosBrutosTests
|
|||||||
[InlineData(100)]
|
[InlineData(100)]
|
||||||
public void ForCreation_AlicuotaEnRango_NoLanza(double alicuota)
|
public void ForCreation_AlicuotaEnRango_NoLanza(double alicuota)
|
||||||
{
|
{
|
||||||
var act = () => IngresosBrutos.ForCreation(ProvinciaArgentina.Salta, "desc", (decimal)alicuota, Desde2020);
|
var act = () => IibbDomain.ForCreation(ProvinciaArgentina.Salta, "desc", (decimal)alicuota, Desde2020);
|
||||||
|
|
||||||
act.Should().NotThrow();
|
act.Should().NotThrow();
|
||||||
}
|
}
|
||||||
@@ -82,7 +82,7 @@ public class IngresosBrutosTests
|
|||||||
var desde = new DateOnly(2026, 6, 1);
|
var desde = new DateOnly(2026, 6, 1);
|
||||||
var hasta = new DateOnly(2026, 1, 1);
|
var hasta = new DateOnly(2026, 1, 1);
|
||||||
|
|
||||||
var act = () => IngresosBrutos.ForCreation(ProvinciaArgentina.SantaFe, "desc", 3m, desde, hasta);
|
var act = () => IibbDomain.ForCreation(ProvinciaArgentina.SantaFe, "desc", 3m, desde, hasta);
|
||||||
|
|
||||||
act.Should().Throw<ArgumentException>()
|
act.Should().Throw<ArgumentException>()
|
||||||
.WithParameterName("vigenciaHasta");
|
.WithParameterName("vigenciaHasta");
|
||||||
@@ -209,7 +209,7 @@ public class IngresosBrutosTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void IngresosBrutos_No_Debe_Exponer_WithAlicuota()
|
public void IngresosBrutos_No_Debe_Exponer_WithAlicuota()
|
||||||
{
|
{
|
||||||
var method = typeof(IngresosBrutos).GetMethod("WithAlicuota", BindingFlags.Public | BindingFlags.Instance);
|
var method = typeof(IibbDomain).GetMethod("WithAlicuota", BindingFlags.Public | BindingFlags.Instance);
|
||||||
|
|
||||||
method.Should().BeNull("Alicuota es inmutable — usar NuevaVersion");
|
method.Should().BeNull("Alicuota es inmutable — usar NuevaVersion");
|
||||||
}
|
}
|
||||||
@@ -217,7 +217,7 @@ public class IngresosBrutosTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void IngresosBrutos_No_Debe_Exponer_WithProvincia()
|
public void IngresosBrutos_No_Debe_Exponer_WithProvincia()
|
||||||
{
|
{
|
||||||
var method = typeof(IngresosBrutos).GetMethod("WithProvincia", BindingFlags.Public | BindingFlags.Instance);
|
var method = typeof(IibbDomain).GetMethod("WithProvincia", BindingFlags.Public | BindingFlags.Instance);
|
||||||
|
|
||||||
method.Should().BeNull("Provincia es inmutable en IngresosBrutos");
|
method.Should().BeNull("Provincia es inmutable en IngresosBrutos");
|
||||||
}
|
}
|
||||||
@@ -228,7 +228,7 @@ public class IngresosBrutosTests
|
|||||||
public void FromDb_SetsAllProperties()
|
public void FromDb_SetsAllProperties()
|
||||||
{
|
{
|
||||||
var fechaCreacion = DateTime.UtcNow;
|
var fechaCreacion = DateTime.UtcNow;
|
||||||
var iibb = IngresosBrutos.FromDb(
|
var iibb = IibbDomain.FromDb(
|
||||||
id: 10, provincia: ProvinciaArgentina.Tucuman, descripcion: "IIBB Tucuman",
|
id: 10, provincia: ProvinciaArgentina.Tucuman, descripcion: "IIBB Tucuman",
|
||||||
alicuota: 1.5m, activo: true,
|
alicuota: 1.5m, activo: true,
|
||||||
vigenciaDesde: Desde2020, vigenciaHasta: null,
|
vigenciaDesde: Desde2020, vigenciaHasta: null,
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
using NSubstitute;
|
||||||
|
using SIGCM2.Application.Abstractions.Persistence;
|
||||||
|
using SIGCM2.Application.Audit;
|
||||||
|
using SIGCM2.Application.TiposDeIva.Create;
|
||||||
|
using SIGCM2.Domain.Entities;
|
||||||
|
using SIGCM2.Domain.Exceptions;
|
||||||
|
|
||||||
|
namespace SIGCM2.Application.Tests.TiposDeIva.Create;
|
||||||
|
|
||||||
|
public class CreateTipoDeIvaCommandHandlerTests
|
||||||
|
{
|
||||||
|
private readonly ITipoDeIvaRepository _repo = Substitute.For<ITipoDeIvaRepository>();
|
||||||
|
private readonly IAuditLogger _audit = Substitute.For<IAuditLogger>();
|
||||||
|
private readonly CreateTipoDeIvaCommandHandler _handler;
|
||||||
|
|
||||||
|
private static CreateTipoDeIvaCommand ValidCommand() => new(
|
||||||
|
Codigo: "IVA_21",
|
||||||
|
Descripcion: "IVA 21%",
|
||||||
|
Porcentaje: 21m,
|
||||||
|
AplicaIVA: true,
|
||||||
|
VigenciaDesde: new DateOnly(2024, 1, 1));
|
||||||
|
|
||||||
|
public CreateTipoDeIvaCommandHandlerTests()
|
||||||
|
{
|
||||||
|
_handler = new CreateTipoDeIvaCommandHandler(_repo, _audit);
|
||||||
|
_repo.InsertAsync(Arg.Any<TipoDeIva>(), Arg.Any<CancellationToken>()).Returns(42);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── happy path ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_HappyPath_ReturnsDtoWithIdFromRepository()
|
||||||
|
{
|
||||||
|
var result = await _handler.Handle(ValidCommand());
|
||||||
|
|
||||||
|
Assert.Equal(42, result.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_HappyPath_DtoContainsCorrectFields()
|
||||||
|
{
|
||||||
|
var result = await _handler.Handle(ValidCommand());
|
||||||
|
|
||||||
|
Assert.Equal("IVA_21", result.Codigo);
|
||||||
|
Assert.Equal("IVA 21%", result.Descripcion);
|
||||||
|
Assert.Equal(21m, result.Porcentaje);
|
||||||
|
Assert.True(result.AplicaIVA);
|
||||||
|
Assert.True(result.Activo);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_HappyPath_CallsAuditWithCreateAction()
|
||||||
|
{
|
||||||
|
await _handler.Handle(ValidCommand());
|
||||||
|
|
||||||
|
await _audit.Received(1).LogAsync(
|
||||||
|
action: "tipo_iva.create",
|
||||||
|
targetType: "TipoDeIva",
|
||||||
|
targetId: "42",
|
||||||
|
metadata: Arg.Any<object?>(),
|
||||||
|
ct: Arg.Any<CancellationToken>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_HappyPath_CallsInsertOnce()
|
||||||
|
{
|
||||||
|
await _handler.Handle(ValidCommand());
|
||||||
|
|
||||||
|
await _repo.Received(1).InsertAsync(Arg.Any<TipoDeIva>(), Arg.Any<CancellationToken>());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── audit fail-closed ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_AuditLoggerThrows_ExceptionBubblesUp()
|
||||||
|
{
|
||||||
|
_audit.LogAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(),
|
||||||
|
Arg.Any<object?>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(Task.FromException(new InvalidOperationException("audit fail")));
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<InvalidOperationException>(
|
||||||
|
() => _handler.Handle(ValidCommand()));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_AuditLoggerThrows_InsertWasCalledButScopeNotCompleted()
|
||||||
|
{
|
||||||
|
_audit.LogAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(),
|
||||||
|
Arg.Any<object?>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(Task.FromException(new InvalidOperationException("audit fail")));
|
||||||
|
|
||||||
|
try { await _handler.Handle(ValidCommand()); } catch (InvalidOperationException) { }
|
||||||
|
|
||||||
|
// Insert was called (it's before audit in the scope), but audit threw = scope not completed
|
||||||
|
await _repo.Received(1).InsertAsync(Arg.Any<TipoDeIva>(), Arg.Any<CancellationToken>());
|
||||||
|
await _audit.Received(1).LogAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(),
|
||||||
|
Arg.Any<object?>(), Arg.Any<CancellationToken>());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── triangulation: different porcentaje value ────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_WithZeroPorcentaje_ReturnsDtoWithCorrectPorcentaje()
|
||||||
|
{
|
||||||
|
var cmd = new CreateTipoDeIvaCommand(
|
||||||
|
Codigo: "EXENTO",
|
||||||
|
Descripcion: "Exento de IVA",
|
||||||
|
Porcentaje: 0m,
|
||||||
|
AplicaIVA: false,
|
||||||
|
VigenciaDesde: new DateOnly(2024, 1, 1));
|
||||||
|
|
||||||
|
var result = await _handler.Handle(cmd);
|
||||||
|
|
||||||
|
Assert.Equal(0m, result.Porcentaje);
|
||||||
|
Assert.False(result.AplicaIVA);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
using NSubstitute;
|
||||||
|
using SIGCM2.Application.Abstractions.Persistence;
|
||||||
|
using SIGCM2.Application.Audit;
|
||||||
|
using SIGCM2.Application.TiposDeIva.Deactivate;
|
||||||
|
using SIGCM2.Domain.Entities;
|
||||||
|
using SIGCM2.Domain.Exceptions;
|
||||||
|
|
||||||
|
namespace SIGCM2.Application.Tests.TiposDeIva.Deactivate;
|
||||||
|
|
||||||
|
public class DeactivateTipoDeIvaCommandHandlerTests
|
||||||
|
{
|
||||||
|
private readonly ITipoDeIvaRepository _repo = Substitute.For<ITipoDeIvaRepository>();
|
||||||
|
private readonly IAuditLogger _audit = Substitute.For<IAuditLogger>();
|
||||||
|
private readonly DeactivateTipoDeIvaCommandHandler _handler;
|
||||||
|
|
||||||
|
private static TipoDeIva MakeEntity(bool activo = true) => TipoDeIva.FromDb(
|
||||||
|
id: 1, codigo: "IVA_21", descripcion: "IVA 21%", porcentaje: 21m, aplicaIVA: true,
|
||||||
|
activo: activo, vigenciaDesde: new DateOnly(2024, 1, 1), vigenciaHasta: null,
|
||||||
|
predecesorId: null, fechaCreacion: DateTime.UtcNow, fechaModificacion: null);
|
||||||
|
|
||||||
|
public DeactivateTipoDeIvaCommandHandlerTests()
|
||||||
|
{
|
||||||
|
_handler = new DeactivateTipoDeIvaCommandHandler(_repo, _audit);
|
||||||
|
_repo.GetByIdAsync(1, Arg.Any<CancellationToken>()).Returns(MakeEntity());
|
||||||
|
_repo.SetActivoAsync(Arg.Any<int>(), Arg.Any<bool>(), Arg.Any<CancellationToken>()).Returns(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_NotFound_ThrowsTipoDeIvaNotFoundException()
|
||||||
|
{
|
||||||
|
_repo.GetByIdAsync(99, Arg.Any<CancellationToken>()).Returns((TipoDeIva?)null);
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<TipoDeIvaNotFoundException>(
|
||||||
|
() => _handler.Handle(new DeactivateTipoDeIvaCommand(99)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_HappyPath_CallsSetActivoFalse()
|
||||||
|
{
|
||||||
|
await _handler.Handle(new DeactivateTipoDeIvaCommand(1));
|
||||||
|
|
||||||
|
await _repo.Received(1).SetActivoAsync(1, false, Arg.Any<CancellationToken>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_HappyPath_CallsAuditDeactivate()
|
||||||
|
{
|
||||||
|
await _handler.Handle(new DeactivateTipoDeIvaCommand(1));
|
||||||
|
|
||||||
|
await _audit.Received(1).LogAsync(
|
||||||
|
action: "tipo_iva.deactivate",
|
||||||
|
targetType: "TipoDeIva",
|
||||||
|
targetId: "1",
|
||||||
|
metadata: Arg.Any<object?>(),
|
||||||
|
ct: Arg.Any<CancellationToken>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_AlreadyInactive_IsIdempotent_NoAudit()
|
||||||
|
{
|
||||||
|
_repo.GetByIdAsync(1, Arg.Any<CancellationToken>()).Returns(MakeEntity(activo: false));
|
||||||
|
|
||||||
|
await _handler.Handle(new DeactivateTipoDeIvaCommand(1));
|
||||||
|
|
||||||
|
await _audit.DidNotReceive().LogAsync(
|
||||||
|
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(),
|
||||||
|
Arg.Any<object?>(), Arg.Any<CancellationToken>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_AuditLoggerThrows_ExceptionBubblesUp()
|
||||||
|
{
|
||||||
|
_audit.LogAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(),
|
||||||
|
Arg.Any<object?>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(Task.FromException(new InvalidOperationException("audit fail")));
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<InvalidOperationException>(
|
||||||
|
() => _handler.Handle(new DeactivateTipoDeIvaCommand(1)));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
using NSubstitute;
|
||||||
|
using SIGCM2.Application.Abstractions.Persistence;
|
||||||
|
using SIGCM2.Application.TiposDeIva.GetById;
|
||||||
|
using SIGCM2.Domain.Entities;
|
||||||
|
using SIGCM2.Domain.Exceptions;
|
||||||
|
|
||||||
|
namespace SIGCM2.Application.Tests.TiposDeIva.GetById;
|
||||||
|
|
||||||
|
public class GetTipoDeIvaByIdQueryHandlerTests
|
||||||
|
{
|
||||||
|
private readonly ITipoDeIvaRepository _repo = Substitute.For<ITipoDeIvaRepository>();
|
||||||
|
private readonly GetTipoDeIvaByIdQueryHandler _handler;
|
||||||
|
|
||||||
|
private static TipoDeIva MakeEntity(int id = 1) => TipoDeIva.FromDb(
|
||||||
|
id: id, codigo: "IVA_21", descripcion: "IVA 21%", porcentaje: 21m, aplicaIVA: true,
|
||||||
|
activo: true, vigenciaDesde: new DateOnly(2024, 1, 1), vigenciaHasta: null,
|
||||||
|
predecesorId: null, fechaCreacion: DateTime.UtcNow, fechaModificacion: null);
|
||||||
|
|
||||||
|
public GetTipoDeIvaByIdQueryHandlerTests()
|
||||||
|
{
|
||||||
|
_handler = new GetTipoDeIvaByIdQueryHandler(_repo);
|
||||||
|
_repo.GetByIdAsync(1, Arg.Any<CancellationToken>()).Returns(MakeEntity());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_Found_ReturnsDtoWithCorrectId()
|
||||||
|
{
|
||||||
|
var result = await _handler.Handle(new GetTipoDeIvaByIdQuery(1));
|
||||||
|
|
||||||
|
Assert.Equal(1, result.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_Found_ReturnsDtoWithAllFields()
|
||||||
|
{
|
||||||
|
var result = await _handler.Handle(new GetTipoDeIvaByIdQuery(1));
|
||||||
|
|
||||||
|
Assert.Equal("IVA_21", result.Codigo);
|
||||||
|
Assert.Equal(21m, result.Porcentaje);
|
||||||
|
Assert.True(result.Activo);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_NotFound_ThrowsTipoDeIvaNotFoundException()
|
||||||
|
{
|
||||||
|
_repo.GetByIdAsync(99, Arg.Any<CancellationToken>()).Returns((TipoDeIva?)null);
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<TipoDeIvaNotFoundException>(
|
||||||
|
() => _handler.Handle(new GetTipoDeIvaByIdQuery(99)));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
using NSubstitute;
|
||||||
|
using SIGCM2.Application.Abstractions.Persistence;
|
||||||
|
using SIGCM2.Application.TiposDeIva.GetHistorial;
|
||||||
|
using SIGCM2.Domain.Entities;
|
||||||
|
|
||||||
|
namespace SIGCM2.Application.Tests.TiposDeIva.GetHistorial;
|
||||||
|
|
||||||
|
public class GetHistorialTipoDeIvaQueryHandlerTests
|
||||||
|
{
|
||||||
|
private readonly ITipoDeIvaRepository _repo = Substitute.For<ITipoDeIvaRepository>();
|
||||||
|
private readonly GetHistorialTipoDeIvaQueryHandler _handler;
|
||||||
|
|
||||||
|
public GetHistorialTipoDeIvaQueryHandlerTests()
|
||||||
|
{
|
||||||
|
_handler = new GetHistorialTipoDeIvaQueryHandler(_repo);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TipoDeIva MakeEntity(int id, int? predecesorId, DateOnly desde) =>
|
||||||
|
TipoDeIva.FromDb(id, "IVA_21", "IVA 21%", 21m, true, true, desde,
|
||||||
|
predecesorId.HasValue ? desde.AddYears(1).AddDays(-1) : null,
|
||||||
|
predecesorId, DateTime.UtcNow, null);
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_ChainOf3_ReturnsListWith3ItemsInOrder()
|
||||||
|
{
|
||||||
|
var chain = new List<TipoDeIva>
|
||||||
|
{
|
||||||
|
MakeEntity(1, null, new DateOnly(2022, 1, 1)),
|
||||||
|
MakeEntity(2, 1, new DateOnly(2023, 1, 1)),
|
||||||
|
MakeEntity(3, 2, new DateOnly(2024, 1, 1)),
|
||||||
|
};
|
||||||
|
_repo.GetHistorialAsync(3, Arg.Any<CancellationToken>()).Returns(chain);
|
||||||
|
|
||||||
|
var result = await _handler.Handle(new GetHistorialTipoDeIvaQuery(3));
|
||||||
|
|
||||||
|
Assert.Equal(3, result.Count);
|
||||||
|
Assert.Equal(1, result[0].Version); // root
|
||||||
|
Assert.Equal(3, result[2].Version); // current
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_SingleVersion_Returns1Item()
|
||||||
|
{
|
||||||
|
var chain = new List<TipoDeIva> { MakeEntity(1, null, new DateOnly(2024, 1, 1)) };
|
||||||
|
_repo.GetHistorialAsync(1, Arg.Any<CancellationToken>()).Returns(chain);
|
||||||
|
|
||||||
|
var result = await _handler.Handle(new GetHistorialTipoDeIvaQuery(1));
|
||||||
|
|
||||||
|
Assert.Single(result);
|
||||||
|
Assert.Equal(1, result[0].Version);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
using NSubstitute;
|
||||||
|
using SIGCM2.Application.Abstractions.Persistence;
|
||||||
|
using SIGCM2.Application.Common;
|
||||||
|
using SIGCM2.Application.TiposDeIva.List;
|
||||||
|
using SIGCM2.Domain.Entities;
|
||||||
|
|
||||||
|
namespace SIGCM2.Application.Tests.TiposDeIva.List;
|
||||||
|
|
||||||
|
public class ListTiposDeIvaQueryHandlerTests
|
||||||
|
{
|
||||||
|
private readonly ITipoDeIvaRepository _repo = Substitute.For<ITipoDeIvaRepository>();
|
||||||
|
private readonly ListTiposDeIvaQueryHandler _handler;
|
||||||
|
|
||||||
|
private static TipoDeIva MakeEntity(int id) => TipoDeIva.FromDb(
|
||||||
|
id: id, codigo: "IVA_21", descripcion: "IVA 21%", porcentaje: 21m, aplicaIVA: true,
|
||||||
|
activo: true, vigenciaDesde: new DateOnly(2024, 1, 1), vigenciaHasta: null,
|
||||||
|
predecesorId: null, fechaCreacion: DateTime.UtcNow, fechaModificacion: null);
|
||||||
|
|
||||||
|
public ListTiposDeIvaQueryHandlerTests()
|
||||||
|
{
|
||||||
|
_handler = new ListTiposDeIvaQueryHandler(_repo);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_WithItems_ReturnsPagedResultWithMappedDtos()
|
||||||
|
{
|
||||||
|
var items = new List<TipoDeIva> { MakeEntity(1), MakeEntity(2) };
|
||||||
|
_repo.ListAsync(Arg.Any<TiposDeIvaQuery>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(new PagedResult<TipoDeIva>(items, 1, 10, 2));
|
||||||
|
|
||||||
|
var result = await _handler.Handle(new ListTiposDeIvaQuery(1, 10, null, null));
|
||||||
|
|
||||||
|
Assert.Equal(2, result.Items.Count);
|
||||||
|
Assert.Equal(2, result.Total);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_EmptyResult_ReturnsPagedResultWithZeroItems()
|
||||||
|
{
|
||||||
|
_repo.ListAsync(Arg.Any<TiposDeIvaQuery>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(new PagedResult<TipoDeIva>(new List<TipoDeIva>(), 1, 10, 0));
|
||||||
|
|
||||||
|
var result = await _handler.Handle(new ListTiposDeIvaQuery(1, 10, null, null));
|
||||||
|
|
||||||
|
Assert.Equal(0, result.Items.Count);
|
||||||
|
Assert.Equal(0, result.Total);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_PassesFiltersToRepository()
|
||||||
|
{
|
||||||
|
_repo.ListAsync(Arg.Any<TiposDeIvaQuery>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(new PagedResult<TipoDeIva>(new List<TipoDeIva>(), 1, 10, 0));
|
||||||
|
|
||||||
|
await _handler.Handle(new ListTiposDeIvaQuery(2, 5, true, "IVA_21"));
|
||||||
|
|
||||||
|
await _repo.Received(1).ListAsync(
|
||||||
|
Arg.Is<TiposDeIvaQuery>(q => q.Page == 2 && q.PageSize == 5 && q.Activo == true && q.Codigo == "IVA_21"),
|
||||||
|
Arg.Any<CancellationToken>());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
using NSubstitute;
|
||||||
|
using SIGCM2.Application.Abstractions.Persistence;
|
||||||
|
using SIGCM2.Application.Audit;
|
||||||
|
using SIGCM2.Application.TiposDeIva.NuevaVersion;
|
||||||
|
using SIGCM2.Domain.Entities;
|
||||||
|
using SIGCM2.Domain.Exceptions;
|
||||||
|
|
||||||
|
namespace SIGCM2.Application.Tests.TiposDeIva.NuevaVersion;
|
||||||
|
|
||||||
|
public class NuevaVersionTipoDeIvaCommandHandlerTests
|
||||||
|
{
|
||||||
|
private readonly ITipoDeIvaRepository _repo = Substitute.For<ITipoDeIvaRepository>();
|
||||||
|
private readonly IAuditLogger _audit = Substitute.For<IAuditLogger>();
|
||||||
|
private readonly NuevaVersionTipoDeIvaCommandHandler _handler;
|
||||||
|
|
||||||
|
private static TipoDeIva MakePredecesora(int id = 1, DateOnly? vigenciaHasta = null) =>
|
||||||
|
TipoDeIva.FromDb(
|
||||||
|
id: id,
|
||||||
|
codigo: "IVA_21",
|
||||||
|
descripcion: "IVA 21%",
|
||||||
|
porcentaje: 21m,
|
||||||
|
aplicaIVA: true,
|
||||||
|
activo: true,
|
||||||
|
vigenciaDesde: new DateOnly(2024, 1, 1),
|
||||||
|
vigenciaHasta: vigenciaHasta,
|
||||||
|
predecesorId: null,
|
||||||
|
fechaCreacion: DateTime.UtcNow,
|
||||||
|
fechaModificacion: null);
|
||||||
|
|
||||||
|
private static NuevaVersionTipoDeIvaCommand ValidCommand() => new(
|
||||||
|
PredecesoraId: 1,
|
||||||
|
NuevoPorcentaje: 27m,
|
||||||
|
VigenciaDesde: new DateOnly(2025, 1, 1));
|
||||||
|
|
||||||
|
public NuevaVersionTipoDeIvaCommandHandlerTests()
|
||||||
|
{
|
||||||
|
_handler = new NuevaVersionTipoDeIvaCommandHandler(_repo, _audit);
|
||||||
|
_repo.GetByIdAsync(1, Arg.Any<CancellationToken>()).Returns(MakePredecesora());
|
||||||
|
_repo.UpdateCierreVigenciaAsync(Arg.Any<int>(), Arg.Any<DateOnly>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(true);
|
||||||
|
_repo.InsertAsync(Arg.Any<TipoDeIva>(), Arg.Any<CancellationToken>()).Returns(99);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── happy path ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_HappyPath_ReturnsDtoWithCorrectIds()
|
||||||
|
{
|
||||||
|
var result = await _handler.Handle(ValidCommand());
|
||||||
|
|
||||||
|
Assert.Equal(1, result.PredecesoraId);
|
||||||
|
Assert.Equal(99, result.NuevaVersionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_HappyPath_CallsUpdateCierreVigenciaOnce()
|
||||||
|
{
|
||||||
|
await _handler.Handle(ValidCommand());
|
||||||
|
|
||||||
|
await _repo.Received(1).UpdateCierreVigenciaAsync(
|
||||||
|
1,
|
||||||
|
new DateOnly(2024, 12, 31), // vigenciaDesde(2025-01-01) - 1 day
|
||||||
|
Arg.Any<CancellationToken>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_HappyPath_CallsInsertOnce()
|
||||||
|
{
|
||||||
|
await _handler.Handle(ValidCommand());
|
||||||
|
|
||||||
|
await _repo.Received(1).InsertAsync(
|
||||||
|
Arg.Is<TipoDeIva>(e => e.Porcentaje == 27m && e.PredecesorId == 1),
|
||||||
|
Arg.Any<CancellationToken>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_HappyPath_CallsAuditOnceWithCorrectAction()
|
||||||
|
{
|
||||||
|
await _handler.Handle(ValidCommand());
|
||||||
|
|
||||||
|
await _audit.Received(1).LogAsync(
|
||||||
|
action: "tipo_iva.nueva_version",
|
||||||
|
targetType: "TipoDeIva",
|
||||||
|
targetId: Arg.Any<string>(),
|
||||||
|
metadata: Arg.Any<object?>(),
|
||||||
|
ct: Arg.Any<CancellationToken>());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── predecesora not found ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_PredecesoraNotFound_ThrowsTipoDeIvaNotFoundException()
|
||||||
|
{
|
||||||
|
_repo.GetByIdAsync(999, Arg.Any<CancellationToken>()).Returns((TipoDeIva?)null);
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<TipoDeIvaNotFoundException>(
|
||||||
|
() => _handler.Handle(new NuevaVersionTipoDeIvaCommand(999, 27m, new DateOnly(2025, 1, 1))));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── predecesora ya cerrada ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_PredecesoraYaCerrada_ThrowsPredecesorYaCerradoException()
|
||||||
|
{
|
||||||
|
_repo.GetByIdAsync(1, Arg.Any<CancellationToken>())
|
||||||
|
.Returns(MakePredecesora(vigenciaHasta: new DateOnly(2024, 12, 31)));
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<PredecesorYaCerradoException>(
|
||||||
|
() => _handler.Handle(ValidCommand()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── predecesora inactiva ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_PredecesoraInactiva_ThrowsPredecesorYaCerradoException()
|
||||||
|
{
|
||||||
|
var inactiva = TipoDeIva.FromDb(1, "IVA_21", "IVA 21%", 21m, true, false,
|
||||||
|
new DateOnly(2024, 1, 1), null, null, DateTime.UtcNow, null);
|
||||||
|
_repo.GetByIdAsync(1, Arg.Any<CancellationToken>()).Returns(inactiva);
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<PredecesorYaCerradoException>(
|
||||||
|
() => _handler.Handle(ValidCommand()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── vigenciaDesde inválida ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_VigenciaDesdeNotAfterPredecesora_ThrowsArgumentException()
|
||||||
|
{
|
||||||
|
var cmd = new NuevaVersionTipoDeIvaCommand(
|
||||||
|
PredecesoraId: 1,
|
||||||
|
NuevoPorcentaje: 27m,
|
||||||
|
VigenciaDesde: new DateOnly(2024, 1, 1)); // same as predecesora
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<ArgumentException>(
|
||||||
|
() => _handler.Handle(cmd));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── race condition: UpdateCierreVigencia returns false ────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_UpdateCierreVigenciaReturnsFalse_ThrowsPredecesorYaCerradoException()
|
||||||
|
{
|
||||||
|
_repo.UpdateCierreVigenciaAsync(Arg.Any<int>(), Arg.Any<DateOnly>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(false);
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<PredecesorYaCerradoException>(
|
||||||
|
() => _handler.Handle(ValidCommand()));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_UpdateCierreVigenciaReturnsFalse_InsertIsNeverCalled()
|
||||||
|
{
|
||||||
|
_repo.UpdateCierreVigenciaAsync(Arg.Any<int>(), Arg.Any<DateOnly>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(false);
|
||||||
|
|
||||||
|
try { await _handler.Handle(ValidCommand()); } catch (PredecesorYaCerradoException) { }
|
||||||
|
|
||||||
|
await _repo.DidNotReceive().InsertAsync(Arg.Any<TipoDeIva>(), Arg.Any<CancellationToken>());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── audit fail-closed ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_AuditLoggerThrows_ExceptionBubblesUp()
|
||||||
|
{
|
||||||
|
_audit.LogAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(),
|
||||||
|
Arg.Any<object?>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(Task.FromException(new InvalidOperationException("audit fail")));
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<InvalidOperationException>(
|
||||||
|
() => _handler.Handle(ValidCommand()));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_AuditLoggerThrows_InsertWasCalledButAuditFailed()
|
||||||
|
{
|
||||||
|
_audit.LogAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(),
|
||||||
|
Arg.Any<object?>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(Task.FromException(new InvalidOperationException("audit fail")));
|
||||||
|
|
||||||
|
try { await _handler.Handle(ValidCommand()); } catch (InvalidOperationException) { }
|
||||||
|
|
||||||
|
// Insert was called before audit — audit throwing means scope.Complete never runs
|
||||||
|
await _repo.Received(1).InsertAsync(Arg.Any<TipoDeIva>(), Arg.Any<CancellationToken>());
|
||||||
|
await _audit.Received(1).LogAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(),
|
||||||
|
Arg.Any<object?>(), Arg.Any<CancellationToken>());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
using NSubstitute;
|
||||||
|
using SIGCM2.Application.Abstractions.Persistence;
|
||||||
|
using SIGCM2.Application.Audit;
|
||||||
|
using SIGCM2.Application.TiposDeIva.Reactivate;
|
||||||
|
using SIGCM2.Domain.Entities;
|
||||||
|
using SIGCM2.Domain.Exceptions;
|
||||||
|
|
||||||
|
namespace SIGCM2.Application.Tests.TiposDeIva.Reactivate;
|
||||||
|
|
||||||
|
public class ReactivateTipoDeIvaCommandHandlerTests
|
||||||
|
{
|
||||||
|
private readonly ITipoDeIvaRepository _repo = Substitute.For<ITipoDeIvaRepository>();
|
||||||
|
private readonly IAuditLogger _audit = Substitute.For<IAuditLogger>();
|
||||||
|
private readonly ReactivateTipoDeIvaCommandHandler _handler;
|
||||||
|
|
||||||
|
private static TipoDeIva MakeEntity(bool activo = false) => TipoDeIva.FromDb(
|
||||||
|
id: 1, codigo: "IVA_21", descripcion: "IVA 21%", porcentaje: 21m, aplicaIVA: true,
|
||||||
|
activo: activo, vigenciaDesde: new DateOnly(2024, 1, 1), vigenciaHasta: null,
|
||||||
|
predecesorId: null, fechaCreacion: DateTime.UtcNow, fechaModificacion: null);
|
||||||
|
|
||||||
|
public ReactivateTipoDeIvaCommandHandlerTests()
|
||||||
|
{
|
||||||
|
_handler = new ReactivateTipoDeIvaCommandHandler(_repo, _audit);
|
||||||
|
_repo.GetByIdAsync(1, Arg.Any<CancellationToken>()).Returns(MakeEntity());
|
||||||
|
_repo.SetActivoAsync(Arg.Any<int>(), Arg.Any<bool>(), Arg.Any<CancellationToken>()).Returns(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_NotFound_ThrowsTipoDeIvaNotFoundException()
|
||||||
|
{
|
||||||
|
_repo.GetByIdAsync(99, Arg.Any<CancellationToken>()).Returns((TipoDeIva?)null);
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<TipoDeIvaNotFoundException>(
|
||||||
|
() => _handler.Handle(new ReactivateTipoDeIvaCommand(99)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_HappyPath_CallsSetActivoTrue()
|
||||||
|
{
|
||||||
|
await _handler.Handle(new ReactivateTipoDeIvaCommand(1));
|
||||||
|
|
||||||
|
await _repo.Received(1).SetActivoAsync(1, true, Arg.Any<CancellationToken>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_HappyPath_CallsAuditReactivate()
|
||||||
|
{
|
||||||
|
await _handler.Handle(new ReactivateTipoDeIvaCommand(1));
|
||||||
|
|
||||||
|
await _audit.Received(1).LogAsync(
|
||||||
|
action: "tipo_iva.reactivate",
|
||||||
|
targetType: "TipoDeIva",
|
||||||
|
targetId: "1",
|
||||||
|
metadata: Arg.Any<object?>(),
|
||||||
|
ct: Arg.Any<CancellationToken>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_AlreadyActive_IsIdempotent_NoAudit()
|
||||||
|
{
|
||||||
|
_repo.GetByIdAsync(1, Arg.Any<CancellationToken>()).Returns(MakeEntity(activo: true));
|
||||||
|
|
||||||
|
await _handler.Handle(new ReactivateTipoDeIvaCommand(1));
|
||||||
|
|
||||||
|
await _audit.DidNotReceive().LogAsync(
|
||||||
|
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(),
|
||||||
|
Arg.Any<object?>(), Arg.Any<CancellationToken>());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
using NSubstitute;
|
||||||
|
using SIGCM2.Application.Abstractions.Persistence;
|
||||||
|
using SIGCM2.Application.Audit;
|
||||||
|
using SIGCM2.Application.TiposDeIva.Update;
|
||||||
|
using SIGCM2.Domain.Entities;
|
||||||
|
using SIGCM2.Domain.Exceptions;
|
||||||
|
|
||||||
|
namespace SIGCM2.Application.Tests.TiposDeIva.Update;
|
||||||
|
|
||||||
|
public class UpdateTipoDeIvaCommandHandlerTests
|
||||||
|
{
|
||||||
|
private readonly ITipoDeIvaRepository _repo = Substitute.For<ITipoDeIvaRepository>();
|
||||||
|
private readonly IAuditLogger _audit = Substitute.For<IAuditLogger>();
|
||||||
|
private readonly UpdateTipoDeIvaCommandHandler _handler;
|
||||||
|
|
||||||
|
private static TipoDeIva MakeEntity(int id = 1) => TipoDeIva.FromDb(
|
||||||
|
id: id,
|
||||||
|
codigo: "IVA_21",
|
||||||
|
descripcion: "IVA 21%",
|
||||||
|
porcentaje: 21m,
|
||||||
|
aplicaIVA: true,
|
||||||
|
activo: true,
|
||||||
|
vigenciaDesde: new DateOnly(2024, 1, 1),
|
||||||
|
vigenciaHasta: null,
|
||||||
|
predecesorId: null,
|
||||||
|
fechaCreacion: DateTime.UtcNow,
|
||||||
|
fechaModificacion: null);
|
||||||
|
|
||||||
|
private static UpdateTipoDeIvaCommand ValidCommand(int id = 1) => new(
|
||||||
|
Id: id,
|
||||||
|
Codigo: "IVA_21",
|
||||||
|
Descripcion: "IVA 21% actualizado",
|
||||||
|
AplicaIVA: true,
|
||||||
|
Activo: true);
|
||||||
|
|
||||||
|
public UpdateTipoDeIvaCommandHandlerTests()
|
||||||
|
{
|
||||||
|
_handler = new UpdateTipoDeIvaCommandHandler(_repo, _audit);
|
||||||
|
_repo.GetByIdAsync(1, Arg.Any<CancellationToken>()).Returns(MakeEntity());
|
||||||
|
_repo.UpdateCosmeticoAsync(Arg.Any<int>(), Arg.Any<string>(), Arg.Any<string>(),
|
||||||
|
Arg.Any<bool>(), Arg.Any<bool>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── not found ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_NotFound_ThrowsTipoDeIvaNotFoundException()
|
||||||
|
{
|
||||||
|
_repo.GetByIdAsync(99, Arg.Any<CancellationToken>()).Returns((TipoDeIva?)null);
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<TipoDeIvaNotFoundException>(
|
||||||
|
() => _handler.Handle(ValidCommand(99)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── happy path ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_HappyPath_ReturnsDtoWithUpdatedDescription()
|
||||||
|
{
|
||||||
|
var result = await _handler.Handle(ValidCommand());
|
||||||
|
|
||||||
|
Assert.Equal("IVA 21% actualizado", result.Descripcion);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_HappyPath_CallsUpdateCosmeticoOnce()
|
||||||
|
{
|
||||||
|
await _handler.Handle(ValidCommand());
|
||||||
|
|
||||||
|
await _repo.Received(1).UpdateCosmeticoAsync(
|
||||||
|
1, "IVA_21", "IVA 21% actualizado", true, true,
|
||||||
|
Arg.Any<CancellationToken>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_HappyPath_CallsAuditWithUpdateAction()
|
||||||
|
{
|
||||||
|
await _handler.Handle(ValidCommand());
|
||||||
|
|
||||||
|
await _audit.Received(1).LogAsync(
|
||||||
|
action: "tipo_iva.update",
|
||||||
|
targetType: "TipoDeIva",
|
||||||
|
targetId: "1",
|
||||||
|
metadata: Arg.Any<object?>(),
|
||||||
|
ct: Arg.Any<CancellationToken>());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── audit fail-closed ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_AuditLoggerThrows_ExceptionBubblesUp()
|
||||||
|
{
|
||||||
|
_audit.LogAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(),
|
||||||
|
Arg.Any<object?>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(Task.FromException(new InvalidOperationException("audit fail")));
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<InvalidOperationException>(
|
||||||
|
() => _handler.Handle(ValidCommand()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── triangulation: activo toggle ─────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_DeactivateViaUpdate_ReturnsDtoWithActivoFalse()
|
||||||
|
{
|
||||||
|
var cmd = new UpdateTipoDeIvaCommand(Id: 1, Codigo: "IVA_21",
|
||||||
|
Descripcion: "IVA 21%", AplicaIVA: true, Activo: false);
|
||||||
|
|
||||||
|
var result = await _handler.Handle(cmd);
|
||||||
|
|
||||||
|
// The handler passes the Activo value to UpdateCosmeticoAsync
|
||||||
|
await _repo.Received(1).UpdateCosmeticoAsync(
|
||||||
|
1, "IVA_21", "IVA 21%", true, false,
|
||||||
|
Arg.Any<CancellationToken>());
|
||||||
|
Assert.False(result.Activo);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user