using Microsoft.Extensions.Logging.Abstractions; using NATS.Client.Core; using NATS.Server.Auth; using NATS.Server.Configuration; using NATS.Server.TestUtilities; namespace NATS.Server.LeafNodes.Tests.LeafNodes; public class LeafAccountScopedDeliveryTests { [Fact] public async Task Remote_message_delivery_uses_target_account_sublist_not_global_sublist() { const string subject = "orders.created"; await using var fixture = await LeafAccountDeliveryFixture.StartAsync(); await using var remoteAccountA = await fixture.ConnectAsync(fixture.Spoke, "a_sub"); await using var remoteAccountB = await fixture.ConnectAsync(fixture.Spoke, "b_sub"); await using var publisher = await fixture.ConnectAsync(fixture.Hub, "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.WaitForRemoteInterestOnHubAsync("A", subject); await publisher.PublishAsync(subject, "from-leaf-a"); using var receiveTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); var msgA = await subA.Msgs.ReadAsync(receiveTimeout.Token); msgA.Data.ShouldBe("from-leaf-a"); using var leakTimeout = new CancellationTokenSource(TimeSpan.FromMilliseconds(500)); await Should.ThrowAsync(async () => await subB.Msgs.ReadAsync(leakTimeout.Token)); } } internal sealed class LeafAccountDeliveryFixture : IAsyncDisposable { private readonly CancellationTokenSource _hubCts; private readonly CancellationTokenSource _spokeCts; private LeafAccountDeliveryFixture(NatsServer hub, NatsServer spoke, CancellationTokenSource hubCts, CancellationTokenSource spokeCts) { Hub = hub; Spoke = spoke; _hubCts = hubCts; _spokeCts = spokeCts; } public NatsServer Hub { get; } public NatsServer Spoke { 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 hubOptions = new NatsOptions { Host = "127.0.0.1", Port = 0, Users = users, LeafNode = new LeafNodeOptions { Host = "127.0.0.1", Port = 0, }, }; var hub = new NatsServer(hubOptions, NullLoggerFactory.Instance); var hubCts = new CancellationTokenSource(); _ = hub.StartAsync(hubCts.Token); await hub.WaitForReadyAsync(); var spokeOptions = new NatsOptions { Host = "127.0.0.1", Port = 0, Users = users, LeafNode = new LeafNodeOptions { Host = "127.0.0.1", Port = 0, Remotes = [hub.LeafListen!], }, }; var spoke = new NatsServer(spokeOptions, NullLoggerFactory.Instance); var spokeCts = new CancellationTokenSource(); _ = spoke.StartAsync(spokeCts.Token); await spoke.WaitForReadyAsync(); await PollHelper.WaitUntilAsync(() => !((hub.Stats.Leafs == 0 || spoke.Stats.Leafs == 0)), timeoutMs: 5000); return new LeafAccountDeliveryFixture(hub, spoke, hubCts, spokeCts); } 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 WaitForRemoteInterestOnHubAsync(string account, string subject) { await PollHelper.WaitOrThrowAsync(() => Hub.HasRemoteInterest(account, subject), $"Timed out waiting for remote interest {account}:{subject}.", timeoutMs: 5000); } public async ValueTask DisposeAsync() { await _hubCts.CancelAsync(); await _spokeCts.CancelAsync(); Hub.Dispose(); Spoke.Dispose(); _hubCts.Dispose(); _spokeCts.Dispose(); } }