fix(configuration-database): resolve ConfigurationDatabase-005,006,008,009,010,011 — bounded gRPC columns, split queries, CSV-parse logging, null guards, coverage

This commit is contained in:
Joseph Doherty
2026-05-16 22:14:23 -04:00
parent 25a05af05d
commit 7d1cc5cbb4
17 changed files with 2188 additions and 25 deletions

View File

@@ -8,7 +8,7 @@
| Last reviewed | 2026-05-16 |
| Reviewer | claude-agent |
| Commit reviewed | `9c60592` |
| Open findings | 6 |
| Open findings | 0 |
## Summary
@@ -261,7 +261,7 @@ follow-up. The code fix in this module is complete.
|--|--|
| Severity | Low |
| Category | Design-document adherence |
| Status | Open |
| Status | Resolved |
| Location | `src/ScadaLink.ConfigurationDatabase/Configurations/AuditConfiguration.cs:11` (entity `src/ScadaLink.Commons/Entities/Audit/AuditLogEntry.cs`) |
**Description**
@@ -282,7 +282,20 @@ Resolve the discrepancy in one direction.
**Resolution**
_Unresolved._
Resolved 2026-05-16 (commit pending). Root cause confirmed against source: the
`AuditLogEntry` entity declares `int Id`, while the design doc's Audit Entry Schema
table said `Long / GUID`. The entity lives in `ScadaLink.Commons`
(`src/ScadaLink.Commons/Entities/Audit/AuditLogEntry.cs`), which is outside this
module's editable scope, so the discrepancy was resolved by aligning the design doc to
the code — the recommendation's second option. The schema table now records `Id` as
`int (identity)` with an explicit justification: a 32-bit identity matches the key type
of every other entity in the schema (uniform repository/query code), and at a sustained
100 rows/second the `int` range is not exhausted for roughly 680 years, so the
indefinite-retention policy poses no realistic overflow risk; if a future deployment
ever approaches the limit the column can be widened to `bigint` via a migration without
a schema redesign. No regression test is meaningful for a documentation alignment; the
existing `AuditConfiguration` (`HasKey(a => a.Id)`) and the audit repository tests
already exercise the `int` key end to end.
### ConfigurationDatabase-006 — `Site.GrpcNodeAAddress` / `GrpcNodeBAddress` columns are unbounded
@@ -290,7 +303,7 @@ _Unresolved._
|--|--|
| Severity | Low |
| Category | Code organization & conventions |
| Status | Open |
| Status | Resolved |
| Location | `src/ScadaLink.ConfigurationDatabase/Configurations/SiteConfiguration.cs:24-25` |
**Description**
@@ -310,7 +323,19 @@ generate a migration to alter the column types.
**Resolution**
_Unresolved._
Resolved 2026-05-16 (commit pending). Root cause confirmed against source:
`SiteConfiguration` configured `NodeAAddress`/`NodeBAddress` with `HasMaxLength(500)` but
left `GrpcNodeAAddress`/`GrpcNodeBAddress` unconfigured, so EF mapped them to
`nvarchar(max)` — inconsistent with the sibling columns and non-indexable. Applied the
recommendation: added `builder.Property(s => s.GrpcNodeAAddress).HasMaxLength(500)` and
the same for `GrpcNodeBAddress`. Generated migration
`20260517020720_BoundGrpcNodeAddressLength` altering both columns from `nvarchar(max)`
to `nvarchar(500)` (the model snapshot was updated to match). Regression tests added in
`SchemaConfigurationTests.cs`:
`GrpcNodeAddressColumns_AreLengthBoundedTo500` (theory over both columns, asserting the
EF model metadata reports `MaxLength == 500`) and
`GrpcNodeAddressColumns_MatchSiblingNodeAddressBounds` (asserting the gRPC columns share
the bound of the `NodeAAddress`/`NodeBAddress` siblings).
### ConfigurationDatabase-007 — `AuditService` does not handle JSON-serialization failure of arbitrary `afterState`
@@ -367,7 +392,7 @@ added in `AuditServiceTests.cs`:
|--|--|
| Severity | Low |
| Category | Correctness & logic bugs |
| Status | Open |
| Status | Resolved |
| Location | `src/ScadaLink.ConfigurationDatabase/Repositories/InboundApiRepository.cs:46-58` |
**Description**
@@ -390,7 +415,25 @@ gives referential integrity and correct cascade behaviour when an API key is del
**Resolution**
_Unresolved._
Resolved 2026-05-16 (commit pending). Root cause confirmed against source:
`GetApprovedKeysForMethodAsync` mapped each CSV token with
`int.TryParse(...) ? id : -1` then filtered `id > 0`, so any unparseable (or
non-positive) token was discarded with no signal — a corrupt `ApprovedApiKeyIds` value
silently approves fewer keys than intended, an authorization-relevant outcome.
Applied the recommendation's short-term fix: the parse loop was rewritten to log a
warning for every token that fails to parse to a positive integer, naming the method id
and the offending token, so corruption is observable in logs. Valid ids still resolve
normally. `InboundApiRepository` gained an optional `ILogger<InboundApiRepository>`
constructor parameter (defaulting to `NullLogger`, matching the `MigrationHelper`
pattern) and the project now references `Microsoft.Extensions.Logging.Abstractions`. The
longer-term join-table redesign would change the `ApiMethod` entity / schema and the
`IInboundApiRepository` contract (Commons, out of this module's scope) and is left as a
future schema-design item. Regression tests added in `InboundApiRepositoryTests.cs`:
`GetApprovedKeysForMethod_WithMalformedCsvToken_LogsWarningAndDropsToken`,
`GetApprovedKeysForMethod_WithValidCsv_ReturnsAllKeys`, and
`GetApprovedKeysForMethod_WithNullOrEmptyCsv_ReturnsEmptyWithoutWarning` (using a
capturing `ILogger` to assert the warning is emitted only on malformed input).
### ConfigurationDatabase-009 — Multi-collection eager loads issue cartesian-product queries
@@ -398,7 +441,7 @@ _Unresolved._
|--|--|
| Severity | Low |
| Category | Performance & resource management |
| Status | Open |
| Status | Resolved |
| Location | `src/ScadaLink.ConfigurationDatabase/Repositories/TemplateEngineRepository.cs:43-51,53-61`, `src/ScadaLink.ConfigurationDatabase/Repositories/CentralUiRepository.cs:45-55` |
**Description**
@@ -421,7 +464,24 @@ cartesian explosion is avoided.
**Resolution**
_Unresolved._
Resolved 2026-05-16 (commit pending). Root cause confirmed against source:
`GetAllTemplatesAsync` and `GetTemplatesComposingAsync` (`TemplateEngineRepository`) and
`GetTemplateTreeAsync` (`CentralUiRepository`) each `Include` three-to-four sibling
collections in a single query, producing a cartesian-product join. The same shape was
also present in `GetTemplateByIdAsync`, `GetInstanceByIdAsync`, `GetAllInstancesAsync`,
`GetInstancesBySiteIdAsync`, and `GetInstanceByUniqueNameAsync`.
Applied the recommendation's per-query option: `.AsSplitQuery()` was added to every
multi-collection-include query in `TemplateEngineRepository` (eight call sites) and to
`GetTemplateTreeAsync` in `CentralUiRepository`, so each collection loads with its own
query and the cartesian explosion is avoided. Per-query `AsSplitQuery()` was preferred
over a global `UseQuerySplittingBehavior` so single-collection queries elsewhere keep
the cheaper single-query plan. Split queries change query *shape* only, not results;
regression tests added in `SchemaConfigurationTests.cs` pin that behaviour:
`GetAllTemplatesAsync_WithMultipleMembersPerCollection_LoadsAllWithoutDuplication`
(a template with 3 attributes, 2 alarms, 4 scripts must return exactly those counts —
not a 24-row cartesian product) and
`GetTemplateByIdAsync_WithMultipleMembers_LoadsAllCollections`.
### ConfigurationDatabase-010 — Several repositories and `InstanceLocator` lack direct test coverage
@@ -429,7 +489,7 @@ _Unresolved._
|--|--|
| Severity | Low |
| Category | Testing coverage |
| Status | Open |
| Status | Resolved |
| Location | `src/ScadaLink.ConfigurationDatabase/Repositories/TemplateEngineRepository.cs`, `Repositories/DeploymentManagerRepository.cs`, `Repositories/ExternalSystemRepository.cs`, `Repositories/InboundApiRepository.cs`, `Repositories/NotificationRepository.cs`, `Repositories/SiteRepository.cs`, `Services/InstanceLocator.cs` |
**Description**
@@ -453,7 +513,24 @@ and `InstanceLocator.GetSiteIdForInstanceAsync` for found/not-found cases.
**Resolution**
_Unresolved._
Resolved 2026-05-16 (commit pending). Direct repository/service tests were added using
the existing `SqliteTestHelper` pattern. `InboundApiRepositoryTests.cs` covers
`InboundApiRepository` (API-key/method CRUD round-trips and the
`GetApprovedKeysForMethodAsync` valid/malformed/empty-CSV cases — see CD-008).
`RepositoryCoverageTests.cs` adds `ExternalSystemRepositoryTests` (definition/method CRUD,
parent-filtered method query, database-connection delete), `NotificationRepositoryTests`
(notification-list-with-recipients and SMTP-configuration round-trips, list delete),
`SiteRepositoryTests` (site/identifier round-trip plus the stub-attach delete fallback
exercised for both `DeleteSiteAsync` and `DeleteDataConnectionAsync` by clearing the
ChangeTracker, and the site-filtered instance query), `DeploymentManagerRepositoryTests`
(deployment-record CRUD and `GetCurrentDeploymentStatusAsync` ordering, the stub-attach
`DeleteDeploymentRecordAsync` fallback, and `DeleteInstanceAsync`'s explicit
Restrict-FK deployment-record cleanup), and `InstanceLocatorTests`
(`GetSiteIdForInstanceAsync` for the found and not-found cases). `TemplateEngineRepository`
gained the CD-001 and CD-009 regression tests
(`TemplateEngineRepositoryTests.cs`, `SchemaConfigurationTests.cs`). A constructor
null-guard test was added for each of the five repositories/services covered, doubling
as the CD-011 regression guard. The full module suite is green.
### ConfigurationDatabase-011 — Inconsistent constructor null-guarding across repositories/services
@@ -461,7 +538,7 @@ _Unresolved._
|--|--|
| Severity | Low |
| Category | Code organization & conventions |
| Status | Open |
| Status | Resolved |
| Location | `src/ScadaLink.ConfigurationDatabase/Repositories/ExternalSystemRepository.cs:11-14`, `Repositories/InboundApiRepository.cs:11-14`, `Repositories/NotificationRepository.cs:11-14`, `Services/InstanceLocator.cs:13-16` |
**Description**
@@ -482,4 +559,14 @@ inconsistent constructors so all data-access types behave uniformly.
**Resolution**
_Unresolved._
Resolved 2026-05-16 (commit pending). Root cause confirmed against source:
`ExternalSystemRepository`, `InboundApiRepository`, `NotificationRepository`, and
`InstanceLocator` assigned the injected `ScadaLinkDbContext` directly with no null
guard, diverging from `SecurityRepository`/`CentralUiRepository`/`TemplateEngineRepository`/
`DeploymentManagerRepository`/`SiteRepository`/`AuditService`. Applied the recommendation:
all four constructors now use `context ?? throw new ArgumentNullException(nameof(context))`
(`InboundApiRepository`'s guard was added as part of its CD-008 constructor change), so
every data-access type behaves uniformly and a hand-constructed instance fails with an
informative exception at construction rather than a later `NullReferenceException`.
Regression: `Constructor_NullContext_Throws` tests were added for all four affected types
(`InboundApiRepositoryTests.cs`, `RepositoryCoverageTests.cs`).