// 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; /// /// EN: Unit tests for ProcessPaymentCallbackCommandHandler. /// VI: Unit tests cho ProcessPaymentCallbackCommandHandler. /// public class ProcessPaymentCallbackCommandHandlerTests { private readonly Mock _paymentRepositoryMock; private readonly Mock _vnpayGatewayMock; private readonly Mock _unitOfWorkMock; private readonly Mock> _loggerMock; private readonly ProcessPaymentCallbackCommandHandler _handler; public ProcessPaymentCallbackCommandHandlerTests() { _paymentRepositoryMock = new Mock(); _vnpayGatewayMock = new Mock(); _unitOfWorkMock = new Mock(); _loggerMock = new Mock>(); _vnpayGatewayMock.Setup(g => g.GatewayName).Returns("VNPay"); _unitOfWorkMock.Setup(u => u.SaveEntitiesAsync(It.IsAny())) .ReturnsAsync(true); _paymentRepositoryMock.Setup(r => r.UnitOfWork).Returns(_unitOfWorkMock.Object); var gateways = new List { _vnpayGatewayMock.Object }; _handler = new ProcessPaymentCallbackCommandHandler( _paymentRepositoryMock.Object, gateways, _loggerMock.Object); } /// /// 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. /// 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>(), It.IsAny())) .Returns(true); _paymentRepositoryMock.Setup(r => r.GetByOrderIdAsync(orderId)) .ReturnsAsync(payment); var parameters = new Dictionary { { "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()), Times.Once); } [Fact] public async Task Handle_WithInvalidSignature_ShouldRejectCallback() { // Arrange _vnpayGatewayMock.Setup(g => g.ValidateCallback(It.IsAny>(), It.IsAny())) .Returns(false); var parameters = new Dictionary { { "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()), Times.Never); _paymentRepositoryMock.Verify(r => r.Update(It.IsAny()), Times.Never); } [Fact] public async Task Handle_WithNonExistentPayment_ShouldReturnNotFound() { // Arrange var orderId = Guid.NewGuid(); _vnpayGatewayMock.Setup(g => g.ValidateCallback(It.IsAny>(), It.IsAny())) .Returns(true); _paymentRepositoryMock.Setup(r => r.GetByOrderIdAsync(orderId)) .ReturnsAsync((Payment?)null); var parameters = new Dictionary { { "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>(), It.IsAny())) .Returns(true); _paymentRepositoryMock.Setup(r => r.GetByOrderIdAsync(orderId)) .ReturnsAsync(payment); var parameters = new Dictionary { { "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()), Times.Once); } [Fact] public async Task Handle_WithInvalidTransactionReference_ShouldReturnInvalidReference() { // Arrange _vnpayGatewayMock.Setup(g => g.ValidateCallback(It.IsAny>(), It.IsAny())) .Returns(true); var parameters = new Dictionary { { "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 { { "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() .WithMessage("*MoMo*not supported*"); } }