using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using ScadaLink.Communication.Grpc; namespace ScadaLink.Communication.Tests.Grpc; public class SiteStreamGrpcClientFactoryTests { private readonly ILoggerFactory _loggerFactory = NullLoggerFactory.Instance; [Fact] public void GetOrCreate_ReturnsSameClientForSameSite() { using var factory = new SiteStreamGrpcClientFactory(_loggerFactory); var client1 = factory.GetOrCreate("site-a", "http://localhost:5100"); var client2 = factory.GetOrCreate("site-a", "http://localhost:5100"); Assert.Same(client1, client2); } [Fact] public void GetOrCreate_ReturnsDifferentClientsForDifferentSites() { using var factory = new SiteStreamGrpcClientFactory(_loggerFactory); var client1 = factory.GetOrCreate("site-a", "http://localhost:5100"); var client2 = factory.GetOrCreate("site-b", "http://localhost:5200"); Assert.NotSame(client1, client2); } [Fact] public async Task RemoveSite_DisposesClient() { var factory = new SiteStreamGrpcClientFactory(_loggerFactory); var client1 = factory.GetOrCreate("site-a", "http://localhost:5100"); await factory.RemoveSiteAsync("site-a"); // After removal, GetOrCreate should return a new instance var client2 = factory.GetOrCreate("site-a", "http://localhost:5100"); Assert.NotSame(client1, client2); } [Fact] public async Task RemoveSite_NonExistent_DoesNotThrow() { var factory = new SiteStreamGrpcClientFactory(_loggerFactory); await factory.RemoveSiteAsync("does-not-exist"); // Should not throw } [Fact] public async Task DisposeAsync_DisposesAllClients() { var factory = new SiteStreamGrpcClientFactory(_loggerFactory); factory.GetOrCreate("site-a", "http://localhost:5100"); factory.GetOrCreate("site-b", "http://localhost:5200"); await factory.DisposeAsync(); // After dispose, creating new clients should work (new instances) // This tests that Dispose doesn't throw } [Fact] public void GetOrCreate_EndpointChanged_ReturnsClientBoundToNewEndpoint() { // Communication-012 regression: when the same site is requested with a // *different* endpoint (the NodeA→NodeB failover flip), the factory must // hand back a client bound to the new endpoint, not the stale cached one. using var factory = new TrackingEndpointFactory(); var nodeA = factory.GetOrCreate("site-a", "http://localhost:5100"); var nodeB = factory.GetOrCreate("site-a", "http://localhost:5200"); Assert.NotSame(nodeA, nodeB); Assert.Equal("http://localhost:5100", nodeA.Endpoint); Assert.Equal("http://localhost:5200", nodeB.Endpoint); } [Fact] public void GetOrCreate_EndpointChanged_DisposesPriorClient() { // Communication-013 regression: a later edit to a site's gRPC address must // invalidate (and dispose) the stale cached client, so the corrected // endpoint takes effect without a central restart. using var factory = new TrackingEndpointFactory(); var first = (TrackingEndpointClient)factory.GetOrCreate("site-a", "http://localhost:5100"); var second = (TrackingEndpointClient)factory.GetOrCreate("site-a", "http://localhost:5200"); Assert.NotSame(first, second); Assert.True(first.Disposed, "stale client for the old endpoint should be disposed"); Assert.False(second.Disposed, "fresh client for the new endpoint should still be live"); } [Fact] public void GetOrCreate_SameEndpoint_DoesNotDisposeOrRecreate() { // Endpoint unchanged → the cached client is reused untouched. using var factory = new TrackingEndpointFactory(); var first = (TrackingEndpointClient)factory.GetOrCreate("site-a", "http://localhost:5100"); var second = (TrackingEndpointClient)factory.GetOrCreate("site-a", "http://localhost:5100"); Assert.Same(first, second); Assert.False(first.Disposed); } /// Test client that records its endpoint and disposal (no real channel). private sealed class TrackingEndpointClient : SiteStreamGrpcClient { public TrackingEndpointClient(string endpoint) : base(endpoint) { } public bool Disposed { get; private set; } public override void Dispose() => Disposed = true; public override ValueTask DisposeAsync() { Disposed = true; return ValueTask.CompletedTask; } } /// Factory that hands out endpoint-tracking clients. private sealed class TrackingEndpointFactory : SiteStreamGrpcClientFactory { public TrackingEndpointFactory() : base(NullLoggerFactory.Instance) { } protected override SiteStreamGrpcClient CreateClient(string grpcEndpoint) => new TrackingEndpointClient(grpcEndpoint); } }