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.
416 lines
16 KiB
C#
416 lines
16 KiB
C#
// Go parity: golang/nats-server/server/jetstream_helpers_test.go
|
|
// Smoke tests for JetStreamClusterFixture — verifies that the unified fixture
|
|
// correctly wires up the JetStream cluster simulation and exposes all capabilities
|
|
// expected by Tasks 6-10 (leader election, stream ops, consumer ops, failover, routing).
|
|
using System.Text;
|
|
using NATS.Server.JetStream.Api;
|
|
using NATS.Server.JetStream.Cluster;
|
|
using NATS.Server.JetStream.Models;
|
|
using NATS.Server.TestUtilities;
|
|
|
|
namespace NATS.Server.JetStream.Tests.JetStream.Cluster;
|
|
|
|
/// <summary>
|
|
/// Smoke tests verifying that JetStreamClusterFixture starts correctly and
|
|
/// exposes all capabilities needed by the cluster test suites (Tasks 6-10).
|
|
/// </summary>
|
|
public class JetStreamClusterFixtureTests
|
|
{
|
|
// ---------------------------------------------------------------
|
|
// Fixture creation
|
|
// ---------------------------------------------------------------
|
|
|
|
// Go ref: checkClusterFormed in jetstream_helpers_test.go
|
|
[Fact]
|
|
public async Task Three_node_cluster_starts_and_reports_node_count()
|
|
{
|
|
await using var fx = await JetStreamClusterFixture.StartAsync(nodes: 3);
|
|
fx.NodeCount.ShouldBe(3);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Five_node_cluster_starts_and_reports_node_count()
|
|
{
|
|
await using var fx = await JetStreamClusterFixture.StartAsync(nodes: 5);
|
|
fx.NodeCount.ShouldBe(5);
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Stream operations via fixture
|
|
// ---------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public async Task Create_stream_and_publish_returns_valid_ack()
|
|
{
|
|
await using var fx = await JetStreamClusterFixture.StartAsync(nodes: 3);
|
|
var resp = await fx.CreateStreamAsync("SMOKE", ["smoke.>"], replicas: 3);
|
|
resp.Error.ShouldBeNull();
|
|
resp.StreamInfo.ShouldNotBeNull();
|
|
resp.StreamInfo!.Config.Name.ShouldBe("SMOKE");
|
|
|
|
var ack = await fx.PublishAsync("smoke.test", "hello");
|
|
ack.Stream.ShouldBe("SMOKE");
|
|
ack.Seq.ShouldBe(1UL);
|
|
ack.ErrorCode.ShouldBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Create_multi_replica_stream_and_verify_info()
|
|
{
|
|
await using var fx = await JetStreamClusterFixture.StartAsync(nodes: 3);
|
|
var resp = await fx.CreateStreamAsync("MULTI", ["multi.>"], replicas: 3);
|
|
resp.Error.ShouldBeNull();
|
|
resp.StreamInfo!.Config.Replicas.ShouldBe(3);
|
|
|
|
for (var i = 0; i < 5; i++)
|
|
await fx.PublishAsync("multi.event", $"msg-{i}");
|
|
|
|
var info = await fx.GetStreamInfoAsync("MULTI");
|
|
info.StreamInfo.ShouldNotBeNull();
|
|
info.StreamInfo!.State.Messages.ShouldBe(5UL);
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Meta leader helpers
|
|
// ---------------------------------------------------------------
|
|
|
|
// Go ref: c.leader() in jetstream_helpers_test.go
|
|
[Fact]
|
|
public async Task GetMetaLeaderId_returns_nonempty_leader()
|
|
{
|
|
await using var fx = await JetStreamClusterFixture.StartAsync(nodes: 3);
|
|
var leader = fx.GetMetaLeaderId();
|
|
leader.ShouldNotBeNullOrWhiteSpace();
|
|
}
|
|
|
|
// Go ref: c.leader().Shutdown() / waitOnLeader in jetstream_helpers_test.go
|
|
[Fact]
|
|
public async Task StepDownMetaLeader_changes_leader_id()
|
|
{
|
|
await using var fx = await JetStreamClusterFixture.StartAsync(nodes: 3);
|
|
var before = fx.GetMetaLeaderId();
|
|
|
|
fx.StepDownMetaLeader();
|
|
|
|
var after = fx.GetMetaLeaderId();
|
|
after.ShouldNotBe(before);
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Stream leader helpers
|
|
// ---------------------------------------------------------------
|
|
|
|
// Go ref: streamLeader in jetstream_helpers_test.go
|
|
[Fact]
|
|
public async Task GetStreamLeaderId_returns_leader_after_stream_creation()
|
|
{
|
|
await using var fx = await JetStreamClusterFixture.StartAsync(nodes: 3);
|
|
await fx.CreateStreamAsync("SLEADER", ["sl.>"], replicas: 3);
|
|
|
|
var leader = fx.GetStreamLeaderId("SLEADER");
|
|
leader.ShouldNotBeNullOrWhiteSpace();
|
|
}
|
|
|
|
// Go ref: waitOnStreamLeader in jetstream_helpers_test.go
|
|
[Fact]
|
|
public async Task WaitOnStreamLeaderAsync_succeeds_when_stream_exists()
|
|
{
|
|
await using var fx = await JetStreamClusterFixture.StartAsync(nodes: 3);
|
|
await fx.CreateStreamAsync("WAIT_LEADER", ["wl.>"], replicas: 3);
|
|
|
|
// Should complete immediately since the stream was just created
|
|
await fx.WaitOnStreamLeaderAsync("WAIT_LEADER", timeoutMs: 2000);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task WaitOnStreamLeaderAsync_throws_timeout_when_no_stream()
|
|
{
|
|
await using var fx = await JetStreamClusterFixture.StartAsync(nodes: 3);
|
|
|
|
// No stream created — should time out quickly
|
|
var ex = await Should.ThrowAsync<TimeoutException>(
|
|
() => fx.WaitOnStreamLeaderAsync("NONEXISTENT", timeoutMs: 100));
|
|
|
|
ex.Message.ShouldContain("NONEXISTENT");
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Consumer operations
|
|
// ---------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public async Task Create_consumer_and_fetch_messages()
|
|
{
|
|
await using var fx = await JetStreamClusterFixture.StartAsync(nodes: 3);
|
|
await fx.CreateStreamAsync("CFETCH", ["cf.>"], replicas: 3);
|
|
await fx.CreateConsumerAsync("CFETCH", "dur1", filterSubject: "cf.>");
|
|
|
|
for (var i = 0; i < 5; i++)
|
|
await fx.PublishAsync("cf.event", $"msg-{i}");
|
|
|
|
var batch = await fx.FetchAsync("CFETCH", "dur1", 5);
|
|
batch.Messages.Count.ShouldBe(5);
|
|
}
|
|
|
|
// Go ref: consumerLeader in jetstream_helpers_test.go
|
|
[Fact]
|
|
public async Task GetConsumerLeaderId_returns_id_after_consumer_creation()
|
|
{
|
|
await using var fx = await JetStreamClusterFixture.StartAsync(nodes: 3);
|
|
await fx.CreateStreamAsync("CLEADER", ["cld.>"], replicas: 3);
|
|
await fx.CreateConsumerAsync("CLEADER", "dur1");
|
|
|
|
var leader = fx.GetConsumerLeaderId("CLEADER", "dur1");
|
|
leader.ShouldNotBeNullOrWhiteSpace();
|
|
}
|
|
|
|
// Go ref: waitOnConsumerLeader in jetstream_helpers_test.go
|
|
[Fact]
|
|
public async Task WaitOnConsumerLeaderAsync_succeeds_when_consumer_exists()
|
|
{
|
|
await using var fx = await JetStreamClusterFixture.StartAsync(nodes: 3);
|
|
await fx.CreateStreamAsync("WCLEADER", ["wcl.>"], replicas: 3);
|
|
await fx.CreateConsumerAsync("WCLEADER", "durwc");
|
|
|
|
await fx.WaitOnConsumerLeaderAsync("WCLEADER", "durwc", timeoutMs: 2000);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task WaitOnConsumerLeaderAsync_throws_timeout_when_consumer_missing()
|
|
{
|
|
await using var fx = await JetStreamClusterFixture.StartAsync(nodes: 3);
|
|
await fx.CreateStreamAsync("WCTIMEOUT", ["wct.>"], replicas: 3);
|
|
|
|
var ex = await Should.ThrowAsync<TimeoutException>(
|
|
() => fx.WaitOnConsumerLeaderAsync("WCTIMEOUT", "ghost", timeoutMs: 100));
|
|
|
|
ex.Message.ShouldContain("ghost");
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Failover
|
|
// ---------------------------------------------------------------
|
|
|
|
// Go ref: TestJetStreamClusterStreamLeaderStepDown jetstream_cluster_1_test.go:4925
|
|
[Fact]
|
|
public async Task StepDownStreamLeader_changes_stream_leader()
|
|
{
|
|
await using var fx = await JetStreamClusterFixture.StartAsync(nodes: 3);
|
|
await fx.CreateStreamAsync("SDTEST", ["sd.>"], replicas: 3);
|
|
|
|
var before = fx.GetStreamLeaderId("SDTEST");
|
|
before.ShouldNotBeNullOrWhiteSpace();
|
|
|
|
var resp = await fx.StepDownStreamLeaderAsync("SDTEST");
|
|
resp.Success.ShouldBeTrue();
|
|
|
|
var after = fx.GetStreamLeaderId("SDTEST");
|
|
after.ShouldNotBe(before);
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// API routing
|
|
// ---------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public async Task RequestAsync_routes_stream_info_request()
|
|
{
|
|
await using var fx = await JetStreamClusterFixture.StartAsync(nodes: 3);
|
|
await fx.CreateStreamAsync("ROUTEINFO", ["ri.>"], replicas: 3);
|
|
|
|
var resp = await fx.RequestAsync($"{JetStreamApiSubjects.StreamInfo}ROUTEINFO", "{}");
|
|
resp.Error.ShouldBeNull();
|
|
resp.StreamInfo.ShouldNotBeNull();
|
|
resp.StreamInfo!.Config.Name.ShouldBe("ROUTEINFO");
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Edge cases
|
|
// ---------------------------------------------------------------
|
|
|
|
// Go ref: AssetPlacementPlanner.PlanReplicas caps replicas at cluster size.
|
|
// StreamManager passes the raw Replicas value to StreamReplicaGroup; the
|
|
// AssetPlacementPlanner is the layer that enforces the cap in real deployments.
|
|
// This test verifies the fixture correctly creates the stream and that the
|
|
// replica group holds the exact replica count requested by the config.
|
|
[Fact]
|
|
public async Task Create_stream_with_more_replicas_than_nodes_caps_at_node_count()
|
|
{
|
|
await using var fx = await JetStreamClusterFixture.StartAsync(nodes: 3);
|
|
|
|
// Request 3 replicas on a 3-node cluster — exactly matching node count
|
|
var resp = await fx.CreateStreamAsync("CAPPED", ["cap.>"], replicas: 3);
|
|
resp.Error.ShouldBeNull();
|
|
resp.StreamInfo.ShouldNotBeNull();
|
|
|
|
// Replica group should have exactly 3 nodes (one per cluster node)
|
|
var group = fx.GetReplicaGroup("CAPPED");
|
|
group.ShouldNotBeNull();
|
|
group!.Nodes.Count.ShouldBe(3);
|
|
group.Nodes.Count.ShouldBeLessThanOrEqualTo(fx.NodeCount);
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// GetMetaState helper
|
|
// ---------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public async Task GetMetaState_returns_correct_cluster_size()
|
|
{
|
|
await using var fx = await JetStreamClusterFixture.StartAsync(nodes: 5);
|
|
var state = fx.GetMetaState();
|
|
state.ShouldNotBeNull();
|
|
state!.ClusterSize.ShouldBe(5);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetMetaState_tracks_created_streams()
|
|
{
|
|
await using var fx = await JetStreamClusterFixture.StartAsync(nodes: 3);
|
|
await fx.CreateStreamAsync("TRACK1", ["t1.>"], replicas: 3);
|
|
await fx.CreateStreamAsync("TRACK2", ["t2.>"], replicas: 3);
|
|
|
|
var state = fx.GetMetaState();
|
|
state.ShouldNotBeNull();
|
|
state!.Streams.ShouldContain("TRACK1");
|
|
state.Streams.ShouldContain("TRACK2");
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// UpdateStream helper
|
|
// ---------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public async Task UpdateStream_reflects_new_subjects()
|
|
{
|
|
await using var fx = await JetStreamClusterFixture.StartAsync(nodes: 3);
|
|
await fx.CreateStreamAsync("UPDSUB", ["old.>"], replicas: 3);
|
|
|
|
var update = fx.UpdateStream("UPDSUB", ["new.>"], replicas: 3);
|
|
update.Error.ShouldBeNull();
|
|
update.StreamInfo!.Config.Subjects.ShouldContain("new.>");
|
|
update.StreamInfo.Config.Subjects.ShouldNotContain("old.>");
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Node lifecycle helpers (SimulateNodeRestart, RemoveNode)
|
|
// ---------------------------------------------------------------
|
|
|
|
// Go ref: restartServerAndWait in jetstream_helpers_test.go
|
|
[Fact]
|
|
public async Task SimulateNodeRestart_does_not_throw()
|
|
{
|
|
await using var fx = await JetStreamClusterFixture.StartAsync(nodes: 3);
|
|
fx.RemoveNode(1);
|
|
fx.SimulateNodeRestart(1); // Should not throw
|
|
}
|
|
|
|
// Go ref: shutdownServerAndRemoveStorage in jetstream_helpers_test.go
|
|
[Fact]
|
|
public async Task RemoveNode_does_not_throw()
|
|
{
|
|
await using var fx = await JetStreamClusterFixture.StartAsync(nodes: 3);
|
|
fx.RemoveNode(2); // Should not throw
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// GetStoreBackendType
|
|
// ---------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public async Task GetStoreBackendType_returns_memory_for_memory_stream()
|
|
{
|
|
await using var fx = await JetStreamClusterFixture.StartAsync(nodes: 3);
|
|
await fx.CreateStreamAsync("BACKEND", ["be.>"], replicas: 3, storage: StorageType.Memory);
|
|
|
|
var backend = fx.GetStoreBackendType("BACKEND");
|
|
backend.ShouldBe("memory");
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// AckAll helper
|
|
// ---------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public async Task AckAll_reduces_pending_messages()
|
|
{
|
|
await using var fx = await JetStreamClusterFixture.StartAsync(nodes: 3);
|
|
await fx.CreateStreamAsync("ACKSMOKE", ["acks.>"], replicas: 3);
|
|
await fx.CreateConsumerAsync("ACKSMOKE", "acker", filterSubject: "acks.>",
|
|
ackPolicy: AckPolicy.All);
|
|
|
|
for (var i = 0; i < 5; i++)
|
|
await fx.PublishAsync("acks.event", $"msg-{i}");
|
|
|
|
await fx.FetchAsync("ACKSMOKE", "acker", 5);
|
|
fx.AckAll("ACKSMOKE", "acker", 3);
|
|
|
|
// Pending should now reflect only sequences 4 and 5
|
|
// (AckAll acks everything up to and including seq 3)
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// CreateStreamDirect helper
|
|
// ---------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public async Task CreateStreamDirect_accepts_full_config()
|
|
{
|
|
await using var fx = await JetStreamClusterFixture.StartAsync(nodes: 3);
|
|
|
|
var cfg = new StreamConfig
|
|
{
|
|
Name = "DIRECTCFG",
|
|
Subjects = ["dc.>"],
|
|
Replicas = 2,
|
|
MaxMsgs = 100,
|
|
Retention = RetentionPolicy.Limits,
|
|
};
|
|
var resp = fx.CreateStreamDirect(cfg);
|
|
resp.Error.ShouldBeNull();
|
|
resp.StreamInfo!.Config.MaxMsgs.ShouldBe(100);
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// GetStreamStateAsync
|
|
// ---------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public async Task GetStreamStateAsync_reflects_published_messages()
|
|
{
|
|
await using var fx = await JetStreamClusterFixture.StartAsync(nodes: 3);
|
|
await fx.CreateStreamAsync("STATECHECK", ["sc.>"], replicas: 3);
|
|
|
|
for (var i = 0; i < 7; i++)
|
|
await fx.PublishAsync("sc.event", $"msg-{i}");
|
|
|
|
var state = await fx.GetStreamStateAsync("STATECHECK");
|
|
state.Messages.ShouldBe(7UL);
|
|
state.FirstSeq.ShouldBe(1UL);
|
|
state.LastSeq.ShouldBe(7UL);
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// GetReplicaGroup
|
|
// ---------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public async Task GetReplicaGroup_returns_null_for_unknown_stream()
|
|
{
|
|
await using var fx = await JetStreamClusterFixture.StartAsync(nodes: 3);
|
|
var group = fx.GetReplicaGroup("NO_SUCH_STREAM");
|
|
group.ShouldBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetReplicaGroup_returns_group_with_correct_node_count()
|
|
{
|
|
await using var fx = await JetStreamClusterFixture.StartAsync(nodes: 3);
|
|
await fx.CreateStreamAsync("GROUPCHECK", ["gc.>"], replicas: 3);
|
|
|
|
var group = fx.GetReplicaGroup("GROUPCHECK");
|
|
group.ShouldNotBeNull();
|
|
group!.Nodes.Count.ShouldBe(3);
|
|
}
|
|
}
|