From 15c9de0f4a55aec1b636131c1b3d197d15474ff2 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 22 Oct 2026 06:05:06 -0400 Subject: [PATCH] feat(commons): GrpcEndpointRules for site gRPC address validation --- .../Validation/GrpcEndpointRules.cs | 37 +++++++++++++++++++ .../Validation/GrpcEndpointRulesTests.cs | 37 +++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 src/ZB.MOM.WW.ScadaBridge.Commons/Validation/GrpcEndpointRules.cs create mode 100644 tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Validation/GrpcEndpointRulesTests.cs diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Validation/GrpcEndpointRules.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Validation/GrpcEndpointRules.cs new file mode 100644 index 00000000..7dc75b32 --- /dev/null +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Validation/GrpcEndpointRules.cs @@ -0,0 +1,37 @@ +namespace ZB.MOM.WW.ScadaBridge.Commons.Validation; + +/// +/// Validation for site gRPC endpoint addresses (Site.GrpcNodeAAddress/GrpcNodeBAddress). +/// A bare host or host:port is rejected because the gRPC channel needs an absolute http(s) URI. +/// +public static class GrpcEndpointRules +{ + /// + /// Validates a gRPC endpoint. Null/whitespace is "not configured": valid only when + /// is true. + /// + public static bool TryValidate(string? value, bool allowEmpty, out string? error) + { + error = null; + if (string.IsNullOrWhiteSpace(value)) + { + if (allowEmpty) + { + return true; + } + + error = "gRPC endpoint is required (e.g. http://host:8083)."; + return false; + } + + if (!Uri.TryCreate(value.Trim(), UriKind.Absolute, out var uri) + || (uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps) + || string.IsNullOrEmpty(uri.Host)) + { + error = $"gRPC endpoint '{value}' must be an absolute http:// or https:// URL with a host (e.g. http://host:8083)."; + return false; + } + + return true; + } +} diff --git a/tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Validation/GrpcEndpointRulesTests.cs b/tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Validation/GrpcEndpointRulesTests.cs new file mode 100644 index 00000000..a356953e --- /dev/null +++ b/tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Validation/GrpcEndpointRulesTests.cs @@ -0,0 +1,37 @@ +using ZB.MOM.WW.ScadaBridge.Commons.Validation; + +namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Validation; + +public class GrpcEndpointRulesTests +{ + [Theory] + [InlineData("scadabridge-site-a-node-a")] + [InlineData("site-a-node-a:8083")] + [InlineData("ftp://x:1")] + public void TryValidate_RejectsNonHttpEndpoints(string value) + { + Assert.False(GrpcEndpointRules.TryValidate(value, false, out var error)); + Assert.NotNull(error); + } + + [Theory] + [InlineData("http://site-a-node-a:8083")] + [InlineData("https://h")] + public void TryValidate_AcceptsHttpEndpoints(string value) + { + Assert.True(GrpcEndpointRules.TryValidate(value, false, out var error)); + Assert.Null(error); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void TryValidate_EmptyHonoursAllowEmpty(string? value) + { + Assert.True(GrpcEndpointRules.TryValidate(value, true, out var ok)); + Assert.Null(ok); + Assert.False(GrpcEndpointRules.TryValidate(value, false, out var err)); + Assert.NotNull(err); + } +}