From 389dda6e5e3452959e5f0db64f923aba8ca52f78 Mon Sep 17 00:00:00 2001 From: dmolinari Date: Sun, 19 Apr 2026 07:49:18 -0300 Subject: [PATCH] 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. --- .../Auth/AuthControllerTests.cs | 5 +- .../Permisos/PermisosEndpointTests.cs | 14 +-- .../Rubros/RubrosControllerTests.cs | 3 +- .../Integration/PermisoRepositoryTests.cs | 7 +- .../Integration/RolPermisoRepositoryTests.cs | 7 +- .../Rubros/RubroRepositoryTests.cs | 86 +++---------------- tests/SIGCM2.TestSupport/SqlTestFixture.cs | 86 ++++++++++++++++++- 7 files changed, 118 insertions(+), 90 deletions(-) diff --git a/tests/SIGCM2.Api.Tests/Auth/AuthControllerTests.cs b/tests/SIGCM2.Api.Tests/Auth/AuthControllerTests.cs index 13c6faf..5649e52 100644 --- a/tests/SIGCM2.Api.Tests/Auth/AuthControllerTests.cs +++ b/tests/SIGCM2.Api.Tests/Auth/AuthControllerTests.cs @@ -49,8 +49,9 @@ public class AuthControllerTests Assert.Equal(JsonValueKind.Array, permisos.ValueKind); // V011 (ADM-001) adds 'administracion:secciones:gestionar' → 22 // V013 (ADM-008) adds 'administracion:puntos_de_venta:gestionar' → 23 - // V014 (ADM-009) adds 'administracion:fiscal:gestionar' → 24 total - Assert.Equal(24, permisos.GetArrayLength()); + // V014 (ADM-009) adds 'administracion:fiscal:gestionar' → 24 + // V016 (CAT-001) adds 'catalogo:rubros:gestionar' → 25 total + Assert.Equal(25, permisos.GetArrayLength()); } // Scenario: invalid credentials return 401 with opaque error diff --git a/tests/SIGCM2.Api.Tests/Permisos/PermisosEndpointTests.cs b/tests/SIGCM2.Api.Tests/Permisos/PermisosEndpointTests.cs index d641a0a..4fa9e24 100644 --- a/tests/SIGCM2.Api.Tests/Permisos/PermisosEndpointTests.cs +++ b/tests/SIGCM2.Api.Tests/Permisos/PermisosEndpointTests.cs @@ -129,7 +129,7 @@ public sealed class PermisosEndpointTests : IAsyncLifetime // ── GET /api/v1/permisos — catalog ─────────────────────────────────────── [Fact] - public async Task GetPermisos_WithAdmin_Returns200With24Items() + public async Task GetPermisos_WithAdmin_Returns200With25Items() { var token = await GetBearerTokenAsync(AdminUsername, AdminPassword); using var req = BuildRequest(HttpMethod.Get, "/api/v1/permisos", bearerToken: token); @@ -139,8 +139,9 @@ public sealed class PermisosEndpointTests : IAsyncLifetime var list = await resp.Content.ReadFromJsonAsync(); // V011 (ADM-001) adds 'administracion:secciones:gestionar' → 22 // V013 (ADM-008) adds 'administracion:puntos_de_venta:gestionar' → 23 - // V014 (ADM-009) adds 'administracion:fiscal:gestionar' → 24 total - Assert.Equal(24, list.GetArrayLength()); + // V014 (ADM-009) adds 'administracion:fiscal:gestionar' → 24 + // V016 (CAT-001) adds 'catalogo:rubros:gestionar' → 25 total + Assert.Equal(25, list.GetArrayLength()); } [Fact] @@ -183,7 +184,7 @@ public sealed class PermisosEndpointTests : IAsyncLifetime // ── GET /api/v1/roles/{codigo}/permisos ────────────────────────────────── [Fact] - public async Task GetRolPermisos_AdminRol_Returns200With24Items() + public async Task GetRolPermisos_AdminRol_Returns200With25Items() { var token = await GetBearerTokenAsync(AdminUsername, AdminPassword); using var req = BuildRequest(HttpMethod.Get, "/api/v1/roles/admin/permisos", bearerToken: token); @@ -193,8 +194,9 @@ public sealed class PermisosEndpointTests : IAsyncLifetime var list = await resp.Content.ReadFromJsonAsync(); // V011 (ADM-001) adds 'administracion:secciones:gestionar' → 22 // V013 (ADM-008) adds 'administracion:puntos_de_venta:gestionar' → 23 - // V014 (ADM-009) adds 'administracion:fiscal:gestionar' → 24 total - Assert.Equal(24, list.GetArrayLength()); + // V014 (ADM-009) adds 'administracion:fiscal:gestionar' → 24 + // V016 (CAT-001) adds 'catalogo:rubros:gestionar' → 25 total + Assert.Equal(25, list.GetArrayLength()); } [Fact] diff --git a/tests/SIGCM2.Api.Tests/Rubros/RubrosControllerTests.cs b/tests/SIGCM2.Api.Tests/Rubros/RubrosControllerTests.cs index 9234b81..4cbed82 100644 --- a/tests/SIGCM2.Api.Tests/Rubros/RubrosControllerTests.cs +++ b/tests/SIGCM2.Api.Tests/Rubros/RubrosControllerTests.cs @@ -17,8 +17,7 @@ namespace SIGCM2.Api.Tests.Rubros; [Collection("ApiIntegration")] public sealed class RubrosControllerTests : IAsyncLifetime { - private const string TestConnectionString = - "Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;"; + private const string TestConnectionString = TestConnectionStrings.ApiTestDb; private const string ReadEndpoint = "/api/v1/rubros"; private const string AdminEndpoint = "/api/v1/admin/rubros"; diff --git a/tests/SIGCM2.Application.Tests/Integration/PermisoRepositoryTests.cs b/tests/SIGCM2.Application.Tests/Integration/PermisoRepositoryTests.cs index 07d5f00..a30e5a2 100644 --- a/tests/SIGCM2.Application.Tests/Integration/PermisoRepositoryTests.cs +++ b/tests/SIGCM2.Application.Tests/Integration/PermisoRepositoryTests.cs @@ -73,15 +73,16 @@ public class PermisoRepositoryTests : IAsyncLifetime // ── ListAsync ──────────────────────────────────────────────────────────── [Fact] - public async Task ListAsync_Returns23CanonicalSeeds() + public async Task ListAsync_Returns25CanonicalSeeds() { var list = await _repository.ListAsync(); // V005 seeds 18 canonical permisos + V007 (UDT-006) adds 3 admin permisos // + V011 (ADM-001) adds 'administracion:secciones:gestionar' // + V013 (ADM-008) adds 'administracion:puntos_de_venta:gestionar' - // + V014 (ADM-009) adds 'administracion:fiscal:gestionar' = 24 total - Assert.Equal(24, list.Count); + // + V014 (ADM-009) adds 'administracion:fiscal:gestionar' + // + V016 (CAT-001) adds 'catalogo:rubros:gestionar' = 25 total + Assert.Equal(25, list.Count); } [Fact] diff --git a/tests/SIGCM2.Application.Tests/Integration/RolPermisoRepositoryTests.cs b/tests/SIGCM2.Application.Tests/Integration/RolPermisoRepositoryTests.cs index 6afcbd8..a387abc 100644 --- a/tests/SIGCM2.Application.Tests/Integration/RolPermisoRepositoryTests.cs +++ b/tests/SIGCM2.Application.Tests/Integration/RolPermisoRepositoryTests.cs @@ -173,15 +173,16 @@ public class RolPermisoRepositoryTests : IAsyncLifetime // ── GetByRolCodigoAsync ────────────────────────────────────────────────── [Fact] - public async Task GetByRolCodigoAsync_Admin_Returns23Permisos() + public async Task GetByRolCodigoAsync_Admin_Returns25Permisos() { // admin has 18 permisos from V006 + 3 new admin permisos from V007 (UDT-006) // + 1 from V011 (ADM-001): 'administracion:secciones:gestionar' // + 1 from V013 (ADM-008): 'administracion:puntos_de_venta:gestionar' - // + 1 from V014 (ADM-009): 'administracion:fiscal:gestionar' = 24 total + // + 1 from V014 (ADM-009): 'administracion:fiscal:gestionar' + // + 1 from V016 (CAT-001): 'catalogo:rubros:gestionar' = 25 total var permisos = await _repository.GetByRolCodigoAsync("admin"); - Assert.Equal(24, permisos.Count); + Assert.Equal(25, permisos.Count); } [Fact] diff --git a/tests/SIGCM2.Application.Tests/Rubros/RubroRepositoryTests.cs b/tests/SIGCM2.Application.Tests/Rubros/RubroRepositoryTests.cs index 33902b3..02b61e0 100644 --- a/tests/SIGCM2.Application.Tests/Rubros/RubroRepositoryTests.cs +++ b/tests/SIGCM2.Application.Tests/Rubros/RubroRepositoryTests.cs @@ -1,73 +1,37 @@ using Dapper; -using Microsoft.Data.SqlClient; -using Respawn; using SIGCM2.Domain.Entities; using SIGCM2.Infrastructure.Persistence; +using SIGCM2.TestSupport; namespace SIGCM2.Application.Tests.Rubros; /// -/// Integration tests for RubroRepository against SIGCM2_Test. -/// TDD: RED written before implementation, GREEN after RubroRepository was created. +/// 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. /// [Collection("Database")] public class RubroRepositoryTests : IAsyncLifetime { - private const string ConnectionString = - "Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;"; - - private SqlConnection _connection = null!; - private Respawner _respawner = null!; + private readonly SqlTestFixture _db; private RubroRepository _repository = null!; private TimeProvider _timeProvider = null!; + public RubroRepositoryTests(SqlTestFixture db) + { + _db = db; + } + public async Task InitializeAsync() { - _connection = new SqlConnection(ConnectionString); - await _connection.OpenAsync(); + await _db.ResetAndSeedAsync(); - _respawner = await Respawner.CreateAsync(_connection, new RespawnerOptions - { - DbAdapter = DbAdapter.SqlServer, - TablesToIgnore = - [ - new Respawn.Graph.Table("dbo", "Rol"), - new Respawn.Graph.Table("dbo", "Permiso"), - new Respawn.Graph.Table("dbo", "RolPermiso"), - // *_History tables are system-versioned — engine rejects direct DELETE. - new Respawn.Graph.Table("dbo", "Usuario_History"), - new Respawn.Graph.Table("dbo", "Rol_History"), - new Respawn.Graph.Table("dbo", "Permiso_History"), - new Respawn.Graph.Table("dbo", "RolPermiso_History"), - // ADM-001 (V011): Medio + Seccion are temporal — history tables cannot be directly deleted. - new Respawn.Graph.Table("dbo", "Medio_History"), - new Respawn.Graph.Table("dbo", "Seccion_History"), - // ADM-008 (V013): PuntoDeVenta is temporal. - new Respawn.Graph.Table("dbo", "PuntoDeVenta_History"), - // ADM-009 (V014): TipoDeIva + IngresosBrutos son temporales. - new Respawn.Graph.Table("dbo", "TipoDeIva_History"), - new Respawn.Graph.Table("dbo", "IngresosBrutos_History"), - new Respawn.Graph.Table("dbo", "TipoDeIva"), - new Respawn.Graph.Table("dbo", "IngresosBrutos"), - // CAT-001 (V016): Rubro es temporal — history no puede deletearse directo. - new Respawn.Graph.Table("dbo", "Rubro_History"), - ] - }); - - await _respawner.ResetAsync(_connection); - await SeedRolCanonicalAsync(); - - var factory = new SqlConnectionFactory(ConnectionString); + var factory = new SqlConnectionFactory(TestConnectionStrings.AppTestDb); _repository = new RubroRepository(factory); _timeProvider = TimeProvider.System; } - public async Task DisposeAsync() - { - await _connection.CloseAsync(); - await _connection.DisposeAsync(); - } + public Task DisposeAsync() => Task.CompletedTask; // ── AddAsync + GetByIdAsync roundtrip ───────────────────────────────────── @@ -381,7 +345,7 @@ public class RubroRepositoryTests : IAsyncLifetime Assert.Equal("Actualizado", result!.Nombre); Assert.NotNull(result.FechaModificacion); - var historyCount = await _connection.ExecuteScalarAsync( + var historyCount = await _db.Connection.ExecuteScalarAsync( "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}"); @@ -423,28 +387,4 @@ public class RubroRepositoryTests : IAsyncLifetime Assert.NotNull(result.FechaModificacion); } - // ── helpers ─────────────────────────────────────────────────────────────── - - private async Task SeedRolCanonicalAsync() - { - const string sql = """ - SET QUOTED_IDENTIFIER ON; - MERGE dbo.Rol AS t - USING (VALUES - ('admin', N'Administrador', N'Supervisor total'), - ('cajero', N'Cajero', N'Mostrador contado'), - ('operador_ctacte', N'Operador Cta Cte', N'Cuenta corriente'), - ('picadora', N'Picadora/Correctora', N'Edición de textos'), - ('jefe_publicidad', N'Jefe de Publicidad', N'Supervisión de pauta'), - ('productor', N'Productor', N'Carga restringida'), - ('diagramacion', N'Diagramación/Taller', N'Solo lectura pauta'), - ('reportes', N'Reportes', N'Solo lectura reportes') - ) AS s (Codigo, Nombre, Descripcion) - ON t.Codigo = s.Codigo - WHEN NOT MATCHED BY TARGET THEN - INSERT (Codigo, Nombre, Descripcion, Activo) - VALUES (s.Codigo, s.Nombre, s.Descripcion, 1); - """; - await _connection.ExecuteAsync(sql); - } } diff --git a/tests/SIGCM2.TestSupport/SqlTestFixture.cs b/tests/SIGCM2.TestSupport/SqlTestFixture.cs index e87885e..c6bc106 100644 --- a/tests/SIGCM2.TestSupport/SqlTestFixture.cs +++ b/tests/SIGCM2.TestSupport/SqlTestFixture.cs @@ -57,6 +57,9 @@ public sealed class SqlTestFixture : IAsyncLifetime // V015 (UDT-011): ensure dbo.v_AuditEvent_Local + dbo.v_SecurityEvent_Local views exist. await EnsureV015SchemaAsync(); + // V016 (CAT-001): ensure dbo.Rubro + temporal + permiso 'catalogo:rubros:gestionar'. + await EnsureV016SchemaAsync(); + _respawner = await Respawner.CreateAsync(_connection, new RespawnerOptions { DbAdapter = DbAdapter.SqlServer, @@ -81,6 +84,8 @@ public sealed class SqlTestFixture : IAsyncLifetime // Seed de TipoDeIva e IngresosBrutos son datos de referencia — no limpiar con Respawn. new Respawn.Graph.Table("dbo", "TipoDeIva"), new Respawn.Graph.Table("dbo", "IngresosBrutos"), + // CAT-001 (V016): Rubro es temporal — history no puede deletearse directo. + new Respawn.Graph.Table("dbo", "Rubro_History"), ] }); @@ -201,7 +206,9 @@ public sealed class SqlTestFixture : IAsyncLifetime -- V013 (ADM-008): permiso para CRUD de Puntos de Venta ('administracion:puntos_de_venta:gestionar', N'Gestionar puntos de venta', N'Crear, editar y desactivar puntos de venta AFIP','administracion'), -- V014 (ADM-009): permiso para tablas fiscales - ('administracion:fiscal:gestionar', N'Gestionar tablas fiscales', N'Gestionar tablas fiscales (IVA, IIBB)', 'administracion') + ('administracion:fiscal:gestionar', N'Gestionar tablas fiscales', N'Gestionar tablas fiscales (IVA, IIBB)', 'administracion'), + -- V016 (CAT-001): permiso para gestionar árbol de rubros + ('catalogo:rubros:gestionar', N'Gestionar rubros del catálogo', N'Crear, editar, mover y desactivar rubros del árbol de catálogo comercial', 'catalogo') ) AS s (Codigo, Nombre, Descripcion, Modulo) ON t.Codigo = s.Codigo WHEN NOT MATCHED BY TARGET THEN @@ -247,6 +254,8 @@ public sealed class SqlTestFixture : IAsyncLifetime ('admin', 'administracion:puntos_de_venta:gestionar'), -- V014 (ADM-009) ('admin', 'administracion:fiscal:gestionar'), + -- V016 (CAT-001) + ('admin', 'catalogo:rubros:gestionar'), ('cajero', 'ventas:contado:crear'), ('cajero', 'ventas:contado:modificar'), ('cajero', 'ventas:contado:cobrar'), @@ -849,4 +858,79 @@ public sealed class SqlTestFixture : IAsyncLifetime await _connection.ExecuteAsync(createAuditEventLocal); await _connection.ExecuteAsync(createSecurityEventLocal); } + + /// + /// CAT-001 (V016): applies dbo.Rubro schema + temporal + filtered unique index + covering index + /// idempotentemente. Mirrors V016__create_rubro.sql. + /// Nota: COLLATE debe ir ANTES de NOT NULL — parser de SQL Server 2019 es estricto con ese orden. + /// Permiso 'catalogo:rubros:gestionar' y asignación a admin se siembran + /// desde SeedPermisosCanonicalAsync / SeedRolPermisosCanonicalAsync (post-respawn). + /// + private async Task EnsureV016SchemaAsync() + { + const string createRubro = """ + IF OBJECT_ID(N'dbo.Rubro', N'U') IS NULL + BEGIN + CREATE TABLE dbo.Rubro ( + Id INT IDENTITY(1,1) NOT NULL CONSTRAINT PK_Rubro PRIMARY KEY, + ParentId INT NULL, + Nombre NVARCHAR(200) COLLATE SQL_Latin1_General_CP1_CI_AI NOT NULL, + Orden INT NOT NULL CONSTRAINT DF_Rubro_Orden DEFAULT(0), + Activo BIT NOT NULL CONSTRAINT DF_Rubro_Activo DEFAULT(1), + TarifarioBaseId INT NULL, + FechaCreacion DATETIME2(3) NOT NULL CONSTRAINT DF_Rubro_FechaCreacion DEFAULT(SYSUTCDATETIME()), + FechaModificacion DATETIME2(3) NULL, + CONSTRAINT FK_Rubro_Parent FOREIGN KEY (ParentId) REFERENCES dbo.Rubro(Id) ON DELETE NO ACTION + ); + END + """; + + const string addRubroPeriod = """ + IF COL_LENGTH('dbo.Rubro', 'ValidFrom') IS NULL + BEGIN + ALTER TABLE dbo.Rubro + ADD + ValidFrom DATETIME2(3) GENERATED ALWAYS AS ROW START HIDDEN NOT NULL + CONSTRAINT DF_Rubro_ValidFrom DEFAULT(SYSUTCDATETIME()), + ValidTo DATETIME2(3) GENERATED ALWAYS AS ROW END HIDDEN NOT NULL + CONSTRAINT DF_Rubro_ValidTo DEFAULT(CONVERT(DATETIME2(3), '9999-12-31 23:59:59.999')), + PERIOD FOR SYSTEM_TIME (ValidFrom, ValidTo); + END + """; + + const string setRubroVersioning = """ + IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE object_id = OBJECT_ID('dbo.Rubro') AND temporal_type = 2) + BEGIN + ALTER TABLE dbo.Rubro + SET (SYSTEM_VERSIONING = ON ( + HISTORY_TABLE = dbo.Rubro_History, + HISTORY_RETENTION_PERIOD = 10 YEARS + )); + END + """; + + const string createUqIndex = """ + IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'UQ_Rubro_ParentId_Nombre_Activo' AND object_id = OBJECT_ID('dbo.Rubro')) + BEGIN + CREATE UNIQUE INDEX UQ_Rubro_ParentId_Nombre_Activo + ON dbo.Rubro(ParentId, Nombre) + WHERE Activo = 1 AND ParentId IS NOT NULL; + END + """; + + const string createCoveringIndex = """ + IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'IX_Rubro_ParentId_Activo' AND object_id = OBJECT_ID('dbo.Rubro')) + BEGIN + CREATE INDEX IX_Rubro_ParentId_Activo + ON dbo.Rubro(ParentId, Activo) + INCLUDE (Nombre, Orden); + END + """; + + await _connection.ExecuteAsync(createRubro); + await _connection.ExecuteAsync(addRubroPeriod); + await _connection.ExecuteAsync(setRubroVersioning); + await _connection.ExecuteAsync(createUqIndex); + await _connection.ExecuteAsync(createCoveringIndex); + } }