using Microsoft.Extensions.Logging.Abstractions; using NATS.Client.Core; using NATS.Server.Auth; using NATS.Server.Configuration; namespace NATS.Server.Tests.Gateways; public class GatewayAccountScopedDeliveryTests { [Fact] public async Task Remote_message_delivery_uses_target_account_sublist_not_global_sublist() { const string subject = "orders.created"; await using var fixture = await GatewayAccountDeliveryFixture.StartAsync(); await using var remoteAccountA = await fixture.ConnectAsync(fixture.Remote, "a_sub"); await using var remoteAccountB = await fixture.ConnectAsync(fixture.Remote, "b_sub"); await using var publisher = await fixture.ConnectAsync(fixture.Local, "a_pub"); await using var subA = await remoteAccountA.SubscribeCoreAsync(subject); await using var subB = await remoteAccountB.SubscribeCoreAsync(subject); await remoteAccountA.PingAsync(); await remoteAccountB.PingAsync(); await fixture.WaitForRemoteInterestOnLocalAsync("A", subject); await publisher.PublishAsync(subject, "from-gateway-a"); using var receiveTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); var msgA = await subA.Msgs.ReadAsync(receiveTimeout.Token); msgA.Data.ShouldBe("from-gateway-a"); using var leakTimeout = new CancellationTokenSource(TimeSpan.FromMilliseconds(500)); await Should.ThrowAsync(async () => await subB.Msgs.ReadAsync(leakTimeout.Token)); } } internal sealed class GatewayAccountDeliveryFixture : IAsyncDisposable { private readonly CancellationTokenSource _localCts; private readonly CancellationTokenSource _remoteCts; private GatewayAccountDeliveryFixture(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 StartAsync() { var users = new User[] { new() { Username = "a_pub", Password = "pass", Account = "A" }, new() { Username = "a_sub", Password = "pass", Account = "A" }, new() { Username = "b_sub", Password = "pass", Account = "B" }, }; var localOptions = new NatsOptions { Host = "127.0.0.1", Port = 0, Users = users, 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, Users = users, 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 GatewayAccountDeliveryFixture(local, remote, localCts, remoteCts); } public async Task ConnectAsync(NatsServer server, string username) { var connection = new NatsConnection(new NatsOpts { Url = $"nats://{username}:pass@127.0.0.1:{server.Port}", }); await connection.ConnectAsync(); return connection; } public async Task WaitForRemoteInterestOnLocalAsync(string account, string subject) { using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); while (!timeout.IsCancellationRequested) { if (Local.HasRemoteInterest(account, subject)) return; await Task.Delay(50, timeout.Token).ContinueWith(_ => { }, TaskScheduler.Default); } throw new TimeoutException($"Timed out waiting for remote interest {account}:{subject}."); } public async ValueTask DisposeAsync() { await _localCts.CancelAsync(); await _remoteCts.CancelAsync(); Local.Dispose(); Remote.Dispose(); _localCts.Dispose(); _remoteCts.Dispose(); } }