Commit Graph

2 Commits

Author SHA1 Message Date
Joseph Doherty
ce9d6301e3 fix(configdb-tests): use Assert.SkipUnless + fast-fail connect timeout for MSSQL fixture (#23)
Reviewer of Bundle C (#23 M1) flagged two blockers in the
AddAuditLogTableMigration integration tests:

1. Tests used 'if (!await EnsureMigrationApplied()) return;' which made
   the xunit runner report them as Passed when the dev MSSQL container
   was absent — a CI false-positive risk. xunit 2.9.x does NOT ship the
   v3 Assert.Skip/SkipUnless/SkipWhen API surface (verified empirically
   against xunit.assert 2.9.3 — only v3.x exposes those static methods),
   so the canonical xunit-v2 equivalent is the Xunit.SkippableFact
   package. Replaced [Fact] with [SkippableFact] and the early-return
   pattern with 'Skip.IfNot(_fixture.Available, _fixture.SkipReason)' as
   the first statement of each of the 8 audit-log test methods. The
   runner now reports them as Skipped (not Passed) when MSSQL is down.

2. MsSqlMigrationFixture relied on SqlClient's 30s default connect
   timeout, so a no-container fixture construction hung ~30s. Added
   'Connect Timeout=3' to DefaultAdminConnectionString. Verified
   fail-fast under ~4s end-to-end with a bad host via env-var override.

Additional fixture cleanups:
- Migration is now applied once in the fixture constructor (was per-test
  via EnsureMigrationApplied for idempotency). Tests reach a fully-
  migrated database with no extra setup. Removed the now-unused
  EnsureMigrationApplied helper from the test class.
- Constructor narrowed its catch to SqlException + InvalidOperationException
  for the OpenAsync step (the only legitimate connect-failure surfaces);
  everything else (CREATE DATABASE, MigrateAsync) is treated as a hard
  fixture failure and bubbles up. Added a best-effort
  TryDropOrphanDatabase() pre-throw cleanup so partial construction
  cannot leak guid-suffixed databases.
- Stale doc comments referencing the (non-existent) xunit 2.9.x Skip
  shim removed; replaced with accurate notes about Xunit.SkippableFact.

Verified:
- dotnet build ScadaLink.slnx: clean (0 warnings, 0 errors).
- dotnet test ScadaLink.ConfigurationDatabase.Tests with MSSQL up:
  Passed 150 / Skipped 0 / Failed 0.
- Same suite with SCADALINK_MSSQL_TEST_CONN pointed at a closed port:
  the 8 AddAuditLogTableMigration tests report as Skipped (visible
  '[SKIP]' lines in runner output), total elapsed ~3s.

Files touched:
- Directory.Packages.props: added Xunit.SkippableFact 1.5.61.
- tests/ScadaLink.ConfigurationDatabase.Tests/ScadaLink.ConfigurationDatabase.Tests.csproj:
  added the SkippableFact PackageReference.
- tests/ScadaLink.ConfigurationDatabase.Tests/Migrations/MsSqlMigrationFixture.cs:
  Connect Timeout=3, constructor refactor, doc-comment fixes.
- tests/ScadaLink.ConfigurationDatabase.Tests/Migrations/AddAuditLogTableMigrationTests.cs:
  [SkippableFact] + Skip.IfNot pattern across all 8 tests.

Untouched (per reviewer guidance):
- Migration file (Bundle C main artifact unchanged).
- Bundle B reconciliation (composite PK + UX_AuditLog_EventId).
- SqlClient VersionOverride 6.1.1 in the test csproj.
- infra/* (separate uncommitted local edits remain in working tree).
2026-05-20 10:49:49 -04:00
Joseph Doherty
d9c99242a3 feat(configdb): add AuditLog migration with monthly partitioning and DB roles (#23)
Bundle C of the #23 M1 foundation. Creates the centralized AuditLog table
with the partition function, partition scheme, partition-aligned
non-clustered indexes, and the two access-control roles documented in
alog.md §4.

Schema:
- pf_AuditLog_Month: RANGE RIGHT, 24 monthly boundaries (Jan 2026 – Dec 2027).
- ps_AuditLog_Month: ALL TO ([PRIMARY]) — dev/test parity.
- dbo.AuditLog: created via raw SQL ON ps_AuditLog_Month(OccurredAtUtc).
  Composite clustered PK {EventId, OccurredAtUtc} (partition column must be
  part of the clustered key). 22 columns matching the EF AuditEvent model.
- 5 reconciliation/query non-clustered indexes from alog.md §4
  (Channel_Status_Occurred, CorrelationId filtered, OccurredAtUtc,
  Site_Occurred, Target_Occurred filtered) — all partition-aligned.
- UX_AuditLog_EventId: non-aligned UNIQUE on EventId alone (preserves
  InsertIfNotExistsAsync idempotency from M1-T8). Non-aligned because
  partition-aligned unique indexes require the partition column in the key,
  which would weaken to composite uniqueness; the purge story (M2/M3)
  rebuilds this index around partition switches.

Access control:
- scadalink_audit_writer: GRANT INSERT + GRANT SELECT, DENY UPDATE + DENY DELETE
  on AuditLog. The explicit DENY guarantees later db_datawriter membership
  cannot quietly re-enable mutation.
- scadalink_audit_purger: GRANT SELECT on AuditLog, GRANT ALTER on SCHEMA::dbo
  (enables ALTER PARTITION FUNCTION SWITCH and SWITCH PARTITION).

Both role definitions are idempotent (IF DATABASE_PRINCIPAL_ID IS NULL).
Down() drops in reverse dependency order with IF EXISTS guards.

Integration tests (tests/ScadaLink.ConfigurationDatabase.Tests/Migrations/):
- MsSqlMigrationFixture: connects to the running infra/mssql container (or
  the SCADALINK_MSSQL_TEST_CONN override), creates a unique per-fixture
  database, applies the migrations, drops the DB on dispose. Marks itself
  Available=false when MSSQL is unreachable so tests early-return cleanly
  on CI without the dev container.
- AddAuditLogTableMigrationTests: 8 tests covering table existence,
  partition function/scheme, partition-aligned PK, the 5 named indexes,
  both roles' grants, and a smoke test that a writer-role user receives
  SqlException with "permission" on UPDATE AuditLog.

ConfigurationDatabase tests: 142 passing -> 150 passing (8 new integration
tests). Full solution builds clean.

Package: tests project locally overrides Microsoft.Data.SqlClient to 6.1.1
(EF SqlServer 10.0.7 needs >= 6.1.1; central package version is pinned at
6.0.2 for the production ExternalSystemGateway).
2026-05-20 10:25:25 -04:00