Security (P0-5): - Implement ITenantProvider + HttpContextTenantProvider per service (order, fnb, inventory, catalog, wallet) - Add EF Core global query filters for tenant isolation (shop_id/user_id based) - Add TenantMiddleware setting PostgreSQL session variables for RLS - Create PostgreSQL RLS policies script (scripts/db/rls-policies.sql) - Adapter pattern bridges API-layer to Infrastructure-layer (Clean Architecture) - Bypass mechanisms for admin roles, service-to-service calls, and migrations Testing (P1-12): - Add 96 unit tests for fnb-engine (up from 3) - 57 domain entity tests: Table(18), KitchenTicket(12), Session(8), Reservation(13), Recipe(6) - 39 command handler tests: CRUD operations, status transitions, validation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
125 lines
4.0 KiB
C#
125 lines
4.0 KiB
C#
using FluentAssertions;
|
|
using FnbEngine.Domain.AggregatesModel.RecipeAggregate;
|
|
using Xunit;
|
|
|
|
namespace FnbEngine.UnitTests.Domain;
|
|
|
|
/// <summary>
|
|
/// EN: Unit tests for Recipe aggregate behavior.
|
|
/// VI: Unit tests cho hanh vi aggregate Recipe.
|
|
/// </summary>
|
|
public class RecipeTests
|
|
{
|
|
private static readonly Guid ValidShopId = Guid.NewGuid();
|
|
private static readonly Guid ValidProductId = Guid.NewGuid();
|
|
|
|
[Fact]
|
|
public void Constructor_WithValidData_ShouldCreateActiveRecipe()
|
|
{
|
|
// Act
|
|
var recipe = new Recipe(ValidShopId, ValidProductId, "Pho Bo", "Boil broth for 8 hours", 480);
|
|
|
|
// Assert
|
|
recipe.Id.Should().NotBeEmpty();
|
|
recipe.ShopId.Should().Be(ValidShopId);
|
|
recipe.ProductId.Should().Be(ValidProductId);
|
|
recipe.Name.Should().Be("Pho Bo");
|
|
recipe.Instructions.Should().Be("Boil broth for 8 hours");
|
|
recipe.PrepTimeMinutes.Should().Be(480);
|
|
recipe.IsActive.Should().BeTrue();
|
|
recipe.Ingredients.Should().BeEmpty();
|
|
recipe.CreatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(5));
|
|
recipe.UpdatedAt.Should().BeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_WithNullInstructions_ShouldAllowNull()
|
|
{
|
|
var recipe = new Recipe(ValidShopId, ValidProductId, "Simple Dish", null, 10);
|
|
recipe.Instructions.Should().BeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void Update_ShouldModifyAllFields()
|
|
{
|
|
// Arrange
|
|
var recipe = new Recipe(ValidShopId, ValidProductId, "Old Name", "Old instructions", 30);
|
|
var newProductId = Guid.NewGuid();
|
|
|
|
// Act
|
|
recipe.Update(newProductId, "New Name", "New instructions", 60);
|
|
|
|
// Assert
|
|
recipe.ProductId.Should().Be(newProductId);
|
|
recipe.Name.Should().Be("New Name");
|
|
recipe.Instructions.Should().Be("New instructions");
|
|
recipe.PrepTimeMinutes.Should().Be(60);
|
|
recipe.UpdatedAt.Should().NotBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void AddIngredient_ShouldAppendToIngredientsList()
|
|
{
|
|
// Arrange
|
|
var recipe = new Recipe(ValidShopId, ValidProductId, "Pho Bo", null, 480);
|
|
|
|
// Act
|
|
recipe.AddIngredient("Beef bones", 2.0m, "kg", 150_000m);
|
|
recipe.AddIngredient("Rice noodles", 0.5m, "kg", 30_000m);
|
|
|
|
// Assert
|
|
recipe.Ingredients.Should().HaveCount(2);
|
|
recipe.Ingredients[0].IngredientName.Should().Be("Beef bones");
|
|
recipe.Ingredients[0].Quantity.Should().Be(2.0m);
|
|
recipe.Ingredients[0].Unit.Should().Be("kg");
|
|
recipe.Ingredients[0].CostPerUnit.Should().Be(150_000m);
|
|
recipe.Ingredients[1].IngredientName.Should().Be("Rice noodles");
|
|
}
|
|
|
|
[Fact]
|
|
public void AddIngredient_WithInventoryItemId_ShouldSetCOGSFields()
|
|
{
|
|
// Arrange
|
|
var recipe = new Recipe(ValidShopId, ValidProductId, "Pho Bo", null, 480);
|
|
var inventoryItemId = Guid.NewGuid();
|
|
|
|
// Act
|
|
recipe.AddIngredient("Beef", 1.0m, "kg", 200_000m, inventoryItemId, 0.3m);
|
|
|
|
// Assert
|
|
var ingredient = recipe.Ingredients.Single();
|
|
ingredient.InventoryItemId.Should().Be(inventoryItemId);
|
|
ingredient.QuantityPerServing.Should().Be(0.3m);
|
|
}
|
|
|
|
[Fact]
|
|
public void ClearIngredients_ShouldRemoveAll()
|
|
{
|
|
// Arrange
|
|
var recipe = new Recipe(ValidShopId, ValidProductId, "Pho Bo", null, 480);
|
|
recipe.AddIngredient("Beef bones", 2.0m, "kg", 150_000m);
|
|
recipe.AddIngredient("Noodles", 0.5m, "kg", 30_000m);
|
|
|
|
// Act
|
|
recipe.ClearIngredients();
|
|
|
|
// Assert
|
|
recipe.Ingredients.Should().BeEmpty();
|
|
recipe.UpdatedAt.Should().NotBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void Deactivate_ShouldSetIsActiveToFalse()
|
|
{
|
|
// Arrange
|
|
var recipe = new Recipe(ValidShopId, ValidProductId, "Pho Bo", null, 480);
|
|
|
|
// Act
|
|
recipe.Deactivate();
|
|
|
|
// Assert
|
|
recipe.IsActive.Should().BeFalse();
|
|
recipe.UpdatedAt.Should().NotBeNull();
|
|
}
|
|
}
|