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));
}
}