// Unit tests for MQTT will message delivery on abnormal disconnection. // Go reference: golang/nats-server/server/mqtt.go — mqttDeliverWill ~line 490, // TestMQTTWill server/mqtt_test.go:4129 using NATS.Server.Mqtt; using Shouldly; namespace NATS.Server.Tests.Mqtt; public class MqttWillMessageTests { // Go ref: mqtt.go mqttSession will field — will message is stored on CONNECT with will flag. [Fact] public void SetWill_stores_will_message() { var store = new MqttSessionStore(); var will = new WillMessage { Topic = "client/status", Payload = "offline"u8.ToArray(), QoS = 0, Retain = false }; store.SetWill("client-1", will); var stored = store.GetWill("client-1"); stored.ShouldNotBeNull(); stored.Topic.ShouldBe("client/status"); stored.Payload.ShouldBe("offline"u8.ToArray()); } // Go ref: mqttDeliverWill — on graceful DISCONNECT, will is cleared (not delivered). [Fact] public void ClearWill_removes_will() { var store = new MqttSessionStore(); var will = new WillMessage { Topic = "client/status", Payload = "offline"u8.ToArray() }; store.SetWill("client-2", will); store.ClearWill("client-2"); store.GetWill("client-2").ShouldBeNull(); } // Go ref: TestMQTTWill server/mqtt_test.go:4129 — will is published on abnormal disconnect. [Fact] public void PublishWillMessage_publishes_on_abnormal_disconnect() { var store = new MqttSessionStore(); string? publishedTopic = null; byte[]? publishedPayload = null; byte publishedQoS = 0xFF; bool publishedRetain = false; store.OnPublish = (topic, payload, qos, retain) => { publishedTopic = topic; publishedPayload = payload; publishedQoS = qos; publishedRetain = retain; }; var will = new WillMessage { Topic = "device/gone", Payload = "disconnected"u8.ToArray(), QoS = 1, Retain = false }; store.SetWill("client-3", will); var result = store.PublishWillMessage("client-3"); result.ShouldBeTrue(); publishedTopic.ShouldBe("device/gone"); publishedPayload.ShouldBe("disconnected"u8.ToArray()); publishedQoS.ShouldBe((byte)1); publishedRetain.ShouldBeFalse(); } // Go ref: mqttDeliverWill — no-op when no will is registered. [Fact] public void PublishWillMessage_returns_false_when_no_will() { var store = new MqttSessionStore(); var invoked = false; store.OnPublish = (_, _, _, _) => { invoked = true; }; var result = store.PublishWillMessage("client-no-will"); result.ShouldBeFalse(); invoked.ShouldBeFalse(); } // Go ref: mqttDeliverWill — will is consumed (not published twice). [Fact] public void PublishWillMessage_clears_will_after_publish() { var store = new MqttSessionStore(); store.OnPublish = (_, _, _, _) => { }; var will = new WillMessage { Topic = "sensor/status", Payload = "gone"u8.ToArray() }; store.SetWill("client-5", will); store.PublishWillMessage("client-5"); store.GetWill("client-5").ShouldBeNull(); store.PublishWillMessage("client-5").ShouldBeFalse(); } // Go ref: TestMQTTWill — graceful DISCONNECT clears the will before disconnect; // subsequent PublishWillMessage has no effect. [Fact] public void CleanDisconnect_does_not_publish_will() { var store = new MqttSessionStore(); var invoked = false; store.OnPublish = (_, _, _, _) => { invoked = true; }; var will = new WillMessage { Topic = "client/status", Payload = "bye"u8.ToArray() }; store.SetWill("client-6", will); // Simulate graceful DISCONNECT: clear will before triggering publish path store.ClearWill("client-6"); var result = store.PublishWillMessage("client-6"); result.ShouldBeFalse(); invoked.ShouldBeFalse(); } // Go ref: TestMQTTWill — published topic and payload must exactly match what was registered. [Fact] public void WillMessage_preserves_topic_and_payload() { var store = new MqttSessionStore(); var capturedTopic = string.Empty; var capturedPayload = Array.Empty(); store.OnPublish = (topic, payload, _, _) => { capturedTopic = topic; capturedPayload = payload; }; var originalPayload = "sensor-offline-payload"u8.ToArray(); store.SetWill("client-7", new WillMessage { Topic = "sensors/temperature/offline", Payload = originalPayload }); store.PublishWillMessage("client-7"); capturedTopic.ShouldBe("sensors/temperature/offline"); capturedPayload.ShouldBe(originalPayload); } // Go ref: TestMQTTWill — QoS level from the will is forwarded to the broker publish path. [Fact] public void WillMessage_preserves_qos() { var store = new MqttSessionStore(); byte capturedQoS = 0xFF; store.OnPublish = (_, _, qos, _) => { capturedQoS = qos; }; store.SetWill("client-8", new WillMessage { Topic = "t", Payload = [], QoS = 1 }); store.PublishWillMessage("client-8"); capturedQoS.ShouldBe((byte)1); } // Go ref: TestMQTTWillRetain — retain flag from the will is forwarded to the broker publish path. [Fact] public void WillMessage_preserves_retain_flag() { var store = new MqttSessionStore(); bool capturedRetain = false; store.OnPublish = (_, _, _, retain) => { capturedRetain = retain; }; store.SetWill("client-9", new WillMessage { Topic = "t", Payload = [], Retain = true }); store.PublishWillMessage("client-9"); capturedRetain.ShouldBeTrue(); } // Go ref: MQTT 5.0 Will-Delay-Interval — a will with delay > 0 is not immediately published; // it is tracked as a delayed will and OnPublish is NOT called immediately. [Fact] public void PublishWillMessage_with_delay_stores_delayed_will_and_does_not_call_OnPublish() { var store = new MqttSessionStore(); var immediatelyPublished = false; store.OnPublish = (_, _, _, _) => { immediatelyPublished = true; }; var will = new WillMessage { Topic = "device/status", Payload = "gone"u8.ToArray(), QoS = 0, Retain = false, DelayIntervalSeconds = 30 }; store.SetWill("client-10", will); var result = store.PublishWillMessage("client-10"); // Returns true because a will was found result.ShouldBeTrue(); // OnPublish must NOT have been called — it is delayed immediatelyPublished.ShouldBeFalse(); // The will must be tracked as a pending delayed will var delayed = store.GetDelayedWill("client-10"); delayed.ShouldNotBeNull(); delayed!.Value.Will.Topic.ShouldBe("device/status"); delayed.Value.Will.DelayIntervalSeconds.ShouldBe(30); } }