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.
165 lines
5.3 KiB
C#
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);
|
|
}
|