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:
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user