feat: enforce account-scoped remote delivery semantics
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using NATS.Client.Core;
|
||||
using NATS.Server.Auth;
|
||||
using NATS.Server.Configuration;
|
||||
|
||||
namespace NATS.Server.Tests.Routes;
|
||||
|
||||
public class RouteAccountScopedDeliveryTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Remote_message_delivery_uses_target_account_sublist_not_global_sublist()
|
||||
{
|
||||
const string subject = "orders.created";
|
||||
await using var fixture = await RouteAccountDeliveryFixture.StartAsync();
|
||||
|
||||
await using var remoteAccountA = await fixture.ConnectAsync(fixture.ServerB, "a_sub");
|
||||
await using var remoteAccountB = await fixture.ConnectAsync(fixture.ServerB, "b_sub");
|
||||
await using var publisher = await fixture.ConnectAsync(fixture.ServerA, "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.WaitForRemoteInterestOnServerAAsync("A", subject);
|
||||
|
||||
await publisher.PublishAsync(subject, "from-route-a");
|
||||
|
||||
using var receiveTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
||||
var msgA = await subA.Msgs.ReadAsync(receiveTimeout.Token);
|
||||
msgA.Data.ShouldBe("from-route-a");
|
||||
|
||||
using var leakTimeout = new CancellationTokenSource(TimeSpan.FromMilliseconds(500));
|
||||
await Should.ThrowAsync<OperationCanceledException>(async () =>
|
||||
await subB.Msgs.ReadAsync(leakTimeout.Token));
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class RouteAccountDeliveryFixture : IAsyncDisposable
|
||||
{
|
||||
private readonly CancellationTokenSource _ctsA;
|
||||
private readonly CancellationTokenSource _ctsB;
|
||||
|
||||
private RouteAccountDeliveryFixture(NatsServer serverA, NatsServer serverB, CancellationTokenSource ctsA, CancellationTokenSource ctsB)
|
||||
{
|
||||
ServerA = serverA;
|
||||
ServerB = serverB;
|
||||
_ctsA = ctsA;
|
||||
_ctsB = ctsB;
|
||||
}
|
||||
|
||||
public NatsServer ServerA { get; }
|
||||
public NatsServer ServerB { get; }
|
||||
|
||||
public static async Task<RouteAccountDeliveryFixture> 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 optsA = new NatsOptions
|
||||
{
|
||||
Host = "127.0.0.1",
|
||||
Port = 0,
|
||||
Users = users,
|
||||
Cluster = new ClusterOptions
|
||||
{
|
||||
Name = Guid.NewGuid().ToString("N"),
|
||||
Host = "127.0.0.1",
|
||||
Port = 0,
|
||||
},
|
||||
};
|
||||
|
||||
var serverA = new NatsServer(optsA, NullLoggerFactory.Instance);
|
||||
var ctsA = new CancellationTokenSource();
|
||||
_ = serverA.StartAsync(ctsA.Token);
|
||||
await serverA.WaitForReadyAsync();
|
||||
|
||||
var optsB = new NatsOptions
|
||||
{
|
||||
Host = "127.0.0.1",
|
||||
Port = 0,
|
||||
Users = users,
|
||||
Cluster = new ClusterOptions
|
||||
{
|
||||
Name = Guid.NewGuid().ToString("N"),
|
||||
Host = "127.0.0.1",
|
||||
Port = 0,
|
||||
Routes = [serverA.ClusterListen!],
|
||||
},
|
||||
};
|
||||
|
||||
var serverB = new NatsServer(optsB, NullLoggerFactory.Instance);
|
||||
var ctsB = new CancellationTokenSource();
|
||||
_ = serverB.StartAsync(ctsB.Token);
|
||||
await serverB.WaitForReadyAsync();
|
||||
|
||||
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
||||
while (!timeout.IsCancellationRequested && (serverA.Stats.Routes == 0 || serverB.Stats.Routes == 0))
|
||||
await Task.Delay(50, timeout.Token).ContinueWith(_ => { }, TaskScheduler.Default);
|
||||
|
||||
return new RouteAccountDeliveryFixture(serverA, serverB, ctsA, ctsB);
|
||||
}
|
||||
|
||||
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 WaitForRemoteInterestOnServerAAsync(string account, string subject)
|
||||
{
|
||||
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
||||
while (!timeout.IsCancellationRequested)
|
||||
{
|
||||
if (ServerA.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 _ctsA.CancelAsync();
|
||||
await _ctsB.CancelAsync();
|
||||
ServerA.Dispose();
|
||||
ServerB.Dispose();
|
||||
_ctsA.Dispose();
|
||||
_ctsB.Dispose();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user