Files
natsdotnet/tests/NATS.Server.JetStream.Tests/JetStream/JetStreamServiceOrchestrationTests.cs
Joseph Doherty 78b4bc2486 refactor: extract NATS.Server.JetStream.Tests project
Move 225 JetStream-related test files from NATS.Server.Tests into a
dedicated NATS.Server.JetStream.Tests project. This includes root-level
JetStream*.cs files, storage test files (FileStore, MemStore,
StreamStoreContract), and the full JetStream/ subfolder tree (Api,
Cluster, Consumers, MirrorSource, Snapshots, Storage, Streams).

Updated all namespaces, added InternalsVisibleTo, registered in the
solution file, and added the JETSTREAM_INTEGRATION_MATRIX define.
2026-03-12 15:58:10 -04:00

243 lines
9.1 KiB
C#

// Ported from golang/nats-server/server/jetstream.go:414-523 (enableJetStream)
// Tests for JetStreamService lifecycle orchestration: store directory creation,
// API subject registration, configuration property exposure, and dispose semantics.
using NATS.Server.Configuration;
using NATS.Server.JetStream;
namespace NATS.Server.JetStream.Tests.JetStream;
public sealed class JetStreamServiceOrchestrationTests : IDisposable
{
private readonly List<string> _tempDirs = [];
private string MakeTempDir()
{
var path = Path.Combine(Path.GetTempPath(), "nats-js-test-" + Guid.NewGuid().ToString("N"));
_tempDirs.Add(path);
return path;
}
public void Dispose()
{
foreach (var dir in _tempDirs)
{
if (Directory.Exists(dir))
Directory.Delete(dir, recursive: true);
}
}
// Go: enableJetStream — jetstream.go:414 — happy path creates store dir and marks running
[Fact]
public async Task StartAsync_creates_store_directory_and_marks_running()
{
var storeDir = MakeTempDir();
var options = new JetStreamOptions { StoreDir = storeDir };
await using var svc = new JetStreamService(options);
Directory.Exists(storeDir).ShouldBeFalse("directory must not exist before start");
await svc.StartAsync(CancellationToken.None);
svc.IsRunning.ShouldBeTrue();
Directory.Exists(storeDir).ShouldBeTrue("StartAsync must create the store directory");
}
// Go: enableJetStream — jetstream.go:430 — existing dir is accepted without error
[Fact]
public async Task StartAsync_accepts_preexisting_store_directory()
{
var storeDir = MakeTempDir();
Directory.CreateDirectory(storeDir);
var options = new JetStreamOptions { StoreDir = storeDir };
await using var svc = new JetStreamService(options);
await svc.StartAsync(CancellationToken.None);
svc.IsRunning.ShouldBeTrue();
Directory.Exists(storeDir).ShouldBeTrue();
}
// Go: enableJetStream — memory-only mode when StoreDir is empty
[Fact]
public async Task StartAsync_with_empty_StoreDir_starts_in_memory_only_mode()
{
var options = new JetStreamOptions { StoreDir = string.Empty };
await using var svc = new JetStreamService(options);
await svc.StartAsync(CancellationToken.None);
svc.IsRunning.ShouldBeTrue();
}
// Go: setJetStreamExportSubs — jetstream.go:489 — all $JS.API subjects registered
[Fact]
public async Task RegisteredApiSubjects_contains_expected_subjects_after_start()
{
var options = new JetStreamOptions();
await using var svc = new JetStreamService(options);
await svc.StartAsync(CancellationToken.None);
var subjects = svc.RegisteredApiSubjects;
subjects.ShouldNotBeEmpty();
subjects.ShouldContain("$JS.API.>");
subjects.ShouldContain("$JS.API.INFO");
subjects.ShouldContain("$JS.API.META.LEADER.STEPDOWN");
subjects.ShouldContain("$JS.API.STREAM.NAMES");
subjects.ShouldContain("$JS.API.STREAM.LIST");
}
// Go: setJetStreamExportSubs — all consumer-related wildcards registered
[Fact]
public async Task RegisteredApiSubjects_includes_consumer_and_stream_wildcard_subjects()
{
var options = new JetStreamOptions();
await using var svc = new JetStreamService(options);
await svc.StartAsync(CancellationToken.None);
var subjects = svc.RegisteredApiSubjects;
// Stream management
subjects.ShouldContain(s => s.StartsWith("$JS.API.STREAM.CREATE."), "stream create wildcard");
subjects.ShouldContain(s => s.StartsWith("$JS.API.STREAM.DELETE."), "stream delete wildcard");
subjects.ShouldContain(s => s.StartsWith("$JS.API.STREAM.INFO."), "stream info wildcard");
subjects.ShouldContain(s => s.StartsWith("$JS.API.STREAM.UPDATE."), "stream update wildcard");
subjects.ShouldContain(s => s.StartsWith("$JS.API.STREAM.PURGE."), "stream purge wildcard");
subjects.ShouldContain(s => s.StartsWith("$JS.API.STREAM.MSG.GET."), "stream msg get wildcard");
subjects.ShouldContain(s => s.StartsWith("$JS.API.STREAM.MSG.DELETE."), "stream msg delete wildcard");
subjects.ShouldContain(s => s.StartsWith("$JS.API.STREAM.SNAPSHOT."), "stream snapshot wildcard");
subjects.ShouldContain(s => s.StartsWith("$JS.API.STREAM.RESTORE."), "stream restore wildcard");
subjects.ShouldContain(s => s.StartsWith("$JS.API.STREAM.LEADER.STEPDOWN."), "stream leader stepdown wildcard");
// Consumer management
subjects.ShouldContain(s => s.StartsWith("$JS.API.CONSUMER.CREATE."), "consumer create wildcard");
subjects.ShouldContain(s => s.StartsWith("$JS.API.CONSUMER.DELETE."), "consumer delete wildcard");
subjects.ShouldContain(s => s.StartsWith("$JS.API.CONSUMER.INFO."), "consumer info wildcard");
subjects.ShouldContain(s => s.StartsWith("$JS.API.CONSUMER.NAMES."), "consumer names wildcard");
subjects.ShouldContain(s => s.StartsWith("$JS.API.CONSUMER.LIST."), "consumer list wildcard");
subjects.ShouldContain(s => s.StartsWith("$JS.API.CONSUMER.PAUSE."), "consumer pause wildcard");
subjects.ShouldContain(s => s.StartsWith("$JS.API.CONSUMER.MSG.NEXT."), "consumer msg next wildcard");
// Direct get
subjects.ShouldContain(s => s.StartsWith("$JS.API.DIRECT.GET."), "direct get wildcard");
}
// RegisteredApiSubjects should be empty before start
[Fact]
public void RegisteredApiSubjects_is_empty_before_start()
{
var options = new JetStreamOptions();
var svc = new JetStreamService(options);
svc.RegisteredApiSubjects.ShouldBeEmpty();
}
// Go: shutdown path — DisposeAsync clears subjects and marks not running
[Fact]
public async Task DisposeAsync_clears_subjects_and_marks_not_running()
{
var options = new JetStreamOptions();
var svc = new JetStreamService(options);
await svc.StartAsync(CancellationToken.None);
svc.IsRunning.ShouldBeTrue();
svc.RegisteredApiSubjects.ShouldNotBeEmpty();
await svc.DisposeAsync();
svc.IsRunning.ShouldBeFalse();
svc.RegisteredApiSubjects.ShouldBeEmpty();
}
// MaxStreams and MaxConsumers reflect config values
[Fact]
public async Task MaxStreams_and_MaxConsumers_reflect_config_values()
{
var options = new JetStreamOptions
{
MaxStreams = 100,
MaxConsumers = 500,
};
await using var svc = new JetStreamService(options);
await svc.StartAsync(CancellationToken.None);
svc.MaxStreams.ShouldBe(100);
svc.MaxConsumers.ShouldBe(500);
}
// MaxMemory and MaxStore reflect config values
[Fact]
public async Task MaxMemory_and_MaxStore_reflect_config_values()
{
var options = new JetStreamOptions
{
MaxMemoryStore = 1_073_741_824L, // 1 GiB
MaxFileStore = 10_737_418_240L, // 10 GiB
};
await using var svc = new JetStreamService(options);
await svc.StartAsync(CancellationToken.None);
svc.MaxMemory.ShouldBe(1_073_741_824L);
svc.MaxStore.ShouldBe(10_737_418_240L);
}
// Default config values are zero (unlimited)
[Fact]
public void Default_config_values_are_unlimited_zero()
{
var options = new JetStreamOptions();
var svc = new JetStreamService(options);
svc.MaxStreams.ShouldBe(0);
svc.MaxConsumers.ShouldBe(0);
svc.MaxMemory.ShouldBe(0L);
svc.MaxStore.ShouldBe(0L);
}
// Go: enableJetStream idempotency — double-start is safe (not an error)
[Fact]
public async Task Double_start_is_idempotent()
{
var options = new JetStreamOptions();
await using var svc = new JetStreamService(options);
await svc.StartAsync(CancellationToken.None);
var subjectCountAfterFirst = svc.RegisteredApiSubjects.Count;
// Second start must not throw and must not duplicate subjects
await svc.StartAsync(CancellationToken.None);
svc.IsRunning.ShouldBeTrue();
svc.RegisteredApiSubjects.Count.ShouldBe(subjectCountAfterFirst);
}
// Store directory is created with a nested path (MkdirAll semantics)
[Fact]
public async Task StartAsync_creates_nested_store_directory()
{
var baseDir = MakeTempDir();
var nestedDir = Path.Combine(baseDir, "level1", "level2", "jetstream");
var options = new JetStreamOptions { StoreDir = nestedDir };
await using var svc = new JetStreamService(options);
await svc.StartAsync(CancellationToken.None);
svc.IsRunning.ShouldBeTrue();
Directory.Exists(nestedDir).ShouldBeTrue("nested store directory must be created");
}
// Service is not running before start
[Fact]
public void IsRunning_is_false_before_start()
{
var options = new JetStreamOptions();
var svc = new JetStreamService(options);
svc.IsRunning.ShouldBeFalse();
}
}