- 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
21 KiB
NATS.E2E.Tests Extended Coverage — Implementation Plan
Date: 2026-03-12 Design: 2026-03-12-e2e-extended-design.md
Batch Structure
7 batches. Each batch is independently verifiable. Phases 1-5 from the design map to batches 2-6. Batch 1 is infrastructure. Batch 7 is final verification.
| Batch | Steps | Can Parallelize |
|---|---|---|
| 1 — Infrastructure | Steps 1-4 | Steps 2-4 in parallel after Step 1 |
| 2 — Phase 1: Core Messaging | Step 5 | No |
| 3 — Phase 2: Auth & Permissions | Steps 6-7 | No (fixture then tests) |
| 4 — Phase 3: Monitoring | Steps 8-9 | No |
| 5 — Phase 4: TLS & Accounts | Steps 10-13 | Steps 10-11 parallel, Steps 12-13 parallel |
| 6 — Phase 5: JetStream | Steps 14-15 | No |
| 7 — Final Verification | Step 16 | No |
Batch 1: Infrastructure
Step 1: Update NuGet packages and csproj
Files:
Directory.Packages.props(edit)tests/NATS.E2E.Tests/NATS.E2E.Tests.csproj(edit)
Details:
Add to Directory.Packages.props:
<PackageVersion Include="NATS.Client.JetStream" Version="2.7.2" />
Add to NATS.E2E.Tests.csproj <ItemGroup>:
<PackageReference Include="NATS.NKeys" />
<PackageReference Include="NATS.Client.JetStream" />
Verify: dotnet build tests/NATS.E2E.Tests succeeds.
Step 2: Extend NatsServerProcess
Files:
tests/NATS.E2E.Tests/Infrastructure/NatsServerProcess.cs(edit)
Details:
Add to the class:
-
New constructor overload:
NatsServerProcess(string[]? extraArgs = null, string? configContent = null, bool enableMonitoring = false)- Stores
_extraArgs,_configContent,_enableMonitoring - If
enableMonitoring, allocate a second free port →MonitorPort - Keep existing no-arg constructor as-is (calls new one with defaults)
- Stores
-
New property:
int? MonitorPort { get; } -
Config file support in
StartAsync():- If
_configContentis not null, write to a temp file (Path.GetTempFileName()with.confextension), store path in_configFilePath - Build args:
exec "{dll}" -p {Port}+ (if config:-c {_configFilePath}) + (if monitoring:-m {MonitorPort}) + (extra args)
- If
-
Cleanup in
DisposeAsync(): Delete_configFilePathif it exists. -
Static factory:
static NatsServerProcess WithConfig(string configContent, bool enableMonitoring = false)— convenience for creating with config.
Verify: dotnet build tests/NATS.E2E.Tests succeeds. Existing tests still pass (dotnet test tests/NATS.E2E.Tests).
Step 3: Create E2ETestHelper
Files:
tests/NATS.E2E.Tests/Infrastructure/E2ETestHelper.cs(new)
Details:
namespace NATS.E2E.Tests.Infrastructure;
public static class E2ETestHelper
{
public static NatsConnection CreateClient(int port)
=> new(new NatsOpts { Url = $"nats://127.0.0.1:{port}" });
public static NatsConnection CreateClient(int port, NatsOpts opts)
=> new(opts with { Url = $"nats://127.0.0.1:{port}" });
public static CancellationToken Timeout(int seconds = 10)
=> new CancellationTokenSource(TimeSpan.FromSeconds(seconds)).Token;
}
Verify: Builds.
Step 4: Create collection definitions file
Files:
tests/NATS.E2E.Tests/Infrastructure/Collections.cs(new)
Details:
Move existing E2ECollection from NatsServerFixture.cs into this file. Add all collection definitions:
[CollectionDefinition("E2E")]
public class E2ECollection : ICollectionFixture<NatsServerFixture>;
[CollectionDefinition("E2E-Auth")]
public class AuthCollection : ICollectionFixture<AuthServerFixture>;
[CollectionDefinition("E2E-Monitor")]
public class MonitorCollection : ICollectionFixture<MonitorServerFixture>;
[CollectionDefinition("E2E-TLS")]
public class TlsCollection : ICollectionFixture<TlsServerFixture>;
[CollectionDefinition("E2E-Accounts")]
public class AccountsCollection : ICollectionFixture<AccountServerFixture>;
[CollectionDefinition("E2E-JetStream")]
public class JetStreamCollection : ICollectionFixture<JetStreamServerFixture>;
Remove E2ECollection from NatsServerFixture.cs.
Note: The fixture classes referenced here (AuthServerFixture, etc.) don't exist yet — they'll be created in later steps. This file will have build errors until then; that's fine as long as we build after each batch completes.
Actually — to keep each batch independently verifiable, only add the E2E collection definition here in Step 4. The other collection definitions will be added in their respective fixture files in later batches.
Verify: dotnet test tests/NATS.E2E.Tests — existing 3 tests still pass.
Batch 2: Phase 1 — Core Messaging
Step 5: Implement CoreMessagingTests
Files:
tests/NATS.E2E.Tests/CoreMessagingTests.cs(new)
Details:
[Collection("E2E")] — uses existing NatsServerFixture. Primary constructor takes NatsServerFixture fixture.
11 tests:
-
WildcardStar_MatchesSingleToken: Subfoo.*, pubfoo.bar→ assert received with correct data. -
WildcardGreaterThan_MatchesMultipleTokens: Subfoo.>, pubfoo.bar.baz→ assert received. -
WildcardStar_DoesNotMatchMultipleTokens: Subfoo.*, pubfoo.bar.baz→ assert NO message within 1s timeout (useTask.WhenAnywith short delay to prove no delivery). -
QueueGroup_LoadBalances: 3 clients sub toqtestwith queue groupworkers. Pub client sends 30 messages. Each sub collects received messages. Assert: total across all 3 = 30, each sub got at least 1 (no single sub got all). -
QueueGroup_MixedWithPlainSub: 1 plain sub + 2 queue subs onqmix. Pub 10 messages. Plain sub should get all 10. Queue subs combined should get 10 (each message to exactly 1 queue sub). -
Unsub_StopsDelivery: Sub tounsub.test, ping to flush, then unsubscribe, pub → assert no message within 1s. -
Unsub_WithMaxMessages: Sub tomaxmsg.test. Use raw socket or low-level NATS protocol to sendUNSUB sid 3. Pub 5 messages → assert exactly 3 received. Note: NATS.Client.Core may not expose auto-unsub-after-N directly. If not, use raw socket for this test. -
FanOut_MultipleSubscribers: 3 clients sub tofanout.test. Pub 1 message. All 3 receive it. -
EchoOff_PublisherDoesNotReceiveSelf: Connect withNatsOpts { Echo = false }. Sub toecho.test, pub toecho.test. Assert no message within 1s. Then connect a second client (default echo=true), sub and pub → that client DOES receive its own message (as control). -
VerboseMode_OkResponses: Use rawTcpClient/NetworkStream. SendCONNECT {"verbose":true}\r\n→ read+OK. SendSUB test 1\r\n→ read+OK. SendPING\r\n→ readPONG. -
NoResponders_Returns503: Connect withNatsOpts { Headers = true, NoResponders = true }(check if NATS.Client.Core exposes this). Send request to subject with no subscribers → expect exception or 503 status in reply headers.
For negative tests (no message expected), use a short 500ms-1s timeout with Task.WhenAny(readTask, Task.Delay(1000)) pattern — assert the delay wins.
Verify: dotnet test tests/NATS.E2E.Tests — all 14 tests pass (3 original + 11 new).
Batch 3: Phase 2 — Auth & Permissions
Step 6: Implement AuthServerFixture
Files:
tests/NATS.E2E.Tests/Infrastructure/AuthServerFixture.cs(new)
Details:
Class AuthServerFixture : IAsyncLifetime.
At construction time, generate an NKey pair using NATS.NKeys:
var kp = KeyPair.CreateUser();
NKeyPublicKey = kp.EncodedPublicKey; // starts with 'U'
NKeySeed = kp.EncodedSeed; // starts with 'SU'
Store these as public properties so tests can use them.
Config content (NATS conf format):
max_payload: 512
authorization {
users = [
{ user: "testuser", password: "testpass" }
{ user: "tokenuser", password: "s3cret_token" }
{ user: "pubonly", password: "pubpass", permissions: { publish: { allow: ["allowed.>"] }, subscribe: { allow: ["_INBOX.>"] } } }
{ user: "subonly", password: "subpass", permissions: { subscribe: { allow: ["allowed.>", "_INBOX.>"] }, publish: { allow: ["_INBOX.>"] } } }
{ user: "limited", password: "limpass", permissions: { publish: ">", subscribe: ">" } }
{ nkey: "<NKEY_PUBLIC_KEY>" }
]
}
Wait — token auth uses authorization { token: "..." } which is separate from users. We can't mix both in one config. Instead, use separate users for each auth mechanism and test user/pass. For token auth, we need a separate fixture or a workaround.
Simpler approach: use a config with users only (user/pass, nkeys, permissions). For token auth, we can test it with a dedicated NatsServerProcess instance inside the test itself (create server, run test, dispose). This keeps the fixture simple.
Actually, let's keep it simpler: make AuthServerFixture handle user/pass + nkeys + permissions. Add the token tests and max_payload test as standalone tests that spin up their own mini-server via NatsServerProcess.
Properties exposed:
int Portstring NKeyPublicKeystring NKeySeedNatsConnection CreateClient(string user, string password)— connects with credentialsNatsConnection CreateClient()— connects without credentials (should fail on auth-required server)
Collection definition: [CollectionDefinition("E2E-Auth")] in this file.
Verify: Builds.
Step 7: Implement AuthTests
Files:
tests/NATS.E2E.Tests/AuthTests.cs(new)
Details:
[Collection("E2E-Auth")] with AuthServerFixture fixture.
12 tests:
-
UsernamePassword_ValidCredentials_Connects:fixture.CreateClient("testuser", "testpass")→ connect, ping → succeeds. -
UsernamePassword_InvalidPassword_Rejected: Connect with wrong password → expectNatsExceptionon connect. -
UsernamePassword_NoCredentials_Rejected:fixture.CreateClient()(no creds) → expect connection error. -
TokenAuth_ValidToken_Connects: Spin up a tempNatsServerProcesswith configauthorization { token: "s3cret" }. Connect withNatsOpts { AuthToken = "s3cret" }→ succeeds. -
TokenAuth_InvalidToken_Rejected: Same temp server, wrong token → rejected. -
NKeyAuth_ValidSignature_Connects: Connect withNatsOptsconfigured for NKey auth usingfixture.NKeySeed→ succeeds. -
NKeyAuth_InvalidSignature_Rejected: Connect with a different NKey seed → rejected. -
Permission_PublishAllowed_Succeeds:pubonlyuser pubs toallowed.foo,testusersub on same → message delivered. -
Permission_PublishDenied_NoDelivery:pubonlyuser pubs todenied.foo→ permission violation, message not delivered. -
Permission_SubscribeDenied_Rejected:pubonlyuser tries to sub todenied.foo→ error or no messages. -
MaxSubscriptions_ExceedsLimit_Rejected: Uselimiteduser config withmax_subs: 5added to fixture config. Create 6 subs → last one triggers error. -
MaxPayload_ExceedsLimit_Disconnected: Fixture config hasmax_payload: 512. Send message > 512 bytes → connection closed.
For tests 4-5 (token auth): create/dispose their own NatsServerProcess within the test. Use await using for cleanup.
Verify: dotnet test tests/NATS.E2E.Tests — all 25 tests pass (14 + 11 new; token tests may take slightly longer due to extra server startup).
Note: Token tests spin up independent servers, so they'll be slightly slower. That's acceptable for E2E.
Batch 4: Phase 3 — Monitoring
Step 8: Implement MonitorServerFixture
Files:
tests/NATS.E2E.Tests/Infrastructure/MonitorServerFixture.cs(new)
Details:
Class MonitorServerFixture : IAsyncLifetime.
Creates NatsServerProcess with enableMonitoring: true. This passes -m <port> to the server.
Properties:
int Port— NATS client portint MonitorPort— HTTP monitoring portHttpClient MonitorClient— pre-configured withBaseAddress = new Uri($"http://127.0.0.1:{MonitorPort}")NatsConnection CreateClient()
Dispose: dispose HttpClient and server process.
Collection definition: [CollectionDefinition("E2E-Monitor")] in this file.
Verify: Builds.
Step 9: Implement MonitoringTests
Files:
tests/NATS.E2E.Tests/MonitoringTests.cs(new)
Details:
[Collection("E2E-Monitor")] with MonitorServerFixture fixture.
All tests use fixture.MonitorClient for HTTP calls and System.Text.Json.JsonDocument for JSON parsing.
7 tests:
-
Healthz_ReturnsOk:GET /healthz→ 200, body contains"status"key with value"ok". -
Varz_ReturnsServerInfo:GET /varz→ 200, JSON hasserver_id(non-empty string),version,port(matches fixture port). -
Varz_ReflectsMessageCounts: Connect client, pub 5 messages to a subject (with a sub to ensure delivery).GET /varz→in_msgs>= 5. -
Connz_ListsActiveConnections: Connect 2 clients, ping both.GET /connz→num_connections>= 2,connectionsarray has entries. -
Connz_SortByParameter: Connect 3 clients, send different amounts of data.GET /connz?sort=bytes_to→connectionsarray returned (verify it doesn't error; exact sort validation optional). -
Connz_LimitAndOffset: Connect 5 clients.GET /connz?limit=2&offset=1→connectionsarray has exactly 2 entries. -
Subz_ReturnsSubscriptionStats: Connect client, sub to 3 subjects.GET /subz→ response has subscription data,num_subscriptions> 0.
Verify: dotnet test tests/NATS.E2E.Tests — all 32 tests pass (25 + 7).
Batch 5: Phase 4 — TLS & Accounts
Step 10: Implement TlsServerFixture
Files:
tests/NATS.E2E.Tests/Infrastructure/TlsServerFixture.cs(new)
Details:
Class TlsServerFixture : IAsyncLifetime.
In InitializeAsync():
- Create a temp directory for certs.
- Generate self-signed CA, server cert, client cert using
System.Security.Cryptography:- CA: RSA 2048, self-signed,
CN=E2E Test CA - Server cert: RSA 2048, signed by CA,
CN=localhost, SAN=127.0.0.1 - Client cert: RSA 2048, signed by CA,
CN=testclient
- CA: RSA 2048, self-signed,
- Export to PEM files in temp dir:
ca.pem,server-cert.pem,server-key.pem,client-cert.pem,client-key.pem - Create
NatsServerProcesswith config:
listen: "0.0.0.0:{port}"
tls {
cert_file: "{server-cert.pem}"
key_file: "{server-key.pem}"
ca_file: "{ca.pem}"
}
- Start server.
Properties:
int Portstring CaCertPath,string ClientCertPath,string ClientKeyPathNatsConnection CreateTlsClient()— creates client with TLS configured, trusting the test CANatsConnection CreatePlainClient()— creates client WITHOUT TLS (for rejection test)
Dispose: stop server, delete temp cert directory.
Collection definition: [CollectionDefinition("E2E-TLS")] in this file.
Verify: Builds.
Step 11: Implement AccountServerFixture
Files:
tests/NATS.E2E.Tests/Infrastructure/AccountServerFixture.cs(new)
Details:
Class AccountServerFixture : IAsyncLifetime.
Config:
accounts {
ACCT_A {
users = [
{ user: "user_a", password: "pass_a" }
]
}
ACCT_B {
users = [
{ user: "user_b", password: "pass_b" }
]
}
}
Properties:
int PortNatsConnection CreateClientA()— connects asuser_aNatsConnection CreateClientB()— connects asuser_b
Collection definition: [CollectionDefinition("E2E-Accounts")] in this file.
Verify: Builds.
Step 12: Implement TlsTests
Files:
tests/NATS.E2E.Tests/TlsTests.cs(new)
Details:
[Collection("E2E-TLS")] with TlsServerFixture fixture.
3 tests:
-
Tls_ClientConnectsSecurely:fixture.CreateTlsClient()→ connect, ping → succeeds. -
Tls_PlainTextConnection_Rejected:fixture.CreatePlainClient()→ connect → expect exception (timeout or auth error since TLS handshake fails). -
Tls_PubSub_WorksOverEncryptedConnection: Two TLS clients, pub/sub round-trip → message received.
Verify: Builds, TLS tests pass.
Step 13: Implement AccountIsolationTests
Files:
tests/NATS.E2E.Tests/AccountIsolationTests.cs(new)
Details:
[Collection("E2E-Accounts")] with AccountServerFixture fixture.
3 tests:
-
Accounts_SameAccount_MessageDelivered: TwoACCT_Aclients. Sub + pub onacct.test→ message received. -
Accounts_CrossAccount_MessageNotDelivered:ACCT_Aclient pubs tocross.test,ACCT_Bclient subs tocross.test→ no message within 1s. -
Accounts_EachAccountHasOwnNamespace:ACCT_Asub onshared.topic,ACCT_Bsub onshared.topic. Pub fromACCT_A→ onlyACCT_Asub receives. Pub fromACCT_B→ onlyACCT_Bsub receives.
Verify: dotnet test tests/NATS.E2E.Tests — all 38 tests pass (32 + 6).
Batch 6: Phase 5 — JetStream
Step 14: Implement JetStreamServerFixture
Files:
tests/NATS.E2E.Tests/Infrastructure/JetStreamServerFixture.cs(new)
Details:
Class JetStreamServerFixture : IAsyncLifetime.
Config:
listen: "0.0.0.0:{port}"
jetstream {
store_dir: "{tmpdir}"
max_mem_store: 64mb
max_file_store: 256mb
}
Where {tmpdir} is created via Path.Combine(Path.GetTempPath(), "nats-e2e-js-" + Guid.NewGuid().ToString("N")[..8]).
Properties:
int PortNatsConnection CreateClient()
Dispose: stop server, delete store_dir.
Collection definition: [CollectionDefinition("E2E-JetStream")] in this file.
Verify: Builds.
Step 15: Implement JetStreamTests
Files:
tests/NATS.E2E.Tests/JetStreamTests.cs(new)
Details:
[Collection("E2E-JetStream")] with JetStreamServerFixture fixture.
Uses NATS.Client.JetStream NuGet — create NatsJSContext from the connection.
10 tests:
-
Stream_CreateAndInfo: Create streamTEST1on subjects["js.test.>"]with limits retention. Get stream info → verify name, subjects, retention policy match. -
Stream_ListAndNames: Create 3 streams (LIST_A,LIST_B,LIST_C). List streams → all 3 present. Get names → all 3 names returned. -
Stream_Delete: Create streamDEL_TEST, delete it, attempt info → expect not-found error. -
Stream_PublishAndGet: Create stream onjs.pub.>. Publish 3 messages. Get message by sequence 1, 2, 3 → verify data matches. -
Stream_Purge: Create stream, publish 5 messages. Purge. Get stream info →state.messages == 0. -
Consumer_CreatePullAndConsume: Create stream + pull consumer. Publish 5 messages. Pull next batch (5) → receive all 5 with correct data. -
Consumer_AckExplicit: Create stream + consumer with explicit ack. Publish message. Pull, ack it. Pull again → no more messages (not redelivered). -
Consumer_ListAndDelete: Create stream + 2 consumers. List consumers → 2 present. Delete one. List → 1 remaining. -
Retention_LimitsMaxMessages: Create stream withMaxMsgs: 10. Publish 15 messages. Stream info →state.messages == 10, first seq is 6. -
Retention_MaxAge: Create stream withMaxAge: TimeSpan.FromSeconds(2). Publish messages. Wait 3s. Stream info →state.messages == 0.
Each test uses unique stream/subject names to avoid interference (tests share one JetStream server).
Verify: dotnet test tests/NATS.E2E.Tests — all 48 tests pass (38 + 10).
Batch 7: Final Verification
Step 16: Full build and test run
Commands:
dotnet build
dotnet test tests/NATS.E2E.Tests -v normal
Success criteria: Solution builds clean, all 49 tests pass (3 original + 46 new).
File Summary
| File | Action | Batch |
|---|---|---|
Directory.Packages.props |
edit | 1 |
NATS.E2E.Tests.csproj |
edit | 1 |
Infrastructure/NatsServerProcess.cs |
edit | 1 |
Infrastructure/E2ETestHelper.cs |
new | 1 |
Infrastructure/NatsServerFixture.cs |
edit (remove collection def) | 1 |
Infrastructure/Collections.cs |
new | 1 |
CoreMessagingTests.cs |
new | 2 |
Infrastructure/AuthServerFixture.cs |
new | 3 |
AuthTests.cs |
new | 3 |
Infrastructure/MonitorServerFixture.cs |
new | 4 |
MonitoringTests.cs |
new | 4 |
Infrastructure/TlsServerFixture.cs |
new | 5 |
Infrastructure/AccountServerFixture.cs |
new | 5 |
TlsTests.cs |
new | 5 |
AccountIsolationTests.cs |
new | 5 |
Infrastructure/JetStreamServerFixture.cs |
new | 6 |
JetStreamTests.cs |
new | 6 |
Agent Model Guidance
- Batch 1 (infrastructure): Opus — involves modifying existing code carefully
- Batches 2-6 (test phases): Sonnet — straightforward test implementation from spec
- Batch 7 (verify): Either — just running commands
- Parallel agents within Batch 5: Steps 10-11 (fixtures) can run in parallel, Steps 12-13 (tests) can run in parallel