test(udt-011): V015 migration tests for timezone views (Red)
This commit is contained in:
262
tests/SIGCM2.Api.Tests/Admin/V015MigrationTests.cs
Normal file
262
tests/SIGCM2.Api.Tests/Admin/V015MigrationTests.cs
Normal 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
|
||||
""");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user