252 lines
9.4 KiB
C#
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");
|
|
}
|
|
}
|