169 lines
5.8 KiB
C#
169 lines
5.8 KiB
C#
namespace MissionService.UnitTests.Application.Commands;
|
|
|
|
using FluentAssertions;
|
|
using Microsoft.Extensions.Logging;
|
|
using MissionService.API.Application.Commands;
|
|
using MissionService.Domain.AggregatesModel.CheckInAggregate;
|
|
using MissionService.Domain.SeedWork;
|
|
using NSubstitute;
|
|
using NSubstitute.ExceptionExtensions;
|
|
using Xunit;
|
|
|
|
/// <summary>
|
|
/// EN: Unit tests for PerformCheckInCommandHandler using NSubstitute.
|
|
/// VI: Unit tests cho PerformCheckInCommandHandler sử dụng NSubstitute.
|
|
/// </summary>
|
|
public class PerformCheckInCommandHandlerTests
|
|
{
|
|
private readonly IUserCheckInRepository _checkInRepository;
|
|
private readonly ILogger<PerformCheckInCommandHandler> _logger;
|
|
private readonly IUnitOfWork _unitOfWork;
|
|
private readonly PerformCheckInCommandHandler _handler;
|
|
|
|
public PerformCheckInCommandHandlerTests()
|
|
{
|
|
// EN: Create mocks with NSubstitute
|
|
// VI: Tạo mocks với NSubstitute
|
|
_checkInRepository = Substitute.For<IUserCheckInRepository>();
|
|
_logger = Substitute.For<ILogger<PerformCheckInCommandHandler>>();
|
|
_unitOfWork = Substitute.For<IUnitOfWork>();
|
|
|
|
// EN: Setup UnitOfWork on repository
|
|
// VI: Setup UnitOfWork trên repository
|
|
_checkInRepository.UnitOfWork.Returns(_unitOfWork);
|
|
|
|
_handler = new PerformCheckInCommandHandler(_checkInRepository, _logger);
|
|
}
|
|
|
|
#region Success Scenarios
|
|
|
|
[Fact]
|
|
public async Task Handle_ValidFirstCheckIn_ReturnsSuccessResult()
|
|
{
|
|
// Arrange
|
|
var userId = Guid.NewGuid();
|
|
var command = new PerformCheckInCommand(userId);
|
|
var userCheckIn = new UserCheckIn(userId);
|
|
|
|
_checkInRepository.GetOrCreateByUserIdAsync(userId, Arg.Any<CancellationToken>())
|
|
.Returns(userCheckIn);
|
|
_unitOfWork.SaveEntitiesAsync(Arg.Any<CancellationToken>())
|
|
.Returns(true);
|
|
|
|
// Act
|
|
var result = await _handler.Handle(command, CancellationToken.None);
|
|
|
|
// Assert
|
|
result.Success.Should().BeTrue();
|
|
result.CurrentStreak.Should().Be(1);
|
|
result.DailyPoints.Should().BeGreaterThan(0);
|
|
result.Message.Should().Contain("successful");
|
|
|
|
// Verify repository was called
|
|
await _checkInRepository.Received(1)
|
|
.GetOrCreateByUserIdAsync(userId, Arg.Any<CancellationToken>());
|
|
await _unitOfWork.Received(1)
|
|
.SaveEntitiesAsync(Arg.Any<CancellationToken>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Handle_MilestoneReached_ReturnsSuccessWithMilestoneMessage()
|
|
{
|
|
// Arrange
|
|
var userId = Guid.NewGuid();
|
|
var command = new PerformCheckInCommand(userId);
|
|
|
|
// EN: Create user with 6-day streak (next check-in is day 7 = milestone)
|
|
// VI: Tạo user với streak 6 ngày (check-in tiếp theo là ngày 7 = mốc thưởng)
|
|
// Note: We need to simulate this scenario differently as CheckIn is a domain method
|
|
var userCheckIn = new UserCheckIn(userId);
|
|
|
|
_checkInRepository.GetOrCreateByUserIdAsync(userId, Arg.Any<CancellationToken>())
|
|
.Returns(userCheckIn);
|
|
_unitOfWork.SaveEntitiesAsync(Arg.Any<CancellationToken>())
|
|
.Returns(true);
|
|
|
|
// Act
|
|
var result = await _handler.Handle(command, CancellationToken.None);
|
|
|
|
// Assert
|
|
result.Success.Should().BeTrue();
|
|
result.TotalPoints.Should().BeGreaterThan(0);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Failure Scenarios
|
|
|
|
[Fact]
|
|
public async Task Handle_AlreadyCheckedInToday_ReturnsFailureResult()
|
|
{
|
|
// Arrange
|
|
var userId = Guid.NewGuid();
|
|
var command = new PerformCheckInCommand(userId);
|
|
|
|
// EN: Create user who already checked in today
|
|
// VI: Tạo user đã check-in hôm nay
|
|
var userCheckIn = new UserCheckIn(userId);
|
|
userCheckIn.CheckIn(StreakBonusConfig.Default()); // First check-in
|
|
|
|
_checkInRepository.GetOrCreateByUserIdAsync(userId, Arg.Any<CancellationToken>())
|
|
.Returns(userCheckIn);
|
|
|
|
// Act
|
|
var result = await _handler.Handle(command, CancellationToken.None);
|
|
|
|
// Assert
|
|
result.Success.Should().BeFalse();
|
|
result.TotalPoints.Should().Be(0);
|
|
result.Message.Should().Contain("Already checked in today");
|
|
|
|
// Verify save was NOT called since no changes
|
|
await _unitOfWork.DidNotReceive()
|
|
.SaveEntitiesAsync(Arg.Any<CancellationToken>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Handle_RepositoryThrows_ReturnsFallbackFailureResult()
|
|
{
|
|
// Arrange
|
|
var userId = Guid.NewGuid();
|
|
var command = new PerformCheckInCommand(userId);
|
|
|
|
_checkInRepository.GetOrCreateByUserIdAsync(userId, Arg.Any<CancellationToken>())
|
|
.Throws(new Exception("Database connection failed"));
|
|
|
|
// Act
|
|
var result = await _handler.Handle(command, CancellationToken.None);
|
|
|
|
// Assert
|
|
result.Success.Should().BeFalse();
|
|
result.Message.Should().Contain("failed");
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Cancellation Tests
|
|
|
|
[Fact]
|
|
public async Task Handle_CancelledToken_PropagatesCancellation()
|
|
{
|
|
// Arrange
|
|
var userId = Guid.NewGuid();
|
|
var command = new PerformCheckInCommand(userId);
|
|
var cts = new CancellationTokenSource();
|
|
cts.Cancel();
|
|
|
|
_checkInRepository.GetOrCreateByUserIdAsync(userId, Arg.Any<CancellationToken>())
|
|
.ThrowsAsync(new OperationCanceledException());
|
|
|
|
// Act & Assert
|
|
// EN: Handler catches all exceptions, so it returns failure result instead of throwing
|
|
// VI: Handler bắt tất cả exceptions, nên trả về failure result thay vì throw
|
|
var result = await _handler.Handle(command, cts.Token);
|
|
result.Success.Should().BeFalse();
|
|
}
|
|
|
|
#endregion
|
|
}
|