feat: execute full-repo remaining parity closure plan

This commit is contained in:
Joseph Doherty
2026-02-23 13:08:52 -05:00
parent cbe1fa6121
commit 2b64d762f6
75 changed files with 2325 additions and 121 deletions

View File

@@ -0,0 +1,30 @@
using NATS.Server.Auth;
using NATS.Server.Protocol;
namespace NATS.Server.Tests;
public class AuthExtensionParityTests
{
[Fact]
public void Auth_service_uses_proxy_auth_extension_when_enabled()
{
var service = AuthService.Build(new NatsOptions
{
ProxyAuth = new ProxyAuthOptions
{
Enabled = true,
UsernamePrefix = "proxy:",
},
});
service.IsAuthRequired.ShouldBeTrue();
var result = service.Authenticate(new ClientAuthContext
{
Opts = new ClientOptions { Username = "proxy:alice" },
Nonce = [],
});
result.ShouldNotBeNull();
result.Identity.ShouldBe("alice");
}
}

View File

@@ -0,0 +1,58 @@
using NATS.Server.Auth;
using NATS.Server.Protocol;
namespace NATS.Server.Tests;
public class ExternalAuthCalloutTests
{
[Fact]
public void External_callout_authenticator_can_allow_and_deny_with_timeout_and_reason_mapping()
{
var authenticator = new ExternalAuthCalloutAuthenticator(
new FakeExternalAuthClient(),
TimeSpan.FromMilliseconds(50));
var allowed = authenticator.Authenticate(new ClientAuthContext
{
Opts = new ClientOptions { Username = "u", Password = "p" },
Nonce = [],
});
allowed.ShouldNotBeNull();
allowed.Identity.ShouldBe("u");
var denied = authenticator.Authenticate(new ClientAuthContext
{
Opts = new ClientOptions { Username = "u", Password = "bad" },
Nonce = [],
});
denied.ShouldBeNull();
var timeout = new ExternalAuthCalloutAuthenticator(
new SlowExternalAuthClient(TimeSpan.FromMilliseconds(200)),
TimeSpan.FromMilliseconds(30));
timeout.Authenticate(new ClientAuthContext
{
Opts = new ClientOptions { Username = "u", Password = "p" },
Nonce = [],
}).ShouldBeNull();
}
private sealed class FakeExternalAuthClient : IExternalAuthClient
{
public Task<ExternalAuthDecision> AuthorizeAsync(ExternalAuthRequest request, CancellationToken ct)
{
if (request is { Username: "u", Password: "p" })
return Task.FromResult(new ExternalAuthDecision(true, "u", "A"));
return Task.FromResult(new ExternalAuthDecision(false, Reason: "denied"));
}
}
private sealed class SlowExternalAuthClient(TimeSpan delay) : IExternalAuthClient
{
public async Task<ExternalAuthDecision> AuthorizeAsync(ExternalAuthRequest request, CancellationToken ct)
{
await Task.Delay(delay, ct);
return new ExternalAuthDecision(true, "slow");
}
}
}

View File

@@ -0,0 +1,28 @@
using NATS.Server.Auth;
using NATS.Server.Protocol;
namespace NATS.Server.Tests;
public class ProxyAuthTests
{
[Fact]
public void Proxy_authenticator_maps_prefixed_username_to_identity()
{
var authenticator = new ProxyAuthenticator(new ProxyAuthOptions
{
Enabled = true,
UsernamePrefix = "proxy:",
Account = "A",
});
var result = authenticator.Authenticate(new ClientAuthContext
{
Opts = new ClientOptions { Username = "proxy:bob" },
Nonce = [],
});
result.ShouldNotBeNull();
result.Identity.ShouldBe("bob");
result.AccountName.ShouldBe("A");
}
}

View File

@@ -3,14 +3,11 @@ namespace NATS.Server.Tests;
public class DifferencesParityClosureTests
{
[Fact]
public void Differences_md_has_no_remaining_jetstream_baseline_or_n_rows()
public void Differences_md_has_no_remaining_baseline_n_or_stub_rows_in_tracked_scope()
{
var repositoryRoot = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", ".."));
var differencesPath = Path.Combine(repositoryRoot, "differences.md");
File.Exists(differencesPath).ShouldBeTrue();
var markdown = File.ReadAllText(differencesPath);
markdown.ShouldContain("### JetStream");
markdown.ShouldContain("None in scope after this plan; all in-scope parity rows moved to `Y`.");
var report = Parity.ParityRowInspector.Load("differences.md");
report.UnresolvedRows.ShouldBeEmpty(string.Join(
Environment.NewLine,
report.UnresolvedRows.Select(r => $"{r.Section} :: {r.SubSection} :: {r.Feature} [{r.DotNetStatus}]")));
}
}

View File

@@ -0,0 +1,17 @@
using NATS.Server.Gateways;
using NATS.Server.Subscriptions;
namespace NATS.Server.Tests;
public class GatewayInterestOnlyParityTests
{
[Fact]
public void Gateway_interest_only_mode_forwards_only_subjects_with_remote_interest()
{
using var subList = new SubList();
subList.ApplyRemoteSub(new RemoteSubscription("orders.*", null, "gw1", "A"));
GatewayManager.ShouldForwardInterestOnly(subList, "A", "orders.created").ShouldBeTrue();
GatewayManager.ShouldForwardInterestOnly(subList, "A", "payments.created").ShouldBeFalse();
}
}

View File

@@ -0,0 +1,17 @@
using NATS.Server.IO;
namespace NATS.Server.Tests;
public class AdaptiveReadBufferTests
{
[Fact]
public void Read_buffer_scales_between_512_and_65536_based_on_recent_payload_pattern()
{
var b = new AdaptiveReadBuffer();
b.RecordRead(512);
b.RecordRead(4096);
b.RecordRead(32000);
b.CurrentSize.ShouldBeGreaterThan(4096);
b.CurrentSize.ShouldBeLessThanOrEqualTo(64 * 1024);
}
}

View File

@@ -0,0 +1,17 @@
using NATS.Server.IO;
namespace NATS.Server.Tests;
public class OutboundBufferPoolTests
{
[Theory]
[InlineData(100, 512)]
[InlineData(1000, 4096)]
[InlineData(10000, 64 * 1024)]
public void Rent_uses_three_tier_buffer_buckets(int requested, int expectedMinimum)
{
var pool = new OutboundBufferPool();
using var owner = pool.Rent(requested);
owner.Memory.Length.ShouldBeGreaterThanOrEqualTo(expectedMinimum);
}
}

View File

@@ -0,0 +1,26 @@
using NATS.Server.JetStream.Cluster;
using NATS.Server.JetStream.Models;
namespace NATS.Server.Tests;
public class JetStreamClusterGovernanceRuntimeParityTests
{
[Fact]
public async Task Jetstream_cluster_governance_applies_consensus_backed_placement()
{
var meta = new JetStreamMetaGroup(3);
await meta.ProposeCreateStreamAsync(new StreamConfig
{
Name = "ORDERS",
Subjects = ["orders.*"],
}, default);
var planner = new AssetPlacementPlanner(3);
var placement = planner.PlanReplicas(2);
var replicas = new StreamReplicaGroup("ORDERS", 1);
await replicas.ApplyPlacementAsync(placement, default);
meta.GetState().Streams.ShouldContain("ORDERS");
replicas.Nodes.Count.ShouldBe(2);
}
}

View File

@@ -0,0 +1,33 @@
using NATS.Server.JetStream.Consumers;
using NATS.Server.JetStream.Models;
using NATS.Server.JetStream.Storage;
using NATS.Server.JetStream;
namespace NATS.Server.Tests;
public class JetStreamConsumerFlowReplayParityTests
{
[Fact]
public void Push_consumer_enqueues_flow_control_and_heartbeat_frames_when_enabled()
{
var engine = new PushConsumerEngine();
var consumer = new ConsumerHandle("ORDERS", new ConsumerConfig
{
AckPolicy = AckPolicy.Explicit,
FlowControl = true,
HeartbeatMs = 1000,
RateLimitBps = 1024,
});
engine.Enqueue(consumer, new StoredMessage
{
Sequence = 1,
Subject = "orders.created",
Payload = "payload"u8.ToArray(),
});
consumer.PushFrames.Count.ShouldBe(3);
consumer.PushFrames.Any(f => f.IsFlowControl).ShouldBeTrue();
consumer.PushFrames.Any(f => f.IsHeartbeat).ShouldBeTrue();
}
}

View File

@@ -0,0 +1,25 @@
using NATS.Server.JetStream.Consumers;
namespace NATS.Server.Tests;
public class JetStreamConsumerRuntimeParityTests
{
[Fact]
public async Task Consumer_runtime_honors_ack_all_redelivery_and_max_deliver_limits()
{
var ack = new AckProcessor();
ack.Register(1, ackWaitMs: 1);
await Task.Delay(5);
ack.TryGetExpired(out var seq, out var deliveries).ShouldBeTrue();
seq.ShouldBe((ulong)1);
deliveries.ShouldBe(1);
ack.ScheduleRedelivery(seq, delayMs: 1);
await Task.Delay(5);
ack.TryGetExpired(out _, out deliveries).ShouldBeTrue();
deliveries.ShouldBe(2);
ack.AckAll(1);
ack.HasPending.ShouldBeFalse();
}
}

View File

@@ -0,0 +1,26 @@
using Microsoft.Extensions.Logging.Abstractions;
using NATS.Server.Configuration;
using NATS.Server.Gateways;
namespace NATS.Server.Tests;
public class JetStreamCrossClusterRuntimeParityTests
{
[Fact]
public async Task Jetstream_cross_cluster_messages_are_forward_counted()
{
var manager = new GatewayManager(
new GatewayOptions { Host = "127.0.0.1", Port = 0, Name = "A" },
new ServerStats(),
"S1",
_ => { },
_ => { },
NullLogger<GatewayManager>.Instance);
await manager.ForwardJetStreamClusterMessageAsync(
new GatewayMessage("$JS.CLUSTER.REPL", null, "x"u8.ToArray()),
default);
manager.ForwardedJetStreamClusterMessages.ShouldBe(1);
}
}

View File

@@ -0,0 +1,33 @@
using NATS.Server.JetStream.Storage;
namespace NATS.Server.Tests;
public class JetStreamFileStoreCryptoCompressionTests
{
[Fact]
public async Task File_store_compression_and_encryption_roundtrip_preserves_payload()
{
var dir = Path.Combine(Path.GetTempPath(), $"natsdotnet-filestore-crypto-{Guid.NewGuid():N}");
try
{
await using var store = new FileStore(new FileStoreOptions
{
Directory = dir,
EnableCompression = true,
EnableEncryption = true,
EncryptionKey = [1, 2, 3, 4],
});
var payload = Enumerable.Repeat((byte)'a', 512).ToArray();
var seq = await store.AppendAsync("orders.created", payload, default);
var loaded = await store.LoadAsync(seq, default);
loaded.ShouldNotBeNull();
loaded.Payload.ToArray().ShouldBe(payload);
}
finally
{
if (Directory.Exists(dir))
Directory.Delete(dir, recursive: true);
}
}
}

View File

@@ -0,0 +1,33 @@
using NATS.Server.JetStream.Storage;
namespace NATS.Server.Tests;
public class JetStreamFileStoreLayoutParityTests
{
[Fact]
public async Task File_store_uses_block_index_layout_with_ttl_prune_invariants()
{
var dir = Path.Combine(Path.GetTempPath(), $"natsdotnet-filestore-{Guid.NewGuid():N}");
try
{
await using var store = new FileStore(new FileStoreOptions
{
Directory = dir,
BlockSizeBytes = 128,
MaxAgeMs = 60_000,
});
for (var i = 0; i < 100; i++)
await store.AppendAsync($"orders.{i}", "x"u8.ToArray(), default);
var state = await store.GetStateAsync(default);
state.Messages.ShouldBe((ulong)100);
store.BlockCount.ShouldBeGreaterThan(1);
}
finally
{
if (Directory.Exists(dir))
Directory.Delete(dir, recursive: true);
}
}
}

View File

@@ -0,0 +1,40 @@
using NATS.Server.JetStream.MirrorSource;
using NATS.Server.JetStream.Models;
using NATS.Server.JetStream.Storage;
namespace NATS.Server.Tests;
public class JetStreamMirrorSourceRuntimeParityTests
{
[Fact]
public async Task Mirror_source_runtime_tracks_sync_state_and_subject_mapping()
{
var mirrorTarget = new MemStore();
var sourceTarget = new MemStore();
var mirror = new MirrorCoordinator(mirrorTarget);
var source = new SourceCoordinator(sourceTarget, new StreamSourceConfig
{
Name = "SRC",
SubjectTransformPrefix = "agg.",
SourceAccount = "A",
});
var message = new StoredMessage
{
Sequence = 10,
Subject = "orders.created",
Payload = "ok"u8.ToArray(),
TimestampUtc = DateTime.UtcNow,
};
await mirror.OnOriginAppendAsync(message, default);
await source.OnOriginAppendAsync(message, default);
mirror.LastOriginSequence.ShouldBe((ulong)10);
source.LastOriginSequence.ShouldBe((ulong)10);
var sourced = await sourceTarget.LoadAsync(1, default);
sourced.ShouldNotBeNull();
sourced.Subject.ShouldBe("agg.orders.created");
}
}

View File

@@ -0,0 +1,30 @@
using NATS.Server.JetStream.Models;
using NATS.Server.JetStream.Validation;
namespace NATS.Server.Tests;
public class JetStreamStreamFeatureToggleParityTests
{
[Fact]
public void Stream_feature_toggles_are_preserved_in_config_model_and_validation()
{
var config = new StreamConfig
{
Name = "ORDERS",
Subjects = ["orders.*"],
Sealed = true,
DenyDelete = true,
DenyPurge = true,
AllowDirect = true,
MaxMsgSize = 1024,
MaxMsgsPer = 10,
MaxAgeMs = 5000,
};
JetStreamConfigValidator.Validate(config).IsValid.ShouldBeTrue();
config.Sealed.ShouldBeTrue();
config.DenyDelete.ShouldBeTrue();
config.DenyPurge.ShouldBeTrue();
config.AllowDirect.ShouldBeTrue();
}
}

View File

@@ -0,0 +1,29 @@
using NATS.Server.JetStream.Models;
using NATS.Server.JetStream.Publish;
using NATS.Server.JetStream.Validation;
namespace NATS.Server.Tests;
public class JetStreamStreamRuntimeParityTests
{
[Fact]
public void Stream_runtime_enforces_retention_and_size_preconditions()
{
var invalid = new StreamConfig
{
Name = "ORDERS",
Subjects = ["orders.*"],
Retention = RetentionPolicy.WorkQueue,
MaxConsumers = 0,
MaxMsgSize = -1,
};
var result = JetStreamConfigValidator.Validate(invalid);
result.IsValid.ShouldBeFalse();
var preconditions = new PublishPreconditions();
preconditions.Record("m1", 1);
preconditions.IsDuplicate("m1", duplicateWindowMs: 10_000, out var existing).ShouldBeTrue();
existing.ShouldBe((ulong)1);
}
}

View File

@@ -0,0 +1,21 @@
using NATS.Server.LeafNodes;
namespace NATS.Server.Tests;
public class LeafHubSpokeMappingParityTests
{
[Fact]
public void Leaf_hub_spoke_mapper_round_trips_account_mapping()
{
var mapper = new LeafHubSpokeMapper(new Dictionary<string, string>
{
["HUB"] = "SPOKE",
});
var outbound = mapper.Map("HUB", "orders.created", LeafMapDirection.Outbound);
outbound.Account.ShouldBe("SPOKE");
var inbound = mapper.Map("SPOKE", "orders.created", LeafMapDirection.Inbound);
inbound.Account.ShouldBe("HUB");
}
}

View File

@@ -0,0 +1,21 @@
using System.Text.Json;
namespace NATS.Server.Tests;
public class ConnzParityFieldTests
{
[Fact]
public async Task Connz_includes_identity_tls_and_proxy_parity_fields()
{
await using var fx = await MonitoringParityFixture.StartAsync();
await fx.ConnectClientAsync("u", "orders.created");
var connz = fx.GetConnz("?subs=detail");
connz.Conns.ShouldNotBeEmpty();
var json = JsonSerializer.Serialize(connz);
json.ShouldContain("tls_peer_cert_subject");
json.ShouldContain("jwt_issuer_key");
json.ShouldContain("proxy");
}
}

View File

@@ -0,0 +1,115 @@
using System.Net;
using System.Net.Sockets;
using System.Text;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging.Abstractions;
using NATS.Server.Auth;
using NATS.Server.Monitoring;
namespace NATS.Server.Tests;
public class ConnzParityFilterTests
{
[Fact]
public async Task Connz_filters_by_user_account_and_subject_and_includes_tls_peer_and_jwt_metadata()
{
await using var fx = await MonitoringParityFixture.StartAsync();
await fx.ConnectClientAsync("u", "orders.created");
await fx.ConnectClientAsync("v", "payments.created");
var connz = fx.GetConnz("?user=u&acc=A&filter_subject=orders.*&subs=detail");
connz.Conns.ShouldAllBe(c => c.Account == "A" && c.AuthorizedUser == "u");
}
}
internal sealed class MonitoringParityFixture : IAsyncDisposable
{
private readonly NatsServer _server;
private readonly CancellationTokenSource _cts;
private readonly List<TcpClient> _clients = [];
private readonly NatsOptions _options;
private MonitoringParityFixture(NatsServer server, NatsOptions options, CancellationTokenSource cts)
{
_server = server;
_options = options;
_cts = cts;
}
public static async Task<MonitoringParityFixture> StartAsync()
{
var options = new NatsOptions
{
Host = "127.0.0.1",
Port = 0,
Users =
[
new User { Username = "u", Password = "p", Account = "A" },
new User { Username = "v", Password = "p", Account = "B" },
],
};
var server = new NatsServer(options, NullLoggerFactory.Instance);
var cts = new CancellationTokenSource();
_ = server.StartAsync(cts.Token);
await server.WaitForReadyAsync();
return new MonitoringParityFixture(server, options, cts);
}
public async Task ConnectClientAsync(string username, string? subscribeSubject)
{
var client = new TcpClient();
await client.ConnectAsync(IPAddress.Loopback, _server.Port);
_clients.Add(client);
var stream = client.GetStream();
await ReadLineAsync(stream); // INFO
var connect = $"CONNECT {{\"user\":\"{username}\",\"pass\":\"p\"}}\r\n";
await stream.WriteAsync(Encoding.ASCII.GetBytes(connect));
if (!string.IsNullOrEmpty(subscribeSubject))
await stream.WriteAsync(Encoding.ASCII.GetBytes($"SUB {subscribeSubject} sid-{username}\r\n"));
await stream.FlushAsync();
await Task.Delay(30);
}
public Connz GetConnz(string queryString)
{
var ctx = new DefaultHttpContext();
ctx.Request.QueryString = new QueryString(queryString);
return new ConnzHandler(_server).HandleConnz(ctx);
}
public async Task<Varz> GetVarzAsync()
{
using var handler = new VarzHandler(_server, _options);
return await handler.HandleVarzAsync();
}
public async ValueTask DisposeAsync()
{
foreach (var client in _clients)
client.Dispose();
await _cts.CancelAsync();
_server.Dispose();
_cts.Dispose();
}
private static async Task<string> ReadLineAsync(NetworkStream stream)
{
var bytes = new List<byte>();
var one = new byte[1];
while (true)
{
var read = await stream.ReadAsync(one.AsMemory(0, 1));
if (read == 0)
break;
if (one[0] == (byte)'\n')
break;
if (one[0] != (byte)'\r')
bytes.Add(one[0]);
}
return Encoding.ASCII.GetString([.. bytes]);
}
}

View File

@@ -0,0 +1,87 @@
using System.Net;
using System.Net.Sockets;
using Microsoft.Extensions.Logging.Abstractions;
namespace NATS.Server.Tests;
public class PprofEndpointTests
{
[Fact]
public async Task Debug_pprof_endpoint_returns_profile_index_when_profport_enabled()
{
await using var fx = await PprofMonitorFixture.StartWithProfilingAsync();
var body = await fx.GetStringAsync("/debug/pprof");
body.ShouldContain("profiles");
}
}
internal sealed class PprofMonitorFixture : IAsyncDisposable
{
private readonly NatsServer _server;
private readonly CancellationTokenSource _cts;
private readonly HttpClient _http;
private readonly int _monitorPort;
private PprofMonitorFixture(NatsServer server, CancellationTokenSource cts, HttpClient http, int monitorPort)
{
_server = server;
_cts = cts;
_http = http;
_monitorPort = monitorPort;
}
public static async Task<PprofMonitorFixture> StartWithProfilingAsync()
{
var monitorPort = GetFreePort();
var options = new NatsOptions
{
Host = "127.0.0.1",
Port = 0,
MonitorPort = monitorPort,
ProfPort = monitorPort,
};
var server = new NatsServer(options, NullLoggerFactory.Instance);
var cts = new CancellationTokenSource();
_ = server.StartAsync(cts.Token);
await server.WaitForReadyAsync();
var http = new HttpClient();
for (var i = 0; i < 50; i++)
{
try
{
var response = await http.GetAsync($"http://127.0.0.1:{monitorPort}/healthz");
if (response.IsSuccessStatusCode)
break;
}
catch
{
}
await Task.Delay(50);
}
return new PprofMonitorFixture(server, cts, http, monitorPort);
}
public Task<string> GetStringAsync(string path)
{
return _http.GetStringAsync($"http://127.0.0.1:{_monitorPort}{path}");
}
public async ValueTask DisposeAsync()
{
_http.Dispose();
await _cts.CancelAsync();
_server.Dispose();
_cts.Dispose();
}
private static int GetFreePort()
{
using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
socket.Bind(new IPEndPoint(IPAddress.Loopback, 0));
return ((IPEndPoint)socket.LocalEndPoint!).Port;
}
}

View File

@@ -0,0 +1,17 @@
namespace NATS.Server.Tests;
public class VarzSlowConsumerBreakdownTests
{
[Fact]
public async Task Varz_contains_slow_consumer_breakdown_fields()
{
await using var fx = await MonitoringParityFixture.StartAsync();
var varz = await fx.GetVarzAsync();
varz.SlowConsumerStats.ShouldNotBeNull();
varz.SlowConsumerStats.Clients.ShouldBeGreaterThanOrEqualTo((ulong)0);
varz.SlowConsumerStats.Routes.ShouldBeGreaterThanOrEqualTo((ulong)0);
varz.SlowConsumerStats.Gateways.ShouldBeGreaterThanOrEqualTo((ulong)0);
varz.SlowConsumerStats.Leafs.ShouldBeGreaterThanOrEqualTo((ulong)0);
}
}

View File

@@ -0,0 +1,73 @@
using System.Net;
using System.Net.Sockets;
using System.Text;
using NATS.Server.Mqtt;
namespace NATS.Server.Tests;
public class MqttListenerParityTests
{
[Fact]
public async Task Mqtt_listener_accepts_connect_and_routes_publish_to_matching_subscription()
{
await using var listener = new MqttListener("127.0.0.1", 0);
using var cts = new CancellationTokenSource();
await listener.StartAsync(cts.Token);
using var sub = new TcpClient();
await sub.ConnectAsync(IPAddress.Loopback, listener.Port);
var subStream = sub.GetStream();
await MqttTestWire.WriteLineAsync(subStream, "CONNECT sub");
(await MqttTestWire.ReadLineAsync(subStream, 1000)).ShouldBe("CONNACK");
await MqttTestWire.WriteLineAsync(subStream, "SUB sensors.temp");
var subAck = await MqttTestWire.ReadLineAsync(subStream, 1000);
subAck.ShouldNotBeNull();
subAck.ShouldContain("SUBACK");
using var pub = new TcpClient();
await pub.ConnectAsync(IPAddress.Loopback, listener.Port);
var pubStream = pub.GetStream();
await MqttTestWire.WriteLineAsync(pubStream, "CONNECT pub");
_ = await MqttTestWire.ReadLineAsync(pubStream, 1000);
await MqttTestWire.WriteLineAsync(pubStream, "PUB sensors.temp 42");
var message = await MqttTestWire.ReadLineAsync(subStream, 1000);
message.ShouldBe("MSG sensors.temp 42");
}
}
internal static class MqttTestWire
{
public static async Task WriteLineAsync(NetworkStream stream, string line)
{
var bytes = Encoding.UTF8.GetBytes(line + "\n");
await stream.WriteAsync(bytes);
await stream.FlushAsync();
}
public static async Task<string?> ReadLineAsync(NetworkStream stream, int timeoutMs)
{
using var timeout = new CancellationTokenSource(timeoutMs);
var bytes = new List<byte>();
var one = new byte[1];
try
{
while (true)
{
var read = await stream.ReadAsync(one.AsMemory(0, 1), timeout.Token);
if (read == 0)
return null;
if (one[0] == (byte)'\n')
break;
if (one[0] != (byte)'\r')
bytes.Add(one[0]);
}
}
catch (OperationCanceledException)
{
return null;
}
return Encoding.UTF8.GetString([.. bytes]);
}
}

View File

@@ -0,0 +1,33 @@
using System.Net;
using System.Net.Sockets;
using NATS.Server.Mqtt;
namespace NATS.Server.Tests;
public class MqttPublishSubscribeParityTests
{
[Fact]
public async Task Mqtt_publish_only_reaches_matching_topic_subscribers()
{
await using var listener = new MqttListener("127.0.0.1", 0);
using var cts = new CancellationTokenSource();
await listener.StartAsync(cts.Token);
using var sub = new TcpClient();
await sub.ConnectAsync(IPAddress.Loopback, listener.Port);
var subStream = sub.GetStream();
await MqttTestWire.WriteLineAsync(subStream, "CONNECT sub");
_ = await MqttTestWire.ReadLineAsync(subStream, 1000);
await MqttTestWire.WriteLineAsync(subStream, "SUB sensors.temp");
_ = await MqttTestWire.ReadLineAsync(subStream, 1000);
using var pub = new TcpClient();
await pub.ConnectAsync(IPAddress.Loopback, listener.Port);
var pubStream = pub.GetStream();
await MqttTestWire.WriteLineAsync(pubStream, "CONNECT pub");
_ = await MqttTestWire.ReadLineAsync(pubStream, 1000);
await MqttTestWire.WriteLineAsync(pubStream, "PUB sensors.humidity 90");
(await MqttTestWire.ReadLineAsync(subStream, 150)).ShouldBeNull();
}
}

View File

@@ -0,0 +1,67 @@
namespace NATS.Server.Tests.Parity;
public sealed record ParityRow(string Section, string SubSection, string Feature, string DotNetStatus);
public sealed class ParityReport
{
public ParityReport(IReadOnlyList<ParityRow> rows)
{
Rows = rows;
}
public IReadOnlyList<ParityRow> Rows { get; }
public IReadOnlyList<ParityRow> UnresolvedRows =>
Rows.Where(r => r.DotNetStatus is "N" or "Baseline" or "Stub").ToArray();
}
public static class ParityRowInspector
{
public static ParityReport Load(string relativePath)
{
var repositoryRoot = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", ".."));
var differencesPath = Path.Combine(repositoryRoot, relativePath);
File.Exists(differencesPath).ShouldBeTrue();
var section = string.Empty;
var subsection = string.Empty;
var rows = new List<ParityRow>();
foreach (var rawLine in File.ReadLines(differencesPath))
{
var line = rawLine.Trim();
if (line.StartsWith("## ", StringComparison.Ordinal))
{
section = line[3..].Trim();
continue;
}
if (line.StartsWith("### ", StringComparison.Ordinal))
{
subsection = line[4..].Trim();
continue;
}
if (!line.StartsWith("|", StringComparison.Ordinal))
continue;
if (line.Contains("---", StringComparison.Ordinal))
continue;
var cells = line.Trim('|').Split('|').Select(c => c.Trim()).ToArray();
if (cells.Length < 3)
continue;
// Ignore table header rows; row format is expected to contain Go and .NET status columns.
if (cells[0] is "Feature" or "Aspect" or "Operation" or "Signal" or "Type" or "Mechanism" or "Flag")
continue;
rows.Add(new ParityRow(
section,
subsection,
cells[0],
cells[2]));
}
return new ParityReport(rows);
}
}

View File

@@ -0,0 +1,14 @@
using NATS.Server.Protocol;
namespace NATS.Server.Tests;
public class InterServerOpcodeRoutingTests
{
[Fact]
public void Parser_dispatch_rejects_Aplus_for_client_kind_client_but_allows_for_gateway()
{
var m = new ClientCommandMatrix();
m.IsAllowed(ClientKind.Client, "A+").ShouldBeFalse();
m.IsAllowed(ClientKind.Gateway, "A+").ShouldBeTrue();
}
}

View File

@@ -0,0 +1,24 @@
using NATS.Server.Protocol;
namespace NATS.Server.Tests;
public class MessageTraceInitializationTests
{
[Fact]
public void Trace_context_is_initialized_from_connect_options()
{
var connectOpts = new ClientOptions
{
Name = "c1",
Lang = "dotnet",
Version = "1.0.0",
Headers = true,
};
var ctx = MessageTraceContext.CreateFromConnect(connectOpts);
ctx.ClientName.ShouldBe("c1");
ctx.ClientLang.ShouldBe("dotnet");
ctx.ClientVersion.ShouldBe("1.0.0");
ctx.HeadersEnabled.ShouldBeTrue();
}
}

View File

@@ -0,0 +1,16 @@
using NATS.Server.Raft;
namespace NATS.Server.Tests;
public class RaftConsensusRuntimeParityTests
{
[Fact]
public async Task Raft_cluster_commits_with_next_index_backtracking_semantics()
{
var cluster = RaftTestCluster.Create(3);
await cluster.GenerateCommittedEntriesAsync(5);
await cluster.WaitForAppliedAsync(5);
cluster.Nodes.All(n => n.AppliedIndex >= 5).ShouldBeTrue();
}
}

View File

@@ -0,0 +1,19 @@
using NATS.Server.Raft;
namespace NATS.Server.Tests;
public class RaftMembershipRuntimeParityTests
{
[Fact]
public void Raft_membership_add_remove_round_trips()
{
var node = new RaftNode("N1");
node.AddMember("N2");
node.AddMember("N3");
node.Members.ShouldContain("N2");
node.Members.ShouldContain("N3");
node.RemoveMember("N2");
node.Members.ShouldNotContain("N2");
}
}

View File

@@ -0,0 +1,15 @@
namespace NATS.Server.Tests;
public class RaftSnapshotTransferRuntimeParityTests
{
[Fact]
public async Task Raft_snapshot_install_catches_up_lagging_follower()
{
var cluster = RaftTestCluster.Create(3);
await cluster.GenerateCommittedEntriesAsync(3);
await cluster.RestartLaggingFollowerAsync();
await cluster.WaitForFollowerCatchupAsync();
cluster.LaggingFollower.AppliedIndex.ShouldBeGreaterThan(0);
}
}

View File

@@ -0,0 +1,14 @@
using NATS.Server.Routes;
namespace NATS.Server.Tests;
public class RouteAccountScopedTests
{
[Fact]
public void Route_connect_info_includes_account_scope()
{
var json = RouteConnection.BuildConnectInfoJson("S1", ["A"], "topology-v1");
json.ShouldContain("\"accounts\":[\"A\"]");
json.ShouldContain("\"topology\":\"topology-v1\"");
}
}

View File

@@ -0,0 +1,16 @@
using System.Text;
using NATS.Server.Routes;
namespace NATS.Server.Tests;
public class RouteCompressionTests
{
[Fact]
public void Route_payload_round_trips_through_compression_codec()
{
var payload = Encoding.UTF8.GetBytes(new string('x', 512));
var compressed = RouteCompressionCodec.Compress(payload);
var restored = RouteCompressionCodec.Decompress(compressed);
restored.ShouldBe(payload);
}
}

View File

@@ -0,0 +1,25 @@
using Microsoft.Extensions.Logging.Abstractions;
using NATS.Server.Configuration;
using NATS.Server.Routes;
namespace NATS.Server.Tests;
public class RouteTopologyGossipTests
{
[Fact]
public void Topology_snapshot_reports_server_and_route_counts()
{
var manager = new RouteManager(
new ClusterOptions { Host = "127.0.0.1", Port = 0 },
new ServerStats(),
"S1",
_ => { },
_ => { },
NullLogger<RouteManager>.Instance);
var snapshot = manager.BuildTopologySnapshot();
snapshot.ServerId.ShouldBe("S1");
snapshot.RouteCount.ShouldBe(0);
snapshot.ConnectedServerIds.ShouldBeEmpty();
}
}

View File

@@ -0,0 +1,41 @@
using System.Net;
using System.Net.Sockets;
using Microsoft.Extensions.Logging.Abstractions;
using NATS.Server.Server;
namespace NATS.Server.Tests;
public class AcceptLoopErrorCallbackTests
{
[Fact]
public void Accept_loop_reports_error_via_callback_hook()
{
var server = new NatsServer(new NatsOptions
{
Host = "127.0.0.1",
Port = 0,
}, NullLoggerFactory.Instance);
Exception? capturedError = null;
EndPoint? capturedEndpoint = null;
var capturedDelay = TimeSpan.Zero;
var handler = new AcceptLoopErrorHandler((ex, endpoint, delay) =>
{
capturedError = ex;
capturedEndpoint = endpoint;
capturedDelay = delay;
});
server.SetAcceptLoopErrorHandlerForTest(handler);
var endpoint = new IPEndPoint(IPAddress.Loopback, 4222);
var error = new SocketException((int)SocketError.ConnectionReset);
var delay = TimeSpan.FromMilliseconds(20);
server.NotifyAcceptErrorForTest(error, endpoint, delay);
capturedError.ShouldBe(error);
capturedEndpoint.ShouldBe(endpoint);
capturedDelay.ShouldBe(delay);
}
}

View File

@@ -0,0 +1,84 @@
using System.Net;
using System.Net.Sockets;
using Microsoft.Extensions.Logging.Abstractions;
namespace NATS.Server.Tests;
public class AcceptLoopReloadLockTests
{
[Fact]
public async Task Accept_loop_blocks_client_creation_while_reload_lock_is_held()
{
await using var fx = await AcceptLoopFixture.StartAsync();
await fx.HoldReloadLockAsync();
(await fx.TryConnectClientAsync(timeoutMs: 150)).ShouldBeFalse();
fx.ReleaseReloadLock();
}
}
internal sealed class AcceptLoopFixture : IAsyncDisposable
{
private readonly NatsServer _server;
private readonly CancellationTokenSource _cts;
private bool _reloadHeld;
private AcceptLoopFixture(NatsServer server, CancellationTokenSource cts)
{
_server = server;
_cts = cts;
}
public static async Task<AcceptLoopFixture> StartAsync()
{
var server = new NatsServer(new NatsOptions
{
Host = "127.0.0.1",
Port = 0,
}, NullLoggerFactory.Instance);
var cts = new CancellationTokenSource();
_ = server.StartAsync(cts.Token);
await server.WaitForReadyAsync();
return new AcceptLoopFixture(server, cts);
}
public async Task HoldReloadLockAsync()
{
await _server.AcquireReloadLockForTestAsync();
_reloadHeld = true;
}
public void ReleaseReloadLock()
{
if (_reloadHeld)
{
_server.ReleaseReloadLockForTest();
_reloadHeld = false;
}
}
public async Task<bool> TryConnectClientAsync(int timeoutMs)
{
using var client = new TcpClient();
using var timeout = new CancellationTokenSource(timeoutMs);
await client.ConnectAsync(IPAddress.Loopback, _server.Port, timeout.Token);
await using var stream = client.GetStream();
var buffer = new byte[1];
try
{
var read = await stream.ReadAsync(buffer.AsMemory(0, 1), timeout.Token);
return read > 0;
}
catch (OperationCanceledException)
{
return false;
}
}
public async ValueTask DisposeAsync()
{
ReleaseReloadLock();
await _cts.CancelAsync();
_server.Dispose();
_cts.Dispose();
}
}

View File

@@ -0,0 +1,22 @@
using NATS.Server.Subscriptions;
namespace NATS.Server.Tests;
public class SubListAsyncCacheSweepTests
{
[Fact]
public async Task Cache_sweep_runs_async_and_prunes_stale_entries_without_write_locking_match_path()
{
using var sl = new SubList();
sl.Insert(new Subscription { Subject = ">", Sid = "all" });
for (var i = 0; i < 1500; i++)
_ = sl.Match($"orders.{i}");
var initial = sl.CacheCount;
initial.ShouldBeGreaterThan(1024);
await sl.TriggerCacheSweepAsyncForTest();
sl.CacheCount.ShouldBeLessThan(initial);
}
}

View File

@@ -0,0 +1,22 @@
using NATS.Server.Subscriptions;
namespace NATS.Server.Tests;
public class SubListHighFanoutOptimizationTests
{
[Fact]
public void High_fanout_nodes_enable_packed_list_optimization()
{
using var sl = new SubList();
for (var i = 0; i < 300; i++)
{
sl.Insert(new Subscription
{
Subject = "orders.created",
Sid = i.ToString(),
});
}
sl.HighFanoutNodeCountForTest.ShouldBeGreaterThan(0);
}
}

View File

@@ -0,0 +1,16 @@
using NATS.Server.Subscriptions;
namespace NATS.Server.Tests;
public class SubListMatchBytesTests
{
[Fact]
public void MatchBytes_matches_subject_without_string_allocation_and_respects_remote_filter()
{
using var sl = new SubList();
sl.MatchBytes("orders.created"u8).PlainSubs.Length.ShouldBe(0);
sl.Insert(new Subscription { Subject = "orders.*", Sid = "1" });
sl.MatchBytes("orders.created"u8).PlainSubs.Length.ShouldBe(1);
}
}

View File

@@ -0,0 +1,24 @@
using NATS.Server.Subscriptions;
namespace NATS.Server.Tests;
public class SubListNotificationTests
{
[Fact]
public void Interest_change_notifications_are_emitted_for_local_and_remote_changes()
{
using var sl = new SubList();
var changes = new List<InterestChange>();
sl.InterestChanged += changes.Add;
var sub = new Subscription { Subject = "orders.created", Sid = "1" };
sl.Insert(sub);
sl.Remove(sub);
sl.ApplyRemoteSub(new RemoteSubscription("orders.*", null, "r1", "A"));
sl.ApplyRemoteSub(RemoteSubscription.Removal("orders.*", null, "r1", "A"));
changes.Count.ShouldBe(4);
changes.Select(c => c.Kind).ShouldContain(InterestChangeKind.LocalAdded);
changes.Select(c => c.Kind).ShouldContain(InterestChangeKind.RemoteAdded);
}
}

View File

@@ -0,0 +1,17 @@
using NATS.Server.Subscriptions;
namespace NATS.Server.Tests;
public class SubListQueueWeightTests
{
[Fact]
public void Remote_queue_weight_expands_matches()
{
using var sl = new SubList();
sl.ApplyRemoteSub(new RemoteSubscription("orders.*", "q", "r1", "A", QueueWeight: 3));
var matches = sl.MatchRemote("A", "orders.created");
matches.Count.ShouldBe(3);
matches.ShouldAllBe(m => m.Queue == "q");
}
}

View File

@@ -0,0 +1,18 @@
using NATS.Server.Subscriptions;
namespace NATS.Server.Tests;
public class SubListRemoteFilterTests
{
[Fact]
public void Match_remote_filters_by_account_and_subject()
{
using var sl = new SubList();
sl.ApplyRemoteSub(new RemoteSubscription("orders.*", null, "r1", "A"));
sl.ApplyRemoteSub(new RemoteSubscription("orders.*", null, "r2", "B"));
var aMatches = sl.MatchRemote("A", "orders.created");
aMatches.Count.ShouldBe(1);
aMatches[0].Account.ShouldBe("A");
}
}