using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.AdminUI.Uns.TagEditors; namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Uns; /// /// Unit tests for — the pure merge helper the TagModal's Galaxy /// address-picked handler uses to apply a freshly-picked tag_name.AttributeName reference. Re-picking /// a Galaxy address must update ONLY the address-derived FullName key and carry every other /// user-edited field over verbatim — ESPECIALLY a manually-authored alarm object (FB-4 / B2). The /// previous handler rebuilt the config from a bare {"FullName":...} blob, silently dropping the alarm. /// public sealed class GalaxyAddressRepickMergeTests { [Fact] public void Repick_preserves_an_already_edited_alarm_object() { // The operator authored an alarm, then re-picks the bound attribute to fix a typo. const string current = """{"FullName":"Pump_001.OldAttr","alarm":{"alarmType":"LimitAlarm","severity":250,"historizeToAveva":false}}"""; var merged = TagConfigJson.SetFullName(current, "Pump_001.NewAttr"); // The new address is applied... var fullName = ReadFullName(merged); fullName.ShouldBe("Pump_001.NewAttr"); // ...and the hand-edited alarm survives the re-pick intact. var alarm = NativeAlarmModel.FromJson(merged); alarm.IsAlarm.ShouldBeTrue(); alarm.AlarmType.ShouldBe("LimitAlarm"); alarm.Severity.ShouldBe(250); alarm.HistorizeToAveva.ShouldBe(false); } [Fact] public void Repick_with_no_alarm_just_sets_the_address() { var merged = TagConfigJson.SetFullName("""{"FullName":"Pump_001.OldAttr"}""", "Pump_001.NewAttr"); ReadFullName(merged).ShouldBe("Pump_001.NewAttr"); NativeAlarmModel.FromJson(merged).IsAlarm.ShouldBeFalse(); } [Fact] public void Repick_preserves_other_user_edited_non_address_fields() { // History intent + an array shape + an arbitrary scaling key the operator set independently of the // address — all must survive a re-pick that only owns FullName. const string current = """{"FullName":"Pump_001.OldAttr","isHistorized":true,"historianTagname":"Line3_Temp","isArray":true,"arrayLength":8,"scale":0.1}"""; var merged = TagConfigJson.SetFullName(current, "Pump_001.NewAttr"); ReadFullName(merged).ShouldBe("Pump_001.NewAttr"); var hist = TagHistorizeConfig.Read(merged); hist.IsHistorized.ShouldBeTrue(); hist.HistorianTagname.ShouldBe("Line3_Temp"); var arr = TagArrayConfig.Read(merged); arr.IsArray.ShouldBeTrue(); arr.ArrayLength.ShouldBe(8u); merged.ShouldContain("\"scale\":0.1"); } [Fact] public void Set_on_empty_or_malformed_input_starts_from_a_bare_FullName_object() { var merged = TagConfigJson.SetFullName("not json", "Pump_001.NewAttr"); ReadFullName(merged).ShouldBe("Pump_001.NewAttr"); merged.ShouldStartWith("{"); merged.ShouldEndWith("}"); } [Fact] public void Repick_with_alarm_flag_does_not_overwrite_existing_alarm_object() { // Mirrors the handler's alarm-typed re-pick: SeedDefaultAlarm(SetFullName(existing, addr)). // The operator authored an alarm, then re-picks an alarm-typed Galaxy attribute — the // SeedDefaultAlarm no-op guard must leave the edited alarm intact, not re-seed a default. const string current = """{"FullName":"Pump_001.OldAttr","alarm":{"alarmType":"LimitAlarm","severity":250,"historizeToAveva":false}}"""; var merged = NativeAlarmModel.SeedDefaultAlarm(TagConfigJson.SetFullName(current, "Pump_001.NewAttr")); ReadFullName(merged).ShouldBe("Pump_001.NewAttr"); var alarm = NativeAlarmModel.FromJson(merged); alarm.IsAlarm.ShouldBeTrue(); alarm.AlarmType.ShouldBe("LimitAlarm"); alarm.Severity.ShouldBe(250); alarm.HistorizeToAveva.ShouldBe(false); } [Fact] public void SetFullName_rejects_a_blank_reference() { Should.Throw(() => TagConfigJson.SetFullName("""{"FullName":"x"}""", " ")); } // Mirror of TagModal.ReadFullName — extracts FullName from a Galaxy TagConfig blob. private static string ReadFullName(string json) { using var doc = System.Text.Json.JsonDocument.Parse(json); return doc.RootElement.TryGetProperty("FullName", out var fn) ? fn.GetString() ?? "" : ""; } }