// Reference: golang/nats-server/server/filestore_test.go // Tests ported: TestFileStoreBasics, TestFileStoreMsgHeaders, // TestFileStoreBasicWriteMsgsAndRestore, TestFileStoreRemove using NATS.Server.JetStream.Storage; namespace NATS.Server.Tests.JetStream.Storage; public sealed class FileStoreBasicTests : IDisposable { private readonly string _dir; public FileStoreBasicTests() { _dir = Path.Combine(Path.GetTempPath(), $"nats-js-fs-basic-{Guid.NewGuid():N}"); Directory.CreateDirectory(_dir); } public void Dispose() { if (Directory.Exists(_dir)) Directory.Delete(_dir, recursive: true); } private FileStore CreateStore(string? subdirectory = null) { var dir = subdirectory is null ? _dir : Path.Combine(_dir, subdirectory); return new FileStore(new FileStoreOptions { Directory = dir }); } // Ref: TestFileStoreBasics — stores 5 msgs, checks sequence numbers, // checks State().Msgs, loads msg by sequence and verifies subject/payload. [Fact] public async Task Store_and_load_messages() { await using var store = CreateStore(); const string subject = "foo"; var payload = "Hello World"u8.ToArray(); for (var i = 1; i <= 5; i++) { var seq = await store.AppendAsync(subject, payload, default); seq.ShouldBe((ulong)i); } var state = await store.GetStateAsync(default); state.Messages.ShouldBe((ulong)5); var msg2 = await store.LoadAsync(2, default); msg2.ShouldNotBeNull(); msg2!.Subject.ShouldBe(subject); msg2.Payload.ToArray().ShouldBe(payload); var msg3 = await store.LoadAsync(3, default); msg3.ShouldNotBeNull(); } // Ref: TestFileStoreMsgHeaders — stores a message whose payload carries raw // NATS header bytes, then loads it back and verifies the bytes are intact. // // The .NET FileStore keeps headers as part of the payload bytes (callers // embed the NATS wire header in the payload slice they pass in). We // verify round-trip fidelity for a payload that happens to look like a // NATS header line. [Fact] public async Task Store_message_with_headers() { await using var store = CreateStore(); // Simulate a NATS header embedded in the payload, e.g. "name:derek\r\n\r\nHello World" var headerBytes = "NATS/1.0\r\nname:derek\r\n\r\n"u8.ToArray(); var bodyBytes = "Hello World"u8.ToArray(); var fullPayload = headerBytes.Concat(bodyBytes).ToArray(); await store.AppendAsync("foo", fullPayload, default); var msg = await store.LoadAsync(1, default); msg.ShouldNotBeNull(); msg!.Payload.ToArray().ShouldBe(fullPayload); } // Ref: TestFileStoreBasicWriteMsgsAndRestore — stores 100 msgs, disposes // the store, recreates from the same directory, verifies message count // is preserved, stores 100 more, verifies total of 200. [Fact] public async Task Stop_and_restart_preserves_messages() { const int firstBatch = 100; const int secondBatch = 100; await using (var store = CreateStore()) { for (var i = 1; i <= firstBatch; i++) { var payload = System.Text.Encoding.UTF8.GetBytes($"[{i:D8}] Hello World!"); var seq = await store.AppendAsync("foo", payload, default); seq.ShouldBe((ulong)i); } var state = await store.GetStateAsync(default); state.Messages.ShouldBe((ulong)firstBatch); } // Reopen the same directory. await using (var store = CreateStore()) { var state = await store.GetStateAsync(default); state.Messages.ShouldBe((ulong)firstBatch); for (var i = firstBatch + 1; i <= firstBatch + secondBatch; i++) { var payload = System.Text.Encoding.UTF8.GetBytes($"[{i:D8}] Hello World!"); var seq = await store.AppendAsync("foo", payload, default); seq.ShouldBe((ulong)i); } state = await store.GetStateAsync(default); state.Messages.ShouldBe((ulong)(firstBatch + secondBatch)); } // Reopen again to confirm the second batch survived. await using (var store = CreateStore()) { var state = await store.GetStateAsync(default); state.Messages.ShouldBe((ulong)(firstBatch + secondBatch)); } } // Ref: TestFileStoreBasics (remove section) and Go TestFileStoreRemove // pattern — stores 5 msgs, removes first, last, and a middle message, // verifies State().Msgs decrements correctly after each removal. [Fact] public async Task Remove_messages_updates_state() { await using var store = CreateStore(); const string subject = "foo"; var payload = "Hello World"u8.ToArray(); for (var i = 0; i < 5; i++) await store.AppendAsync(subject, payload, default); // Remove first (seq 1) — expect 4 remaining. (await store.RemoveAsync(1, default)).ShouldBeTrue(); (await store.GetStateAsync(default)).Messages.ShouldBe((ulong)4); // Remove last (seq 5) — expect 3 remaining. (await store.RemoveAsync(5, default)).ShouldBeTrue(); (await store.GetStateAsync(default)).Messages.ShouldBe((ulong)3); // Remove a middle message (seq 3) — expect 2 remaining. (await store.RemoveAsync(3, default)).ShouldBeTrue(); (await store.GetStateAsync(default)).Messages.ShouldBe((ulong)2); // Sequences 2 and 4 should still be loadable. (await store.LoadAsync(2, default)).ShouldNotBeNull(); (await store.LoadAsync(4, default)).ShouldNotBeNull(); // Removed sequences must return null. (await store.LoadAsync(1, default)).ShouldBeNull(); (await store.LoadAsync(3, default)).ShouldBeNull(); (await store.LoadAsync(5, default)).ShouldBeNull(); } }