// 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"); } }