using FluentAssertions; using Microsoft.Extensions.Time.Testing; using NSubstitute; using NSubstitute.ExceptionExtensions; using SIGCM2.Application.Abstractions.Persistence; using SIGCM2.Application.Audit; using SIGCM2.Application.Pricing.ChargeableChars.Reactivate; using SIGCM2.Domain.Pricing.ChargeableChars; using SIGCM2.Domain.Pricing.Exceptions; namespace SIGCM2.Application.Tests.Pricing.ChargeableChars; /// /// PRC-001 — ReactivateChargeableCharConfigCommandHandler tests. /// Strict TDD — RED written before implementation. /// Covers: happy path, audit emission, audit fail-closed, repo exception propagation. /// public class ReactivateChargeableCharConfigHandlerTests { private readonly FakeTimeProvider _time = new(new DateTimeOffset(2026, 4, 20, 12, 0, 0, TimeSpan.Zero)); private readonly IChargeableCharConfigRepository _repo = Substitute.For(); private readonly IAuditLogger _audit = Substitute.For(); private readonly ReactivateChargeableCharConfigCommandHandler _handler; private static readonly DateOnly Today = new(2026, 4, 20); private static ChargeableCharConfig ClosedConfig() => ChargeableCharConfig.Rehydrate( id: 1L, productTypeId: null, symbol: "$", category: ChargeableCharCategories.Currency, price: 1.5m, validFrom: new DateOnly(2026, 1, 1), validTo: new DateOnly(2026, 4, 19), isActive: false); private static ChargeableCharConfig ActiveConfig() => ChargeableCharConfig.Rehydrate( id: 1L, productTypeId: null, symbol: "$", category: ChargeableCharCategories.Currency, price: 1.5m, validFrom: new DateOnly(2026, 1, 1), validTo: null, isActive: true); public ReactivateChargeableCharConfigHandlerTests() { _repo.ReactivateAsync(1L, Arg.Any()) .Returns(ActiveConfig()); _handler = new ReactivateChargeableCharConfigCommandHandler(_repo, _audit, _time); } private static ReactivateChargeableCharConfigCommand ValidCmd() => new(Id: 1L); // ── Happy path ────────────────────────────────────────────────────────────── [Fact] public async Task Handle_HappyPath_ReturnsResponse() { var result = await _handler.Handle(ValidCmd()); result.Should().NotBeNull(); result.Id.Should().Be(1L); result.Symbol.Should().Be("$"); result.IsActive.Should().BeTrue(); } [Fact] public async Task Handle_HappyPath_CallsReactivateAsync() { await _handler.Handle(ValidCmd()); await _repo.Received(1).ReactivateAsync(1L, Arg.Any()); } [Fact] public async Task Handle_HappyPath_EmitsAuditReactivate() { await _handler.Handle(ValidCmd()); await _audit.Received(1).LogAsync( action: "tasacion.chargeable_char.reactivate", targetType: "ChargeableCharConfig", targetId: "1", metadata: Arg.Any(), ct: Arg.Any()); } // ── Guard failures propagate ──────────────────────────────────────────────── [Fact] public async Task Handle_AlreadyActive_ThrowsReactivationNotAllowed() { _repo.ReactivateAsync(2L, Arg.Any()) .ThrowsAsync(new ChargeableCharConfigReactivationNotAllowedException(2L, "ALREADY_ACTIVE")); var act = async () => await _handler.Handle(new ReactivateChargeableCharConfigCommand(Id: 2L)); await act.Should().ThrowAsync() .Where(e => e.Reason == "ALREADY_ACTIVE"); } [Fact] public async Task Handle_VigenteExists_ThrowsReactivationNotAllowed() { _repo.ReactivateAsync(3L, Arg.Any()) .ThrowsAsync(new ChargeableCharConfigReactivationNotAllowedException(3L, "VIGENTE_EXISTS")); var act = async () => await _handler.Handle(new ReactivateChargeableCharConfigCommand(Id: 3L)); await act.Should().ThrowAsync() .Where(e => e.Reason == "VIGENTE_EXISTS"); } [Fact] public async Task Handle_PosteriorRowsExist_ThrowsReactivationNotAllowed() { _repo.ReactivateAsync(4L, Arg.Any()) .ThrowsAsync(new ChargeableCharConfigReactivationNotAllowedException(4L, "POSTERIOR_ROWS_EXIST")); var act = async () => await _handler.Handle(new ReactivateChargeableCharConfigCommand(Id: 4L)); await act.Should().ThrowAsync() .Where(e => e.Reason == "POSTERIOR_ROWS_EXIST"); } // ── Audit fail → rollback (fail-closed) ───────────────────────────────────── [Fact] public async Task Handle_AuditThrows_ExceptionPropagates() { _audit.LogAsync( Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .ThrowsAsync(new InvalidOperationException("Audit down")); var act = async () => await _handler.Handle(ValidCmd()); await act.Should().ThrowAsync() .WithMessage("Audit down"); } [Fact] public async Task Handle_AuditThrows_ReactivateWasCalled_TransactionNotCompleted() { _audit.LogAsync( Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .ThrowsAsync(new InvalidOperationException("Audit down")); var act = async () => await _handler.Handle(ValidCmd()); await act.Should().ThrowAsync(); await _repo.Received(1).ReactivateAsync(1L, Arg.Any()); } }