Files
SIG-CM2.0/tests/SIGCM2.Application.Tests/Rubros/RubroRepositoryTests.cs
dmolinari 389dda6e5e fix(tests): consolidar V016 en SqlTestFixture post issue #29
Rebase de CAT-001 sobre main (post #29) requiere:
- EnsureV016SchemaAsync en SqlTestFixture
- Rubro_History en TablesToIgnore central (el commit original b1be4a5 se skipeo por ser obsoleto post consolidacion)
- catalogo:rubros:gestionar en seed canonical de Permiso + RolPermiso admin
- RubroRepositoryTests refactorizado al patron [Collection] + SqlTestFixture
- RubrosControllerTests apunta a TestConnectionStrings.ApiTestDb
- Counts de permisos admin actualizados 24 -> 25 en 5 tests

Verify: App 819/819 + Api 251/251 + vitest 349/349 verde post-rebase.
2026-04-19 07:49:18 -03:00

391 lines
15 KiB
C#

using Dapper;
using SIGCM2.Domain.Entities;
using SIGCM2.Infrastructure.Persistence;
using SIGCM2.TestSupport;
namespace SIGCM2.Application.Tests.Rubros;
/// <summary>
/// Integration tests for RubroRepository against SIGCM2_Test_App.
/// Uses shared SqlTestFixture via [Collection("Database")] — fixture maneja Respawn + seeds.
/// Temporal: after UpdateAsync, dbo.Rubro_History MUST have ≥1 row for that Id.
/// </summary>
[Collection("Database")]
public class RubroRepositoryTests : IAsyncLifetime
{
private readonly SqlTestFixture _db;
private RubroRepository _repository = null!;
private TimeProvider _timeProvider = null!;
public RubroRepositoryTests(SqlTestFixture db)
{
_db = db;
}
public async Task InitializeAsync()
{
await _db.ResetAndSeedAsync();
var factory = new SqlConnectionFactory(TestConnectionStrings.AppTestDb);
_repository = new RubroRepository(factory);
_timeProvider = TimeProvider.System;
}
public Task DisposeAsync() => Task.CompletedTask;
// ── AddAsync + GetByIdAsync roundtrip ─────────────────────────────────────
[Fact]
public async Task AddAsync_AndGetById_ReturnsAllFields()
{
var rubro = Rubro.ForCreation("Automotores", parentId: null, orden: 0, tarifarioBaseId: null, _timeProvider);
var id = await _repository.AddAsync(rubro);
var result = await _repository.GetByIdAsync(id);
Assert.NotNull(result);
Assert.Equal(id, result!.Id);
Assert.Equal("Automotores", result.Nombre);
Assert.Null(result.ParentId);
Assert.Equal(0, result.Orden);
Assert.True(result.Activo);
Assert.Null(result.TarifarioBaseId);
Assert.True(result.FechaCreacion > DateTime.MinValue);
Assert.Null(result.FechaModificacion);
}
[Fact]
public async Task AddAsync_WithTarifarioBaseId_PersistsValue()
{
var rubro = Rubro.ForCreation("Tecnología", parentId: null, orden: 0, tarifarioBaseId: 42, _timeProvider);
var id = await _repository.AddAsync(rubro);
var result = await _repository.GetByIdAsync(id);
Assert.NotNull(result);
Assert.Equal(42, result!.TarifarioBaseId);
}
[Fact]
public async Task GetByIdAsync_NonExistent_ReturnsNull()
{
var result = await _repository.GetByIdAsync(999999);
Assert.Null(result);
}
// ── GetAllAsync ────────────────────────────────────────────────────────────
[Fact]
public async Task GetAllAsync_IncluirInactivosFalse_OmitsInactive()
{
var activo = Rubro.ForCreation("Activo", null, 0, null, _timeProvider);
var inactivo = Rubro.ForCreation("Inactivo", null, 1, null, _timeProvider);
await _repository.AddAsync(activo);
var inactivoId = await _repository.AddAsync(inactivo);
// Deactivate the second one via UpdateAsync
var inactivoEntity = await _repository.GetByIdAsync(inactivoId);
await _repository.UpdateAsync(inactivoEntity!.WithActivo(false, _timeProvider));
var all = await _repository.GetAllAsync(incluirInactivos: false);
Assert.Contains(all, r => r.Nombre == "Activo");
Assert.DoesNotContain(all, r => r.Nombre == "Inactivo");
}
[Fact]
public async Task GetAllAsync_IncluirInactivosTrue_ReturnsAll()
{
var activo = Rubro.ForCreation("ActivoAll", null, 0, null, _timeProvider);
var inactivo = Rubro.ForCreation("InactivoAll", null, 1, null, _timeProvider);
await _repository.AddAsync(activo);
var inactivoId = await _repository.AddAsync(inactivo);
var inactivoEntity = await _repository.GetByIdAsync(inactivoId);
await _repository.UpdateAsync(inactivoEntity!.WithActivo(false, _timeProvider));
var all = await _repository.GetAllAsync(incluirInactivos: true);
Assert.Contains(all, r => r.Nombre == "ActivoAll");
Assert.Contains(all, r => r.Nombre == "InactivoAll");
}
// ── GetDescendantsAsync ────────────────────────────────────────────────────
[Fact]
public async Task GetDescendantsAsync_3LevelsDeep_ReturnsAllDescendants()
{
// Level 0: root
var root = Rubro.ForCreation("Root", null, 0, null, _timeProvider);
var rootId = await _repository.AddAsync(root);
// Level 1: child of root
var child = Rubro.ForCreation("Child", rootId, 0, null, _timeProvider);
var childId = await _repository.AddAsync(child);
// Level 2: grandchild of root
var grandchild = Rubro.ForCreation("Grandchild", childId, 0, null, _timeProvider);
var grandchildId = await _repository.AddAsync(grandchild);
var descendants = await _repository.GetDescendantsAsync(rootId);
Assert.Equal(2, descendants.Count);
Assert.Contains(descendants, d => d.Id == childId);
Assert.Contains(descendants, d => d.Id == grandchildId);
}
[Fact]
public async Task GetDescendantsAsync_Leaf_ReturnsEmpty()
{
var leaf = Rubro.ForCreation("Leaf", null, 0, null, _timeProvider);
var leafId = await _repository.AddAsync(leaf);
var descendants = await _repository.GetDescendantsAsync(leafId);
Assert.Empty(descendants);
}
// ── CountActiveChildrenAsync ───────────────────────────────────────────────
[Fact]
public async Task CountActiveChildrenAsync_NoChildren_ReturnsZero()
{
var parent = Rubro.ForCreation("ParentNoHijos", null, 0, null, _timeProvider);
var parentId = await _repository.AddAsync(parent);
var count = await _repository.CountActiveChildrenAsync(parentId);
Assert.Equal(0, count);
}
[Fact]
public async Task CountActiveChildrenAsync_TwoActive_ReturnsTwo()
{
var parent = Rubro.ForCreation("ParentDosHijos", null, 0, null, _timeProvider);
var parentId = await _repository.AddAsync(parent);
await _repository.AddAsync(Rubro.ForCreation("Hijo1", parentId, 0, null, _timeProvider));
await _repository.AddAsync(Rubro.ForCreation("Hijo2", parentId, 1, null, _timeProvider));
var count = await _repository.CountActiveChildrenAsync(parentId);
Assert.Equal(2, count);
}
[Fact]
public async Task CountActiveChildrenAsync_ActiveAndInactive_CountsOnlyActive()
{
var parent = Rubro.ForCreation("ParentMixed", null, 0, null, _timeProvider);
var parentId = await _repository.AddAsync(parent);
await _repository.AddAsync(Rubro.ForCreation("HijoActivo", parentId, 0, null, _timeProvider));
var inactivoId = await _repository.AddAsync(Rubro.ForCreation("HijoInactivo", parentId, 1, null, _timeProvider));
var inactivo = await _repository.GetByIdAsync(inactivoId);
await _repository.UpdateAsync(inactivo!.WithActivo(false, _timeProvider));
var count = await _repository.CountActiveChildrenAsync(parentId);
Assert.Equal(1, count);
}
// ── GetMaxOrdenAsync ───────────────────────────────────────────────────────
[Fact]
public async Task GetMaxOrdenAsync_Empty_ReturnsZero()
{
// No siblings → first slot is 0
var orden = await _repository.GetMaxOrdenAsync(parentId: null);
Assert.Equal(0, orden);
}
[Fact]
public async Task GetMaxOrdenAsync_ThreeSiblings_ReturnsThree()
{
// Orden = [0, 1, 2] → next slot = 3 (MAX+1)
var parent = Rubro.ForCreation("ParentOrden", null, 0, null, _timeProvider);
var parentId = await _repository.AddAsync(parent);
await _repository.AddAsync(Rubro.ForCreation("S1", parentId, 0, null, _timeProvider));
await _repository.AddAsync(Rubro.ForCreation("S2", parentId, 1, null, _timeProvider));
await _repository.AddAsync(Rubro.ForCreation("S3", parentId, 2, null, _timeProvider));
var orden = await _repository.GetMaxOrdenAsync(parentId);
Assert.Equal(3, orden);
}
[Fact]
public async Task GetMaxOrdenAsync_ParentIdNull_WorksForRoots()
{
// Insert one root with Orden=0
await _repository.AddAsync(Rubro.ForCreation("RootOrden1", null, 0, null, _timeProvider));
var orden = await _repository.GetMaxOrdenAsync(parentId: null);
Assert.Equal(1, orden); // MAX(0) + 1 = 1
}
// ── ExistsByNombreUnderParentAsync ─────────────────────────────────────────
[Fact]
public async Task ExistsByNombreUnderParentAsync_Exists_ReturnsTrue()
{
var parent = Rubro.ForCreation("ParentExists", null, 0, null, _timeProvider);
var parentId = await _repository.AddAsync(parent);
await _repository.AddAsync(Rubro.ForCreation("Autos", parentId, 0, null, _timeProvider));
var exists = await _repository.ExistsByNombreUnderParentAsync(parentId, "Autos", excludeId: null);
Assert.True(exists);
}
[Fact]
public async Task ExistsByNombreUnderParentAsync_SameNameDifferentParent_ReturnsFalse()
{
var parent1 = Rubro.ForCreation("Parent1", null, 0, null, _timeProvider);
var parent1Id = await _repository.AddAsync(parent1);
var parent2 = Rubro.ForCreation("Parent2", null, 1, null, _timeProvider);
var parent2Id = await _repository.AddAsync(parent2);
// Add "Autos" under parent1
await _repository.AddAsync(Rubro.ForCreation("Autos", parent1Id, 0, null, _timeProvider));
// Check under parent2 — should not exist
var exists = await _repository.ExistsByNombreUnderParentAsync(parent2Id, "Autos", excludeId: null);
Assert.False(exists);
}
[Fact]
public async Task ExistsByNombreUnderParentAsync_ExcludeId_WhenProvidedSkipsSelf()
{
var parent = Rubro.ForCreation("ParentExclude", null, 0, null, _timeProvider);
var parentId = await _repository.AddAsync(parent);
var id = await _repository.AddAsync(Rubro.ForCreation("AutosExclude", parentId, 0, null, _timeProvider));
// Excluding self → should return false (no other rubro with same name)
var exists = await _repository.ExistsByNombreUnderParentAsync(parentId, "AutosExclude", excludeId: id);
Assert.False(exists);
}
[Fact]
public async Task ExistsByNombreUnderParentAsync_CaseInsensitive_InsensibleAMayusculas()
{
var parent = Rubro.ForCreation("ParentCI", null, 0, null, _timeProvider);
var parentId = await _repository.AddAsync(parent);
await _repository.AddAsync(Rubro.ForCreation("autos", parentId, 0, null, _timeProvider));
// "AUTOS" should match "autos" (case-insensitive)
var exists = await _repository.ExistsByNombreUnderParentAsync(parentId, "AUTOS", excludeId: null);
Assert.True(exists);
}
[Fact]
public async Task ExistsByNombreUnderParentAsync_ForRoot_ParentIdNull_WorksWithApplicationDefense()
{
// The DB filtered index only covers non-root rubros (WHERE ParentId IS NOT NULL AND Activo = 1).
// Application must check roots via full scan (no unique index guarantee at DB level).
await _repository.AddAsync(Rubro.ForCreation("RootCI", null, 0, null, _timeProvider));
var exists = await _repository.ExistsByNombreUnderParentAsync(null, "RootCI", excludeId: null);
Assert.True(exists);
}
// ── GetDepthAsync ──────────────────────────────────────────────────────────
[Fact]
public async Task GetDepthAsync_RootParent_ReturnsZero()
{
// parentId = null means we're creating a root → depth = 0
var depth = await _repository.GetDepthAsync(parentId: null);
Assert.Equal(0, depth);
}
[Fact]
public async Task GetDepthAsync_3LevelsDeep_ReturnsThree()
{
// root (depth 0) → child (depth 1) → grandchild (depth 2) → great-grandchild (depth 3)
var rootId = await _repository.AddAsync(Rubro.ForCreation("RootDepth", null, 0, null, _timeProvider));
var childId = await _repository.AddAsync(Rubro.ForCreation("ChildDepth", rootId, 0, null, _timeProvider));
var grandchildId = await _repository.AddAsync(Rubro.ForCreation("GrandchildDepth", childId, 0, null, _timeProvider));
// Depth of the grandchild's own id as parentId = 3 levels deep (root=1, child=2, grandchild=3)
var depth = await _repository.GetDepthAsync(grandchildId);
Assert.Equal(3, depth);
}
// ── UpdateAsync + Temporal ────────────────────────────────────────────────
[Fact]
public async Task UpdateAsync_ModificaCamposYProduceHistoryRow()
{
var id = await _repository.AddAsync(Rubro.ForCreation("Original", null, 0, null, _timeProvider));
var original = await _repository.GetByIdAsync(id);
var renamed = original!.WithRenamed("Actualizado", _timeProvider);
await _repository.UpdateAsync(renamed);
var result = await _repository.GetByIdAsync(id);
Assert.NotNull(result);
Assert.Equal("Actualizado", result!.Nombre);
Assert.NotNull(result.FechaModificacion);
var historyCount = await _db.Connection.ExecuteScalarAsync<int>(
"SELECT COUNT(*) FROM dbo.Rubro_History WHERE Id = @Id", new { Id = id });
Assert.True(historyCount >= 1, $"Expected ≥1 history row for Rubro Id={id}, got {historyCount}");
}
[Fact]
public async Task SoftDeleteAsync_FlipActivoYActualizaFechaModificacion()
{
// Deactivate via UpdateAsync (with WithActivo(false)) — repository has no separate SoftDelete
var id = await _repository.AddAsync(Rubro.ForCreation("ToDeactivate", null, 0, null, _timeProvider));
var rubro = await _repository.GetByIdAsync(id);
var deactivated = rubro!.WithActivo(false, _timeProvider);
await _repository.UpdateAsync(deactivated);
var result = await _repository.GetByIdAsync(id);
Assert.NotNull(result);
Assert.False(result!.Activo);
Assert.NotNull(result.FechaModificacion);
}
[Fact]
public async Task MoveAsync_CambiaParentIdOrdenYFechaModificacion()
{
var parent1Id = await _repository.AddAsync(Rubro.ForCreation("MoveParent1", null, 0, null, _timeProvider));
var parent2Id = await _repository.AddAsync(Rubro.ForCreation("MoveParent2", null, 1, null, _timeProvider));
var childId = await _repository.AddAsync(Rubro.ForCreation("MoveChild", parent1Id, 0, null, _timeProvider));
var child = await _repository.GetByIdAsync(childId);
var moved = child!.WithMoved(parent2Id, 5, _timeProvider);
await _repository.UpdateAsync(moved);
var result = await _repository.GetByIdAsync(childId);
Assert.NotNull(result);
Assert.Equal(parent2Id, result!.ParentId);
Assert.Equal(5, result.Orden);
Assert.NotNull(result.FechaModificacion);
}
}