Init
This commit is contained in:
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user