// Reference: golang/nats-server/server/memstore_test.go and filestore_test.go // Tests ported from: TestMemStoreBasics, TestMemStorePurge, TestMemStoreMsgHeaders, // TestMemStoreTimeStamps, TestMemStoreEraseMsg, // TestMemStoreMsgLimit, TestMemStoreBytesLimit, // TestMemStoreAgeLimit, plus parity tests matching // filestore behavior in MemStore. using System.Text; using NATS.Server.JetStream.Storage; namespace NATS.Server.JetStream.Tests.JetStream.Storage; public sealed class MemStoreTests { // Go: TestMemStoreBasics server/memstore_test.go [Fact] public async Task Store_and_load_messages() { var store = new MemStore(); var seq1 = await store.AppendAsync("foo", "Hello World"u8.ToArray(), default); var seq2 = await store.AppendAsync("bar", "Second"u8.ToArray(), default); seq1.ShouldBe((ulong)1); seq2.ShouldBe((ulong)2); var state = await store.GetStateAsync(default); state.Messages.ShouldBe((ulong)2); state.FirstSeq.ShouldBe((ulong)1); state.LastSeq.ShouldBe((ulong)2); var msg1 = await store.LoadAsync(1, default); msg1.ShouldNotBeNull(); msg1!.Subject.ShouldBe("foo"); msg1.Payload.ToArray().ShouldBe("Hello World"u8.ToArray()); var msg2 = await store.LoadAsync(2, default); msg2.ShouldNotBeNull(); msg2!.Subject.ShouldBe("bar"); } // Go: TestMemStoreBasics server/memstore_test.go [Fact] public async Task Load_non_existent_returns_null() { var store = new MemStore(); await store.AppendAsync("foo", "data"u8.ToArray(), default); (await store.LoadAsync(99, default)).ShouldBeNull(); (await store.LoadAsync(0, default)).ShouldBeNull(); } // Go: TestMemStoreEraseMsg server/memstore_test.go [Fact] public async Task Remove_messages() { var store = new MemStore(); for (var i = 0; i < 5; i++) await store.AppendAsync("foo", Encoding.UTF8.GetBytes($"msg-{i}"), default); (await store.RemoveAsync(2, default)).ShouldBeTrue(); (await store.RemoveAsync(4, default)).ShouldBeTrue(); var state = await store.GetStateAsync(default); state.Messages.ShouldBe((ulong)3); (await store.LoadAsync(2, default)).ShouldBeNull(); (await store.LoadAsync(4, default)).ShouldBeNull(); (await store.LoadAsync(1, default)).ShouldNotBeNull(); (await store.LoadAsync(3, default)).ShouldNotBeNull(); (await store.LoadAsync(5, default)).ShouldNotBeNull(); } // Go: TestMemStoreEraseMsg server/memstore_test.go [Fact] public async Task Remove_non_existent_returns_false() { var store = new MemStore(); await store.AppendAsync("foo", "data"u8.ToArray(), default); (await store.RemoveAsync(99, default)).ShouldBeFalse(); } // Go: TestMemStorePurge server/memstore_test.go [Fact] public async Task Purge_clears_all() { var store = new MemStore(); for (var i = 0; i < 10; i++) await store.AppendAsync("foo", "data"u8.ToArray(), default); (await store.GetStateAsync(default)).Messages.ShouldBe((ulong)10); await store.PurgeAsync(default); var state = await store.GetStateAsync(default); state.Messages.ShouldBe((ulong)0); state.Bytes.ShouldBe((ulong)0); } // Go: TestMemStorePurge server/memstore_test.go [Fact] public async Task Purge_empty_store_is_safe() { var store = new MemStore(); await store.PurgeAsync(default); (await store.GetStateAsync(default)).Messages.ShouldBe((ulong)0); } // Go: TestMemStoreTimeStamps server/memstore_test.go [Fact] public async Task Timestamps_non_decreasing() { var store = new MemStore(); for (var i = 0; i < 10; i++) await store.AppendAsync("foo", "data"u8.ToArray(), default); var messages = await store.ListAsync(default); messages.Count.ShouldBe(10); DateTime? prev = null; foreach (var msg in messages) { if (prev.HasValue) msg.TimestampUtc.ShouldBeGreaterThanOrEqualTo(prev.Value); prev = msg.TimestampUtc; } } // Go: TestMemStoreMsgHeaders (adapted) server/memstore_test.go [Fact] public async Task Payload_with_header_bytes_round_trips() { var store = new MemStore(); var headerBytes = "NATS/1.0\r\nName: derek\r\n\r\n"u8.ToArray(); var bodyBytes = "Hello World"u8.ToArray(); byte[] combined = [.. headerBytes, .. bodyBytes]; await store.AppendAsync("foo", combined, default); var msg = await store.LoadAsync(1, default); msg.ShouldNotBeNull(); msg!.Payload.ToArray().ShouldBe(combined); } // Go: TestMemStoreBasics server/memstore_test.go [Fact] public async Task LoadLastBySubject_returns_most_recent() { var store = new MemStore(); await store.AppendAsync("foo", "first"u8.ToArray(), default); await store.AppendAsync("bar", "other"u8.ToArray(), default); await store.AppendAsync("foo", "second"u8.ToArray(), default); await store.AppendAsync("foo", "third"u8.ToArray(), default); var last = await store.LoadLastBySubjectAsync("foo", default); last.ShouldNotBeNull(); last!.Payload.ToArray().ShouldBe("third"u8.ToArray()); last.Sequence.ShouldBe((ulong)4); (await store.LoadLastBySubjectAsync("does.not.exist", default)).ShouldBeNull(); } // Go: TestMemStoreMsgLimit server/memstore_test.go [Fact] public async Task TrimToMaxMessages_evicts_oldest() { var store = new MemStore(); for (var i = 0; i < 20; i++) await store.AppendAsync("foo", Encoding.UTF8.GetBytes($"msg-{i}"), default); store.TrimToMaxMessages(10); var state = await store.GetStateAsync(default); state.Messages.ShouldBe((ulong)10); state.FirstSeq.ShouldBe((ulong)11); state.LastSeq.ShouldBe((ulong)20); (await store.LoadAsync(1, default)).ShouldBeNull(); (await store.LoadAsync(10, default)).ShouldBeNull(); (await store.LoadAsync(11, default)).ShouldNotBeNull(); } // Go: TestMemStoreMsgLimit server/memstore_test.go [Fact] public async Task TrimToMaxMessages_to_zero() { var store = new MemStore(); for (var i = 0; i < 5; i++) await store.AppendAsync("foo", "data"u8.ToArray(), default); store.TrimToMaxMessages(0); (await store.GetStateAsync(default)).Messages.ShouldBe((ulong)0); } // Go: TestMemStoreBytesLimit server/memstore_test.go [Fact] public async Task Bytes_tracks_payload_sizes() { var store = new MemStore(); var payload = new byte[100]; for (var i = 0; i < 5; i++) await store.AppendAsync("foo", payload, default); var state = await store.GetStateAsync(default); // Go parity: MsgSize = subj.Length + hdr + data + 16 overhead // "foo"(3) + 100 + 16 = 119 per msg × 5 = 595 state.Bytes.ShouldBe((ulong)595); } // Go: TestMemStoreBytesLimit server/memstore_test.go [Fact] public async Task Bytes_decrease_after_remove() { var store = new MemStore(); var payload = new byte[100]; for (var i = 0; i < 5; i++) await store.AppendAsync("foo", payload, default); await store.RemoveAsync(1, default); await store.RemoveAsync(3, default); var state = await store.GetStateAsync(default); // Go parity: MsgSize = subj.Length + hdr + data + 16 overhead // "foo"(3) + 100 + 16 = 119 per msg × 3 remaining = 357 state.Bytes.ShouldBe((ulong)357); } // Snapshot and restore. [Fact] public async Task Snapshot_and_restore() { var store = new MemStore(); for (var i = 0; i < 20; i++) await store.AppendAsync("foo", Encoding.UTF8.GetBytes($"msg-{i}"), default); var snap = await store.CreateSnapshotAsync(default); snap.Length.ShouldBeGreaterThan(0); var restored = new MemStore(); await restored.RestoreSnapshotAsync(snap, default); var srcState = await store.GetStateAsync(default); var dstState = await restored.GetStateAsync(default); dstState.Messages.ShouldBe(srcState.Messages); dstState.FirstSeq.ShouldBe(srcState.FirstSeq); dstState.LastSeq.ShouldBe(srcState.LastSeq); for (ulong i = 1; i <= srcState.Messages; i++) { var original = await store.LoadAsync(i, default); var copy = await restored.LoadAsync(i, default); copy.ShouldNotBeNull(); copy!.Payload.ToArray().ShouldBe(original!.Payload.ToArray()); } } // Snapshot after removes. [Fact] public async Task Snapshot_after_removes() { var store = new MemStore(); for (var i = 0; i < 10; i++) await store.AppendAsync("foo", Encoding.UTF8.GetBytes($"msg-{i}"), default); await store.RemoveAsync(2, default); await store.RemoveAsync(5, default); await store.RemoveAsync(8, default); var snap = await store.CreateSnapshotAsync(default); var restored = new MemStore(); await restored.RestoreSnapshotAsync(snap, default); var dstState = await restored.GetStateAsync(default); dstState.Messages.ShouldBe((ulong)7); (await restored.LoadAsync(2, default)).ShouldBeNull(); (await restored.LoadAsync(5, default)).ShouldBeNull(); (await restored.LoadAsync(8, default)).ShouldBeNull(); (await restored.LoadAsync(1, default)).ShouldNotBeNull(); } // ListAsync ordered. [Fact] public async Task ListAsync_returns_ordered() { var store = new MemStore(); await store.AppendAsync("c", "three"u8.ToArray(), default); await store.AppendAsync("a", "one"u8.ToArray(), default); await store.AppendAsync("b", "two"u8.ToArray(), default); var messages = await store.ListAsync(default); messages.Count.ShouldBe(3); messages[0].Sequence.ShouldBe((ulong)1); messages[1].Sequence.ShouldBe((ulong)2); messages[2].Sequence.ShouldBe((ulong)3); } // Purge then append. [Fact] public async Task Purge_then_append() { var store = new MemStore(); for (var i = 0; i < 5; i++) await store.AppendAsync("foo", "data"u8.ToArray(), default); await store.PurgeAsync(default); var seq = await store.AppendAsync("foo", "after purge"u8.ToArray(), default); seq.ShouldBeGreaterThan((ulong)0); var msg = await store.LoadAsync(seq, default); msg.ShouldNotBeNull(); msg!.Payload.ToArray().ShouldBe("after purge"u8.ToArray()); } // Empty payload. [Fact] public async Task Empty_payload_round_trips() { var store = new MemStore(); await store.AppendAsync("foo", ReadOnlyMemory.Empty, default); var msg = await store.LoadAsync(1, default); msg.ShouldNotBeNull(); msg!.Payload.Length.ShouldBe(0); } // State on empty store. [Fact] public async Task Empty_store_state() { var store = new MemStore(); var state = await store.GetStateAsync(default); state.Messages.ShouldBe((ulong)0); state.Bytes.ShouldBe((ulong)0); state.FirstSeq.ShouldBe((ulong)0); state.LastSeq.ShouldBe((ulong)0); } }