using Serilog; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; using ZB.MOM.WW.OtOpcUa.Core.Scripting; using ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms; namespace ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms.Tests; [Trait("Category", "Unit")] public sealed class ScriptedAlarmSourceTests { private static async Task<(ScriptedAlarmEngine e, ScriptedAlarmSource s, FakeUpstream u)> BuildAsync() { var up = new FakeUpstream(); up.Set("Temp", 50); var logger = new LoggerConfiguration().CreateLogger(); var engine = new ScriptedAlarmEngine(up, new InMemoryAlarmStateStore(), new ScriptLoggerFactory(logger), logger); await engine.LoadAsync([ new ScriptedAlarmDefinition( "Plant/Line1::HighTemp", "Plant/Line1", "HighTemp", AlarmKind.LimitAlarm, AlarmSeverity.High, "Temp {Temp}C", """return (int)ctx.GetTag("Temp").Value > 100;"""), new ScriptedAlarmDefinition( "Plant/Line2::OtherAlarm", "Plant/Line2", "OtherAlarm", AlarmKind.AlarmCondition, AlarmSeverity.Low, "other", """return false;"""), ], CancellationToken.None); var source = new ScriptedAlarmSource(engine); return (engine, source, up); } [Fact] public async Task Subscribe_with_empty_filter_receives_every_alarm_emission() { var (engine, source, up) = await BuildAsync(); using var _e = engine; using var _s = source; var events = new List(); source.OnAlarmEvent += (_, e) => events.Add(e); var handle = await source.SubscribeAlarmsAsync([], TestContext.Current.CancellationToken); up.Push("Temp", 150); await Task.Delay(200); events.Count.ShouldBe(1); events[0].ConditionId.ShouldBe("Plant/Line1::HighTemp"); events[0].SourceNodeId.ShouldBe("Plant/Line1"); events[0].Severity.ShouldBe(AlarmSeverity.High); events[0].AlarmType.ShouldBe("LimitAlarm"); events[0].Message.ShouldBe("Temp 150C"); await source.UnsubscribeAlarmsAsync(handle, TestContext.Current.CancellationToken); } [Fact] public async Task Subscribe_with_equipment_prefix_filters_by_that_prefix() { var (engine, source, up) = await BuildAsync(); using var _e = engine; using var _s = source; var events = new List(); source.OnAlarmEvent += (_, e) => events.Add(e); // Subscribe only to Line1 alarms. var handle = await source.SubscribeAlarmsAsync(["Plant/Line1"], TestContext.Current.CancellationToken); up.Push("Temp", 150); await Task.Delay(200); events.Count.ShouldBe(1); events[0].SourceNodeId.ShouldBe("Plant/Line1"); await source.UnsubscribeAlarmsAsync(handle, TestContext.Current.CancellationToken); } [Fact] public async Task Unsubscribe_stops_further_events() { var (engine, source, up) = await BuildAsync(); using var _e = engine; using var _s = source; var events = new List(); source.OnAlarmEvent += (_, e) => events.Add(e); var handle = await source.SubscribeAlarmsAsync([], TestContext.Current.CancellationToken); await source.UnsubscribeAlarmsAsync(handle, TestContext.Current.CancellationToken); up.Push("Temp", 150); await Task.Delay(200); events.Count.ShouldBe(0); } [Fact] public async Task AcknowledgeAsync_routes_to_engine_with_default_user() { var (engine, source, up) = await BuildAsync(); using var _e = engine; using var _s = source; up.Push("Temp", 150); await Task.Delay(200); engine.GetState("Plant/Line1::HighTemp")!.Acked.ShouldBe(AlarmAckedState.Unacknowledged); await source.AcknowledgeAsync([new AlarmAcknowledgeRequest( "Plant/Line1", "Plant/Line1::HighTemp", "ack via opcua")], TestContext.Current.CancellationToken); var state = engine.GetState("Plant/Line1::HighTemp")!; state.Acked.ShouldBe(AlarmAckedState.Acknowledged); state.LastAckUser.ShouldBe("opcua-client"); state.LastAckComment.ShouldBe("ack via opcua"); } [Fact] public async Task Null_arguments_rejected() { var (engine, source, _) = await BuildAsync(); using var _e = engine; using var _s = source; await Should.ThrowAsync(async () => await source.SubscribeAlarmsAsync(null!, TestContext.Current.CancellationToken)); await Should.ThrowAsync(async () => await source.UnsubscribeAlarmsAsync(null!, TestContext.Current.CancellationToken)); await Should.ThrowAsync(async () => await source.AcknowledgeAsync(null!, TestContext.Current.CancellationToken)); } }