Files
pos-system/microservices/services/wallet-service-net/tests/WalletService.UnitTests/Application/Commands/CreatePaymentCommandHandlerTests.cs
Ho Ngoc Hai 76d75c753b Migrate
2026-05-23 18:37:02 +07:00

252 lines
9.4 KiB
C#

// 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;
/// <summary>
/// EN: Unit tests for CreatePaymentCommandHandler.
/// VI: Unit tests cho CreatePaymentCommandHandler.
/// </summary>
public class CreatePaymentCommandHandlerTests
{
private readonly Mock<IPaymentRepository> _paymentRepositoryMock;
private readonly Mock<IPaymentGateway> _vnpayGatewayMock;
private readonly Mock<IUnitOfWork> _unitOfWorkMock;
private readonly Mock<ILogger<CreatePaymentCommandHandler>> _loggerMock;
private readonly CreatePaymentCommandHandler _handler;
public CreatePaymentCommandHandlerTests()
{
_paymentRepositoryMock = new Mock<IPaymentRepository>();
_vnpayGatewayMock = new Mock<IPaymentGateway>();
_unitOfWorkMock = new Mock<IUnitOfWork>();
_loggerMock = new Mock<ILogger<CreatePaymentCommandHandler>>();
_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);
_paymentRepositoryMock.Setup(r => r.Add(It.IsAny<Payment>()))
.Returns((Payment p) => p);
var gateways = new List<IPaymentGateway> { _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<PaymentRequest>(), It.IsAny<CancellationToken>()))
.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<Payment>()), Times.Once);
_unitOfWorkMock.Verify(u => u.SaveEntitiesAsync(It.IsAny<CancellationToken>()), 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<WalletDomainException>()
.WithMessage("*NonExistentGateway*not supported*");
}
[Fact]
public async Task Handle_WithGatewayFailure_ShouldCreatePaymentWithFailedStatus()
{
// Arrange
_vnpayGatewayMock.Setup(g => g.CreatePaymentAsync(It.IsAny<PaymentRequest>(), It.IsAny<CancellationToken>()))
.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<Payment>()), Times.Once);
_unitOfWorkMock.Verify(u => u.SaveEntitiesAsync(It.IsAny<CancellationToken>()), 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<PaymentRequest>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new PaymentResult(Success: true, TransactionId: "TXN123", PaymentUrl: "https://vnpay.vn/pay"));
// Act
var action = () => _handler.Handle(command, CancellationToken.None);
// Assert
action.Should().ThrowAsync<WalletDomainException>()
.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<PaymentRequest>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new PaymentResult(Success: true, TransactionId: "TXN123", PaymentUrl: "https://vnpay.vn/pay"));
// Act
var action = () => _handler.Handle(command, CancellationToken.None);
// Assert
action.Should().ThrowAsync<WalletDomainException>()
.WithMessage("*amount*greater than zero*");
}
[Fact]
public async Task Handle_ShouldRaiseDomainEvent()
{
// Arrange
_vnpayGatewayMock.Setup(g => g.CreatePaymentAsync(It.IsAny<PaymentRequest>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new PaymentResult(
Success: true,
TransactionId: "TXN456",
PaymentUrl: "https://vnpay.vn/pay?txn=TXN456"));
Payment? capturedPayment = null;
_paymentRepositoryMock.Setup(r => r.Add(It.IsAny<Payment>()))
.Callback<Payment>(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<PaymentCreatedDomainEvent>();
}
[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<PaymentRequest>(), It.IsAny<CancellationToken>()))
.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");
}
}