using FluentAssertions; using FnbEngine.API.Application.Commands; using FnbEngine.Domain.AggregatesModel.RecipeAggregate; using FnbEngine.Domain.SeedWork; using Moq; using Xunit; namespace FnbEngine.UnitTests.Application.Commands; /// /// EN: Unit tests for Recipe command handlers (Create, Update, Delete). /// VI: Unit tests cho cac Recipe command handlers (Create, Update, Delete). /// public class RecipeCommandHandlersTests { private readonly Mock _repoMock; public RecipeCommandHandlersTests() { _repoMock = new Mock(); _repoMock.Setup(r => r.UnitOfWork.SaveEntitiesAsync(It.IsAny())) .ReturnsAsync(true); } // --- CreateRecipeCommandHandler --- [Fact] public async Task CreateRecipe_WithValidCommand_ShouldCreateAndReturnId() { // Arrange _repoMock.Setup(r => r.Add(It.IsAny())).Returns((Recipe r) => r); var handler = new CreateRecipeCommandHandler(_repoMock.Object); var command = new CreateRecipeCommand { ShopId = Guid.NewGuid(), ProductId = Guid.NewGuid(), Name = "Pho Bo", Instructions = "Boil broth", PrepTimeMinutes = 480 }; // Act var result = await handler.Handle(command, CancellationToken.None); // Assert result.Should().NotBeEmpty(); _repoMock.Verify(r => r.Add(It.IsAny()), Times.Once); _repoMock.Verify(r => r.UnitOfWork.SaveEntitiesAsync(It.IsAny()), Times.Once); } [Fact] public async Task CreateRecipe_WithIngredients_ShouldAddIngredientsToRecipe() { // Arrange Recipe? capturedRecipe = null; _repoMock.Setup(r => r.Add(It.IsAny())) .Callback(r => capturedRecipe = r) .Returns((Recipe r) => r); var handler = new CreateRecipeCommandHandler(_repoMock.Object); var command = new CreateRecipeCommand { ShopId = Guid.NewGuid(), ProductId = Guid.NewGuid(), Name = "Pho Bo", PrepTimeMinutes = 480, Ingredients = new List { new("Beef bones", 2.0m, "kg", 150_000m), new("Rice noodles", 0.5m, "kg", 30_000m, Guid.NewGuid(), 0.1m) } }; // Act await handler.Handle(command, CancellationToken.None); // Assert capturedRecipe.Should().NotBeNull(); capturedRecipe!.Ingredients.Should().HaveCount(2); capturedRecipe.Ingredients[0].IngredientName.Should().Be("Beef bones"); capturedRecipe.Ingredients[1].IngredientName.Should().Be("Rice noodles"); } [Fact] public async Task CreateRecipe_WithNullIngredients_ShouldCreateRecipeWithoutIngredients() { // Arrange Recipe? capturedRecipe = null; _repoMock.Setup(r => r.Add(It.IsAny())) .Callback(r => capturedRecipe = r) .Returns((Recipe r) => r); var handler = new CreateRecipeCommandHandler(_repoMock.Object); var command = new CreateRecipeCommand { ShopId = Guid.NewGuid(), ProductId = Guid.NewGuid(), Name = "Simple Dish", PrepTimeMinutes = 10 }; // Act await handler.Handle(command, CancellationToken.None); // Assert capturedRecipe.Should().NotBeNull(); capturedRecipe!.Ingredients.Should().BeEmpty(); } // --- UpdateRecipeCommandHandler --- [Fact] public async Task UpdateRecipe_WithExistingRecipe_ShouldUpdateAndReturnTrue() { // Arrange var recipeId = Guid.NewGuid(); var existingRecipe = new Recipe(Guid.NewGuid(), Guid.NewGuid(), "Old Name", "Old", 30); _repoMock.Setup(r => r.GetByIdAsync(recipeId, It.IsAny())) .ReturnsAsync(existingRecipe); var handler = new UpdateRecipeCommandHandler(_repoMock.Object); var command = new UpdateRecipeCommand { RecipeId = recipeId, ShopId = Guid.NewGuid(), ProductId = Guid.NewGuid(), Name = "New Name", Instructions = "New instructions", PrepTimeMinutes = 60, Ingredients = new List { new("Salt", 10m, "g", 500m) } }; // Act var result = await handler.Handle(command, CancellationToken.None); // Assert result.Should().BeTrue(); existingRecipe.Name.Should().Be("New Name"); existingRecipe.Ingredients.Should().HaveCount(1); _repoMock.Verify(r => r.Update(existingRecipe), Times.Once); } [Fact] public async Task UpdateRecipe_WithNonExistentRecipe_ShouldReturnFalse() { // Arrange _repoMock.Setup(r => r.GetByIdAsync(It.IsAny(), It.IsAny())) .ReturnsAsync((Recipe?)null); var handler = new UpdateRecipeCommandHandler(_repoMock.Object); var command = new UpdateRecipeCommand { RecipeId = Guid.NewGuid(), ShopId = Guid.NewGuid(), ProductId = Guid.NewGuid(), Name = "Anything", PrepTimeMinutes = 10 }; // Act var result = await handler.Handle(command, CancellationToken.None); // Assert result.Should().BeFalse(); _repoMock.Verify(r => r.Update(It.IsAny()), Times.Never); } // --- DeleteRecipeCommandHandler --- [Fact] public async Task DeleteRecipe_WithExistingRecipe_ShouldDeactivateAndReturnTrue() { // Arrange var recipeId = Guid.NewGuid(); var existingRecipe = new Recipe(Guid.NewGuid(), Guid.NewGuid(), "Pho Bo", null, 480); _repoMock.Setup(r => r.GetByIdAsync(recipeId, It.IsAny())) .ReturnsAsync(existingRecipe); var handler = new DeleteRecipeCommandHandler(_repoMock.Object); var command = new DeleteRecipeCommand(recipeId); // Act var result = await handler.Handle(command, CancellationToken.None); // Assert result.Should().BeTrue(); existingRecipe.IsActive.Should().BeFalse(); _repoMock.Verify(r => r.Update(existingRecipe), Times.Once); _repoMock.Verify(r => r.UnitOfWork.SaveEntitiesAsync(It.IsAny()), Times.Once); } [Fact] public async Task DeleteRecipe_WithNonExistentRecipe_ShouldReturnFalse() { // Arrange _repoMock.Setup(r => r.GetByIdAsync(It.IsAny(), It.IsAny())) .ReturnsAsync((Recipe?)null); var handler = new DeleteRecipeCommandHandler(_repoMock.Object); var command = new DeleteRecipeCommand(Guid.NewGuid()); // Act var result = await handler.Handle(command, CancellationToken.None); // Assert result.Should().BeFalse(); } }