// Reference: golang/nats-server/server/filestore_test.go // Tests ported from: TestFileStoreNoFSSWhenNoSubjects, // TestFileStoreNoFSSBugAfterRemoveFirst, // TestFileStoreNoFSSAfterRecover, // TestFileStoreSubjectStateCacheExpiration, // TestFileStoreSubjectsTotals, // TestFileStoreSubjectCorruption, // TestFileStoreFilteredPendingBug, // TestFileStoreFilteredFirstMatchingBug, // TestFileStoreExpireSubjectMeta, // TestFileStoreAllFilteredStateWithDeleted using System.Text; using NATS.Server.JetStream.Storage; namespace NATS.Server.JetStream.Tests.JetStream.Storage; public sealed class FileStoreSubjectTests : IDisposable { private readonly string _dir; public FileStoreSubjectTests() { _dir = Path.Combine(Path.GetTempPath(), $"nats-js-fs-subject-{Guid.NewGuid():N}"); Directory.CreateDirectory(_dir); } public void Dispose() { if (Directory.Exists(_dir)) Directory.Delete(_dir, recursive: true); } private FileStore CreateStore(string subdirectory, FileStoreOptions? options = null) { var dir = Path.Combine(_dir, subdirectory); var opts = options ?? new FileStoreOptions(); opts.Directory = dir; return new FileStore(opts); } // Go: TestFileStoreNoFSSWhenNoSubjects server/filestore_test.go:4251 [Fact] public async Task Store_with_empty_subject() { await using var store = CreateStore("empty-subj"); // Store messages with empty subject (like raft state). for (var i = 0; i < 10; i++) await store.AppendAsync(string.Empty, "raft state"u8.ToArray(), default); var state = await store.GetStateAsync(default); state.Messages.ShouldBe((ulong)10); // Should be loadable. var msg = await store.LoadAsync(1, default); msg.ShouldNotBeNull(); msg!.Subject.ShouldBe(string.Empty); } // Go: TestFileStoreNoFSSBugAfterRemoveFirst server/filestore_test.go:4289 [Fact] public async Task Remove_first_with_different_subjects() { await using var store = CreateStore("rm-first-subj"); await store.AppendAsync("foo", "first"u8.ToArray(), default); await store.AppendAsync("bar", "second"u8.ToArray(), default); await store.AppendAsync("foo", "third"u8.ToArray(), default); // Remove first message. (await store.RemoveAsync(1, default)).ShouldBeTrue(); var state = await store.GetStateAsync(default); state.Messages.ShouldBe((ulong)2); state.FirstSeq.ShouldBe((ulong)2); // LoadLastBySubject should still work for "foo". var lastFoo = await store.LoadLastBySubjectAsync("foo", default); lastFoo.ShouldNotBeNull(); lastFoo!.Sequence.ShouldBe((ulong)3); } // Go: TestFileStoreNoFSSAfterRecover server/filestore_test.go:4333 [Fact] public async Task Subject_filtering_after_recovery() { var subDir = "subj-after-recover"; await using (var store = CreateStore(subDir)) { await store.AppendAsync("foo.1", "a"u8.ToArray(), default); await store.AppendAsync("foo.2", "b"u8.ToArray(), default); await store.AppendAsync("bar.1", "c"u8.ToArray(), default); await store.AppendAsync("foo.1", "d"u8.ToArray(), default); } // Recover. await using (var store = CreateStore(subDir)) { var state = await store.GetStateAsync(default); state.Messages.ShouldBe((ulong)4); // LoadLastBySubject should work after recovery. var lastFoo1 = await store.LoadLastBySubjectAsync("foo.1", default); lastFoo1.ShouldNotBeNull(); lastFoo1!.Sequence.ShouldBe((ulong)4); lastFoo1.Payload.ToArray().ShouldBe("d"u8.ToArray()); var lastBar1 = await store.LoadLastBySubjectAsync("bar.1", default); lastBar1.ShouldNotBeNull(); lastBar1!.Sequence.ShouldBe((ulong)3); } } // Go: TestFileStoreSubjectStateCacheExpiration server/filestore_test.go:4143 [Fact(Skip = "SubjectsState not yet implemented in .NET FileStore")] public async Task Subject_state_cache_expiration() { await Task.CompletedTask; } // Go: TestFileStoreSubjectsTotals server/filestore_test.go:4948 [Fact(Skip = "SubjectsTotals not yet implemented in .NET FileStore")] public async Task Subjects_totals_with_wildcards() { await Task.CompletedTask; } // Go: TestFileStoreSubjectCorruption server/filestore_test.go:6466 [Fact(Skip = "SubjectForSeq not yet implemented in .NET FileStore")] public async Task Subject_corruption_detection() { await Task.CompletedTask; } // Go: TestFileStoreFilteredPendingBug server/filestore_test.go:3414 [Fact(Skip = "FilteredState not yet implemented in .NET FileStore")] public async Task Filtered_pending_no_match_returns_zero() { await Task.CompletedTask; } // Go: TestFileStoreFilteredFirstMatchingBug server/filestore_test.go:4448 [Fact(Skip = "LoadNextMsg not yet implemented in .NET FileStore")] public async Task Filtered_first_matching_finds_correct_sequence() { await Task.CompletedTask; } // Go: TestFileStoreExpireSubjectMeta server/filestore_test.go:4014 [Fact(Skip = "SubjectsState not yet implemented in .NET FileStore")] public async Task Expired_subject_metadata_cleans_up() { await Task.CompletedTask; } // Go: TestFileStoreAllFilteredStateWithDeleted server/filestore_test.go:4827 [Fact(Skip = "FilteredState not yet implemented in .NET FileStore")] public async Task Filtered_state_with_deleted_messages() { await Task.CompletedTask; } // Test LoadLastBySubject with multiple subjects and removes. [Fact] public async Task LoadLastBySubject_after_removes() { await using var store = CreateStore("last-after-rm"); await store.AppendAsync("foo", "a"u8.ToArray(), default); await store.AppendAsync("foo", "b"u8.ToArray(), default); await store.AppendAsync("foo", "c"u8.ToArray(), default); // Remove the last message on "foo" (seq 3). await store.RemoveAsync(3, default); var last = await store.LoadLastBySubjectAsync("foo", default); last.ShouldNotBeNull(); last!.Sequence.ShouldBe((ulong)2); last.Payload.ToArray().ShouldBe("b"u8.ToArray()); } // Test LoadLastBySubject when all messages on that subject are removed. [Fact] public async Task LoadLastBySubject_all_removed_returns_null() { await using var store = CreateStore("last-all-rm"); await store.AppendAsync("foo", "a"u8.ToArray(), default); await store.AppendAsync("foo", "b"u8.ToArray(), default); await store.AppendAsync("bar", "c"u8.ToArray(), default); await store.RemoveAsync(1, default); await store.RemoveAsync(2, default); var last = await store.LoadLastBySubjectAsync("foo", default); last.ShouldBeNull(); // "bar" should still be present. var lastBar = await store.LoadLastBySubjectAsync("bar", default); lastBar.ShouldNotBeNull(); lastBar!.Sequence.ShouldBe((ulong)3); } // Test multiple subjects interleaved. [Fact] public async Task Multiple_subjects_interleaved() { await using var store = CreateStore("interleaved"); for (var i = 0; i < 20; i++) { var subject = i % 3 == 0 ? "alpha" : (i % 3 == 1 ? "beta" : "gamma"); await store.AppendAsync(subject, Encoding.UTF8.GetBytes($"msg-{i}"), default); } var state = await store.GetStateAsync(default); state.Messages.ShouldBe((ulong)20); // Verify all subjects are loadable and correct. for (ulong i = 1; i <= 20; i++) { var msg = await store.LoadAsync(i, default); msg.ShouldNotBeNull(); var idx = (int)(i - 1); var expectedSubj = idx % 3 == 0 ? "alpha" : (idx % 3 == 1 ? "beta" : "gamma"); msg!.Subject.ShouldBe(expectedSubj); } } // Test LoadLastBySubject with case-sensitive subjects. [Fact] public async Task LoadLastBySubject_is_case_sensitive() { await using var store = CreateStore("case-sensitive"); await store.AppendAsync("Foo", "upper"u8.ToArray(), default); await store.AppendAsync("foo", "lower"u8.ToArray(), default); var lastUpper = await store.LoadLastBySubjectAsync("Foo", default); lastUpper.ShouldNotBeNull(); lastUpper!.Payload.ToArray().ShouldBe("upper"u8.ToArray()); var lastLower = await store.LoadLastBySubjectAsync("foo", default); lastLower.ShouldNotBeNull(); lastLower!.Payload.ToArray().ShouldBe("lower"u8.ToArray()); } // Test subject preservation across restarts. [Fact] public async Task Subject_preserved_across_restart() { var subDir = "subj-restart"; await using (var store = CreateStore(subDir)) { await store.AppendAsync("topic.a", "one"u8.ToArray(), default); await store.AppendAsync("topic.b", "two"u8.ToArray(), default); await store.AppendAsync("topic.c", "three"u8.ToArray(), default); } await using (var store = CreateStore(subDir)) { var msg1 = await store.LoadAsync(1, default); msg1.ShouldNotBeNull(); msg1!.Subject.ShouldBe("topic.a"); var msg2 = await store.LoadAsync(2, default); msg2.ShouldNotBeNull(); msg2!.Subject.ShouldBe("topic.b"); var msg3 = await store.LoadAsync(3, default); msg3.ShouldNotBeNull(); msg3!.Subject.ShouldBe("topic.c"); } } // Go: TestFileStoreNumPendingLastBySubject server/filestore_test.go:6501 [Fact(Skip = "NumPending not yet implemented in .NET FileStore")] public async Task NumPending_last_per_subject() { await Task.CompletedTask; } // Test many distinct subjects. [Fact] public async Task Many_distinct_subjects() { await using var store = CreateStore("many-subjects"); for (var i = 0; i < 100; i++) await store.AppendAsync($"kv.{i}", Encoding.UTF8.GetBytes($"value-{i}"), default); var state = await store.GetStateAsync(default); state.Messages.ShouldBe((ulong)100); // Each subject should have exactly one message. for (var i = 0; i < 100; i++) { var last = await store.LoadLastBySubjectAsync($"kv.{i}", default); last.ShouldNotBeNull(); last!.Payload.ToArray().ShouldBe(Encoding.UTF8.GetBytes($"value-{i}")); } } }