using FluentAssertions; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using StellaOps.Router.Common.Enums; using StellaOps.Router.Common.Models; using StellaOps.Router.Gateway.Services; using StellaOps.Router.Gateway.State; using StellaOps.Router.Transport.InMemory; using Xunit; namespace StellaOps.Router.Gateway.Tests; public sealed class ConnectionManagerTests { [Fact] public async Task StartAsync_WhenHelloInvalid_RejectsAndClosesChannel() { var (manager, server, registry, routingState) = Create(); var client = CreateClient(registry, server); try { await manager.StartAsync(CancellationToken.None); var invalid = new InstanceDescriptor { InstanceId = "inv-1", ServiceName = "", Version = "1.0.0", Region = "eu1" }; await client.ConnectAsync( invalid, endpoints: [ new EndpointDescriptor { ServiceName = "inventory", Version = "1.0.0", Method = "GET", Path = "/items" } ], CancellationToken.None); await EventuallyAsync( () => registry.Count == 0, timeout: TimeSpan.FromSeconds(5)); routingState.GetAllConnections().Should().BeEmpty(); } finally { client.Dispose(); await manager.StopAsync(CancellationToken.None); server.Dispose(); registry.Dispose(); } } [Fact] public async Task WhenClientDisconnects_RemovesFromRoutingState() { var (manager, server, registry, routingState) = Create(); var client = CreateClient(registry, server); try { await manager.StartAsync(CancellationToken.None); await client.ConnectAsync( CreateInstance("inventory", "1.0.0", "inv-1"), endpoints: [ new EndpointDescriptor { ServiceName = "inventory", Version = "1.0.0", Method = "GET", Path = "/items" } ], CancellationToken.None); await EventuallyAsync( () => routingState.GetAllConnections().Count == 1, timeout: TimeSpan.FromSeconds(5)); client.Dispose(); await EventuallyAsync( () => routingState.GetAllConnections().Count == 0, timeout: TimeSpan.FromSeconds(5)); } finally { client.Dispose(); await manager.StopAsync(CancellationToken.None); server.Dispose(); registry.Dispose(); } } [Fact] public async Task WhenMultipleClientsConnect_TracksAndCleansIndependently() { var (manager, server, registry, routingState) = Create(); var client1 = CreateClient(registry, server); var client2 = CreateClient(registry, server); try { await manager.StartAsync(CancellationToken.None); var endpoints = new[] { new EndpointDescriptor { ServiceName = "inventory", Version = "1.0.0", Method = "GET", Path = "/items" } }; await client1.ConnectAsync( CreateInstance("inventory", "1.0.0", "inv-1"), endpoints, CancellationToken.None); await client2.ConnectAsync( CreateInstance("inventory", "1.0.0", "inv-2"), endpoints, CancellationToken.None); await EventuallyAsync( () => routingState.GetConnectionsFor("inventory", "1.0.0", "GET", "/items").Count == 2, timeout: TimeSpan.FromSeconds(5)); var before = routingState.GetConnectionsFor("inventory", "1.0.0", "GET", "/items") .Select(c => c.Instance.InstanceId) .ToHashSet(StringComparer.Ordinal); before.Should().BeEquivalentTo(new[] { "inv-1", "inv-2" }); client1.Dispose(); await EventuallyAsync( () => routingState.GetConnectionsFor("inventory", "1.0.0", "GET", "/items").Count == 1, timeout: TimeSpan.FromSeconds(5)); var after = routingState.GetConnectionsFor("inventory", "1.0.0", "GET", "/items") .Single() .Instance.InstanceId; after.Should().Be("inv-2"); } finally { client1.Dispose(); client2.Dispose(); await manager.StopAsync(CancellationToken.None); server.Dispose(); registry.Dispose(); } } private static (ConnectionManager Manager, InMemoryTransportServer Server, InMemoryConnectionRegistry Registry, InMemoryRoutingState RoutingState) Create() { var registry = new InMemoryConnectionRegistry(); var server = new InMemoryTransportServer( registry, Options.Create(new InMemoryTransportOptions()), NullLogger.Instance); var routingState = new InMemoryRoutingState(); var manager = new ConnectionManager( server, registry, routingState, NullLogger.Instance); return (manager, server, registry, routingState); } private static InMemoryTransportClient CreateClient(InMemoryConnectionRegistry registry, InMemoryTransportServer server) { return new InMemoryTransportClient( registry, Options.Create(new InMemoryTransportOptions()), NullLogger.Instance, server); } private static InstanceDescriptor CreateInstance(string serviceName, string version, string instanceId) { return new InstanceDescriptor { InstanceId = instanceId, ServiceName = serviceName, Version = version, Region = "eu1" }; } private static async Task EventuallyAsync(Func predicate, TimeSpan timeout, TimeSpan? pollInterval = null) { pollInterval ??= TimeSpan.FromMilliseconds(25); var deadline = DateTime.UtcNow + timeout; while (DateTime.UtcNow < deadline) { if (predicate()) { return; } await Task.Delay(pollInterval.Value); } predicate().Should().BeTrue("condition should become true within {0}", timeout); } }