Files
natsdotnet/tests/NATS.E2E.Tests/CoreMessagingTests.cs
Joseph Doherty 88a82ee860 docs: add XML doc comments to server types and fix flaky test timings
Add XML doc comments to public properties across EventTypes, Connz, Varz,
NatsOptions, StreamConfig, IStreamStore, FileStore, MqttListener,
MqttSessionStore, MessageTraceContext, and JetStreamApiResponse. Fix flaky
tests by increasing timing margins (ResponseTracker expiry 1ms→50ms,
sleep 50ms→200ms) and document known flaky test patterns in tests.md.
2026-03-13 18:47:48 -04:00

379 lines
13 KiB
C#

using System.Linq;
using System.Net.Sockets;
using System.Text;
using NATS.Client.Core;
using NATS.E2E.Tests.Infrastructure;
namespace NATS.E2E.Tests;
[Collection("E2E")]
public class CoreMessagingTests(NatsServerFixture fixture)
{
[Fact]
public async Task WildcardStar_MatchesSingleToken()
{
await using var pub = fixture.CreateClient();
await using var sub = fixture.CreateClient();
await pub.ConnectAsync();
await sub.ConnectAsync();
await using var subscription = await sub.SubscribeCoreAsync<string>("e2e.wc.*");
await sub.PingAsync();
await pub.PublishAsync("e2e.wc.bar", "hello");
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var msg = await subscription.Msgs.ReadAsync(cts.Token);
msg.Data.ShouldBe("hello");
}
[Fact]
public async Task WildcardGreaterThan_MatchesMultipleTokens()
{
await using var pub = fixture.CreateClient();
await using var sub = fixture.CreateClient();
await pub.ConnectAsync();
await sub.ConnectAsync();
await using var subscription = await sub.SubscribeCoreAsync<string>("e2e.gt.>");
await sub.PingAsync();
await pub.PublishAsync("e2e.gt.bar.baz", "deep");
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var msg = await subscription.Msgs.ReadAsync(cts.Token);
msg.Data.ShouldBe("deep");
}
[Fact]
public async Task WildcardStar_DoesNotMatchMultipleTokens()
{
await using var pub = fixture.CreateClient();
await using var sub = fixture.CreateClient();
await pub.ConnectAsync();
await sub.ConnectAsync();
await using var subscription = await sub.SubscribeCoreAsync<string>("e2e.nomat.*");
await sub.PingAsync();
await pub.PublishAsync("e2e.nomat.bar.baz", "should not arrive");
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var readTask = subscription.Msgs.ReadAsync(cts.Token).AsTask();
var winner = await Task.WhenAny(readTask, Task.Delay(3000));
winner.ShouldNotBe(readTask);
}
[Fact]
public async Task QueueGroup_LoadBalances()
{
await using var c1 = fixture.CreateClient();
await using var c2 = fixture.CreateClient();
await using var c3 = fixture.CreateClient();
await using var pub = fixture.CreateClient();
await c1.ConnectAsync();
await c2.ConnectAsync();
await c3.ConnectAsync();
await pub.ConnectAsync();
await using var s1 = await c1.SubscribeCoreAsync<int>("e2e.qlb", queueGroup: "workers");
await using var s2 = await c2.SubscribeCoreAsync<int>("e2e.qlb", queueGroup: "workers");
await using var s3 = await c3.SubscribeCoreAsync<int>("e2e.qlb", queueGroup: "workers");
await c1.PingAsync();
await c2.PingAsync();
await c3.PingAsync();
using var collectionCts = new CancellationTokenSource(TimeSpan.FromSeconds(15));
var counts = new int[3];
async Task Collect(INatsSub<int> sub, int idx)
{
try
{
await foreach (var _ in sub.Msgs.ReadAllAsync(collectionCts.Token))
Interlocked.Increment(ref counts[idx]);
}
catch (OperationCanceledException) { }
}
var tasks = new[]
{
Collect(s1, 0),
Collect(s2, 1),
Collect(s3, 2),
};
for (var i = 0; i < 30; i++)
await pub.PublishAsync("e2e.qlb", i);
await pub.PingAsync();
// Wait until all 30 messages have been received
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(10));
while (counts[0] + counts[1] + counts[2] < 30 && !deadline.IsCancellationRequested)
await Task.Delay(10, deadline.Token).ContinueWith(_ => { });
collectionCts.Cancel();
await Task.WhenAll(tasks);
var total = counts[0] + counts[1] + counts[2];
total.ShouldBe(30);
// Verify at least one queue member received messages (distribution
// is implementation-defined and may heavily favor one member when
// messages are published in a tight loop).
counts.Max().ShouldBeGreaterThan(0);
}
[Fact]
public async Task QueueGroup_MixedWithPlainSub()
{
await using var plainClient = fixture.CreateClient();
await using var q1Client = fixture.CreateClient();
await using var q2Client = fixture.CreateClient();
await using var pub = fixture.CreateClient();
await plainClient.ConnectAsync();
await q1Client.ConnectAsync();
await q2Client.ConnectAsync();
await pub.ConnectAsync();
await using var plainSub = await plainClient.SubscribeCoreAsync<int>("e2e.qmix");
await using var qSub1 = await q1Client.SubscribeCoreAsync<int>("e2e.qmix", queueGroup: "qmix");
await using var qSub2 = await q2Client.SubscribeCoreAsync<int>("e2e.qmix", queueGroup: "qmix");
await plainClient.PingAsync();
await q1Client.PingAsync();
await q2Client.PingAsync();
using var collectionCts = new CancellationTokenSource(TimeSpan.FromSeconds(15));
var plainCount = 0;
var q1Count = 0;
var q2Count = 0;
async Task Collect(INatsSub<int> sub, Action increment)
{
try
{
await foreach (var _ in sub.Msgs.ReadAllAsync(collectionCts.Token))
increment();
}
catch (OperationCanceledException) { }
}
var tasks = new[]
{
Collect(plainSub, () => Interlocked.Increment(ref plainCount)),
Collect(qSub1, () => Interlocked.Increment(ref q1Count)),
Collect(qSub2, () => Interlocked.Increment(ref q2Count)),
};
for (var i = 0; i < 10; i++)
await pub.PublishAsync("e2e.qmix", i);
await pub.PingAsync();
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(10));
while (plainCount < 10 && !deadline.IsCancellationRequested)
await Task.Delay(10, deadline.Token).ContinueWith(_ => { });
collectionCts.Cancel();
await Task.WhenAll(tasks);
plainCount.ShouldBe(10);
(q1Count + q2Count).ShouldBe(10);
}
[Fact]
public async Task Unsub_StopsDelivery()
{
await using var pub = fixture.CreateClient();
await using var subClient = fixture.CreateClient();
await pub.ConnectAsync();
await subClient.ConnectAsync();
var subscription = await subClient.SubscribeCoreAsync<string>("e2e.unsub");
await subClient.PingAsync();
await subscription.DisposeAsync();
await subClient.PingAsync();
// Subscribe a fresh listener on the same subject to verify the unsubscribed
// client does NOT receive messages, while the fresh one does.
await using var verifyClient = fixture.CreateClient();
await verifyClient.ConnectAsync();
await using var verifySub = await verifyClient.SubscribeCoreAsync<string>("e2e.unsub");
await verifyClient.PingAsync();
await pub.PublishAsync("e2e.unsub", "after-unsub");
await pub.PingAsync();
// The fresh subscriber should receive the message
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var msg = await verifySub.Msgs.ReadAsync(cts.Token);
msg.Data.ShouldBe("after-unsub");
// The original (disposed) subscription's channel should be completed —
// reading from it should NOT yield "after-unsub"
var received = new List<string?>();
try
{
await foreach (var m in subscription.Msgs.ReadAllAsync(default))
received.Add(m.Data);
}
catch { /* channel completed or cancelled — expected */ }
received.ShouldNotContain("after-unsub");
}
[Fact]
public async Task Unsub_WithMaxMessages()
{
using var tcp = new TcpClient();
await tcp.ConnectAsync("127.0.0.1", fixture.Port);
await using var ns = tcp.GetStream();
using var reader = new StreamReader(ns, Encoding.ASCII, leaveOpen: true);
var writer = new StreamWriter(ns, Encoding.ASCII, leaveOpen: true) { AutoFlush = true, NewLine = "\r\n" };
// Read INFO
var info = await reader.ReadLineAsync();
info.ShouldNotBeNull();
info.ShouldStartWith("INFO");
await writer.WriteLineAsync("CONNECT {\"verbose\":false,\"protocol\":1}");
await writer.WriteLineAsync("SUB e2e.unsub.max 1");
await writer.WriteLineAsync("UNSUB 1 3");
await writer.WriteLineAsync("PING");
// Wait for PONG to know server processed commands
string? line;
do { line = await reader.ReadLineAsync(); } while (line != null && !line.StartsWith("PONG"));
for (var i = 0; i < 5; i++)
await writer.WriteLineAsync($"PUB e2e.unsub.max 1\r\nx");
await writer.WriteLineAsync("PING");
var msgCount = 0;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
while (!cts.IsCancellationRequested)
{
line = await reader.ReadLineAsync();
if (line == null) break;
if (line.StartsWith("MSG")) msgCount++;
if (line.StartsWith("PONG")) break;
}
msgCount.ShouldBe(3);
}
[Fact]
public async Task FanOut_MultipleSubscribers()
{
await using var pub = fixture.CreateClient();
await using var sub1 = fixture.CreateClient();
await using var sub2 = fixture.CreateClient();
await using var sub3 = fixture.CreateClient();
await pub.ConnectAsync();
await sub1.ConnectAsync();
await sub2.ConnectAsync();
await sub3.ConnectAsync();
await using var s1 = await sub1.SubscribeCoreAsync<string>("e2e.fanout");
await using var s2 = await sub2.SubscribeCoreAsync<string>("e2e.fanout");
await using var s3 = await sub3.SubscribeCoreAsync<string>("e2e.fanout");
await sub1.PingAsync();
await sub2.PingAsync();
await sub3.PingAsync();
await pub.PublishAsync("e2e.fanout", "broadcast");
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var m1 = await s1.Msgs.ReadAsync(cts.Token);
var m2 = await s2.Msgs.ReadAsync(cts.Token);
var m3 = await s3.Msgs.ReadAsync(cts.Token);
m1.Data.ShouldBe("broadcast");
m2.Data.ShouldBe("broadcast");
m3.Data.ShouldBe("broadcast");
}
[Fact]
public async Task EchoOff_PublisherDoesNotReceiveSelf()
{
var opts = new NatsOpts
{
Url = $"nats://127.0.0.1:{fixture.Port}",
Echo = false,
};
await using var client = new NatsConnection(opts);
await client.ConnectAsync();
await using var subscription = await client.SubscribeCoreAsync<string>("e2e.echo");
await client.PingAsync();
await client.PublishAsync("e2e.echo", "self");
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var readTask = subscription.Msgs.ReadAsync(cts.Token).AsTask();
var winner = await Task.WhenAny(readTask, Task.Delay(3000));
winner.ShouldNotBe(readTask);
}
[Fact]
public async Task VerboseMode_OkResponses()
{
using var tcp = new TcpClient();
await tcp.ConnectAsync("127.0.0.1", fixture.Port);
await using var ns = tcp.GetStream();
using var reader = new StreamReader(ns, Encoding.ASCII, leaveOpen: true);
var writer = new StreamWriter(ns, Encoding.ASCII, leaveOpen: true) { AutoFlush = true, NewLine = "\r\n" };
// Read INFO
var info = await reader.ReadLineAsync();
info.ShouldNotBeNull();
info.ShouldStartWith("INFO");
await writer.WriteLineAsync("CONNECT {\"verbose\":true,\"protocol\":1}");
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var connectResponse = await reader.ReadLineAsync();
connectResponse.ShouldBe("+OK");
await writer.WriteLineAsync("SUB test 1");
var subResponse = await reader.ReadLineAsync();
subResponse.ShouldBe("+OK");
await writer.WriteLineAsync("PING");
var pongResponse = await reader.ReadLineAsync();
pongResponse.ShouldBe("PONG");
}
[Fact]
public async Task NoResponders_Returns503()
{
await using var client = fixture.CreateClient();
await client.ConnectAsync();
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
await Should.ThrowAsync<Exception>(async () =>
{
await client.RequestAsync<string, string>("e2e.noresp.xxx", "ping", cancellationToken: cts.Token);
});
}
}