Files
Ho Ngoc Hai 76d75c753b Migrate
2026-05-23 18:37:02 +07:00

244 lines
8.9 KiB
C#

// EN: Unit tests for ProcessPaymentCallbackCommandHandler - payment callback (IPN) processing.
// VI: Unit tests cho ProcessPaymentCallbackCommandHandler - xu ly callback (IPN) thanh toan.
using FluentAssertions;
using Moq;
using WalletService.API.Application.Commands.Payments;
using WalletService.Domain.AggregatesModel.PaymentAggregate;
using WalletService.Domain.Exceptions;
using WalletService.Domain.SeedWork;
using Microsoft.Extensions.Logging;
using Xunit;
namespace WalletService.UnitTests.Application.Commands;
/// <summary>
/// EN: Unit tests for ProcessPaymentCallbackCommandHandler.
/// VI: Unit tests cho ProcessPaymentCallbackCommandHandler.
/// </summary>
public class ProcessPaymentCallbackCommandHandlerTests
{
private readonly Mock<IPaymentRepository> _paymentRepositoryMock;
private readonly Mock<IPaymentGateway> _vnpayGatewayMock;
private readonly Mock<IUnitOfWork> _unitOfWorkMock;
private readonly Mock<ILogger<ProcessPaymentCallbackCommandHandler>> _loggerMock;
private readonly ProcessPaymentCallbackCommandHandler _handler;
public ProcessPaymentCallbackCommandHandlerTests()
{
_paymentRepositoryMock = new Mock<IPaymentRepository>();
_vnpayGatewayMock = new Mock<IPaymentGateway>();
_unitOfWorkMock = new Mock<IUnitOfWork>();
_loggerMock = new Mock<ILogger<ProcessPaymentCallbackCommandHandler>>();
_vnpayGatewayMock.Setup(g => g.GatewayName).Returns("VNPay");
_unitOfWorkMock.Setup(u => u.SaveEntitiesAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
_paymentRepositoryMock.Setup(r => r.UnitOfWork).Returns(_unitOfWorkMock.Object);
var gateways = new List<IPaymentGateway> { _vnpayGatewayMock.Object };
_handler = new ProcessPaymentCallbackCommandHandler(
_paymentRepositoryMock.Object,
gateways,
_loggerMock.Object);
}
/// <summary>
/// EN: Helper to create a Payment entity in Processing state for callback testing.
/// VI: Helper de tao Payment entity o trang thai Processing de test callback.
/// </summary>
private static Payment CreateProcessingPayment(Guid orderId)
{
var payment = new Payment(orderId, 100_000m, "VND", "VNPay");
payment.MarkAsProcessing("https://vnpay.vn/pay", "TXN_ORIGINAL");
return payment;
}
[Fact]
public async Task Handle_WithValidCallback_ShouldUpdatePaymentStatusToCompleted()
{
// Arrange
var orderId = Guid.NewGuid();
var payment = CreateProcessingPayment(orderId);
_vnpayGatewayMock.Setup(g => g.ValidateCallback(It.IsAny<IDictionary<string, string>>(), It.IsAny<string>()))
.Returns(true);
_paymentRepositoryMock.Setup(r => r.GetByOrderIdAsync(orderId))
.ReturnsAsync(payment);
var parameters = new Dictionary<string, string>
{
{ "vnp_SecureHash", "valid_hash_123" },
{ "vnp_TxnRef", orderId.ToString() },
{ "vnp_ResponseCode", "00" },
{ "vnp_TransactionNo", "VNP_TXN_001" }
};
var command = new ProcessPaymentCallbackCommand("VNPay", parameters);
// Act
var result = await _handler.Handle(command, CancellationToken.None);
// Assert
result.Should().NotBeNull();
result.Success.Should().BeTrue();
result.PaymentId.Should().Be(payment.Id);
result.OrderId.Should().Be(orderId);
result.Status.Should().Be("Completed");
result.ErrorMessage.Should().BeNull();
_paymentRepositoryMock.Verify(r => r.Update(payment), Times.Once);
_unitOfWorkMock.Verify(u => u.SaveEntitiesAsync(It.IsAny<CancellationToken>()), Times.Once);
}
[Fact]
public async Task Handle_WithInvalidSignature_ShouldRejectCallback()
{
// Arrange
_vnpayGatewayMock.Setup(g => g.ValidateCallback(It.IsAny<IDictionary<string, string>>(), It.IsAny<string>()))
.Returns(false);
var parameters = new Dictionary<string, string>
{
{ "vnp_SecureHash", "invalid_hash" },
{ "vnp_TxnRef", Guid.NewGuid().ToString() },
{ "vnp_ResponseCode", "00" }
};
var command = new ProcessPaymentCallbackCommand("VNPay", parameters);
// Act
var result = await _handler.Handle(command, CancellationToken.None);
// Assert
// EN: Invalid signature should be rejected without updating any payment.
// VI: Chu ky khong hop le nen bi tu choi ma khong cap nhat payment nao.
result.Should().NotBeNull();
result.Success.Should().BeFalse();
result.Status.Should().Be("InvalidSignature");
result.ErrorMessage.Should().Contain("Invalid callback signature");
_paymentRepositoryMock.Verify(r => r.GetByOrderIdAsync(It.IsAny<Guid>()), Times.Never);
_paymentRepositoryMock.Verify(r => r.Update(It.IsAny<Payment>()), Times.Never);
}
[Fact]
public async Task Handle_WithNonExistentPayment_ShouldReturnNotFound()
{
// Arrange
var orderId = Guid.NewGuid();
_vnpayGatewayMock.Setup(g => g.ValidateCallback(It.IsAny<IDictionary<string, string>>(), It.IsAny<string>()))
.Returns(true);
_paymentRepositoryMock.Setup(r => r.GetByOrderIdAsync(orderId))
.ReturnsAsync((Payment?)null);
var parameters = new Dictionary<string, string>
{
{ "vnp_SecureHash", "valid_hash" },
{ "vnp_TxnRef", orderId.ToString() },
{ "vnp_ResponseCode", "00" },
{ "vnp_TransactionNo", "VNP_TXN_002" }
};
var command = new ProcessPaymentCallbackCommand("VNPay", parameters);
// Act
var result = await _handler.Handle(command, CancellationToken.None);
// Assert
result.Should().NotBeNull();
result.Success.Should().BeFalse();
result.PaymentId.Should().BeNull();
result.OrderId.Should().Be(orderId);
result.Status.Should().Be("NotFound");
result.ErrorMessage.Should().Contain("Payment not found");
}
[Fact]
public async Task Handle_WithFailedResponseCode_ShouldUpdatePaymentToFailed()
{
// Arrange
var orderId = Guid.NewGuid();
var payment = CreateProcessingPayment(orderId);
_vnpayGatewayMock.Setup(g => g.ValidateCallback(It.IsAny<IDictionary<string, string>>(), It.IsAny<string>()))
.Returns(true);
_paymentRepositoryMock.Setup(r => r.GetByOrderIdAsync(orderId))
.ReturnsAsync(payment);
var parameters = new Dictionary<string, string>
{
{ "vnp_SecureHash", "valid_hash" },
{ "vnp_TxnRef", orderId.ToString() },
{ "vnp_ResponseCode", "24" },
{ "vnp_TransactionNo", "" },
{ "vnp_OrderInfo", "Customer cancelled payment" }
};
var command = new ProcessPaymentCallbackCommand("VNPay", parameters);
// Act
var result = await _handler.Handle(command, CancellationToken.None);
// Assert
// EN: Failed response code should mark payment as failed.
// VI: Ma phan hoi that bai nen danh dau payment la that bai.
result.Should().NotBeNull();
result.Success.Should().BeFalse();
result.Status.Should().Be("Failed");
result.ErrorMessage.Should().Contain("24");
_paymentRepositoryMock.Verify(r => r.Update(payment), Times.Once);
_unitOfWorkMock.Verify(u => u.SaveEntitiesAsync(It.IsAny<CancellationToken>()), Times.Once);
}
[Fact]
public async Task Handle_WithInvalidTransactionReference_ShouldReturnInvalidReference()
{
// Arrange
_vnpayGatewayMock.Setup(g => g.ValidateCallback(It.IsAny<IDictionary<string, string>>(), It.IsAny<string>()))
.Returns(true);
var parameters = new Dictionary<string, string>
{
{ "vnp_SecureHash", "valid_hash" },
{ "vnp_TxnRef", "not-a-valid-guid" },
{ "vnp_ResponseCode", "00" }
};
var command = new ProcessPaymentCallbackCommand("VNPay", parameters);
// Act
var result = await _handler.Handle(command, CancellationToken.None);
// Assert
result.Should().NotBeNull();
result.Success.Should().BeFalse();
result.Status.Should().Be("InvalidReference");
result.ErrorMessage.Should().Contain("Invalid transaction reference");
}
[Fact]
public async Task Handle_WithUnsupportedGateway_ShouldThrowWalletDomainException()
{
// Arrange
var parameters = new Dictionary<string, string>
{
{ "vnp_SecureHash", "hash" },
{ "vnp_TxnRef", Guid.NewGuid().ToString() }
};
var command = new ProcessPaymentCallbackCommand("MoMo", parameters);
// Act
var action = () => _handler.Handle(command, CancellationToken.None);
// Assert
await action.Should().ThrowAsync<WalletDomainException>()
.WithMessage("*MoMo*not supported*");
}
}