feat(domain): Product entity + 5 domain exceptions (PRD-002)
This commit is contained in:
218
tests/SIGCM2.Application.Tests/Domain/ProductTests.cs
Normal file
218
tests/SIGCM2.Application.Tests/Domain/ProductTests.cs
Normal file
@@ -0,0 +1,218 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using SIGCM2.Domain.Entities;
|
||||
|
||||
namespace SIGCM2.Application.Tests.Domain;
|
||||
|
||||
/// <summary>
|
||||
/// PRD-002 — Domain entity tests for Product.
|
||||
/// Validates factory method, mutation methods, and validation rules.
|
||||
/// </summary>
|
||||
public class ProductTests
|
||||
{
|
||||
private static readonly FakeTimeProvider _time =
|
||||
new(new DateTimeOffset(2026, 4, 19, 10, 0, 0, TimeSpan.Zero));
|
||||
|
||||
private static Product ValidProduct(int? rubroId = null, int? priceDurationDays = null)
|
||||
=> Product.ForCreation(
|
||||
nombre: "Clasificado Estándar",
|
||||
medioId: 1,
|
||||
productTypeId: 2,
|
||||
rubroId: rubroId,
|
||||
basePrice: 100.50m,
|
||||
priceDurationDays: priceDurationDays,
|
||||
timeProvider: _time);
|
||||
|
||||
// ── R1-S1: ForCreation happy path ─────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void ForCreation_ValidData_ReturnsEntityWithDefaults()
|
||||
{
|
||||
var p = ValidProduct();
|
||||
|
||||
p.IsActive.Should().BeTrue();
|
||||
p.Id.Should().Be(0);
|
||||
p.FechaCreacion.Should().Be(_time.GetUtcNow().UtcDateTime);
|
||||
p.FechaModificacion.Should().BeNull();
|
||||
p.Nombre.Should().Be("Clasificado Estándar");
|
||||
p.MedioId.Should().Be(1);
|
||||
p.ProductTypeId.Should().Be(2);
|
||||
p.RubroId.Should().BeNull();
|
||||
p.BasePrice.Should().Be(100.50m);
|
||||
p.PriceDurationDays.Should().BeNull();
|
||||
}
|
||||
|
||||
// ── R1-S2: Nombre vacío ────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void ForCreation_EmptyNombre_ThrowsArgumentException()
|
||||
{
|
||||
var act = () => Product.ForCreation("", 1, 2, null, 10m, null, _time);
|
||||
|
||||
act.Should().Throw<ArgumentException>()
|
||||
.WithMessage("*Nombre*");
|
||||
}
|
||||
|
||||
// ── R1-S3: Nombre solo espacios ────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void ForCreation_WhitespaceNombre_ThrowsArgumentException()
|
||||
{
|
||||
var act = () => Product.ForCreation(" ", 1, 2, null, 10m, null, _time);
|
||||
|
||||
act.Should().Throw<ArgumentException>();
|
||||
}
|
||||
|
||||
// ── R1-S4: BasePrice negativo ──────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void ForCreation_NegativeBasePrice_ThrowsArgumentException()
|
||||
{
|
||||
var act = () => Product.ForCreation("Test", 1, 2, null, -1m, null, _time);
|
||||
|
||||
act.Should().Throw<ArgumentException>()
|
||||
.WithMessage("*basePrice*");
|
||||
}
|
||||
|
||||
// ── R1-S5: BasePrice = 0 es válido ─────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void ForCreation_ZeroBasePrice_DoesNotThrow()
|
||||
{
|
||||
var act = () => Product.ForCreation("Test", 1, 2, null, 0m, null, _time);
|
||||
|
||||
act.Should().NotThrow();
|
||||
}
|
||||
|
||||
// ── R1-S6: PriceDurationDays = 0 ──────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void ForCreation_ZeroPriceDurationDays_ThrowsArgumentException()
|
||||
{
|
||||
var act = () => Product.ForCreation("Test", 1, 2, null, 10m, 0, _time);
|
||||
|
||||
act.Should().Throw<ArgumentException>()
|
||||
.WithMessage("*priceDurationDays*");
|
||||
}
|
||||
|
||||
// ── R1-S7: PriceDurationDays negativo ─────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void ForCreation_NegativePriceDurationDays_ThrowsArgumentException()
|
||||
{
|
||||
var act = () => Product.ForCreation("Test", 1, 2, null, 10m, -5, _time);
|
||||
|
||||
act.Should().Throw<ArgumentException>();
|
||||
}
|
||||
|
||||
// ── R1-S8: PriceDurationDays válido ───────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void ForCreation_ValidPriceDurationDays_SetsValue()
|
||||
{
|
||||
var p = Product.ForCreation("Test", 1, 2, null, 10m, 30, _time);
|
||||
|
||||
p.PriceDurationDays.Should().Be(30);
|
||||
}
|
||||
|
||||
// ── R1-S9: WithDeactivated ─────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void WithDeactivated_ActiveProduct_ReturnsInactiveWithModDate()
|
||||
{
|
||||
var product = ValidProduct();
|
||||
var tp2 = new FakeTimeProvider(new DateTimeOffset(2026, 4, 20, 12, 0, 0, TimeSpan.Zero));
|
||||
|
||||
var deactivated = product.WithDeactivated(tp2);
|
||||
|
||||
deactivated.IsActive.Should().BeFalse();
|
||||
deactivated.FechaModificacion.Should().Be(tp2.GetUtcNow().UtcDateTime);
|
||||
// Immutability: original unchanged
|
||||
product.IsActive.Should().BeTrue();
|
||||
}
|
||||
|
||||
// ── R1-S10: WithDeactivated idempotente ───────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void WithDeactivated_AlreadyInactive_ReturnsInactiveNoProblem()
|
||||
{
|
||||
var product = ValidProduct();
|
||||
var deactivated = product.WithDeactivated(_time);
|
||||
|
||||
var act = () => deactivated.WithDeactivated(_time);
|
||||
|
||||
act.Should().NotThrow();
|
||||
act().IsActive.Should().BeFalse();
|
||||
}
|
||||
|
||||
// ── R1-S11: WithUpdated ───────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void WithUpdated_ValidFields_ReturnsNewInstanceWithUpdatedValues()
|
||||
{
|
||||
var product = ValidProduct();
|
||||
var tp2 = new FakeTimeProvider(new DateTimeOffset(2026, 4, 20, 12, 0, 0, TimeSpan.Zero));
|
||||
|
||||
var updated = product.WithUpdated("Nuevo Nombre", rubroId: null, basePrice: 200m, priceDurationDays: 15, tp2);
|
||||
|
||||
updated.Nombre.Should().Be("Nuevo Nombre");
|
||||
updated.BasePrice.Should().Be(200m);
|
||||
updated.PriceDurationDays.Should().Be(15);
|
||||
updated.FechaModificacion.Should().Be(tp2.GetUtcNow().UtcDateTime);
|
||||
// Immutability: original unchanged
|
||||
product.Nombre.Should().Be("Clasificado Estándar");
|
||||
product.BasePrice.Should().Be(100.50m);
|
||||
}
|
||||
|
||||
// ── Immutability: With* return new instances ──────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void WithRenamed_ReturnsNewInstance()
|
||||
{
|
||||
var p = ValidProduct();
|
||||
var renamed = p.WithRenamed("Nuevo", _time);
|
||||
|
||||
renamed.Should().NotBeSameAs(p);
|
||||
renamed.Nombre.Should().Be("Nuevo");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WithUpdatedPrice_ReturnsNewInstance()
|
||||
{
|
||||
var p = ValidProduct();
|
||||
var updated = p.WithUpdatedPrice(999m, null, _time);
|
||||
|
||||
updated.Should().NotBeSameAs(p);
|
||||
updated.BasePrice.Should().Be(999m);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WithUpdatedCategory_ReturnsNewInstance()
|
||||
{
|
||||
var p = ValidProduct();
|
||||
var updated = p.WithUpdatedCategory(5, _time);
|
||||
|
||||
updated.Should().NotBeSameAs(p);
|
||||
updated.RubroId.Should().Be(5);
|
||||
}
|
||||
|
||||
// ── MedioId and ProductTypeId are immutable ───────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Product_HasNoMethodToChangeMedioId()
|
||||
{
|
||||
var type = typeof(Product);
|
||||
var methods = type.GetMethods(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
|
||||
|
||||
methods.Should().NotContain(m => m.Name.Contains("Medio") && m.Name.StartsWith("With"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Product_HasNoMethodToChangeProductTypeId()
|
||||
{
|
||||
var type = typeof(Product);
|
||||
var methods = type.GetMethods(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
|
||||
|
||||
methods.Should().NotContain(m => m.Name.Contains("ProductType") && m.Name.StartsWith("With"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using FluentAssertions;
|
||||
using SIGCM2.Domain.Exceptions;
|
||||
|
||||
namespace SIGCM2.Application.Tests.Products.Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// PRD-002 — Tests for new Product domain exceptions.
|
||||
/// Verifies message content and property values.
|
||||
/// </summary>
|
||||
public class ProductExceptionsTests
|
||||
{
|
||||
[Fact]
|
||||
public void ProductNotFoundException_ContainsId_InMessage()
|
||||
{
|
||||
var ex = new ProductNotFoundException(5);
|
||||
|
||||
ex.Message.Should().Contain("5");
|
||||
ex.ProductId.Should().Be(5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProductNombreDuplicadoEnMedioTipoException_ContainsDetails()
|
||||
{
|
||||
var ex = new ProductNombreDuplicadoEnMedioTipoException(2, 3, "Clasificado");
|
||||
|
||||
ex.Message.Should().Contain("Clasificado");
|
||||
ex.Message.Should().Contain("2");
|
||||
ex.Message.Should().Contain("3");
|
||||
ex.MedioId.Should().Be(2);
|
||||
ex.ProductTypeId.Should().Be(3);
|
||||
ex.Nombre.Should().Be("Clasificado");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProductTipoFlagsIncoherentesException_FieldAndMessage()
|
||||
{
|
||||
var ex = new ProductTipoFlagsIncoherentesException("requiere RubroId", "rubroId");
|
||||
|
||||
ex.Field.Should().Be("rubroId");
|
||||
ex.Message.Should().Contain("requiere RubroId");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProductTypeInactivoException_ContainsProductTypeId()
|
||||
{
|
||||
var ex = new ProductTypeInactivoException(7);
|
||||
|
||||
ex.Message.Should().Contain("7");
|
||||
ex.ProductTypeId.Should().Be(7);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RubroInactivoException_ContainsRubroId()
|
||||
{
|
||||
var ex = new RubroInactivoException(12);
|
||||
|
||||
ex.Message.Should().Contain("12");
|
||||
ex.RubroId.Should().Be(12);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user