feat(domain): Product entity + 5 domain exceptions (PRD-002)
This commit is contained in:
172
src/api/SIGCM2.Domain/Entities/Product.cs
Normal file
172
src/api/SIGCM2.Domain/Entities/Product.cs
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
namespace SIGCM2.Domain.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Immutable product entity for the commercial catalog.
|
||||||
|
/// Factory method ForCreation creates new products (Id=0).
|
||||||
|
/// Mutation methods (With*) return new instances — original is never modified.
|
||||||
|
/// Flag coherence (RequiresCategory/HasDuration) is enforced by Application handlers
|
||||||
|
/// at creation/update time against the ProductType, NOT here in the entity.
|
||||||
|
/// MedioId and ProductTypeId are immutable post-creation by design.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class Product
|
||||||
|
{
|
||||||
|
private const int NombreMaxLength = 300;
|
||||||
|
|
||||||
|
public int Id { get; }
|
||||||
|
public string Nombre { get; }
|
||||||
|
public int MedioId { get; }
|
||||||
|
public int ProductTypeId { get; }
|
||||||
|
public int? RubroId { get; }
|
||||||
|
public decimal BasePrice { get; }
|
||||||
|
public int? PriceDurationDays { get; }
|
||||||
|
public bool IsActive { get; }
|
||||||
|
public DateTime FechaCreacion { get; }
|
||||||
|
public DateTime? FechaModificacion { get; }
|
||||||
|
|
||||||
|
/// <summary>Full hydration constructor — used by the repository to reconstruct from DB rows.</summary>
|
||||||
|
public Product(
|
||||||
|
int id,
|
||||||
|
string nombre,
|
||||||
|
int medioId,
|
||||||
|
int productTypeId,
|
||||||
|
int? rubroId,
|
||||||
|
decimal basePrice,
|
||||||
|
int? priceDurationDays,
|
||||||
|
bool isActive,
|
||||||
|
DateTime fechaCreacion,
|
||||||
|
DateTime? fechaModificacion)
|
||||||
|
{
|
||||||
|
Id = id;
|
||||||
|
Nombre = nombre;
|
||||||
|
MedioId = medioId;
|
||||||
|
ProductTypeId = productTypeId;
|
||||||
|
RubroId = rubroId;
|
||||||
|
BasePrice = basePrice;
|
||||||
|
PriceDurationDays = priceDurationDays;
|
||||||
|
IsActive = isActive;
|
||||||
|
FechaCreacion = fechaCreacion;
|
||||||
|
FechaModificacion = fechaModificacion;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Factory for a new Product. Id=0 — DB assigns via IDENTITY.
|
||||||
|
/// IsActive=true, FechaModificacion=null.
|
||||||
|
/// </summary>
|
||||||
|
public static Product ForCreation(
|
||||||
|
string nombre,
|
||||||
|
int medioId,
|
||||||
|
int productTypeId,
|
||||||
|
int? rubroId,
|
||||||
|
decimal basePrice,
|
||||||
|
int? priceDurationDays,
|
||||||
|
TimeProvider timeProvider)
|
||||||
|
{
|
||||||
|
ValidateNombre(nombre);
|
||||||
|
ValidateMedioId(medioId);
|
||||||
|
ValidateProductTypeId(productTypeId);
|
||||||
|
ValidateRubroId(rubroId);
|
||||||
|
ValidateBasePrice(basePrice);
|
||||||
|
ValidatePriceDurationDays(priceDurationDays);
|
||||||
|
|
||||||
|
return new Product(
|
||||||
|
id: 0,
|
||||||
|
nombre: nombre.Trim(),
|
||||||
|
medioId: medioId,
|
||||||
|
productTypeId: productTypeId,
|
||||||
|
rubroId: rubroId,
|
||||||
|
basePrice: basePrice,
|
||||||
|
priceDurationDays: priceDurationDays,
|
||||||
|
isActive: true,
|
||||||
|
fechaCreacion: timeProvider.GetUtcNow().UtcDateTime,
|
||||||
|
fechaModificacion: null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Product WithRenamed(string nuevoNombre, TimeProvider timeProvider)
|
||||||
|
{
|
||||||
|
ValidateNombre(nuevoNombre);
|
||||||
|
return new Product(Id, nuevoNombre.Trim(), MedioId, ProductTypeId, RubroId,
|
||||||
|
BasePrice, PriceDurationDays, IsActive, FechaCreacion,
|
||||||
|
timeProvider.GetUtcNow().UtcDateTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Product WithUpdatedPrice(decimal basePrice, int? priceDurationDays, TimeProvider timeProvider)
|
||||||
|
{
|
||||||
|
ValidateBasePrice(basePrice);
|
||||||
|
ValidatePriceDurationDays(priceDurationDays);
|
||||||
|
return new Product(Id, Nombre, MedioId, ProductTypeId, RubroId,
|
||||||
|
basePrice, priceDurationDays, IsActive, FechaCreacion,
|
||||||
|
timeProvider.GetUtcNow().UtcDateTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Product WithUpdatedCategory(int? rubroId, TimeProvider timeProvider)
|
||||||
|
{
|
||||||
|
ValidateRubroId(rubroId);
|
||||||
|
return new Product(Id, Nombre, MedioId, ProductTypeId, rubroId,
|
||||||
|
BasePrice, PriceDurationDays, IsActive, FechaCreacion,
|
||||||
|
timeProvider.GetUtcNow().UtcDateTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Combo mutator: renames, updates price and category in one call.
|
||||||
|
/// Used by UpdateProductCommandHandler.
|
||||||
|
/// </summary>
|
||||||
|
public Product WithUpdated(string nombre, int? rubroId, decimal basePrice, int? priceDurationDays, TimeProvider timeProvider)
|
||||||
|
{
|
||||||
|
ValidateNombre(nombre);
|
||||||
|
ValidateRubroId(rubroId);
|
||||||
|
ValidateBasePrice(basePrice);
|
||||||
|
ValidatePriceDurationDays(priceDurationDays);
|
||||||
|
return new Product(Id, nombre.Trim(), MedioId, ProductTypeId, rubroId,
|
||||||
|
basePrice, priceDurationDays, IsActive, FechaCreacion,
|
||||||
|
timeProvider.GetUtcNow().UtcDateTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Product WithDeactivated(TimeProvider timeProvider)
|
||||||
|
=> new(Id, Nombre, MedioId, ProductTypeId, RubroId,
|
||||||
|
BasePrice, PriceDurationDays, isActive: false, FechaCreacion,
|
||||||
|
timeProvider.GetUtcNow().UtcDateTime);
|
||||||
|
|
||||||
|
// ── Private validators ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static void ValidateNombre(string nombre)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(nombre))
|
||||||
|
throw new ArgumentException(
|
||||||
|
"El Nombre del producto no puede estar vacío o ser solo espacios.", nameof(nombre));
|
||||||
|
if (nombre.Length > NombreMaxLength)
|
||||||
|
throw new ArgumentException(
|
||||||
|
$"El Nombre del producto no puede superar los {NombreMaxLength} caracteres.", nameof(nombre));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateMedioId(int medioId)
|
||||||
|
{
|
||||||
|
if (medioId <= 0)
|
||||||
|
throw new ArgumentException("medioId debe ser un entero positivo.", nameof(medioId));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateProductTypeId(int productTypeId)
|
||||||
|
{
|
||||||
|
if (productTypeId <= 0)
|
||||||
|
throw new ArgumentException("productTypeId debe ser un entero positivo.", nameof(productTypeId));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateRubroId(int? rubroId)
|
||||||
|
{
|
||||||
|
if (rubroId.HasValue && rubroId.Value <= 0)
|
||||||
|
throw new ArgumentException(
|
||||||
|
"rubroId debe ser un entero positivo cuando no es nulo.", nameof(rubroId));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateBasePrice(decimal basePrice)
|
||||||
|
{
|
||||||
|
if (basePrice < 0m)
|
||||||
|
throw new ArgumentException("basePrice no puede ser negativo.", nameof(basePrice));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidatePriceDurationDays(int? priceDurationDays)
|
||||||
|
{
|
||||||
|
if (priceDurationDays.HasValue && priceDurationDays.Value <= 0)
|
||||||
|
throw new ArgumentException(
|
||||||
|
"priceDurationDays debe ser >= 1 cuando se provee.", nameof(priceDurationDays));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
namespace SIGCM2.Domain.Exceptions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Thrown when a Product with the same Nombre already exists for a given MedioId+ProductTypeId. → HTTP 409
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ProductNombreDuplicadoEnMedioTipoException : DomainException
|
||||||
|
{
|
||||||
|
public int MedioId { get; }
|
||||||
|
public int ProductTypeId { get; }
|
||||||
|
public string Nombre { get; }
|
||||||
|
|
||||||
|
public ProductNombreDuplicadoEnMedioTipoException(int medioId, int productTypeId, string nombre)
|
||||||
|
: base($"Ya existe un producto activo con nombre '{nombre}' para medioId={medioId} y productTypeId={productTypeId}.")
|
||||||
|
{
|
||||||
|
MedioId = medioId;
|
||||||
|
ProductTypeId = productTypeId;
|
||||||
|
Nombre = nombre;
|
||||||
|
}
|
||||||
|
}
|
||||||
15
src/api/SIGCM2.Domain/Exceptions/ProductNotFoundException.cs
Normal file
15
src/api/SIGCM2.Domain/Exceptions/ProductNotFoundException.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
namespace SIGCM2.Domain.Exceptions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Thrown when a requested Product does not exist. → HTTP 404
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ProductNotFoundException : DomainException
|
||||||
|
{
|
||||||
|
public int ProductId { get; }
|
||||||
|
|
||||||
|
public ProductNotFoundException(int id)
|
||||||
|
: base($"El producto con id={id} no existe.")
|
||||||
|
{
|
||||||
|
ProductId = id;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
namespace SIGCM2.Domain.Exceptions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Thrown when a Product's field violates the flags coherence rules of its ProductType
|
||||||
|
/// (e.g. RequiresCategory=true but RubroId is null, or HasDuration=true but PriceDurationDays is null). → HTTP 422
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ProductTipoFlagsIncoherentesException : DomainException
|
||||||
|
{
|
||||||
|
public string Field { get; }
|
||||||
|
|
||||||
|
public ProductTipoFlagsIncoherentesException(string reason, string field)
|
||||||
|
: base($"Incoherencia de flags del tipo de producto: {reason}.")
|
||||||
|
{
|
||||||
|
Field = field;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
namespace SIGCM2.Domain.Exceptions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Thrown when attempting to create/update a Product referencing an inactive ProductType. → HTTP 422
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ProductTypeInactivoException : DomainException
|
||||||
|
{
|
||||||
|
public int ProductTypeId { get; }
|
||||||
|
|
||||||
|
public ProductTypeInactivoException(int productTypeId)
|
||||||
|
: base($"El tipo de producto con id={productTypeId} está inactivo y no puede asignarse a un producto.")
|
||||||
|
{
|
||||||
|
ProductTypeId = productTypeId;
|
||||||
|
}
|
||||||
|
}
|
||||||
15
src/api/SIGCM2.Domain/Exceptions/RubroInactivoException.cs
Normal file
15
src/api/SIGCM2.Domain/Exceptions/RubroInactivoException.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
namespace SIGCM2.Domain.Exceptions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Thrown when attempting to create/update a Product referencing an inactive Rubro. → HTTP 422
|
||||||
|
/// </summary>
|
||||||
|
public sealed class RubroInactivoException : DomainException
|
||||||
|
{
|
||||||
|
public int RubroId { get; }
|
||||||
|
|
||||||
|
public RubroInactivoException(int rubroId)
|
||||||
|
: base($"El rubro con id={rubroId} está inactivo y no puede asignarse a un producto.")
|
||||||
|
{
|
||||||
|
RubroId = rubroId;
|
||||||
|
}
|
||||||
|
}
|
||||||
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