using System.Collections.Concurrent;
using System.Reflection;
using System.Runtime.CompilerServices;
using Akka.Actor;
using Akka.TestKit.Xunit2;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using NSubstitute;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
using ZB.MOM.WW.ScadaBridge.ManagementService;
using ZB.MOM.WW.ScadaBridge.Security;
namespace ZB.MOM.WW.ScadaBridge.ManagementService.Tests;
///
/// arch-review UA2: dispatch-coverage guard. Every command type registered in
/// must be reachable by a real dispatch arm
/// in . A command with no arm falls through to the
/// registry's default case, which throws
/// ("Unknown command type"). This test proves that signature never occurs for any
/// HTTP-registered command, so a newly-added command
/// that someone forgot to wire up fails CI here rather than at runtime.
///
///
/// Mechanism: for every registered command type we send a
/// instance (all fields default —
/// we never execute the handler for real) inside an envelope carrying ALL roles, so
/// the authorization gate always passes. The actor runs on a service provider whose
/// scoped passes through configuration transactions
/// and throws a sentinel exception for handler dependencies, so
/// any handler that touches the provider faults with the sentinel (a pass — the arm
/// exists). Only a command with NO arm faults with .
/// A recording logger captures the exception maps in
/// its fault path, keyed by correlation id; commands that answer without ever
/// touching the provider (or throw some other fault) simply are not
/// and therefore pass.
///
public class DispatchCoverageTests : TestKit, IDisposable
{
private static IEnumerable RegisteredCommandTypes() =>
typeof(ManagementEnvelope).Assembly.GetTypes()
.Where(t => t.Namespace == typeof(ManagementEnvelope).Namespace
&& t.Name.EndsWith("Command", StringComparison.Ordinal)
&& !t.IsAbstract
&& ManagementCommandRegistry.Resolve(t.Name[..^"Command".Length]) == t);
void IDisposable.Dispose() => Shutdown();
[Fact]
public void EveryRegisteredCommand_ReachesADispatchArm()
{
var logger = new RecordingLogger();
var actor = CreateSentinelActor(logger, out var transaction);
var commandTypes = RegisteredCommandTypes()
.OrderBy(t => t.Name, StringComparer.Ordinal)
.ToList();
var noDispatchArm = new List();
foreach (var type in commandTypes)
{
var command = RuntimeHelpers.GetUninitializedObject(type);
var correlationId = Guid.NewGuid().ToString("N");
var envelope = new ManagementEnvelope(
new AuthenticatedUser("dispatch-probe", "Dispatch Probe", Roles.All, Array.Empty()),
command, correlationId);
actor.Tell(envelope);
// Barrier: every command produces exactly one response (Success / Error /
// Unauthorized). By the time it arrives, MapFault has already logged any
// fault, so the recording logger is populated for this correlation id.
ExpectMsg