# NATS.E2E.Tests Extended Coverage — Implementation Plan **Date:** 2026-03-12 **Design:** [2026-03-12-e2e-extended-design.md](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`: ```xml ``` Add to `NATS.E2E.Tests.csproj` ``: ```xml ``` **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: 1. **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) 2. **New property**: `int? MonitorPort { get; }` 3. **Config file support in `StartAsync()`**: - If `_configContent` is not null, write to a temp file (`Path.GetTempFileName()` with `.conf` extension), store path in `_configFilePath` - Build args: `exec "{dll}" -p {Port}` + (if config: `-c {_configFilePath}`) + (if monitoring: `-m {MonitorPort}`) + (extra args) 4. **Cleanup in `DisposeAsync()`**: Delete `_configFilePath` if it exists. 5. **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:** ```csharp 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: ```csharp [CollectionDefinition("E2E")] public class E2ECollection : ICollectionFixture; [CollectionDefinition("E2E-Auth")] public class AuthCollection : ICollectionFixture; [CollectionDefinition("E2E-Monitor")] public class MonitorCollection : ICollectionFixture; [CollectionDefinition("E2E-TLS")] public class TlsCollection : ICollectionFixture; [CollectionDefinition("E2E-Accounts")] public class AccountsCollection : ICollectionFixture; [CollectionDefinition("E2E-JetStream")] public class JetStreamCollection : ICollectionFixture; ``` 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:** 1. **`WildcardStar_MatchesSingleToken`**: Sub `foo.*`, pub `foo.bar` → assert received with correct data. 2. **`WildcardGreaterThan_MatchesMultipleTokens`**: Sub `foo.>`, pub `foo.bar.baz` → assert received. 3. **`WildcardStar_DoesNotMatchMultipleTokens`**: Sub `foo.*`, pub `foo.bar.baz` → assert NO message within 1s timeout (use `Task.WhenAny` with short delay to prove no delivery). 4. **`QueueGroup_LoadBalances`**: 3 clients sub to `qtest` with queue group `workers`. 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). 5. **`QueueGroup_MixedWithPlainSub`**: 1 plain sub + 2 queue subs on `qmix`. Pub 10 messages. Plain sub should get all 10. Queue subs combined should get 10 (each message to exactly 1 queue sub). 6. **`Unsub_StopsDelivery`**: Sub to `unsub.test`, ping to flush, then unsubscribe, pub → assert no message within 1s. 7. **`Unsub_WithMaxMessages`**: Sub to `maxmsg.test`. Use raw socket or low-level NATS protocol to send `UNSUB 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. 8. **`FanOut_MultipleSubscribers`**: 3 clients sub to `fanout.test`. Pub 1 message. All 3 receive it. 9. **`EchoOff_PublisherDoesNotReceiveSelf`**: Connect with `NatsOpts { Echo = false }`. Sub to `echo.test`, pub to `echo.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). 10. **`VerboseMode_OkResponses`**: Use raw `TcpClient`/`NetworkStream`. Send `CONNECT {"verbose":true}\r\n` → read `+OK`. Send `SUB test 1\r\n` → read `+OK`. Send `PING\r\n` → read `PONG`. 11. **`NoResponders_Returns503`**: Connect with `NatsOpts { 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`: ```csharp 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: "" } ] } ``` 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 Port` - `string NKeyPublicKey` - `string NKeySeed` - `NatsConnection CreateClient(string user, string password)` — connects with credentials - `NatsConnection 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:** 1. **`UsernamePassword_ValidCredentials_Connects`**: `fixture.CreateClient("testuser", "testpass")` → connect, ping → succeeds. 2. **`UsernamePassword_InvalidPassword_Rejected`**: Connect with wrong password → expect `NatsException` on connect. 3. **`UsernamePassword_NoCredentials_Rejected`**: `fixture.CreateClient()` (no creds) → expect connection error. 4. **`TokenAuth_ValidToken_Connects`**: Spin up a temp `NatsServerProcess` with config `authorization { token: "s3cret" }`. Connect with `NatsOpts { AuthToken = "s3cret" }` → succeeds. 5. **`TokenAuth_InvalidToken_Rejected`**: Same temp server, wrong token → rejected. 6. **`NKeyAuth_ValidSignature_Connects`**: Connect with `NatsOpts` configured for NKey auth using `fixture.NKeySeed` → succeeds. 7. **`NKeyAuth_InvalidSignature_Rejected`**: Connect with a different NKey seed → rejected. 8. **`Permission_PublishAllowed_Succeeds`**: `pubonly` user pubs to `allowed.foo`, `testuser` sub on same → message delivered. 9. **`Permission_PublishDenied_NoDelivery`**: `pubonly` user pubs to `denied.foo` → permission violation, message not delivered. 10. **`Permission_SubscribeDenied_Rejected`**: `pubonly` user tries to sub to `denied.foo` → error or no messages. 11. **`MaxSubscriptions_ExceedsLimit_Rejected`**: Use `limited` user config with `max_subs: 5` added to fixture config. Create 6 subs → last one triggers error. 12. **`MaxPayload_ExceedsLimit_Disconnected`**: Fixture config has `max_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 ` to the server. Properties: - `int Port` — NATS client port - `int MonitorPort` — HTTP monitoring port - `HttpClient MonitorClient` — pre-configured with `BaseAddress = 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:** 1. **`Healthz_ReturnsOk`**: `GET /healthz` → 200, body contains `"status"` key with value `"ok"`. 2. **`Varz_ReturnsServerInfo`**: `GET /varz` → 200, JSON has `server_id` (non-empty string), `version`, `port` (matches fixture port). 3. **`Varz_ReflectsMessageCounts`**: Connect client, pub 5 messages to a subject (with a sub to ensure delivery). `GET /varz` → `in_msgs` >= 5. 4. **`Connz_ListsActiveConnections`**: Connect 2 clients, ping both. `GET /connz` → `num_connections` >= 2, `connections` array has entries. 5. **`Connz_SortByParameter`**: Connect 3 clients, send different amounts of data. `GET /connz?sort=bytes_to` → `connections` array returned (verify it doesn't error; exact sort validation optional). 6. **`Connz_LimitAndOffset`**: Connect 5 clients. `GET /connz?limit=2&offset=1` → `connections` array has exactly 2 entries. 7. **`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()`: 1. Create a temp directory for certs. 2. 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` 3. Export to PEM files in temp dir: `ca.pem`, `server-cert.pem`, `server-key.pem`, `client-cert.pem`, `client-key.pem` 4. Create `NatsServerProcess` with config: ``` listen: "0.0.0.0:{port}" tls { cert_file: "{server-cert.pem}" key_file: "{server-key.pem}" ca_file: "{ca.pem}" } ``` 5. Start server. Properties: - `int Port` - `string CaCertPath`, `string ClientCertPath`, `string ClientKeyPath` - `NatsConnection CreateTlsClient()` — creates client with TLS configured, trusting the test CA - `NatsConnection 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 Port` - `NatsConnection CreateClientA()` — connects as `user_a` - `NatsConnection CreateClientB()` — connects as `user_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:** 1. **`Tls_ClientConnectsSecurely`**: `fixture.CreateTlsClient()` → connect, ping → succeeds. 2. **`Tls_PlainTextConnection_Rejected`**: `fixture.CreatePlainClient()` → connect → expect exception (timeout or auth error since TLS handshake fails). 3. **`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:** 1. **`Accounts_SameAccount_MessageDelivered`**: Two `ACCT_A` clients. Sub + pub on `acct.test` → message received. 2. **`Accounts_CrossAccount_MessageNotDelivered`**: `ACCT_A` client pubs to `cross.test`, `ACCT_B` client subs to `cross.test` → no message within 1s. 3. **`Accounts_EachAccountHasOwnNamespace`**: `ACCT_A` sub on `shared.topic`, `ACCT_B` sub on `shared.topic`. Pub from `ACCT_A` → only `ACCT_A` sub receives. Pub from `ACCT_B` → only `ACCT_B` sub 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 Port` - `NatsConnection 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:** 1. **`Stream_CreateAndInfo`**: Create stream `TEST1` on subjects `["js.test.>"]` with limits retention. Get stream info → verify name, subjects, retention policy match. 2. **`Stream_ListAndNames`**: Create 3 streams (`LIST_A`, `LIST_B`, `LIST_C`). List streams → all 3 present. Get names → all 3 names returned. 3. **`Stream_Delete`**: Create stream `DEL_TEST`, delete it, attempt info → expect not-found error. 4. **`Stream_PublishAndGet`**: Create stream on `js.pub.>`. Publish 3 messages. Get message by sequence 1, 2, 3 → verify data matches. 5. **`Stream_Purge`**: Create stream, publish 5 messages. Purge. Get stream info → `state.messages == 0`. 6. **`Consumer_CreatePullAndConsume`**: Create stream + pull consumer. Publish 5 messages. Pull next batch (5) → receive all 5 with correct data. 7. **`Consumer_AckExplicit`**: Create stream + consumer with explicit ack. Publish message. Pull, ack it. Pull again → no more messages (not redelivered). 8. **`Consumer_ListAndDelete`**: Create stream + 2 consumers. List consumers → 2 present. Delete one. List → 1 remaining. 9. **`Retention_LimitsMaxMessages`**: Create stream with `MaxMsgs: 10`. Publish 15 messages. Stream info → `state.messages == 10`, first seq is 6. 10. **`Retention_MaxAge`**: Create stream with `MaxAge: 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:** ```bash 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