Files
natsdotnet/tests/NATS.Server.Mqtt.Tests/Mqtt/MqttQoSTrackingTests.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

136 lines
4.1 KiB
C#

// Tests for MqttQoS1Tracker (Gap 6.3 — JetStream-backed QoS 1/2 tracking).
// Go reference: golang/nats-server/server/mqtt.go mqttProcessPub (~line 1200).
using NATS.Server.Mqtt;
using Shouldly;
namespace NATS.Server.Mqtt.Tests.Mqtt;
public sealed class MqttQoSTrackingTests
{
// ── QoS 1 Tracker ────────────────────────────────────────────────────────
[Fact]
public void Register_assigns_packet_id()
{
// Go reference: server/mqtt.go mqttProcessPub — assigns non-zero packet ID for QoS 1
var tracker = new MqttQoS1Tracker();
var id = tracker.Register("sensors/temp", [0x01, 0x02]);
id.ShouldNotBe((ushort)0);
}
[Fact]
public void Register_increments_packet_id()
{
// Go reference: server/mqtt.go — each outgoing QoS 1 message gets a unique packet ID
var tracker = new MqttQoS1Tracker();
var id1 = tracker.Register("sensors/temp", [0x01]);
var id2 = tracker.Register("sensors/humidity", [0x02]);
id1.ShouldNotBe(id2);
}
[Fact]
public void Acknowledge_removes_pending()
{
// Go reference: server/mqtt.go mqttProcessPubAck — removes message from pending set
var tracker = new MqttQoS1Tracker();
var id = tracker.Register("sensors/temp", [0xAB]);
tracker.PendingCount.ShouldBe(1);
var removed = tracker.Acknowledge(id);
removed.ShouldNotBeNull();
tracker.PendingCount.ShouldBe(0);
}
[Fact]
public void Acknowledge_returns_null_for_unknown()
{
// Go reference: server/mqtt.go — PUBACK for unknown packet ID is silently ignored
var tracker = new MqttQoS1Tracker();
var result = tracker.Acknowledge(9999);
result.ShouldBeNull();
}
[Fact]
public void PendingCount_reflects_current_state()
{
// Register 3 messages, acknowledge 1, expect count of 2
var tracker = new MqttQoS1Tracker();
var id1 = tracker.Register("a/b", [1]);
tracker.Register("c/d", [2]);
tracker.Register("e/f", [3]);
tracker.Acknowledge(id1);
tracker.PendingCount.ShouldBe(2);
}
[Fact]
public void IsPending_true_for_registered()
{
// Go reference: server/mqtt.go — registered QoS 1 message is in the pending set
var tracker = new MqttQoS1Tracker();
var id = tracker.Register("topic/x", [0xFF]);
tracker.IsPending(id).ShouldBeTrue();
}
[Fact]
public void IsPending_false_after_acknowledge()
{
// Go reference: server/mqtt.go — message is removed from pending after PUBACK
var tracker = new MqttQoS1Tracker();
var id = tracker.Register("topic/x", [0xFF]);
tracker.Acknowledge(id);
tracker.IsPending(id).ShouldBeFalse();
}
[Fact]
public void GetPendingForRedelivery_returns_all_pending()
{
// Go reference: server/mqtt.go reconnect path — all unacked messages are redelivered
var tracker = new MqttQoS1Tracker();
tracker.Register("a", [1]);
tracker.Register("b", [2]);
tracker.Register("c", [3]);
var pending = tracker.GetPendingForRedelivery();
pending.Count.ShouldBe(3);
}
[Fact]
public void GetPendingForRedelivery_increments_delivery_count()
{
// Go reference: server/mqtt.go reconnect redelivery — DUP flag set, delivery count increments
var tracker = new MqttQoS1Tracker();
tracker.Register("test/topic", [0xDE, 0xAD]);
var pending = tracker.GetPendingForRedelivery();
pending[0].DeliveryCount.ShouldBe(2);
}
[Fact]
public void Clear_removes_all_pending()
{
// Go reference: server/mqtt.go session cleanup — all pending messages discarded on clean session
var tracker = new MqttQoS1Tracker();
tracker.Register("x", [1]);
tracker.Register("y", [2]);
tracker.Register("z", [3]);
tracker.Clear();
tracker.PendingCount.ShouldBe(0);
}
}