feat(domain): RubroConProductosActivosException + guard en DeactivateRubro (closes #41) #44
@@ -34,4 +34,16 @@ public sealed class ProductQueryRepository : IProductQueryRepository
|
||||
var result = await connection.ExecuteScalarAsync<int>(sql, new { ProductTypeId = productTypeId });
|
||||
return result == 1;
|
||||
}
|
||||
|
||||
public async Task<int> CountActiveByRubroAsync(int rubroId, CancellationToken ct = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT COUNT(1) FROM dbo.Product WHERE RubroId = @RubroId AND IsActive = 1
|
||||
""";
|
||||
|
||||
await using var connection = _factory.CreateConnection();
|
||||
await connection.OpenAsync(ct);
|
||||
|
||||
return await connection.ExecuteScalarAsync<int>(sql, new { RubroId = rubroId });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ namespace SIGCM2.Application.Tests.Products.Repository;
|
||||
/// <summary>
|
||||
/// PRD-002 — Integration tests for ProductQueryRepository against SIGCM2_Test_App.
|
||||
/// These tests verify the real Dapper implementation replaces NullProductQueryRepository.
|
||||
/// Issue #41: CountActiveByRubroAsync tests added here (same class, same DB, same fixture).
|
||||
/// </summary>
|
||||
[Collection("Database")]
|
||||
public class ProductQueryRepositoryTests : IAsyncLifetime
|
||||
@@ -44,7 +45,7 @@ public class ProductQueryRepositoryTests : IAsyncLifetime
|
||||
{
|
||||
// Arrange: insert a ProductType and an active Product referencing it
|
||||
var (medioId, productTypeId) = await InsertMedioAndProductTypeAsync();
|
||||
await InsertActiveProductAsync(medioId, productTypeId);
|
||||
await InsertActiveProductAsync(medioId, productTypeId, rubroId: null);
|
||||
|
||||
var result = await _repository.ExistsActiveByProductTypeAsync(productTypeId);
|
||||
|
||||
@@ -56,7 +57,7 @@ public class ProductQueryRepositoryTests : IAsyncLifetime
|
||||
{
|
||||
// Arrange: insert an inactive product
|
||||
var (medioId, productTypeId) = await InsertMedioAndProductTypeAsync();
|
||||
await InsertInactiveProductAsync(medioId, productTypeId);
|
||||
await InsertInactiveProductAsync(medioId, productTypeId, rubroId: null);
|
||||
|
||||
var result = await _repository.ExistsActiveByProductTypeAsync(productTypeId);
|
||||
|
||||
@@ -68,7 +69,7 @@ public class ProductQueryRepositoryTests : IAsyncLifetime
|
||||
{
|
||||
// Arrange: insert active product for productTypeId=A, query for productTypeId=B
|
||||
var (medioId, productTypeId) = await InsertMedioAndProductTypeAsync();
|
||||
await InsertActiveProductAsync(medioId, productTypeId);
|
||||
await InsertActiveProductAsync(medioId, productTypeId, rubroId: null);
|
||||
var otherProductTypeId = productTypeId + 100;
|
||||
|
||||
var result = await _repository.ExistsActiveByProductTypeAsync(otherProductTypeId);
|
||||
@@ -76,8 +77,65 @@ public class ProductQueryRepositoryTests : IAsyncLifetime
|
||||
result.Should().BeFalse();
|
||||
}
|
||||
|
||||
// ── CountActiveByRubroAsync (issue #41) ──────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task CountActiveByRubroAsync_NoProducts_ReturnsZero()
|
||||
{
|
||||
var result = await _repository.CountActiveByRubroAsync(rubroId: 99999);
|
||||
|
||||
result.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CountActiveByRubroAsync_Returns_CorrectCount()
|
||||
{
|
||||
// Arrange: insert a Rubro and products in various states
|
||||
var rubroId = await InsertRubroAsync("Rubro Clasificados");
|
||||
var (medioId, productTypeId) = await InsertMedioAndProductTypeAsync();
|
||||
|
||||
// 2 active products referencing the rubro
|
||||
await InsertActiveProductAsync(medioId, productTypeId, rubroId: rubroId);
|
||||
await InsertActiveProductAsync(medioId, productTypeId, rubroId: rubroId);
|
||||
|
||||
// 1 inactive product referencing the same rubro (should NOT count)
|
||||
await InsertInactiveProductAsync(medioId, productTypeId, rubroId: rubroId);
|
||||
|
||||
// 1 active product with a DIFFERENT rubroId (should NOT count)
|
||||
var otherRubroId = await InsertRubroAsync("Otro Rubro");
|
||||
await InsertActiveProductAsync(medioId, productTypeId, rubroId: otherRubroId);
|
||||
|
||||
var result = await _repository.CountActiveByRubroAsync(rubroId);
|
||||
|
||||
result.Should().Be(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CountActiveByRubroAsync_WithOnlyInactiveProducts_ReturnsZero()
|
||||
{
|
||||
var rubroId = await InsertRubroAsync("Rubro Solo Inactivos");
|
||||
var (medioId, productTypeId) = await InsertMedioAndProductTypeAsync();
|
||||
|
||||
await InsertInactiveProductAsync(medioId, productTypeId, rubroId: rubroId);
|
||||
|
||||
var result = await _repository.CountActiveByRubroAsync(rubroId);
|
||||
|
||||
result.Should().Be(0);
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
private async Task<int> InsertRubroAsync(string nombre)
|
||||
{
|
||||
await using var conn = new Microsoft.Data.SqlClient.SqlConnection(TestConnectionStrings.AppTestDb);
|
||||
await conn.OpenAsync();
|
||||
return await conn.ExecuteScalarAsync<int>("""
|
||||
INSERT INTO dbo.Rubro (Nombre, ParentId, Orden, Activo, TarifarioBaseId, FechaCreacion)
|
||||
OUTPUT INSERTED.Id
|
||||
VALUES (@Nombre, NULL, 0, 1, NULL, SYSUTCDATETIME())
|
||||
""", new { Nombre = nombre });
|
||||
}
|
||||
|
||||
private async Task<(int MedioId, int ProductTypeId)> InsertMedioAndProductTypeAsync()
|
||||
{
|
||||
await using var conn = new Microsoft.Data.SqlClient.SqlConnection(TestConnectionStrings.AppTestDb);
|
||||
@@ -98,23 +156,25 @@ public class ProductQueryRepositoryTests : IAsyncLifetime
|
||||
return (medioId, productTypeId);
|
||||
}
|
||||
|
||||
private async Task InsertActiveProductAsync(int medioId, int productTypeId)
|
||||
private async Task InsertActiveProductAsync(int medioId, int productTypeId, int? rubroId)
|
||||
{
|
||||
var nombre = $"ProdActivo-{Guid.NewGuid():N}";
|
||||
await using var conn = new Microsoft.Data.SqlClient.SqlConnection(TestConnectionStrings.AppTestDb);
|
||||
await conn.OpenAsync();
|
||||
await conn.ExecuteAsync("""
|
||||
INSERT INTO dbo.Product (Nombre, MedioId, ProductTypeId, BasePrice, IsActive, FechaCreacion)
|
||||
VALUES ('Producto Activo', @MedioId, @ProductTypeId, 100, 1, SYSUTCDATETIME())
|
||||
""", new { MedioId = medioId, ProductTypeId = productTypeId });
|
||||
INSERT INTO dbo.Product (Nombre, MedioId, ProductTypeId, RubroId, BasePrice, IsActive, FechaCreacion)
|
||||
VALUES (@Nombre, @MedioId, @ProductTypeId, @RubroId, 100, 1, SYSUTCDATETIME())
|
||||
""", new { Nombre = nombre, MedioId = medioId, ProductTypeId = productTypeId, RubroId = rubroId });
|
||||
}
|
||||
|
||||
private async Task InsertInactiveProductAsync(int medioId, int productTypeId)
|
||||
private async Task InsertInactiveProductAsync(int medioId, int productTypeId, int? rubroId)
|
||||
{
|
||||
var nombre = $"ProdInactivo-{Guid.NewGuid():N}";
|
||||
await using var conn = new Microsoft.Data.SqlClient.SqlConnection(TestConnectionStrings.AppTestDb);
|
||||
await conn.OpenAsync();
|
||||
await conn.ExecuteAsync("""
|
||||
INSERT INTO dbo.Product (Nombre, MedioId, ProductTypeId, BasePrice, IsActive, FechaCreacion)
|
||||
VALUES ('Producto Inactivo', @MedioId, @ProductTypeId, 100, 0, SYSUTCDATETIME())
|
||||
""", new { MedioId = medioId, ProductTypeId = productTypeId });
|
||||
INSERT INTO dbo.Product (Nombre, MedioId, ProductTypeId, RubroId, BasePrice, IsActive, FechaCreacion)
|
||||
VALUES (@Nombre, @MedioId, @ProductTypeId, @RubroId, 100, 0, SYSUTCDATETIME())
|
||||
""", new { Nombre = nombre, MedioId = medioId, ProductTypeId = productTypeId, RubroId = rubroId });
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user