Files
natsdotnet/tests/NATS.Server.LeafNodes.Tests/LeafNodes/LeafAccountScopedDeliveryTests.cs
Joseph Doherty 3f7d896a34 refactor: extract NATS.Server.LeafNodes.Tests project
Move 28 leaf node test files from NATS.Server.Tests into a dedicated
NATS.Server.LeafNodes.Tests project. Update namespaces, add
InternalsVisibleTo, register in solution file. Replace all Task.Delay
polling loops with PollHelper.WaitUntilAsync/YieldForAsync from
TestUtilities. Replace private ReadUntilAsync in LeafProtocolTests
with SocketTestHelper.ReadUntilAsync.

All 281 tests pass.
2026-03-12 15:23:33 -04:00

129 lines
4.4 KiB
C#

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<string>(subject);
await using var subB = await remoteAccountB.SubscribeCoreAsync<string>(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<OperationCanceledException>(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<LeafAccountDeliveryFixture> 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<NatsConnection> 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();
}
}