244 lines
8.9 KiB
C#
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*");
|
|
}
|
|
}
|