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

@@ -668,4 +668,66 @@ public class ManagementActorTests : TestKit, IDisposable
var response = ExpectMsg<ManagementUnauthorized>(TimeSpan.FromSeconds(5));
Assert.Equal(envelope.CorrelationId, response.CorrelationId);
}
// ========================================================================
// Serialization (finding ManagementService-007)
//
// Command results are serialized with System.Text.Json configured with
// ReferenceHandler.IgnoreCycles, so an entity graph with a bidirectional
// navigation property does not throw. Property names are camelCase, which
// the CLI's case-insensitive deserializer accepts.
// ========================================================================
private sealed class CyclicNode
{
public string Name { get; set; } = "";
public CyclicNode? Parent { get; set; }
public List<CyclicNode> Children { get; set; } = new();
}
[Fact]
public void SerializeResult_WithCyclicGraph_DoesNotThrow()
{
var parent = new CyclicNode { Name = "Parent" };
var child = new CyclicNode { Name = "Child", Parent = parent };
parent.Children.Add(child); // parent <-> child cycle
var json = ManagementActor.SerializeResult(parent);
Assert.Contains("Parent", json);
Assert.Contains("Child", json);
}
[Fact]
public void SerializeResult_UsesCamelCasePropertyNames()
{
var json = ManagementActor.SerializeResult(new CyclicNode { Name = "X" });
Assert.Contains("\"name\"", json);
Assert.DoesNotContain("\"Name\"", json);
}
// ========================================================================
// PipeTo fault mapping (finding ManagementService-004)
//
// Command processing is piped back via PipeTo; a fault raised inside
// DispatchCommand must be mapped to ManagementError by the failure
// continuation rather than escaping or being silently dropped.
// ========================================================================
[Fact]
public void UnknownCommandType_FaultMappedToManagementError()
{
// ManagementEnvelope.Command is typed object; an unrecognised payload
// makes DispatchCommand throw NotSupportedException. The PipeTo failure
// continuation must map it to ManagementError.
var actor = CreateActor();
var envelope = Envelope("not-a-command");
actor.Tell(envelope);
var response = ExpectMsg<ManagementError>(TimeSpan.FromSeconds(5));
Assert.Equal(envelope.CorrelationId, response.CorrelationId);
Assert.Equal("COMMAND_FAILED", response.ErrorCode);
}
}

View File

@@ -0,0 +1,65 @@
using ScadaLink.Commons.Messages.Management;
using ScadaLink.ManagementService;
namespace ScadaLink.ManagementService.Tests;
/// <summary>
/// Tests for <see cref="ManagementEndpoints"/> request-body parsing
/// (findings ManagementService-006 / -013).
/// </summary>
public class ManagementEndpointsTests
{
[Fact]
public void ParseCommand_WithExplicitPayload_DeserializesIntoCommandType()
{
var json = """{ "command": "CreateSite", "payload": { "name": "Site1", "siteIdentifier": "SITE1", "description": "Desc" } }""";
var result = ManagementEndpoints.ParseCommand(json);
Assert.True(result.Success);
var command = Assert.IsType<CreateSiteCommand>(result.Command);
Assert.Equal("Site1", command.Name);
Assert.Equal("SITE1", command.SiteIdentifier);
Assert.Equal("Desc", command.Description);
}
[Fact]
public void ParseCommand_WithMissingPayload_DeserializesParameterlessCommand()
{
// No "payload" field at all -- the fallback must not allocate a throwaway
// JsonDocument and must still produce a valid parameterless command.
var json = """{ "command": "ListTemplates" }""";
var result = ManagementEndpoints.ParseCommand(json);
Assert.True(result.Success);
Assert.IsType<ListTemplatesCommand>(result.Command);
}
[Fact]
public void ParseCommand_WithInvalidJson_ReturnsFailure()
{
var result = ManagementEndpoints.ParseCommand("{ not json");
Assert.False(result.Success);
Assert.Equal("BAD_REQUEST", result.ErrorCode);
}
[Fact]
public void ParseCommand_WithMissingCommandField_ReturnsFailure()
{
var result = ManagementEndpoints.ParseCommand("""{ "payload": {} }""");
Assert.False(result.Success);
Assert.Equal("BAD_REQUEST", result.ErrorCode);
}
[Fact]
public void ParseCommand_WithUnknownCommand_ReturnsFailure()
{
var result = ManagementEndpoints.ParseCommand("""{ "command": "NoSuchCommand" }""");
Assert.False(result.Success);
Assert.Equal("BAD_REQUEST", result.ErrorCode);
}
}