From 19223a08cfd479a8f4b3cceb9c866af35a2e5f28 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Fri, 29 May 2026 07:46:28 -0400 Subject: [PATCH] feat(commons): MxGatewayEndpointConfig validator + tests --- .../MxGatewayEndpointConfigValidator.cs | 46 +++++++++++ .../MxGatewayEndpointConfigValidatorTests.cs | 77 +++++++++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 src/ZB.MOM.WW.ScadaBridge.Commons/Validators/MxGatewayEndpointConfigValidator.cs create mode 100644 tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Validators/MxGatewayEndpointConfigValidatorTests.cs diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Validators/MxGatewayEndpointConfigValidator.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Validators/MxGatewayEndpointConfigValidator.cs new file mode 100644 index 00000000..8443e77d --- /dev/null +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Validators/MxGatewayEndpointConfigValidator.cs @@ -0,0 +1,46 @@ +using ZB.MOM.WW.ScadaBridge.Commons.Types.DataConnections; +using ZB.MOM.WW.ScadaBridge.Commons.Types.Flattening; + +namespace ZB.MOM.WW.ScadaBridge.Commons.Validators; + +/// +/// Pure-function validator for . Errors carry +/// the offending property name in +/// (optionally prefixed, e.g. "Primary.Endpoint") so the form can render +/// per-field messages. +/// +public static class MxGatewayEndpointConfigValidator +{ + /// + /// Validates all fields of an , returning errors with optionally-prefixed field names. + /// + /// The MxGateway endpoint configuration to validate. + /// Optional prefix prepended to each field name in error entries (e.g., "Primary."). + public static ValidationResult Validate(MxGatewayEndpointConfig config, string fieldPrefix = "") + { + var errors = new List(); + + if (string.IsNullOrWhiteSpace(config.Endpoint)) + errors.Add(Err("Endpoint", "Endpoint URL is required.")); + else if (!Uri.TryCreate(config.Endpoint, UriKind.Absolute, out var uri) + || (uri.Scheme != "http" && uri.Scheme != "https") + || string.IsNullOrEmpty(uri.Host)) + errors.Add(Err("Endpoint", "Endpoint URL must be a valid http:// or https:// URI.")); + + if (string.IsNullOrWhiteSpace(config.ApiKey)) + errors.Add(Err("ApiKey", "API key is required.")); + + if (config.ReadTimeoutMs <= 0) + errors.Add(Err("ReadTimeoutMs", "Must be > 0.")); + + return errors.Count == 0 + ? ValidationResult.Success() + : ValidationResult.FromErrors(errors.ToArray()); + + ValidationEntry Err(string field, string message) => + ValidationEntry.Error( + ValidationCategory.ConnectionConfig, + message, + entityName: $"{fieldPrefix}{field}"); + } +} diff --git a/tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Validators/MxGatewayEndpointConfigValidatorTests.cs b/tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Validators/MxGatewayEndpointConfigValidatorTests.cs new file mode 100644 index 00000000..0813132a --- /dev/null +++ b/tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Validators/MxGatewayEndpointConfigValidatorTests.cs @@ -0,0 +1,77 @@ +using ZB.MOM.WW.ScadaBridge.Commons.Types.DataConnections; +using ZB.MOM.WW.ScadaBridge.Commons.Types.Flattening; +using ZB.MOM.WW.ScadaBridge.Commons.Validators; + +namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Validators; + +public class MxGatewayEndpointConfigValidatorTests +{ + private static MxGatewayEndpointConfig Valid() => new() + { + Endpoint = "http://gw:5000", + ApiKey = "key", + }; + + [Fact] + public void Validate_ValidConfig_IsValid() + { + var result = MxGatewayEndpointConfigValidator.Validate(Valid()); + Assert.True(result.IsValid); + Assert.Empty(result.Errors); + } + + [Fact] + public void Validate_MissingEndpoint_Fails() + { + var c = Valid(); + c.Endpoint = ""; + var r = MxGatewayEndpointConfigValidator.Validate(c); + Assert.False(r.IsValid); + Assert.Contains(r.Errors, e => + e.EntityName == "Endpoint" + && e.Category == ValidationCategory.ConnectionConfig + && e.Message.Contains("required", StringComparison.OrdinalIgnoreCase)); + } + + [Theory] + [InlineData("opc.tcp://x:4840")] + [InlineData("ftp://x")] + [InlineData("not a url")] + public void Validate_BadEndpointScheme_Fails(string url) + { + var c = Valid(); + c.Endpoint = url; + var r = MxGatewayEndpointConfigValidator.Validate(c); + Assert.False(r.IsValid); + Assert.Contains(r.Errors, e => e.EntityName == "Endpoint"); + } + + [Fact] + public void Validate_MissingApiKey_Fails() + { + var c = Valid(); + c.ApiKey = ""; + var r = MxGatewayEndpointConfigValidator.Validate(c); + Assert.False(r.IsValid); + Assert.Contains(r.Errors, e => e.EntityName == "ApiKey"); + } + + [Fact] + public void Validate_NonPositiveReadTimeout_Fails() + { + var c = Valid(); + c.ReadTimeoutMs = 0; + var r = MxGatewayEndpointConfigValidator.Validate(c); + Assert.False(r.IsValid); + Assert.Contains(r.Errors, e => e.EntityName == "ReadTimeoutMs"); + } + + [Fact] + public void Validate_PrefixedFieldNames_AppearInErrors() + { + var c = Valid(); + c.Endpoint = ""; + var r = MxGatewayEndpointConfigValidator.Validate(c, "Primary."); + Assert.Contains(r.Errors, e => e.EntityName == "Primary.Endpoint"); + } +}