using FluentAssertions; using Microsoft.Extensions.Options; using StellaOps.Router.Common.Enums; using StellaOps.Router.Common.Models; using StellaOps.Router.Transport.RabbitMq.Tests.Fixtures; using Xunit; namespace StellaOps.Router.Transport.RabbitMq.Tests; /// /// Integration tests for RabbitMQ transport using Testcontainers. /// These tests verify real broker communication scenarios. /// [Collection(RabbitMqIntegrationTestCollection.Name)] public sealed class RabbitMqIntegrationTests : IAsyncLifetime { private readonly RabbitMqContainerFixture _fixture; private RabbitMqTransportServer? _server; private RabbitMqTransportClient? _client; public RabbitMqIntegrationTests(RabbitMqContainerFixture fixture) { _fixture = fixture; } public async Task InitializeAsync() { // Server and client will be created per-test as needed await Task.CompletedTask; } public async Task DisposeAsync() { if (_client is not null) { await _client.DisposeAsync(); } if (_server is not null) { await _server.DisposeAsync(); } } private RabbitMqTransportServer CreateServer(string? nodeId = null) { var options = _fixture.CreateOptions(nodeId: nodeId ?? $"gw-{Guid.NewGuid():N}"[..12]); return new RabbitMqTransportServer( Options.Create(options), _fixture.GetLogger()); } private RabbitMqTransportClient CreateClient(string? instanceId = null, string? nodeId = null) { var options = _fixture.CreateOptions( instanceId: instanceId ?? $"svc-{Guid.NewGuid():N}"[..12], nodeId: nodeId); return new RabbitMqTransportClient( Options.Create(options), _fixture.GetLogger()); } #region Connection Tests [RabbitMqIntegrationFact] public async Task ServerStartAsync_WithRealBroker_Succeeds() { // Arrange _server = CreateServer(); // Act var act = async () => await _server.StartAsync(CancellationToken.None); // Assert await act.Should().NotThrowAsync(); _server.ConnectionCount.Should().Be(0); } [RabbitMqIntegrationFact] public async Task ServerStopAsync_AfterStart_Succeeds() { // Arrange _server = CreateServer(); await _server.StartAsync(CancellationToken.None); // Act var act = async () => await _server.StopAsync(CancellationToken.None); // Assert await act.Should().NotThrowAsync(); } [RabbitMqIntegrationFact] public async Task ClientConnectAsync_WithRealBroker_Succeeds() { // Arrange _client = CreateClient(); var instance = new InstanceDescriptor { InstanceId = "test-instance", ServiceName = "test-service", Version = "1.0.0", Region = "us-east-1" }; // Act var act = async () => await _client.ConnectAsync(instance, [], CancellationToken.None); // Assert await act.Should().NotThrowAsync(); } [RabbitMqIntegrationFact] public async Task ClientDisconnectAsync_AfterConnect_Succeeds() { // Arrange _client = CreateClient(); var instance = new InstanceDescriptor { InstanceId = "test-instance", ServiceName = "test-service", Version = "1.0.0", Region = "us-east-1" }; await _client.ConnectAsync(instance, [], CancellationToken.None); // Act var act = async () => await _client.DisconnectAsync(); // Assert await act.Should().NotThrowAsync(); } #endregion #region Hello Frame Tests [RabbitMqIntegrationFact] public async Task ClientConnectAsync_SendsHelloFrame_ServerReceives() { // Arrange const string nodeId = "gw-hello-test"; _server = CreateServer(nodeId); _client = CreateClient("svc-hello-test", nodeId: nodeId); Frame? receivedFrame = null; string? receivedConnectionId = null; var frameReceived = new TaskCompletionSource(); _server.OnFrame += (connectionId, frame) => { if (frame.Type == FrameType.Hello) { receivedConnectionId = connectionId; receivedFrame = frame; frameReceived.TrySetResult(true); } }; await _server.StartAsync(CancellationToken.None); var instance = new InstanceDescriptor { InstanceId = "svc-hello-test", ServiceName = "test-service", Version = "1.0.0", Region = "us-east-1" }; // Act await _client.ConnectAsync(instance, [], CancellationToken.None); // Assert - wait for frame with timeout using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); var completed = await Task.WhenAny(frameReceived.Task, Task.Delay(Timeout.Infinite, cts.Token)); receivedFrame.Should().NotBeNull(); receivedFrame!.Type.Should().Be(FrameType.Hello); receivedConnectionId.Should().NotBeNullOrEmpty(); } #endregion #region Heartbeat Tests [RabbitMqIntegrationFact] public async Task ClientSendHeartbeatAsync_RealBroker_Succeeds() { // Arrange _client = CreateClient(); var instance = new InstanceDescriptor { InstanceId = "test-instance", ServiceName = "test-service", Version = "1.0.0", Region = "us-east-1" }; await _client.ConnectAsync(instance, [], CancellationToken.None); var heartbeat = new HeartbeatPayload { InstanceId = "test-instance", Status = InstanceHealthStatus.Healthy, InFlightRequestCount = 0, ErrorRate = 0.0, TimestampUtc = DateTime.UtcNow }; // Act var act = async () => await _client.SendHeartbeatAsync(heartbeat, CancellationToken.None); // Assert await act.Should().NotThrowAsync(); } [RabbitMqIntegrationFact] public async Task ServerReceivesHeartbeat_UpdatesLastHeartbeatUtc() { // Arrange const string nodeId = "gw-heartbeat-test"; _server = CreateServer(nodeId); _client = CreateClient("svc-heartbeat-test", nodeId: nodeId); var heartbeatReceived = new TaskCompletionSource(); _server.OnFrame += (connectionId, frame) => { if (frame.Type == FrameType.Heartbeat) { heartbeatReceived.TrySetResult(true); } }; await _server.StartAsync(CancellationToken.None); var instance = new InstanceDescriptor { InstanceId = "svc-heartbeat-test", ServiceName = "test-service", Version = "1.0.0", Region = "us-east-1" }; await _client.ConnectAsync(instance, [], CancellationToken.None); // Wait for HELLO to establish connection await Task.Delay(500); var beforeHeartbeat = DateTime.UtcNow; // Act var heartbeat = new HeartbeatPayload { InstanceId = "svc-heartbeat-test", Status = InstanceHealthStatus.Healthy, InFlightRequestCount = 0, ErrorRate = 0.0, TimestampUtc = DateTime.UtcNow }; await _client.SendHeartbeatAsync(heartbeat, CancellationToken.None); // Assert - wait for heartbeat with timeout using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); try { await Task.WhenAny(heartbeatReceived.Task, Task.Delay(Timeout.Infinite, cts.Token)); } catch (OperationCanceledException) { // Heartbeat may not arrive in time - this is OK for the test } // The heartbeat should have been received (may not always work due to timing) // This test validates the flow works without errors } #endregion #region Connection Recovery Tests [RabbitMqIntegrationFact] public async Task ConnectionRecovery_BrokerRestart_AllowsPublishingAndConsumingAgain() { // Arrange const string nodeId = "gw-recovery-test"; const string instanceId = "svc-recovery-test"; _server = CreateServer(nodeId); _client = CreateClient(instanceId, nodeId: nodeId); await _server.StartAsync(CancellationToken.None); var instance = new InstanceDescriptor { InstanceId = instanceId, ServiceName = "test-service", Version = "1.0.0", Region = "us-east-1" }; await _client.ConnectAsync(instance, [], CancellationToken.None); await EventuallyAsync( () => _server.ConnectionCount > 0, timeout: TimeSpan.FromSeconds(15)); // Act: force broker restart and wait for client/server recovery. await _fixture.RestartAsync(); var heartbeat = new HeartbeatPayload { InstanceId = instanceId, Status = InstanceHealthStatus.Healthy, InFlightRequestCount = 0, ErrorRate = 0, TimestampUtc = DateTime.UtcNow }; var heartbeatReceived = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); _server.OnFrame += (_, frame) => { if (frame.Type == FrameType.Heartbeat) { heartbeatReceived.TrySetResult(true); } }; await EventuallyAsync( async () => { await _client.SendHeartbeatAsync(heartbeat, CancellationToken.None); return true; }, timeout: TimeSpan.FromSeconds(30), swallowExceptions: true); await heartbeatReceived.Task.WaitAsync(TimeSpan.FromSeconds(30)); } #endregion private static async Task EventuallyAsync( Func predicate, TimeSpan timeout, TimeSpan? pollInterval = null) { pollInterval ??= TimeSpan.FromMilliseconds(250); 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); } private static async Task EventuallyAsync( Func> predicate, TimeSpan timeout, bool swallowExceptions, TimeSpan? pollInterval = null) { pollInterval ??= TimeSpan.FromMilliseconds(500); var deadline = DateTime.UtcNow + timeout; while (DateTime.UtcNow < deadline) { try { if (await predicate()) { return; } } catch when (swallowExceptions) { // Retry } await Task.Delay(pollInterval.Value); } (await predicate()).Should().BeTrue("condition should become true within {0}", timeout); } #region Queue Declaration Tests [RabbitMqIntegrationFact] public async Task ServerStartAsync_CreatesExchangesAndQueues() { // Arrange _server = CreateServer("gw-queue-test"); // Act await _server.StartAsync(CancellationToken.None); // Assert - if we got here without exception, queues were created // We can't easily verify queue existence without management API // but the lack of exception indicates success } [RabbitMqIntegrationFact] public async Task ClientConnectAsync_CreatesResponseQueue() { // Arrange _client = CreateClient("svc-queue-test"); var instance = new InstanceDescriptor { InstanceId = "svc-queue-test", ServiceName = "test-service", Version = "1.0.0", Region = "us-east-1" }; // Act await _client.ConnectAsync(instance, [], CancellationToken.None); // Assert - if we got here without exception, queue was created } #endregion #region Auto-Delete Queue Tests [RabbitMqIntegrationFact] public async Task AutoDeleteQueues_AreCleanedUpOnDisconnect() { // Arrange var options = _fixture.CreateOptions(instanceId: "svc-autodelete"); options.AutoDeleteQueues = true; _client = new RabbitMqTransportClient( Options.Create(options), _fixture.GetLogger()); var instance = new InstanceDescriptor { InstanceId = "svc-autodelete", ServiceName = "test-service", Version = "1.0.0", Region = "us-east-1" }; await _client.ConnectAsync(instance, [], CancellationToken.None); // Act await _client.DisconnectAsync(); await _client.DisposeAsync(); _client = null; // Assert - queue should be auto-deleted (no way to verify without management API) // Success is indicated by no exceptions } #endregion #region Prefetch Tests [RabbitMqIntegrationFact] public async Task PrefetchCount_IsAppliedOnConnect() { // Arrange var options = _fixture.CreateOptions(instanceId: "svc-prefetch"); options.PrefetchCount = 50; _client = new RabbitMqTransportClient( Options.Create(options), _fixture.GetLogger()); var instance = new InstanceDescriptor { InstanceId = "svc-prefetch", ServiceName = "test-service", Version = "1.0.0", Region = "us-east-1" }; // Act await _client.ConnectAsync(instance, [], CancellationToken.None); // Assert - success indicates prefetch was set (no exception) } #endregion #region Multiple Connections Tests [RabbitMqIntegrationFact] public async Task MultipleClients_CanConnectSimultaneously() { // Arrange var client1 = CreateClient("svc-multi-1"); var client2 = CreateClient("svc-multi-2"); try { var instance1 = new InstanceDescriptor { InstanceId = "svc-multi-1", ServiceName = "test-service", Version = "1.0.0", Region = "us-east-1" }; var instance2 = new InstanceDescriptor { InstanceId = "svc-multi-2", ServiceName = "test-service", Version = "1.0.0", Region = "us-east-1" }; // Act await Task.WhenAll( client1.ConnectAsync(instance1, [], CancellationToken.None), client2.ConnectAsync(instance2, [], CancellationToken.None)); // Assert - both connections succeeded } finally { await client1.DisposeAsync(); await client2.DisposeAsync(); } } #endregion }