feat(db): V010 audit infrastructure + temporal tables
Applied to SIGCM2 (dev) and SIGCM2_Test.
V010__audit_infrastructure.sql (idempotent, ~280 LoC):
- Filegroups AUDIT_HOT + AUDIT_COLD with physical files (per-DB logical names
via DB_NAME() prefix to avoid collision in dev/test).
- pf/ps_AuditEvent_Monthly + pf/ps_SecurityEvent_Monthly (RANGE RIGHT,
DATETIME2(3), 14 boundaries 2026-01..2027-02 → 15 partitions). Job extends
forward monthly in B11.
- dbo.AuditEvent (partitioned, clustered PK on OccurredAt+Id) + 4 indexes
(Actor/Target/Action/Correlation) with PAGE compression.
- dbo.SecurityEvent (partitioned) + 3 indexes (Actor/Action_Result/Ip_Failure).
- CHECK constraints: Action LIKE '%.%', ISJSON(Metadata), Result IN (success|failure).
- SYSTEM_VERSIONING ON in Usuario/Rol/Permiso/RolPermiso with 10 YEARS retention +
PAGE compression in history tables.
- No hard FK on ActorUserId → Usuario.Id (soft FK — audit must survive user deletion).
V010_ROLLBACK.sql: emergency reversal (WARNING: destroys all audit history).
database/README.md: migration order + V010 prod-apply notes.
tests/SIGCM2.TestSupport/SqlTestFixture.cs:
- EnsureV010SchemaAsync() validates audit infra is applied (fails fast with
clear message if not — migration itself requires ALTER DATABASE privileges
and is applied manually via sqlcmd).
- Respawn TablesToIgnore extended with *_History (engine rejects direct DELETE
on system-versioned history tables).
tests/SIGCM2.Api.Tests/Audit/V010MigrationTests.cs — 5 smoke tests:
- AuditEvent insert+roundtrip with CorrelationId.
- CK_AuditEvent_Action rejects Action without '.'.
- CK_AuditEvent_Metadata rejects non-JSON.
- CK_SecurityEvent_Result rejects invalid Result.
- Usuario SYSTEM_VERSIONING: temporal query FOR SYSTEM_TIME AS OF returns
pre-update state + Usuario_History populated.
Suite: 130/130 passing (previous 124 + spike B0 + 5 new B1). No regressions.
Refs: sdd/udt-010-auditoria-trazabilidad/{spec#REQ-AUD-1,2, #REQ-SEC-1,
design#D-4, tasks}
This commit is contained in:
@@ -32,16 +32,25 @@ public sealed class SqlTestFixture : IAsyncLifetime
|
||||
// V009: update PermisosJson DEFAULT constraint and migrate legacy rows
|
||||
await EnsureV009SchemaAsync();
|
||||
|
||||
// V010 (UDT-010): verify audit infrastructure + temporal tables are active.
|
||||
// Applied manually via: sqlcmd ... -i database/migrations/V010__audit_infrastructure.sql
|
||||
await EnsureV010SchemaAsync();
|
||||
|
||||
_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.
|
||||
// Permiso and RolPermiso are seeded by V005/V006 — never wipe or integration tests lose the permission catalog.
|
||||
// *_History tables: UDT-010 system-versioned — Respawn cannot DELETE them directly (engine rejects).
|
||||
TablesToIgnore =
|
||||
[
|
||||
new Respawn.Graph.Table("dbo", "Rol"),
|
||||
new Respawn.Graph.Table("dbo", "Permiso"),
|
||||
new Respawn.Graph.Table("dbo", "RolPermiso"),
|
||||
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"),
|
||||
]
|
||||
});
|
||||
|
||||
@@ -232,6 +241,29 @@ public sealed class SqlTestFixture : IAsyncLifetime
|
||||
await _connection.ExecuteAsync(sql);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// UDT-010 (V010): verifies that the audit infrastructure is present.
|
||||
/// Does NOT re-apply the migration (the ALTER DATABASE ADD FILEGROUP/FILE + partition
|
||||
/// function/scheme creation requires the full script). If missing, fails with a clear
|
||||
/// message pointing to the migration script.
|
||||
/// </summary>
|
||||
private async Task EnsureV010SchemaAsync()
|
||||
{
|
||||
const string check = """
|
||||
SELECT
|
||||
CAST(CASE WHEN OBJECT_ID('dbo.AuditEvent','U') IS NULL THEN 0 ELSE 1 END AS BIT) AS HasAuditEvent,
|
||||
CAST(CASE WHEN OBJECT_ID('dbo.SecurityEvent','U') IS NULL THEN 0 ELSE 1 END AS BIT) AS HasSecurityEvent,
|
||||
CAST(CASE WHEN EXISTS (SELECT 1 FROM sys.tables WHERE object_id = OBJECT_ID('dbo.Usuario') AND temporal_type = 2) THEN 1 ELSE 0 END AS BIT) AS UsuarioVersioned
|
||||
""";
|
||||
var result = await _connection.QuerySingleAsync<(bool HasAuditEvent, bool HasSecurityEvent, bool UsuarioVersioned)>(check);
|
||||
if (!result.HasAuditEvent || !result.HasSecurityEvent || !result.UsuarioVersioned)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"V010 audit infrastructure is not applied in the test database. " +
|
||||
"Run: sqlcmd -S <server> -d SIGCM2_Test -U <user> -P <pass> -i database/migrations/V010__audit_infrastructure.sql");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies V009 schema changes idempotently to the test database.
|
||||
/// Mirrors V009__activate_permisos_overrides.sql.
|
||||
|
||||
Reference in New Issue
Block a user