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,152 @@
using System.Text.Json;
using NATS.Server.Events;
namespace NATS.Server.Tests.Events;
public class EventApiAndSubjectsParityBatch2Tests
{
[Fact]
public void EventSubjects_DefineMissingServerRequestSubjects()
{
EventSubjects.RemoteLatency.ShouldBe("$SYS.SERVER.{0}.ACC.{1}.LATENCY.M2");
EventSubjects.UserDirectInfo.ShouldBe("$SYS.REQ.USER.INFO");
EventSubjects.UserDirectReq.ShouldBe("$SYS.REQ.USER.{0}.INFO");
EventSubjects.AccountNumSubsReq.ShouldBe("$SYS.REQ.ACCOUNT.NSUBS");
EventSubjects.AccountSubs.ShouldBe("$SYS._INBOX_.{0}.NSUBS");
EventSubjects.ClientKickReq.ShouldBe("$SYS.REQ.SERVER.{0}.KICK");
EventSubjects.ClientLdmReq.ShouldBe("$SYS.REQ.SERVER.{0}.LDM");
EventSubjects.ServerStatsPingReq.ShouldBe("$SYS.REQ.SERVER.PING.STATSZ");
EventSubjects.ServerReloadReq.ShouldBe("$SYS.REQ.SERVER.{0}.RELOAD");
}
[Fact]
public void OcspSubjects_MatchGoPatterns()
{
EventSubjects.OcspPeerReject.ShouldBe("$SYS.SERVER.{0}.OCSP.PEER.CONN.REJECT");
EventSubjects.OcspPeerChainlinkInvalid.ShouldBe("$SYS.SERVER.{0}.OCSP.PEER.LINK.INVALID");
}
[Fact]
public void OcspPeerRejectEvent_IncludesPeerCertInfo()
{
var evt = new OcspPeerRejectEventMsg
{
Id = "id",
Kind = "client",
Reason = "revoked",
Peer = new EventCertInfo
{
Subject = "CN=client",
Issuer = "CN=issuer",
Fingerprint = "fingerprint",
Raw = "raw",
},
};
var json = JsonSerializer.Serialize(evt);
json.ShouldContain("\"peer\":");
json.ShouldContain("\"subject\":\"CN=client\"");
}
[Fact]
public void OcspPeerChainlinkInvalidEvent_SerializesExpectedShape()
{
var evt = new OcspPeerChainlinkInvalidEventMsg
{
Id = "id",
Link = new EventCertInfo { Subject = "CN=link" },
Peer = new EventCertInfo { Subject = "CN=peer" },
};
var json = JsonSerializer.Serialize(evt);
json.ShouldContain("\"type\":\"io.nats.server.advisory.v1.ocsp_peer_link_invalid\"");
json.ShouldContain("\"link\":");
json.ShouldContain("\"peer\":");
}
[Fact]
public void EventFilterOptions_HasCoreGoFields()
{
var opts = new EventFilterOptions
{
Name = "srv-a",
Cluster = "cluster-a",
Host = "127.0.0.1",
Tags = ["a", "b"],
Domain = "domain-a",
};
opts.Name.ShouldBe("srv-a");
opts.Cluster.ShouldBe("cluster-a");
opts.Host.ShouldBe("127.0.0.1");
opts.Tags.ShouldBe(["a", "b"]);
opts.Domain.ShouldBe("domain-a");
}
[Fact]
public void OptionRequestTypes_IncludeBaseFilterFields()
{
new StatszEventOptions { Name = "n" }.Name.ShouldBe("n");
new ConnzEventOptions { Cluster = "c" }.Cluster.ShouldBe("c");
new RoutezEventOptions { Host = "h" }.Host.ShouldBe("h");
new HealthzEventOptions { Domain = "d" }.Domain.ShouldBe("d");
new JszEventOptions { Tags = ["t"] }.Tags.ShouldBe(["t"]);
}
[Fact]
public void ServerApiResponses_ExposeDataAndError()
{
var response = new ServerAPIResponse
{
Server = new EventServerInfo { Id = "S1" },
Data = new { ok = true },
Error = new ServerAPIError { Code = 500, Description = "err" },
};
response.Server.Id.ShouldBe("S1");
response.Error?.Code.ShouldBe(500);
response.Error?.Description.ShouldBe("err");
}
[Fact]
public void TypedServerApiWrappers_CarryResponsePayload()
{
new ServerAPIConnzResponse { Data = new object() }.Data.ShouldNotBeNull();
new ServerAPIRoutezResponse { Data = new object() }.Data.ShouldNotBeNull();
new ServerAPIGatewayzResponse { Data = new object() }.Data.ShouldNotBeNull();
new ServerAPIJszResponse { Data = new object() }.Data.ShouldNotBeNull();
new ServerAPIHealthzResponse { Data = new object() }.Data.ShouldNotBeNull();
new ServerAPIVarzResponse { Data = new object() }.Data.ShouldNotBeNull();
new ServerAPISubszResponse { Data = new object() }.Data.ShouldNotBeNull();
new ServerAPILeafzResponse { Data = new object() }.Data.ShouldNotBeNull();
new ServerAPIAccountzResponse { Data = new object() }.Data.ShouldNotBeNull();
new ServerAPIExpvarzResponse { Data = new object() }.Data.ShouldNotBeNull();
new ServerAPIpqueueszResponse { Data = new object() }.Data.ShouldNotBeNull();
new ServerAPIRaftzResponse { Data = new object() }.Data.ShouldNotBeNull();
}
[Fact]
public void RequestPayloadTypes_KickAndLdm()
{
var kick = new KickClientReq { ClientId = 22 };
var ldm = new LDMClientReq { ClientId = 33 };
kick.ClientId.ShouldBe(22UL);
ldm.ClientId.ShouldBe(33UL);
}
[Fact]
public void UserInfo_IncludesExpectedIdentityFields()
{
var info = new UserInfo
{
User = "alice",
Account = "A",
Permissions = "pubsub",
};
info.User.ShouldBe("alice");
info.Account.ShouldBe("A");
info.Permissions.ShouldBe("pubsub");
}
}

View File

@@ -168,4 +168,31 @@ public class EventCompressionTests : IDisposable
EventCompressor.TotalUncompressed.ShouldBe(0L);
EventCompressor.BytesSaved.ShouldBe(0L);
}
[Fact]
public void GetAcceptEncoding_ParsesSnappyAndGzip()
{
EventCompressor.GetAcceptEncoding("gzip, snappy").ShouldBe(EventCompressionType.Snappy);
EventCompressor.GetAcceptEncoding("gzip").ShouldBe(EventCompressionType.Gzip);
EventCompressor.GetAcceptEncoding("br").ShouldBe(EventCompressionType.Unsupported);
EventCompressor.GetAcceptEncoding(null).ShouldBe(EventCompressionType.None);
}
[Fact]
public void CompressionHeaderConstants_MatchGo()
{
EventCompressor.AcceptEncodingHeader.ShouldBe("Accept-Encoding");
EventCompressor.ContentEncodingHeader.ShouldBe("Content-Encoding");
}
[Fact]
public void CompressAndDecompress_Gzip_RoundTrip_MatchesOriginal()
{
var payload = Encoding.UTF8.GetBytes("""{"server":"s1","data":"gzip-payload"}""");
var compressed = EventCompressor.Compress(payload, EventCompressionType.Gzip);
var restored = EventCompressor.Decompress(compressed, EventCompressionType.Gzip);
restored.ShouldBe(payload);
}
}

View File

@@ -0,0 +1,46 @@
using System.Text.Json;
using NATS.Server.Events;
namespace NATS.Server.Tests.Events;
public class EventServerInfoCapabilityParityBatch1Tests
{
[Fact]
public void ServerCapability_flags_match_expected_values()
{
((ulong)ServerCapability.JetStreamEnabled).ShouldBe(1UL << 0);
((ulong)ServerCapability.BinaryStreamSnapshot).ShouldBe(1UL << 1);
((ulong)ServerCapability.AccountNRG).ShouldBe(1UL << 2);
}
[Fact]
public void EventServerInfo_capability_methods_set_and_read_flags()
{
var info = new EventServerInfo();
info.SetJetStreamEnabled();
info.SetBinaryStreamSnapshot();
info.SetAccountNRG();
info.JetStream.ShouldBeTrue();
info.JetStreamEnabled().ShouldBeTrue();
info.BinaryStreamSnapshot().ShouldBeTrue();
info.AccountNRG().ShouldBeTrue();
}
[Fact]
public void ServerID_serializes_with_name_host_id_fields()
{
var payload = new ServerID
{
Name = "srv-a",
Host = "127.0.0.1",
Id = "N1",
};
var json = JsonSerializer.Serialize(payload);
json.ShouldContain("\"name\":\"srv-a\"");
json.ShouldContain("\"host\":\"127.0.0.1\"");
json.ShouldContain("\"id\":\"N1\"");
}
}

View File

@@ -129,7 +129,7 @@ public class RemoteServerEventTests
string.Format(EventSubjects.RemoteServerShutdown, serverId)
.ShouldBe($"$SYS.SERVER.{serverId}.REMOTE.SHUTDOWN");
string.Format(EventSubjects.LeafNodeConnected, serverId)
.ShouldBe($"$SYS.SERVER.{serverId}.LEAFNODE.CONNECT");
.ShouldBe($"$SYS.ACCOUNT.{serverId}.LEAFNODE.CONNECT");
}
// --- JSON serialization ---