Files
SIG-CM2.0/tests/SIGCM2.Application.Tests/Domain/ProductTests.cs

219 lines
8.1 KiB
C#
Raw Normal View History

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"));
}
}