using System; using System.Collections.Generic; using ZB.MOM.WW.MxGateway.Contracts.Proto; using ZB.MOM.WW.MxGateway.Worker.MxAccess; using ZB.MOM.WW.MxGateway.Worker.Sta; using ZB.MOM.WW.MxGateway.Worker.Tests.TestSupport; namespace ZB.MOM.WW.MxGateway.Worker.Tests.MxAccess; /// /// Verifies that the four new alarm values /// route through to a fake /// and that the resulting /// carries the expected payload. /// /// The data-side is constructed via a /// no-op factory because the executor only touches it for non-alarm /// command kinds — alarm dispatch never reaches the data session. /// public sealed class AlarmCommandExecutorTests { private const string SessionId = "S"; private const string CorrelationId = "C"; /// Verifies that the handler routes alarm subscriptions and returns ok. [Fact] public void SubscribeAlarms_WithHandler_RoutesToHandlerAndReturnsOk() { FakeAlarmHandler handler = new FakeAlarmHandler(); MxAccessCommandExecutor executor = NewExecutor(handler); StaCommand command = new StaCommand( SessionId, CorrelationId, new MxCommand { Kind = MxCommandKind.SubscribeAlarms, SubscribeAlarms = new SubscribeAlarmsCommand { SubscriptionExpression = @"\\HOST\Galaxy!Area", }, }); MxCommandReply reply = executor.Execute(command); Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code); Assert.Equal(@"\\HOST\Galaxy!Area", handler.LastSubscription); Assert.Equal(SessionId, handler.LastSessionId); } /// Verifies that subscription without handler returns invalid request. [Fact] public void SubscribeAlarms_WithoutHandler_ReturnsInvalidRequest() { MxAccessCommandExecutor executor = NewExecutor(alarmHandler: null); StaCommand command = new StaCommand( SessionId, CorrelationId, new MxCommand { Kind = MxCommandKind.SubscribeAlarms, SubscribeAlarms = new SubscribeAlarmsCommand { SubscriptionExpression = @"\\HOST\Galaxy!Area", }, }); MxCommandReply reply = executor.Execute(command); Assert.Equal(ProtocolStatusCode.InvalidRequest, reply.ProtocolStatus.Code); } /// Verifies that empty subscription expression returns invalid request. [Fact] public void SubscribeAlarms_WithEmptyExpression_ReturnsInvalidRequest() { MxAccessCommandExecutor executor = NewExecutor(new FakeAlarmHandler()); StaCommand command = new StaCommand( SessionId, CorrelationId, new MxCommand { Kind = MxCommandKind.SubscribeAlarms, SubscribeAlarms = new SubscribeAlarmsCommand { SubscriptionExpression = " ", }, }); MxCommandReply reply = executor.Execute(command); Assert.Equal(ProtocolStatusCode.InvalidRequest, reply.ProtocolStatus.Code); } /// Verifies that acknowledge routes native status into hresult and payload. [Fact] public void AcknowledgeAlarm_WithHandler_RoutesNativeStatusIntoHresultAndPayload() { FakeAlarmHandler handler = new FakeAlarmHandler { AcknowledgeReturn = 0 }; MxAccessCommandExecutor executor = NewExecutor(handler); Guid g = Guid.NewGuid(); StaCommand command = new StaCommand( SessionId, CorrelationId, new MxCommand { Kind = MxCommandKind.AcknowledgeAlarm, AcknowledgeAlarmCommand = new AcknowledgeAlarmCommand { AlarmGuid = g.ToString(), Comment = "ack", OperatorUser = "alice", OperatorNode = "WS", OperatorDomain = "CORP", OperatorFullName = "Alice S", }, }); MxCommandReply reply = executor.Execute(command); Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code); Assert.Equal(0, reply.Hresult); Assert.NotNull(reply.AcknowledgeAlarm); Assert.Equal(0, reply.AcknowledgeAlarm.NativeStatus); Assert.Equal(g, handler.LastAckGuid); Assert.Equal("alice", handler.LastAckOperatorName); } /// Verifies that invalid alarm GUID returns invalid request. [Fact] public void AcknowledgeAlarm_WithInvalidGuid_ReturnsInvalidRequest() { MxAccessCommandExecutor executor = NewExecutor(new FakeAlarmHandler()); StaCommand command = new StaCommand( SessionId, CorrelationId, new MxCommand { Kind = MxCommandKind.AcknowledgeAlarm, AcknowledgeAlarmCommand = new AcknowledgeAlarmCommand { AlarmGuid = "not-a-guid", }, }); MxCommandReply reply = executor.Execute(command); Assert.Equal(ProtocolStatusCode.InvalidRequest, reply.ProtocolStatus.Code); } /// Verifies that nonzero native status carries a diagnostic message. [Fact] public void AcknowledgeAlarm_WithNonzeroNativeStatus_CarriesDiagnostic() { FakeAlarmHandler handler = new FakeAlarmHandler { AcknowledgeReturn = -123 }; MxAccessCommandExecutor executor = NewExecutor(handler); StaCommand command = new StaCommand( SessionId, CorrelationId, new MxCommand { Kind = MxCommandKind.AcknowledgeAlarm, AcknowledgeAlarmCommand = new AcknowledgeAlarmCommand { AlarmGuid = Guid.NewGuid().ToString(), }, }); MxCommandReply reply = executor.Execute(command); Assert.Equal(-123, reply.Hresult); Assert.Contains("-123", reply.DiagnosticMessage); } /// Verifies that acknowledge by name routes tuple to handler. [Fact] public void AcknowledgeAlarmByName_WithHandler_RoutesTupleToHandler() { FakeAlarmHandler handler = new FakeAlarmHandler { AcknowledgeReturn = 0 }; MxAccessCommandExecutor executor = NewExecutor(handler); StaCommand command = new StaCommand( SessionId, CorrelationId, new MxCommand { Kind = MxCommandKind.AcknowledgeAlarmByName, AcknowledgeAlarmByNameCommand = new AcknowledgeAlarmByNameCommand { AlarmName = "TestMachine_001.TestAlarm001", ProviderName = "Galaxy", GroupName = "TestArea", Comment = "ack", OperatorUser = "alice", }, }); MxCommandReply reply = executor.Execute(command); Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code); Assert.NotNull(reply.AcknowledgeAlarm); Assert.Equal(0, reply.AcknowledgeAlarm.NativeStatus); Assert.NotNull(handler.LastAckByNameTuple); Assert.Equal("TestMachine_001.TestAlarm001", handler.LastAckByNameTuple!.Value.Name); Assert.Equal("Galaxy", handler.LastAckByNameTuple!.Value.Provider); Assert.Equal("TestArea", handler.LastAckByNameTuple!.Value.Group); Assert.Equal("alice", handler.LastAckOperatorName); } /// Verifies that empty alarm name returns invalid request. [Fact] public void AcknowledgeAlarmByName_WithEmptyName_ReturnsInvalidRequest() { MxAccessCommandExecutor executor = NewExecutor(new FakeAlarmHandler()); StaCommand command = new StaCommand( SessionId, CorrelationId, new MxCommand { Kind = MxCommandKind.AcknowledgeAlarmByName, AcknowledgeAlarmByNameCommand = new AcknowledgeAlarmByNameCommand { AlarmName = " ", ProviderName = "Galaxy", GroupName = "TestArea", }, }); MxCommandReply reply = executor.Execute(command); Assert.Equal(ProtocolStatusCode.InvalidRequest, reply.ProtocolStatus.Code); } /// Verifies that query active alarms returns payload with snapshots. [Fact] public void QueryActiveAlarms_WithHandler_ReturnsPayloadWithSnapshots() { FakeAlarmHandler handler = new FakeAlarmHandler { QueryResult = new[] { new ActiveAlarmSnapshot { AlarmFullReference = "Galaxy!A.T1" }, new ActiveAlarmSnapshot { AlarmFullReference = "Galaxy!A.T2" }, }, }; MxAccessCommandExecutor executor = NewExecutor(handler); StaCommand command = new StaCommand( SessionId, CorrelationId, new MxCommand { Kind = MxCommandKind.QueryActiveAlarms, QueryActiveAlarmsCommand = new QueryActiveAlarmsCommand { AlarmFilterPrefix = "Galaxy!A", }, }); MxCommandReply reply = executor.Execute(command); Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code); Assert.NotNull(reply.QueryActiveAlarms); Assert.Equal(2, reply.QueryActiveAlarms.Snapshots.Count); Assert.Equal("Galaxy!A", handler.LastFilterPrefix); } /// Verifies that unsubscribe routes to handler. [Fact] public void UnsubscribeAlarms_WithHandler_RoutesToHandler() { FakeAlarmHandler handler = new FakeAlarmHandler(); MxAccessCommandExecutor executor = NewExecutor(handler); StaCommand command = new StaCommand( SessionId, CorrelationId, new MxCommand { Kind = MxCommandKind.UnsubscribeAlarms, UnsubscribeAlarms = new UnsubscribeAlarmsCommand(), }); MxCommandReply reply = executor.Execute(command); Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code); Assert.True(handler.UnsubscribeCalled); } /// Verifies that unsubscribe without handler is an ok noop. [Fact] public void UnsubscribeAlarms_WithoutHandler_IsOkNoop() { MxAccessCommandExecutor executor = NewExecutor(alarmHandler: null); StaCommand command = new StaCommand( SessionId, CorrelationId, new MxCommand { Kind = MxCommandKind.UnsubscribeAlarms, UnsubscribeAlarms = new UnsubscribeAlarmsCommand(), }); MxCommandReply reply = executor.Execute(command); Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code); } /// Verifies that handler exception returns MXAccess failure status. [Fact] public void AcknowledgeAlarm_WhenHandlerThrows_ReturnsMxaccessFailure() { FakeAlarmHandler handler = new FakeAlarmHandler { AcknowledgeThrow = true }; MxAccessCommandExecutor executor = NewExecutor(handler); StaCommand command = new StaCommand( SessionId, CorrelationId, new MxCommand { Kind = MxCommandKind.AcknowledgeAlarm, AcknowledgeAlarmCommand = new AcknowledgeAlarmCommand { AlarmGuid = Guid.NewGuid().ToString(), }, }); MxCommandReply reply = executor.Execute(command); Assert.Equal(ProtocolStatusCode.MxaccessFailure, reply.ProtocolStatus.Code); Assert.Contains("simulated", reply.DiagnosticMessage); } private static MxAccessCommandExecutor NewExecutor(IAlarmCommandHandler? alarmHandler) { // Construct an executor with a no-op data session — we only exercise // the alarm switch arms, which never touch the data session. The // session is built through the internal MxAccessSession.CreateForTesting // factory (exposed via [assembly: InternalsVisibleTo("ZB.MOM.WW.MxGateway.Worker.Tests")] // on ZB.MOM.WW.MxGateway.Worker), so no reflection is needed. return new MxAccessCommandExecutor( session: MxAccessSession.CreateForTesting( mxAccessServer: new NoopMxAccessServer(), eventSink: new NoopEventSink()), variantConverter: new ZB.MOM.WW.MxGateway.Worker.Conversion.VariantConverter(), alarmCommandHandler: alarmHandler); } private sealed class FakeAlarmHandler : IAlarmCommandHandler { /// Gets the last subscription expression. public string? LastSubscription { get; private set; } /// Gets the last session ID. public string? LastSessionId { get; private set; } /// Gets a value indicating whether unsubscribe was called. public bool UnsubscribeCalled { get; private set; } /// Gets the last acknowledge alarm GUID. public Guid LastAckGuid { get; private set; } /// Gets the last acknowledge operator name. public string? LastAckOperatorName { get; private set; } /// Gets or sets the value returned by acknowledge. public int AcknowledgeReturn { get; set; } /// Gets or sets a value indicating whether acknowledge throws. public bool AcknowledgeThrow { get; set; } /// Gets or sets the query result snapshots. public IReadOnlyList QueryResult { get; set; } = Array.Empty(); /// Gets the last alarm filter prefix. public string? LastFilterPrefix { get; private set; } /// Records a subscription. /// The subscribe-alarms command. /// The session identifier. public void Subscribe(SubscribeAlarmsCommand command, string sessionId) { LastSubscription = command.SubscriptionExpression; LastSessionId = sessionId; } /// Records an unsubscribe request. public void Unsubscribe() { UnsubscribeCalled = true; } /// Records an acknowledge request. /// The alarm identifier. /// The acknowledge comment. /// The operator user name. /// The operator node name. /// The operator domain. /// The operator full name. public int Acknowledge( Guid alarmGuid, string comment, string operatorUser, string operatorNode, string operatorDomain, string operatorFullName) { LastAckGuid = alarmGuid; LastAckOperatorName = operatorUser; if (AcknowledgeThrow) { throw new InvalidOperationException("simulated alarm-handler failure"); } return AcknowledgeReturn; } /// Records an acknowledge by name request. /// The alarm name. /// The provider name. /// The group name. /// The acknowledge comment. /// The operator user name. /// The operator node name. /// The operator domain. /// The operator full name. public int AcknowledgeByName( string alarmName, string providerName, string groupName, string comment, string operatorUser, string operatorNode, string operatorDomain, string operatorFullName) { LastAckByNameTuple = (alarmName, providerName, groupName); LastAckOperatorName = operatorUser; return AcknowledgeReturn; } /// Gets the last acknowledge by name tuple. public (string Name, string Provider, string Group)? LastAckByNameTuple { get; private set; } /// Queries the active alarms with the given filter prefix. /// The alarm filter prefix for the query. public IReadOnlyList QueryActive(string? alarmFilterPrefix) { LastFilterPrefix = alarmFilterPrefix; return QueryResult; } /// Gets the number of poll calls. public int PollCount { get; private set; } /// Increments the poll count. public void PollOnce() { PollCount++; } /// public void Dispose() { } } }