Compare commits
12 Commits
389dda6e5e
...
f5ed9c4b3c
| Author | SHA1 | Date | |
|---|---|---|---|
| f5ed9c4b3c | |||
| d49d2f7536 | |||
| 443380d1d1 | |||
| f8d861a25a | |||
| f6733acfbb | |||
| ff7c28789e | |||
| cc3108dfdb | |||
| b1be4a5573 | |||
| d4c05cc364 | |||
| 4c9b7eabaf | |||
| 4a88cb4319 | |||
| d3ed8300f0 |
@@ -40,24 +40,23 @@ 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 TRES bases**: `SIGCM2` (dev), `SIGCM2_Test_App` (Application.Tests) y `SIGCM2_Test_Api` (Api.Tests). El orden debe ser idéntico en las tres.
|
||||
- **Se aplican a AMBAS bases**: `SIGCM2` (dev) y `SIGCM2_Test` (integration tests). El orden debe ser idéntico.
|
||||
|
||||
## Cómo aplicar migraciones
|
||||
|
||||
### En dev (manual)
|
||||
|
||||
```bash
|
||||
# Con sqlcmd (aplicar a las tres bases en orden):
|
||||
# Con sqlcmd:
|
||||
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
|
||||
sqlcmd -S TECNICA3 -d SIGCM2_Test -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_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.
|
||||
`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.
|
||||
|
||||
### En producción (roadmap futuro)
|
||||
|
||||
@@ -95,22 +94,6 @@ 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`
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
-- 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
|
||||
@@ -33,5 +33,4 @@ function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components -- shadcn/ui generated: badgeVariants is intentionally co-located with the component
|
||||
export { Badge, badgeVariants }
|
||||
|
||||
@@ -53,5 +53,4 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
)
|
||||
Button.displayName = "Button"
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components -- shadcn/ui generated: buttonVariants is intentionally co-located with the component
|
||||
export { Button, buttonVariants }
|
||||
|
||||
@@ -165,7 +165,6 @@ const FormMessage = React.forwardRef<
|
||||
FormMessage.displayName = 'FormMessage'
|
||||
|
||||
export {
|
||||
// eslint-disable-next-line react-refresh/only-export-components -- shadcn/ui generated: useFormField hook is intentionally co-located with form components
|
||||
useFormField,
|
||||
Form,
|
||||
FormItem,
|
||||
|
||||
@@ -14,7 +14,6 @@ import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
@@ -174,11 +173,6 @@ export function IngresosBrutosFormModal({
|
||||
<DialogTitle>
|
||||
{isEdit ? 'Editar Ingresos Brutos' : 'Crear Ingresos Brutos'}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{isEdit
|
||||
? 'Modificá los datos de Ingresos Brutos. La alícuota no puede cambiarse aquí; usá "Nueva vigencia".'
|
||||
: 'Completá los datos para crear un nuevo registro de Ingresos Brutos con su alícuota inicial.'}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<Form {...form}>
|
||||
|
||||
@@ -12,7 +12,6 @@ import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
@@ -140,9 +139,6 @@ export function NuevaVigenciaIibbModal({
|
||||
<TriangleAlert className="h-5 w-5 text-warning" />
|
||||
Nueva vigencia — {item?.provinciaDisplay}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Crea una nueva versión de Ingresos Brutos para esta provincia con una alícuota y fecha de vigencia nuevas. La versión actual quedará cerrada.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div
|
||||
|
||||
@@ -13,7 +13,6 @@ import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
@@ -141,9 +140,6 @@ export function NuevaVigenciaModal({
|
||||
<TriangleAlert className="h-5 w-5 text-warning" />
|
||||
Nueva vigencia — {item?.codigo}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Crea una nueva versión del tipo de IVA con un porcentaje y fecha de vigencia nuevos. La versión actual quedará cerrada.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{/* Banner de advertencia — usa token --warning-bg */}
|
||||
|
||||
@@ -14,7 +14,6 @@ import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
@@ -181,11 +180,6 @@ export function TipoDeIvaFormModal({
|
||||
<DialogTitle>
|
||||
{isEdit ? 'Editar tipo de IVA' : 'Crear tipo de IVA'}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{isEdit
|
||||
? 'Modificá los datos del tipo de IVA. El porcentaje no puede cambiarse aquí; usá "Nueva vigencia".'
|
||||
: 'Completá los datos para crear un nuevo tipo de IVA con su alícuota inicial.'}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<Form {...form}>
|
||||
|
||||
@@ -33,7 +33,6 @@ export function RolPermisosEditor({ rolCodigo }: RolPermisosEditorProps) {
|
||||
// Prefill checkboxes cuando lleguen los permisos asignados al rol
|
||||
useEffect(() => {
|
||||
if (asignados) {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect -- sincroniza prop externa (asignados) con estado local; patrón válido de derived state
|
||||
setSelected(new Set(asignados.map((p) => p.codigo)))
|
||||
setSaved(false)
|
||||
}
|
||||
|
||||
@@ -59,7 +59,6 @@ export function PermisosEditor({ userId }: PermisosEditorProps) {
|
||||
for (const c of permisoData.overrides.grant) map.set(c, 'concedido')
|
||||
// Apply deny overrides
|
||||
for (const c of permisoData.overrides.deny) map.set(c, 'denegado')
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect -- sincroniza prop externa (permisoData) con mapa local de overrides; patrón válido de derived state
|
||||
setStates(map)
|
||||
setSaveError(null)
|
||||
}, [permisoData])
|
||||
|
||||
@@ -12,7 +12,6 @@ import type { CreatedUserDto } from '../api/createUser'
|
||||
export function CreateUserPage() {
|
||||
const navigate = useNavigate()
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- el callback recibe CreatedUserDto por contrato de UserForm pero solo necesitamos navegar
|
||||
function handleSuccess(_created: CreatedUserDto) {
|
||||
void navigate('/')
|
||||
}
|
||||
|
||||
@@ -25,7 +25,6 @@ interface FormatInstantOptions {
|
||||
*/
|
||||
export function formatInstant(
|
||||
iso: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- parámetro reservado para futura extensibilidad; el formato está hardcodeado por compatibilidad con entornos donde Intl.DateTimeFormat ignora dateStyle/timeStyle
|
||||
_opts: FormatInstantOptions = { dateStyle: 'short', timeStyle: 'medium' }
|
||||
): string {
|
||||
const parts = new Intl.DateTimeFormat('es-AR', {
|
||||
|
||||
@@ -13,7 +13,6 @@ export interface AuditFiltersValue {
|
||||
to: string
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components -- constante de reset co-ubicada con el componente que la consume como valor inicial
|
||||
export const EMPTY_FILTERS: AuditFiltersValue = {
|
||||
actor: '',
|
||||
targetType: '',
|
||||
@@ -138,7 +137,6 @@ export function AuditFilters({
|
||||
* Los convertimos a ISO UTC vía `parseArgentinaDateTimeToUtc()` (fix BUG-FE-05).
|
||||
* - Strings vacíos → omitidos.
|
||||
*/
|
||||
// eslint-disable-next-line react-refresh/only-export-components -- función utilitaria de mapeo co-ubicada con el componente que la necesita; extraerla a otro archivo aumentaría la fragmentación innecesariamente
|
||||
export function toApiFilter(
|
||||
value: AuditFiltersValue,
|
||||
): import('@/api/audit').AuditEventsFilter {
|
||||
|
||||
@@ -67,7 +67,6 @@ export function AuditPage() {
|
||||
useEffect(() => {
|
||||
if (!data) return
|
||||
if (cursor === undefined) {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect -- acumula datos paginados de una query externa; reset en primera página es intencional
|
||||
setAccumulated(data.items)
|
||||
} else {
|
||||
setAccumulated((prev) => {
|
||||
|
||||
@@ -131,8 +131,11 @@ describe('axiosClient', () => {
|
||||
setAuth('expired-access', 'valid-refresh')
|
||||
|
||||
let refreshCallCount = 0
|
||||
let requestCount = 0
|
||||
|
||||
server.use(
|
||||
http.get(`${API_URL}/api/v1/protected`, ({ request }) => {
|
||||
requestCount++
|
||||
const auth = request.headers.get('Authorization')
|
||||
if (auth === 'Bearer new-access-from-refresh') {
|
||||
return HttpResponse.json({ data: 'ok' })
|
||||
|
||||
@@ -17,7 +17,8 @@ namespace SIGCM2.Api.Tests.Admin;
|
||||
[Collection("ApiIntegration")]
|
||||
public sealed class FiscalControllerTests : IAsyncLifetime
|
||||
{
|
||||
private const string TestConnectionString = TestConnectionStrings.ApiTestDb;
|
||||
private const string TestConnectionString =
|
||||
"Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;";
|
||||
|
||||
private const string IvaEndpoint = "/api/v1/admin/fiscal/iva";
|
||||
private const string IibbEndpoint = "/api/v1/admin/fiscal/iibb";
|
||||
|
||||
@@ -15,7 +15,8 @@ namespace SIGCM2.Api.Tests.Admin;
|
||||
[Collection("ApiIntegration")]
|
||||
public sealed class MediosControllerTests : IAsyncLifetime
|
||||
{
|
||||
private const string TestConnectionString = TestConnectionStrings.ApiTestDb;
|
||||
private const string TestConnectionString =
|
||||
"Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;";
|
||||
|
||||
private const string Endpoint = "/api/v1/admin/medios";
|
||||
private const string AdminUsername = "admin";
|
||||
|
||||
@@ -16,7 +16,8 @@ namespace SIGCM2.Api.Tests.Admin;
|
||||
[Collection("ApiIntegration")]
|
||||
public sealed class PuntosDeVentaControllerTests : IAsyncLifetime
|
||||
{
|
||||
private const string TestConnectionString = TestConnectionStrings.ApiTestDb;
|
||||
private const string TestConnectionString =
|
||||
"Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;";
|
||||
|
||||
private const string Endpoint = "/api/v1/admin/puntos-de-venta";
|
||||
private const string MediosEndpoint = "/api/v1/admin/medios";
|
||||
|
||||
@@ -15,7 +15,8 @@ namespace SIGCM2.Api.Tests.Admin;
|
||||
[Collection("ApiIntegration")]
|
||||
public sealed class SeccionesControllerTests : IAsyncLifetime
|
||||
{
|
||||
private const string TestConnectionString = TestConnectionStrings.ApiTestDb;
|
||||
private const string TestConnectionString =
|
||||
"Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;";
|
||||
|
||||
private const string Endpoint = "/api/v1/admin/secciones";
|
||||
private const string MediosEndpoint = "/api/v1/admin/medios";
|
||||
|
||||
@@ -21,7 +21,8 @@ namespace SIGCM2.Api.Tests.Admin;
|
||||
[Collection("ApiIntegration")]
|
||||
public sealed class V014MigrationTests : IClassFixture<SIGCM2.TestSupport.TestWebAppFactory>
|
||||
{
|
||||
private const string ConnectionString = TestConnectionStrings.ApiTestDb;
|
||||
private const string ConnectionString =
|
||||
"Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;";
|
||||
|
||||
public V014MigrationTests(SIGCM2.TestSupport.TestWebAppFactory _)
|
||||
{
|
||||
|
||||
@@ -21,7 +21,8 @@ namespace SIGCM2.Api.Tests.Admin;
|
||||
[Collection("ApiIntegration")]
|
||||
public sealed class V015MigrationTests : IClassFixture<SIGCM2.TestSupport.TestWebAppFactory>
|
||||
{
|
||||
private const string ConnectionString = TestConnectionStrings.ApiTestDb;
|
||||
private const string ConnectionString =
|
||||
"Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;";
|
||||
|
||||
public V015MigrationTests(SIGCM2.TestSupport.TestWebAppFactory _)
|
||||
{
|
||||
|
||||
@@ -18,7 +18,8 @@ namespace SIGCM2.Api.Tests.Audit;
|
||||
[Collection("ApiIntegration")]
|
||||
public sealed class AuditControllerTests : IClassFixture<TestWebAppFactory>
|
||||
{
|
||||
private const string ConnectionString = TestConnectionStrings.ApiTestDb;
|
||||
private const string ConnectionString =
|
||||
"Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;";
|
||||
|
||||
private readonly TestWebAppFactory _factory;
|
||||
|
||||
|
||||
@@ -12,7 +12,8 @@ namespace SIGCM2.Api.Tests.Audit;
|
||||
[Collection("ApiIntegration")]
|
||||
public sealed class TransactionScopeSpikeTests
|
||||
{
|
||||
private const string ConnectionString = TestConnectionStrings.ApiTestDb;
|
||||
private const string ConnectionString =
|
||||
"Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;";
|
||||
|
||||
[Fact]
|
||||
public async Task TransactionScope_DoesNotEscalateToMSDTC_WithSingleConnectionString()
|
||||
|
||||
@@ -13,7 +13,8 @@ namespace SIGCM2.Api.Tests.Audit;
|
||||
[Collection("ApiIntegration")]
|
||||
public sealed class V010MigrationTests : IClassFixture<SIGCM2.TestSupport.TestWebAppFactory>
|
||||
{
|
||||
private const string ConnectionString = TestConnectionStrings.ApiTestDb;
|
||||
private const string ConnectionString =
|
||||
"Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;";
|
||||
|
||||
public V010MigrationTests(SIGCM2.TestSupport.TestWebAppFactory _)
|
||||
{
|
||||
|
||||
@@ -15,7 +15,8 @@ namespace SIGCM2.Api.Tests.Permisos;
|
||||
[Collection("ApiIntegration")]
|
||||
public sealed class PermisosEndpointTests : IAsyncLifetime
|
||||
{
|
||||
private const string TestConnectionString = TestConnectionStrings.ApiTestDb;
|
||||
private const string TestConnectionString =
|
||||
"Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;";
|
||||
|
||||
private const string AdminUsername = "admin";
|
||||
private const string AdminPassword = "@Diego550@";
|
||||
|
||||
@@ -14,7 +14,8 @@ namespace SIGCM2.Api.Tests.Roles;
|
||||
[Collection("ApiIntegration")]
|
||||
public sealed class RolesEndpointTests : IAsyncLifetime
|
||||
{
|
||||
private const string TestConnectionString = TestConnectionStrings.ApiTestDb;
|
||||
private const string TestConnectionString =
|
||||
"Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;";
|
||||
|
||||
private const string Endpoint = "/api/v1/roles";
|
||||
private const string AdminUsername = "admin";
|
||||
|
||||
@@ -17,7 +17,8 @@ namespace SIGCM2.Api.Tests.Rubros;
|
||||
[Collection("ApiIntegration")]
|
||||
public sealed class RubrosControllerTests : IAsyncLifetime
|
||||
{
|
||||
private const string TestConnectionString = TestConnectionStrings.ApiTestDb;
|
||||
private const string TestConnectionString =
|
||||
"Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;";
|
||||
|
||||
private const string ReadEndpoint = "/api/v1/rubros";
|
||||
private const string AdminEndpoint = "/api/v1/admin/rubros";
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
<Using Include="SIGCM2.TestSupport" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -14,7 +14,8 @@ namespace SIGCM2.Api.Tests.Usuarios;
|
||||
[Collection("ApiIntegration")]
|
||||
public sealed class ChangeMyPasswordEndpointTests : IAsyncLifetime
|
||||
{
|
||||
private const string TestConnectionString = TestConnectionStrings.ApiTestDb;
|
||||
private const string TestConnectionString =
|
||||
"Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;";
|
||||
|
||||
// This hash corresponds to "@Diego550@"
|
||||
private const string DefaultHash = "$2a$12$rmq6tlSAQ8WXhR2CwLCSeuwCJKz/.8Eab95UQCUNfwe4dokeOqMcW";
|
||||
|
||||
@@ -17,7 +17,8 @@ namespace SIGCM2.Api.Tests.Usuarios;
|
||||
[Collection("ApiIntegration")]
|
||||
public sealed class CreateUsuarioEndpointTests : IAsyncLifetime
|
||||
{
|
||||
private const string TestConnectionString = TestConnectionStrings.ApiTestDb;
|
||||
private const string TestConnectionString =
|
||||
"Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;";
|
||||
|
||||
private const string Endpoint = "/api/v1/users";
|
||||
private const string AdminUsername = "admin";
|
||||
|
||||
@@ -14,7 +14,8 @@ namespace SIGCM2.Api.Tests.Usuarios;
|
||||
[Collection("ApiIntegration")]
|
||||
public sealed class DeactivateReactivateEndpointTests : IAsyncLifetime
|
||||
{
|
||||
private const string TestConnectionString = TestConnectionStrings.ApiTestDb;
|
||||
private const string TestConnectionString =
|
||||
"Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;";
|
||||
|
||||
private readonly HttpClient _client;
|
||||
private readonly SqlTestFixture _db;
|
||||
|
||||
@@ -14,7 +14,8 @@ namespace SIGCM2.Api.Tests.Usuarios;
|
||||
[Collection("ApiIntegration")]
|
||||
public sealed class GetUsuarioByIdEndpointTests : IAsyncLifetime
|
||||
{
|
||||
private const string TestConnectionString = TestConnectionStrings.ApiTestDb;
|
||||
private const string TestConnectionString =
|
||||
"Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;";
|
||||
|
||||
private readonly HttpClient _client;
|
||||
private readonly SqlTestFixture _db;
|
||||
|
||||
@@ -14,7 +14,8 @@ namespace SIGCM2.Api.Tests.Usuarios;
|
||||
[Collection("ApiIntegration")]
|
||||
public sealed class ListUsuariosEndpointTests : IAsyncLifetime
|
||||
{
|
||||
private const string TestConnectionString = TestConnectionStrings.ApiTestDb;
|
||||
private const string TestConnectionString =
|
||||
"Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;";
|
||||
|
||||
private readonly HttpClient _client;
|
||||
private readonly SqlTestFixture _db;
|
||||
|
||||
@@ -14,7 +14,8 @@ namespace SIGCM2.Api.Tests.Usuarios;
|
||||
[Collection("ApiIntegration")]
|
||||
public sealed class ResetPasswordEndpointTests : IAsyncLifetime
|
||||
{
|
||||
private const string TestConnectionString = TestConnectionStrings.ApiTestDb;
|
||||
private const string TestConnectionString =
|
||||
"Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;";
|
||||
|
||||
private readonly HttpClient _client;
|
||||
private readonly SqlTestFixture _db;
|
||||
|
||||
@@ -14,7 +14,8 @@ namespace SIGCM2.Api.Tests.Usuarios;
|
||||
[Collection("ApiIntegration")]
|
||||
public sealed class UpdateUsuarioEndpointTests : IAsyncLifetime
|
||||
{
|
||||
private const string TestConnectionString = TestConnectionStrings.ApiTestDb;
|
||||
private const string TestConnectionString =
|
||||
"Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;";
|
||||
|
||||
private readonly HttpClient _client;
|
||||
private readonly SqlTestFixture _db;
|
||||
|
||||
@@ -15,7 +15,8 @@ namespace SIGCM2.Api.Tests.Usuarios;
|
||||
[Collection("ApiIntegration")]
|
||||
public sealed class UsuarioPermisosEndpointTests : IAsyncLifetime
|
||||
{
|
||||
private const string TestConnectionString = TestConnectionStrings.ApiTestDb;
|
||||
private const string TestConnectionString =
|
||||
"Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;";
|
||||
|
||||
private const string AdminUsername = "admin";
|
||||
private const string AdminPassword = "@Diego550@";
|
||||
|
||||
@@ -44,7 +44,7 @@ public sealed class PermisoResolverTests
|
||||
|
||||
Assert.DoesNotContain("A", result);
|
||||
Assert.Contains("B", result);
|
||||
Assert.Single(result);
|
||||
Assert.Equal(1, result.Count);
|
||||
}
|
||||
|
||||
// R-04: Grant duplicado (ya en rol) → idempotente, no duplicados
|
||||
|
||||
@@ -66,10 +66,10 @@ public class TempPasswordGeneratorTests
|
||||
{
|
||||
var pwd = TempPasswordGenerator.Generate(12);
|
||||
Assert.True(pwd.Length >= 12);
|
||||
Assert.Contains(pwd, char.IsUpper);
|
||||
Assert.Contains(pwd, char.IsLower);
|
||||
Assert.Contains(pwd, char.IsDigit);
|
||||
Assert.Contains(pwd, c => symbols.Contains(c));
|
||||
Assert.True(pwd.Any(char.IsUpper));
|
||||
Assert.True(pwd.Any(char.IsLower));
|
||||
Assert.True(pwd.Any(char.IsDigit));
|
||||
Assert.True(pwd.Any(c => symbols.Contains(c)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
using SIGCM2.TestSupport;
|
||||
using Xunit;
|
||||
|
||||
namespace SIGCM2.Application.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[CollectionDefinition("Database")]
|
||||
public sealed class DatabaseCollection : ICollectionFixture<SqlTestFixture>
|
||||
{
|
||||
// Intentionally empty: this class only exists to declare the collection/fixture binding.
|
||||
}
|
||||
@@ -13,7 +13,8 @@ namespace SIGCM2.Application.Tests.Infrastructure.Audit;
|
||||
[Collection("Database")]
|
||||
public sealed class AuditEventRepositoryTests : IAsyncLifetime
|
||||
{
|
||||
private const string ConnectionString = TestConnectionStrings.AppTestDb;
|
||||
private const string ConnectionString =
|
||||
"Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;";
|
||||
|
||||
private SqlConnection _connection = null!;
|
||||
private AuditEventRepository _repo = null!;
|
||||
@@ -129,10 +130,7 @@ public sealed class AuditEventRepositoryTests : IAsyncLifetime
|
||||
[Fact]
|
||||
public async Task QueryAsync_Limit_EmitsCursor_WhenMoreRowsAvailable()
|
||||
{
|
||||
// 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);
|
||||
var t0 = DateTime.UtcNow.AddMinutes(-10);
|
||||
await Seed(5, t0);
|
||||
|
||||
var page1 = await _repo.QueryAsync(new AuditEventFilter(null, null, null, null, null, null, Limit: 2));
|
||||
|
||||
@@ -16,7 +16,8 @@ namespace SIGCM2.Application.Tests.Infrastructure.Audit;
|
||||
[Collection("Database")]
|
||||
public sealed class AuditJobsTests : IAsyncLifetime
|
||||
{
|
||||
private const string ConnectionString = TestConnectionStrings.AppTestDb;
|
||||
private const string ConnectionString =
|
||||
"Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;";
|
||||
|
||||
private SqlConnection _connection = null!;
|
||||
private SqlConnectionFactory _factory = null!;
|
||||
|
||||
@@ -154,17 +154,9 @@ public sealed class JsonSanitizerTests
|
||||
|
||||
var input = new
|
||||
{
|
||||
password = "1",
|
||||
passwordHash = "2",
|
||||
token = "3",
|
||||
refreshToken = "4",
|
||||
accessToken = "5",
|
||||
cvv = "6",
|
||||
card = "7",
|
||||
cardNumber = "8",
|
||||
secret = "9",
|
||||
apiKey = "10",
|
||||
privateKey = "11",
|
||||
password = "1", passwordHash = "2", token = "3", refreshToken = "4",
|
||||
accessToken = "5", cvv = "6", card = "7", cardNumber = "8",
|
||||
secret = "9", apiKey = "10", privateKey = "11",
|
||||
keep = "yes",
|
||||
};
|
||||
|
||||
|
||||
@@ -11,7 +11,8 @@ namespace SIGCM2.Application.Tests.Infrastructure.Audit;
|
||||
[Collection("Database")]
|
||||
public sealed class SecurityEventRepositoryTests : IAsyncLifetime
|
||||
{
|
||||
private const string ConnectionString = TestConnectionStrings.AppTestDb;
|
||||
private const string ConnectionString =
|
||||
"Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;";
|
||||
|
||||
private SqlConnection _connection = null!;
|
||||
private SecurityEventRepository _repo = null!;
|
||||
|
||||
@@ -18,7 +18,8 @@ namespace SIGCM2.Application.Tests.Infrastructure;
|
||||
[Collection("Database")]
|
||||
public class IngresosBrutosRepositoryTests : IAsyncLifetime
|
||||
{
|
||||
private const string ConnectionString = TestConnectionStrings.AppTestDb;
|
||||
private const string ConnectionString =
|
||||
"Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;";
|
||||
|
||||
private SqlConnection _connection = null!;
|
||||
private IIngresosBrutosRepository _repo = null!;
|
||||
|
||||
@@ -1,44 +1,105 @@
|
||||
using Dapper;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using Respawn;
|
||||
using SIGCM2.Domain.Entities;
|
||||
using SIGCM2.Infrastructure.Persistence;
|
||||
using SIGCM2.TestSupport;
|
||||
|
||||
namespace SIGCM2.Application.Tests.Infrastructure;
|
||||
|
||||
/// <summary>
|
||||
/// Integration tests for RefreshTokenRepository against SIGCM2_Test_App.
|
||||
/// Uses shared SqlTestFixture via xUnit collection fixture; the repository opens its own
|
||||
/// Integration tests for RefreshTokenRepository against SIGCM2_Test.
|
||||
/// Uses Respawn to reset the DB between test classes; the repository opens its own
|
||||
/// connections so transaction-scoped isolation would block on FK locks.
|
||||
/// </summary>
|
||||
[Collection("Database")]
|
||||
public class RefreshTokenRepositoryTests : IAsyncLifetime
|
||||
{
|
||||
private readonly SqlTestFixture _db;
|
||||
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 RefreshTokenRepository _repository = null!;
|
||||
private int _testUserId;
|
||||
|
||||
public RefreshTokenRepositoryTests(SqlTestFixture db)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
await _db.ResetAndSeedAsync();
|
||||
_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"),
|
||||
// CAT-001 (V016): Rubro es temporal — history no puede deletearse directo.
|
||||
new Respawn.Graph.Table("dbo", "Rubro_History"),
|
||||
]
|
||||
});
|
||||
|
||||
await _respawner.ResetAsync(_connection);
|
||||
await SeedRolCanonicalAsync();
|
||||
await SeedTestUserAsync();
|
||||
|
||||
_testUserId = await _db.Connection.QuerySingleAsync<int>(
|
||||
_testUserId = await _connection.QuerySingleAsync<int>(
|
||||
"SELECT Id FROM dbo.Usuario WHERE Username = 'test_rt_user'");
|
||||
|
||||
var factory = new SqlConnectionFactory(TestConnectionStrings.AppTestDb);
|
||||
var factory = new SqlConnectionFactory(ConnectionString);
|
||||
_repository = new RefreshTokenRepository(factory);
|
||||
}
|
||||
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
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);
|
||||
}
|
||||
|
||||
private async Task SeedTestUserAsync()
|
||||
{
|
||||
await _db.Connection.ExecuteAsync("""
|
||||
await _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)
|
||||
|
||||
@@ -19,7 +19,8 @@ namespace SIGCM2.Application.Tests.Infrastructure;
|
||||
[Collection("Database")]
|
||||
public class TipoDeIvaRepositoryTests : IAsyncLifetime
|
||||
{
|
||||
private const string ConnectionString = TestConnectionStrings.AppTestDb;
|
||||
private const string ConnectionString =
|
||||
"Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;";
|
||||
|
||||
private SqlConnection _connection = null!;
|
||||
private ITipoDeIvaRepository _repo = null!;
|
||||
|
||||
@@ -11,7 +11,8 @@ namespace SIGCM2.Application.Tests.Integration;
|
||||
[Collection("Database")]
|
||||
public class PermisoRepositoryTests : IAsyncLifetime
|
||||
{
|
||||
private const string ConnectionString = TestConnectionStrings.AppTestDb;
|
||||
private const string ConnectionString =
|
||||
"Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;";
|
||||
|
||||
private SqlConnection _connection = null!;
|
||||
private PermisoRepository _repository = null!;
|
||||
|
||||
@@ -11,7 +11,8 @@ namespace SIGCM2.Application.Tests.Integration;
|
||||
[Collection("Database")]
|
||||
public class RolPermisoRepositoryTests : IAsyncLifetime
|
||||
{
|
||||
private const string ConnectionString = TestConnectionStrings.AppTestDb;
|
||||
private const string ConnectionString =
|
||||
"Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;";
|
||||
|
||||
private SqlConnection _connection = null!;
|
||||
private RolPermisoRepository _repository = null!;
|
||||
|
||||
@@ -8,7 +8,8 @@ namespace SIGCM2.Application.Tests.Integration;
|
||||
[Collection("Database")]
|
||||
public class RolRepositoryTests : IAsyncLifetime
|
||||
{
|
||||
private const string ConnectionString = TestConnectionStrings.AppTestDb;
|
||||
private const string ConnectionString =
|
||||
"Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;";
|
||||
|
||||
private SqlConnection _connection = null!;
|
||||
private RolRepository _repository = null!;
|
||||
|
||||
@@ -1,29 +1,68 @@
|
||||
using Dapper;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using Respawn;
|
||||
using SIGCM2.Infrastructure.Persistence;
|
||||
using SIGCM2.TestSupport;
|
||||
|
||||
namespace SIGCM2.Application.Tests.Integration;
|
||||
|
||||
[Collection("Database")]
|
||||
public class UsuarioRepositoryTests : IAsyncLifetime
|
||||
{
|
||||
private readonly SqlTestFixture _db;
|
||||
private UsuarioRepository _repository = null!;
|
||||
private const string ConnectionString =
|
||||
"Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;";
|
||||
|
||||
public UsuarioRepositoryTests(SqlTestFixture db)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
private SqlConnection _connection = null!;
|
||||
private Respawner _respawner = null!;
|
||||
private UsuarioRepository _repository = null!;
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
await _db.ResetAndSeedAsync();
|
||||
_connection = new SqlConnection(ConnectionString);
|
||||
await _connection.OpenAsync();
|
||||
|
||||
var factory = new SqlConnectionFactory(TestConnectionStrings.AppTestDb);
|
||||
_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"),
|
||||
// CAT-001 (V016): Rubro es temporal — history no puede deletearse directo.
|
||||
new Respawn.Graph.Table("dbo", "Rubro_History"),
|
||||
]
|
||||
});
|
||||
|
||||
// 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);
|
||||
_repository = new UsuarioRepository(factory);
|
||||
}
|
||||
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
await _respawner.ResetAsync(_connection);
|
||||
await _connection.CloseAsync();
|
||||
await _connection.DisposeAsync();
|
||||
}
|
||||
|
||||
// Scenario: GetByUsername returns correct entity when user exists
|
||||
[Fact]
|
||||
@@ -51,7 +90,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 _db.Connection.ExecuteAsync(
|
||||
await _connection.ExecuteAsync(
|
||||
"INSERT INTO dbo.Usuario (Username, PasswordHash, Nombre, Apellido, Rol, PermisosJson) " +
|
||||
"VALUES ('cajero1', '$2a$12$hash2', 'Juan', 'Pérez', 'cajero', '[]')");
|
||||
|
||||
@@ -64,4 +103,48 @@ 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,46 +1,86 @@
|
||||
using Dapper;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using Respawn;
|
||||
using SIGCM2.Infrastructure.Persistence;
|
||||
using SIGCM2.TestSupport;
|
||||
|
||||
namespace SIGCM2.Application.Tests.Integration;
|
||||
|
||||
/// <summary>
|
||||
/// Integration tests for IUsuarioRepository.UpdatePermisosJsonAsync (UDT-009).
|
||||
/// Uses SIGCM2_Test_App database via shared SqlTestFixture.
|
||||
/// Uses SIGCM2_Test database directly.
|
||||
/// </summary>
|
||||
[Collection("Database")]
|
||||
public sealed class UsuarioRepository_PermisosTests : IAsyncLifetime
|
||||
{
|
||||
private readonly SqlTestFixture _db;
|
||||
private UsuarioRepository _repository = null!;
|
||||
private const string ConnectionString =
|
||||
"Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;";
|
||||
|
||||
public UsuarioRepository_PermisosTests(SqlTestFixture db)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
private SqlConnection _connection = null!;
|
||||
private Respawner _respawner = null!;
|
||||
private UsuarioRepository _repository = null!;
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
await _db.ResetAndSeedAsync();
|
||||
_connection = new SqlConnection(ConnectionString);
|
||||
await _connection.OpenAsync();
|
||||
|
||||
var factory = new SqlConnectionFactory(TestConnectionStrings.AppTestDb);
|
||||
_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"),
|
||||
// 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);
|
||||
_repository = new UsuarioRepository(factory);
|
||||
|
||||
// Seed a test user
|
||||
await _db.Connection.ExecuteAsync("""
|
||||
await _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 Task DisposeAsync() => Task.CompletedTask;
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
if (_connection is not null)
|
||||
{
|
||||
await _respawner.ResetAsync(_connection);
|
||||
await _connection.CloseAsync();
|
||||
await _connection.DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
// UPJ-01: UpdatePermisosJsonAsync persists PermisosJson and FechaModificacion
|
||||
[Fact]
|
||||
public async Task UpdatePermisosJsonAsync_PersistsJsonAndFechaModificacion()
|
||||
{
|
||||
// Arrange
|
||||
var userId = await _db.Connection.QuerySingleAsync<int>(
|
||||
var userId = await _connection.QuerySingleAsync<int>(
|
||||
"SELECT Id FROM dbo.Usuario WHERE Username = 'testuser'");
|
||||
var newJson = """{"grant":["textos:editar"],"deny":[]}""";
|
||||
var fechaMod = DateTime.UtcNow;
|
||||
@@ -49,7 +89,7 @@ public sealed class UsuarioRepository_PermisosTests : IAsyncLifetime
|
||||
await _repository.UpdatePermisosJsonAsync(userId, newJson, fechaMod);
|
||||
|
||||
// Assert
|
||||
var row = await _db.Connection.QuerySingleAsync<(string PermisosJson, DateTime? FechaModificacion)>(
|
||||
var row = await _connection.QuerySingleAsync<(string PermisosJson, DateTime? FechaModificacion)>(
|
||||
"SELECT PermisosJson, FechaModificacion FROM dbo.Usuario WHERE Id = @Id",
|
||||
new { Id = userId });
|
||||
|
||||
@@ -74,7 +114,7 @@ public sealed class UsuarioRepository_PermisosTests : IAsyncLifetime
|
||||
public async Task UpdatePermisosJsonAsync_GetByIdReflectsChange()
|
||||
{
|
||||
// Arrange
|
||||
var userId = await _db.Connection.QuerySingleAsync<int>(
|
||||
var userId = await _connection.QuerySingleAsync<int>(
|
||||
"SELECT Id FROM dbo.Usuario WHERE Username = 'testuser'");
|
||||
var newJson = """{"grant":["pauta:azanu:ver"],"deny":["ventas:contado:cobrar"]}""";
|
||||
|
||||
@@ -87,4 +127,22 @@ 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);
|
||||
""");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +1,68 @@
|
||||
using Dapper;
|
||||
using SIGCM2.TestSupport;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using Respawn;
|
||||
|
||||
namespace SIGCM2.Application.Tests.Integration;
|
||||
|
||||
/// <summary>
|
||||
/// SUITE-B-MIGRATION-V009 — M-01 a M-07 (UDT-009)
|
||||
/// Validates the V009 migration SQL and SqlTestFixture.EnsureV009SchemaAsync.
|
||||
/// Uses SIGCM2_Test_App database via shared SqlTestFixture.
|
||||
/// Uses SIGCM2_Test database directly.
|
||||
/// </summary>
|
||||
[Collection("Database")]
|
||||
public sealed class V009MigrationTests : IAsyncLifetime
|
||||
{
|
||||
private readonly SqlTestFixture _db;
|
||||
private const string ConnectionString =
|
||||
"Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;";
|
||||
|
||||
public V009MigrationTests(SqlTestFixture db)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
private SqlConnection _connection = null!;
|
||||
private Respawner _respawner = null!;
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
await _db.ResetAndSeedAsync();
|
||||
_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"),
|
||||
// CAT-001 (V016): Rubro es temporal — history no puede deletearse directo.
|
||||
new Respawn.Graph.Table("dbo", "Rubro_History"),
|
||||
]
|
||||
});
|
||||
|
||||
await _respawner.ResetAsync(_connection);
|
||||
await SeedRolAsync();
|
||||
}
|
||||
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
if (_connection is not null)
|
||||
{
|
||||
await _connection.CloseAsync();
|
||||
await _connection.DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
// M-01: migration file exists on filesystem
|
||||
[Fact]
|
||||
@@ -73,7 +112,7 @@ public sealed class V009MigrationTests : IAsyncLifetime
|
||||
AND name = 'PermisosJson'
|
||||
""";
|
||||
|
||||
var definition = await _db.Connection.QuerySingleOrDefaultAsync<string>(sql);
|
||||
var definition = await _connection.QuerySingleOrDefaultAsync<string>(sql);
|
||||
|
||||
Assert.NotNull(definition);
|
||||
Assert.Contains(@"{""grant"":[]", definition);
|
||||
@@ -86,7 +125,7 @@ public sealed class V009MigrationTests : IAsyncLifetime
|
||||
{
|
||||
await EnsureV009SchemaAsync();
|
||||
|
||||
await _db.Connection.ExecuteAsync("""
|
||||
await _connection.ExecuteAsync("""
|
||||
INSERT INTO dbo.Usuario (Username, PasswordHash, Nombre, Apellido, Rol, PermisosJson, Activo, MustChangePassword)
|
||||
VALUES ('legacyempty', '$2a$12$hash', 'L', 'E', 'admin', '[]', 1, 0)
|
||||
""");
|
||||
@@ -94,7 +133,7 @@ public sealed class V009MigrationTests : IAsyncLifetime
|
||||
// Run migration again to migrate the newly inserted row
|
||||
await EnsureV009SchemaAsync();
|
||||
|
||||
var permisosJson = await _db.Connection.QuerySingleAsync<string>(
|
||||
var permisosJson = await _connection.QuerySingleAsync<string>(
|
||||
"SELECT PermisosJson FROM dbo.Usuario WHERE Username = 'legacyempty'");
|
||||
|
||||
Assert.Equal("""{"grant":[],"deny":[]}""", permisosJson);
|
||||
@@ -106,14 +145,14 @@ public sealed class V009MigrationTests : IAsyncLifetime
|
||||
{
|
||||
await EnsureV009SchemaAsync();
|
||||
|
||||
await _db.Connection.ExecuteAsync("""
|
||||
await _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 _db.Connection.QuerySingleAsync<string>(
|
||||
var permisosJson = await _connection.QuerySingleAsync<string>(
|
||||
"SELECT PermisosJson FROM dbo.Usuario WHERE Username = 'legacywild'");
|
||||
|
||||
Assert.Equal("""{"grant":[],"deny":[]}""", permisosJson);
|
||||
@@ -129,7 +168,7 @@ public sealed class V009MigrationTests : IAsyncLifetime
|
||||
await EnsureV009SchemaAsync();
|
||||
|
||||
// Temporarily drop and re-add without the DEFAULT so we can insert ''
|
||||
await _db.Connection.ExecuteAsync("""
|
||||
await _connection.ExecuteAsync("""
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM sys.default_constraints
|
||||
WHERE name = 'DF_Usuario_Permisos'
|
||||
@@ -138,7 +177,7 @@ public sealed class V009MigrationTests : IAsyncLifetime
|
||||
ALTER TABLE dbo.Usuario DROP CONSTRAINT DF_Usuario_Permisos;
|
||||
""");
|
||||
|
||||
await _db.Connection.ExecuteAsync("""
|
||||
await _connection.ExecuteAsync("""
|
||||
INSERT INTO dbo.Usuario (Username, PasswordHash, Nombre, Apellido, Rol, PermisosJson, Activo, MustChangePassword)
|
||||
VALUES ('emptystruser', '$2a$12$hash', 'E', 'S', 'admin', '', 1, 0)
|
||||
""");
|
||||
@@ -146,7 +185,7 @@ public sealed class V009MigrationTests : IAsyncLifetime
|
||||
// Re-apply V009 (which restores constraint and migrates '' rows)
|
||||
await EnsureV009SchemaAsync();
|
||||
|
||||
var permisosJson = await _db.Connection.QuerySingleAsync<string>(
|
||||
var permisosJson = await _connection.QuerySingleAsync<string>(
|
||||
"SELECT PermisosJson FROM dbo.Usuario WHERE Username = 'emptystruser'");
|
||||
|
||||
Assert.Equal("""{"grant":[],"deny":[]}""", permisosJson);
|
||||
@@ -159,7 +198,7 @@ public sealed class V009MigrationTests : IAsyncLifetime
|
||||
await EnsureV009SchemaAsync();
|
||||
|
||||
// Seed admin as TestFixture does post-V009
|
||||
await _db.Connection.ExecuteAsync("""
|
||||
await _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 (
|
||||
@@ -169,7 +208,7 @@ public sealed class V009MigrationTests : IAsyncLifetime
|
||||
)
|
||||
""");
|
||||
|
||||
var permisosJson = await _db.Connection.QuerySingleAsync<string>(
|
||||
var permisosJson = await _connection.QuerySingleAsync<string>(
|
||||
"SELECT PermisosJson FROM dbo.Usuario WHERE Username = 'admin'");
|
||||
|
||||
Assert.Equal("""{"grant":[],"deny":[]}""", permisosJson);
|
||||
@@ -177,6 +216,19 @@ 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);
|
||||
""");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replicates V009 migration idempotently — mirrors SqlTestFixture.EnsureV009SchemaAsync.
|
||||
/// </summary>
|
||||
@@ -214,8 +266,8 @@ public sealed class V009MigrationTests : IAsyncLifetime
|
||||
OR LTRIM(RTRIM(PermisosJson)) = ''
|
||||
""";
|
||||
|
||||
await _db.Connection.ExecuteAsync(dropConstraint);
|
||||
await _db.Connection.ExecuteAsync(addConstraint);
|
||||
await _db.Connection.ExecuteAsync(migrateRows);
|
||||
await _connection.ExecuteAsync(dropConstraint);
|
||||
await _connection.ExecuteAsync(addConstraint);
|
||||
await _connection.ExecuteAsync(migrateRows);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,35 +1,71 @@
|
||||
using Dapper;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using Respawn;
|
||||
using SIGCM2.Domain.Entities;
|
||||
using SIGCM2.Infrastructure.Persistence;
|
||||
using SIGCM2.TestSupport;
|
||||
|
||||
namespace SIGCM2.Application.Tests.Medios;
|
||||
|
||||
/// <summary>
|
||||
/// Integration tests for MedioRepository against SIGCM2_Test_App.
|
||||
/// Integration tests for MedioRepository against SIGCM2_Test.
|
||||
/// TDD: RED written before implementation, GREEN after MedioRepository was created.
|
||||
/// Temporal: after UpdateAsync, dbo.Medio_History MUST have ≥1 row for that Id.
|
||||
/// </summary>
|
||||
[Collection("Database")]
|
||||
public class MedioRepositoryTests : IAsyncLifetime
|
||||
{
|
||||
private readonly SqlTestFixture _db;
|
||||
private MedioRepository _repository = null!;
|
||||
private const string ConnectionString =
|
||||
"Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;";
|
||||
|
||||
public MedioRepositoryTests(SqlTestFixture db)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
private SqlConnection _connection = null!;
|
||||
private Respawner _respawner = null!;
|
||||
private MedioRepository _repository = null!;
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
await _db.ResetAndSeedAsync();
|
||||
_connection = new SqlConnection(ConnectionString);
|
||||
await _connection.OpenAsync();
|
||||
|
||||
var factory = new SqlConnectionFactory(TestConnectionStrings.AppTestDb);
|
||||
_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"),
|
||||
// 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);
|
||||
_repository = new MedioRepository(factory);
|
||||
}
|
||||
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
await _connection.CloseAsync();
|
||||
await _connection.DisposeAsync();
|
||||
}
|
||||
|
||||
// ── AddAsync + GetByIdAsync roundtrip ─────────────────────────────────────
|
||||
|
||||
@@ -136,7 +172,7 @@ public class MedioRepositoryTests : IAsyncLifetime
|
||||
var updated = original!.WithUpdatedProfile("Historial v2", TipoMedio.Web, null, DateTime.UtcNow);
|
||||
await _repository.UpdateAsync(updated);
|
||||
|
||||
var historyCount = await _db.Connection.ExecuteScalarAsync<int>(
|
||||
var historyCount = await _connection.ExecuteScalarAsync<int>(
|
||||
"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}");
|
||||
@@ -207,4 +243,29 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +1,73 @@
|
||||
using Dapper;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using Respawn;
|
||||
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.
|
||||
/// Integration tests for RubroRepository against SIGCM2_Test.
|
||||
/// TDD: RED written before implementation, GREEN after RubroRepository was created.
|
||||
/// 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 const string ConnectionString =
|
||||
"Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;";
|
||||
|
||||
private SqlConnection _connection = null!;
|
||||
private Respawner _respawner = null!;
|
||||
private RubroRepository _repository = null!;
|
||||
private TimeProvider _timeProvider = null!;
|
||||
|
||||
public RubroRepositoryTests(SqlTestFixture db)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
await _db.ResetAndSeedAsync();
|
||||
_connection = new SqlConnection(ConnectionString);
|
||||
await _connection.OpenAsync();
|
||||
|
||||
var factory = new SqlConnectionFactory(TestConnectionStrings.AppTestDb);
|
||||
_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);
|
||||
_repository = new RubroRepository(factory);
|
||||
_timeProvider = TimeProvider.System;
|
||||
}
|
||||
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
await _connection.CloseAsync();
|
||||
await _connection.DisposeAsync();
|
||||
}
|
||||
|
||||
// ── AddAsync + GetByIdAsync roundtrip ─────────────────────────────────────
|
||||
|
||||
@@ -345,7 +381,7 @@ public class RubroRepositoryTests : IAsyncLifetime
|
||||
Assert.Equal("Actualizado", result!.Nombre);
|
||||
Assert.NotNull(result.FechaModificacion);
|
||||
|
||||
var historyCount = await _db.Connection.ExecuteScalarAsync<int>(
|
||||
var historyCount = await _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}");
|
||||
@@ -387,4 +423,28 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,12 +24,10 @@
|
||||
<ProjectReference Include="..\..\src\api\SIGCM2.Application\SIGCM2.Application.csproj" />
|
||||
<ProjectReference Include="..\..\src\api\SIGCM2.Infrastructure\SIGCM2.Infrastructure.csproj" />
|
||||
<ProjectReference Include="..\..\src\api\SIGCM2.Domain\SIGCM2.Domain.csproj" />
|
||||
<ProjectReference Include="..\SIGCM2.TestSupport\SIGCM2.TestSupport.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
<Using Include="SIGCM2.TestSupport" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,33 +1,64 @@
|
||||
using Dapper;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using Respawn;
|
||||
using SIGCM2.Domain.Entities;
|
||||
using SIGCM2.Infrastructure.Persistence;
|
||||
using SIGCM2.TestSupport;
|
||||
|
||||
namespace SIGCM2.Application.Tests.Secciones;
|
||||
|
||||
/// <summary>
|
||||
/// Integration tests for SeccionRepository against SIGCM2_Test_App.
|
||||
/// Integration tests for SeccionRepository against SIGCM2_Test.
|
||||
/// TDD: RED written before implementation, GREEN after SeccionRepository was created.
|
||||
/// Temporal: after UpdateAsync, dbo.Seccion_History MUST have ≥1 row for that Id.
|
||||
/// </summary>
|
||||
[Collection("Database")]
|
||||
public class SeccionRepositoryTests : IAsyncLifetime
|
||||
{
|
||||
private readonly SqlTestFixture _db;
|
||||
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 SeccionRepository _repository = null!;
|
||||
private MedioRepository _medioRepository = null!;
|
||||
private int _medioId;
|
||||
|
||||
public SeccionRepositoryTests(SqlTestFixture db)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
await _db.ResetAndSeedAsync();
|
||||
_connection = new SqlConnection(ConnectionString);
|
||||
await _connection.OpenAsync();
|
||||
|
||||
var factory = new SqlConnectionFactory(TestConnectionStrings.AppTestDb);
|
||||
_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"),
|
||||
// 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);
|
||||
_repository = new SeccionRepository(factory);
|
||||
_medioRepository = new MedioRepository(factory);
|
||||
|
||||
@@ -35,7 +66,11 @@ public class SeccionRepositoryTests : IAsyncLifetime
|
||||
_medioId = await _medioRepository.AddAsync(Medio.ForCreation("TESTMEDIO", "Medio de Prueba", TipoMedio.Diario, null));
|
||||
}
|
||||
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
await _connection.CloseAsync();
|
||||
await _connection.DisposeAsync();
|
||||
}
|
||||
|
||||
// ── AddAsync + GetByIdAsync roundtrip ─────────────────────────────────────
|
||||
|
||||
@@ -138,7 +173,7 @@ public class SeccionRepositoryTests : IAsyncLifetime
|
||||
var updated = original!.WithUpdatedProfile("Historial v2", "suplementos", DateTime.UtcNow);
|
||||
await _repository.UpdateAsync(updated);
|
||||
|
||||
var historyCount = await _db.Connection.ExecuteScalarAsync<int>(
|
||||
var historyCount = await _connection.ExecuteScalarAsync<int>(
|
||||
"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}");
|
||||
@@ -201,4 +236,29 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ public class ListTiposDeIvaQueryHandlerTests
|
||||
|
||||
var result = await _handler.Handle(new ListTiposDeIvaQuery(1, 10, null, null));
|
||||
|
||||
Assert.Empty(result.Items);
|
||||
Assert.Equal(0, result.Items.Count);
|
||||
Assert.Equal(0, result.Total);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,14 +16,7 @@ public sealed class SqlTestFixture : IAsyncLifetime
|
||||
private SqlConnection _connection = null!;
|
||||
private Respawner _respawner = null!;
|
||||
|
||||
/// <summary>Parameterless ctor for xUnit ICollectionFixture — uses SIGCM2_Test_App.</summary>
|
||||
public SqlTestFixture() : this(TestConnectionStrings.AppTestDb) { }
|
||||
|
||||
/// <summary>
|
||||
/// Explicit connection string ctor — used by TestWebAppFactory (same assembly).
|
||||
/// Internal to satisfy xUnit's "single public constructor" rule for ICollectionFixture.
|
||||
/// </summary>
|
||||
internal SqlTestFixture(string connectionString)
|
||||
public SqlTestFixture(string connectionString)
|
||||
{
|
||||
_connectionString = connectionString;
|
||||
}
|
||||
@@ -84,7 +77,7 @@ 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.
|
||||
// CAT-001 (V016): Rubro es system-versioned — Respawn no puede DELETE su history.
|
||||
new Respawn.Graph.Table("dbo", "Rubro_History"),
|
||||
]
|
||||
});
|
||||
@@ -92,13 +85,6 @@ public sealed class SqlTestFixture : IAsyncLifetime
|
||||
await ResetAndSeedAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public SqlConnection Connection => _connection;
|
||||
|
||||
public async Task ResetAndSeedAsync()
|
||||
{
|
||||
await _respawner.ResetAsync(_connection);
|
||||
@@ -801,70 +787,11 @@ public sealed class SqlTestFixture : IAsyncLifetime
|
||||
// desde SeedPermisosCanonicalAsync / SeedRolPermisosCanonicalAsync (post-respawn).
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// UDT-011 (V015): applies dbo.v_AuditEvent_Local + dbo.v_SecurityEvent_Local views
|
||||
/// idempotently to the test database. Mirrors V015__create_local_timezone_views.sql.
|
||||
/// Views expose OccurredAtLocal (DateTimeOffset, offset -03:00 Argentina Standard Time).
|
||||
/// Note: CREATE VIEW cannot be inside IF...BEGIN...END directly — uses EXEC('CREATE VIEW ...').
|
||||
/// </summary>
|
||||
private async Task EnsureV015SchemaAsync()
|
||||
{
|
||||
const string createAuditEventLocal = """
|
||||
IF OBJECT_ID('dbo.v_AuditEvent_Local', 'V') IS NULL
|
||||
BEGIN
|
||||
EXEC('
|
||||
CREATE VIEW dbo.v_AuditEvent_Local AS
|
||||
SELECT
|
||||
Id,
|
||||
OccurredAt,
|
||||
OccurredAt AT TIME ZONE ''UTC'' AT TIME ZONE ''Argentina Standard Time'' AS OccurredAtLocal,
|
||||
ActorUserId,
|
||||
ActorRoleId,
|
||||
Action,
|
||||
TargetType,
|
||||
TargetId,
|
||||
CorrelationId,
|
||||
IpAddress,
|
||||
UserAgent,
|
||||
Metadata
|
||||
FROM dbo.AuditEvent;
|
||||
');
|
||||
END
|
||||
""";
|
||||
|
||||
const string createSecurityEventLocal = """
|
||||
IF OBJECT_ID('dbo.v_SecurityEvent_Local', 'V') IS NULL
|
||||
BEGIN
|
||||
EXEC('
|
||||
CREATE VIEW dbo.v_SecurityEvent_Local AS
|
||||
SELECT
|
||||
Id,
|
||||
OccurredAt,
|
||||
OccurredAt AT TIME ZONE ''UTC'' AT TIME ZONE ''Argentina Standard Time'' AS OccurredAtLocal,
|
||||
ActorUserId,
|
||||
AttemptedUsername,
|
||||
SessionId,
|
||||
Action,
|
||||
Result,
|
||||
FailureReason,
|
||||
IpAddress,
|
||||
UserAgent,
|
||||
Metadata
|
||||
FROM dbo.SecurityEvent;
|
||||
');
|
||||
END
|
||||
""";
|
||||
|
||||
await _connection.ExecuteAsync(createAuditEventLocal);
|
||||
await _connection.ExecuteAsync(createSecurityEventLocal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CAT-001 (V016): applies dbo.Rubro schema + temporal + filtered unique index + covering index
|
||||
/// idempotentemente. Mirrors V016__create_rubro.sql.
|
||||
/// + permiso 'catalogo:rubros:gestionar' 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).
|
||||
/// Permiso y asignación a admin se siembran desde SeedPermisosCanonicalAsync / SeedRolPermisosCanonicalAsync.
|
||||
/// </summary>
|
||||
private async Task EnsureV016SchemaAsync()
|
||||
{
|
||||
@@ -932,5 +859,65 @@ public sealed class SqlTestFixture : IAsyncLifetime
|
||||
await _connection.ExecuteAsync(setRubroVersioning);
|
||||
await _connection.ExecuteAsync(createUqIndex);
|
||||
await _connection.ExecuteAsync(createCoveringIndex);
|
||||
// Permiso 'catalogo:rubros:gestionar' y asignación a admin se siembran
|
||||
// desde SeedPermisosCanonicalAsync / SeedRolPermisosCanonicalAsync (post-respawn).
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// UDT-011 (V015): applies dbo.v_AuditEvent_Local + dbo.v_SecurityEvent_Local views
|
||||
/// idempotently to the test database. Mirrors V015__create_local_timezone_views.sql.
|
||||
/// Views expose OccurredAtLocal (DateTimeOffset, offset -03:00 Argentina Standard Time).
|
||||
/// Note: CREATE VIEW cannot be inside IF...BEGIN...END directly — uses EXEC('CREATE VIEW ...').
|
||||
/// </summary>
|
||||
private async Task EnsureV015SchemaAsync()
|
||||
{
|
||||
const string createAuditEventLocal = """
|
||||
IF OBJECT_ID('dbo.v_AuditEvent_Local', 'V') IS NULL
|
||||
BEGIN
|
||||
EXEC('
|
||||
CREATE VIEW dbo.v_AuditEvent_Local AS
|
||||
SELECT
|
||||
Id,
|
||||
OccurredAt,
|
||||
OccurredAt AT TIME ZONE ''UTC'' AT TIME ZONE ''Argentina Standard Time'' AS OccurredAtLocal,
|
||||
ActorUserId,
|
||||
ActorRoleId,
|
||||
Action,
|
||||
TargetType,
|
||||
TargetId,
|
||||
CorrelationId,
|
||||
IpAddress,
|
||||
UserAgent,
|
||||
Metadata
|
||||
FROM dbo.AuditEvent;
|
||||
');
|
||||
END
|
||||
""";
|
||||
|
||||
const string createSecurityEventLocal = """
|
||||
IF OBJECT_ID('dbo.v_SecurityEvent_Local', 'V') IS NULL
|
||||
BEGIN
|
||||
EXEC('
|
||||
CREATE VIEW dbo.v_SecurityEvent_Local AS
|
||||
SELECT
|
||||
Id,
|
||||
OccurredAt,
|
||||
OccurredAt AT TIME ZONE ''UTC'' AT TIME ZONE ''Argentina Standard Time'' AS OccurredAtLocal,
|
||||
ActorUserId,
|
||||
AttemptedUsername,
|
||||
SessionId,
|
||||
Action,
|
||||
Result,
|
||||
FailureReason,
|
||||
IpAddress,
|
||||
UserAgent,
|
||||
Metadata
|
||||
FROM dbo.SecurityEvent;
|
||||
');
|
||||
END
|
||||
""";
|
||||
|
||||
await _connection.ExecuteAsync(createAuditEventLocal);
|
||||
await _connection.ExecuteAsync(createSecurityEventLocal);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("SIGCM2.Api.Tests")]
|
||||
|
||||
namespace SIGCM2.TestSupport;
|
||||
|
||||
/// <summary>
|
||||
/// Centralized connection string constants for integration test databases.
|
||||
/// Single source of truth — change server/credentials here only.
|
||||
/// </summary>
|
||||
public static class TestConnectionStrings
|
||||
{
|
||||
/// <summary>Used by SIGCM2.Application.Tests via SqlTestFixture (parameterless ctor).</summary>
|
||||
public const string AppTestDb =
|
||||
"Server=TECNICA3;Database=SIGCM2_Test_App;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;";
|
||||
|
||||
/// <summary>Used by SIGCM2.Api.Tests via TestWebAppFactory.</summary>
|
||||
public const string ApiTestDb =
|
||||
"Server=TECNICA3;Database=SIGCM2_Test_Api;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;";
|
||||
}
|
||||
@@ -13,11 +13,12 @@ namespace SIGCM2.TestSupport;
|
||||
|
||||
/// <summary>
|
||||
/// WebApplicationFactory for integration tests against SIGCM2.Api.
|
||||
/// Uses SIGCM2_Test_Api database (isolated from Application.Tests which uses SIGCM2_Test_App).
|
||||
/// Uses SIGCM2_Test database (separate from production SIGCM2).
|
||||
/// </summary>
|
||||
public sealed class TestWebAppFactory : WebApplicationFactory<Program>, IAsyncLifetime
|
||||
{
|
||||
private const string TestConnectionString = TestConnectionStrings.ApiTestDb;
|
||||
private const string TestConnectionString =
|
||||
"Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;";
|
||||
|
||||
// Resolved once — absolute paths independent of working directory
|
||||
private static readonly string RepoRoot = ResolveRepoRoot();
|
||||
|
||||
Reference in New Issue
Block a user