From cb6337cb7c9115babc04884730fca303c6d4c19a Mon Sep 17 00:00:00 2001 From: Ho Ngoc Hai Date: Sun, 1 Mar 2026 05:50:58 +0700 Subject: [PATCH] test(merchant-service): add 38 unit tests for Shop aggregate and ShopFeatures --- .../Domain/ShopAggregateTests.cs | 354 ++++++++++++++++++ .../Domain/ShopFeaturesTests.cs | 148 ++++++++ 2 files changed, 502 insertions(+) create mode 100644 services/merchant-service-net/tests/MerchantService.UnitTests/Domain/ShopAggregateTests.cs create mode 100644 services/merchant-service-net/tests/MerchantService.UnitTests/Domain/ShopFeaturesTests.cs diff --git a/services/merchant-service-net/tests/MerchantService.UnitTests/Domain/ShopAggregateTests.cs b/services/merchant-service-net/tests/MerchantService.UnitTests/Domain/ShopAggregateTests.cs new file mode 100644 index 00000000..df2a7790 --- /dev/null +++ b/services/merchant-service-net/tests/MerchantService.UnitTests/Domain/ShopAggregateTests.cs @@ -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; + +/// +/// 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. +/// +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(); + } + + [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() + .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() + .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() + .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(); + } + + [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() + .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() + .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() + .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() + .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() + .WithMessage("Shop is already deleted"); + } +} diff --git a/services/merchant-service-net/tests/MerchantService.UnitTests/Domain/ShopFeaturesTests.cs b/services/merchant-service-net/tests/MerchantService.UnitTests/Domain/ShopFeaturesTests.cs new file mode 100644 index 00000000..d49ccc2f --- /dev/null +++ b/services/merchant-service-net/tests/MerchantService.UnitTests/Domain/ShopFeaturesTests.cs @@ -0,0 +1,148 @@ +using FluentAssertions; +using MerchantService.Domain.AggregatesModel.ShopAggregate; +using MerchantService.Domain.SeedWork; +using Xunit; + +namespace MerchantService.UnitTests.Domain; + +/// +/// 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. +/// +public class ShopFeaturesTests +{ + // ──────── HELPER ──────── + + private static BusinessCategory Cat(int id) => Enumeration.FromValue(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(); + } +}