using NSubstitute; using NATS.Server.JetStream.Storage; using NATS.Server.Mqtt; namespace NATS.Server.Tests; // Go reference: server/mqtt.go ($MQTT_msgs, $MQTT_sess, $MQTT_rmsgs JetStream streams) public class MqttPersistenceTests { [Fact] public async Task Session_persists_across_restart() { // Go reference: server/mqtt.go mqttStoreSession — session survives restart var store = MqttSessionStoreTestHelper.CreateWithJetStream(); await store.ConnectAsync("client-1", cleanSession: false); store.AddSubscription("client-1", "topic/test", qos: 1); await store.SaveSessionAsync("client-1"); // Simulate restart — new store backed by the same IStreamStore var recovered = MqttSessionStoreTestHelper.CreateWithJetStream(store.BackingStore!); await recovered.ConnectAsync("client-1", cleanSession: false); var subs = recovered.GetSubscriptions("client-1"); subs.ShouldContainKey("topic/test"); } [Fact] public async Task Clean_session_deletes_existing() { // Go reference: server/mqtt.go cleanSession=true deletes saved state var store = MqttSessionStoreTestHelper.CreateWithJetStream(); await store.ConnectAsync("client-2", cleanSession: false); store.AddSubscription("client-2", "persist/me", qos: 1); await store.SaveSessionAsync("client-2"); // Reconnect with clean session await store.ConnectAsync("client-2", cleanSession: true); var subs = store.GetSubscriptions("client-2"); subs.ShouldBeEmpty(); } [Fact] public async Task Retained_message_survives_restart() { // Go reference: server/mqtt.go retained message persistence via JetStream var retained = MqttRetainedStoreTestHelper.CreateWithJetStream(); await retained.SetRetainedAsync("sensors/temp", "72.5"u8.ToArray()); // Simulate restart var recovered = MqttRetainedStoreTestHelper.CreateWithJetStream(retained.BackingStore!); var msg = await recovered.GetRetainedAsync("sensors/temp"); msg.ShouldNotBeNull(); System.Text.Encoding.UTF8.GetString(msg).ShouldBe("72.5"); } [Fact] public async Task Retained_message_cleared_with_empty_payload() { // Go reference: server/mqtt.go empty payload clears retained var retained = MqttRetainedStoreTestHelper.CreateWithJetStream(); await retained.SetRetainedAsync("sensors/temp", "72.5"u8.ToArray()); await retained.SetRetainedAsync("sensors/temp", ReadOnlyMemory.Empty); // clear var msg = await retained.GetRetainedAsync("sensors/temp"); msg.ShouldBeNull(); } } public static class MqttSessionStoreTestHelper { public static MqttSessionStore CreateWithJetStream(IStreamStore? backingStore = null) { var store = backingStore ?? new MemStore(); return new MqttSessionStore(store); } } public static class MqttRetainedStoreTestHelper { public static MqttRetainedStore CreateWithJetStream(IStreamStore? backingStore = null) { var store = backingStore ?? new MemStore(); return new MqttRetainedStore(store); } }