Part A — MedioId → ProductTypeId rename across all C# layers:
Domain, Application, Infrastructure, API, all test projects.
Solution was non-compilable after BD refactor (5c1675e); now compiles clean (0 errors).
Part B — PATCH /api/v1/admin/chargeable-chars/{id}/reactivate:
ReactivateChargeableCharConfigCommand/Handler, SP guard maps 50410/50411/50412
→ ChargeableCharConfigReactivationNotAllowedException(Reason) → HTTP 409.
Part C — DELETE /api/v1/admin/chargeable-chars/{id}:
DeleteChargeableCharConfigCommand/Handler, physical DELETE on SYSTEM_VERSIONED table.
KeyNotFoundException → 404 via ExceptionFilter.
Tests: +30 unit tests (TDD RED→GREEN). All 1266 unit tests pass.
114 lines
4.4 KiB
C#
114 lines
4.4 KiB
C#
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.Delete;
|
|
using SIGCM2.Domain.Pricing.ChargeableChars;
|
|
|
|
namespace SIGCM2.Application.Tests.Pricing.ChargeableChars;
|
|
|
|
/// <summary>
|
|
/// PRC-001 — DeleteChargeableCharConfigCommandHandler tests.
|
|
/// Strict TDD — RED written before implementation.
|
|
/// Covers: happy path, not-found, audit emission, audit fail-closed.
|
|
/// </summary>
|
|
public class DeleteChargeableCharConfigHandlerTests
|
|
{
|
|
private readonly FakeTimeProvider _time = new(new DateTimeOffset(2026, 4, 20, 12, 0, 0, TimeSpan.Zero));
|
|
private readonly IChargeableCharConfigRepository _repo = Substitute.For<IChargeableCharConfigRepository>();
|
|
private readonly IAuditLogger _audit = Substitute.For<IAuditLogger>();
|
|
private readonly DeleteChargeableCharConfigCommandHandler _handler;
|
|
|
|
private static ChargeableCharConfig SomeConfig() =>
|
|
ChargeableCharConfig.Rehydrate(
|
|
id: 1L, productTypeId: null, symbol: "$", category: ChargeableCharCategories.Currency,
|
|
price: 1.5m, validFrom: new DateOnly(2026, 1, 1), validTo: null, isActive: true);
|
|
|
|
public DeleteChargeableCharConfigHandlerTests()
|
|
{
|
|
_repo.GetByIdAsync(1L, Arg.Any<CancellationToken>())
|
|
.Returns(SomeConfig());
|
|
|
|
_handler = new DeleteChargeableCharConfigCommandHandler(_repo, _audit, _time);
|
|
}
|
|
|
|
private static DeleteChargeableCharConfigCommand 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);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Handle_HappyPath_CallsDeleteAsync()
|
|
{
|
|
await _handler.Handle(ValidCmd());
|
|
|
|
await _repo.Received(1).DeleteAsync(1L, Arg.Any<CancellationToken>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Handle_HappyPath_EmitsAuditDelete()
|
|
{
|
|
await _handler.Handle(ValidCmd());
|
|
|
|
await _audit.Received(1).LogAsync(
|
|
action: "tasacion.chargeable_char.delete",
|
|
targetType: "ChargeableCharConfig",
|
|
targetId: "1",
|
|
metadata: Arg.Any<object?>(),
|
|
ct: Arg.Any<CancellationToken>());
|
|
}
|
|
|
|
// ── Not found ───────────────────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public async Task Handle_ConfigNotFound_ThrowsKeyNotFoundException()
|
|
{
|
|
_repo.GetByIdAsync(99L, Arg.Any<CancellationToken>())
|
|
.Returns((ChargeableCharConfig?)null);
|
|
|
|
var act = async () => await _handler.Handle(new DeleteChargeableCharConfigCommand(Id: 99L));
|
|
|
|
await act.Should().ThrowAsync<KeyNotFoundException>();
|
|
}
|
|
|
|
// ── Audit fail → rollback (fail-closed) ─────────────────────────────────────
|
|
|
|
[Fact]
|
|
public async Task Handle_AuditThrows_ExceptionPropagates()
|
|
{
|
|
_audit.LogAsync(
|
|
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(),
|
|
Arg.Any<object?>(), Arg.Any<CancellationToken>())
|
|
.ThrowsAsync(new InvalidOperationException("Audit down"));
|
|
|
|
var act = async () => await _handler.Handle(ValidCmd());
|
|
|
|
await act.Should().ThrowAsync<InvalidOperationException>()
|
|
.WithMessage("Audit down");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Handle_AuditThrows_DeleteWasCalled_TransactionNotCompleted()
|
|
{
|
|
_audit.LogAsync(
|
|
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(),
|
|
Arg.Any<object?>(), Arg.Any<CancellationToken>())
|
|
.ThrowsAsync(new InvalidOperationException("Audit down"));
|
|
|
|
var act = async () => await _handler.Handle(ValidCmd());
|
|
await act.Should().ThrowAsync<InvalidOperationException>();
|
|
|
|
await _repo.Received(1).DeleteAsync(1L, Arg.Any<CancellationToken>());
|
|
}
|
|
}
|