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.
This commit is contained in:
Joseph Doherty
2026-03-12 15:58:10 -04:00
parent 36b9dfa654
commit 78b4bc2486
228 changed files with 253 additions and 227 deletions

View File

@@ -0,0 +1,344 @@
using System.Threading.Channels;
using NATS.Server.JetStream.Cluster;
using NATS.Server.Raft;
namespace NATS.Server.JetStream.Tests.JetStream.Cluster;
/// <summary>
/// Tests for JetStreamClusterMonitor — background meta entry processing.
/// Go reference: jetstream_cluster.go:1455-1825 (monitorCluster).
/// </summary>
public class JetStreamClusterMonitorTests
{
// Each test uses a 5-second CancellationToken as a hard upper bound so a
// hung monitor doesn't stall the test run indefinitely.
private static CancellationTokenSource TestTimeout() =>
new(TimeSpan.FromSeconds(5));
[Fact]
public async Task Monitor_processes_stream_assignment_entry()
{
// Go reference: jetstream_cluster.go monitorCluster assignStream op
var meta = new JetStreamMetaGroup(3);
var channel = Channel.CreateUnbounded<RaftLogEntry>();
var monitor = new JetStreamClusterMonitor(meta, channel.Reader);
using var cts = TestTimeout();
var monitorTask = monitor.StartAsync(cts.Token);
var assignJson = System.Text.Json.JsonSerializer.Serialize(new
{
Op = "assignStream",
StreamName = "test-stream",
Peers = new[] { "n1", "n2", "n3" },
Config = """{"subjects":["test.>"]}""",
});
await channel.Writer.WriteAsync(new RaftLogEntry(1, 1, assignJson));
await monitor.WaitForProcessedAsync(1, cts.Token);
meta.StreamCount.ShouldBe(1);
meta.GetStreamAssignment("test-stream").ShouldNotBeNull();
cts.Cancel();
await monitorTask;
}
[Fact]
public async Task Monitor_processes_consumer_assignment_entry()
{
// Go reference: jetstream_cluster.go monitorCluster assignConsumer op
var meta = new JetStreamMetaGroup(3);
var channel = Channel.CreateUnbounded<RaftLogEntry>();
var monitor = new JetStreamClusterMonitor(meta, channel.Reader);
using var cts = TestTimeout();
var monitorTask = monitor.StartAsync(cts.Token);
var streamJson = System.Text.Json.JsonSerializer.Serialize(new
{
Op = "assignStream",
StreamName = "s1",
Peers = new[] { "n1", "n2", "n3" },
Config = """{"subjects":["x.>"]}""",
});
await channel.Writer.WriteAsync(new RaftLogEntry(1, 1, streamJson));
var consumerJson = System.Text.Json.JsonSerializer.Serialize(new
{
Op = "assignConsumer",
StreamName = "s1",
ConsumerName = "c1",
Peers = new[] { "n1", "n2", "n3" },
});
await channel.Writer.WriteAsync(new RaftLogEntry(2, 1, consumerJson));
await monitor.WaitForProcessedAsync(2, cts.Token);
meta.ConsumerCount.ShouldBe(1);
cts.Cancel();
await monitorTask;
}
[Fact]
public async Task Monitor_processes_stream_removal()
{
// Go reference: jetstream_cluster.go monitorCluster removeStream op
var meta = new JetStreamMetaGroup(3);
var channel = Channel.CreateUnbounded<RaftLogEntry>();
var monitor = new JetStreamClusterMonitor(meta, channel.Reader);
using var cts = TestTimeout();
var monitorTask = monitor.StartAsync(cts.Token);
var assignJson = System.Text.Json.JsonSerializer.Serialize(new
{
Op = "assignStream",
StreamName = "to-remove",
Peers = new[] { "n1", "n2", "n3" },
Config = """{"subjects":["rm.>"]}""",
});
await channel.Writer.WriteAsync(new RaftLogEntry(1, 1, assignJson));
var removeJson = System.Text.Json.JsonSerializer.Serialize(new
{
Op = "removeStream",
StreamName = "to-remove",
});
await channel.Writer.WriteAsync(new RaftLogEntry(2, 1, removeJson));
await monitor.WaitForProcessedAsync(2, cts.Token);
meta.StreamCount.ShouldBe(0);
cts.Cancel();
await monitorTask;
}
[Fact]
public async Task Monitor_applies_meta_snapshot()
{
// Go reference: jetstream_cluster.go monitorCluster snapshot op — replaces all state
var meta = new JetStreamMetaGroup(3);
var channel = Channel.CreateUnbounded<RaftLogEntry>();
var monitor = new JetStreamClusterMonitor(meta, channel.Reader);
using var cts = TestTimeout();
var monitorTask = monitor.StartAsync(cts.Token);
var assignments = new Dictionary<string, StreamAssignment>
{
["snap-stream"] = new StreamAssignment
{
StreamName = "snap-stream",
Group = new RaftGroup { Name = "rg-snap", Peers = ["n1", "n2", "n3"] },
},
};
var snapshotB64 = Convert.ToBase64String(MetaSnapshotCodec.Encode(assignments));
var snapshotJson = System.Text.Json.JsonSerializer.Serialize(new
{
Op = "snapshot",
Data = snapshotB64,
});
await channel.Writer.WriteAsync(new RaftLogEntry(1, 1, snapshotJson));
await monitor.WaitForProcessedAsync(1, cts.Token);
meta.StreamCount.ShouldBe(1);
meta.GetStreamAssignment("snap-stream").ShouldNotBeNull();
cts.Cancel();
await monitorTask;
}
[Fact]
public async Task Monitor_processes_consumer_removal()
{
// Go reference: jetstream_cluster.go monitorCluster removeConsumer op
var meta = new JetStreamMetaGroup(3);
var channel = Channel.CreateUnbounded<RaftLogEntry>();
var monitor = new JetStreamClusterMonitor(meta, channel.Reader);
using var cts = TestTimeout();
var monitorTask = monitor.StartAsync(cts.Token);
var streamJson = System.Text.Json.JsonSerializer.Serialize(new
{
Op = "assignStream",
StreamName = "s1",
Peers = new[] { "n1", "n2", "n3" },
});
await channel.Writer.WriteAsync(new RaftLogEntry(1, 1, streamJson));
var consumerJson = System.Text.Json.JsonSerializer.Serialize(new
{
Op = "assignConsumer",
StreamName = "s1",
ConsumerName = "c1",
Peers = new[] { "n1", "n2", "n3" },
});
await channel.Writer.WriteAsync(new RaftLogEntry(2, 1, consumerJson));
await monitor.WaitForProcessedAsync(2, cts.Token);
meta.ConsumerCount.ShouldBe(1);
var removeJson = System.Text.Json.JsonSerializer.Serialize(new
{
Op = "removeConsumer",
StreamName = "s1",
ConsumerName = "c1",
});
await channel.Writer.WriteAsync(new RaftLogEntry(3, 1, removeJson));
await monitor.WaitForProcessedAsync(3, cts.Token);
meta.ConsumerCount.ShouldBe(0);
cts.Cancel();
await monitorTask;
}
[Fact]
public async Task Monitor_skips_malformed_entries()
{
// Go reference: jetstream_cluster.go monitorCluster — malformed entries must not abort the loop
var meta = new JetStreamMetaGroup(3);
var channel = Channel.CreateUnbounded<RaftLogEntry>();
var monitor = new JetStreamClusterMonitor(meta, channel.Reader);
using var cts = TestTimeout();
var monitorTask = monitor.StartAsync(cts.Token);
await channel.Writer.WriteAsync(new RaftLogEntry(1, 1, "not-json"));
var assignJson = System.Text.Json.JsonSerializer.Serialize(new
{
Op = "assignStream",
StreamName = "after-bad",
Peers = new[] { "n1", "n2", "n3" },
});
await channel.Writer.WriteAsync(new RaftLogEntry(2, 1, assignJson));
await monitor.WaitForProcessedAsync(2, cts.Token);
meta.StreamCount.ShouldBe(1);
meta.GetStreamAssignment("after-bad").ShouldNotBeNull();
cts.Cancel();
await monitorTask;
}
[Fact]
public async Task Monitor_stops_on_cancellation()
{
// Go reference: jetstream_cluster.go monitorCluster shuts down cleanly when stop channel closes
var meta = new JetStreamMetaGroup(3);
var channel = Channel.CreateUnbounded<RaftLogEntry>();
var monitor = new JetStreamClusterMonitor(meta, channel.Reader);
using var cts = TestTimeout();
var monitorTask = monitor.StartAsync(cts.Token);
cts.Cancel();
await monitorTask; // Should complete without throwing
}
[Fact]
public async Task Monitor_ignores_entry_with_no_op_field()
{
// Entries missing the "Op" property are silently ignored (forward-compat).
var meta = new JetStreamMetaGroup(3);
var channel = Channel.CreateUnbounded<RaftLogEntry>();
var monitor = new JetStreamClusterMonitor(meta, channel.Reader);
using var cts = TestTimeout();
var monitorTask = monitor.StartAsync(cts.Token);
await channel.Writer.WriteAsync(new RaftLogEntry(1, 1, """{"NotOp":"whatever"}"""));
var assignJson = System.Text.Json.JsonSerializer.Serialize(new
{
Op = "assignStream",
StreamName = "after-no-op",
Peers = new[] { "n1" },
});
await channel.Writer.WriteAsync(new RaftLogEntry(2, 1, assignJson));
await monitor.WaitForProcessedAsync(2, cts.Token);
meta.StreamCount.ShouldBe(1);
cts.Cancel();
await monitorTask;
}
[Fact]
public async Task Monitor_ignores_unknown_op()
{
// Unknown op names are silently ignored — forward compatibility.
var meta = new JetStreamMetaGroup(3);
var channel = Channel.CreateUnbounded<RaftLogEntry>();
var monitor = new JetStreamClusterMonitor(meta, channel.Reader);
using var cts = TestTimeout();
var monitorTask = monitor.StartAsync(cts.Token);
await channel.Writer.WriteAsync(new RaftLogEntry(1, 1, """{"Op":"futureFoo","Data":"xyz"}"""));
var assignJson = System.Text.Json.JsonSerializer.Serialize(new
{
Op = "assignStream",
StreamName = "after-unknown-op",
Peers = new[] { "n1" },
});
await channel.Writer.WriteAsync(new RaftLogEntry(2, 1, assignJson));
await monitor.WaitForProcessedAsync(2, cts.Token);
meta.StreamCount.ShouldBe(1);
cts.Cancel();
await monitorTask;
}
[Fact]
public async Task Monitor_snapshot_replaces_existing_state()
{
// Go reference: jetstream_cluster.go — snapshot apply wipes old assignments
var meta = new JetStreamMetaGroup(3);
var channel = Channel.CreateUnbounded<RaftLogEntry>();
var monitor = new JetStreamClusterMonitor(meta, channel.Reader);
using var cts = TestTimeout();
var monitorTask = monitor.StartAsync(cts.Token);
var assignJson = System.Text.Json.JsonSerializer.Serialize(new
{
Op = "assignStream",
StreamName = "old-stream",
Peers = new[] { "n1" },
});
await channel.Writer.WriteAsync(new RaftLogEntry(1, 1, assignJson));
await monitor.WaitForProcessedAsync(1, cts.Token);
meta.StreamCount.ShouldBe(1);
var newAssignments = new Dictionary<string, StreamAssignment>
{
["new-stream"] = new StreamAssignment
{
StreamName = "new-stream",
Group = new RaftGroup { Name = "rg-new", Peers = ["n1", "n2", "n3"] },
},
};
var snapshotB64 = Convert.ToBase64String(MetaSnapshotCodec.Encode(newAssignments));
var snapshotJson = System.Text.Json.JsonSerializer.Serialize(new
{
Op = "snapshot",
Data = snapshotB64,
});
await channel.Writer.WriteAsync(new RaftLogEntry(2, 1, snapshotJson));
await monitor.WaitForProcessedAsync(2, cts.Token);
meta.StreamCount.ShouldBe(1);
meta.GetStreamAssignment("old-stream").ShouldBeNull();
meta.GetStreamAssignment("new-stream").ShouldNotBeNull();
cts.Cancel();
await monitorTask;
}
}