Move 25 gateway-related test files from NATS.Server.Tests into a dedicated NATS.Server.Gateways.Tests project. Update namespaces, replace private ReadUntilAsync with SocketTestHelper from TestUtilities, inline TestServerFactory usage, add InternalsVisibleTo, and register the project in the solution file. All 261 tests pass.
186 lines
6.6 KiB
C#
186 lines
6.6 KiB
C#
using Microsoft.Extensions.Logging.Abstractions;
|
|
using NATS.Client.Core;
|
|
using NATS.Server.Configuration;
|
|
|
|
namespace NATS.Server.Gateways.Tests.Gateways;
|
|
|
|
/// <summary>
|
|
/// Ports TestGatewayBasic and TestGatewayDoesntSendBackToItself from
|
|
/// golang/nats-server/server/gateway_test.go.
|
|
/// </summary>
|
|
public class GatewayBasicTests
|
|
{
|
|
[Fact]
|
|
public async Task Gateway_forwards_messages_between_clusters()
|
|
{
|
|
// Reference: TestGatewayBasic (gateway_test.go:399)
|
|
// Start LOCAL and REMOTE gateway servers. Subscribe on REMOTE,
|
|
// publish on LOCAL, verify message arrives on REMOTE via gateway.
|
|
await using var fixture = await TwoClusterFixture.StartAsync();
|
|
|
|
await using var subscriber = new NatsConnection(new NatsOpts
|
|
{
|
|
Url = $"nats://127.0.0.1:{fixture.Remote.Port}",
|
|
});
|
|
await subscriber.ConnectAsync();
|
|
|
|
await using var publisher = new NatsConnection(new NatsOpts
|
|
{
|
|
Url = $"nats://127.0.0.1:{fixture.Local.Port}",
|
|
});
|
|
await publisher.ConnectAsync();
|
|
|
|
await using var sub = await subscriber.SubscribeCoreAsync<string>("gw.test");
|
|
await subscriber.PingAsync();
|
|
|
|
// Wait for remote interest to propagate through gateway
|
|
await fixture.WaitForRemoteInterestOnLocalAsync("gw.test");
|
|
|
|
await publisher.PublishAsync("gw.test", "hello-from-local");
|
|
|
|
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
|
var msg = await sub.Msgs.ReadAsync(timeout.Token);
|
|
msg.Data.ShouldBe("hello-from-local");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Gateway_does_not_echo_back_to_origin()
|
|
{
|
|
// Reference: TestGatewayDoesntSendBackToItself (gateway_test.go:2150)
|
|
// Subscribe on REMOTE and LOCAL, publish on LOCAL. Expect exactly 2
|
|
// deliveries (one local, one via gateway to REMOTE) — no echo cycle.
|
|
await using var fixture = await TwoClusterFixture.StartAsync();
|
|
|
|
await using var remoteConn = new NatsConnection(new NatsOpts
|
|
{
|
|
Url = $"nats://127.0.0.1:{fixture.Remote.Port}",
|
|
});
|
|
await remoteConn.ConnectAsync();
|
|
|
|
await using var localConn = new NatsConnection(new NatsOpts
|
|
{
|
|
Url = $"nats://127.0.0.1:{fixture.Local.Port}",
|
|
});
|
|
await localConn.ConnectAsync();
|
|
|
|
await using var remoteSub = await remoteConn.SubscribeCoreAsync<string>("foo");
|
|
await remoteConn.PingAsync();
|
|
|
|
await using var localSub = await localConn.SubscribeCoreAsync<string>("foo");
|
|
await localConn.PingAsync();
|
|
|
|
// Wait for remote interest to propagate through gateway
|
|
await fixture.WaitForRemoteInterestOnLocalAsync("foo");
|
|
|
|
await localConn.PublishAsync("foo", "cycle");
|
|
await localConn.PingAsync();
|
|
|
|
// Should receive exactly 2 messages: one on local sub, one on remote sub.
|
|
// If there is a cycle, we'd see many more after a short delay.
|
|
using var receiveTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
|
|
|
var localMsg = await localSub.Msgs.ReadAsync(receiveTimeout.Token);
|
|
localMsg.Data.ShouldBe("cycle");
|
|
|
|
var remoteMsg = await remoteSub.Msgs.ReadAsync(receiveTimeout.Token);
|
|
remoteMsg.Data.ShouldBe("cycle");
|
|
|
|
// Wait a bit to see if any echo/cycle messages arrive
|
|
await Task.Delay(TimeSpan.FromMilliseconds(200));
|
|
|
|
// Try to read more — should time out because there should be no more messages
|
|
using var noMoreTimeout = new CancellationTokenSource(TimeSpan.FromMilliseconds(300));
|
|
await Should.ThrowAsync<OperationCanceledException>(async () =>
|
|
await localSub.Msgs.ReadAsync(noMoreTimeout.Token));
|
|
|
|
using var noMoreTimeout2 = new CancellationTokenSource(TimeSpan.FromMilliseconds(300));
|
|
await Should.ThrowAsync<OperationCanceledException>(async () =>
|
|
await remoteSub.Msgs.ReadAsync(noMoreTimeout2.Token));
|
|
}
|
|
}
|
|
|
|
internal sealed class TwoClusterFixture : IAsyncDisposable
|
|
{
|
|
private readonly CancellationTokenSource _localCts;
|
|
private readonly CancellationTokenSource _remoteCts;
|
|
|
|
private TwoClusterFixture(NatsServer local, NatsServer remote, CancellationTokenSource localCts, CancellationTokenSource remoteCts)
|
|
{
|
|
Local = local;
|
|
Remote = remote;
|
|
_localCts = localCts;
|
|
_remoteCts = remoteCts;
|
|
}
|
|
|
|
public NatsServer Local { get; }
|
|
public NatsServer Remote { get; }
|
|
|
|
public static async Task<TwoClusterFixture> StartAsync()
|
|
{
|
|
var localOptions = new NatsOptions
|
|
{
|
|
Host = "127.0.0.1",
|
|
Port = 0,
|
|
Gateway = new GatewayOptions
|
|
{
|
|
Name = "LOCAL",
|
|
Host = "127.0.0.1",
|
|
Port = 0,
|
|
},
|
|
};
|
|
|
|
var local = new NatsServer(localOptions, NullLoggerFactory.Instance);
|
|
var localCts = new CancellationTokenSource();
|
|
_ = local.StartAsync(localCts.Token);
|
|
await local.WaitForReadyAsync();
|
|
|
|
var remoteOptions = new NatsOptions
|
|
{
|
|
Host = "127.0.0.1",
|
|
Port = 0,
|
|
Gateway = new GatewayOptions
|
|
{
|
|
Name = "REMOTE",
|
|
Host = "127.0.0.1",
|
|
Port = 0,
|
|
Remotes = [local.GatewayListen!],
|
|
},
|
|
};
|
|
|
|
var remote = new NatsServer(remoteOptions, NullLoggerFactory.Instance);
|
|
var remoteCts = new CancellationTokenSource();
|
|
_ = remote.StartAsync(remoteCts.Token);
|
|
await remote.WaitForReadyAsync();
|
|
|
|
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
|
while (!timeout.IsCancellationRequested && (local.Stats.Gateways == 0 || remote.Stats.Gateways == 0))
|
|
await Task.Delay(50, timeout.Token).ContinueWith(_ => { }, TaskScheduler.Default);
|
|
|
|
return new TwoClusterFixture(local, remote, localCts, remoteCts);
|
|
}
|
|
|
|
public async Task WaitForRemoteInterestOnLocalAsync(string subject)
|
|
{
|
|
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
|
while (!timeout.IsCancellationRequested)
|
|
{
|
|
if (Local.HasRemoteInterest(subject))
|
|
return;
|
|
|
|
await Task.Delay(50, timeout.Token).ContinueWith(_ => { }, TaskScheduler.Default);
|
|
}
|
|
|
|
throw new TimeoutException($"Timed out waiting for remote interest on subject '{subject}'.");
|
|
}
|
|
|
|
public async ValueTask DisposeAsync()
|
|
{
|
|
await _localCts.CancelAsync();
|
|
await _remoteCts.CancelAsync();
|
|
Local.Dispose();
|
|
Remote.Dispose();
|
|
_localCts.Dispose();
|
|
_remoteCts.Dispose();
|
|
}
|
|
}
|