using Akka.Actor; using Akka.TestKit.Xunit2; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Abstractions; using NSubstitute; using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites; using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories; using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services; using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management; namespace ZB.MOM.WW.ScadaBridge.ManagementService.Tests; /// /// Review 2026-10-21 03/R-1: a malformed site gRPC address used to be persisted /// unvalidated and then throw inside SitePairChannelProvider.UpdateSite. /// Create/Update must reject it at the write boundary with a readable error. /// public class SiteGrpcEndpointValidationTests : TestKit { private readonly ISiteRepository _siteRepo = Substitute.For(); private readonly ServiceCollection _services; /// Initializes the harness. public SiteGrpcEndpointValidationTests() { _services = new ServiceCollection().AddTestConfigurationTransaction(); _services.AddScoped(_ => _siteRepo); _services.AddScoped(_ => Substitute.For()); _siteRepo.GetSiteByIdAsync(7, Arg.Any()) .Returns(new Site("S", "S1") { Id = 7 }); } private IActorRef CreateActor() => Sys.ActorOf(Props.Create(() => new ManagementActor( _services.BuildServiceProvider(), NullLogger.Instance))); private static ManagementEnvelope Admin(object command) => new(new AuthenticatedUser("admin", "Admin", new[] { "Administrator" }, Array.Empty()), command, Guid.NewGuid().ToString("N")); /// Create with a scheme-less node-A address is rejected and nothing is saved. [Theory] [InlineData("scadabridge-site-a-node-a")] [InlineData("site-a-node-a:8083")] [InlineData("ftp://host:8083")] public void CreateSite_MalformedGrpcAddress_ReturnsReadableError(string bad) { var actor = CreateActor(); actor.Tell(Admin(new CreateSiteCommand("S", "S1", null, GrpcNodeAAddress: bad))); var error = ExpectMsg(TimeSpan.FromSeconds(5)); Assert.Contains("GrpcNodeAAddress", error.Error); Assert.Contains("http://", error.Error); _siteRepo.DidNotReceive().AddSiteAsync(Arg.Any(), Arg.Any()); } /// Update with a malformed node-B address is rejected and nothing is saved. [Fact] public void UpdateSite_MalformedGrpcNodeB_ReturnsReadableError() { var actor = CreateActor(); actor.Tell(Admin(new UpdateSiteCommand(7, "S", null, GrpcNodeAAddress: "http://a:8083", GrpcNodeBAddress: "b:8083"))); var error = ExpectMsg(TimeSpan.FromSeconds(5)); Assert.Contains("GrpcNodeBAddress", error.Error); _siteRepo.DidNotReceive().UpdateSiteAsync(Arg.Any(), Arg.Any()); } /// Both gRPC addresses are optional; valid http(s) URLs (and blanks) are accepted. [Fact] public void CreateSite_ValidOrEmptyGrpcAddresses_Succeeds() { var actor = CreateActor(); actor.Tell(Admin(new CreateSiteCommand("S", "S1", null, GrpcNodeAAddress: "http://site-a-node-a:8083", GrpcNodeBAddress: null))); ExpectMsg(TimeSpan.FromSeconds(5)); } }