diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Uns/TagEditors/HistorianWonderwareTagConfigEditor.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Uns/TagEditors/HistorianWonderwareTagConfigEditor.razor new file mode 100644 index 00000000..8a5ee6eb --- /dev/null +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Uns/TagEditors/HistorianWonderwareTagConfigEditor.razor @@ -0,0 +1,43 @@ +@using ZB.MOM.WW.OtOpcUa.AdminUI.Uns.TagEditors + +
+
+ +
The AVEVA Historian tagname the driver reads against.
+
+
+ + +
+
+
+ +
Blank defaults the historian tagname to the FullName above.
+
+ +@code { + [Parameter] public string? ConfigJson { get; set; } + [Parameter] public EventCallback ConfigJsonChanged { get; set; } + + private HistorianWonderwareTagConfigModel _m = new(); + private string? _lastConfigJson; + + // Re-parse only when the incoming JSON actually changes, so an unrelated parent re-render + // (Blazor Server live-status pushes do this) can't reset the user's in-progress edits. + protected override void OnParametersSet() + { + if (ConfigJson == _lastConfigJson) { return; } + _lastConfigJson = ConfigJson; + _m = HistorianWonderwareTagConfigModel.FromJson(ConfigJson); + } + + private async Task Update(Action apply) + { + apply(); + await ConfigJsonChanged.InvokeAsync(_m.ToJson()); + } +} diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Uns/TagEditors/OpcUaClientTagConfigEditor.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Uns/TagEditors/OpcUaClientTagConfigEditor.razor new file mode 100644 index 00000000..1a1721b9 --- /dev/null +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Uns/TagEditors/OpcUaClientTagConfigEditor.razor @@ -0,0 +1,43 @@ +@using ZB.MOM.WW.OtOpcUa.AdminUI.Uns.TagEditors + +
+
+ +
The remote OPC UA NodeId the driver reads/writes/subscribes against. Use the browse picker on the driver page to find it.
+
+
+ + +
+
+
+ +
Blank defaults the historian tagname to the FullName above.
+
+ +@code { + [Parameter] public string? ConfigJson { get; set; } + [Parameter] public EventCallback ConfigJsonChanged { get; set; } + + private OpcUaClientTagConfigModel _m = new(); + private string? _lastConfigJson; + + // Re-parse only when the incoming JSON actually changes, so an unrelated parent re-render + // (Blazor Server live-status pushes do this) can't reset the user's in-progress edits. + protected override void OnParametersSet() + { + if (ConfigJson == _lastConfigJson) { return; } + _lastConfigJson = ConfigJson; + _m = OpcUaClientTagConfigModel.FromJson(ConfigJson); + } + + private async Task Update(Action apply) + { + apply(); + await ConfigJsonChanged.InvokeAsync(_m.ToJson()); + } +} diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/HistorianWonderwareTagConfigModel.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/HistorianWonderwareTagConfigModel.cs new file mode 100644 index 00000000..68dc6054 --- /dev/null +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/HistorianWonderwareTagConfigModel.cs @@ -0,0 +1,62 @@ +using System.Text.Json.Nodes; + +namespace ZB.MOM.WW.OtOpcUa.AdminUI.Uns.TagEditors; + +/// Typed working model for a Wonderware (AVEVA) Historian equipment tag's TagConfig JSON. The +/// tag binds to a historian tag by its full reference (FullName — the historian tagname/source +/// the driver reads against), plus the optional driver-agnostic server-side HistoryRead intent +/// (isHistorized / historianTagname). Preserves unrecognised JSON keys across a load→save. +/// +/// The FullName key is intentionally PascalCase: the deploy-time composer + node walker +/// (Phase7Composer.ExtractTagFullName, EquipmentNodeWalker) read it via a +/// case-sensitive TryGetProperty("FullName", …), so the editor MUST persist that exact +/// casing. The history keys (isHistorized / historianTagname) are camelCase to match +/// Phase7Composer.ExtractTagHistorize. +/// +public sealed class HistorianWonderwareTagConfigModel +{ + /// Historian tagname/source the tag binds to (the driver-side full reference). Required. + public string FullName { get; set; } = ""; + + /// Whether the server exposes OPC UA HistoryRead over this tag's variable node. + public bool IsHistorized { get; set; } + + /// Optional historian tagname override; blank means the historian tagname defaults to . + public string HistorianTagname { get; set; } = ""; + + private JsonObject _bag = new(); + + /// Loads a model from a TagConfig JSON string, defaulting any absent field and retaining + /// every original key (so fields this editor doesn't expose survive a load→save). + /// The tag's TagConfig JSON (null/blank/malformed ⇒ defaults). + public static HistorianWonderwareTagConfigModel FromJson(string? json) + { + var o = TagConfigJson.ParseOrNew(json); + return new HistorianWonderwareTagConfigModel + { + FullName = TagConfigJson.GetString(o, "FullName") ?? "", + IsHistorized = TagConfigJson.GetBool(o, "isHistorized"), + HistorianTagname = TagConfigJson.GetString(o, "historianTagname") ?? "", + _bag = o, + }; + } + + /// Serialises this model back to a TagConfig JSON string over the preserved key bag. + /// FullName is written PascalCase (the composer/walker contract key); the history keys are + /// written camelCase and dropped when default (absent isHistorized ⇒ false at the composer; + /// blank historianTagname ⇒ defaults to FullName). + public string ToJson() + { + TagConfigJson.Set(_bag, "FullName", FullName.Trim()); + // Drop isHistorized when false so the persisted blob stays minimal and matches the + // composer's "absent ⇒ false" convention; same for a blank historianTagname override. + TagConfigJson.Set(_bag, "isHistorized", IsHistorized ? true : null); + TagConfigJson.Set(_bag, "historianTagname", + string.IsNullOrWhiteSpace(HistorianTagname) ? null : HistorianTagname.Trim()); + return TagConfigJson.Serialize(_bag); + } + + /// Validation hook; returns an error message or null when the model is valid. + public string? Validate() + => string.IsNullOrWhiteSpace(FullName) ? "A historian tagname (FullName) is required." : null; +} diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/OpcUaClientTagConfigModel.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/OpcUaClientTagConfigModel.cs new file mode 100644 index 00000000..ef65da78 --- /dev/null +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/OpcUaClientTagConfigModel.cs @@ -0,0 +1,63 @@ +using System.Text.Json.Nodes; + +namespace ZB.MOM.WW.OtOpcUa.AdminUI.Uns.TagEditors; + +/// Typed working model for an OPC UA Client (gateway) equipment tag's TagConfig JSON. The tag +/// is bound to the upstream OPC UA server node by its full reference (FullName — the persisted +/// stable nsu=…;s=… or plain ns=2;s=… NodeId the driver reads/writes/subscribes against), +/// plus the optional driver-agnostic server-side HistoryRead intent (isHistorized / +/// historianTagname). Preserves unrecognised JSON keys across a load→save. +/// +/// The FullName key is intentionally PascalCase: the deploy-time composer + node walker +/// (Phase7Composer.ExtractTagFullName, EquipmentNodeWalker) read it via a +/// case-sensitive TryGetProperty("FullName", …), so the editor MUST persist that exact +/// casing. The history keys (isHistorized / historianTagname) are camelCase to match +/// Phase7Composer.ExtractTagHistorize. +/// +public sealed class OpcUaClientTagConfigModel +{ + /// Upstream OPC UA node reference the tag binds to (the driver-side full reference). Required. + public string FullName { get; set; } = ""; + + /// Whether the server exposes OPC UA HistoryRead over this tag's variable node. + public bool IsHistorized { get; set; } + + /// Optional historian tagname override; blank means the historian tagname defaults to . + public string HistorianTagname { get; set; } = ""; + + private JsonObject _bag = new(); + + /// Loads a model from a TagConfig JSON string, defaulting any absent field and retaining + /// every original key (so fields this editor doesn't expose survive a load→save). + /// The tag's TagConfig JSON (null/blank/malformed ⇒ defaults). + public static OpcUaClientTagConfigModel FromJson(string? json) + { + var o = TagConfigJson.ParseOrNew(json); + return new OpcUaClientTagConfigModel + { + FullName = TagConfigJson.GetString(o, "FullName") ?? "", + IsHistorized = TagConfigJson.GetBool(o, "isHistorized"), + HistorianTagname = TagConfigJson.GetString(o, "historianTagname") ?? "", + _bag = o, + }; + } + + /// Serialises this model back to a TagConfig JSON string over the preserved key bag. + /// FullName is written PascalCase (the composer/walker contract key); the history keys are + /// written camelCase and dropped when default (absent isHistorized ⇒ false at the composer; + /// blank historianTagname ⇒ defaults to FullName). + public string ToJson() + { + TagConfigJson.Set(_bag, "FullName", FullName.Trim()); + // Drop isHistorized when false so the persisted blob stays minimal and matches the + // composer's "absent ⇒ false" convention; same for a blank historianTagname override. + TagConfigJson.Set(_bag, "isHistorized", IsHistorized ? true : null); + TagConfigJson.Set(_bag, "historianTagname", + string.IsNullOrWhiteSpace(HistorianTagname) ? null : HistorianTagname.Trim()); + return TagConfigJson.Serialize(_bag); + } + + /// Validation hook; returns an error message or null when the model is valid. + public string? Validate() + => string.IsNullOrWhiteSpace(FullName) ? "An upstream node reference (FullName) is required." : null; +} diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/TagConfigEditorMap.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/TagConfigEditorMap.cs index 378db6ec..dc929706 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/TagConfigEditorMap.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/TagConfigEditorMap.cs @@ -16,6 +16,8 @@ public static class TagConfigEditorMap ["AbLegacy"] = typeof(Components.Shared.Uns.TagEditors.AbLegacyTagConfigEditor), ["TwinCat"] = typeof(Components.Shared.Uns.TagEditors.TwinCATTagConfigEditor), ["Focas"] = typeof(Components.Shared.Uns.TagEditors.FocasTagConfigEditor), + ["OpcUaClient"] = typeof(Components.Shared.Uns.TagEditors.OpcUaClientTagConfigEditor), + ["Historian.Wonderware"] = typeof(Components.Shared.Uns.TagEditors.HistorianWonderwareTagConfigEditor), }; /// Returns the editor component type for a driver type, or null if none is registered. diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/TagConfigJson.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/TagConfigJson.cs index 673dd433..8f317a4c 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/TagConfigJson.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/TagConfigJson.cs @@ -29,6 +29,10 @@ public static class TagConfigJson public static int GetInt(JsonObject o, string name, int fallback = 0) => o.TryGetPropertyValue(name, out var n) && n is JsonValue v && v.TryGetValue(out var i) ? i : fallback; + /// Reads a bool value, or if absent/null/non-boolean (incl. object/array/number/string nodes). + public static bool GetBool(JsonObject o, string name, bool fallback = false) + => o.TryGetPropertyValue(name, out var n) && n is JsonValue v && v.TryGetValue(out var b) ? b : fallback; + /// Reads an enum by its serialised name, or if absent/unparseable. public static TEnum GetEnum(JsonObject o, string name, TEnum fallback) where TEnum : struct, Enum => GetString(o, name) is { } s && Enum.TryParse(s, ignoreCase: true, out var v) ? v : fallback; diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/TagConfigValidator.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/TagConfigValidator.cs index 8e5f8722..e37b3d44 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/TagConfigValidator.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/TagConfigValidator.cs @@ -18,6 +18,8 @@ public static class TagConfigValidator ["AbLegacy"] = j => AbLegacyTagConfigModel.FromJson(j).Validate(), ["TwinCat"] = j => TwinCATTagConfigModel.FromJson(j).Validate(), ["Focas"] = j => FocasTagConfigModel.FromJson(j).Validate(), + ["OpcUaClient"] = j => OpcUaClientTagConfigModel.FromJson(j).Validate(), + ["Historian.Wonderware"] = j => HistorianWonderwareTagConfigModel.FromJson(j).Validate(), }; /// diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/HistorianWonderwareTagConfigModelTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/HistorianWonderwareTagConfigModelTests.cs new file mode 100644 index 00000000..8d976d65 --- /dev/null +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/HistorianWonderwareTagConfigModelTests.cs @@ -0,0 +1,114 @@ +using Shouldly; +using Xunit; +using ZB.MOM.WW.OtOpcUa.AdminUI.Uns.TagEditors; + +namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Uns; + +public sealed class HistorianWonderwareTagConfigModelTests +{ + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + [InlineData("{}")] + public void FromJson_returns_defaults_for_empty_input(string? json) + { + var m = HistorianWonderwareTagConfigModel.FromJson(json); + + m.FullName.ShouldBe(""); + m.IsHistorized.ShouldBeFalse(); + m.HistorianTagname.ShouldBe(""); + } + + [Fact] + public void FromJson_reads_all_fields() + { + var m = HistorianWonderwareTagConfigModel.FromJson( + """{"FullName":"SysTimeSec","isHistorized":true,"historianTagname":"Reactor1.Temp"}"""); + + m.FullName.ShouldBe("SysTimeSec"); + m.IsHistorized.ShouldBeTrue(); + m.HistorianTagname.ShouldBe("Reactor1.Temp"); + } + + [Fact] + public void Round_trip_preserves_all_fields() + { + var m = new HistorianWonderwareTagConfigModel + { + FullName = "Reactor1.Temp", + IsHistorized = true, + HistorianTagname = "Reactor1.Temp.Override", + }; + + var json = m.ToJson(); + var m2 = HistorianWonderwareTagConfigModel.FromJson(json); + + m2.FullName.ShouldBe("Reactor1.Temp"); + m2.IsHistorized.ShouldBeTrue(); + m2.HistorianTagname.ShouldBe("Reactor1.Temp.Override"); + } + + [Fact] + public void ToJson_emits_PascalCase_FullName_and_camelCase_history_keys() + { + var m = new HistorianWonderwareTagConfigModel + { + FullName = "Reactor1.Temp", + IsHistorized = true, + HistorianTagname = "Reactor1.Temp.Override", + }; + + var json = m.ToJson(); + + // FullName is the composer/walker contract key — PascalCase, case-sensitive. + json.ShouldContain("\"FullName\":\"Reactor1.Temp\""); + json.ShouldNotContain("\"fullName\"", Case.Sensitive); + json.ShouldContain("\"isHistorized\":true"); + json.ShouldContain("\"historianTagname\":\"Reactor1.Temp.Override\""); + } + + [Fact] + public void ToJson_omits_history_keys_when_default() + { + var json = new HistorianWonderwareTagConfigModel { FullName = "Reactor1.Temp" }.ToJson(); + + json.ShouldContain("\"FullName\":\"Reactor1.Temp\""); + json.ShouldNotContain("isHistorized"); + json.ShouldNotContain("historianTagname"); + } + + [Fact] + public void FromJson_then_ToJson_preserves_unknown_keys() + { + var json = HistorianWonderwareTagConfigModel + .FromJson("""{"FullName":"Reactor1.Temp","deadband":0.5}""") + .ToJson(); + + json.ShouldContain("deadband"); + json.ShouldContain("0.5"); + // and the exposed field still round-trips + json.ShouldContain("\"FullName\":\"Reactor1.Temp\""); + } + + [Fact] + public void ToJson_trims_FullName() + { + var json = new HistorianWonderwareTagConfigModel { FullName = " Reactor1.Temp " }.ToJson(); + + json.ShouldContain("\"FullName\":\"Reactor1.Temp\""); + } + + [Fact] + public void Validate_returns_error_when_FullName_blank() + { + new HistorianWonderwareTagConfigModel().Validate().ShouldNotBeNullOrEmpty(); + new HistorianWonderwareTagConfigModel { FullName = " " }.Validate().ShouldNotBeNullOrEmpty(); + } + + [Fact] + public void Validate_returns_null_when_FullName_present() + { + new HistorianWonderwareTagConfigModel { FullName = "Reactor1.Temp" }.Validate().ShouldBeNull(); + } +} diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/OpcUaClientTagConfigModelTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/OpcUaClientTagConfigModelTests.cs new file mode 100644 index 00000000..6d75d995 --- /dev/null +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/OpcUaClientTagConfigModelTests.cs @@ -0,0 +1,114 @@ +using Shouldly; +using Xunit; +using ZB.MOM.WW.OtOpcUa.AdminUI.Uns.TagEditors; + +namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Uns; + +public sealed class OpcUaClientTagConfigModelTests +{ + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + [InlineData("{}")] + public void FromJson_returns_defaults_for_empty_input(string? json) + { + var m = OpcUaClientTagConfigModel.FromJson(json); + + m.FullName.ShouldBe(""); + m.IsHistorized.ShouldBeFalse(); + m.HistorianTagname.ShouldBe(""); + } + + [Fact] + public void FromJson_reads_all_fields() + { + var m = OpcUaClientTagConfigModel.FromJson( + """{"FullName":"nsu=urn:srv;s=Line3.Temp","isHistorized":true,"historianTagname":"Line3_Temp"}"""); + + m.FullName.ShouldBe("nsu=urn:srv;s=Line3.Temp"); + m.IsHistorized.ShouldBeTrue(); + m.HistorianTagname.ShouldBe("Line3_Temp"); + } + + [Fact] + public void Round_trip_preserves_all_fields() + { + var m = new OpcUaClientTagConfigModel + { + FullName = "ns=2;s=Line3.Temp", + IsHistorized = true, + HistorianTagname = "Line3_Temp", + }; + + var json = m.ToJson(); + var m2 = OpcUaClientTagConfigModel.FromJson(json); + + m2.FullName.ShouldBe("ns=2;s=Line3.Temp"); + m2.IsHistorized.ShouldBeTrue(); + m2.HistorianTagname.ShouldBe("Line3_Temp"); + } + + [Fact] + public void ToJson_emits_PascalCase_FullName_and_camelCase_history_keys() + { + var m = new OpcUaClientTagConfigModel + { + FullName = "ns=2;s=Line3.Temp", + IsHistorized = true, + HistorianTagname = "Line3_Temp", + }; + + var json = m.ToJson(); + + // FullName is the composer/walker contract key — PascalCase, case-sensitive. + json.ShouldContain("\"FullName\":\"ns=2;s=Line3.Temp\""); + json.ShouldNotContain("\"fullName\"", Case.Sensitive); + json.ShouldContain("\"isHistorized\":true"); + json.ShouldContain("\"historianTagname\":\"Line3_Temp\""); + } + + [Fact] + public void ToJson_omits_history_keys_when_default() + { + var json = new OpcUaClientTagConfigModel { FullName = "ns=2;s=X" }.ToJson(); + + json.ShouldContain("\"FullName\":\"ns=2;s=X\""); + json.ShouldNotContain("isHistorized"); + json.ShouldNotContain("historianTagname"); + } + + [Fact] + public void FromJson_then_ToJson_preserves_unknown_keys() + { + var json = OpcUaClientTagConfigModel + .FromJson("""{"FullName":"ns=2;s=X","samplingIntervalMs":250}""") + .ToJson(); + + json.ShouldContain("samplingIntervalMs"); + json.ShouldContain("250"); + // and the exposed field still round-trips + json.ShouldContain("\"FullName\":\"ns=2;s=X\""); + } + + [Fact] + public void ToJson_trims_FullName() + { + var json = new OpcUaClientTagConfigModel { FullName = " ns=2;s=X " }.ToJson(); + + json.ShouldContain("\"FullName\":\"ns=2;s=X\""); + } + + [Fact] + public void Validate_returns_error_when_FullName_blank() + { + new OpcUaClientTagConfigModel().Validate().ShouldNotBeNullOrEmpty(); + new OpcUaClientTagConfigModel { FullName = " " }.Validate().ShouldNotBeNullOrEmpty(); + } + + [Fact] + public void Validate_returns_null_when_FullName_present() + { + new OpcUaClientTagConfigModel { FullName = "ns=2;s=X" }.Validate().ShouldBeNull(); + } +} diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/TagConfigJsonTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/TagConfigJsonTests.cs index e1d4e454..31a54beb 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/TagConfigJsonTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/TagConfigJsonTests.cs @@ -87,6 +87,31 @@ public sealed class TagConfigJsonTests TagConfigJson.GetString(obj, "nested").ShouldBeNull(); } + [Fact] + public void GetBool_reads_a_bool() + { + var obj = TagConfigJson.ParseOrNew("""{"isHistorized":true}"""); + + TagConfigJson.GetBool(obj, "isHistorized").ShouldBeTrue(); + } + + [Fact] + public void GetBool_falls_back_when_absent() + { + var obj = new JsonObject(); + + TagConfigJson.GetBool(obj, "isHistorized", fallback: true).ShouldBeTrue(); + } + + [Fact] + public void GetBool_falls_back_when_value_is_not_a_bool() + { + var obj = TagConfigJson.ParseOrNew("""{"isHistorized":"yes","other":1}"""); + + TagConfigJson.GetBool(obj, "isHistorized").ShouldBeFalse(); + TagConfigJson.GetBool(obj, "other").ShouldBeFalse(); + } + [Fact] public void GetEnum_parses_by_name_case_insensitive() { diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/TagConfigValidatorTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/TagConfigValidatorTests.cs index 328b4f49..160382c3 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/TagConfigValidatorTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/TagConfigValidatorTests.cs @@ -12,7 +12,7 @@ public sealed class TagConfigValidatorTests [Fact] public void Unmapped_driver_has_no_typed_validator_so_is_valid() - => TagConfigValidator.Validate("OpcUaClient", "{}").ShouldBeNull(); + => TagConfigValidator.Validate("GalaxyMxGateway", "{}").ShouldBeNull(); [Fact] public void Modbus_has_no_required_field_so_empty_config_is_valid() @@ -25,12 +25,22 @@ public sealed class TagConfigValidatorTests [InlineData("AbLegacy")] [InlineData("TwinCat")] [InlineData("Focas")] + [InlineData("OpcUaClient")] + [InlineData("Historian.Wonderware")] public void Required_field_blank_is_rejected(string driverType) { TagConfigValidator.Validate(driverType, "{}").ShouldNotBeNullOrEmpty(); TagConfigValidator.Validate(driverType, null).ShouldNotBeNullOrEmpty(); } + [Fact] + public void OpcUaClient_with_full_name_is_valid() + => TagConfigValidator.Validate("OpcUaClient", """{"FullName":"ns=2;s=Line3.Temp"}""").ShouldBeNull(); + + [Fact] + public void HistorianWonderware_with_full_name_is_valid() + => TagConfigValidator.Validate("Historian.Wonderware", """{"FullName":"Reactor1.Temp"}""").ShouldBeNull(); + [Fact] public void S7_with_address_is_valid() => TagConfigValidator.Validate("S7", """{"address":"DB1.DBW0"}""").ShouldBeNull();