# 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
<PackageVersion Include="NATS.Client.JetStream" Version="2.7.2" />
```

Add to `NATS.E2E.Tests.csproj` `<ItemGroup>`:
```xml
<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:

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<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:**

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: "<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 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 <port>` 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
