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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user