feat(domain): ProductType entity + domain exceptions (PRD-001)
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
using FluentAssertions;
|
||||
using SIGCM2.Domain.Exceptions;
|
||||
|
||||
namespace SIGCM2.Application.Tests.Domain.ProductTypes;
|
||||
|
||||
public class ProductTypeExceptionsTests
|
||||
{
|
||||
// ── ProductTypeNotFoundException ──────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void ProductTypeNotFoundException_ContainsIdInMessage()
|
||||
{
|
||||
var ex = new ProductTypeNotFoundException(42);
|
||||
|
||||
ex.Message.Should().Contain("42");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProductTypeNotFoundException_ExposesProductTypeId()
|
||||
{
|
||||
var ex = new ProductTypeNotFoundException(99);
|
||||
|
||||
ex.ProductTypeId.Should().Be(99);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProductTypeNotFoundException_IsSubclassOfDomainException()
|
||||
{
|
||||
var ex = new ProductTypeNotFoundException(1);
|
||||
|
||||
ex.Should().BeAssignableTo<DomainException>();
|
||||
}
|
||||
|
||||
// ── ProductTypeNombreDuplicadoException ───────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void ProductTypeNombreDuplicadoException_ContainsNombreInMessage()
|
||||
{
|
||||
var ex = new ProductTypeNombreDuplicadoException("Clasificados");
|
||||
|
||||
ex.Message.Should().Contain("Clasificados");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProductTypeNombreDuplicadoException_ExposesNombre()
|
||||
{
|
||||
var ex = new ProductTypeNombreDuplicadoException("Notables");
|
||||
|
||||
ex.Nombre.Should().Be("Notables");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProductTypeNombreDuplicadoException_IsSubclassOfDomainException()
|
||||
{
|
||||
var ex = new ProductTypeNombreDuplicadoException("x");
|
||||
|
||||
ex.Should().BeAssignableTo<DomainException>();
|
||||
}
|
||||
|
||||
// ── ProductTypeEnUsoException ─────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void ProductTypeEnUsoException_ContainsCountInMessage()
|
||||
{
|
||||
var ex = new ProductTypeEnUsoException(id: 5, productsActivos: 7);
|
||||
|
||||
ex.Message.Should().Contain("7");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProductTypeEnUsoException_ExposesProductTypeIdAndCount()
|
||||
{
|
||||
var ex = new ProductTypeEnUsoException(id: 10, productsActivos: 3);
|
||||
|
||||
ex.ProductTypeId.Should().Be(10);
|
||||
ex.ProductsActivos.Should().Be(3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProductTypeEnUsoException_IsSubclassOfDomainException()
|
||||
{
|
||||
var ex = new ProductTypeEnUsoException(1, 0);
|
||||
|
||||
ex.Should().BeAssignableTo<DomainException>();
|
||||
}
|
||||
|
||||
// ── ProductTypeFlagsIncoherentesException ─────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void ProductTypeFlagsIncoherentesException_ContainsReasonInMessage()
|
||||
{
|
||||
var ex = new ProductTypeFlagsIncoherentesException("IsBundle sin hijos definidos");
|
||||
|
||||
ex.Message.Should().Contain("IsBundle sin hijos definidos");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProductTypeFlagsIncoherentesException_ExposesReason()
|
||||
{
|
||||
var ex = new ProductTypeFlagsIncoherentesException("razón técnica");
|
||||
|
||||
ex.Reason.Should().Be("razón técnica");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProductTypeFlagsIncoherentesException_IsSubclassOfDomainException()
|
||||
{
|
||||
var ex = new ProductTypeFlagsIncoherentesException("x");
|
||||
|
||||
ex.Should().BeAssignableTo<DomainException>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using SIGCM2.Domain.Entities;
|
||||
|
||||
namespace SIGCM2.Application.Tests.Domain.ProductTypes;
|
||||
|
||||
public class ProductTypeTests
|
||||
{
|
||||
private static readonly FakeTimeProvider FakeTime =
|
||||
new(new DateTimeOffset(2026, 4, 19, 10, 0, 0, TimeSpan.Zero));
|
||||
|
||||
// ── ForCreation: happy path ──────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void ForCreation_ValidInputs_ReturnsNewInstance_WithActiveTrue()
|
||||
{
|
||||
var pt = ProductType.ForCreation(
|
||||
"Clasificados",
|
||||
hasDuration: true, requiresText: true, requiresCategory: false, isBundle: false,
|
||||
allowImages: false,
|
||||
maxImages: null, maxImageSizeMB: null, maxImageWidth: null, maxImageHeight: null,
|
||||
FakeTime);
|
||||
|
||||
pt.Id.Should().Be(0);
|
||||
pt.Nombre.Should().Be("Clasificados");
|
||||
pt.HasDuration.Should().BeTrue();
|
||||
pt.RequiresText.Should().BeTrue();
|
||||
pt.RequiresCategory.Should().BeFalse();
|
||||
pt.IsBundle.Should().BeFalse();
|
||||
pt.AllowImages.Should().BeFalse();
|
||||
pt.IsActive.Should().BeTrue();
|
||||
pt.FechaCreacion.Should().Be(FakeTime.GetUtcNow().UtcDateTime);
|
||||
pt.FechaModificacion.Should().BeNull();
|
||||
}
|
||||
|
||||
// ── ForCreation: Nombre validations ──────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void ForCreation_NombreNull_ThrowsArgumentException()
|
||||
{
|
||||
var act = () => ProductType.ForCreation(
|
||||
null!, false, false, false, false, false,
|
||||
null, null, null, null, FakeTime);
|
||||
|
||||
act.Should().Throw<ArgumentException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ForCreation_NombreWhitespace_ThrowsArgumentException()
|
||||
{
|
||||
var act = () => ProductType.ForCreation(
|
||||
" ", false, false, false, false, false,
|
||||
null, null, null, null, FakeTime);
|
||||
|
||||
act.Should().Throw<ArgumentException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ForCreation_NombreOver200Chars_ThrowsArgumentException()
|
||||
{
|
||||
var nombre = new string('X', 201);
|
||||
|
||||
var act = () => ProductType.ForCreation(
|
||||
nombre, false, false, false, false, false,
|
||||
null, null, null, null, FakeTime);
|
||||
|
||||
act.Should().Throw<ArgumentException>();
|
||||
}
|
||||
|
||||
// ── ForCreation: multimedia normalization ────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void ForCreation_AllowImagesFalse_NormalizesAll4MultimediaFieldsToNull()
|
||||
{
|
||||
var pt = ProductType.ForCreation(
|
||||
"Clasificados",
|
||||
false, false, false, false,
|
||||
allowImages: false,
|
||||
maxImages: 5, maxImageSizeMB: 2.5m, maxImageWidth: 1920, maxImageHeight: 1080,
|
||||
FakeTime);
|
||||
|
||||
pt.MaxImages.Should().BeNull();
|
||||
pt.MaxImageSizeMB.Should().BeNull();
|
||||
pt.MaxImageWidth.Should().BeNull();
|
||||
pt.MaxImageHeight.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ForCreation_AllowImagesTrue_PreservesMultimediaValues()
|
||||
{
|
||||
var pt = ProductType.ForCreation(
|
||||
"Fotos",
|
||||
false, false, false, false,
|
||||
allowImages: true,
|
||||
maxImages: 10, maxImageSizeMB: 5.0m, maxImageWidth: 800, maxImageHeight: 600,
|
||||
FakeTime);
|
||||
|
||||
pt.AllowImages.Should().BeTrue();
|
||||
pt.MaxImages.Should().Be(10);
|
||||
pt.MaxImageSizeMB.Should().Be(5.0m);
|
||||
pt.MaxImageWidth.Should().Be(800);
|
||||
pt.MaxImageHeight.Should().Be(600);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ForCreation_AllowImagesTrue_AllMaxNull_IsValid()
|
||||
{
|
||||
var pt = ProductType.ForCreation(
|
||||
"Fotos sin límite",
|
||||
false, false, false, false,
|
||||
allowImages: true,
|
||||
maxImages: null, maxImageSizeMB: null, maxImageWidth: null, maxImageHeight: null,
|
||||
FakeTime);
|
||||
|
||||
pt.AllowImages.Should().BeTrue();
|
||||
pt.MaxImages.Should().BeNull();
|
||||
pt.MaxImageSizeMB.Should().BeNull();
|
||||
pt.MaxImageWidth.Should().BeNull();
|
||||
pt.MaxImageHeight.Should().BeNull();
|
||||
}
|
||||
|
||||
// ── WithRenamed ──────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void WithRenamed_ValidNombre_ReturnsNewInstance_WithFechaModificacionUpdated()
|
||||
{
|
||||
var original = ProductType.ForCreation("Original", false, false, false, false, false, null, null, null, null, FakeTime);
|
||||
var updated = FakeTimeProvider2();
|
||||
var renamed = original.WithRenamed("Nuevo", updated);
|
||||
|
||||
renamed.Nombre.Should().Be("Nuevo");
|
||||
renamed.FechaModificacion.Should().Be(updated.GetUtcNow().UtcDateTime);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WithRenamed_DoesNotMutateOriginal()
|
||||
{
|
||||
var original = ProductType.ForCreation("Original", false, false, false, false, false, null, null, null, null, FakeTime);
|
||||
_ = original.WithRenamed("Nuevo", FakeTime);
|
||||
|
||||
original.Nombre.Should().Be("Original");
|
||||
original.FechaModificacion.Should().BeNull();
|
||||
}
|
||||
|
||||
// ── WithUpdatedFlags ─────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void WithUpdatedFlags_SetsFlags_PreservesMultimedia()
|
||||
{
|
||||
var original = ProductType.ForCreation(
|
||||
"Tipo", false, false, false, false,
|
||||
allowImages: true, maxImages: 5, maxImageSizeMB: 2.0m, maxImageWidth: null, maxImageHeight: null,
|
||||
FakeTime);
|
||||
var updated = original.WithUpdatedFlags(true, true, true, true, FakeTime);
|
||||
|
||||
updated.HasDuration.Should().BeTrue();
|
||||
updated.RequiresText.Should().BeTrue();
|
||||
updated.RequiresCategory.Should().BeTrue();
|
||||
updated.IsBundle.Should().BeTrue();
|
||||
updated.AllowImages.Should().BeTrue();
|
||||
updated.MaxImages.Should().Be(5);
|
||||
updated.MaxImageSizeMB.Should().Be(2.0m);
|
||||
}
|
||||
|
||||
// ── WithUpdatedMultimedia ─────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void WithUpdatedMultimedia_AllowImagesFalse_NullifiesAllLimits()
|
||||
{
|
||||
var original = ProductType.ForCreation(
|
||||
"Tipo", false, false, false, false,
|
||||
allowImages: true, maxImages: 5, maxImageSizeMB: 2.0m, maxImageWidth: 1024, maxImageHeight: 768,
|
||||
FakeTime);
|
||||
var updated = original.WithUpdatedMultimedia(false, 3, 1.0m, 800, 600, FakeTime);
|
||||
|
||||
updated.AllowImages.Should().BeFalse();
|
||||
updated.MaxImages.Should().BeNull();
|
||||
updated.MaxImageSizeMB.Should().BeNull();
|
||||
updated.MaxImageWidth.Should().BeNull();
|
||||
updated.MaxImageHeight.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WithUpdatedMultimedia_AllowImagesTrue_PreservesLimits()
|
||||
{
|
||||
var original = ProductType.ForCreation("Tipo", false, false, false, false, false, null, null, null, null, FakeTime);
|
||||
var updated = original.WithUpdatedMultimedia(true, 8, 3.5m, 1920, 1080, FakeTime);
|
||||
|
||||
updated.AllowImages.Should().BeTrue();
|
||||
updated.MaxImages.Should().Be(8);
|
||||
updated.MaxImageSizeMB.Should().Be(3.5m);
|
||||
updated.MaxImageWidth.Should().Be(1920);
|
||||
updated.MaxImageHeight.Should().Be(1080);
|
||||
}
|
||||
|
||||
// ── WithDeactivated ──────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void WithDeactivated_SetsIsActiveFalse_AndFechaModificacion()
|
||||
{
|
||||
var original = ProductType.ForCreation("Tipo", false, false, false, false, false, null, null, null, null, FakeTime);
|
||||
var tp2 = FakeTimeProvider2();
|
||||
var deactivated = original.WithDeactivated(tp2);
|
||||
|
||||
deactivated.IsActive.Should().BeFalse();
|
||||
deactivated.FechaModificacion.Should().Be(tp2.GetUtcNow().UtcDateTime);
|
||||
original.IsActive.Should().BeTrue(); // original not mutated
|
||||
}
|
||||
|
||||
// ── Hydration ────────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Constructor_HydrationRoundtrip_PreservesAllFields()
|
||||
{
|
||||
var fechaCreacion = new DateTime(2026, 1, 15, 8, 0, 0, DateTimeKind.Utc);
|
||||
var fechaModificacion = new DateTime(2026, 3, 10, 9, 30, 0, DateTimeKind.Utc);
|
||||
|
||||
var pt = new ProductType(
|
||||
id: 42,
|
||||
nombre: "Bundle Aniversario",
|
||||
hasDuration: true,
|
||||
requiresText: false,
|
||||
requiresCategory: true,
|
||||
isBundle: true,
|
||||
allowImages: true,
|
||||
maxImages: 12,
|
||||
maxImageSizeMB: 2.75m,
|
||||
maxImageWidth: 1920,
|
||||
maxImageHeight: 1080,
|
||||
isActive: false,
|
||||
fechaCreacion: fechaCreacion,
|
||||
fechaModificacion: fechaModificacion);
|
||||
|
||||
pt.Id.Should().Be(42);
|
||||
pt.Nombre.Should().Be("Bundle Aniversario");
|
||||
pt.HasDuration.Should().BeTrue();
|
||||
pt.RequiresText.Should().BeFalse();
|
||||
pt.RequiresCategory.Should().BeTrue();
|
||||
pt.IsBundle.Should().BeTrue();
|
||||
pt.AllowImages.Should().BeTrue();
|
||||
pt.MaxImages.Should().Be(12);
|
||||
pt.MaxImageSizeMB.Should().Be(2.75m);
|
||||
pt.MaxImageWidth.Should().Be(1920);
|
||||
pt.MaxImageHeight.Should().Be(1080);
|
||||
pt.IsActive.Should().BeFalse();
|
||||
pt.FechaCreacion.Should().Be(fechaCreacion);
|
||||
pt.FechaModificacion.Should().Be(fechaModificacion);
|
||||
}
|
||||
|
||||
// ── Helper ───────────────────────────────────────────────────────────────
|
||||
|
||||
private static FakeTimeProvider FakeTimeProvider2() =>
|
||||
new(new DateTimeOffset(2026, 4, 20, 15, 0, 0, TimeSpan.Zero));
|
||||
}
|
||||
Reference in New Issue
Block a user