# MQTT Connection Type Parity + Config Parsing Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers-extended-cc:executing-plans to implement this plan task-by-task. **Goal:** Port Go-compatible MQTT connection-type handling for JWT `allowed_connection_types`, add `/connz` `mqtt_client` filtering, parse all Go `MQTTOpts` config fields, and expand `MqttOptsVarz` monitoring output — with tests and docs updates. **Architecture:** Thread a connection-type value into auth context and enforce Go-style allowed-connection-type semantics in `JwtAuthenticator`. Add connz query-option filtering for `mqtt_client` across open and closed connections. Parse the full `mqtt {}` config block into a new `MqttOptions` model following the existing `ParseTls()` pattern in `ConfigProcessor`. Expand `MqttOptsVarz` and wire into `/varz`. Keep behavior backward-compatible and transport-agnostic so MQTT runtime plumbing can be added later without changing auth/monitoring/config semantics. **Tech Stack:** .NET 10, xUnit 3, Shouldly, ASP.NET minimal APIs, System.Text.Json. --- ### Task 1: Add failing JWT connection-type behavior tests **Files:** - Modify: `tests/NATS.Server.Tests/JwtAuthenticatorTests.cs` **Step 1: Write the failing tests** Add these 5 test methods to the existing `JwtAuthenticatorTests` class. Each test must build a valid operator/account/user JWT chain (reuse the existing helper pattern from other tests in the file). The user JWT's `nats.allowed_connection_types` array controls which connection types are permitted. ```csharp [Fact] public async Task Allowed_connection_types_allows_standard_context() { // Build valid operator/account/user JWT chain. // User JWT includes: "allowed_connection_types":["STANDARD"] // Context sets ConnectionType = "STANDARD". // Assert Authenticate() is not null. } [Fact] public async Task Allowed_connection_types_rejects_mqtt_only_for_standard_context() { // User JWT includes: "allowed_connection_types":["MQTT"] // Context sets ConnectionType = "STANDARD". // Assert Authenticate() is null. } [Fact] public async Task Allowed_connection_types_allows_known_even_with_unknown_values() { // User JWT includes: ["STANDARD", "SOME_NEW_TYPE"] // Context sets ConnectionType = "STANDARD". // Assert Authenticate() is not null. } [Fact] public async Task Allowed_connection_types_rejects_when_only_unknown_values_present() { // User JWT includes: ["SOME_NEW_TYPE"] // Context sets ConnectionType = "STANDARD". // Assert Authenticate() is null. } [Fact] public async Task Allowed_connection_types_is_case_insensitive_for_input_values() { // User JWT includes: ["standard"] // Context sets ConnectionType = "STANDARD". // Assert Authenticate() is not null. } ``` **Step 2: Run test to verify it fails** Run: `dotnet test tests/NATS.Server.Tests/NATS.Server.Tests.csproj --filter "FullyQualifiedName~JwtAuthenticatorTests.Allowed_connection_types" -v minimal` Expected: FAIL (current implementation ignores `allowed_connection_types`). **Step 3: Commit test-only checkpoint** ```bash git add tests/NATS.Server.Tests/JwtAuthenticatorTests.cs git commit -m "test: add failing jwt allowed connection type coverage" ``` --- ### Task 2: Implement auth connection-type model and Go-style allowed-type conversion **Files:** - Modify: `src/NATS.Server/Auth/IAuthenticator.cs` (line 11-16: add `ConnectionType` property to `ClientAuthContext`) - Create: `src/NATS.Server/Auth/Jwt/JwtConnectionTypes.cs` - Modify: `src/NATS.Server/Auth/JwtAuthenticator.cs` (insert check after step 7 revocation check, before step 8 permissions, around line 97) - Modify: `src/NATS.Server/NatsClient.cs` (line 382-387: add `ConnectionType` to auth context construction) **Step 1: Add connection type to auth context** In `src/NATS.Server/Auth/IAuthenticator.cs`, add the `ConnectionType` property to `ClientAuthContext`. Note: this requires adding a `using NATS.Server.Auth.Jwt;` at the top of the file. ```csharp public sealed class ClientAuthContext { public required ClientOptions Opts { get; init; } public required byte[] Nonce { get; init; } public string ConnectionType { get; init; } = JwtConnectionTypes.Standard; public X509Certificate2? ClientCertificate { get; init; } } ``` **Step 2: Create JWT connection-type constants + converter helper** Create new file `src/NATS.Server/Auth/Jwt/JwtConnectionTypes.cs`: ```csharp namespace NATS.Server.Auth.Jwt; /// /// Known connection type constants matching Go server/client.go. /// Used for JWT allowed_connection_types claim validation. /// Reference: golang/nats-server/server/client.go connectionType constants. /// internal static class JwtConnectionTypes { public const string Standard = "STANDARD"; public const string Websocket = "WEBSOCKET"; public const string Leafnode = "LEAFNODE"; public const string LeafnodeWs = "LEAFNODE_WS"; public const string Mqtt = "MQTT"; public const string MqttWs = "MQTT_WS"; public const string InProcess = "INPROCESS"; private static readonly HashSet Known = [ Standard, Websocket, Leafnode, LeafnodeWs, Mqtt, MqttWs, InProcess, ]; /// /// Converts a list of connection type strings (from JWT claims) into a set of /// known valid types plus a flag indicating unknown values were present. /// Reference: Go server/client.go convertAllowedConnectionTypes. /// public static (HashSet Valid, bool HasUnknown) Convert(IEnumerable? values) { var valid = new HashSet(StringComparer.Ordinal); var hasUnknown = false; if (values is null) return (valid, false); foreach (var raw in values) { var up = (raw ?? string.Empty).Trim().ToUpperInvariant(); if (up.Length == 0) continue; if (Known.Contains(up)) valid.Add(up); else hasUnknown = true; } return (valid, hasUnknown); } } ``` **Step 3: Enforce allowed connection types in JWT auth** In `src/NATS.Server/Auth/JwtAuthenticator.cs`, insert the following block after the revocation check (step 7, around line 96) and before the permissions build (step 8): ```csharp // 7b. Check allowed connection types var (allowedTypes, hasUnknown) = JwtConnectionTypes.Convert(userClaims.Nats?.AllowedConnectionTypes); if (allowedTypes.Count == 0) { if (hasUnknown) return null; // unknown-only list should reject } else { var connType = string.IsNullOrWhiteSpace(context.ConnectionType) ? JwtConnectionTypes.Standard : context.ConnectionType.ToUpperInvariant(); if (!allowedTypes.Contains(connType)) return null; } ``` **Step 4: Set auth context connection type in client connect path** In `src/NATS.Server/NatsClient.cs` around line 382, add `ConnectionType` to the existing `ClientAuthContext` construction: ```csharp var context = new ClientAuthContext { Opts = ClientOpts, Nonce = _nonce ?? [], ConnectionType = JwtConnectionTypes.Standard, ClientCertificate = TlsState?.PeerCert, }; ``` Add `using NATS.Server.Auth.Jwt;` at the top of the file. **Step 5: Run tests to verify pass** Run: `dotnet test tests/NATS.Server.Tests/NATS.Server.Tests.csproj --filter "FullyQualifiedName~JwtAuthenticatorTests.Allowed_connection_types" -v minimal` Expected: PASS. **Step 6: Commit implementation checkpoint** ```bash git add src/NATS.Server/Auth/IAuthenticator.cs src/NATS.Server/Auth/Jwt/JwtConnectionTypes.cs src/NATS.Server/Auth/JwtAuthenticator.cs src/NATS.Server/NatsClient.cs git commit -m "feat: enforce jwt allowed connection types with go-compatible semantics" ``` --- ### Task 3: Add failing connz mqtt_client filter tests **Files:** - Modify: `tests/NATS.Server.Tests/MonitorTests.cs` **Step 1: Write the failing tests** Add these 2 test methods to the existing `MonitorTests` class. These test the `/connz?mqtt_client=` query parameter filtering. ```csharp [Fact] public async Task Connz_filters_by_mqtt_client_for_open_connections() { // Start server with monitoring port. // Connect a regular NATS client (no MQTT ID). // Query /connz?mqtt_client=some-id. // Assert num_connections == 0 (no client has that MQTT ID). } [Fact] public async Task Connz_filters_by_mqtt_client_for_closed_connections() { // Start server with monitoring port. // Query /connz?state=closed&mqtt_client=missing-id. // Assert num_connections == 0. } ``` **Step 2: Run tests to verify expected failure mode** Run: `dotnet test tests/NATS.Server.Tests/NATS.Server.Tests.csproj --filter "FullyQualifiedName~MonitorTests.Connz_filters_by_mqtt_client" -v minimal` Expected: FAIL (query option not implemented yet — `mqtt_client` param ignored, so all connections returned). **Step 3: Commit test-only checkpoint** ```bash git add tests/NATS.Server.Tests/MonitorTests.cs git commit -m "test: add failing connz mqtt_client filter coverage" ``` --- ### Task 4: Implement connz mqtt_client filtering and closed snapshot support **Files:** - Modify: `src/NATS.Server/Monitoring/Connz.cs` (line 191-210: add `MqttClient` to `ConnzOptions`) - Modify: `src/NATS.Server/Monitoring/ConnzHandler.cs` (line 148-201: parse query param; line 18-29: apply filter after collection but before sort) - Modify: `src/NATS.Server/Monitoring/ClosedClient.cs` (line 6-25: add `MqttClient` property) - Modify: `src/NATS.Server/NatsServer.cs` (line 695-714: add `MqttClient` to closed snapshot) **Step 1: Add `MqttClient` to `ConnzOptions`** In `src/NATS.Server/Monitoring/Connz.cs`, add after `FilterSubject` property (line 205): ```csharp public string MqttClient { get; set; } = ""; ``` **Step 2: Parse `mqtt_client` query param in handler** In `src/NATS.Server/Monitoring/ConnzHandler.cs` `ParseQueryParams` method, add after the existing `limit` parse block (around line 198): ```csharp if (q.TryGetValue("mqtt_client", out var mqttClient)) opts.MqttClient = mqttClient.ToString(); ``` **Step 3: Apply `mqtt_client` filter in `HandleConnz`** In `src/NATS.Server/Monitoring/ConnzHandler.cs` `HandleConnz` method, add after the closed connections collection block (after line 29) and before the sort validation (line 32): ```csharp // Filter by MQTT client ID if (!string.IsNullOrEmpty(opts.MqttClient)) connInfos = connInfos.Where(c => c.MqttClient == opts.MqttClient).ToList(); ``` **Step 4: Add `MqttClient` to `ClosedClient` model** In `src/NATS.Server/Monitoring/ClosedClient.cs`, add after line 24 (`TlsCipherSuite`): ```csharp public string MqttClient { get; init; } = ""; ``` **Step 5: Add `MqttClient` to closed snapshot creation in `NatsServer.RemoveClient`** In `src/NATS.Server/NatsServer.cs` around line 713 (inside the `new ClosedClient { ... }` block), add: ```csharp MqttClient = "", // populated when MQTT transport is implemented ``` **Step 6: Add `MqttClient` to `BuildClosedConnInfo`** In `src/NATS.Server/Monitoring/ConnzHandler.cs` `BuildClosedConnInfo` method (line 119-146), add to the `new ConnInfo { ... }` initializer: ```csharp MqttClient = closed.MqttClient, ``` **Step 7: Run connz mqtt filter tests** Run: `dotnet test tests/NATS.Server.Tests/NATS.Server.Tests.csproj --filter "FullyQualifiedName~MonitorTests.Connz_filters_by_mqtt_client" -v minimal` Expected: PASS. **Step 8: Commit implementation checkpoint** ```bash git add src/NATS.Server/Monitoring/Connz.cs src/NATS.Server/Monitoring/ConnzHandler.cs src/NATS.Server/Monitoring/ClosedClient.cs src/NATS.Server/NatsServer.cs git commit -m "feat: add connz mqtt_client filtering" ``` --- ### Task 5: Verification checkpoint for JWT + connz tasks **Step 1: Run all JWT connection-type tests** Run: `dotnet test tests/NATS.Server.Tests/NATS.Server.Tests.csproj --filter "FullyQualifiedName~JwtAuthenticatorTests.Allowed_connection_types" -v minimal` Expected: PASS. **Step 2: Run all connz tests** Run: `dotnet test tests/NATS.Server.Tests/NATS.Server.Tests.csproj --filter "FullyQualifiedName~MonitorTests.Connz" -v minimal` Expected: PASS. **Step 3: Run full test suite** Run: `dotnet test tests/NATS.Server.Tests/NATS.Server.Tests.csproj -v minimal` Expected: PASS (no regressions). --- ### Task 6: Add MqttOptions model and config parsing **Files:** - Create: `src/NATS.Server/MqttOptions.cs` - Modify: `src/NATS.Server/NatsOptions.cs` (line 116-117: add `Mqtt` property) - Modify: `src/NATS.Server/Configuration/ConfigProcessor.cs` (line 248: add `mqtt` case; add `ParseMqtt` + `ParseMqttAuth` + `ParseMqttTls` + `ToDouble` methods) **Step 1: Create `MqttOptions` model** Create new file `src/NATS.Server/MqttOptions.cs`. This matches Go `MQTTOpts` struct (golang/nats-server/server/opts.go:613-707): ```csharp namespace NATS.Server; /// /// MQTT protocol configuration options. /// Corresponds to Go server/opts.go MQTTOpts struct. /// Config is parsed and stored but no MQTT listener is started yet. /// public sealed class MqttOptions { // Network public string Host { get; set; } = ""; public int Port { get; set; } // Auth override (MQTT-specific, separate from global auth) public string? NoAuthUser { get; set; } public string? Username { get; set; } public string? Password { get; set; } public string? Token { get; set; } public double AuthTimeout { get; set; } // TLS public string? TlsCert { get; set; } public string? TlsKey { get; set; } public string? TlsCaCert { get; set; } public bool TlsVerify { get; set; } public double TlsTimeout { get; set; } = 2.0; public bool TlsMap { get; set; } public HashSet? TlsPinnedCerts { get; set; } // JetStream integration public string? JsDomain { get; set; } public int StreamReplicas { get; set; } public int ConsumerReplicas { get; set; } public bool ConsumerMemoryStorage { get; set; } public TimeSpan ConsumerInactiveThreshold { get; set; } // QoS public TimeSpan AckWait { get; set; } = TimeSpan.FromSeconds(30); public ushort MaxAckPending { get; set; } public TimeSpan JsApiTimeout { get; set; } = TimeSpan.FromSeconds(5); public bool HasTls => TlsCert != null && TlsKey != null; } ``` **Step 2: Add `Mqtt` property to `NatsOptions`** In `src/NATS.Server/NatsOptions.cs`, add before the `HasTls` property (around line 117): ```csharp // MQTT configuration (parsed from config, no listener yet) public MqttOptions? Mqtt { get; set; } ``` **Step 3: Add `ToDouble` helper to `ConfigProcessor`** In `src/NATS.Server/Configuration/ConfigProcessor.cs`, add after the `ToString` helper (around line 654): ```csharp private static double ToDouble(object? value) => value switch { double d => d, long l => l, int i => i, string s when double.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out var d) => d, _ => throw new FormatException($"Cannot convert {value?.GetType().Name ?? "null"} to double"), }; ``` **Step 4: Add `mqtt` case to `ProcessKey` switch** In `src/NATS.Server/Configuration/ConfigProcessor.cs`, replace the default case comment at line 248: ```csharp // MQTT case "mqtt": if (value is Dictionary mqttDict) ParseMqtt(mqttDict, opts, errors); break; // Unknown keys silently ignored (cluster, jetstream, gateway, leafnode, etc.) default: break; ``` **Step 5: Add `ParseMqtt` method** Add this method after `ParseTags` (around line 621). It follows the exact key/alias structure from Go `parseMQTT` (opts.go:5443-5541): ```csharp // ─── MQTT parsing ───────────────────────────────────────────── private static void ParseMqtt(Dictionary dict, NatsOptions opts, List errors) { var mqtt = opts.Mqtt ?? new MqttOptions(); foreach (var (key, value) in dict) { switch (key.ToLowerInvariant()) { case "listen": var (host, port) = ParseHostPort(value); if (host is not null) mqtt.Host = host; if (port is not null) mqtt.Port = port.Value; break; case "port": mqtt.Port = ToInt(value); break; case "host" or "net": mqtt.Host = ToString(value); break; case "no_auth_user": mqtt.NoAuthUser = ToString(value); break; case "tls": if (value is Dictionary tlsDict) ParseMqttTls(tlsDict, mqtt, errors); break; case "authorization" or "authentication": if (value is Dictionary authDict) ParseMqttAuth(authDict, mqtt, errors); break; case "ack_wait" or "ackwait": mqtt.AckWait = ParseDuration(value); break; case "js_api_timeout" or "api_timeout": mqtt.JsApiTimeout = ParseDuration(value); break; case "max_ack_pending" or "max_pending" or "max_inflight": var pending = ToInt(value); if (pending < 0 || pending > 0xFFFF) errors.Add($"mqtt max_ack_pending invalid value {pending}, should be in [0..{0xFFFF}] range"); else mqtt.MaxAckPending = (ushort)pending; break; case "js_domain": mqtt.JsDomain = ToString(value); break; case "stream_replicas": mqtt.StreamReplicas = ToInt(value); break; case "consumer_replicas": mqtt.ConsumerReplicas = ToInt(value); break; case "consumer_memory_storage": mqtt.ConsumerMemoryStorage = ToBool(value); break; case "consumer_inactive_threshold" or "consumer_auto_cleanup": mqtt.ConsumerInactiveThreshold = ParseDuration(value); break; default: // Unknown MQTT keys silently ignored break; } } opts.Mqtt = mqtt; } private static void ParseMqttAuth(Dictionary dict, MqttOptions mqtt, List errors) { foreach (var (key, value) in dict) { switch (key.ToLowerInvariant()) { case "user" or "username": mqtt.Username = ToString(value); break; case "pass" or "password": mqtt.Password = ToString(value); break; case "token": mqtt.Token = ToString(value); break; case "timeout": mqtt.AuthTimeout = ToDouble(value); break; default: break; } } } private static void ParseMqttTls(Dictionary dict, MqttOptions mqtt, List errors) { foreach (var (key, value) in dict) { switch (key.ToLowerInvariant()) { case "cert_file": mqtt.TlsCert = ToString(value); break; case "key_file": mqtt.TlsKey = ToString(value); break; case "ca_file": mqtt.TlsCaCert = ToString(value); break; case "verify": mqtt.TlsVerify = ToBool(value); break; case "verify_and_map": var map = ToBool(value); mqtt.TlsMap = map; if (map) mqtt.TlsVerify = true; break; case "timeout": mqtt.TlsTimeout = ToDouble(value); break; case "pinned_certs": if (value is List pinnedList) { var certs = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var item in pinnedList) { if (item is string s) certs.Add(s.ToLowerInvariant()); } mqtt.TlsPinnedCerts = certs; } break; default: break; } } } ``` **Step 6: Build to verify compilation** Run: `dotnet build` Expected: Build succeeded. **Step 7: Commit** ```bash git add src/NATS.Server/MqttOptions.cs src/NATS.Server/NatsOptions.cs src/NATS.Server/Configuration/ConfigProcessor.cs git commit -m "feat: add mqtt config model and parser for all Go MQTTOpts fields" ``` --- ### Task 7: Add MQTT config parsing tests **Files:** - Create: `tests/NATS.Server.Tests/TestData/mqtt.conf` - Modify: `tests/NATS.Server.Tests/ConfigProcessorTests.cs` **Step 1: Create MQTT test config file** Create `tests/NATS.Server.Tests/TestData/mqtt.conf`: ``` mqtt { listen: "10.0.0.1:1883" no_auth_user: "mqtt_default" authorization { user: "mqtt_user" pass: "mqtt_pass" token: "mqtt_token" timeout: 3.0 } tls { cert_file: "/path/to/mqtt-cert.pem" key_file: "/path/to/mqtt-key.pem" ca_file: "/path/to/mqtt-ca.pem" verify: true timeout: 5.0 } ack_wait: "60s" max_ack_pending: 2048 js_domain: "mqtt-domain" js_api_timeout: "10s" stream_replicas: 3 consumer_replicas: 1 consumer_memory_storage: true consumer_inactive_threshold: "5m" } ``` Ensure this file is copied to output: check that `.csproj` has a wildcard for TestData, or add: ```xml ``` **Step 2: Add MQTT config tests** Add to `tests/NATS.Server.Tests/ConfigProcessorTests.cs`: ```csharp // ─── MQTT config ──────────────────────────────────────────── [Fact] public void MqttConf_ListenHostAndPort() { var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("mqtt.conf")); opts.Mqtt.ShouldNotBeNull(); opts.Mqtt!.Host.ShouldBe("10.0.0.1"); opts.Mqtt.Port.ShouldBe(1883); } [Fact] public void MqttConf_NoAuthUser() { var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("mqtt.conf")); opts.Mqtt.ShouldNotBeNull(); opts.Mqtt!.NoAuthUser.ShouldBe("mqtt_default"); } [Fact] public void MqttConf_Authorization() { var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("mqtt.conf")); opts.Mqtt.ShouldNotBeNull(); opts.Mqtt!.Username.ShouldBe("mqtt_user"); opts.Mqtt.Password.ShouldBe("mqtt_pass"); opts.Mqtt.Token.ShouldBe("mqtt_token"); opts.Mqtt.AuthTimeout.ShouldBe(3.0); } [Fact] public void MqttConf_Tls() { var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("mqtt.conf")); opts.Mqtt.ShouldNotBeNull(); opts.Mqtt!.TlsCert.ShouldBe("/path/to/mqtt-cert.pem"); opts.Mqtt.TlsKey.ShouldBe("/path/to/mqtt-key.pem"); opts.Mqtt.TlsCaCert.ShouldBe("/path/to/mqtt-ca.pem"); opts.Mqtt.TlsVerify.ShouldBeTrue(); opts.Mqtt.TlsTimeout.ShouldBe(5.0); opts.Mqtt.HasTls.ShouldBeTrue(); } [Fact] public void MqttConf_QosSettings() { var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("mqtt.conf")); opts.Mqtt.ShouldNotBeNull(); opts.Mqtt!.AckWait.ShouldBe(TimeSpan.FromSeconds(60)); opts.Mqtt.MaxAckPending.ShouldBe((ushort)2048); opts.Mqtt.JsApiTimeout.ShouldBe(TimeSpan.FromSeconds(10)); } [Fact] public void MqttConf_JetStreamSettings() { var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("mqtt.conf")); opts.Mqtt.ShouldNotBeNull(); opts.Mqtt!.JsDomain.ShouldBe("mqtt-domain"); opts.Mqtt.StreamReplicas.ShouldBe(3); opts.Mqtt.ConsumerReplicas.ShouldBe(1); opts.Mqtt.ConsumerMemoryStorage.ShouldBeTrue(); opts.Mqtt.ConsumerInactiveThreshold.ShouldBe(TimeSpan.FromMinutes(5)); } [Fact] public void MqttConf_MaxAckPendingValidation_ReportsError() { var ex = Should.Throw(() => ConfigProcessor.ProcessConfig(""" mqtt { max_ack_pending: 70000 } """)); ex.Errors.ShouldContain(e => e.Contains("max_ack_pending")); } [Fact] public void MqttConf_Aliases() { // Test alias keys: "ackwait" (alias for "ack_wait"), "net" (alias for "host"), // "max_inflight" (alias for "max_ack_pending"), "consumer_auto_cleanup" (alias) var opts = ConfigProcessor.ProcessConfig(""" mqtt { net: "127.0.0.1" port: 1884 ackwait: "45s" max_inflight: 500 api_timeout: "8s" consumer_auto_cleanup: "10m" } """); opts.Mqtt.ShouldNotBeNull(); opts.Mqtt!.Host.ShouldBe("127.0.0.1"); opts.Mqtt.Port.ShouldBe(1884); opts.Mqtt.AckWait.ShouldBe(TimeSpan.FromSeconds(45)); opts.Mqtt.MaxAckPending.ShouldBe((ushort)500); opts.Mqtt.JsApiTimeout.ShouldBe(TimeSpan.FromSeconds(8)); opts.Mqtt.ConsumerInactiveThreshold.ShouldBe(TimeSpan.FromMinutes(10)); } [Fact] public void MqttConf_Absent_ReturnsNull() { var opts = ConfigProcessor.ProcessConfig("port: 4222"); opts.Mqtt.ShouldBeNull(); } ``` **Step 3: Run MQTT config tests** Run: `dotnet test tests/NATS.Server.Tests/NATS.Server.Tests.csproj --filter "FullyQualifiedName~ConfigProcessorTests.MqttConf" -v minimal` Expected: PASS. **Step 4: Commit** ```bash git add tests/NATS.Server.Tests/TestData/mqtt.conf tests/NATS.Server.Tests/ConfigProcessorTests.cs git commit -m "test: add mqtt config parsing coverage" ``` --- ### Task 8: Expand MqttOptsVarz and wire into /varz **Files:** - Modify: `src/NATS.Server/Monitoring/Varz.cs` (lines 350-360: expand `MqttOptsVarz`) - Modify: `src/NATS.Server/Monitoring/VarzHandler.cs` (line 67-124: populate MQTT block from options) **Step 1: Expand `MqttOptsVarz` class** In `src/NATS.Server/Monitoring/Varz.cs`, replace the existing minimal `MqttOptsVarz` (lines 350-360) with the full Go-compatible struct (matching Go server/monitor.go:1365-1378): ```csharp /// /// MQTT configuration monitoring information. /// Corresponds to Go server/monitor.go MQTTOptsVarz struct. /// public sealed class MqttOptsVarz { [JsonPropertyName("host")] public string Host { get; set; } = ""; [JsonPropertyName("port")] public int Port { get; set; } [JsonPropertyName("no_auth_user")] public string NoAuthUser { get; set; } = ""; [JsonPropertyName("auth_timeout")] public double AuthTimeout { get; set; } [JsonPropertyName("tls_map")] public bool TlsMap { get; set; } [JsonPropertyName("tls_timeout")] public double TlsTimeout { get; set; } [JsonPropertyName("tls_pinned_certs")] public string[] TlsPinnedCerts { get; set; } = []; [JsonPropertyName("js_domain")] public string JsDomain { get; set; } = ""; [JsonPropertyName("ack_wait")] public long AckWait { get; set; } [JsonPropertyName("max_ack_pending")] public ushort MaxAckPending { get; set; } } ``` Note: Go's `AckWait` is serialized as `time.Duration` (nanoseconds as int64). We follow the same pattern used for `PingInterval` and `WriteDeadline` in the existing Varz class. **Step 2: Populate MQTT block in VarzHandler** In `src/NATS.Server/Monitoring/VarzHandler.cs`, add MQTT population to the `return new Varz { ... }` block (around line 123, after `HttpReqStats`): ```csharp Mqtt = BuildMqttVarz(), ``` And add the helper method to `VarzHandler`: ```csharp private MqttOptsVarz BuildMqttVarz() { var mqtt = _options.Mqtt; if (mqtt is null) return new MqttOptsVarz(); return new MqttOptsVarz { Host = mqtt.Host, Port = mqtt.Port, NoAuthUser = mqtt.NoAuthUser ?? "", AuthTimeout = mqtt.AuthTimeout, TlsMap = mqtt.TlsMap, TlsTimeout = mqtt.TlsTimeout, TlsPinnedCerts = mqtt.TlsPinnedCerts?.ToArray() ?? [], JsDomain = mqtt.JsDomain ?? "", AckWait = (long)mqtt.AckWait.TotalNanoseconds, MaxAckPending = mqtt.MaxAckPending, }; } ``` **Step 3: Build to verify compilation** Run: `dotnet build` Expected: Build succeeded. **Step 4: Add varz MQTT test** In `tests/NATS.Server.Tests/MonitorTests.cs`, add a test that verifies the MQTT section appears in `/varz` response. If there's an existing varz test pattern, follow it. Otherwise add: ```csharp [Fact] public async Task Varz_includes_mqtt_config_when_set() { // Start server with monitoring enabled and mqtt config set. // GET /varz. // Assert response contains "mqtt" block with expected host/port values. } ``` The exact test implementation depends on how the existing varz tests create and query the server — follow the existing pattern in MonitorTests.cs. **Step 5: Run full test suite** Run: `dotnet test tests/NATS.Server.Tests/NATS.Server.Tests.csproj -v minimal` Expected: PASS. **Step 6: Commit** ```bash git add src/NATS.Server/Monitoring/Varz.cs src/NATS.Server/Monitoring/VarzHandler.cs tests/NATS.Server.Tests/MonitorTests.cs git commit -m "feat: expand mqtt varz monitoring with all Go-compatible fields" ``` --- ### Task 9: Final verification and differences.md update **Files:** - Modify: `differences.md` **Step 1: Run full test suite** Run: `dotnet test tests/NATS.Server.Tests/NATS.Server.Tests.csproj -v minimal` Expected: PASS (all tests green, no regressions). **Step 2: Update parity document** Edit `differences.md`: 1. In the **Connection Types** table (section 2), update the MQTT row: ```markdown | MQTT clients | Y | Partial | JWT connection-type constants + config parsing; no MQTT transport yet | ``` 2. In the **Connz Response** table (section 7), update the MQTT client ID filtering row: ```markdown | MQTT client ID filtering | Y | Y | `mqtt_client` query param filters open and closed connections | ``` 3. In the **Missing Options Categories** (section 6), replace the "WebSocket/MQTT options" line: ```markdown - WebSocket options - ~~MQTT options~~ — `mqtt {}` config block parsed with all Go `MQTTOpts` fields; no listener yet ``` 4. In the **Auth Mechanisms** table (section 5), add note to JWT row: ```markdown | JWT validation | Y | Y | ... + `allowed_connection_types` enforcement with Go-compatible semantics | ``` **Step 3: Commit docs update** ```bash git add differences.md git commit -m "docs: update differences.md for mqtt connection type parity" ```