Files
natsdotnet/tests/NATS.Server.Mqtt.Tests/Mqtt/MqttCrossProtocolTests.cs
Joseph Doherty 845441b32c feat: implement full MQTT Go parity across 5 phases — binary protocol, auth/TLS, cross-protocol bridging, monitoring, and JetStream persistence
Phase 1: Binary MQTT 3.1.1 wire protocol with PipeReader-based parsing,
full packet type dispatch, and MQTT 3.1.1 compliance checks.

Phase 2: Auth pipeline routing MQTT CONNECT through AuthService,
TLS transport with SslStream wrapping, pinned cert validation.

Phase 3: IMessageRouter refactor (NatsClient → INatsClient),
MqttNatsClientAdapter for cross-protocol bridging, MqttTopicMapper
with full Go-parity topic/subject translation.

Phase 4: /connz mqtt_client field population, /varz actual MQTT port.

Phase 5: JetStream persistence — MqttStreamInitializer creates 5
internal streams, MqttConsumerManager for QoS 1/2 consumers,
subject-keyed session/retained lookups replacing linear scans.

All 503 MQTT tests and 1589 Core tests pass.
2026-03-13 10:09:40 -04:00

165 lines
5.3 KiB
C#

using NATS.Server.Auth;
using NATS.Server.Mqtt;
using NATS.Server.Subscriptions;
namespace NATS.Server.Mqtt.Tests;
/// <summary>
/// Tests for the MqttNatsClientAdapter and cross-protocol bridging concepts.
/// Verifies that MQTT connections can participate in the NATS SubList and
/// that topic/subject translation works end-to-end.
/// </summary>
public class MqttCrossProtocolTests
{
[Fact]
public void Adapter_implements_INatsClient()
{
using var stream = new MemoryStream();
var listener = CreateTestListener();
var connection = new MqttConnection(stream, listener);
var adapter = new MqttNatsClientAdapter(connection, 42);
adapter.Id.ShouldBe((ulong)42);
adapter.Kind.ShouldBe(ClientKind.Client);
adapter.ClientOpts.ShouldBeNull();
}
[Fact]
public void Adapter_add_and_remove_subscription()
{
using var stream = new MemoryStream();
var listener = CreateTestListener();
var connection = new MqttConnection(stream, listener);
var adapter = new MqttNatsClientAdapter(connection, 1);
var account = new Account("test");
adapter.Account = account;
// Add subscription
var sub = adapter.AddSubscription("sensor.temp", "sid1");
sub.Subject.ShouldBe("sensor.temp");
sub.Client.ShouldBe(adapter);
adapter.Subscriptions.Count.ShouldBe(1);
// Verify it's in the SubList
var result = account.SubList.Match("sensor.temp");
result.PlainSubs.ShouldContain(s => s.Sid == "sid1");
// Remove subscription
adapter.RemoveSubscription("sid1");
adapter.Subscriptions.Count.ShouldBe(0);
// Verify removed from SubList
result = account.SubList.Match("sensor.temp");
result.PlainSubs.ShouldNotContain(s => s.Sid == "sid1");
}
[Fact]
public void Adapter_remove_all_subscriptions()
{
using var stream = new MemoryStream();
var listener = CreateTestListener();
var connection = new MqttConnection(stream, listener);
var adapter = new MqttNatsClientAdapter(connection, 1);
var account = new Account("test");
adapter.Account = account;
adapter.AddSubscription("a.b", "s1");
adapter.AddSubscription("c.d", "s2");
adapter.AddSubscription("e.f", "s3");
adapter.Subscriptions.Count.ShouldBe(3);
adapter.RemoveAllSubscriptions();
adapter.Subscriptions.Count.ShouldBe(0);
}
[Fact]
public void Adapter_queue_outbound_is_noop()
{
using var stream = new MemoryStream();
var listener = CreateTestListener();
var connection = new MqttConnection(stream, listener);
var adapter = new MqttNatsClientAdapter(connection, 1);
adapter.QueueOutbound(new byte[] { 1, 2, 3 }).ShouldBeTrue();
}
[Fact]
public void Adapter_signal_flush_is_noop()
{
using var stream = new MemoryStream();
var listener = CreateTestListener();
var connection = new MqttConnection(stream, listener);
var adapter = new MqttNatsClientAdapter(connection, 1);
// Should not throw
adapter.SignalFlush();
}
[Fact]
public void Topic_mapper_integration_with_sublist()
{
var account = new Account("test");
// Simulate an MQTT client subscribing to "sensor/+"
var natsSubject = MqttTopicMapper.MqttToNats("sensor/+");
natsSubject.ShouldBe("sensor.*");
var sub = new Subscription
{
Subject = natsSubject,
Sid = "mqtt-sub-1",
};
account.SubList.Insert(sub);
// Simulate a NATS publish to "sensor.temp" — should match
var result = account.SubList.Match("sensor.temp");
result.PlainSubs.ShouldContain(s => s.Sid == "mqtt-sub-1");
// "sensor.humidity" should also match
result = account.SubList.Match("sensor.humidity");
result.PlainSubs.ShouldContain(s => s.Sid == "mqtt-sub-1");
// "other.temp" should NOT match
result = account.SubList.Match("other.temp");
result.PlainSubs.ShouldNotContain(s => s.Sid == "mqtt-sub-1");
}
[Fact]
public void Topic_mapper_multilevel_wildcard_with_sublist()
{
var account = new Account("test");
// MQTT subscribe to "home/#"
var natsSubject = MqttTopicMapper.MqttToNats("home/#");
natsSubject.ShouldBe("home.>");
var sub = new Subscription
{
Subject = natsSubject,
Sid = "mqtt-sub-2",
};
account.SubList.Insert(sub);
// Should match multi-level subjects
account.SubList.Match("home.living.light").PlainSubs
.ShouldContain(s => s.Sid == "mqtt-sub-2");
account.SubList.Match("home.kitchen").PlainSubs
.ShouldContain(s => s.Sid == "mqtt-sub-2");
}
[Fact]
public void Adapter_mqtt_client_id_exposed()
{
using var stream = new MemoryStream();
var listener = CreateTestListener();
var connection = new MqttConnection(stream, listener);
var adapter = new MqttNatsClientAdapter(connection, 1);
// ClientId comes from the underlying connection
adapter.MqttClientId.ShouldBe(string.Empty); // not yet connected
}
private static MqttListener CreateTestListener()
=> new("127.0.0.1", 0);
}