test(merchant-service): add 38 unit tests for Shop aggregate and ShopFeatures
This commit is contained in:
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user