using System.Text.Json.Nodes; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.AdminUI.Uns.TagEditors; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Uns; /// Gitea #642 — the shared NativeAlarmEditor's pure-model behaviour: remove-on-off, composition /// with the per-driver editors' models, validation parity with the deploy gate, driver-type gating. public sealed class NativeAlarmEditorModelTests { [Fact] public void ToJson_with_IsAlarm_false_removes_the_alarm_key_and_keeps_the_rest() { var m = NativeAlarmModel.FromJson("""{"FullName":"X.Y","alarm":{"alarmType":"LimitAlarm","severity":300,"extra":1}}"""); m.IsAlarm = false; var o = JsonNode.Parse(m.ToJson())!.AsObject(); o.ContainsKey("alarm").ShouldBeFalse(); o["FullName"]!.GetValue().ShouldBe("X.Y"); } [Fact] public void Toggle_off_then_on_again_starts_from_defaults() { var off = NativeAlarmModel.Edit("""{"alarm":{"severity":300}}""", m => m.IsAlarm = false); var on = NativeAlarmModel.Edit(off, m => m.IsAlarm = true); var back = NativeAlarmModel.FromJson(on); back.IsAlarm.ShouldBeTrue(); back.Severity.ShouldBe(500); back.AlarmType.ShouldBe("AlarmCondition"); } [Fact] public void Edit_preserves_unknown_keys_at_both_levels() { var json = NativeAlarmModel.Edit("""{"FullName":"A","zz":1,"alarm":{"alarmType":"OffNormalAlarm","custom":"k"}}""", m => m.Severity = 900); var o = JsonNode.Parse(json)!.AsObject(); o["zz"]!.GetValue().ShouldBe(1); o["alarm"]!["custom"]!.GetValue().ShouldBe("k"); o["alarm"]!["severity"]!.GetValue().ShouldBe(900); } [Fact] public void Alarm_and_mtconnect_driver_editor_compose_in_either_order() { const string start = """{"fullName":"Xact"}"""; // alarm first, then driver editor (driver model re-parses the alarm-bearing JSON) var a1 = NativeAlarmModel.Edit(start, m => { m.IsAlarm = true; m.Severity = 800; }); var d1 = MTConnectTagConfigModel.FromJson(a1); d1.FullName = "Yact"; var r1 = d1.ToJson(); // driver editor first, then alarm var d2 = MTConnectTagConfigModel.FromJson(start); d2.FullName = "Yact"; var r2 = NativeAlarmModel.Edit(d2.ToJson(), m => { m.IsAlarm = true; m.Severity = 800; }); foreach (var r in new[] { r1, r2 }) { var o = JsonNode.Parse(r)!.AsObject(); o["fullName"]!.GetValue().ShouldBe("Yact"); o["alarm"]!["severity"]!.GetValue().ShouldBe(800); } } [Theory] [InlineData("Modbus")] [InlineData("S7")] [InlineData("AbCip")] [InlineData("AbLegacy")] [InlineData("TwinCAT")] [InlineData("FOCAS")] [InlineData("OpcUaClient")] [InlineData("Sql")] [InlineData("Mqtt")] [InlineData("MTConnect")] public void Every_typed_driver_model_preserves_an_alarm_object(string driverType) { const string json = """{"alarm":{"alarmType":"LimitAlarm","severity":123}}"""; var round = TagConfigRoundTrip.Through(driverType, json); NativeAlarmModel.FromJson(round).Severity.ShouldBe(123); } [Fact] public void Validate_is_not_stricter_than_the_deploy_gate() { // The gate/parser accepts any severity (clamped 1..1000 at publish) and any alarmType string. var m = NativeAlarmModel.FromJson("""{"alarm":{"alarmType":"Whatever","severity":5000}}"""); m.Validate().ShouldBeNull(); m.Warnings().ShouldNotBeEmpty(); // advisory only } [Fact] public void SupportsNativeAlarms_covers_exactly_the_IAlarmSource_drivers() { foreach (var t in new[] { DriverTypeNames.MTConnect, DriverTypeNames.AbCip, DriverTypeNames.FOCAS, DriverTypeNames.OpcUaClient, DriverTypeNames.Galaxy }) NativeAlarmModel.SupportsNativeAlarms(t).ShouldBeTrue(t); foreach (var t in new[] { DriverTypeNames.Modbus, DriverTypeNames.S7, DriverTypeNames.Mqtt, null }) NativeAlarmModel.SupportsNativeAlarms(t).ShouldBeFalse(t); } }