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,196 @@
// Go parity: golang/nats-server/server/jetstream_cluster.go
// Covers: Per-stream RAFT group message proposals, message count tracking,
// sequence tracking, leader change events, replica status reporting,
// and non-leader rejection.
using NATS.Server.JetStream.Cluster;
namespace NATS.Server.JetStream.Tests.JetStream.Cluster;
/// <summary>
/// Tests for StreamReplicaGroup stream-specific RAFT apply logic:
/// message proposals, message count, last sequence, leader change
/// event, and replica status reporting.
/// Go reference: jetstream_cluster.go processStreamMsg, processStreamEntries.
/// </summary>
public class StreamRaftGroupTests
{
// ---------------------------------------------------------------
// ProposeMessageAsync succeeds as leader
// Go reference: jetstream_cluster.go processStreamMsg
// ---------------------------------------------------------------
[Fact]
public async Task Propose_message_succeeds_as_leader()
{
var group = new StreamReplicaGroup("MSGS", replicas: 3);
var index = await group.ProposeMessageAsync(
"orders.new", ReadOnlyMemory<byte>.Empty, "hello"u8.ToArray(), default);
index.ShouldBeGreaterThan(0);
}
// ---------------------------------------------------------------
// ProposeMessageAsync fails when not leader
// Go reference: jetstream_cluster.go leader check
// ---------------------------------------------------------------
[Fact]
public async Task Propose_message_fails_when_not_leader()
{
var group = new StreamReplicaGroup("NOLEAD", replicas: 3);
// Step down so the current leader is no longer leader
group.Leader.RequestStepDown();
await Should.ThrowAsync<InvalidOperationException>(async () =>
await group.ProposeMessageAsync(
"test.sub", ReadOnlyMemory<byte>.Empty, "data"u8.ToArray(), default));
}
// ---------------------------------------------------------------
// Message count increments after proposal
// Go reference: stream.go state.Msgs tracking
// ---------------------------------------------------------------
[Fact]
public async Task Message_count_increments_after_proposal()
{
var group = new StreamReplicaGroup("COUNT", replicas: 3);
group.MessageCount.ShouldBe(0);
await group.ProposeMessageAsync("a.1", ReadOnlyMemory<byte>.Empty, "m1"u8.ToArray(), default);
group.MessageCount.ShouldBe(1);
await group.ProposeMessageAsync("a.2", ReadOnlyMemory<byte>.Empty, "m2"u8.ToArray(), default);
group.MessageCount.ShouldBe(2);
await group.ProposeMessageAsync("a.3", ReadOnlyMemory<byte>.Empty, "m3"u8.ToArray(), default);
group.MessageCount.ShouldBe(3);
}
// ---------------------------------------------------------------
// Last sequence tracks correctly
// Go reference: stream.go state.LastSeq
// ---------------------------------------------------------------
[Fact]
public async Task Last_sequence_tracks_correctly()
{
var group = new StreamReplicaGroup("SEQ", replicas: 3);
group.LastSequence.ShouldBe(0);
var idx1 = await group.ProposeMessageAsync("s.1", ReadOnlyMemory<byte>.Empty, "d1"u8.ToArray(), default);
group.LastSequence.ShouldBe(idx1);
var idx2 = await group.ProposeMessageAsync("s.2", ReadOnlyMemory<byte>.Empty, "d2"u8.ToArray(), default);
group.LastSequence.ShouldBe(idx2);
idx2.ShouldBeGreaterThan(idx1);
}
// ---------------------------------------------------------------
// Step down triggers leader change event
// Go reference: jetstream_cluster.go leader change notification
// ---------------------------------------------------------------
[Fact]
public async Task Step_down_triggers_leader_change_event()
{
var group = new StreamReplicaGroup("EVENT", replicas: 3);
var previousId = group.Leader.Id;
LeaderChangedEventArgs? receivedArgs = null;
group.LeaderChanged += (_, args) => receivedArgs = args;
await group.StepDownAsync(default);
receivedArgs.ShouldNotBeNull();
receivedArgs.PreviousLeaderId.ShouldBe(previousId);
receivedArgs.NewLeaderId.ShouldNotBe(previousId);
receivedArgs.NewTerm.ShouldBeGreaterThan(0);
}
[Fact]
public async Task Multiple_stepdowns_fire_leader_changed_each_time()
{
var group = new StreamReplicaGroup("MULTI_EVENT", replicas: 3);
var eventCount = 0;
group.LeaderChanged += (_, _) => eventCount++;
await group.StepDownAsync(default);
await group.StepDownAsync(default);
await group.StepDownAsync(default);
eventCount.ShouldBe(3);
}
// ---------------------------------------------------------------
// Replica status reports correct state
// Go reference: jetstream_cluster.go stream replica status
// ---------------------------------------------------------------
[Fact]
public async Task Replica_status_reports_correct_state()
{
var group = new StreamReplicaGroup("STATUS", replicas: 3);
await group.ProposeMessageAsync("x.1", ReadOnlyMemory<byte>.Empty, "m1"u8.ToArray(), default);
await group.ProposeMessageAsync("x.2", ReadOnlyMemory<byte>.Empty, "m2"u8.ToArray(), default);
var status = group.GetStatus();
status.StreamName.ShouldBe("STATUS");
status.LeaderId.ShouldBe(group.Leader.Id);
status.LeaderTerm.ShouldBeGreaterThan(0);
status.MessageCount.ShouldBe(2);
status.LastSequence.ShouldBeGreaterThan(0);
status.ReplicaCount.ShouldBe(3);
}
[Fact]
public void Initial_status_has_zero_messages()
{
var group = new StreamReplicaGroup("EMPTY", replicas: 1);
var status = group.GetStatus();
status.MessageCount.ShouldBe(0);
status.LastSequence.ShouldBe(0);
status.ReplicaCount.ShouldBe(1);
}
// ---------------------------------------------------------------
// Status updates after step down
// ---------------------------------------------------------------
[Fact]
public async Task Status_reflects_new_leader_after_stepdown()
{
var group = new StreamReplicaGroup("NEWLEAD", replicas: 3);
var statusBefore = group.GetStatus();
await group.StepDownAsync(default);
var statusAfter = group.GetStatus();
statusAfter.LeaderId.ShouldNotBe(statusBefore.LeaderId);
}
// ---------------------------------------------------------------
// ProposeAsync still works after ProposeMessageAsync
// ---------------------------------------------------------------
[Fact]
public async Task ProposeAsync_and_ProposeMessageAsync_coexist()
{
var group = new StreamReplicaGroup("COEXIST", replicas: 3);
var idx1 = await group.ProposeAsync("PUB test.1", default);
var idx2 = await group.ProposeMessageAsync("test.2", ReadOnlyMemory<byte>.Empty, "data"u8.ToArray(), default);
idx2.ShouldBeGreaterThan(idx1);
group.MessageCount.ShouldBe(1); // Only ProposeMessageAsync increments message count
}
}