80 lines
2.8 KiB
C#
80 lines
2.8 KiB
C#
|
|
using FluentAssertions;
|
||
|
|
using NSubstitute;
|
||
|
|
using SIGCM2.Application.Abstractions.Persistence;
|
||
|
|
using SIGCM2.Application.Products.Pricing;
|
||
|
|
using SIGCM2.Domain.Entities;
|
||
|
|
|
||
|
|
namespace SIGCM2.Application.Tests.Products.Pricing;
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// PRD-003 — ProductPricingService tests.
|
||
|
|
/// Covers: GetPriceAtAsync happy path, fecha sin precio → null (OQ-B contrato).
|
||
|
|
/// </summary>
|
||
|
|
public class ProductPricingServiceTests
|
||
|
|
{
|
||
|
|
private readonly IProductPriceRepository _repo = Substitute.For<IProductPriceRepository>();
|
||
|
|
private readonly ProductPricingService _service;
|
||
|
|
|
||
|
|
private static readonly DateOnly QueryDate = new(2026, 4, 19);
|
||
|
|
|
||
|
|
private static ProductPrice ActivePrice(decimal price = 150m) =>
|
||
|
|
new(Id: 1, ProductId: 1, Price: price,
|
||
|
|
PriceValidFrom: new DateOnly(2026, 1, 1), PriceValidTo: null,
|
||
|
|
FechaCreacion: new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc));
|
||
|
|
|
||
|
|
public ProductPricingServiceTests()
|
||
|
|
{
|
||
|
|
_service = new ProductPricingService(_repo);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Happy path ─────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task GetPriceAtAsync_ActivePriceCoverDate_ReturnsPrice()
|
||
|
|
{
|
||
|
|
_repo.GetActiveAsync(1, QueryDate, Arg.Any<CancellationToken>())
|
||
|
|
.Returns(ActivePrice(200m));
|
||
|
|
|
||
|
|
var result = await _service.GetPriceAtAsync(1, QueryDate);
|
||
|
|
|
||
|
|
result.Should().Be(200m);
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task GetPriceAtAsync_NoPriceForDate_ReturnsNull()
|
||
|
|
{
|
||
|
|
// OQ-B — si no hay historial para la fecha, retorna null.
|
||
|
|
// El consumidor (PRC-001) decide si usar BasePrice o lanzar excepción.
|
||
|
|
_repo.GetActiveAsync(1, QueryDate, Arg.Any<CancellationToken>())
|
||
|
|
.Returns((ProductPrice?)null);
|
||
|
|
|
||
|
|
var result = await _service.GetPriceAtAsync(1, QueryDate);
|
||
|
|
|
||
|
|
result.Should().BeNull();
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task GetPriceAtAsync_CallsGetActiveAsync_WithCorrectArgs()
|
||
|
|
{
|
||
|
|
_repo.GetActiveAsync(Arg.Any<int>(), Arg.Any<DateOnly>(), Arg.Any<CancellationToken>())
|
||
|
|
.Returns((ProductPrice?)null);
|
||
|
|
|
||
|
|
await _service.GetPriceAtAsync(productId: 5, date: QueryDate);
|
||
|
|
|
||
|
|
await _repo.Received(1).GetActiveAsync(5, QueryDate, Arg.Any<CancellationToken>());
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task GetPriceAtAsync_FutureDateOutsideWindow_ReturnsNull()
|
||
|
|
{
|
||
|
|
// Precio cerrado cuya ventana no cubre la fecha consultada → repo devuelve null
|
||
|
|
var futureDate = new DateOnly(2030, 1, 1);
|
||
|
|
_repo.GetActiveAsync(1, futureDate, Arg.Any<CancellationToken>())
|
||
|
|
.Returns((ProductPrice?)null);
|
||
|
|
|
||
|
|
var result = await _service.GetPriceAtAsync(1, futureDate);
|
||
|
|
|
||
|
|
result.Should().BeNull();
|
||
|
|
}
|
||
|
|
}
|