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:
87
tests/NATS.Server.Tests/Protocol/ProtoWireParityTests.cs
Normal file
87
tests/NATS.Server.Tests/Protocol/ProtoWireParityTests.cs
Normal file
@@ -0,0 +1,87 @@
|
||||
using NATS.Server.Protocol;
|
||||
|
||||
namespace NATS.Server.Tests.ProtocolParity;
|
||||
|
||||
public class ProtoWireParityTests
|
||||
{
|
||||
[Fact]
|
||||
public void ScanField_reads_tag_and_value_size_for_length_delimited_field()
|
||||
{
|
||||
// field=2, type=2, len=3, bytes=abc
|
||||
byte[] bytes = [0x12, 0x03, (byte)'a', (byte)'b', (byte)'c'];
|
||||
|
||||
var (number, wireType, size) = ProtoWire.ScanField(bytes);
|
||||
|
||||
number.ShouldBe(2);
|
||||
wireType.ShouldBe(2);
|
||||
size.ShouldBe(5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScanTag_rejects_invalid_field_numbers()
|
||||
{
|
||||
var zeroFieldEx = Should.Throw<ProtoWireException>(() => ProtoWire.ScanTag([0x00]));
|
||||
zeroFieldEx.Message.ShouldBe(ProtoWire.ErrProtoInvalidFieldNumber);
|
||||
|
||||
var tooLargeTag = ProtoWire.EncodeVarint(((ulong)int.MaxValue + 1UL) << 3);
|
||||
var tooLargeEx = Should.Throw<ProtoWireException>(() => ProtoWire.ScanTag(tooLargeTag));
|
||||
tooLargeEx.Message.ShouldBe(ProtoWire.ErrProtoInvalidFieldNumber);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScanFieldValue_supports_expected_wire_types()
|
||||
{
|
||||
ProtoWire.ScanFieldValue(5, [0, 0, 0, 0]).ShouldBe(4);
|
||||
ProtoWire.ScanFieldValue(1, [0, 0, 0, 0, 0, 0, 0, 0]).ShouldBe(8);
|
||||
ProtoWire.ScanFieldValue(0, [0x01]).ShouldBe(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScanFieldValue_rejects_unsupported_wire_type()
|
||||
{
|
||||
var ex = Should.Throw<ProtoWireException>(() => ProtoWire.ScanFieldValue(3, [0x00]));
|
||||
ex.Message.ShouldBe("unsupported type: 3");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScanVarint_reports_insufficient_and_overflow_errors()
|
||||
{
|
||||
var insufficient = Should.Throw<ProtoWireException>(() => ProtoWire.ScanVarint([0x80]));
|
||||
insufficient.Message.ShouldBe(ProtoWire.ErrProtoInsufficient);
|
||||
|
||||
byte[] overflow = [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x02];
|
||||
var tooLarge = Should.Throw<ProtoWireException>(() => ProtoWire.ScanVarint(overflow));
|
||||
tooLarge.Message.ShouldBe(ProtoWire.ErrProtoOverflow);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScanBytes_reports_insufficient_when_length_prefix_exceeds_payload()
|
||||
{
|
||||
var ex = Should.Throw<ProtoWireException>(() => ProtoWire.ScanBytes([0x04, 0x01, 0x02]));
|
||||
ex.Message.ShouldBe(ProtoWire.ErrProtoInsufficient);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EncodeVarint_round_trips_values_via_scan_varint()
|
||||
{
|
||||
ulong[] values =
|
||||
[
|
||||
0UL,
|
||||
1UL,
|
||||
127UL,
|
||||
128UL,
|
||||
16_383UL,
|
||||
16_384UL,
|
||||
(1UL << 32) - 1,
|
||||
ulong.MaxValue,
|
||||
];
|
||||
|
||||
foreach (var value in values)
|
||||
{
|
||||
var encoded = ProtoWire.EncodeVarint(value);
|
||||
var (decoded, size) = ProtoWire.ScanVarint(encoded);
|
||||
decoded.ShouldBe(value);
|
||||
size.ShouldBe(encoded.Length);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
using NATS.Server;
|
||||
using NATS.Server.Protocol;
|
||||
|
||||
namespace NATS.Server.Tests.ProtocolParity;
|
||||
|
||||
public class ProtocolDefaultConstantsGapParityTests
|
||||
{
|
||||
[Fact]
|
||||
public void NatsProtocol_exposes_core_default_constants()
|
||||
{
|
||||
NatsProtocol.DefaultHost.ShouldBe("0.0.0.0");
|
||||
NatsProtocol.DefaultHttpPort.ShouldBe(8222);
|
||||
NatsProtocol.DefaultHttpBasePath.ShouldBe("/");
|
||||
NatsProtocol.DefaultRoutePoolSize.ShouldBe(3);
|
||||
NatsProtocol.DefaultLeafNodePort.ShouldBe(7422);
|
||||
NatsProtocol.MaxPayloadMaxSize.ShouldBe(8 * 1024 * 1024);
|
||||
NatsProtocol.DefaultMaxConnections.ShouldBe(64 * 1024);
|
||||
NatsProtocol.DefaultPingMaxOut.ShouldBe(2);
|
||||
NatsProtocol.DefaultMaxClosedClients.ShouldBe(10_000);
|
||||
NatsProtocol.DefaultConnectErrorReports.ShouldBe(3600);
|
||||
NatsProtocol.DefaultReconnectErrorReports.ShouldBe(1);
|
||||
NatsProtocol.DefaultAllowResponseMaxMsgs.ShouldBe(1);
|
||||
NatsProtocol.DefaultServiceLatencySampling.ShouldBe(100);
|
||||
NatsProtocol.DefaultSystemAccount.ShouldBe("$SYS");
|
||||
NatsProtocol.DefaultGlobalAccount.ShouldBe("$G");
|
||||
NatsProtocol.ProtoSnippetSize.ShouldBe(32);
|
||||
NatsProtocol.MaxControlLineSnippetSize.ShouldBe(128);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NatsProtocol_exposes_core_default_timespans()
|
||||
{
|
||||
NatsProtocol.TlsTimeout.ShouldBe(TimeSpan.FromSeconds(2));
|
||||
NatsProtocol.DefaultTlsHandshakeFirstFallbackDelay.ShouldBe(TimeSpan.FromMilliseconds(50));
|
||||
NatsProtocol.AuthTimeout.ShouldBe(TimeSpan.FromSeconds(2));
|
||||
NatsProtocol.DefaultRouteConnect.ShouldBe(TimeSpan.FromSeconds(1));
|
||||
NatsProtocol.DefaultRouteConnectMax.ShouldBe(TimeSpan.FromSeconds(30));
|
||||
NatsProtocol.DefaultRouteReconnect.ShouldBe(TimeSpan.FromSeconds(1));
|
||||
NatsProtocol.DefaultRouteDial.ShouldBe(TimeSpan.FromSeconds(1));
|
||||
NatsProtocol.DefaultLeafNodeReconnect.ShouldBe(TimeSpan.FromSeconds(1));
|
||||
NatsProtocol.DefaultLeafTlsTimeout.ShouldBe(TimeSpan.FromSeconds(2));
|
||||
NatsProtocol.DefaultLeafNodeInfoWait.ShouldBe(TimeSpan.FromSeconds(1));
|
||||
NatsProtocol.DefaultRttMeasurementInterval.ShouldBe(TimeSpan.FromHours(1));
|
||||
NatsProtocol.DefaultAllowResponseExpiration.ShouldBe(TimeSpan.FromMinutes(2));
|
||||
NatsProtocol.DefaultServiceExportResponseThreshold.ShouldBe(TimeSpan.FromMinutes(2));
|
||||
NatsProtocol.DefaultAccountFetchTimeout.ShouldBe(TimeSpan.FromMilliseconds(1900));
|
||||
NatsProtocol.DefaultPingInterval.ShouldBe(TimeSpan.FromMinutes(2));
|
||||
NatsProtocol.DefaultFlushDeadline.ShouldBe(TimeSpan.FromSeconds(10));
|
||||
NatsProtocol.AcceptMinSleep.ShouldBe(TimeSpan.FromMilliseconds(10));
|
||||
NatsProtocol.AcceptMaxSleep.ShouldBe(TimeSpan.FromSeconds(1));
|
||||
NatsProtocol.DefaultLameDuckDuration.ShouldBe(TimeSpan.FromMinutes(2));
|
||||
NatsProtocol.DefaultLameDuckGracePeriod.ShouldBe(TimeSpan.FromSeconds(10));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NatsOptions_defaults_are_bound_to_protocol_defaults()
|
||||
{
|
||||
var options = new NatsOptions();
|
||||
|
||||
options.Host.ShouldBe(NatsProtocol.DefaultHost);
|
||||
options.Port.ShouldBe(NatsProtocol.DefaultPort);
|
||||
options.MaxConnections.ShouldBe(NatsProtocol.DefaultMaxConnections);
|
||||
options.AuthTimeout.ShouldBe(NatsProtocol.AuthTimeout);
|
||||
options.PingInterval.ShouldBe(NatsProtocol.DefaultPingInterval);
|
||||
options.MaxPingsOut.ShouldBe(NatsProtocol.DefaultPingMaxOut);
|
||||
options.WriteDeadline.ShouldBe(NatsProtocol.DefaultFlushDeadline);
|
||||
options.TlsTimeout.ShouldBe(NatsProtocol.TlsTimeout);
|
||||
options.TlsHandshakeFirstFallback.ShouldBe(NatsProtocol.DefaultTlsHandshakeFirstFallbackDelay);
|
||||
options.MaxClosedClients.ShouldBe(NatsProtocol.DefaultMaxClosedClients);
|
||||
options.LameDuckDuration.ShouldBe(NatsProtocol.DefaultLameDuckDuration);
|
||||
options.LameDuckGracePeriod.ShouldBe(NatsProtocol.DefaultLameDuckGracePeriod);
|
||||
options.ConnectErrorReports.ShouldBe(NatsProtocol.DefaultConnectErrorReports);
|
||||
options.ReconnectErrorReports.ShouldBe(NatsProtocol.DefaultReconnectErrorReports);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using System.Buffers;
|
||||
using System.Text;
|
||||
using NATS.Server.Protocol;
|
||||
|
||||
namespace NATS.Server.Tests.ProtocolParity;
|
||||
|
||||
public class ProtocolParserSnippetGapParityTests
|
||||
{
|
||||
[Fact]
|
||||
public void ProtoSnippet_returns_empty_quotes_when_start_is_out_of_range()
|
||||
{
|
||||
var bytes = "PING"u8.ToArray();
|
||||
var snippet = NatsParser.ProtoSnippet(bytes.Length, 2, bytes);
|
||||
snippet.ShouldBe("\"\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProtoSnippet_limits_to_requested_window_and_quotes_output()
|
||||
{
|
||||
var bytes = "ABCDEFGHIJ"u8.ToArray();
|
||||
var snippet = NatsParser.ProtoSnippet(2, 4, bytes);
|
||||
snippet.ShouldBe("\"CDEF\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProtoSnippet_matches_go_behavior_when_max_runs_past_buffer_end()
|
||||
{
|
||||
var bytes = "ABCDE"u8.ToArray();
|
||||
var snippet = NatsParser.ProtoSnippet(0, 32, bytes);
|
||||
snippet.ShouldBe("\"ABCD\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_exceeding_max_control_line_includes_snippet_context_in_error()
|
||||
{
|
||||
var parser = new NatsParser();
|
||||
var longSubject = new string('a', NatsProtocol.MaxControlLineSize + 1);
|
||||
var input = Encoding.ASCII.GetBytes($"PUB {longSubject} 0\r\n\r\n");
|
||||
ReadOnlySequence<byte> buffer = new(input);
|
||||
|
||||
var ex = Should.Throw<ProtocolViolationException>(() => parser.TryParse(ref buffer, out _));
|
||||
ex.Message.ShouldContain("Maximum control line exceeded");
|
||||
ex.Message.ShouldContain("snip=");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user