This commit is contained in:
2025-10-27 17:39:18 -04:00
commit 31f723bea4
1579 changed files with 642409 additions and 0 deletions

View File

@@ -0,0 +1,362 @@
using Prefab.Catalog.Domain.Entities;
using Prefab.Catalog.Domain.Events;
using Prefab.Catalog.Domain.Services;
using Shouldly;
namespace Prefab.Tests.Unit.Modules.Catalog.Domain.Events;
[Trait(TraitName.Category, TraitCategory.Unit)]
public class ProductEventsShould
{
private static readonly CancellationToken Ct = CancellationToken.None;
[Fact]
public async Task EmitProductCreatedWhenCreatingModel()
{
var checker = new StubUniqueChecker();
var product = await Product.CreateModel("Model A", "model-a", "desc", uniqueChecker: checker, cancellationToken: Ct);
var evt = product.Events.ShouldHaveSingleItem().ShouldBeOfType<ProductCreated>();
evt.ProductId.ShouldBe(product.Id);
evt.Kind.ShouldBe("Model");
evt.Name.ShouldBe("Model A");
evt.Slug.ShouldBe("model-a");
}
[Fact]
public async Task EmitProductVariantCreatedWhenCreatingVariant()
{
var checker = new StubUniqueChecker();
var parent = await Product.CreateModel("Parent", null, null, uniqueChecker: checker, cancellationToken: Ct);
var variant = await Product.CreateVariant(parent.Id, "SKU-001", "Variant", 12.5m, uniqueChecker: checker, cancellationToken: Ct);
var evt = variant.Events.ShouldHaveSingleItem().ShouldBeOfType<ProductVariantCreated>();
evt.ProductId.ShouldBe(variant.Id);
evt.ParentProductId.ShouldBe(parent.Id);
evt.Sku.ShouldBe("SKU-001");
evt.Price.ShouldBe(12.5m);
}
[Fact]
public async Task EmitProductPriceChangedWhenSettingBasePrice()
{
var checker = new StubUniqueChecker();
var product = await Product.CreateModel("Model B", null, null, uniqueChecker: checker, cancellationToken: Ct);
product.ClearEvents();
product.SetBasePrice(19.99m);
var evt = product.Events.ShouldHaveSingleItem().ShouldBeOfType<ProductPriceChanged>();
evt.OldPrice.ShouldBeNull();
evt.NewPrice.ShouldBe(19.99m);
}
[Fact]
public async Task EmitProductRenamedWhenRenaming()
{
var checker = new StubUniqueChecker();
var product = await Product.CreateModel("Model C", null, null, uniqueChecker: checker, cancellationToken: Ct);
product.ClearEvents();
await product.Rename("Model C v2", checker, Ct);
var evt = product.Events.ShouldHaveSingleItem().ShouldBeOfType<ProductRenamed>();
evt.OldName.ShouldBe("Model C");
evt.NewName.ShouldBe("Model C v2");
}
[Fact]
public async Task EnforceUniqueModelNamesWhenRenaming()
{
var checker = new ConfigurableUniqueChecker();
checker.ModelNameUnique = _ => true;
var existing = await Product.CreateModel("A", null, null, uniqueChecker: checker, cancellationToken: Ct);
existing.ShouldNotBeNull();
var target = await Product.CreateModel("Second", null, null, uniqueChecker: checker, cancellationToken: Ct);
target.ClearEvents();
checker.ModelNameUnique = name => !string.Equals(name, "A", StringComparison.Ordinal);
var duplicate = await Should.ThrowAsync<Exception>(() => target.Rename("A", checker, Ct));
duplicate.GetType().Name.ShouldBe("DuplicateNameException");
checker.ModelNameUnique = _ => true;
await target.Rename("B", checker, Ct);
target.Name.ShouldBe("B");
var evt = target.Events.ShouldHaveSingleItem().ShouldBeOfType<ProductRenamed>();
evt.OldName.ShouldBe("Second");
evt.NewName.ShouldBe("B");
}
[Fact]
public async Task EmitProductVariantAttachedWhenAttachingVariant()
{
var checker = new StubUniqueChecker();
var parent = await Product.CreateModel("Parent", null, null, uniqueChecker: checker, cancellationToken: Ct);
parent.ClearEvents();
var variant = await Product.CreateVariant(parent.Id, "SKU-002", "Variant A", 15m, uniqueChecker: checker, cancellationToken: Ct);
variant.ClearEvents();
parent.AttachVariant(variant);
var evt = parent.Events.ShouldHaveSingleItem().ShouldBeOfType<ProductVariantAttached>();
evt.ParentProductId.ShouldBe(parent.Id);
evt.VariantProductId.ShouldBe(variant.Id);
}
[Fact]
public async Task EmitProductAttributeValueUpsertedWhenUpsertingSpec()
{
var checker = new StubUniqueChecker();
var product = await Product.CreateModel("Model Specs", null, null, uniqueChecker: checker, cancellationToken: Ct);
product.ClearEvents();
var attribute = AttributeDefinition.Create("Depth", AttributeDataType.Number);
attribute.ClearEvents();
product.UpsertSpec(attribute.Id, "12", 12m, "in", null);
var evt = product.Events.ShouldHaveSingleItem().ShouldBeOfType<ProductAttributeValueUpserted>();
evt.ProductId.ShouldBe(product.Id);
evt.AttributeDefinitionId.ShouldBe(attribute.Id);
}
[Fact]
public async Task EmitProductCategoryAssignedWhenAssigningCategory()
{
var checker = new StubUniqueChecker();
var product = await Product.CreateModel("Model Cat", null, null, uniqueChecker: checker, cancellationToken: Ct);
product.ClearEvents();
var categoryId = Guid.NewGuid();
product.AssignToCategory(categoryId, true);
var evt = product.Events.ShouldHaveSingleItem().ShouldBeOfType<ProductCategoryAssigned>();
evt.ProductId.ShouldBe(product.Id);
evt.CategoryId.ShouldBe(categoryId);
evt.IsPrimary.ShouldBeTrue();
}
[Fact]
public async Task EmitProductCategoryUnassignedWhenUnassigningCategory()
{
var checker = new StubUniqueChecker();
var product = await Product.CreateModel("Model Cat", null, null, uniqueChecker: checker, cancellationToken: Ct);
var categoryId = Guid.NewGuid();
product.AssignToCategory(categoryId, false);
product.ClearEvents();
var removed = product.UnassignFromCategory(categoryId);
removed.ShouldBeTrue();
var evt = product.Events.ShouldHaveSingleItem().ShouldBeOfType<ProductCategoryUnassigned>();
evt.CategoryId.ShouldBe(categoryId);
}
private sealed class ConfigurableUniqueChecker : IUniqueChecker
{
public Func<string, bool> ModelNameUnique { get; set; } = _ => true;
public Task<bool> ProductModelNameIsUnique(string name, CancellationToken cancellationToken = default)
=> Task.FromResult(ModelNameUnique(name));
public Task<bool> CategoryNameIsUnique(string name, CancellationToken cancellationToken = default)
=> Task.FromResult(true);
public Task<bool> ProductSlugIsUnique(string? slug, CancellationToken cancellationToken = default)
=> Task.FromResult(true);
public Task<bool> ProductSkuIsUnique(string sku, CancellationToken cancellationToken = default)
=> Task.FromResult(true);
public Task<bool> OptionCodeIsUniqueForProduct(Guid productId, string code, CancellationToken cancellationToken = default)
=> Task.FromResult(true);
}
}
public class OptionDefinitionEventsShould
{
private static readonly CancellationToken Ct = CancellationToken.None;
[Fact]
public async Task EmitOptionDefinitionCreatedForChoice()
{
var checker = new StubUniqueChecker();
var product = await Product.CreateModel("Model Options", null, null, uniqueChecker: checker, cancellationToken: Ct);
var option = await OptionDefinition.CreateChoice(product, "color", "Color", checker, isVariantAxis: false, cancellationToken: Ct);
var evt = option.Events.ShouldHaveSingleItem().ShouldBeOfType<OptionDefinitionCreated>();
evt.ProductId.ShouldBe(product.Id);
evt.OptionDefinitionId.ShouldBe(option.Id);
evt.DataType.ShouldBe("Choice");
}
[Fact]
public async Task EmitOptionDefinitionCreatedForNumber()
{
var checker = new StubUniqueChecker();
var product = await Product.CreateModel("Model Options", null, null, uniqueChecker: checker, cancellationToken: Ct);
var option = await OptionDefinition.CreateNumber(product, "lead", "Lead Length", "ft", 1, 100, 1, 0.5m, isVariantAxis: true, uniqueChecker: checker, cancellationToken: Ct);
var evt = option.Events.ShouldHaveSingleItem().ShouldBeOfType<OptionDefinitionCreated>();
evt.DataType.ShouldBe("Number");
evt.IsVariantAxis.ShouldBeTrue();
option.IsVariantAxis.ShouldBeTrue();
}
[Fact]
public async Task EmitOptionValueAddedWhenAddingValue()
{
var checker = new StubUniqueChecker();
var product = await Product.CreateModel("Model Options", null, null, uniqueChecker: checker, cancellationToken: Ct);
var option = await OptionDefinition.CreateChoice(product, "finish", "Finish", checker, isVariantAxis: false, cancellationToken: Ct);
option.ClearEvents();
var value = option.AddValue("ivory", "Ivory", 0.5m, PriceDeltaKind.Absolute);
var evt = option.Events.ShouldHaveSingleItem().ShouldBeOfType<OptionValueAdded>();
evt.OptionValueId.ShouldBe(value.Id);
evt.PriceDelta.ShouldBe(0.5m);
evt.Kind.ShouldBe(PriceDeltaKind.Absolute);
}
[Fact]
public async Task EmitOptionValueChangedWhenChangingValue()
{
var checker = new StubUniqueChecker();
var product = await Product.CreateModel("Model Options", null, null, uniqueChecker: checker, cancellationToken: Ct);
var option = await OptionDefinition.CreateChoice(product, "shade", "Shade", checker, isVariantAxis: false, cancellationToken: Ct);
var value = option.AddValue("warm", "Warm", 0.2m, PriceDeltaKind.Absolute);
option.ClearEvents();
option.ChangeValue(value.Id, "Warm Glow", 0.3m, PriceDeltaKind.Percent);
var evt = option.Events.ShouldHaveSingleItem().ShouldBeOfType<OptionValueChanged>();
evt.OptionValueId.ShouldBe(value.Id);
}
[Fact]
public async Task EmitOptionValueRemovedWhenRemovingValue()
{
var checker = new StubUniqueChecker();
var product = await Product.CreateModel("Model Options", null, null, uniqueChecker: checker, cancellationToken: Ct);
var option = await OptionDefinition.CreateChoice(product, "texture", "Texture", checker, isVariantAxis: false, cancellationToken: Ct);
var value = option.AddValue("smooth", "Smooth", null, PriceDeltaKind.Absolute);
option.ClearEvents();
option.RemoveValue(value.Id);
var evt = option.Events.ShouldHaveSingleItem().ShouldBeOfType<OptionValueRemoved>();
evt.OptionValueId.ShouldBe(value.Id);
}
[Fact]
public async Task EmitOptionTierAddedWhenAddingTier()
{
var checker = new StubUniqueChecker();
var product = await Product.CreateModel("Model Options", null, null, uniqueChecker: checker, cancellationToken: Ct);
var option = await OptionDefinition.CreateNumber(product, "length", "Length", "ft", 0, 100, 1, 0.7m, uniqueChecker: checker, cancellationToken: Ct);
option.ClearEvents();
var tier = option.AddTier(10, 20, 0.6m, 5m);
var evt = option.Events.ShouldHaveSingleItem().ShouldBeOfType<OptionTierAdded>();
evt.OptionTierId.ShouldBe(tier.Id);
evt.FromInclusive.ShouldBe(10);
evt.ToInclusive.ShouldBe(20);
}
[Fact]
public async Task EmitOptionTierChangedWhenChangingTier()
{
var checker = new StubUniqueChecker();
var product = await Product.CreateModel("Model Options", null, null, uniqueChecker: checker, cancellationToken: Ct);
var option = await OptionDefinition.CreateNumber(product, "height", "Height", "ft", 0, 100, 1, 0.7m, uniqueChecker: checker, cancellationToken: Ct);
var tier = option.AddTier(0, 10, 0.5m, null);
option.ClearEvents();
option.ChangeTier(tier.Id, 5, 15, 0.55m, 2m);
var evt = option.Events.ShouldHaveSingleItem().ShouldBeOfType<OptionTierChanged>();
evt.OptionTierId.ShouldBe(tier.Id);
}
[Fact]
public async Task EmitOptionTierRemovedWhenRemovingTier()
{
var checker = new StubUniqueChecker();
var product = await Product.CreateModel("Model Options", null, null, uniqueChecker: checker, cancellationToken: Ct);
var option = await OptionDefinition.CreateNumber(product, "width", "Width", "ft", 0, 100, 1, 0.7m, uniqueChecker: checker, cancellationToken: Ct);
var tier = option.AddTier(0, 5, 0.4m, null);
option.ClearEvents();
option.RemoveTier(tier.Id);
var evt = option.Events.ShouldHaveSingleItem().ShouldBeOfType<OptionTierRemoved>();
evt.OptionTierId.ShouldBe(tier.Id);
}
}
public class AttributeDefinitionEventsShould
{
[Fact]
public void EmitAttributeDefinitionCreatedWhenCreatingDefinition()
{
var attribute = AttributeDefinition.Create("Material", AttributeDataType.Enum);
var evt = attribute.Events.ShouldHaveSingleItem().ShouldBeOfType<AttributeDefinitionCreated>();
evt.AttributeDefinitionId.ShouldBe(attribute.Id);
evt.DataType.ShouldBe(AttributeDataType.Enum);
}
}
public class CategoryEventsShould
{
private static readonly CancellationToken Ct = CancellationToken.None;
[Fact]
public async Task EmitCategoryCreatedWhenCreatingCategory()
{
var checker = new StubUniqueChecker();
var category = await Category.Create("Lighting", "Lighting fixtures", check: checker, cancellationToken: Ct);
var evt = category.Events.ShouldHaveSingleItem().ShouldBeOfType<CategoryCreated>();
evt.CategoryId.ShouldBe(category.Id);
evt.Name.ShouldBe("Lighting");
}
[Fact]
public async Task EmitCategoryRenamedWhenRenamingCategory()
{
var checker = new StubUniqueChecker();
var category = await Category.Create("Legacy", "Legacy", check: checker, cancellationToken: Ct);
category.ClearEvents();
await category.Rename("Modern", check: checker, cancellationToken: Ct);
var evt = category.Events.ShouldHaveSingleItem().ShouldBeOfType<CategoryRenamed>();
evt.OldName.ShouldBe("Legacy");
evt.NewName.ShouldBe("Modern");
}
}
internal sealed class StubUniqueChecker : IUniqueChecker
{
public Task<bool> CategoryNameIsUnique(string name, CancellationToken cancellationToken = default) => Task.FromResult(true);
public Task<bool> ProductModelNameIsUnique(string name, CancellationToken cancellationToken = default) => Task.FromResult(true);
public Task<bool> ProductSlugIsUnique(string? slug, CancellationToken cancellationToken = default) => Task.FromResult(true);
public Task<bool> ProductSkuIsUnique(string sku, CancellationToken cancellationToken = default) => Task.FromResult(true);
public Task<bool> OptionCodeIsUniqueForProduct(Guid productId, string code, CancellationToken cancellationToken = default) => Task.FromResult(true);
}