From d4a2b3bc3e20c14c0ee3bdccc871b7e7dbde688f Mon Sep 17 00:00:00 2001 From: dmolinari Date: Sat, 18 Apr 2026 21:44:12 -0300 Subject: [PATCH 1/7] =?UTF-8?q?feat(tests):=20a=C3=B1ade=20TestConnectionS?= =?UTF-8?q?trings=20y=20script=20de=20creaci=C3=B3n=20de=20DBs=20de=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce SIGCM2_Test_App y SIGCM2_Test_Api como bases aisladas para Application.Tests y Api.Tests respectivamente. TestConnectionStrings.cs centraliza las connection strings; create-test-api-db.sql documenta el setup idempotente de ambas bases con COLLATE Modern_Spanish_CI_AS. --- database/init/create-test-api-db.sql | 30 +++++++++++++++++++ .../TestConnectionStrings.cs | 20 +++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 database/init/create-test-api-db.sql create mode 100644 tests/SIGCM2.TestSupport/TestConnectionStrings.cs diff --git a/database/init/create-test-api-db.sql b/database/init/create-test-api-db.sql new file mode 100644 index 0000000..b672276 --- /dev/null +++ b/database/init/create-test-api-db.sql @@ -0,0 +1,30 @@ +-- create-test-api-db.sql +-- Creates test databases for integration tests (idempotent). +-- Run once per environment on TECNICA3 before executing integration tests. +-- +-- SIGCM2_Test_App -> used by SIGCM2.Application.Tests +-- SIGCM2_Test_Api -> used by SIGCM2.Api.Tests +-- SIGCM2_Test -> legacy (kept for old branches e.g. pre-merge CAT-001) +-- +-- After creating the DBs, apply V010 to both new DBs: +-- See database/README.md > "Test DBs" section for the PowerShell runbook. + +IF DB_ID(N'SIGCM2_Test_App') IS NULL +BEGIN + CREATE DATABASE [SIGCM2_Test_App] + COLLATE Modern_Spanish_CI_AS; + PRINT 'Database SIGCM2_Test_App created.'; +END +ELSE + PRINT 'Database SIGCM2_Test_App already exists -- skip.'; +GO + +IF DB_ID(N'SIGCM2_Test_Api') IS NULL +BEGIN + CREATE DATABASE [SIGCM2_Test_Api] + COLLATE Modern_Spanish_CI_AS; + PRINT 'Database SIGCM2_Test_Api created.'; +END +ELSE + PRINT 'Database SIGCM2_Test_Api already exists -- skip.'; +GO diff --git a/tests/SIGCM2.TestSupport/TestConnectionStrings.cs b/tests/SIGCM2.TestSupport/TestConnectionStrings.cs new file mode 100644 index 0000000..bf251ea --- /dev/null +++ b/tests/SIGCM2.TestSupport/TestConnectionStrings.cs @@ -0,0 +1,20 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("SIGCM2.Api.Tests")] + +namespace SIGCM2.TestSupport; + +/// +/// Centralized connection string constants for integration test databases. +/// Single source of truth — change server/credentials here only. +/// +public static class TestConnectionStrings +{ + /// Used by SIGCM2.Application.Tests via SqlTestFixture (parameterless ctor). + public const string AppTestDb = + "Server=TECNICA3;Database=SIGCM2_Test_App;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;"; + + /// Used by SIGCM2.Api.Tests via TestWebAppFactory. + public const string ApiTestDb = + "Server=TECNICA3;Database=SIGCM2_Test_Api;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;"; +} From e987228f14197eee74d9ea03b27868cf126645a2 Mon Sep 17 00:00:00 2001 From: dmolinari Date: Sat, 18 Apr 2026 21:44:19 -0300 Subject: [PATCH 2/7] refactor(tests): SqlTestFixture usa TestConnectionStrings; ctor interno para Api.Tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agrega ctor parameterless que apunta a SIGCM2_Test_App (requerido por xUnit ICollectionFixture). El ctor con string se marca internal y expone via InternalsVisibleTo a SIGCM2.Api.Tests. TestWebAppFactory apunta a SIGCM2_Test_Api. Se agrega propiedad Connection pública para que los tests que necesitan queries ad-hoc la usen. --- tests/SIGCM2.TestSupport/SqlTestFixture.cs | 16 +++++++++++++++- tests/SIGCM2.TestSupport/TestWebAppFactory.cs | 5 ++--- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/tests/SIGCM2.TestSupport/SqlTestFixture.cs b/tests/SIGCM2.TestSupport/SqlTestFixture.cs index 828bd42..e87885e 100644 --- a/tests/SIGCM2.TestSupport/SqlTestFixture.cs +++ b/tests/SIGCM2.TestSupport/SqlTestFixture.cs @@ -16,7 +16,14 @@ public sealed class SqlTestFixture : IAsyncLifetime private SqlConnection _connection = null!; private Respawner _respawner = null!; - public SqlTestFixture(string connectionString) + /// Parameterless ctor for xUnit ICollectionFixture — uses SIGCM2_Test_App. + public SqlTestFixture() : this(TestConnectionStrings.AppTestDb) { } + + /// + /// Explicit connection string ctor — used by TestWebAppFactory (same assembly). + /// Internal to satisfy xUnit's "single public constructor" rule for ICollectionFixture. + /// + internal SqlTestFixture(string connectionString) { _connectionString = connectionString; } @@ -80,6 +87,13 @@ public sealed class SqlTestFixture : IAsyncLifetime await ResetAndSeedAsync(); } + /// + /// Exposes the open SqlConnection for tests that need to run ad-hoc queries + /// (e.g. seed extra rows, assert history tables). Connection is opened during + /// InitializeAsync and closed in DisposeAsync. + /// + public SqlConnection Connection => _connection; + public async Task ResetAndSeedAsync() { await _respawner.ResetAsync(_connection); diff --git a/tests/SIGCM2.TestSupport/TestWebAppFactory.cs b/tests/SIGCM2.TestSupport/TestWebAppFactory.cs index 93cf2c0..a0e2317 100644 --- a/tests/SIGCM2.TestSupport/TestWebAppFactory.cs +++ b/tests/SIGCM2.TestSupport/TestWebAppFactory.cs @@ -13,12 +13,11 @@ namespace SIGCM2.TestSupport; /// /// WebApplicationFactory for integration tests against SIGCM2.Api. -/// Uses SIGCM2_Test database (separate from production SIGCM2). +/// Uses SIGCM2_Test_Api database (isolated from Application.Tests which uses SIGCM2_Test_App). /// public sealed class TestWebAppFactory : WebApplicationFactory, IAsyncLifetime { - private const string TestConnectionString = - "Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;"; + private const string TestConnectionString = TestConnectionStrings.ApiTestDb; // Resolved once — absolute paths independent of working directory private static readonly string RepoRoot = ResolveRepoRoot(); From 03a695feb9ef724281ac12e95eb294967ae6c322 Mon Sep 17 00:00:00 2001 From: dmolinari Date: Sat, 18 Apr 2026 21:44:24 -0300 Subject: [PATCH 3/7] refactor(tests): DatabaseCollection centraliza ICollectionFixture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Registra la colección "Database" con SqlTestFixture como fixture compartido para Application.Tests (elimina el ctor-con-string inline en cada test class). Agrega Using global a ambos proyectos para evitar usings por archivo. --- tests/SIGCM2.Api.Tests/SIGCM2.Api.Tests.csproj | 1 + .../DatabaseCollection.cs | 15 +++++++++++++++ .../SIGCM2.Application.Tests.csproj | 2 ++ 3 files changed, 18 insertions(+) create mode 100644 tests/SIGCM2.Application.Tests/DatabaseCollection.cs diff --git a/tests/SIGCM2.Api.Tests/SIGCM2.Api.Tests.csproj b/tests/SIGCM2.Api.Tests/SIGCM2.Api.Tests.csproj index b7f3581..872163a 100644 --- a/tests/SIGCM2.Api.Tests/SIGCM2.Api.Tests.csproj +++ b/tests/SIGCM2.Api.Tests/SIGCM2.Api.Tests.csproj @@ -27,6 +27,7 @@ + diff --git a/tests/SIGCM2.Application.Tests/DatabaseCollection.cs b/tests/SIGCM2.Application.Tests/DatabaseCollection.cs new file mode 100644 index 0000000..f3f6086 --- /dev/null +++ b/tests/SIGCM2.Application.Tests/DatabaseCollection.cs @@ -0,0 +1,15 @@ +using SIGCM2.TestSupport; +using Xunit; + +namespace SIGCM2.Application.Tests; + +/// +/// Declares the "Database" xUnit collection backed by a single shared SqlTestFixture. +/// All test classes decorated with [Collection("Database")] share one fixture instance +/// per test run — eliminating concurrent Respawner collisions. +/// +[CollectionDefinition("Database")] +public sealed class DatabaseCollection : ICollectionFixture +{ + // Intentionally empty: this class only exists to declare the collection/fixture binding. +} diff --git a/tests/SIGCM2.Application.Tests/SIGCM2.Application.Tests.csproj b/tests/SIGCM2.Application.Tests/SIGCM2.Application.Tests.csproj index d94bcf5..8c51a38 100644 --- a/tests/SIGCM2.Application.Tests/SIGCM2.Application.Tests.csproj +++ b/tests/SIGCM2.Application.Tests/SIGCM2.Application.Tests.csproj @@ -24,10 +24,12 @@ + + From e0b9cba94804836476347764c449f089910e58b7 Mon Sep 17 00:00:00 2001 From: dmolinari Date: Sat, 18 Apr 2026 21:44:36 -0300 Subject: [PATCH 4/7] refactor(tests): Application.Tests elimina Respawner inline; usa SqlTestFixture compartido MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 6 clases que instanciaban Respawner directamente migran a recibir SqlTestFixture vía ICollectionFixture. 8 clases restantes solo actualizan ConnectionString a TestConnectionStrings.AppTestDb. Cada clase ahora es responsable únicamente de sus seeds específicos; la limpieza de la base queda centralizada en el fixture. --- .../Audit/AuditEventRepositoryTests.cs | 3 +- .../Infrastructure/Audit/AuditJobsTests.cs | 3 +- .../Audit/SecurityEventRepositoryTests.cs | 3 +- .../IngresosBrutosRepositoryTests.cs | 3 +- .../RefreshTokenRepositoryTests.cs | 87 +++------------ .../TipoDeIvaRepositoryTests.cs | 3 +- .../Integration/PermisoRepositoryTests.cs | 3 +- .../Integration/RolPermisoRepositoryTests.cs | 3 +- .../Integration/RolRepositoryTests.cs | 3 +- .../Integration/UsuarioRepositoryTests.cs | 105 ++---------------- .../UsuarioRepository_PermisosTests.cs | 86 +++----------- .../Integration/V009MigrationTests.cs | 94 ++++------------ .../Medios/MedioRepositoryTests.cs | 83 ++------------ .../Secciones/SeccionRepositoryTests.cs | 82 ++------------ 14 files changed, 95 insertions(+), 466 deletions(-) diff --git a/tests/SIGCM2.Application.Tests/Infrastructure/Audit/AuditEventRepositoryTests.cs b/tests/SIGCM2.Application.Tests/Infrastructure/Audit/AuditEventRepositoryTests.cs index cf39f63..4420348 100644 --- a/tests/SIGCM2.Application.Tests/Infrastructure/Audit/AuditEventRepositoryTests.cs +++ b/tests/SIGCM2.Application.Tests/Infrastructure/Audit/AuditEventRepositoryTests.cs @@ -13,8 +13,7 @@ namespace SIGCM2.Application.Tests.Infrastructure.Audit; [Collection("Database")] public sealed class AuditEventRepositoryTests : IAsyncLifetime { - private const string ConnectionString = - "Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;"; + private const string ConnectionString = TestConnectionStrings.AppTestDb; private SqlConnection _connection = null!; private AuditEventRepository _repo = null!; diff --git a/tests/SIGCM2.Application.Tests/Infrastructure/Audit/AuditJobsTests.cs b/tests/SIGCM2.Application.Tests/Infrastructure/Audit/AuditJobsTests.cs index 270716c..75cfec0 100644 --- a/tests/SIGCM2.Application.Tests/Infrastructure/Audit/AuditJobsTests.cs +++ b/tests/SIGCM2.Application.Tests/Infrastructure/Audit/AuditJobsTests.cs @@ -16,8 +16,7 @@ namespace SIGCM2.Application.Tests.Infrastructure.Audit; [Collection("Database")] public sealed class AuditJobsTests : IAsyncLifetime { - private const string ConnectionString = - "Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;"; + private const string ConnectionString = TestConnectionStrings.AppTestDb; private SqlConnection _connection = null!; private SqlConnectionFactory _factory = null!; diff --git a/tests/SIGCM2.Application.Tests/Infrastructure/Audit/SecurityEventRepositoryTests.cs b/tests/SIGCM2.Application.Tests/Infrastructure/Audit/SecurityEventRepositoryTests.cs index 277183a..26a9e76 100644 --- a/tests/SIGCM2.Application.Tests/Infrastructure/Audit/SecurityEventRepositoryTests.cs +++ b/tests/SIGCM2.Application.Tests/Infrastructure/Audit/SecurityEventRepositoryTests.cs @@ -11,8 +11,7 @@ namespace SIGCM2.Application.Tests.Infrastructure.Audit; [Collection("Database")] public sealed class SecurityEventRepositoryTests : IAsyncLifetime { - private const string ConnectionString = - "Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;"; + private const string ConnectionString = TestConnectionStrings.AppTestDb; private SqlConnection _connection = null!; private SecurityEventRepository _repo = null!; diff --git a/tests/SIGCM2.Application.Tests/Infrastructure/IngresosBrutosRepositoryTests.cs b/tests/SIGCM2.Application.Tests/Infrastructure/IngresosBrutosRepositoryTests.cs index 642434d..a47319b 100644 --- a/tests/SIGCM2.Application.Tests/Infrastructure/IngresosBrutosRepositoryTests.cs +++ b/tests/SIGCM2.Application.Tests/Infrastructure/IngresosBrutosRepositoryTests.cs @@ -18,8 +18,7 @@ namespace SIGCM2.Application.Tests.Infrastructure; [Collection("Database")] public class IngresosBrutosRepositoryTests : IAsyncLifetime { - private const string ConnectionString = - "Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;"; + private const string ConnectionString = TestConnectionStrings.AppTestDb; private SqlConnection _connection = null!; private IIngresosBrutosRepository _repo = null!; diff --git a/tests/SIGCM2.Application.Tests/Infrastructure/RefreshTokenRepositoryTests.cs b/tests/SIGCM2.Application.Tests/Infrastructure/RefreshTokenRepositoryTests.cs index ad6b299..230ff72 100644 --- a/tests/SIGCM2.Application.Tests/Infrastructure/RefreshTokenRepositoryTests.cs +++ b/tests/SIGCM2.Application.Tests/Infrastructure/RefreshTokenRepositoryTests.cs @@ -1,103 +1,44 @@ using Dapper; -using Microsoft.Data.SqlClient; -using Respawn; using SIGCM2.Domain.Entities; using SIGCM2.Infrastructure.Persistence; +using SIGCM2.TestSupport; namespace SIGCM2.Application.Tests.Infrastructure; /// -/// Integration tests for RefreshTokenRepository against SIGCM2_Test. -/// Uses Respawn to reset the DB between test classes; the repository opens its own +/// Integration tests for RefreshTokenRepository against SIGCM2_Test_App. +/// Uses shared SqlTestFixture via xUnit collection fixture; the repository opens its own /// connections so transaction-scoped isolation would block on FK locks. /// [Collection("Database")] public class RefreshTokenRepositoryTests : 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 RefreshTokenRepository _repository = null!; private int _testUserId; + public RefreshTokenRepositoryTests(SqlTestFixture db) + { + _db = db; + } + public async Task InitializeAsync() { - _connection = new SqlConnection(ConnectionString); - await _connection.OpenAsync(); - - _respawner = await Respawner.CreateAsync(_connection, new RespawnerOptions - { - DbAdapter = DbAdapter.SqlServer, - // Rol is a lookup table seeded by migration V003 — never wipe or Usuario FK breaks. - TablesToIgnore = - [ - new Respawn.Graph.Table("dbo", "Rol"), - new Respawn.Graph.Table("dbo", "Permiso"), - new Respawn.Graph.Table("dbo", "RolPermiso"), - // UDT-010: *_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; SecuenciaComprobante is NOT temporal (AD8 revisitado). - 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"), - ] - }); - - await _respawner.ResetAsync(_connection); - await SeedRolCanonicalAsync(); + await _db.ResetAndSeedAsync(); await SeedTestUserAsync(); - _testUserId = await _connection.QuerySingleAsync( + _testUserId = await _db.Connection.QuerySingleAsync( "SELECT Id FROM dbo.Usuario WHERE Username = 'test_rt_user'"); - var factory = new SqlConnectionFactory(ConnectionString); + var factory = new SqlConnectionFactory(TestConnectionStrings.AppTestDb); _repository = new RefreshTokenRepository(factory); } - public async Task DisposeAsync() - { - await _respawner.ResetAsync(_connection); - await _connection.CloseAsync(); - await _connection.DisposeAsync(); - } - - 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); - } + public Task DisposeAsync() => Task.CompletedTask; private async Task SeedTestUserAsync() { - await _connection.ExecuteAsync(""" + await _db.Connection.ExecuteAsync(""" SET QUOTED_IDENTIFIER ON; IF NOT EXISTS (SELECT 1 FROM dbo.Usuario WHERE Username = 'test_rt_user') INSERT INTO dbo.Usuario (Username, PasswordHash, Nombre, Apellido, Rol, PermisosJson, Activo) diff --git a/tests/SIGCM2.Application.Tests/Infrastructure/TipoDeIvaRepositoryTests.cs b/tests/SIGCM2.Application.Tests/Infrastructure/TipoDeIvaRepositoryTests.cs index 75bc2f3..690473f 100644 --- a/tests/SIGCM2.Application.Tests/Infrastructure/TipoDeIvaRepositoryTests.cs +++ b/tests/SIGCM2.Application.Tests/Infrastructure/TipoDeIvaRepositoryTests.cs @@ -19,8 +19,7 @@ namespace SIGCM2.Application.Tests.Infrastructure; [Collection("Database")] public class TipoDeIvaRepositoryTests : IAsyncLifetime { - private const string ConnectionString = - "Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;"; + private const string ConnectionString = TestConnectionStrings.AppTestDb; private SqlConnection _connection = null!; private ITipoDeIvaRepository _repo = null!; diff --git a/tests/SIGCM2.Application.Tests/Integration/PermisoRepositoryTests.cs b/tests/SIGCM2.Application.Tests/Integration/PermisoRepositoryTests.cs index 9d64583..07d5f00 100644 --- a/tests/SIGCM2.Application.Tests/Integration/PermisoRepositoryTests.cs +++ b/tests/SIGCM2.Application.Tests/Integration/PermisoRepositoryTests.cs @@ -11,8 +11,7 @@ namespace SIGCM2.Application.Tests.Integration; [Collection("Database")] public class PermisoRepositoryTests : IAsyncLifetime { - private const string ConnectionString = - "Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;"; + private const string ConnectionString = TestConnectionStrings.AppTestDb; private SqlConnection _connection = null!; private PermisoRepository _repository = null!; diff --git a/tests/SIGCM2.Application.Tests/Integration/RolPermisoRepositoryTests.cs b/tests/SIGCM2.Application.Tests/Integration/RolPermisoRepositoryTests.cs index 0a90f50..6afcbd8 100644 --- a/tests/SIGCM2.Application.Tests/Integration/RolPermisoRepositoryTests.cs +++ b/tests/SIGCM2.Application.Tests/Integration/RolPermisoRepositoryTests.cs @@ -11,8 +11,7 @@ namespace SIGCM2.Application.Tests.Integration; [Collection("Database")] public class RolPermisoRepositoryTests : IAsyncLifetime { - private const string ConnectionString = - "Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;"; + private const string ConnectionString = TestConnectionStrings.AppTestDb; private SqlConnection _connection = null!; private RolPermisoRepository _repository = null!; diff --git a/tests/SIGCM2.Application.Tests/Integration/RolRepositoryTests.cs b/tests/SIGCM2.Application.Tests/Integration/RolRepositoryTests.cs index 17ec627..50c8545 100644 --- a/tests/SIGCM2.Application.Tests/Integration/RolRepositoryTests.cs +++ b/tests/SIGCM2.Application.Tests/Integration/RolRepositoryTests.cs @@ -8,8 +8,7 @@ namespace SIGCM2.Application.Tests.Integration; [Collection("Database")] public class RolRepositoryTests : IAsyncLifetime { - private const string ConnectionString = - "Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;"; + private const string ConnectionString = TestConnectionStrings.AppTestDb; private SqlConnection _connection = null!; private RolRepository _repository = null!; diff --git a/tests/SIGCM2.Application.Tests/Integration/UsuarioRepositoryTests.cs b/tests/SIGCM2.Application.Tests/Integration/UsuarioRepositoryTests.cs index bb35e14..1783989 100644 --- a/tests/SIGCM2.Application.Tests/Integration/UsuarioRepositoryTests.cs +++ b/tests/SIGCM2.Application.Tests/Integration/UsuarioRepositoryTests.cs @@ -1,66 +1,29 @@ -using Microsoft.Data.SqlClient; -using Respawn; +using Dapper; using SIGCM2.Infrastructure.Persistence; +using SIGCM2.TestSupport; namespace SIGCM2.Application.Tests.Integration; [Collection("Database")] public class UsuarioRepositoryTests : 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 UsuarioRepository _repository = null!; + public UsuarioRepositoryTests(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, - // Rol is a lookup table seeded by migration V003 — never wipe or Usuario FK breaks. - TablesToIgnore = - [ - new Respawn.Graph.Table("dbo", "Rol"), - new Respawn.Graph.Table("dbo", "Permiso"), - new Respawn.Graph.Table("dbo", "RolPermiso"), - // UDT-010: *_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; SecuenciaComprobante is NOT temporal (AD8 revisitado). - 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"), - ] - }); - - // Reset DB, re-seed Rol canonical table (lookup) and admin user for each test class run. - await _respawner.ResetAsync(_connection); - await SeedRolCanonicalAsync(); - await SeedAdminAsync(); - - var factory = new SqlConnectionFactory(ConnectionString); + var factory = new SqlConnectionFactory(TestConnectionStrings.AppTestDb); _repository = new UsuarioRepository(factory); } - public async Task DisposeAsync() - { - await _respawner.ResetAsync(_connection); - await _connection.CloseAsync(); - await _connection.DisposeAsync(); - } + public Task DisposeAsync() => Task.CompletedTask; // Scenario: GetByUsername returns correct entity when user exists [Fact] @@ -88,7 +51,7 @@ public class UsuarioRepositoryTests : IAsyncLifetime public async Task GetByUsernameAsync_DifferentUser_ReturnsCorrectUser_Cajero() { // Insert a second user with canonical rol 'cajero' (post-UDT-004 FK requires Rol.Codigo to exist). - await _connection.ExecuteAsync( + await _db.Connection.ExecuteAsync( "INSERT INTO dbo.Usuario (Username, PasswordHash, Nombre, Apellido, Rol, PermisosJson) " + "VALUES ('cajero1', '$2a$12$hash2', 'Juan', 'Pérez', 'cajero', '[]')"); @@ -101,48 +64,4 @@ public class UsuarioRepositoryTests : IAsyncLifetime Assert.Equal("admin", admin.Rol); Assert.Equal("cajero", cajero.Rol); } - - 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); - } - - private async Task SeedAdminAsync() - { - await _connection.ExecuteAsync( - "SET QUOTED_IDENTIFIER ON; " + - "IF NOT EXISTS (SELECT 1 FROM dbo.Usuario WHERE Username = 'admin') " + - "INSERT INTO dbo.Usuario (Username, PasswordHash, Nombre, Apellido, Rol, PermisosJson, Activo) " + - "VALUES ('admin', '$2a$12$rmq6tlSAQ8WXhR2CwLCSeuwCJKz/.8Eab95UQCUNfwe4dokeOqMcW', " + - "'Administrador', 'Sistema', 'admin', '[\"*\"]', 1)"); - } -} - -// Dapper extension helper for IDbConnection -file static class DapperHelper -{ - public static async Task ExecuteAsync(this SqlConnection conn, string sql) - { - using var cmd = conn.CreateCommand(); - cmd.CommandText = sql; - await cmd.ExecuteNonQueryAsync(); - } } diff --git a/tests/SIGCM2.Application.Tests/Integration/UsuarioRepository_PermisosTests.cs b/tests/SIGCM2.Application.Tests/Integration/UsuarioRepository_PermisosTests.cs index ed0186d..bf5aa56 100644 --- a/tests/SIGCM2.Application.Tests/Integration/UsuarioRepository_PermisosTests.cs +++ b/tests/SIGCM2.Application.Tests/Integration/UsuarioRepository_PermisosTests.cs @@ -1,84 +1,46 @@ using Dapper; -using Microsoft.Data.SqlClient; -using Respawn; using SIGCM2.Infrastructure.Persistence; +using SIGCM2.TestSupport; namespace SIGCM2.Application.Tests.Integration; /// /// Integration tests for IUsuarioRepository.UpdatePermisosJsonAsync (UDT-009). -/// Uses SIGCM2_Test database directly. +/// Uses SIGCM2_Test_App database via shared SqlTestFixture. /// [Collection("Database")] public sealed class UsuarioRepository_PermisosTests : 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 UsuarioRepository _repository = null!; + public UsuarioRepository_PermisosTests(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"), - // UDT-010: *_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; SecuenciaComprobante is NOT temporal (AD8 revisitado). - 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"), - ] - }); - - await _respawner.ResetAsync(_connection); - await SeedRolCanonicalAsync(); - - var factory = new SqlConnectionFactory(ConnectionString); + var factory = new SqlConnectionFactory(TestConnectionStrings.AppTestDb); _repository = new UsuarioRepository(factory); // Seed a test user - await _connection.ExecuteAsync(""" + await _db.Connection.ExecuteAsync(""" INSERT INTO dbo.Usuario (Username, PasswordHash, Nombre, Apellido, Rol, PermisosJson, Activo, MustChangePassword) VALUES ('testuser', '$2a$12$hash', 'Test', 'User', 'cajero', '{"grant":[],"deny":[]}', 1, 0) """); } - public async Task DisposeAsync() - { - if (_connection is not null) - { - await _respawner.ResetAsync(_connection); - await _connection.CloseAsync(); - await _connection.DisposeAsync(); - } - } + public Task DisposeAsync() => Task.CompletedTask; // UPJ-01: UpdatePermisosJsonAsync persists PermisosJson and FechaModificacion [Fact] public async Task UpdatePermisosJsonAsync_PersistsJsonAndFechaModificacion() { // Arrange - var userId = await _connection.QuerySingleAsync( + var userId = await _db.Connection.QuerySingleAsync( "SELECT Id FROM dbo.Usuario WHERE Username = 'testuser'"); var newJson = """{"grant":["textos:editar"],"deny":[]}"""; var fechaMod = DateTime.UtcNow; @@ -87,7 +49,7 @@ public sealed class UsuarioRepository_PermisosTests : IAsyncLifetime await _repository.UpdatePermisosJsonAsync(userId, newJson, fechaMod); // Assert - var row = await _connection.QuerySingleAsync<(string PermisosJson, DateTime? FechaModificacion)>( + var row = await _db.Connection.QuerySingleAsync<(string PermisosJson, DateTime? FechaModificacion)>( "SELECT PermisosJson, FechaModificacion FROM dbo.Usuario WHERE Id = @Id", new { Id = userId }); @@ -112,7 +74,7 @@ public sealed class UsuarioRepository_PermisosTests : IAsyncLifetime public async Task UpdatePermisosJsonAsync_GetByIdReflectsChange() { // Arrange - var userId = await _connection.QuerySingleAsync( + var userId = await _db.Connection.QuerySingleAsync( "SELECT Id FROM dbo.Usuario WHERE Username = 'testuser'"); var newJson = """{"grant":["pauta:azanu:ver"],"deny":["ventas:contado:cobrar"]}"""; @@ -125,22 +87,4 @@ public sealed class UsuarioRepository_PermisosTests : IAsyncLifetime Assert.NotNull(usuario); Assert.Equal(newJson, usuario!.PermisosJson); } - - // ── helpers ─────────────────────────────────────────────────────────────── - - private async Task SeedRolCanonicalAsync() - { - await _connection.ExecuteAsync(""" - SET QUOTED_IDENTIFIER ON; - MERGE dbo.Rol AS t - USING (VALUES - ('admin', N'Administrador', N'Supervisor total'), - ('cajero', N'Cajero', N'Mostrador contado') - ) 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); - """); - } } diff --git a/tests/SIGCM2.Application.Tests/Integration/V009MigrationTests.cs b/tests/SIGCM2.Application.Tests/Integration/V009MigrationTests.cs index 530e4e2..8b5878d 100644 --- a/tests/SIGCM2.Application.Tests/Integration/V009MigrationTests.cs +++ b/tests/SIGCM2.Application.Tests/Integration/V009MigrationTests.cs @@ -1,66 +1,29 @@ using Dapper; -using Microsoft.Data.SqlClient; -using Respawn; +using SIGCM2.TestSupport; namespace SIGCM2.Application.Tests.Integration; /// /// SUITE-B-MIGRATION-V009 — M-01 a M-07 (UDT-009) /// Validates the V009 migration SQL and SqlTestFixture.EnsureV009SchemaAsync. -/// Uses SIGCM2_Test database directly. +/// Uses SIGCM2_Test_App database via shared SqlTestFixture. /// [Collection("Database")] public sealed class V009MigrationTests : IAsyncLifetime { - private const string ConnectionString = - "Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;"; + private readonly SqlTestFixture _db; - private SqlConnection _connection = null!; - private Respawner _respawner = null!; + public V009MigrationTests(SqlTestFixture db) + { + _db = db; + } public async Task InitializeAsync() { - _connection = new SqlConnection(ConnectionString); - await _connection.OpenAsync(); - - _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"), - // UDT-010: *_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; SecuenciaComprobante is NOT temporal (AD8 revisitado). - 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"), - ] - }); - - await _respawner.ResetAsync(_connection); - await SeedRolAsync(); + await _db.ResetAndSeedAsync(); } - public async Task DisposeAsync() - { - if (_connection is not null) - { - await _connection.CloseAsync(); - await _connection.DisposeAsync(); - } - } + public Task DisposeAsync() => Task.CompletedTask; // M-01: migration file exists on filesystem [Fact] @@ -110,7 +73,7 @@ public sealed class V009MigrationTests : IAsyncLifetime AND name = 'PermisosJson' """; - var definition = await _connection.QuerySingleOrDefaultAsync(sql); + var definition = await _db.Connection.QuerySingleOrDefaultAsync(sql); Assert.NotNull(definition); Assert.Contains(@"{""grant"":[]", definition); @@ -123,7 +86,7 @@ public sealed class V009MigrationTests : IAsyncLifetime { await EnsureV009SchemaAsync(); - await _connection.ExecuteAsync(""" + await _db.Connection.ExecuteAsync(""" INSERT INTO dbo.Usuario (Username, PasswordHash, Nombre, Apellido, Rol, PermisosJson, Activo, MustChangePassword) VALUES ('legacyempty', '$2a$12$hash', 'L', 'E', 'admin', '[]', 1, 0) """); @@ -131,7 +94,7 @@ public sealed class V009MigrationTests : IAsyncLifetime // Run migration again to migrate the newly inserted row await EnsureV009SchemaAsync(); - var permisosJson = await _connection.QuerySingleAsync( + var permisosJson = await _db.Connection.QuerySingleAsync( "SELECT PermisosJson FROM dbo.Usuario WHERE Username = 'legacyempty'"); Assert.Equal("""{"grant":[],"deny":[]}""", permisosJson); @@ -143,14 +106,14 @@ public sealed class V009MigrationTests : IAsyncLifetime { await EnsureV009SchemaAsync(); - await _connection.ExecuteAsync(""" + await _db.Connection.ExecuteAsync(""" INSERT INTO dbo.Usuario (Username, PasswordHash, Nombre, Apellido, Rol, PermisosJson, Activo, MustChangePassword) VALUES ('legacywild', '$2a$12$hash', 'L', 'W', 'admin', '["*"]', 1, 0) """); await EnsureV009SchemaAsync(); - var permisosJson = await _connection.QuerySingleAsync( + var permisosJson = await _db.Connection.QuerySingleAsync( "SELECT PermisosJson FROM dbo.Usuario WHERE Username = 'legacywild'"); Assert.Equal("""{"grant":[],"deny":[]}""", permisosJson); @@ -166,7 +129,7 @@ public sealed class V009MigrationTests : IAsyncLifetime await EnsureV009SchemaAsync(); // Temporarily drop and re-add without the DEFAULT so we can insert '' - await _connection.ExecuteAsync(""" + await _db.Connection.ExecuteAsync(""" IF EXISTS ( SELECT 1 FROM sys.default_constraints WHERE name = 'DF_Usuario_Permisos' @@ -175,7 +138,7 @@ public sealed class V009MigrationTests : IAsyncLifetime ALTER TABLE dbo.Usuario DROP CONSTRAINT DF_Usuario_Permisos; """); - await _connection.ExecuteAsync(""" + await _db.Connection.ExecuteAsync(""" INSERT INTO dbo.Usuario (Username, PasswordHash, Nombre, Apellido, Rol, PermisosJson, Activo, MustChangePassword) VALUES ('emptystruser', '$2a$12$hash', 'E', 'S', 'admin', '', 1, 0) """); @@ -183,7 +146,7 @@ public sealed class V009MigrationTests : IAsyncLifetime // Re-apply V009 (which restores constraint and migrates '' rows) await EnsureV009SchemaAsync(); - var permisosJson = await _connection.QuerySingleAsync( + var permisosJson = await _db.Connection.QuerySingleAsync( "SELECT PermisosJson FROM dbo.Usuario WHERE Username = 'emptystruser'"); Assert.Equal("""{"grant":[],"deny":[]}""", permisosJson); @@ -196,7 +159,7 @@ public sealed class V009MigrationTests : IAsyncLifetime await EnsureV009SchemaAsync(); // Seed admin as TestFixture does post-V009 - await _connection.ExecuteAsync(""" + await _db.Connection.ExecuteAsync(""" IF NOT EXISTS (SELECT 1 FROM dbo.Usuario WHERE Username = 'admin') INSERT INTO dbo.Usuario (Username, PasswordHash, Nombre, Apellido, Rol, PermisosJson, Activo, MustChangePassword) VALUES ( @@ -206,7 +169,7 @@ public sealed class V009MigrationTests : IAsyncLifetime ) """); - var permisosJson = await _connection.QuerySingleAsync( + var permisosJson = await _db.Connection.QuerySingleAsync( "SELECT PermisosJson FROM dbo.Usuario WHERE Username = 'admin'"); Assert.Equal("""{"grant":[],"deny":[]}""", permisosJson); @@ -214,19 +177,6 @@ public sealed class V009MigrationTests : IAsyncLifetime // ── helpers ─────────────────────────────────────────────────────────────── - private async Task SeedRolAsync() - { - await _connection.ExecuteAsync(""" - SET QUOTED_IDENTIFIER ON; - MERGE dbo.Rol AS t - USING (VALUES ('admin', N'Administrador', N'Supervisor total')) - 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); - """); - } - /// /// Replicates V009 migration idempotently — mirrors SqlTestFixture.EnsureV009SchemaAsync. /// @@ -264,8 +214,8 @@ public sealed class V009MigrationTests : IAsyncLifetime OR LTRIM(RTRIM(PermisosJson)) = '' """; - await _connection.ExecuteAsync(dropConstraint); - await _connection.ExecuteAsync(addConstraint); - await _connection.ExecuteAsync(migrateRows); + await _db.Connection.ExecuteAsync(dropConstraint); + await _db.Connection.ExecuteAsync(addConstraint); + await _db.Connection.ExecuteAsync(migrateRows); } } diff --git a/tests/SIGCM2.Application.Tests/Medios/MedioRepositoryTests.cs b/tests/SIGCM2.Application.Tests/Medios/MedioRepositoryTests.cs index 80c9fdf..9871e91 100644 --- a/tests/SIGCM2.Application.Tests/Medios/MedioRepositoryTests.cs +++ b/tests/SIGCM2.Application.Tests/Medios/MedioRepositoryTests.cs @@ -1,69 +1,35 @@ using Dapper; -using Microsoft.Data.SqlClient; -using Respawn; using SIGCM2.Domain.Entities; using SIGCM2.Infrastructure.Persistence; +using SIGCM2.TestSupport; namespace SIGCM2.Application.Tests.Medios; /// -/// Integration tests for MedioRepository against SIGCM2_Test. +/// Integration tests for MedioRepository against SIGCM2_Test_App. /// TDD: RED written before implementation, GREEN after MedioRepository was created. /// Temporal: after UpdateAsync, dbo.Medio_History MUST have ≥1 row for that Id. /// [Collection("Database")] public class MedioRepositoryTests : 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 MedioRepository _repository = null!; + public MedioRepositoryTests(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; SecuenciaComprobante is NOT temporal (AD8 revisitado). - 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"), - ] - }); - - await _respawner.ResetAsync(_connection); - await SeedRolCanonicalAsync(); - - var factory = new SqlConnectionFactory(ConnectionString); + var factory = new SqlConnectionFactory(TestConnectionStrings.AppTestDb); _repository = new MedioRepository(factory); } - public async Task DisposeAsync() - { - await _connection.CloseAsync(); - await _connection.DisposeAsync(); - } + public Task DisposeAsync() => Task.CompletedTask; // ── AddAsync + GetByIdAsync roundtrip ───────────────────────────────────── @@ -170,7 +136,7 @@ public class MedioRepositoryTests : IAsyncLifetime var updated = original!.WithUpdatedProfile("Historial v2", TipoMedio.Web, null, DateTime.UtcNow); await _repository.UpdateAsync(updated); - var historyCount = await _connection.ExecuteScalarAsync( + var historyCount = await _db.Connection.ExecuteScalarAsync( "SELECT COUNT(*) FROM dbo.Medio_History WHERE Id = @Id", new { Id = id }); Assert.True(historyCount >= 1, $"Expected ≥1 history row for Medio Id={id}, got {historyCount}"); @@ -241,29 +207,4 @@ public class MedioRepositoryTests : IAsyncLifetime Assert.Equal(3, result.Total); Assert.Equal(2, result.Items.Count); } - - // ── 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.Application.Tests/Secciones/SeccionRepositoryTests.cs b/tests/SIGCM2.Application.Tests/Secciones/SeccionRepositoryTests.cs index 541fb13..e0bde00 100644 --- a/tests/SIGCM2.Application.Tests/Secciones/SeccionRepositoryTests.cs +++ b/tests/SIGCM2.Application.Tests/Secciones/SeccionRepositoryTests.cs @@ -1,62 +1,33 @@ using Dapper; -using Microsoft.Data.SqlClient; -using Respawn; using SIGCM2.Domain.Entities; using SIGCM2.Infrastructure.Persistence; +using SIGCM2.TestSupport; namespace SIGCM2.Application.Tests.Secciones; /// -/// Integration tests for SeccionRepository against SIGCM2_Test. +/// Integration tests for SeccionRepository against SIGCM2_Test_App. /// TDD: RED written before implementation, GREEN after SeccionRepository was created. /// Temporal: after UpdateAsync, dbo.Seccion_History MUST have ≥1 row for that Id. /// [Collection("Database")] public class SeccionRepositoryTests : 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 SeccionRepository _repository = null!; private MedioRepository _medioRepository = null!; private int _medioId; + public SeccionRepositoryTests(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"), - 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"), - ] - }); - - await _respawner.ResetAsync(_connection); - await SeedRolCanonicalAsync(); - - var factory = new SqlConnectionFactory(ConnectionString); + var factory = new SqlConnectionFactory(TestConnectionStrings.AppTestDb); _repository = new SeccionRepository(factory); _medioRepository = new MedioRepository(factory); @@ -64,11 +35,7 @@ public class SeccionRepositoryTests : IAsyncLifetime _medioId = await _medioRepository.AddAsync(Medio.ForCreation("TESTMEDIO", "Medio de Prueba", TipoMedio.Diario, null)); } - public async Task DisposeAsync() - { - await _connection.CloseAsync(); - await _connection.DisposeAsync(); - } + public Task DisposeAsync() => Task.CompletedTask; // ── AddAsync + GetByIdAsync roundtrip ───────────────────────────────────── @@ -171,7 +138,7 @@ public class SeccionRepositoryTests : IAsyncLifetime var updated = original!.WithUpdatedProfile("Historial v2", "suplementos", DateTime.UtcNow); await _repository.UpdateAsync(updated); - var historyCount = await _connection.ExecuteScalarAsync( + var historyCount = await _db.Connection.ExecuteScalarAsync( "SELECT COUNT(*) FROM dbo.Seccion_History WHERE Id = @Id", new { Id = id }); Assert.True(historyCount >= 1, $"Expected ≥1 history row for Seccion Id={id}, got {historyCount}"); @@ -234,29 +201,4 @@ public class SeccionRepositoryTests : IAsyncLifetime Assert.Equal(3, result.Total); Assert.Equal(2, result.Items.Count); } - - // ── 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); - } } From e5b6c06f644ea119bfec941406a5b7756be91384 Mon Sep 17 00:00:00 2001 From: dmolinari Date: Sat, 18 Apr 2026 21:44:40 -0300 Subject: [PATCH 5/7] refactor(tests): Api.Tests apunta a SIGCM2_Test_Api via TestConnectionStrings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Todos los archivos de Api.Tests reemplazan la connection string hardcodeada por TestConnectionStrings.ApiTestDb. Cada proyecto de tests ahora tiene su propia base de datos aislada, eliminando la contención entre Application.Tests y Api.Tests que causaba flakiness. --- tests/SIGCM2.Api.Tests/Admin/FiscalControllerTests.cs | 3 +-- tests/SIGCM2.Api.Tests/Admin/MediosControllerTests.cs | 3 +-- tests/SIGCM2.Api.Tests/Admin/PuntosDeVentaControllerTests.cs | 3 +-- tests/SIGCM2.Api.Tests/Admin/SeccionesControllerTests.cs | 3 +-- tests/SIGCM2.Api.Tests/Admin/V014MigrationTests.cs | 3 +-- tests/SIGCM2.Api.Tests/Admin/V015MigrationTests.cs | 3 +-- tests/SIGCM2.Api.Tests/Audit/AuditControllerTests.cs | 3 +-- tests/SIGCM2.Api.Tests/Audit/TransactionScopeSpikeTests.cs | 3 +-- tests/SIGCM2.Api.Tests/Audit/V010MigrationTests.cs | 3 +-- tests/SIGCM2.Api.Tests/Permisos/PermisosEndpointTests.cs | 3 +-- tests/SIGCM2.Api.Tests/Roles/RolesEndpointTests.cs | 3 +-- .../SIGCM2.Api.Tests/Usuarios/ChangeMyPasswordEndpointTests.cs | 3 +-- tests/SIGCM2.Api.Tests/Usuarios/CreateUsuarioEndpointTests.cs | 3 +-- .../Usuarios/DeactivateReactivateEndpointTests.cs | 3 +-- tests/SIGCM2.Api.Tests/Usuarios/GetUsuarioByIdEndpointTests.cs | 3 +-- tests/SIGCM2.Api.Tests/Usuarios/ListUsuariosEndpointTests.cs | 3 +-- tests/SIGCM2.Api.Tests/Usuarios/ResetPasswordEndpointTests.cs | 3 +-- tests/SIGCM2.Api.Tests/Usuarios/UpdateUsuarioEndpointTests.cs | 3 +-- .../SIGCM2.Api.Tests/Usuarios/UsuarioPermisosEndpointTests.cs | 3 +-- 19 files changed, 19 insertions(+), 38 deletions(-) diff --git a/tests/SIGCM2.Api.Tests/Admin/FiscalControllerTests.cs b/tests/SIGCM2.Api.Tests/Admin/FiscalControllerTests.cs index 9c7ed2b..b8ae790 100644 --- a/tests/SIGCM2.Api.Tests/Admin/FiscalControllerTests.cs +++ b/tests/SIGCM2.Api.Tests/Admin/FiscalControllerTests.cs @@ -17,8 +17,7 @@ namespace SIGCM2.Api.Tests.Admin; [Collection("ApiIntegration")] public sealed class FiscalControllerTests : 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 IvaEndpoint = "/api/v1/admin/fiscal/iva"; private const string IibbEndpoint = "/api/v1/admin/fiscal/iibb"; diff --git a/tests/SIGCM2.Api.Tests/Admin/MediosControllerTests.cs b/tests/SIGCM2.Api.Tests/Admin/MediosControllerTests.cs index bb079a1..467c668 100644 --- a/tests/SIGCM2.Api.Tests/Admin/MediosControllerTests.cs +++ b/tests/SIGCM2.Api.Tests/Admin/MediosControllerTests.cs @@ -15,8 +15,7 @@ namespace SIGCM2.Api.Tests.Admin; [Collection("ApiIntegration")] public sealed class MediosControllerTests : 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 Endpoint = "/api/v1/admin/medios"; private const string AdminUsername = "admin"; diff --git a/tests/SIGCM2.Api.Tests/Admin/PuntosDeVentaControllerTests.cs b/tests/SIGCM2.Api.Tests/Admin/PuntosDeVentaControllerTests.cs index 5bffba0..2f89ba7 100644 --- a/tests/SIGCM2.Api.Tests/Admin/PuntosDeVentaControllerTests.cs +++ b/tests/SIGCM2.Api.Tests/Admin/PuntosDeVentaControllerTests.cs @@ -16,8 +16,7 @@ namespace SIGCM2.Api.Tests.Admin; [Collection("ApiIntegration")] public sealed class PuntosDeVentaControllerTests : 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 Endpoint = "/api/v1/admin/puntos-de-venta"; private const string MediosEndpoint = "/api/v1/admin/medios"; diff --git a/tests/SIGCM2.Api.Tests/Admin/SeccionesControllerTests.cs b/tests/SIGCM2.Api.Tests/Admin/SeccionesControllerTests.cs index 8aeee47..00ac00a 100644 --- a/tests/SIGCM2.Api.Tests/Admin/SeccionesControllerTests.cs +++ b/tests/SIGCM2.Api.Tests/Admin/SeccionesControllerTests.cs @@ -15,8 +15,7 @@ namespace SIGCM2.Api.Tests.Admin; [Collection("ApiIntegration")] public sealed class SeccionesControllerTests : 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 Endpoint = "/api/v1/admin/secciones"; private const string MediosEndpoint = "/api/v1/admin/medios"; diff --git a/tests/SIGCM2.Api.Tests/Admin/V014MigrationTests.cs b/tests/SIGCM2.Api.Tests/Admin/V014MigrationTests.cs index 963983e..b1c98e4 100644 --- a/tests/SIGCM2.Api.Tests/Admin/V014MigrationTests.cs +++ b/tests/SIGCM2.Api.Tests/Admin/V014MigrationTests.cs @@ -21,8 +21,7 @@ namespace SIGCM2.Api.Tests.Admin; [Collection("ApiIntegration")] public sealed class V014MigrationTests : IClassFixture { - private const string ConnectionString = - "Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;"; + private const string ConnectionString = TestConnectionStrings.ApiTestDb; public V014MigrationTests(SIGCM2.TestSupport.TestWebAppFactory _) { diff --git a/tests/SIGCM2.Api.Tests/Admin/V015MigrationTests.cs b/tests/SIGCM2.Api.Tests/Admin/V015MigrationTests.cs index ad9990d..0d22daf 100644 --- a/tests/SIGCM2.Api.Tests/Admin/V015MigrationTests.cs +++ b/tests/SIGCM2.Api.Tests/Admin/V015MigrationTests.cs @@ -21,8 +21,7 @@ namespace SIGCM2.Api.Tests.Admin; [Collection("ApiIntegration")] public sealed class V015MigrationTests : IClassFixture { - private const string ConnectionString = - "Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;"; + private const string ConnectionString = TestConnectionStrings.ApiTestDb; public V015MigrationTests(SIGCM2.TestSupport.TestWebAppFactory _) { diff --git a/tests/SIGCM2.Api.Tests/Audit/AuditControllerTests.cs b/tests/SIGCM2.Api.Tests/Audit/AuditControllerTests.cs index ee7ddd3..4d3d4d5 100644 --- a/tests/SIGCM2.Api.Tests/Audit/AuditControllerTests.cs +++ b/tests/SIGCM2.Api.Tests/Audit/AuditControllerTests.cs @@ -18,8 +18,7 @@ namespace SIGCM2.Api.Tests.Audit; [Collection("ApiIntegration")] public sealed class AuditControllerTests : IClassFixture { - private const string ConnectionString = - "Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;"; + private const string ConnectionString = TestConnectionStrings.ApiTestDb; private readonly TestWebAppFactory _factory; diff --git a/tests/SIGCM2.Api.Tests/Audit/TransactionScopeSpikeTests.cs b/tests/SIGCM2.Api.Tests/Audit/TransactionScopeSpikeTests.cs index 8c7bcf6..622ec9b 100644 --- a/tests/SIGCM2.Api.Tests/Audit/TransactionScopeSpikeTests.cs +++ b/tests/SIGCM2.Api.Tests/Audit/TransactionScopeSpikeTests.cs @@ -12,8 +12,7 @@ namespace SIGCM2.Api.Tests.Audit; [Collection("ApiIntegration")] public sealed class TransactionScopeSpikeTests { - private const string ConnectionString = - "Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;"; + private const string ConnectionString = TestConnectionStrings.ApiTestDb; [Fact] public async Task TransactionScope_DoesNotEscalateToMSDTC_WithSingleConnectionString() diff --git a/tests/SIGCM2.Api.Tests/Audit/V010MigrationTests.cs b/tests/SIGCM2.Api.Tests/Audit/V010MigrationTests.cs index e433d95..15ff12b 100644 --- a/tests/SIGCM2.Api.Tests/Audit/V010MigrationTests.cs +++ b/tests/SIGCM2.Api.Tests/Audit/V010MigrationTests.cs @@ -13,8 +13,7 @@ namespace SIGCM2.Api.Tests.Audit; [Collection("ApiIntegration")] public sealed class V010MigrationTests : IClassFixture { - private const string ConnectionString = - "Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;"; + private const string ConnectionString = TestConnectionStrings.ApiTestDb; public V010MigrationTests(SIGCM2.TestSupport.TestWebAppFactory _) { diff --git a/tests/SIGCM2.Api.Tests/Permisos/PermisosEndpointTests.cs b/tests/SIGCM2.Api.Tests/Permisos/PermisosEndpointTests.cs index 2936a27..d641a0a 100644 --- a/tests/SIGCM2.Api.Tests/Permisos/PermisosEndpointTests.cs +++ b/tests/SIGCM2.Api.Tests/Permisos/PermisosEndpointTests.cs @@ -15,8 +15,7 @@ namespace SIGCM2.Api.Tests.Permisos; [Collection("ApiIntegration")] public sealed class PermisosEndpointTests : 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 AdminUsername = "admin"; private const string AdminPassword = "@Diego550@"; diff --git a/tests/SIGCM2.Api.Tests/Roles/RolesEndpointTests.cs b/tests/SIGCM2.Api.Tests/Roles/RolesEndpointTests.cs index 856b011..c2f27cb 100644 --- a/tests/SIGCM2.Api.Tests/Roles/RolesEndpointTests.cs +++ b/tests/SIGCM2.Api.Tests/Roles/RolesEndpointTests.cs @@ -14,8 +14,7 @@ namespace SIGCM2.Api.Tests.Roles; [Collection("ApiIntegration")] public sealed class RolesEndpointTests : 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 Endpoint = "/api/v1/roles"; private const string AdminUsername = "admin"; diff --git a/tests/SIGCM2.Api.Tests/Usuarios/ChangeMyPasswordEndpointTests.cs b/tests/SIGCM2.Api.Tests/Usuarios/ChangeMyPasswordEndpointTests.cs index 5c3641b..8f5accd 100644 --- a/tests/SIGCM2.Api.Tests/Usuarios/ChangeMyPasswordEndpointTests.cs +++ b/tests/SIGCM2.Api.Tests/Usuarios/ChangeMyPasswordEndpointTests.cs @@ -14,8 +14,7 @@ namespace SIGCM2.Api.Tests.Usuarios; [Collection("ApiIntegration")] public sealed class ChangeMyPasswordEndpointTests : IAsyncLifetime { - private const string TestConnectionString = - "Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;"; + private const string TestConnectionString = TestConnectionStrings.ApiTestDb; // This hash corresponds to "@Diego550@" private const string DefaultHash = "$2a$12$rmq6tlSAQ8WXhR2CwLCSeuwCJKz/.8Eab95UQCUNfwe4dokeOqMcW"; diff --git a/tests/SIGCM2.Api.Tests/Usuarios/CreateUsuarioEndpointTests.cs b/tests/SIGCM2.Api.Tests/Usuarios/CreateUsuarioEndpointTests.cs index abaa702..b9b742f 100644 --- a/tests/SIGCM2.Api.Tests/Usuarios/CreateUsuarioEndpointTests.cs +++ b/tests/SIGCM2.Api.Tests/Usuarios/CreateUsuarioEndpointTests.cs @@ -17,8 +17,7 @@ namespace SIGCM2.Api.Tests.Usuarios; [Collection("ApiIntegration")] public sealed class CreateUsuarioEndpointTests : 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 Endpoint = "/api/v1/users"; private const string AdminUsername = "admin"; diff --git a/tests/SIGCM2.Api.Tests/Usuarios/DeactivateReactivateEndpointTests.cs b/tests/SIGCM2.Api.Tests/Usuarios/DeactivateReactivateEndpointTests.cs index 47a6e6d..8b23c6e 100644 --- a/tests/SIGCM2.Api.Tests/Usuarios/DeactivateReactivateEndpointTests.cs +++ b/tests/SIGCM2.Api.Tests/Usuarios/DeactivateReactivateEndpointTests.cs @@ -14,8 +14,7 @@ namespace SIGCM2.Api.Tests.Usuarios; [Collection("ApiIntegration")] public sealed class DeactivateReactivateEndpointTests : IAsyncLifetime { - private const string TestConnectionString = - "Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;"; + private const string TestConnectionString = TestConnectionStrings.ApiTestDb; private readonly HttpClient _client; private readonly SqlTestFixture _db; diff --git a/tests/SIGCM2.Api.Tests/Usuarios/GetUsuarioByIdEndpointTests.cs b/tests/SIGCM2.Api.Tests/Usuarios/GetUsuarioByIdEndpointTests.cs index 57088c6..c7f3f55 100644 --- a/tests/SIGCM2.Api.Tests/Usuarios/GetUsuarioByIdEndpointTests.cs +++ b/tests/SIGCM2.Api.Tests/Usuarios/GetUsuarioByIdEndpointTests.cs @@ -14,8 +14,7 @@ namespace SIGCM2.Api.Tests.Usuarios; [Collection("ApiIntegration")] public sealed class GetUsuarioByIdEndpointTests : IAsyncLifetime { - private const string TestConnectionString = - "Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;"; + private const string TestConnectionString = TestConnectionStrings.ApiTestDb; private readonly HttpClient _client; private readonly SqlTestFixture _db; diff --git a/tests/SIGCM2.Api.Tests/Usuarios/ListUsuariosEndpointTests.cs b/tests/SIGCM2.Api.Tests/Usuarios/ListUsuariosEndpointTests.cs index 8180731..39a0bb2 100644 --- a/tests/SIGCM2.Api.Tests/Usuarios/ListUsuariosEndpointTests.cs +++ b/tests/SIGCM2.Api.Tests/Usuarios/ListUsuariosEndpointTests.cs @@ -14,8 +14,7 @@ namespace SIGCM2.Api.Tests.Usuarios; [Collection("ApiIntegration")] public sealed class ListUsuariosEndpointTests : IAsyncLifetime { - private const string TestConnectionString = - "Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;"; + private const string TestConnectionString = TestConnectionStrings.ApiTestDb; private readonly HttpClient _client; private readonly SqlTestFixture _db; diff --git a/tests/SIGCM2.Api.Tests/Usuarios/ResetPasswordEndpointTests.cs b/tests/SIGCM2.Api.Tests/Usuarios/ResetPasswordEndpointTests.cs index 6ce411e..98e58db 100644 --- a/tests/SIGCM2.Api.Tests/Usuarios/ResetPasswordEndpointTests.cs +++ b/tests/SIGCM2.Api.Tests/Usuarios/ResetPasswordEndpointTests.cs @@ -14,8 +14,7 @@ namespace SIGCM2.Api.Tests.Usuarios; [Collection("ApiIntegration")] public sealed class ResetPasswordEndpointTests : IAsyncLifetime { - private const string TestConnectionString = - "Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;"; + private const string TestConnectionString = TestConnectionStrings.ApiTestDb; private readonly HttpClient _client; private readonly SqlTestFixture _db; diff --git a/tests/SIGCM2.Api.Tests/Usuarios/UpdateUsuarioEndpointTests.cs b/tests/SIGCM2.Api.Tests/Usuarios/UpdateUsuarioEndpointTests.cs index 7e6f64c..76397d0 100644 --- a/tests/SIGCM2.Api.Tests/Usuarios/UpdateUsuarioEndpointTests.cs +++ b/tests/SIGCM2.Api.Tests/Usuarios/UpdateUsuarioEndpointTests.cs @@ -14,8 +14,7 @@ namespace SIGCM2.Api.Tests.Usuarios; [Collection("ApiIntegration")] public sealed class UpdateUsuarioEndpointTests : IAsyncLifetime { - private const string TestConnectionString = - "Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;"; + private const string TestConnectionString = TestConnectionStrings.ApiTestDb; private readonly HttpClient _client; private readonly SqlTestFixture _db; diff --git a/tests/SIGCM2.Api.Tests/Usuarios/UsuarioPermisosEndpointTests.cs b/tests/SIGCM2.Api.Tests/Usuarios/UsuarioPermisosEndpointTests.cs index 7571acb..436e1b8 100644 --- a/tests/SIGCM2.Api.Tests/Usuarios/UsuarioPermisosEndpointTests.cs +++ b/tests/SIGCM2.Api.Tests/Usuarios/UsuarioPermisosEndpointTests.cs @@ -15,8 +15,7 @@ namespace SIGCM2.Api.Tests.Usuarios; [Collection("ApiIntegration")] public sealed class UsuarioPermisosEndpointTests : 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 AdminUsername = "admin"; private const string AdminPassword = "@Diego550@"; From a0dcc7258b89330379e13e9169b937d8668d406d Mon Sep 17 00:00:00 2001 From: dmolinari Date: Sat, 18 Apr 2026 21:44:45 -0300 Subject: [PATCH 6/7] =?UTF-8?q?docs(database):=20actualiza=20README=20con?= =?UTF-8?q?=20V013-V015=20y=20secci=C3=B3n=20Test=20DBs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agrega filas V013, V014, V015 a la tabla de migraciones. Actualiza convención de "3 bases" (SIGCM2, SIGCM2_Test_App, SIGCM2_Test_Api). Añade sección "Bases de datos de integration tests" con tabla de propósito y referencia al script de creación. --- database/README.md | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/database/README.md b/database/README.md index 2b3f549..75779b5 100644 --- a/database/README.md +++ b/database/README.md @@ -29,6 +29,9 @@ database/ | **V010** | **`V010__audit_infrastructure.sql`** | **UDT-010** | **Infra de auditoría + Temporal Tables. Ver nota abajo.** | | V011 | `V011__create_medio_seccion.sql` | ADM-001 | Medio + Seccion (temporal, retention 10y) + permiso `administracion:secciones:gestionar` | | V012 | `V012__seed_medios.sql` | ADM-001 | Seed idempotente de Medios ELDIA y ELPLATA | +| V013 | `V013__create_punto_de_venta.sql` | ADM-008 | PuntoDeVenta (temporal) + permiso `administracion:puntosdeventa:gestionar` | +| V014 | `V014__create_tipo_de_iva_ingresos_brutos.sql` | ADM-009 | TipoDeIva + IngresosBrutos (temporales con vigencias) + permiso fiscal | +| V015 | `V015__create_audit_views.sql` | UDT-011 | Vistas `v_AuditEvent_Local` + `v_SecurityEvent_Local` con timezone local | ## Convenciones @@ -36,23 +39,24 @@ database/ - **Idempotentes**: cada migración usa `IF NOT EXISTS` / `MERGE` / `IF NOT EXISTS (SELECT 1 FROM sys....)`. Re-ejecutarlas es seguro. - **Boilerplate** inicial: `SET QUOTED_IDENTIFIER ON; SET ANSI_NULLS ON; SET NOCOUNT ON; GO`. - **Naming**: `PK_*`, `UQ_*`, `FK_*`, `DF_*`, `CK_*`, `IX_*`. -- **Se aplican a AMBAS bases**: `SIGCM2` (dev) y `SIGCM2_Test` (integration tests). El orden debe ser idéntico. +- **Se aplican a TRES bases**: `SIGCM2` (dev), `SIGCM2_Test_App` (Application.Tests) y `SIGCM2_Test_Api` (Api.Tests). El orden debe ser idéntico en las tres. ## Cómo aplicar migraciones ### En dev (manual) ```bash -# Con sqlcmd: -sqlcmd -S TECNICA3 -d SIGCM2 -U desarrollo -P desarrollo2026 -i database/migrations/V010__audit_infrastructure.sql -sqlcmd -S TECNICA3 -d SIGCM2_Test -U desarrollo -P desarrollo2026 -i database/migrations/V010__audit_infrastructure.sql +# Con sqlcmd (aplicar a las tres bases en orden): +sqlcmd -S TECNICA3 -d SIGCM2 -U desarrollo -P desarrollo2026 -i database/migrations/V010__audit_infrastructure.sql +sqlcmd -S TECNICA3 -d SIGCM2_Test_App -U desarrollo -P desarrollo2026 -i database/migrations/V010__audit_infrastructure.sql +sqlcmd -S TECNICA3 -d SIGCM2_Test_Api -U desarrollo -P desarrollo2026 -i database/migrations/V010__audit_infrastructure.sql ``` O desde SSMS: abrir el archivo, conectar a cada base, F5. ### En integration tests -`tests/SIGCM2.TestSupport/SqlTestFixture.cs` aplica automáticamente el schema necesario en `SIGCM2_Test` al inicializar el fixture (`EnsureV008SchemaAsync`, `EnsureV009SchemaAsync`, `EnsureV010SchemaAsync`…). **NO** hace falta correr el script manualmente. +`tests/SIGCM2.TestSupport/SqlTestFixture.cs` aplica automáticamente el schema necesario en `SIGCM2_Test_App` al inicializar el fixture (`EnsureV008SchemaAsync`, `EnsureV009SchemaAsync`, `EnsureV010SchemaAsync`…). `TestWebAppFactory` hace lo mismo contra `SIGCM2_Test_Api`. **NO** hace falta correr los scripts manualmente si el fixture ya lo cubre. ### En producción (roadmap futuro) @@ -90,6 +94,22 @@ O desde SSMS: abrir el archivo, conectar a cada base, F5. - `Seccion.Tipo` acepta `'clasificados' | 'notables' | 'suplementos'` (CHECK constraint). Config avanzada (par/impar, suplementos, cupos) se introduce en ADM-003. - Rollback: `V012_ROLLBACK.sql` (falla si hay Secciones vivas) → `V011_ROLLBACK.sql`. Rollback en prod NO soportado si ADM-008/009 o PRC-* ya aplicados. +## Bases de datos de integration tests + +| Base | Propósito | Usada por | +|---|---|---| +| `SIGCM2_Test_App` | Tests de repositorios y Application layer | `SIGCM2.Application.Tests` vía `SqlTestFixture` (parameterless ctor) | +| `SIGCM2_Test_Api` | Tests de endpoints HTTP / WebApplicationFactory | `SIGCM2.Api.Tests` vía `TestWebAppFactory` | + +**Script de creación inicial** (idempotente): `database/init/create-test-api-db.sql` + +Ambas bases deben tener **todas las migraciones V001–V015** aplicadas en orden. Al crear una base nueva o al agregar un desarrollador: +1. Crear las bases con `create-test-api-db.sql` +2. Aplicar V001–V015 en orden (ver tabla de arriba) contra cada base de test +3. Las `EnsureV0XX` del fixture validan presencia; no aplican migraciones pesadas + +Fuente única de connection strings: `tests/SIGCM2.TestSupport/TestConnectionStrings.cs` + ## Recursos - Design autoritativo: `Obsidian/02-ARQUITECTURA-y-TECH-STACK/2.5 📋 Auditoría.md` From 8daadc8a77ce6379e6ad60ba3a750e90d326b2fb Mon Sep 17 00:00:00 2001 From: dmolinari Date: Sun, 19 Apr 2026 07:40:32 -0300 Subject: [PATCH 7/7] =?UTF-8?q?fix(tests):=20timestamp=20determin=C3=ADsti?= =?UTF-8?q?co=20en=20QueryAsync=5FLimit=5FEmitsCursor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DATETIME2(3) + cursor roundtrip via O format perdía sub-ms de DateTime.UtcNow causando ~37% flake rate. Timestamp fijo con sub-ms=0 elimina la ambigüedad. Fixes residual flake del issue #29. --- .../Infrastructure/Audit/AuditEventRepositoryTests.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/SIGCM2.Application.Tests/Infrastructure/Audit/AuditEventRepositoryTests.cs b/tests/SIGCM2.Application.Tests/Infrastructure/Audit/AuditEventRepositoryTests.cs index 4420348..c637aa6 100644 --- a/tests/SIGCM2.Application.Tests/Infrastructure/Audit/AuditEventRepositoryTests.cs +++ b/tests/SIGCM2.Application.Tests/Infrastructure/Audit/AuditEventRepositoryTests.cs @@ -129,7 +129,10 @@ public sealed class AuditEventRepositoryTests : IAsyncLifetime [Fact] public async Task QueryAsync_Limit_EmitsCursor_WhenMoreRowsAvailable() { - var t0 = DateTime.UtcNow.AddMinutes(-10); + // Determinístico: DATETIME2(3) + cursor roundtrip via "O" format puede perder ticks + // sub-ms de `DateTime.UtcNow` (observado ~37% flake rate, cursor vuelve como parentesis + // de la página anterior). Timestamp fijo con sub-ms = 0 elimina la ambigüedad. + var t0 = new DateTime(2026, 4, 19, 12, 0, 0, DateTimeKind.Utc); await Seed(5, t0); var page1 = await _repo.QueryAsync(new AuditEventFilter(null, null, null, null, null, null, Limit: 2));