Fix E2E test gaps and add comprehensive E2E + parity test suites

- Fix pull consumer fetch: send original stream subject in HMSG (not inbox)
  so NATS client distinguishes data messages from control messages
- Fix MaxAge expiry: add background timer in StreamManager for periodic pruning
- Fix JetStream wire format: Go-compatible anonymous objects with string enums,
  proper offset-based pagination for stream/consumer list APIs
- Add 42 E2E black-box tests (core messaging, auth, TLS, accounts, JetStream)
- Add ~1000 parity tests across all subsystems (gaps closure)
- Update gap inventory docs to reflect implementation status
This commit is contained in:
Joseph Doherty
2026-03-12 14:09:23 -04:00
parent 79c1ee8776
commit c30e67a69d
226 changed files with 17801 additions and 709 deletions

View File

@@ -0,0 +1,136 @@
using System.Net;
using System.Net.Sockets;
using Microsoft.Extensions.Logging.Abstractions;
using NATS.Server.Configuration;
using NATS.Server.LeafNodes;
namespace NATS.Server.Tests.LeafNodes;
public class LeafConnectionAndRemoteConfigParityBatch1Tests
{
[Fact]
public async Task LeafConnection_role_helpers_reflect_connection_flags()
{
await using var connection = CreateConnection();
connection.IsSolicitedLeafNode().ShouldBeFalse();
connection.IsSpokeLeafNode().ShouldBeFalse();
connection.IsHubLeafNode().ShouldBeTrue();
connection.IsIsolatedLeafNode().ShouldBeFalse();
connection.IsSolicited = true;
connection.IsSpoke = true;
connection.Isolated = true;
connection.IsSolicitedLeafNode().ShouldBeTrue();
connection.IsSpokeLeafNode().ShouldBeTrue();
connection.IsHubLeafNode().ShouldBeFalse();
connection.IsIsolatedLeafNode().ShouldBeTrue();
}
[Fact]
public void RemoteLeafOptions_pick_next_url_round_robins()
{
var remote = new RemoteLeafOptions
{
Urls =
[
"nats://127.0.0.1:7422",
"nats://127.0.0.1:7423",
"nats://127.0.0.1:7424",
],
};
remote.PickNextUrl().ShouldBe("nats://127.0.0.1:7422");
remote.GetCurrentUrl().ShouldBe("nats://127.0.0.1:7422");
remote.PickNextUrl().ShouldBe("nats://127.0.0.1:7423");
remote.PickNextUrl().ShouldBe("nats://127.0.0.1:7424");
remote.PickNextUrl().ShouldBe("nats://127.0.0.1:7422");
}
[Fact]
public void RemoteLeafOptions_pick_next_url_without_entries_throws()
{
var remote = new RemoteLeafOptions();
Should.Throw<InvalidOperationException>(() => remote.PickNextUrl());
}
[Fact]
public void RemoteLeafOptions_saves_tls_hostname_and_user_password_from_url()
{
var remote = new RemoteLeafOptions();
remote.SaveTlsHostname("nats://leaf.example.com:7422");
remote.TlsName.ShouldBe("leaf.example.com");
remote.SaveUserPassword("nats://demo:secret@leaf.example.com:7422");
remote.Username.ShouldBe("demo");
remote.Password.ShouldBe("secret");
}
[Fact]
public void RemoteLeafOptions_connect_delay_round_trips()
{
var remote = new RemoteLeafOptions();
remote.GetConnectDelay().ShouldBe(TimeSpan.Zero);
remote.SetConnectDelay(TimeSpan.FromSeconds(30));
remote.GetConnectDelay().ShouldBe(TimeSpan.FromSeconds(30));
}
[Fact]
public void RemoteLeafNodeStillValid_checks_configured_and_disabled_remotes()
{
var manager = new LeafNodeManager(
new LeafNodeOptions
{
Host = "127.0.0.1",
Port = 0,
Remotes = ["127.0.0.1:7422"],
RemoteLeaves =
[
new RemoteLeafOptions
{
Urls = ["nats://127.0.0.1:7423"],
},
],
},
new ServerStats(),
"S1",
_ => { },
_ => { },
NullLogger<LeafNodeManager>.Instance);
manager.RemoteLeafNodeStillValid("127.0.0.1:7422").ShouldBeTrue();
manager.RemoteLeafNodeStillValid("nats://127.0.0.1:7423").ShouldBeTrue();
manager.RemoteLeafNodeStillValid("127.0.0.1:7999").ShouldBeFalse();
manager.DisableLeafConnect("127.0.0.1:7422");
manager.RemoteLeafNodeStillValid("127.0.0.1:7422").ShouldBeFalse();
}
[Fact]
public void LeafNode_delay_constants_match_go_defaults()
{
LeafNodeManager.LeafNodeReconnectDelayAfterLoopDetected.ShouldBe(TimeSpan.FromSeconds(30));
LeafNodeManager.LeafNodeReconnectAfterPermViolation.ShouldBe(TimeSpan.FromSeconds(30));
LeafNodeManager.LeafNodeReconnectDelayAfterClusterNameSame.ShouldBe(TimeSpan.FromSeconds(30));
LeafNodeManager.LeafNodeWaitBeforeClose.ShouldBe(TimeSpan.FromSeconds(5));
}
private static LeafConnection CreateConnection()
{
var listener = new TcpListener(IPAddress.Loopback, 0);
listener.Start();
var endpoint = (IPEndPoint)listener.LocalEndpoint;
var client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
client.Connect(endpoint);
var server = listener.AcceptSocket();
server.Dispose();
listener.Stop();
return new LeafConnection(client);
}
}

View File

@@ -0,0 +1,181 @@
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Text.Json;
using NATS.Server.Configuration;
using NATS.Server.LeafNodes;
namespace NATS.Server.Tests.LeafNodes;
public class LeafConnectionParityBatch3Tests
{
[Fact]
public async Task SendLeafConnect_writes_connect_json_payload_with_expected_fields()
{
using var listener = new TcpListener(IPAddress.Loopback, 0);
listener.Start();
var endpoint = (IPEndPoint)listener.LocalEndpoint;
using var remoteSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
await remoteSocket.ConnectAsync(IPAddress.Loopback, endpoint.Port);
using var leafSocket = await listener.AcceptSocketAsync();
await using var leaf = new LeafConnection(leafSocket);
var info = new LeafConnectInfo
{
Jwt = "jwt-token",
Nkey = "nkey",
Sig = "sig",
Hub = true,
Cluster = "C1",
Headers = true,
JetStream = true,
Compression = "s2_auto",
RemoteAccount = "A",
Proto = 1,
};
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
await leaf.SendLeafConnectAsync(info, timeout.Token);
var line = await ReadLineAsync(remoteSocket, timeout.Token);
line.ShouldStartWith("CONNECT ");
var payload = line["CONNECT ".Length..];
using var json = JsonDocument.Parse(payload);
var root = json.RootElement;
root.GetProperty("jwt").GetString().ShouldBe("jwt-token");
root.GetProperty("nkey").GetString().ShouldBe("nkey");
root.GetProperty("sig").GetString().ShouldBe("sig");
root.GetProperty("hub").GetBoolean().ShouldBeTrue();
root.GetProperty("cluster").GetString().ShouldBe("C1");
root.GetProperty("headers").GetBoolean().ShouldBeTrue();
root.GetProperty("jetstream").GetBoolean().ShouldBeTrue();
root.GetProperty("compression").GetString().ShouldBe("s2_auto");
root.GetProperty("remote_account").GetString().ShouldBe("A");
root.GetProperty("proto").GetInt32().ShouldBe(1);
}
[Fact]
public async Task RemoteCluster_returns_cluster_from_leaf_handshake_attributes()
{
using var listener = new TcpListener(IPAddress.Loopback, 0);
listener.Start();
var endpoint = (IPEndPoint)listener.LocalEndpoint;
using var remoteSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
await remoteSocket.ConnectAsync(IPAddress.Loopback, endpoint.Port);
using var leafSocket = await listener.AcceptSocketAsync();
await using var leaf = new LeafConnection(leafSocket);
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var handshakeTask = leaf.PerformOutboundHandshakeAsync("LOCAL", timeout.Token);
(await ReadLineAsync(remoteSocket, timeout.Token)).ShouldBe("LEAF LOCAL");
await WriteLineAsync(remoteSocket, "LEAF REMOTE cluster=HUB-A domain=JS-A", timeout.Token);
await handshakeTask;
leaf.RemoteId.ShouldBe("REMOTE");
leaf.RemoteCluster().ShouldBe("HUB-A");
leaf.RemoteJetStreamDomain.ShouldBe("JS-A");
}
[Fact]
public async Task SetLeafConnectDelayIfSoliciting_sets_delay_only_for_solicited_connections()
{
await using var solicited = await CreateConnectionAsync();
solicited.IsSolicited = true;
solicited.SetLeafConnectDelayIfSoliciting(TimeSpan.FromSeconds(30));
solicited.GetConnectDelay().ShouldBe(TimeSpan.FromSeconds(30));
await using var inbound = await CreateConnectionAsync();
inbound.IsSolicited = false;
inbound.SetLeafConnectDelayIfSoliciting(TimeSpan.FromSeconds(30));
inbound.GetConnectDelay().ShouldBe(TimeSpan.Zero);
}
[Fact]
public async Task LeafProcessErr_maps_known_errors_to_reconnect_delays_on_solicited_connections()
{
await using var leaf = await CreateConnectionAsync();
leaf.IsSolicited = true;
leaf.LeafProcessErr("Permissions Violation for Subscription to foo");
leaf.GetConnectDelay().ShouldBe(LeafNodeManager.LeafNodeReconnectAfterPermViolation);
leaf.LeafProcessErr("Loop detected");
leaf.GetConnectDelay().ShouldBe(LeafNodeManager.LeafNodeReconnectDelayAfterLoopDetected);
leaf.LeafProcessErr("Cluster name is same");
leaf.GetConnectDelay().ShouldBe(LeafNodeManager.LeafNodeReconnectDelayAfterClusterNameSame);
}
[Fact]
public async Task LeafSubPermViolation_and_LeafPermViolation_set_permission_delay()
{
await using var leaf = await CreateConnectionAsync();
leaf.IsSolicited = true;
leaf.LeafSubPermViolation("subj.A");
leaf.GetConnectDelay().ShouldBe(LeafNodeManager.LeafNodeReconnectAfterPermViolation);
leaf.SetLeafConnectDelayIfSoliciting(TimeSpan.Zero);
leaf.LeafPermViolation(pub: true, subj: "subj.B");
leaf.GetConnectDelay().ShouldBe(LeafNodeManager.LeafNodeReconnectAfterPermViolation);
}
[Fact]
public async Task CancelMigrateTimer_stops_pending_timer_callback()
{
var remote = new RemoteLeafOptions();
using var signal = new SemaphoreSlim(0, 1);
remote.StartMigrateTimer(_ => signal.Release(), TimeSpan.FromMilliseconds(120));
remote.CancelMigrateTimer();
// The timer was disposed before its 120 ms deadline. We wait 300 ms via WaitAsync;
// if the callback somehow fires it will release the semaphore and WaitAsync returns
// true, which the assertion catches. No Task.Delay required.
var fired = await signal.WaitAsync(TimeSpan.FromMilliseconds(300));
fired.ShouldBeFalse();
}
private static async Task<LeafConnection> CreateConnectionAsync()
{
var listener = new TcpListener(IPAddress.Loopback, 0);
listener.Start();
var endpoint = (IPEndPoint)listener.LocalEndpoint;
var client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
await client.ConnectAsync(IPAddress.Loopback, endpoint.Port);
var server = await listener.AcceptSocketAsync();
listener.Stop();
client.Dispose();
return new LeafConnection(server);
}
private static async Task<string> ReadLineAsync(Socket socket, CancellationToken ct)
{
var bytes = new List<byte>(64);
var single = new byte[1];
while (true)
{
var read = await socket.ReceiveAsync(single, SocketFlags.None, ct);
if (read == 0)
throw new IOException("Connection closed while reading line");
if (single[0] == (byte)'\n')
break;
if (single[0] != (byte)'\r')
bytes.Add(single[0]);
}
return Encoding.ASCII.GetString([.. bytes]);
}
private static Task WriteLineAsync(Socket socket, string line, CancellationToken ct)
=> socket.SendAsync(Encoding.ASCII.GetBytes($"{line}\r\n"), SocketFlags.None, ct).AsTask();
}

View File

@@ -0,0 +1,132 @@
using System.Net;
using System.Net.Sockets;
using System.Text;
using NATS.Server.LeafNodes;
using NATS.Server.Subscriptions;
namespace NATS.Server.Tests.LeafNodes;
public class LeafConnectionParityBatch4Tests
{
[Fact]
public async Task SendLsPlus_with_queue_weight_writes_weighted_frame()
{
using var listener = new TcpListener(IPAddress.Loopback, 0);
listener.Start();
var endpoint = (IPEndPoint)listener.LocalEndpoint;
using var remoteSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
await remoteSocket.ConnectAsync(IPAddress.Loopback, endpoint.Port);
using var leafSocket = await listener.AcceptSocketAsync();
await using var leaf = new LeafConnection(leafSocket);
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var handshakeTask = leaf.PerformOutboundHandshakeAsync("LOCAL", timeout.Token);
(await ReadLineAsync(remoteSocket, timeout.Token)).ShouldBe("LEAF LOCAL");
await WriteLineAsync(remoteSocket, "LEAF REMOTE", timeout.Token);
await handshakeTask;
await leaf.SendLsPlusAsync("$G", "jobs.>", "workers", queueWeight: 3, timeout.Token);
(await ReadLineAsync(remoteSocket, timeout.Token)).ShouldBe("LS+ $G jobs.> workers 3");
}
[Fact]
public async Task ReadLoop_parses_queue_weight_from_ls_plus()
{
using var listener = new TcpListener(IPAddress.Loopback, 0);
listener.Start();
var endpoint = (IPEndPoint)listener.LocalEndpoint;
using var remoteSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
await remoteSocket.ConnectAsync(IPAddress.Loopback, endpoint.Port);
using var leafSocket = await listener.AcceptSocketAsync();
await using var leaf = new LeafConnection(leafSocket);
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var handshakeTask = leaf.PerformOutboundHandshakeAsync("LOCAL", timeout.Token);
(await ReadLineAsync(remoteSocket, timeout.Token)).ShouldBe("LEAF LOCAL");
await WriteLineAsync(remoteSocket, "LEAF REMOTE", timeout.Token);
await handshakeTask;
var received = new List<RemoteSubscription>();
leaf.RemoteSubscriptionReceived = sub =>
{
received.Add(sub);
return Task.CompletedTask;
};
leaf.StartLoop(timeout.Token);
await WriteLineAsync(remoteSocket, "LS+ $G jobs.> workers 7", timeout.Token);
await WaitForAsync(() => received.Count >= 1, timeout.Token);
received[0].Subject.ShouldBe("jobs.>");
received[0].Queue.ShouldBe("workers");
received[0].QueueWeight.ShouldBe(7);
}
[Fact]
public async Task ReadLoop_defaults_invalid_queue_weight_to_one()
{
using var listener = new TcpListener(IPAddress.Loopback, 0);
listener.Start();
var endpoint = (IPEndPoint)listener.LocalEndpoint;
using var remoteSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
await remoteSocket.ConnectAsync(IPAddress.Loopback, endpoint.Port);
using var leafSocket = await listener.AcceptSocketAsync();
await using var leaf = new LeafConnection(leafSocket);
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var handshakeTask = leaf.PerformOutboundHandshakeAsync("LOCAL", timeout.Token);
(await ReadLineAsync(remoteSocket, timeout.Token)).ShouldBe("LEAF LOCAL");
await WriteLineAsync(remoteSocket, "LEAF REMOTE", timeout.Token);
await handshakeTask;
var received = new List<RemoteSubscription>();
leaf.RemoteSubscriptionReceived = sub =>
{
received.Add(sub);
return Task.CompletedTask;
};
leaf.StartLoop(timeout.Token);
await WriteLineAsync(remoteSocket, "LS+ $G jobs.> workers 0", timeout.Token);
await WaitForAsync(() => received.Count >= 1, timeout.Token);
received[0].QueueWeight.ShouldBe(1);
}
private static async Task<string> ReadLineAsync(Socket socket, CancellationToken ct)
{
var bytes = new List<byte>(64);
var single = new byte[1];
while (true)
{
var read = await socket.ReceiveAsync(single, SocketFlags.None, ct);
if (read == 0)
throw new IOException("Connection closed while reading line");
if (single[0] == (byte)'\n')
break;
if (single[0] != (byte)'\r')
bytes.Add(single[0]);
}
return Encoding.ASCII.GetString([.. bytes]);
}
private static Task WriteLineAsync(Socket socket, string line, CancellationToken ct)
=> socket.SendAsync(Encoding.ASCII.GetBytes($"{line}\r\n"), SocketFlags.None, ct).AsTask();
private static async Task WaitForAsync(Func<bool> predicate, CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
if (predicate())
return;
await Task.Yield();
}
throw new TimeoutException("Timed out waiting for condition.");
}
}

View File

@@ -0,0 +1,135 @@
using System.Net;
using System.Net.Sockets;
using System.Text;
using Microsoft.Extensions.Logging.Abstractions;
using NATS.Server.Configuration;
using NATS.Server.LeafNodes;
namespace NATS.Server.Tests.LeafNodes;
public class LeafNodeManagerParityBatch5Tests
{
[Fact]
[SlopwatchSuppress("SW004", "Delay verifies a blocked subject is NOT forwarded; absence of a frame cannot be observed via synchronization primitives")]
public async Task PropagateLocalSubscription_enforces_spoke_subscribe_permissions_and_keeps_queue_weight()
{
await using var ctx = await CreateManagerWithInboundConnectionAsync();
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var conn = ctx.Manager.GetConnectionByRemoteId("SPOKE1");
conn.ShouldNotBeNull();
conn!.IsSpoke = true;
var sync = ctx.Manager.SendPermsAndAccountInfo(
ctx.ConnectionId,
"$G",
pubAllow: null,
subAllow: ["allowed.>"]);
sync.Found.ShouldBeTrue();
sync.PermsSynced.ShouldBeTrue();
ctx.Manager.PropagateLocalSubscription("$G", "blocked.data", null);
ctx.Manager.PropagateLocalSubscription("$G", "allowed.data", "workers", queueWeight: 4);
// Only the allowed subject should appear on the wire; the blocked one is filtered synchronously.
var line = await ReadLineAsync(ctx.RemoteSocket, timeout.Token);
line.ShouldBe("LS+ $G allowed.data workers 4");
}
[Fact]
public async Task PropagateLocalSubscription_allows_loop_and_gateway_reply_prefixes_for_spoke()
{
await using var ctx = await CreateManagerWithInboundConnectionAsync();
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var conn = ctx.Manager.GetConnectionByRemoteId("SPOKE1");
conn.ShouldNotBeNull();
conn!.IsSpoke = true;
ctx.Manager.SendPermsAndAccountInfo(
ctx.ConnectionId,
"$G",
pubAllow: null,
subAllow: ["allowed.>"]);
ctx.Manager.PropagateLocalSubscription("$G", "$LDS.HUB.loop", null);
(await ReadLineAsync(ctx.RemoteSocket, timeout.Token)).ShouldBe("LS+ $G $LDS.HUB.loop");
ctx.Manager.PropagateLocalSubscription("$G", "_GR_.A.reply", null);
(await ReadLineAsync(ctx.RemoteSocket, timeout.Token)).ShouldBe("LS+ $G _GR_.A.reply");
}
private sealed class ManagerContext : IAsyncDisposable
{
private readonly CancellationTokenSource _cts;
public ManagerContext(LeafNodeManager manager, string connectionId, Socket remoteSocket, CancellationTokenSource cts)
{
Manager = manager;
ConnectionId = connectionId;
RemoteSocket = remoteSocket;
_cts = cts;
}
public LeafNodeManager Manager { get; }
public string ConnectionId { get; }
public Socket RemoteSocket { get; }
public async ValueTask DisposeAsync()
{
RemoteSocket.Close();
await Manager.DisposeAsync();
_cts.Dispose();
}
}
private static async Task<ManagerContext> CreateManagerWithInboundConnectionAsync()
{
var options = new LeafNodeOptions { Host = "127.0.0.1", Port = 0 };
var manager = new LeafNodeManager(
options,
new ServerStats(),
"HUB",
_ => { },
_ => { },
NullLogger<LeafNodeManager>.Instance);
var cts = new CancellationTokenSource();
await manager.StartAsync(cts.Token);
var remoteSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
await remoteSocket.ConnectAsync(IPAddress.Loopback, options.Port);
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var registered = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
manager.OnConnectionRegistered = id => registered.TrySetResult(id);
timeout.Token.Register(() => registered.TrySetCanceled(timeout.Token));
await WriteLineAsync(remoteSocket, "LEAF SPOKE1", timeout.Token);
(await ReadLineAsync(remoteSocket, timeout.Token)).ShouldStartWith("LEAF ");
var connectionId = await registered.Task;
return new ManagerContext(manager, connectionId, remoteSocket, cts);
}
private static async Task<string> ReadLineAsync(Socket socket, CancellationToken ct)
{
var bytes = new List<byte>(64);
var single = new byte[1];
while (true)
{
var read = await socket.ReceiveAsync(single, SocketFlags.None, ct);
if (read == 0)
throw new IOException("Connection closed while reading line");
if (single[0] == (byte)'\n')
break;
if (single[0] != (byte)'\r')
bytes.Add(single[0]);
}
return Encoding.ASCII.GetString([.. bytes]);
}
private static Task WriteLineAsync(Socket socket, string line, CancellationToken ct)
=> socket.SendAsync(Encoding.ASCII.GetBytes($"{line}\r\n"), SocketFlags.None, ct).AsTask();
}

View File

@@ -0,0 +1,42 @@
using NATS.Server.LeafNodes;
using NATS.Server.Subscriptions;
namespace NATS.Server.Tests.LeafNodes;
public class LeafSubKeyParityBatch2Tests
{
[Fact]
public void Constants_match_go_leaf_key_and_delay_values()
{
LeafSubKey.KeyRoutedSub.ShouldBe("R");
LeafSubKey.KeyRoutedSubByte.ShouldBe((byte)'R');
LeafSubKey.KeyRoutedLeafSub.ShouldBe("L");
LeafSubKey.KeyRoutedLeafSubByte.ShouldBe((byte)'L');
LeafSubKey.SharedSysAccDelay.ShouldBe(TimeSpan.FromMilliseconds(250));
LeafSubKey.ConnectProcessTimeout.ShouldBe(TimeSpan.FromSeconds(2));
}
[Fact]
public void KeyFromSub_matches_go_subject_and_queue_shape()
{
LeafSubKey.KeyFromSub(NewSub("foo")).ShouldBe("foo");
LeafSubKey.KeyFromSub(NewSub("foo", "bar")).ShouldBe("foo bar");
}
[Fact]
public void KeyFromSubWithOrigin_matches_go_routed_and_leaf_routed_shapes()
{
LeafSubKey.KeyFromSubWithOrigin(NewSub("foo")).ShouldBe("R foo");
LeafSubKey.KeyFromSubWithOrigin(NewSub("foo", "bar")).ShouldBe("R foo bar");
LeafSubKey.KeyFromSubWithOrigin(NewSub("foo"), "leaf").ShouldBe("L foo leaf");
LeafSubKey.KeyFromSubWithOrigin(NewSub("foo", "bar"), "leaf").ShouldBe("L foo bar leaf");
}
private static Subscription NewSub(string subject, string? queue = null)
=> new()
{
Subject = subject,
Queue = queue,
Sid = Guid.NewGuid().ToString("N"),
};
}