Files
pos-system/services/fnb-engine-net/tests/FnbEngine.UnitTests/Domain/TableAggregateTests.cs
Ho Ngoc Hai 6061164873 feat: add multi-tenant row-level security across 5 services and 96 FnB engine unit tests
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>
2026-03-06 13:40:34 +07:00

240 lines
5.9 KiB
C#

using FluentAssertions;
using FnbEngine.Domain.AggregatesModel.TableAggregate;
using FnbEngine.Domain.Exceptions;
using Xunit;
namespace FnbEngine.UnitTests.Domain;
/// <summary>
/// EN: Unit tests for Table aggregate behavior.
/// VI: Unit tests cho hanh vi aggregate Table.
/// </summary>
public class TableAggregateTests
{
private static readonly Guid ValidShopId = Guid.NewGuid();
[Fact]
public void Constructor_WithValidData_ShouldCreateAvailableTable()
{
// Act
var table = new Table(ValidShopId, "A-01", 4, "Main Hall");
// Assert
table.Id.Should().NotBeEmpty();
table.ShopId.Should().Be(ValidShopId);
table.TableNumber.Should().Be("A-01");
table.Capacity.Should().Be(4);
table.Zone.Should().Be("Main Hall");
table.Status.Should().Be(TableStatus.Available);
table.StatusId.Should().Be(TableStatus.Available.Id);
table.HourlyRate.Should().Be(0);
table.QrToken.Should().BeNull();
table.PositionX.Should().BeNull();
table.PositionY.Should().BeNull();
table.CreatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(5));
}
[Fact]
public void Constructor_WithEmptyShopId_ShouldThrowDomainException()
{
// Act
var action = () => new Table(Guid.Empty, "A-01", 4);
// Assert
action.Should().Throw<DomainException>()
.WithMessage("*Shop ID*");
}
[Fact]
public void Constructor_WithEmptyTableNumber_ShouldThrowDomainException()
{
// Act
var action = () => new Table(ValidShopId, "", 4);
// Assert
action.Should().Throw<DomainException>()
.WithMessage("*Table number*");
}
[Fact]
public void Constructor_WithWhitespaceTableNumber_ShouldThrowDomainException()
{
// Act
var action = () => new Table(ValidShopId, " ", 4);
// Assert
action.Should().Throw<DomainException>();
}
[Fact]
public void Constructor_WithZeroCapacity_ShouldThrowDomainException()
{
// Act
var action = () => new Table(ValidShopId, "A-01", 0);
// Assert
action.Should().Throw<DomainException>()
.WithMessage("*Capacity*");
}
[Fact]
public void Constructor_WithNegativeCapacity_ShouldThrowDomainException()
{
// Act
var action = () => new Table(ValidShopId, "A-01", -1);
// Assert
action.Should().Throw<DomainException>();
}
[Fact]
public void Constructor_ShouldTrimTableNumber()
{
// Act
var table = new Table(ValidShopId, " A-01 ", 4);
// Assert
table.TableNumber.Should().Be("A-01");
}
[Fact]
public void Constructor_ShouldTrimZone()
{
// Act
var table = new Table(ValidShopId, "A-01", 4, " VIP ");
// Assert
table.Zone.Should().Be("VIP");
}
[Fact]
public void MarkAsOccupied_WhenAvailable_ShouldSucceed()
{
// Arrange
var table = new Table(ValidShopId, "B-02", 6);
// Act
table.MarkAsOccupied();
// Assert
table.Status.Should().Be(TableStatus.Occupied);
table.StatusId.Should().Be(TableStatus.Occupied.Id);
table.UpdatedAt.Should().NotBeNull();
}
[Fact]
public void MarkAsOccupied_WhenCleaning_ShouldThrowDomainException()
{
// Arrange
var table = new Table(ValidShopId, "C-03", 2);
table.MarkAsCleaning();
// Act
var action = () => table.MarkAsOccupied();
// Assert
action.Should().Throw<DomainException>();
}
[Fact]
public void MarkAsOccupied_WhenAlreadyOccupied_ShouldThrowDomainException()
{
// Arrange
var table = new Table(ValidShopId, "D-04", 2);
table.MarkAsOccupied();
// Act
var action = () => table.MarkAsOccupied();
// Assert
action.Should().Throw<DomainException>();
}
[Fact]
public void MarkAsAvailable_FromAnyStatus_ShouldSucceed()
{
// Arrange
var table = new Table(ValidShopId, "E-05", 4);
table.MarkAsOccupied();
// Act
table.MarkAsAvailable();
// Assert
table.Status.Should().Be(TableStatus.Available);
}
[Fact]
public void MarkAsCleaning_ShouldSetCleaningStatus()
{
// Arrange
var table = new Table(ValidShopId, "F-06", 4);
// Act
table.MarkAsCleaning();
// Assert
table.Status.Should().Be(TableStatus.Cleaning);
table.StatusId.Should().Be(TableStatus.Cleaning.Id);
}
[Fact]
public void SetHourlyRate_ShouldUpdateRate()
{
// Arrange
var table = new Table(ValidShopId, "G-07", 4);
// Act
table.SetHourlyRate(50_000m);
// Assert
table.HourlyRate.Should().Be(50_000m);
table.UpdatedAt.Should().NotBeNull();
}
[Fact]
public void SetPosition_ShouldUpdateCoordinates()
{
// Arrange
var table = new Table(ValidShopId, "H-08", 4);
// Act
table.SetPosition(100, 200);
// Assert
table.PositionX.Should().Be(100);
table.PositionY.Should().Be(200);
table.UpdatedAt.Should().NotBeNull();
}
[Fact]
public void GenerateQrToken_ShouldReturnNonEmptyToken()
{
// Arrange
var table = new Table(ValidShopId, "I-09", 4);
// Act
var token = table.GenerateQrToken();
// Assert
token.Should().NotBeNullOrEmpty();
token.Should().HaveLength(16);
table.QrToken.Should().Be(token);
table.UpdatedAt.Should().NotBeNull();
}
[Fact]
public void ClearQrToken_ShouldSetTokenToNull()
{
// Arrange
var table = new Table(ValidShopId, "J-10", 4);
table.GenerateQrToken();
// Act
table.ClearQrToken();
// Assert
table.QrToken.Should().BeNull();
}
}