// EN: Unit tests for CreatePaymentCommandHandler - payment creation with gateway integration.
// VI: Unit tests cho CreatePaymentCommandHandler - tao thanh toan voi tich hop cong thanh toan.
using FluentAssertions;
using MediatR;
using Moq;
using WalletService.API.Application.Commands.Payments;
using WalletService.Domain.AggregatesModel.PaymentAggregate;
using WalletService.Domain.Events;
using WalletService.Domain.Exceptions;
using WalletService.Domain.SeedWork;
using Microsoft.Extensions.Logging;
using Xunit;
namespace WalletService.UnitTests.Application.Commands;
///
/// EN: Unit tests for CreatePaymentCommandHandler.
/// VI: Unit tests cho CreatePaymentCommandHandler.
///
public class CreatePaymentCommandHandlerTests
{
private readonly Mock _paymentRepositoryMock;
private readonly Mock _vnpayGatewayMock;
private readonly Mock _unitOfWorkMock;
private readonly Mock> _loggerMock;
private readonly CreatePaymentCommandHandler _handler;
public CreatePaymentCommandHandlerTests()
{
_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);
_paymentRepositoryMock.Setup(r => r.Add(It.IsAny()))
.Returns((Payment p) => p);
var gateways = new List { _vnpayGatewayMock.Object };
_handler = new CreatePaymentCommandHandler(
_paymentRepositoryMock.Object,
gateways,
_loggerMock.Object);
}
[Fact]
public async Task Handle_WithValidPayment_ShouldCreatePayment()
{
// Arrange
_vnpayGatewayMock.Setup(g => g.CreatePaymentAsync(It.IsAny(), It.IsAny()))
.ReturnsAsync(new PaymentResult(
Success: true,
TransactionId: "TXN123",
PaymentUrl: "https://vnpay.vn/pay?txn=TXN123"));
var command = new CreatePaymentCommand(
OrderId: Guid.NewGuid(),
Amount: 100_000m,
Currency: "VND",
GatewayName: "VNPay",
ReturnUrl: "https://goodgo.vn/payment/return",
IpAddress: "127.0.0.1");
// Act
var result = await _handler.Handle(command, CancellationToken.None);
// Assert
result.Should().NotBeNull();
result.PaymentId.Should().NotBeEmpty();
result.OrderId.Should().Be(command.OrderId);
result.Amount.Should().Be(100_000m);
result.Currency.Should().Be("VND");
result.GatewayName.Should().Be("VNPay");
result.Status.Should().Be("Processing");
result.PaymentUrl.Should().Be("https://vnpay.vn/pay?txn=TXN123");
_paymentRepositoryMock.Verify(r => r.Add(It.IsAny()), Times.Once);
_unitOfWorkMock.Verify(u => u.SaveEntitiesAsync(It.IsAny()), Times.Once);
}
[Fact]
public async Task Handle_WithInvalidGateway_ShouldThrowWalletDomainException()
{
// Arrange
var command = new CreatePaymentCommand(
OrderId: Guid.NewGuid(),
Amount: 50_000m,
Currency: "VND",
GatewayName: "NonExistentGateway",
ReturnUrl: "https://goodgo.vn/payment/return",
IpAddress: "127.0.0.1");
// Act
var action = () => _handler.Handle(command, CancellationToken.None);
// Assert
// EN: Should throw WalletDomainException for unsupported gateway.
// VI: Nen throw WalletDomainException cho cong thanh toan khong ho tro.
await action.Should().ThrowAsync()
.WithMessage("*NonExistentGateway*not supported*");
}
[Fact]
public async Task Handle_WithGatewayFailure_ShouldCreatePaymentWithFailedStatus()
{
// Arrange
_vnpayGatewayMock.Setup(g => g.CreatePaymentAsync(It.IsAny(), It.IsAny()))
.ReturnsAsync(new PaymentResult(
Success: false,
ErrorCode: "TIMEOUT",
ErrorMessage: "Gateway timeout"));
var command = new CreatePaymentCommand(
OrderId: Guid.NewGuid(),
Amount: 200_000m,
Currency: "VND",
GatewayName: "VNPay",
ReturnUrl: "https://goodgo.vn/payment/return",
IpAddress: "127.0.0.1");
// Act
var result = await _handler.Handle(command, CancellationToken.None);
// Assert
// EN: Should create the payment entity but mark it as failed.
// VI: Nen tao payment entity nhung danh dau la that bai.
result.Should().NotBeNull();
result.Status.Should().Be("Failed");
result.PaymentUrl.Should().BeNull();
_paymentRepositoryMock.Verify(r => r.Add(It.IsAny()), Times.Once);
_unitOfWorkMock.Verify(u => u.SaveEntitiesAsync(It.IsAny()), Times.Once);
}
[Fact]
public void Handle_WithZeroAmount_ShouldThrowWalletDomainException()
{
// Arrange
// EN: Payment entity constructor validates amount > 0.
// VI: Constructor cua Payment entity validate amount > 0.
var command = new CreatePaymentCommand(
OrderId: Guid.NewGuid(),
Amount: 0m,
Currency: "VND",
GatewayName: "VNPay",
ReturnUrl: "https://goodgo.vn/payment/return",
IpAddress: "127.0.0.1");
_vnpayGatewayMock.Setup(g => g.CreatePaymentAsync(It.IsAny(), It.IsAny()))
.ReturnsAsync(new PaymentResult(Success: true, TransactionId: "TXN123", PaymentUrl: "https://vnpay.vn/pay"));
// Act
var action = () => _handler.Handle(command, CancellationToken.None);
// Assert
action.Should().ThrowAsync()
.WithMessage("*amount*greater than zero*");
}
[Fact]
public void Handle_WithNegativeAmount_ShouldThrowWalletDomainException()
{
// Arrange
var command = new CreatePaymentCommand(
OrderId: Guid.NewGuid(),
Amount: -100_000m,
Currency: "VND",
GatewayName: "VNPay",
ReturnUrl: "https://goodgo.vn/payment/return",
IpAddress: "127.0.0.1");
_vnpayGatewayMock.Setup(g => g.CreatePaymentAsync(It.IsAny(), It.IsAny()))
.ReturnsAsync(new PaymentResult(Success: true, TransactionId: "TXN123", PaymentUrl: "https://vnpay.vn/pay"));
// Act
var action = () => _handler.Handle(command, CancellationToken.None);
// Assert
action.Should().ThrowAsync()
.WithMessage("*amount*greater than zero*");
}
[Fact]
public async Task Handle_ShouldRaiseDomainEvent()
{
// Arrange
_vnpayGatewayMock.Setup(g => g.CreatePaymentAsync(It.IsAny(), It.IsAny()))
.ReturnsAsync(new PaymentResult(
Success: true,
TransactionId: "TXN456",
PaymentUrl: "https://vnpay.vn/pay?txn=TXN456"));
Payment? capturedPayment = null;
_paymentRepositoryMock.Setup(r => r.Add(It.IsAny()))
.Callback(p => capturedPayment = p)
.Returns((Payment p) => p);
var command = new CreatePaymentCommand(
OrderId: Guid.NewGuid(),
Amount: 150_000m,
Currency: "VND",
GatewayName: "VNPay",
ReturnUrl: "https://goodgo.vn/payment/return",
IpAddress: "127.0.0.1");
// Act
await _handler.Handle(command, CancellationToken.None);
// Assert
// EN: Payment constructor raises PaymentCreatedDomainEvent.
// VI: Constructor cua Payment phat ra PaymentCreatedDomainEvent.
capturedPayment.Should().NotBeNull();
capturedPayment!.DomainEvents.Should().NotBeEmpty();
capturedPayment.DomainEvents.Should().ContainItemsAssignableTo();
}
[Fact]
public async Task Handle_WithSuccessfulGateway_ShouldSetPaymentUrlAndTransactionId()
{
// Arrange
var expectedUrl = "https://vnpay.vn/pay?txn=ABC789";
var expectedTxnId = "ABC789";
_vnpayGatewayMock.Setup(g => g.CreatePaymentAsync(It.IsAny(), It.IsAny()))
.ReturnsAsync(new PaymentResult(
Success: true,
TransactionId: expectedTxnId,
PaymentUrl: expectedUrl));
var command = new CreatePaymentCommand(
OrderId: Guid.NewGuid(),
Amount: 300_000m,
Currency: "VND",
GatewayName: "VNPay",
ReturnUrl: "https://goodgo.vn/payment/return",
IpAddress: "192.168.1.1");
// Act
var result = await _handler.Handle(command, CancellationToken.None);
// Assert
result.PaymentUrl.Should().Be(expectedUrl);
result.Status.Should().Be("Processing");
}
}