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.
This commit is contained in:
164
tests/NATS.Server.Mqtt.Tests/Mqtt/MqttCrossProtocolTests.cs
Normal file
164
tests/NATS.Server.Mqtt.Tests/Mqtt/MqttCrossProtocolTests.cs
Normal file
@@ -0,0 +1,164 @@
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user