test(merchant-service): add 38 unit tests for Shop aggregate and ShopFeatures

This commit is contained in:
Ho Ngoc Hai
2026-03-01 05:50:58 +07:00
parent 9c51d268c6
commit cb6337cb7c
2 changed files with 502 additions and 0 deletions

View File

@@ -0,0 +1,354 @@
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");
}
}

View File

@@ -0,0 +1,148 @@
using FluentAssertions;
using MerchantService.Domain.AggregatesModel.ShopAggregate;
using MerchantService.Domain.SeedWork;
using Xunit;
namespace MerchantService.UnitTests.Domain;
/// <summary>
/// EN: Unit tests for ShopFeatures value object — validates ForCategory() mapping for all verticals.
/// VI: Unit tests cho value object ShopFeatures — kiểm tra mapping ForCategory() cho tất cả ngành hàng.
/// </summary>
public class ShopFeaturesTests
{
// ──────── HELPER ────────
private static BusinessCategory Cat(int id) => Enumeration.FromValue<BusinessCategory>(id);
// ──────── F&B VERTICALS ────────
[Theory]
[InlineData(1)] // FoodBeverage
[InlineData(11)] // Cafe
[InlineData(12)] // Restaurant
public void ForCategory_FnBVerticals_ShouldHaveTablesKitchenInventoryDelivery(int categoryId)
{
// Act
var features = ShopFeatures.ForCategory(Cat(categoryId));
// Assert
features.HasInventory.Should().BeTrue($"Category {categoryId} should have Inventory");
features.HasTables.Should().BeTrue($"Category {categoryId} should have Tables");
features.HasKitchen.Should().BeTrue($"Category {categoryId} should have Kitchen");
features.HasDelivery.Should().BeTrue($"Category {categoryId} should have Delivery");
features.HasBooking.Should().BeFalse($"Category {categoryId} should not have Booking");
features.HasShipping.Should().BeFalse($"Category {categoryId} should not have Shipping");
}
// ──────── RETAIL VERTICALS ────────
[Theory]
[InlineData(2)] // Fashion
[InlineData(3)] // Electronics
[InlineData(9)] // Grocery
[InlineData(10)] // HomeFurniture
public void ForCategory_RetailVerticals_ShouldHaveInventoryAndShipping(int categoryId)
{
// Act
var features = ShopFeatures.ForCategory(Cat(categoryId));
// Assert
features.HasInventory.Should().BeTrue();
features.HasShipping.Should().BeTrue();
features.HasTables.Should().BeFalse();
features.HasKitchen.Should().BeFalse();
features.HasBooking.Should().BeFalse();
}
// ──────── SERVICE VERTICALS ────────
[Theory]
[InlineData(4)] // Healthcare
[InlineData(6)] // Education
[InlineData(8)] // Services
public void ForCategory_ServiceVerticals_ShouldHaveBookingOnly(int categoryId)
{
// Act
var features = ShopFeatures.ForCategory(Cat(categoryId));
// Assert
features.HasBooking.Should().BeTrue();
features.HasInventory.Should().BeFalse();
features.HasTables.Should().BeFalse();
}
// ──────── BEAUTY (SPECIAL CASE) ────────
[Fact]
public void ForCategory_Beauty_ShouldHaveBookingAndInventory()
{
// Act
var features = ShopFeatures.ForCategory(BusinessCategory.Beauty);
// Assert
features.HasBooking.Should().BeTrue();
features.HasInventory.Should().BeTrue("Beauty clinics sell products");
features.HasTables.Should().BeFalse();
features.HasKitchen.Should().BeFalse();
}
// ──────── ENTERTAINMENT VERTICALS ────────
[Theory]
[InlineData(7)] // Entertainment
[InlineData(13)] // Karaoke
public void ForCategory_EntertainmentVerticals_ShouldHaveBookingAndInventory(int categoryId)
{
// Act
var features = ShopFeatures.ForCategory(Cat(categoryId));
// Assert
features.HasBooking.Should().BeTrue();
features.HasInventory.Should().BeTrue();
}
// ──────── SPA ────────
[Fact]
public void ForCategory_Spa_ShouldHaveBookingOnly()
{
// Act
var features = ShopFeatures.ForCategory(BusinessCategory.Spa);
// Assert
features.HasBooking.Should().BeTrue();
features.HasInventory.Should().BeFalse("Spa focuses on services, not products");
features.HasTables.Should().BeFalse();
}
// ──────── DEFAULT (OTHER) ────────
[Fact]
public void ForCategory_OtherCategory_ShouldHaveInventoryAndBooking()
{
// Act
var features = ShopFeatures.ForCategory(BusinessCategory.Other);
// Assert
features.HasInventory.Should().BeTrue();
features.HasBooking.Should().BeTrue();
}
// ──────── VALUE OBJECT TESTS ────────
[Fact]
public void Empty_ShouldReturnAllFlagsDisabled()
{
// Act
var features = ShopFeatures.Empty;
// Assert
features.HasInventory.Should().BeFalse();
features.HasBooking.Should().BeFalse();
features.HasTables.Should().BeFalse();
features.HasKitchen.Should().BeFalse();
features.HasShipping.Should().BeFalse();
features.HasDelivery.Should().BeFalse();
}
}