diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Uns/TagModal.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Uns/TagModal.razor index b03337fd..aff7f627 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Uns/TagModal.razor +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Uns/TagModal.razor @@ -197,6 +197,15 @@ _error = null; try { + // Client-side per-driver config validation (the typed editor's Validate()), so a blank + // required field is caught here rather than silently saving and failing at deploy/connect. + var configError = TagConfigValidator.Validate(SelectedDriverType, _form.TagConfig); + if (configError is not null) + { + _error = configError; + return; + } + var input = new TagInput( _form.TagId, _form.Name, 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 new file mode 100644 index 00000000..8e5f8722 --- /dev/null +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/TagConfigValidator.cs @@ -0,0 +1,29 @@ +namespace ZB.MOM.WW.OtOpcUa.AdminUI.Uns.TagEditors; + +/// +/// Client-side TagConfig validation dispatched by DriverType (parallel to +/// ). Each entry parses the tag's config JSON into the driver's typed +/// model and runs its Validate(). Returns an error string to block the TagModal save, or null +/// when the config is valid — or when no typed validator is registered (unmapped drivers use the raw +/// textarea and are validated server-side at deploy). +/// +public static class TagConfigValidator +{ + private static readonly IReadOnlyDictionary> Validators = + new Dictionary>(StringComparer.OrdinalIgnoreCase) + { + ["ModbusTcp"] = j => ModbusTagConfigModel.FromJson(j).Validate(), + ["S7"] = j => S7TagConfigModel.FromJson(j).Validate(), + ["AbCip"] = j => AbCipTagConfigModel.FromJson(j).Validate(), + ["AbLegacy"] = j => AbLegacyTagConfigModel.FromJson(j).Validate(), + ["TwinCat"] = j => TwinCATTagConfigModel.FromJson(j).Validate(), + ["Focas"] = j => FocasTagConfigModel.FromJson(j).Validate(), + }; + + /// + /// Validates a tag's for the given . + /// Returns an error string to block save, or null when valid / no typed validator is registered. + /// + public static string? Validate(string? driverType, string? configJson) + => driverType is not null && Validators.TryGetValue(driverType, out var v) ? v(configJson) : null; +} 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 new file mode 100644 index 00000000..328b4f49 --- /dev/null +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/TagConfigValidatorTests.cs @@ -0,0 +1,49 @@ +using Shouldly; +using ZB.MOM.WW.OtOpcUa.AdminUI.Uns.TagEditors; +using Xunit; + +namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Uns; + +public sealed class TagConfigValidatorTests +{ + [Fact] + public void No_driver_type_is_valid() + => TagConfigValidator.Validate(null, "{}").ShouldBeNull(); + + [Fact] + public void Unmapped_driver_has_no_typed_validator_so_is_valid() + => TagConfigValidator.Validate("OpcUaClient", "{}").ShouldBeNull(); + + [Fact] + public void Modbus_has_no_required_field_so_empty_config_is_valid() + => TagConfigValidator.Validate("ModbusTcp", "{}").ShouldBeNull(); + + // Drivers whose Validate() requires a field: blank config must return an error string. + [Theory] + [InlineData("S7")] + [InlineData("AbCip")] + [InlineData("AbLegacy")] + [InlineData("TwinCat")] + [InlineData("Focas")] + public void Required_field_blank_is_rejected(string driverType) + { + TagConfigValidator.Validate(driverType, "{}").ShouldNotBeNullOrEmpty(); + TagConfigValidator.Validate(driverType, null).ShouldNotBeNullOrEmpty(); + } + + [Fact] + public void S7_with_address_is_valid() + => TagConfigValidator.Validate("S7", """{"address":"DB1.DBW0"}""").ShouldBeNull(); + + [Fact] + public void AbCip_with_tag_path_is_valid() + => TagConfigValidator.Validate("AbCip", """{"tagPath":"Program:Main.Speed"}""").ShouldBeNull(); + + [Fact] + public void TwinCat_with_symbol_path_is_valid() + => TagConfigValidator.Validate("TwinCat", """{"symbolPath":"MAIN.bStart"}""").ShouldBeNull(); + + [Fact] + public void Driver_type_lookup_is_case_insensitive() + => TagConfigValidator.Validate("s7", "{}").ShouldNotBeNullOrEmpty(); +}