fix(management-service): resolve ManagementService-004,006,007,013 — PipeTo dispatch, JsonDocument disposal, unified serialization, endpoint tests; re-triage MS-009

This commit is contained in:
Joseph Doherty
2026-05-16 21:22:01 -04:00
parent da955042aa
commit 57679d49f2
5 changed files with 340 additions and 78 deletions

View File

@@ -8,7 +8,7 @@
| Last reviewed | 2026-05-16 |
| Reviewer | claude-agent |
| Commit reviewed | `9c60592` |
| Open findings | 10 |
| Open findings | 5 |
## Summary
@@ -171,7 +171,7 @@ tests: `IsInstanceAccessAllowed_SiteScopedUser_OutOfScopeInstance_Denied`,
|--|--|
| Severity | Medium |
| Category | Akka.NET conventions |
| Status | Open |
| Status | Resolved |
| Location | `src/ScadaLink.ManagementService/ManagementActor.cs:61` |
**Description**
@@ -196,7 +196,14 @@ that explicit with a router/dispatcher rather than ad-hoc `Task.Run`.
**Resolution**
_Unresolved._
Resolved 2026-05-16 (commit pending). Confirmed: `HandleEnvelope` ran every command via
`Task.Run` and replied from inside the continuation, contrary to the project's PipeTo
convention. Replaced it with a `ProcessCommand` method returning a `Task<object>` and
`PipeTo(sender, success, failure)`; faults are now mapped uniformly in a `MapFault` failure
continuation (`SiteScopeViolationException` -> `ManagementUnauthorized`, otherwise
`ManagementError`), which also unwraps `AggregateException`. Regression test:
`UnknownCommandType_FaultMappedToManagementError`. Existing success/error/unauthorized
mapping tests confirm behaviour is preserved.
### ManagementService-005 — ManagementActor declares no supervision strategy
@@ -231,7 +238,7 @@ _Unresolved._
|--|--|
| Severity | Medium |
| Category | Performance & resource management |
| Status | Open |
| Status | Resolved |
| Location | `src/ScadaLink.ManagementService/ManagementEndpoints.cs:83`, `:112` |
**Description**
@@ -250,7 +257,16 @@ object, or restructure so the fallback path does not parse a throwaway document.
**Resolution**
_Unresolved._
Resolved 2026-05-16 (commit pending). Confirmed: the request `JsonDocument` was never
disposed and the empty-payload path allocated a second throwaway `JsonDocument`. Extracted
request parsing into a testable `ManagementEndpoints.ParseCommand` helper that wraps the
document in `using`; the missing-payload case now deserializes from the `"{}"` literal
string rather than parsing a throwaway document. Regression tests:
`ParseCommand_WithExplicitPayload_DeserializesIntoCommandType`,
`ParseCommand_WithMissingPayload_DeserializesParameterlessCommand`,
`ParseCommand_WithInvalidJson_ReturnsFailure`,
`ParseCommand_WithMissingCommandField_ReturnsFailure`,
`ParseCommand_WithUnknownCommand_ReturnsFailure`.
### ManagementService-007 — Inconsistent and cycle-prone serialization of repository entities
@@ -258,7 +274,7 @@ _Unresolved._
|--|--|
| Severity | Medium |
| Category | Code organization & conventions |
| Status | Open |
| Status | Resolved |
| Location | `src/ScadaLink.ManagementService/ManagementActor.cs:67`; `src/ScadaLink.ManagementService/ManagementEndpoints.cs:113` |
**Description**
@@ -281,7 +297,14 @@ correctly.
**Resolution**
_Unresolved._
Resolved 2026-05-16 (commit pending). Confirmed: the actor serialized results with
`Newtonsoft.Json` (not even a direct package reference) while the HTTP endpoint uses
`System.Text.Json`. Standardised the actor on `System.Text.Json` via a new
`ManagementActor.SerializeResult` helper using a shared `JsonSerializerOptions` with
`ReferenceHandler.IgnoreCycles` (cycle-safe for EF entity graphs) and camelCase naming
(matches the CLI's case-insensitive deserializer). Removed the `Newtonsoft.Json` import.
Regression tests: `SerializeResult_WithCyclicGraph_DoesNotThrow`,
`SerializeResult_UsesCamelCasePropertyNames`.
### ManagementService-008 — HandleResolveRoles constructs RoleMapper manually instead of via DI
@@ -312,9 +335,9 @@ _Unresolved._
| | |
|--|--|
| Severity | Medium |
| Category | Error handling & resilience |
| Status | Open |
| Severity | Low — re-triaged from Medium; the claimed audit gap does not exist (see Description), leaving only an undocumented-convention issue. |
| Category | Code organization & conventions |
| Status | Resolved |
| Location | `src/ScadaLink.ManagementService/ManagementActor.cs:357`, `:1134`, `:1085`, `:526`, `:1275` |
**Description**
@@ -325,21 +348,36 @@ external-system/notification/security/area mutations), but the handlers that del
domain service do **not**`HandleCreateTemplate`/`HandleUpdateTemplate`/`HandleDeleteTemplate`,
all template-member handlers (`HandleAddAttribute` ... `HandleDeleteComposition`), template-folder
handlers, shared-script handlers, `HandleDeployArtifacts`, `HandleDeployInstance`,
`HandleEnableInstance`/`Disable`/`Delete`, and the instance-binding/override handlers. This is
correct only if every one of those services performs its own audit logging internally; the
mixed pattern makes that impossible to verify by reading this module and creates a real risk
of silent audit gaps for template authoring and deployment operations.
`HandleEnableInstance`/`Disable`/`Delete`, and the instance-binding/override handlers.
**Re-triage (2026-05-16):** the original finding claimed this "creates a real risk of silent
audit gaps for template authoring and deployment operations." That claim was verified against
the actual sources and is **false**. Every domain service the delegating handlers call —
`TemplateService`, `SharedScriptService`, `InstanceService`, `AreaService`, `SiteService`,
`TemplateFolderService`, `DeploymentService`, `ArtifactDeploymentService` — injects
`IAuditService` and calls `LogAsync` on every mutation (`grep` confirms an `_auditService.LogAsync`
call after each `Create`/`Update`/`Delete` in `TemplateService.cs`, `DeploymentService.cs`,
`ArtifactDeploymentService.cs`, etc.). There is therefore no audit gap; if anything, adding
explicit `AuditAsync` to a delegating handler would *double-log*. The genuine issue is purely
organizational: the two-layer split (actor audits repo-direct mutations, services audit their
own) was undocumented, which is what made it look risky. This is a Low-severity
code-organization issue, not a Medium error-handling/resilience defect.
**Recommendation**
Decide on one layer that owns auditing. Either route all mutations through services that audit
internally (and remove the explicit `AuditAsync` calls here), or audit uniformly in the actor
after every successful mutation. Document the chosen contract so the inconsistency cannot
recur, and confirm template/deployment services actually audit.
Document the chosen contract so the split cannot be misread as a gap. (The original
alternative — moving all auditing into the actor — would require un-auditing eight services
and is not warranted given they already audit correctly.)
**Resolution**
_Unresolved._
Resolved 2026-05-16 (commit pending). Re-triaged to Low / Code organization after verifying
all eight delegated-to services audit internally — no audit gap exists. Documented the
two-layer audit contract in an XML `<remarks>` block on `ManagementActor.AuditAsync`:
repository-direct mutations call `AuditAsync`; service-delegating handlers must not, because
the services own auditing and a duplicate call would double-log. No behavioural change, so
no new regression test; existing `CreateInstanceCommand_WithDeploymentRole_ReturnsSuccess`
covers the explicit-audit path.
### ManagementService-010 — ManagementServiceOptions.CommandTimeout is defined but never used
@@ -433,7 +471,7 @@ _Unresolved._
|--|--|
| Severity | Medium |
| Category | Testing coverage |
| Status | Open |
| Status | Resolved |
| Location | `tests/ScadaLink.ManagementService.Tests/ManagementActorTests.cs:1` |
**Description**
@@ -457,4 +495,14 @@ malformed bodies, unknown commands, and the 200/400/403/401/504 mappings.
**Resolution**
_Unresolved._
Resolved 2026-05-16 (commit pending). The site-scope and `DebugStreamHub` coverage gaps
were closed by the resolution of findings 001/002/003 (the `ScopedEnvelope` helper plus the
`*_OutOfScopeForSiteScopedUser_ReturnsUnauthorized` tests and `DebugStreamHubTests`). The
remaining HTTP-endpoint gap is now covered by a new `ManagementEndpointsTests.cs` exercising
`ManagementEndpoints.ParseCommand` — command deserialization, malformed JSON, missing
`command` field, and unknown commands. Full `WebApplicationFactory` auth-flow tests were
deliberately not added: `ManagementEndpoints` depends on `LdapAuthService` and live LDAP
infrastructure, so the testable command-parsing/dispatch logic was extracted into the pure
`ParseCommand` helper and covered instead. Tests: `ParseCommand_*` (5),
`SerializeResult_*` (2), `UnknownCommandType_FaultMappedToManagementError`, plus the
pre-existing site-scope and DebugStreamHub suites. `dotnet test` -> 48 passed.