159 lines
5.7 KiB
C#
159 lines
5.7 KiB
C#
using FluentAssertions;
|
|
using SIGCM2.Domain.Entities;
|
|
|
|
namespace SIGCM2.Application.Tests.Domain;
|
|
|
|
/// <summary>
|
|
/// PRD-003 — Domain unit tests for ProductPrice record.
|
|
/// Covers: §REQ-4.1 (IsActive), §REQ-4.2 (CoversDate inclusive bounds),
|
|
/// §REQ-4.4 (active row covers any date on or after PriceValidFrom).
|
|
/// </summary>
|
|
public class ProductPriceTests
|
|
{
|
|
private static ProductPrice MakePrice(
|
|
long id = 1,
|
|
int productId = 10,
|
|
decimal price = 1500.00m,
|
|
DateOnly? from = null,
|
|
DateOnly? to = null,
|
|
DateTime? fechaCreacion = null)
|
|
=> new(
|
|
Id: id,
|
|
ProductId: productId,
|
|
Price: price,
|
|
PriceValidFrom: from ?? new DateOnly(2026, 4, 1),
|
|
PriceValidTo: to,
|
|
FechaCreacion: fechaCreacion ?? new DateTime(2026, 4, 1, 0, 0, 0, DateTimeKind.Utc));
|
|
|
|
// ── §REQ-4.1: IsActive ────────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void IsActive_TrueWhenPriceValidToIsNull()
|
|
{
|
|
var price = MakePrice(to: null);
|
|
|
|
price.IsActive.Should().BeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void IsActive_FalseWhenPriceValidToHasValue()
|
|
{
|
|
var price = MakePrice(to: new DateOnly(2026, 4, 30));
|
|
|
|
price.IsActive.Should().BeFalse();
|
|
}
|
|
|
|
// ── §REQ-4.2: CoversDate — inclusive lower bound ──────────────────────────
|
|
|
|
[Fact]
|
|
public void CoversDate_InclusiveLowerBound_ReturnsTrue()
|
|
{
|
|
// Row: 2026-04-01 → 2026-04-30. Query date = PriceValidFrom.
|
|
var price = MakePrice(from: new DateOnly(2026, 4, 1), to: new DateOnly(2026, 4, 30));
|
|
|
|
price.CoversDate(new DateOnly(2026, 4, 1)).Should().BeTrue();
|
|
}
|
|
|
|
// ── §REQ-4.2: CoversDate — inclusive upper bound ──────────────────────────
|
|
|
|
[Fact]
|
|
public void CoversDate_InclusiveUpperBound_ReturnsTrue()
|
|
{
|
|
// Row: 2026-04-01 → 2026-04-30. Query date = PriceValidTo.
|
|
var price = MakePrice(from: new DateOnly(2026, 4, 1), to: new DateOnly(2026, 4, 30));
|
|
|
|
price.CoversDate(new DateOnly(2026, 4, 30)).Should().BeTrue();
|
|
}
|
|
|
|
// ── §REQ-4.2: CoversDate — exclusive outside range ────────────────────────
|
|
|
|
[Fact]
|
|
public void CoversDate_DateBeforePriceValidFrom_ReturnsFalse()
|
|
{
|
|
var price = MakePrice(from: new DateOnly(2026, 4, 10), to: new DateOnly(2026, 4, 20));
|
|
|
|
price.CoversDate(new DateOnly(2026, 4, 9)).Should().BeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public void CoversDate_DateAfterPriceValidTo_ReturnsFalse()
|
|
{
|
|
var price = MakePrice(from: new DateOnly(2026, 4, 10), to: new DateOnly(2026, 4, 20));
|
|
|
|
price.CoversDate(new DateOnly(2026, 4, 21)).Should().BeFalse();
|
|
}
|
|
|
|
// ── §REQ-4.4: Active row (PriceValidTo = null) covers any date on or after PriceValidFrom ──
|
|
|
|
[Fact]
|
|
public void ActiveRow_CoversAnyDateOnOrAfterPvf()
|
|
{
|
|
// Active row: PriceValidFrom = 2026-04-01, PriceValidTo = null
|
|
var price = MakePrice(from: new DateOnly(2026, 4, 1), to: null);
|
|
|
|
// On PriceValidFrom itself
|
|
price.CoversDate(new DateOnly(2026, 4, 1)).Should().BeTrue();
|
|
// Well in the future
|
|
price.CoversDate(new DateOnly(2030, 12, 31)).Should().BeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void ActiveRow_DoesNotCoverDateBeforePvf()
|
|
{
|
|
var price = MakePrice(from: new DateOnly(2026, 4, 1), to: null);
|
|
|
|
price.CoversDate(new DateOnly(2026, 3, 31)).Should().BeFalse();
|
|
}
|
|
|
|
// ── Record value equality (records compare by value) ─────────────────────
|
|
|
|
[Fact]
|
|
public void Equality_TwoIdenticalRecords_AreEqual()
|
|
{
|
|
var a = MakePrice(id: 1, productId: 10, price: 1500m,
|
|
from: new DateOnly(2026, 4, 1), to: null,
|
|
fechaCreacion: new DateTime(2026, 4, 1, 0, 0, 0, DateTimeKind.Utc));
|
|
|
|
var b = MakePrice(id: 1, productId: 10, price: 1500m,
|
|
from: new DateOnly(2026, 4, 1), to: null,
|
|
fechaCreacion: new DateTime(2026, 4, 1, 0, 0, 0, DateTimeKind.Utc));
|
|
|
|
a.Should().Be(b);
|
|
(a == b).Should().BeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void Equality_DifferentId_AreNotEqual()
|
|
{
|
|
var a = MakePrice(id: 1);
|
|
var b = MakePrice(id: 2);
|
|
|
|
a.Should().NotBe(b);
|
|
}
|
|
|
|
// ── Immutability: record positional props are init-only (no regular set) ─
|
|
|
|
[Fact]
|
|
public void Record_PropertiesAreInitOnly_NotMutableSet()
|
|
{
|
|
// C# records expose init-only accessors, not regular set.
|
|
// IsExternalInit attribute on the setter's return type distinguishes init from set.
|
|
var type = typeof(ProductPrice);
|
|
var mutableSetters = type.GetProperties()
|
|
.Where(p =>
|
|
{
|
|
var setter = p.GetSetMethod(nonPublic: false);
|
|
if (setter is null) return false;
|
|
// init-only setters have a modreq on IsExternalInit in their signature;
|
|
// a simple way to detect them: they are NOT init-only when
|
|
// ReturnParameter.GetRequiredCustomModifiers() does NOT contain IsExternalInit.
|
|
var modifiers = setter.ReturnParameter.GetRequiredCustomModifiers();
|
|
bool isInitOnly = modifiers.Any(m => m.Name == "IsExternalInit");
|
|
return !isInitOnly; // only flag non-init setters as mutable
|
|
})
|
|
.ToList();
|
|
|
|
mutableSetters.Should().BeEmpty("ProductPrice must be immutable — all setters should be init-only.");
|
|
}
|
|
}
|