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; /// /// EN: Unit tests for PerformCheckInCommandHandler using NSubstitute. /// VI: Unit tests cho PerformCheckInCommandHandler sử dụng NSubstitute. /// public class PerformCheckInCommandHandlerTests { private readonly IUserCheckInRepository _checkInRepository; private readonly ILogger _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(); _logger = Substitute.For>(); _unitOfWork = Substitute.For(); // 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()) .Returns(userCheckIn); _unitOfWork.SaveEntitiesAsync(Arg.Any()) .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()); await _unitOfWork.Received(1) .SaveEntitiesAsync(Arg.Any()); } [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()) .Returns(userCheckIn); _unitOfWork.SaveEntitiesAsync(Arg.Any()) .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()) .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()); } [Fact] public async Task Handle_RepositoryThrows_ReturnsFallbackFailureResult() { // Arrange var userId = Guid.NewGuid(); var command = new PerformCheckInCommand(userId); _checkInRepository.GetOrCreateByUserIdAsync(userId, Arg.Any()) .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()) .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 }