Files
pos-system/services/merchant-service-net/tests/MerchantService.UnitTests/Domain/ShopAggregateTests.cs

355 lines
9.3 KiB
C#

using FluentAssertions;
using MerchantService.Domain.AggregatesModel.ShopAggregate;
using MerchantService.Domain.Events;
using MerchantService.Domain.Exceptions;
using Xunit;
namespace MerchantService.UnitTests.Domain;
/// <summary>
/// EN: Unit tests for Shop aggregate root — creation, updates, status lifecycle, and branches.
/// VI: Unit tests cho aggregate root Shop — tạo mới, cập nhật, vòng đời trạng thái, và chi nhánh.
/// </summary>
public class ShopAggregateTests
{
// ──────── HELPERS ────────
private static Shop CreateValidShop(
ShopType? type = null,
BusinessCategory? category = null)
{
return new Shop(
Guid.NewGuid(),
"Test Coffee Shop",
"test-coffee-shop",
type ?? ShopType.Hybrid,
category ?? BusinessCategory.Cafe);
}
private static Address CreateTestAddress() => new()
{
Street = "123 Nguyễn Huệ",
District = "Quận 1",
City = "Hồ Chí Minh",
PostalCode = "700000",
CountryCode = "VN"
};
// ──────── CREATION TESTS ────────
[Fact]
public void CreateShop_WithValidData_ShouldStartInDraftStatus()
{
// Act
var shop = CreateValidShop();
// Assert
shop.Id.Should().NotBe(Guid.Empty);
shop.Name.Should().Be("Test Coffee Shop");
shop.Slug.Should().Be("test-coffee-shop");
shop.Status.Should().Be(ShopStatus.Draft);
shop.IsDeleted.Should().BeFalse();
shop.Branches.Should().BeEmpty();
}
[Fact]
public void CreateShop_ShouldRaiseShopCreatedDomainEvent()
{
// Act
var shop = CreateValidShop();
// Assert
shop.DomainEvents.Should().ContainSingle()
.Which.Should().BeOfType<ShopCreatedDomainEvent>();
}
[Fact]
public void CreateShop_WithCafeCategory_ShouldAssignFnBFeatures()
{
// Act
var shop = CreateValidShop(category: BusinessCategory.Cafe);
// Assert
shop.Features.HasInventory.Should().BeTrue();
shop.Features.HasTables.Should().BeTrue();
shop.Features.HasKitchen.Should().BeTrue();
shop.Features.HasDelivery.Should().BeTrue();
shop.Features.HasBooking.Should().BeFalse();
}
[Fact]
public void CreateShop_WithEmptyMerchantId_ShouldThrowDomainException()
{
// Act
var act = () => new Shop(Guid.Empty, "Shop", "shop", ShopType.Hybrid, BusinessCategory.FoodBeverage);
// Assert
act.Should().Throw<DomainException>()
.WithMessage("Merchant ID cannot be empty");
}
[Fact]
public void CreateShop_WithEmptyName_ShouldThrowDomainException()
{
// Act
var act = () => new Shop(Guid.NewGuid(), "", "shop", ShopType.Hybrid, BusinessCategory.FoodBeverage);
// Assert
act.Should().Throw<DomainException>()
.WithMessage("Shop name cannot be empty");
}
[Fact]
public void CreateShop_WithInvalidSlug_ShouldThrowDomainException()
{
// Act
var act = () => new Shop(
Guid.NewGuid(), "Shop", "INVALID SLUG!", ShopType.Hybrid, BusinessCategory.FoodBeverage);
// Assert
act.Should().Throw<DomainException>()
.WithMessage("Slug must contain only lowercase letters, numbers, and hyphens");
}
// ──────── UPDATE TESTS ────────
[Fact]
public void UpdateInfo_ShouldUpdateNameAndDescription()
{
// Arrange
var shop = CreateValidShop();
// Act
shop.UpdateInfo("New Name", "New desc");
// Assert
shop.Name.Should().Be("New Name");
shop.Description.Should().Be("New desc");
shop.UpdatedAt.Should().NotBeNull();
}
[Fact]
public void UpdateSlug_WithValidSlug_ShouldUpdateSuccessfully()
{
// Arrange
var shop = CreateValidShop();
// Act
shop.UpdateSlug("new-slug-123");
// Assert
shop.Slug.Should().Be("new-slug-123");
}
[Fact]
public void UpdateSlug_WithInvalidSlug_ShouldThrowDomainException()
{
// Arrange
var shop = CreateValidShop();
// Act
var act = () => shop.UpdateSlug("Invalid Slug!");
// Assert
act.Should().Throw<DomainException>();
}
[Fact]
public void UpdateFeatures_ShouldOverrideFeatures()
{
// Arrange
var shop = CreateValidShop();
var newFeatures = new ShopFeatures { HasBooking = true, HasShipping = true };
// Act
shop.UpdateFeatures(newFeatures);
// Assert
shop.Features.HasBooking.Should().BeTrue();
shop.Features.HasShipping.Should().BeTrue();
shop.Features.HasTables.Should().BeFalse(); // Overridden from Cafe defaults
}
[Fact]
public void UpdateImages_ShouldSetLogoAndCover()
{
// Arrange
var shop = CreateValidShop();
// Act
shop.UpdateImages("https://cdn/logo.png", "https://cdn/cover.png");
// Assert
shop.LogoUrl.Should().Be("https://cdn/logo.png");
shop.CoverImageUrl.Should().Be("https://cdn/cover.png");
}
// ──────── STATUS LIFECYCLE TESTS ────────
[Fact]
public void Publish_DraftShop_ShouldSetStatusToActive()
{
// Arrange
var shop = CreateValidShop();
// Act
shop.Publish();
// Assert
shop.Status.Should().Be(ShopStatus.Active);
shop.DomainEvents.Should().Contain(e => e is ShopPublishedDomainEvent);
}
[Fact]
public void Publish_ActiveShop_ShouldThrowDomainException()
{
// Arrange
var shop = CreateValidShop();
shop.Publish(); // Already active
// Act
var act = () => shop.Publish();
// Assert
act.Should().Throw<DomainException>()
.WithMessage("Cannot publish shop with status Active");
}
[Fact]
public void SetInactive_ActiveShop_ShouldSetStatusInactive()
{
// Arrange
var shop = CreateValidShop();
shop.Publish();
// Act
shop.SetInactive();
// Assert
shop.Status.Should().Be(ShopStatus.Inactive);
}
[Fact]
public void SetInactive_ClosedShop_ShouldThrowDomainException()
{
// Arrange
var shop = CreateValidShop();
shop.Close();
// Act
var act = () => shop.SetInactive();
// Assert
act.Should().Throw<DomainException>()
.WithMessage("Cannot change status of closed shop");
}
[Fact]
public void Close_ShouldSetStatusClosed()
{
// Arrange
var shop = CreateValidShop();
shop.Publish();
// Act
shop.Close();
// Assert
shop.Status.Should().Be(ShopStatus.Closed);
shop.DomainEvents.Should().Contain(e => e is ShopClosedDomainEvent);
}
// ──────── BRANCH TESTS ────────
[Fact]
public void AddBranch_HybridShop_ShouldAddSuccessfully()
{
// Arrange
var shop = CreateValidShop(type: ShopType.Hybrid);
var address = CreateTestAddress();
// Act
var branch = shop.AddBranch("Chi nhánh Quận 1", address);
// Assert
shop.Branches.Should().HaveCount(1);
branch.Name.Should().Be("Chi nhánh Quận 1");
branch.IsActive.Should().BeTrue();
shop.DomainEvents.Should().Contain(e => e is ShopBranchAddedDomainEvent);
}
[Fact]
public void AddBranch_OnlineOnlyShop_ShouldThrowDomainException()
{
// Arrange
var shop = CreateValidShop(type: ShopType.OnlineOnly);
var address = CreateTestAddress();
// Act
var act = () => shop.AddBranch("Branch 1", address);
// Assert
act.Should().Throw<DomainException>()
.WithMessage("Online-only shops cannot have physical branches");
}
[Fact]
public void RemoveBranch_ExistingBranch_ShouldRemoveSuccessfully()
{
// Arrange
var shop = CreateValidShop(type: ShopType.PhysicalOnly);
var branch = shop.AddBranch("Branch 1", CreateTestAddress());
var branchId = branch.Id;
// Act
shop.RemoveBranch(branchId);
// Assert
shop.Branches.Should().BeEmpty();
}
[Fact]
public void RemoveBranch_NonExistingBranch_ShouldThrowDomainException()
{
// Arrange
var shop = CreateValidShop();
// Act
var act = () => shop.RemoveBranch(Guid.NewGuid());
// Assert
act.Should().Throw<DomainException>()
.WithMessage("Branch not found");
}
// ──────── SOFT DELETE TESTS ────────
[Fact]
public void Delete_ShouldSoftDeleteShop()
{
// Arrange
var shop = CreateValidShop();
// Act
shop.Delete();
// Assert
shop.IsDeleted.Should().BeTrue();
}
[Fact]
public void Delete_AlreadyDeletedShop_ShouldThrowDomainException()
{
// Arrange
var shop = CreateValidShop();
shop.Delete();
// Act
var act = () => shop.Delete();
// Assert
act.Should().Throw<DomainException>()
.WithMessage("Shop is already deleted");
}
}