test(udt-011): V015 migration tests for timezone views (Red)

This commit is contained in:
2026-04-18 09:38:55 -03:00
parent d4b2183628
commit be6f76d107

View File

@@ -0,0 +1,262 @@
using Dapper;
using FluentAssertions;
using Microsoft.Data.SqlClient;
using Xunit;
namespace SIGCM2.Api.Tests.Admin;
/// <summary>
/// UDT-011 Batch 2 — V015 migration integration tests.
/// Validates:
/// REQ-DB-VIEWS-001 : Vista dbo.v_AuditEvent_Local existe tras aplicar V015.
/// REQ-DB-VIEWS-002 : Vista dbo.v_SecurityEvent_Local existe tras aplicar V015.
/// REQ-DB-VIEWS-003 : OccurredAtLocal retorna offset -03:00 (Argentina Standard Time).
/// REQ-DB-VIEWS-004 : V015 es idempotente (re-ejecución no falla ni duplica vistas).
/// REQ-DB-VIEWS-005 : V015_ROLLBACK elimina ambas vistas.
///
/// NOTA: Esta suite opera directamente sobre SIGCM2_Test con Dapper.
/// NO usa WebApplicationFactory (es test de migración pura, no API).
/// La migración se aplica via SqlTestFixture.EnsureV015SchemaAsync().
/// </summary>
[Collection("ApiIntegration")]
public sealed class V015MigrationTests : IClassFixture<SIGCM2.TestSupport.TestWebAppFactory>
{
private const string ConnectionString =
"Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;";
public V015MigrationTests(SIGCM2.TestSupport.TestWebAppFactory _)
{
// Depend on the factory so SqlTestFixture.InitializeAsync runs
// (ensures V015 schema is present via EnsureV015SchemaAsync).
}
// ── REQ-DB-VIEWS-001 ─────────────────────────────────────────────────────
[Fact]
public async Task V015_DebeCrearVistaAuditEventLocal()
{
await using var conn = new SqlConnection(ConnectionString);
await conn.OpenAsync();
var objectId = await conn.ExecuteScalarAsync<int?>("""
SELECT OBJECT_ID('dbo.v_AuditEvent_Local', 'V')
""");
objectId.Should().NotBeNull("dbo.v_AuditEvent_Local debe existir tras aplicar V015");
}
// ── REQ-DB-VIEWS-002 ─────────────────────────────────────────────────────
[Fact]
public async Task V015_DebeCrearVistaSecurityEventLocal()
{
await using var conn = new SqlConnection(ConnectionString);
await conn.OpenAsync();
var objectId = await conn.ExecuteScalarAsync<int?>("""
SELECT OBJECT_ID('dbo.v_SecurityEvent_Local', 'V')
""");
objectId.Should().NotBeNull("dbo.v_SecurityEvent_Local debe existir tras aplicar V015");
}
// ── REQ-DB-VIEWS-003 ─────────────────────────────────────────────────────
// Inserta 1 fila con OccurredAt = 2026-05-01 01:30:00 UTC
// Espera OccurredAtLocal con offset -03:00 → 2026-04-30 22:30:00-03:00
[Fact]
public async Task v_AuditEvent_Local_RetornaOccurredAtLocalConOffsetArgentina()
{
await using var conn = new SqlConnection(ConnectionString);
await conn.OpenAsync();
// Insert a test row with known OccurredAt UTC value.
// OccurredAt = 2026-05-01 01:30:00 UTC → Argentina (UTC-3) = 2026-04-30 22:30:00
var occurredAt = new DateTime(2026, 5, 1, 1, 30, 0, DateTimeKind.Utc);
long insertedId;
try
{
insertedId = await conn.ExecuteScalarAsync<long>("""
INSERT INTO dbo.AuditEvent
(OccurredAt, ActorUserId, ActorRoleId, Action, TargetType, TargetId, CorrelationId, IpAddress, UserAgent, Metadata)
VALUES
(@OccurredAt, NULL, NULL, 'test.v015', 'Test', '0', NULL, '127.0.0.1', NULL, NULL);
SELECT SCOPE_IDENTITY();
""", new { OccurredAt = occurredAt });
}
catch
{
// If insert fails (e.g. CHECK constraint on Action), skip — vista test only
throw;
}
var result = await conn.QuerySingleOrDefaultAsync<(DateTimeOffset OccurredAtLocal, DateTime OccurredAt)>("""
SELECT TOP 1 OccurredAtLocal, OccurredAt
FROM dbo.v_AuditEvent_Local
WHERE Action = 'test.v015'
AND OccurredAt = @OccurredAt
""", new { OccurredAt = occurredAt });
result.OccurredAt.Should().Be(occurredAt, "OccurredAt debe ser el UTC insertado");
// Argentina Standard Time = UTC-3 (sin DST desde 2009). Offset fijo = -03:00.
result.OccurredAtLocal.Offset.TotalHours.Should().Be(-3,
"OccurredAtLocal debe tener offset -03:00 (Argentina Standard Time)");
// 2026-05-01 01:30 UTC → 2026-04-30 22:30 ART
result.OccurredAtLocal.DateTime.Date.Should().Be(new DateTime(2026, 4, 30),
"La fecha local Argentina debe ser 2026-04-30 (día anterior al UTC)");
result.OccurredAtLocal.Hour.Should().Be(22, "La hora local Argentina debe ser 22");
result.OccurredAtLocal.Minute.Should().Be(30, "Los minutos deben ser 30");
}
// ── REQ-DB-VIEWS-004 — Idempotencia ──────────────────────────────────────
[Fact]
public async Task V015_EsIdempotente()
{
await using var conn = new SqlConnection(ConnectionString);
await conn.OpenAsync();
// Re-ejecutar el DDL idempotente de V015 por segunda vez — NO debe fallar.
// La vista ya existe (creada en EnsureV015SchemaAsync), el guard IF OBJECT_ID IS NULL
// debe cortocircuitar sin error.
await conn.ExecuteAsync("""
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
""");
await conn.ExecuteAsync("""
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
""");
// Ambas vistas deben seguir existiendo.
var auditExists = await conn.ExecuteScalarAsync<int?>("""
SELECT OBJECT_ID('dbo.v_AuditEvent_Local', 'V')
""");
var secExists = await conn.ExecuteScalarAsync<int?>("""
SELECT OBJECT_ID('dbo.v_SecurityEvent_Local', 'V')
""");
auditExists.Should().NotBeNull("v_AuditEvent_Local debe seguir existiendo tras re-ejecución idempotente");
secExists.Should().NotBeNull("v_SecurityEvent_Local debe seguir existiendo tras re-ejecución idempotente");
}
// ── REQ-DB-VIEWS-005 — Rollback ───────────────────────────────────────────
[Fact]
public async Task V015_ROLLBACK_EliminaVistas()
{
await using var conn = new SqlConnection(ConnectionString);
await conn.OpenAsync();
// Apply rollback DDL (simula V015_ROLLBACK.sql).
await conn.ExecuteAsync("""
IF OBJECT_ID('dbo.v_AuditEvent_Local', 'V') IS NOT NULL
DROP VIEW dbo.v_AuditEvent_Local;
""");
await conn.ExecuteAsync("""
IF OBJECT_ID('dbo.v_SecurityEvent_Local', 'V') IS NOT NULL
DROP VIEW dbo.v_SecurityEvent_Local;
""");
var auditExists = await conn.ExecuteScalarAsync<int?>("""
SELECT OBJECT_ID('dbo.v_AuditEvent_Local', 'V')
""");
var secExists = await conn.ExecuteScalarAsync<int?>("""
SELECT OBJECT_ID('dbo.v_SecurityEvent_Local', 'V')
""");
auditExists.Should().BeNull("v_AuditEvent_Local debe ser NULL tras el rollback");
secExists.Should().BeNull("v_SecurityEvent_Local debe ser NULL tras el rollback");
// IMPORTANT: Re-create views after rollback test so remaining tests still pass.
// (This test tears down — re-apply V015 DDL to restore the fixture state.)
await conn.ExecuteAsync("""
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
""");
await conn.ExecuteAsync("""
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
""");
}
}