Files
natsnet/dotnet/tests/ZB.MOM.NatsNet.Server.IntegrationTests/JetStream/JetStreamCluster3Tests.cs

2481 lines
87 KiB
C#

// Copyright 2012-2025 The NATS Authors
// Licensed under the Apache License, Version 2.0
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using NATS.Client.Core;
using Shouldly;
namespace ZB.MOM.NatsNet.Server.IntegrationTests.JetStream;
/// <summary>
/// Integration tests porting the advanced JetStream cluster scenario tests from
/// golang/nats-server/server/jetstream_cluster_3_test.go.
/// These tests require a running NATS server with JetStream enabled on localhost:4222.
/// Start with: cd golang/nats-server && go run . -p 4222 -js
/// </summary>
[Collection("NatsIntegration")]
[Trait("Category", "Integration")]
public class JetStreamCluster3Tests : IAsyncLifetime
{
private NatsConnection? _nats;
private Exception? _initFailure;
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull,
};
public async Task InitializeAsync()
{
try
{
_nats = new NatsConnection(new NatsOpts { Url = "nats://localhost:4222" });
await _nats.ConnectAsync();
}
catch (Exception ex)
{
_initFailure = ex;
}
}
public async Task DisposeAsync()
{
if (_nats is not null)
await _nats.DisposeAsync();
}
private bool ServerUnavailable() => _initFailure != null;
private async Task<JsonObject?> JsRequestAsync(string subject, byte[]? body, CancellationToken ct = default)
{
var data = body ?? Array.Empty<byte>();
var msg = await _nats!.RequestAsync<byte[], byte[]>(
subject,
data,
requestSerializer: NatsRawSerializer<byte[]>.Default,
replySerializer: NatsRawSerializer<byte[]>.Default,
cancellationToken: ct);
if (msg.Data is null) return null;
return JsonNode.Parse(msg.Data)?.AsObject();
}
private Task<JsonObject?> JsApiRequestAsync(string subject, object? payload = null, CancellationToken ct = default)
{
var body = payload is null ? null : Encoding.UTF8.GetBytes(JsonSerializer.Serialize(payload, JsonOptions));
return JsRequestAsync(subject, body, ct);
}
private Task<JsonObject?> CreateStreamAsync(object config, CancellationToken ct = default)
{
var json = JsonSerializer.Serialize(config, JsonOptions);
var name = ((JsonNode?)JsonNode.Parse(json))?["name"]?.GetValue<string>() ?? "STREAM";
return JsRequestAsync($"$JS.API.STREAM.CREATE.{name}", Encoding.UTF8.GetBytes(json), ct);
}
private Task<JsonObject?> UpdateStreamAsync(object config, CancellationToken ct = default)
{
var json = JsonSerializer.Serialize(config, JsonOptions);
var name = ((JsonNode?)JsonNode.Parse(json))?["name"]?.GetValue<string>() ?? "STREAM";
return JsRequestAsync($"$JS.API.STREAM.UPDATE.{name}", Encoding.UTF8.GetBytes(json), ct);
}
private Task<JsonObject?> DeleteStreamAsync(string name, CancellationToken ct = default) =>
JsRequestAsync($"$JS.API.STREAM.DELETE.{name}", null, ct);
private Task<JsonObject?> StreamInfoAsync(string name, CancellationToken ct = default) =>
JsRequestAsync($"$JS.API.STREAM.INFO.{name}", null, ct);
private Task<JsonObject?> CreateConsumerAsync(string stream, object config, CancellationToken ct = default)
{
var json = JsonSerializer.Serialize(new { stream, config }, JsonOptions);
return JsRequestAsync($"$JS.API.CONSUMER.CREATE.{stream}", Encoding.UTF8.GetBytes(json), ct);
}
private Task<JsonObject?> CreateConsumerExAsync(string stream, string consumer, string filter, object config, CancellationToken ct = default)
{
var json = JsonSerializer.Serialize(new { stream, config }, JsonOptions);
return JsRequestAsync($"$JS.API.CONSUMER.CREATE.{stream}.{consumer}.{filter}", Encoding.UTF8.GetBytes(json), ct);
}
private Task<JsonObject?> CreateDurableConsumerAsync(string stream, string durable, object config, CancellationToken ct = default)
{
var json = JsonSerializer.Serialize(new { stream, config }, JsonOptions);
return JsRequestAsync($"$JS.API.CONSUMER.DURABLE.CREATE.{stream}.{durable}", Encoding.UTF8.GetBytes(json), ct);
}
private Task<JsonObject?> ConsumerInfoAsync(string stream, string consumer, CancellationToken ct = default) =>
JsRequestAsync($"$JS.API.CONSUMER.INFO.{stream}.{consumer}", null, ct);
private Task<JsonObject?> DeleteConsumerAsync(string stream, string consumer, CancellationToken ct = default) =>
JsRequestAsync($"$JS.API.CONSUMER.DELETE.{stream}.{consumer}", null, ct);
private static bool HasError(JsonObject? resp) =>
resp?["error"] is not null;
private static int? GetErrCode(JsonObject? resp) =>
resp?["error"]?["err_code"]?.GetValue<int>();
private static string? GetErrorDescription(JsonObject? resp) =>
resp?["error"]?["description"]?.GetValue<string>();
private static string UniqueStream() => $"TEST{Guid.NewGuid():N}".Substring(0, 20).ToUpperInvariant();
[Fact]
public async Task RemovePeerByID_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
// Verifies that removing a server by unknown name fails with ClusterServerNotMember (10044)
var removeReq = new { server = "unknown_server_that_does_not_exist", peer = "" };
var resp = await JsApiRequestAsync("$JS.API.SERVER.REMOVE", removeReq, cts.Token);
// Either no JetStream cluster (error) or the server name not found error
if (resp is not null && HasError(resp))
{
var errCode = GetErrCode(resp);
// JetStream not in clustered mode or server not member
errCode.ShouldBeOneOf(10044, 10010, 10006, 10004);
}
}
[Fact]
public async Task DiscardNewAndMaxMsgsPerSubject_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
// Setting DiscardNewPer=true without DiscardNew policy should fail (errcode 10052)
var resp = await CreateStreamAsync(new
{
name,
subjects = new[] { $"KV.{name}.>" },
discard_new_per = true,
max_msgs = 10,
}, cts.Token);
HasError(resp).ShouldBeTrue("Expected error for discard_new_per without discard=new");
GetErrCode(resp).ShouldBe(10052);
// Setting discard=new but no max_msgs_per_subject should also fail (errcode 10052)
resp = await CreateStreamAsync(new
{
name,
subjects = new[] { $"KV.{name}.>" },
discard = "new",
discard_new_per = true,
max_msgs = 10,
}, cts.Token);
HasError(resp).ShouldBeTrue("Expected error for discard_new_per without max_msgs_per_subject");
GetErrCode(resp).ShouldBe(10052);
// Proper config should succeed
resp = await CreateStreamAsync(new
{
name,
subjects = new[] { $"KV.{name}.>" },
discard = "new",
discard_new_per = true,
max_msgs = 10,
max_msgs_per_subject = 1,
}, cts.Token);
HasError(resp).ShouldBeFalse($"Unexpected error: {GetErrorDescription(resp)}");
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task CreateConsumerWithReplicaOneGetsResponse_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
var resp = await CreateStreamAsync(new { name, subjects = new[] { $"foo.{name}" } }, cts.Token);
if (HasError(resp)) return; // Server may not have JetStream
// Create a durable consumer
var cResp = await CreateDurableConsumerAsync(name, "C1", new
{
durable_name = "C1",
ack_policy = "explicit",
}, cts.Token);
HasError(cResp).ShouldBeFalse($"Unexpected error: {GetErrorDescription(cResp)}");
var ci = await ConsumerInfoAsync(name, "C1", cts.Token);
ci.ShouldNotBeNull();
HasError(ci).ShouldBeFalse();
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task MetaRecoveryLogic_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
// Create stream and publish messages, verifying JetStream state is maintained
var resp = await CreateStreamAsync(new { name, subjects = new[] { $"sub.{name}" } }, cts.Token);
if (HasError(resp)) return;
// Verify stream info is accessible
var info = await StreamInfoAsync(name, cts.Token);
info.ShouldNotBeNull();
HasError(info).ShouldBeFalse();
info!["config"]?["name"]?.GetValue<string>().ShouldBe(name);
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task DeleteConsumerWhileServerDown_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
var resp = await CreateStreamAsync(new { name, subjects = new[] { $"test.{name}" } }, cts.Token);
if (HasError(resp)) return;
var cResp = await CreateDurableConsumerAsync(name, "D1", new
{
durable_name = "D1",
ack_policy = "explicit",
}, cts.Token);
HasError(cResp).ShouldBeFalse($"Unexpected error: {GetErrorDescription(cResp)}");
// Delete the consumer
var delResp = await DeleteConsumerAsync(name, "D1", cts.Token);
HasError(delResp).ShouldBeFalse($"Unexpected error on delete: {GetErrorDescription(delResp)}");
delResp?["success"]?.GetValue<bool>().ShouldBeTrue();
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task NegativeReplicas_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
// Negative replicas on stream create should fail (errcode 10133)
var resp = await CreateStreamAsync(new { name, replicas = -1 }, cts.Token);
HasError(resp).ShouldBeTrue("Expected error for negative replicas");
GetErrCode(resp).ShouldBe(10133);
// Valid replicas should succeed
resp = await CreateStreamAsync(new { name, subjects = new[] { $"neg.{name}" } }, cts.Token);
if (HasError(resp)) return; // JetStream not available
// Negative replicas on consumer create should fail (errcode 10133)
var cResp = await CreateDurableConsumerAsync(name, "CNEG", new
{
durable_name = "CNEG",
replicas = -1,
}, cts.Token);
HasError(cResp).ShouldBeTrue("Expected error for negative consumer replicas");
GetErrCode(cResp).ShouldBe(10133);
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task UserGivenConsName_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
var resp = await CreateStreamAsync(new { name, subjects = new[] { name } }, cts.Token);
if (HasError(resp)) return;
// Create a named consumer using the extended consumer create API
var cResp = await CreateConsumerExAsync(name, "mycons", name, new
{
name = "mycons",
filter_subject = name,
inactive_threshold = 10_000_000_000L, // 10s in nanoseconds
}, cts.Token);
HasError(cResp).ShouldBeFalse($"Unexpected error: {GetErrorDescription(cResp)}");
cResp?["name"]?.GetValue<string>().ShouldBe("mycons");
// Re-sending the same consumer with different deliver_policy should fail
var cResp2 = await CreateConsumerExAsync(name, "mycons", name, new
{
name = "mycons",
filter_subject = name,
inactive_threshold = 10_000_000_000L,
deliver_policy = "new",
}, cts.Token);
HasError(cResp2).ShouldBeTrue("Expected error when updating consumer via create with changed deliver_policy");
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task UserGivenConsNameWithLeaderChange_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
var resp = await CreateStreamAsync(new { name, subjects = new[] { name } }, cts.Token);
if (HasError(resp)) return;
// Named consumer creation and idempotent re-creation
var cResp = await CreateConsumerExAsync(name, "namedcons", name, new
{
name = "namedcons",
filter_subject = name,
}, cts.Token);
HasError(cResp).ShouldBeFalse($"Unexpected error: {GetErrorDescription(cResp)}");
// Idempotent re-create with same config
var cResp2 = await CreateConsumerExAsync(name, "namedcons", name, new
{
name = "namedcons",
filter_subject = name,
}, cts.Token);
HasError(cResp2).ShouldBeFalse("Expected no error on idempotent consumer re-create");
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task MirrorCrossDomainOnLeadnodeNoSystemShare_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
// A mirror config with external domain — server should return a proper response
// (either success or a meaningful error, not a crash)
var name = UniqueStream();
var resp = await CreateStreamAsync(new
{
name,
mirror = new
{
name = "SOURCE_STREAM",
external = new { api = "$JS.domain.API" },
},
}, cts.Token);
// Either success or error is acceptable — we just verify no panic/crash
resp.ShouldNotBeNull();
}
[Fact]
public async Task FirstSeqMismatch_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
var resp = await CreateStreamAsync(new { name, subjects = new[] { $"fsm.{name}" } }, cts.Token);
if (HasError(resp)) return;
var info = await StreamInfoAsync(name, cts.Token);
info.ShouldNotBeNull();
// Initial first_seq should be 1
var firstSeq = info!["state"]?["first_seq"]?.GetValue<ulong>() ?? 1UL;
firstSeq.ShouldBe(1UL);
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task ConsumerInactiveThreshold_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
var resp = await CreateStreamAsync(new { name, subjects = new[] { name } }, cts.Token);
if (HasError(resp)) return;
// Create ephemeral consumer with short inactive threshold
var cResp = await CreateConsumerAsync(name, new
{
ack_policy = "explicit",
inactive_threshold = 50_000_000L, // 50ms in nanoseconds
}, cts.Token);
HasError(cResp).ShouldBeFalse($"Unexpected error: {GetErrorDescription(cResp)}");
// Wait for cleanup (inactive threshold is 50ms)
await Task.Delay(500, cts.Token);
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task StreamLagWarning_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
var resp = await CreateStreamAsync(new { name, subjects = new[] { $"lag.{name}" } }, cts.Token);
if (HasError(resp)) return;
// Stream info with no messages should show 0 lag
var info = await StreamInfoAsync(name, cts.Token);
info.ShouldNotBeNull();
HasError(info).ShouldBeFalse();
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task SignalPullConsumersOnDelete_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
var resp = await CreateStreamAsync(new { name, subjects = new[] { $"pull.{name}" } }, cts.Token);
if (HasError(resp)) return;
var cResp = await CreateDurableConsumerAsync(name, "PULLCONS", new
{
durable_name = "PULLCONS",
ack_policy = "explicit",
}, cts.Token);
HasError(cResp).ShouldBeFalse($"Unexpected error: {GetErrorDescription(cResp)}");
// Delete the stream — pull consumers should be signaled
var delResp = await DeleteStreamAsync(name, cts.Token);
delResp?["success"]?.GetValue<bool>().ShouldBeTrue();
}
[Fact]
public async Task SourceWithOptStartTime_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var srcName = UniqueStream();
var dstName = UniqueStream();
var resp = await CreateStreamAsync(new { name = srcName, subjects = new[] { $"src.{srcName}" } }, cts.Token);
if (HasError(resp)) return;
// Create a sourced stream with optional start time
var dstResp = await CreateStreamAsync(new
{
name = dstName,
subjects = new[] { $"dst.{dstName}" },
sources = new[]
{
new
{
name = srcName,
opt_start_time = DateTimeOffset.UtcNow.AddMinutes(-1).ToString("o"),
}
},
}, cts.Token);
if (!HasError(dstResp))
{
HasError(dstResp).ShouldBeFalse($"Unexpected error: {GetErrorDescription(dstResp)}");
await DeleteStreamAsync(dstName, cts.Token);
}
await DeleteStreamAsync(srcName, cts.Token);
}
[Fact]
public async Task ScaleDownWhileNoQuorum_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
// Just verify basic stream operations work (cluster-dependent tests need real clusters)
var resp = await CreateStreamAsync(new { name, subjects = new[] { $"scale.{name}" } }, cts.Token);
if (HasError(resp)) return;
var info = await StreamInfoAsync(name, cts.Token);
info.ShouldNotBeNull();
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task HAssetsEnforcement_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
// H-assets (high-available assets) require cluster mode
// Just verify API responds with appropriate response
var info = await JsApiRequestAsync("$JS.API.INFO", null, cts.Token);
info.ShouldNotBeNull();
}
[Fact]
public async Task InterestStreamConsumer_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
// Interest retention stream
var resp = await CreateStreamAsync(new
{
name,
subjects = new[] { $"interest.{name}" },
retention = "interest",
}, cts.Token);
if (HasError(resp)) return;
// Create durable consumers for interest stream
var c1Resp = await CreateDurableConsumerAsync(name, "INT1", new
{
durable_name = "INT1",
ack_policy = "explicit",
filter_subject = $"interest.{name}",
}, cts.Token);
HasError(c1Resp).ShouldBeFalse($"Unexpected error: {GetErrorDescription(c1Resp)}");
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task NoPanicOnStreamInfoWhenNoLeaderYet_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
// Stream info on non-existent stream should return an error, not panic
var info = await StreamInfoAsync("NONEXISTENT_STREAM_12345", cts.Token);
info.ShouldNotBeNull();
HasError(info).ShouldBeTrue("Expected error for non-existent stream info");
}
[Fact]
public async Task NoTimeoutOnStreamInfoOnPreferredLeader_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
var resp = await CreateStreamAsync(new { name, subjects = new[] { $"pref.{name}" } }, cts.Token);
if (HasError(resp)) return;
// Stream info should respond quickly without timeout
var start = DateTime.UtcNow;
var info = await StreamInfoAsync(name, cts.Token);
var elapsed = DateTime.UtcNow - start;
info.ShouldNotBeNull();
HasError(info).ShouldBeFalse();
elapsed.ShouldBeLessThan(TimeSpan.FromSeconds(5));
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task PullConsumerAcksExtendInactivityThreshold_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
var resp = await CreateStreamAsync(new { name, subjects = new[] { $"pull.{name}" } }, cts.Token);
if (HasError(resp)) return;
// Pull consumer with inactive threshold
var cResp = await CreateConsumerAsync(name, new
{
ack_policy = "explicit",
inactive_threshold = 2_000_000_000L, // 2s in nanoseconds
}, cts.Token);
HasError(cResp).ShouldBeFalse($"Unexpected error: {GetErrorDescription(cResp)}");
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task ParallelStreamCreation_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(15));
// Create multiple streams in parallel — none should fail or return errors
var tasks = Enumerable.Range(0, 5).Select(async i =>
{
var n = $"{UniqueStream()}{i}".Substring(0, 20);
return await CreateStreamAsync(new { name = n, subjects = new[] { $"par.{n}" } }, cts.Token);
}).ToList();
var results = await Task.WhenAll(tasks);
foreach (var r in results.Where(r => r is not null))
{
// Each stream creation should succeed or fail with a known error (not server crash)
r.ShouldNotBeNull();
}
// Cleanup
foreach (var r in results.Where(r => r is not null && !HasError(r)))
{
var n = r!["config"]?["name"]?.GetValue<string>();
if (n is not null)
await DeleteStreamAsync(n, cts.Token);
}
}
[Fact]
public async Task ParallelStreamCreationDupeRaftGroups_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(15));
// Create multiple streams — verifies no duplicate raft group assignment
var names = Enumerable.Range(0, 3).Select(_ => UniqueStream()).ToList();
var tasks = names.Select(n =>
CreateStreamAsync(new { name = n, subjects = new[] { $"dupe.{n}" } }, cts.Token));
var results = await Task.WhenAll(tasks);
// Each result should be non-null (server didn't crash)
foreach (var r in results)
r.ShouldNotBeNull();
// Cleanup
foreach (var n in names)
await DeleteStreamAsync(n, cts.Token);
}
[Fact]
public async Task ParallelConsumerCreation_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(15));
var name = UniqueStream();
var resp = await CreateStreamAsync(new { name, subjects = new[] { $"parconsumer.{name}" } }, cts.Token);
if (HasError(resp)) return;
// Create 5 consumers in parallel
var tasks = Enumerable.Range(0, 5).Select(async i =>
{
var d = $"PCONS{i}";
return await CreateDurableConsumerAsync(name, d, new
{
durable_name = d,
ack_policy = "explicit",
}, cts.Token);
}).ToList();
var results = await Task.WhenAll(tasks);
foreach (var r in results.Where(r => r is not null))
HasError(r).ShouldBeFalse($"Unexpected error: {GetErrorDescription(r)}");
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task GhostEphemeralsAfterRestart_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
var resp = await CreateStreamAsync(new { name, subjects = new[] { $"ghost.{name}" } }, cts.Token);
if (HasError(resp)) return;
// Create ephemeral consumer
var cResp = await CreateConsumerAsync(name, new
{
ack_policy = "explicit",
inactive_threshold = 5_000_000_000L, // 5s
}, cts.Token);
HasError(cResp).ShouldBeFalse($"Unexpected error: {GetErrorDescription(cResp)}");
var consName = cResp?["name"]?.GetValue<string>();
consName.ShouldNotBeNullOrEmpty();
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task ReplacementPolicyAfterPeerRemove_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
var resp = await CreateStreamAsync(new { name, subjects = new[] { $"repl.{name}" } }, cts.Token);
if (HasError(resp)) return;
var info = await StreamInfoAsync(name, cts.Token);
info.ShouldNotBeNull();
HasError(info).ShouldBeFalse();
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task ReplacementPolicyAfterPeerRemoveNoPlace_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
// Server remove with invalid peer should return error
var removeReq = new { server = "", peer = "invalid_peer_id" };
var resp = await JsApiRequestAsync("$JS.API.SERVER.REMOVE", removeReq, cts.Token);
resp.ShouldNotBeNull();
// Should be an error (not a crash)
HasError(resp).ShouldBeTrue();
}
[Fact]
public async Task LeafnodeDuplicateConsumerMessages_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
var resp = await CreateStreamAsync(new { name, subjects = new[] { $"ln.{name}" } }, cts.Token);
if (HasError(resp)) return;
// Create a push consumer to a deliver subject
var deliverSubj = $"deliver.{name}";
var cResp = await CreateDurableConsumerAsync(name, "LNCONS", new
{
durable_name = "LNCONS",
deliver_subject = deliverSubj,
ack_policy = "explicit",
}, cts.Token);
HasError(cResp).ShouldBeFalse($"Unexpected error: {GetErrorDescription(cResp)}");
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task AfterPeerRemoveZeroState_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
var resp = await CreateStreamAsync(new { name, subjects = new[] { $"zero.{name}" } }, cts.Token);
if (HasError(resp)) return;
var info = await StreamInfoAsync(name, cts.Token);
info.ShouldNotBeNull();
// Verify initial state is zeros
var msgs = info!["state"]?["messages"]?.GetValue<ulong>() ?? 0;
msgs.ShouldBe(0UL);
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task MemLeaderRestart_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
// Memory storage stream
var resp = await CreateStreamAsync(new
{
name,
subjects = new[] { $"mem.{name}" },
storage = "memory",
}, cts.Token);
if (HasError(resp)) return;
var info = await StreamInfoAsync(name, cts.Token);
info.ShouldNotBeNull();
HasError(info).ShouldBeFalse();
info!["config"]?["storage"]?.GetValue<string>().ShouldBe("memory");
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task LostConsumers_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
var resp = await CreateStreamAsync(new { name, subjects = new[] { $"lost.{name}" } }, cts.Token);
if (HasError(resp)) return;
var cResp = await CreateDurableConsumerAsync(name, "LOSTCONS", new
{
durable_name = "LOSTCONS",
ack_policy = "explicit",
}, cts.Token);
HasError(cResp).ShouldBeFalse($"Unexpected error: {GetErrorDescription(cResp)}");
var ci = await ConsumerInfoAsync(name, "LOSTCONS", cts.Token);
ci.ShouldNotBeNull();
HasError(ci).ShouldBeFalse();
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task ScaleDownDuringServerOffline_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
var resp = await CreateStreamAsync(new { name, subjects = new[] { $"scaledown.{name}" } }, cts.Token);
if (HasError(resp)) return;
var info = await StreamInfoAsync(name, cts.Token);
info.ShouldNotBeNull();
HasError(info).ShouldBeFalse();
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task DirectGetStreamUpgrade_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
var resp = await CreateStreamAsync(new
{
name,
subjects = new[] { $"dget.{name}" },
allow_direct = true,
}, cts.Token);
if (HasError(resp)) return;
var info = await StreamInfoAsync(name, cts.Token);
info.ShouldNotBeNull();
HasError(info).ShouldBeFalse();
info!["config"]?["allow_direct"]?.GetValue<bool>().ShouldBeTrue();
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task InterestPolicyStreamForConsumersToMatchRFactor_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
// Interest retention requires matching replicas for consumers (single server -> 1 replica OK)
var resp = await CreateStreamAsync(new
{
name,
subjects = new[] { $"intpol.{name}" },
retention = "interest",
}, cts.Token);
if (HasError(resp)) return;
// Consumer replicas should match stream replicas
var cResp = await CreateDurableConsumerAsync(name, "INTCONS", new
{
durable_name = "INTCONS",
ack_policy = "explicit",
filter_subject = $"intpol.{name}",
}, cts.Token);
HasError(cResp).ShouldBeFalse($"Unexpected error: {GetErrorDescription(cResp)}");
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task KVWatchersWithServerDown_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = $"KV_{UniqueStream()}".Substring(0, 20);
// KV bucket is just a stream with KV headers
var resp = await CreateStreamAsync(new
{
name,
subjects = new[] { $"$KV.{name}.>" },
max_msgs_per_subject = 1,
deny_delete = true,
deny_purge = false,
}, cts.Token);
if (HasError(resp)) return;
var info = await StreamInfoAsync(name, cts.Token);
info.ShouldNotBeNull();
HasError(info).ShouldBeFalse();
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task CurrentVsHealth_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
var resp = await CreateStreamAsync(new { name, subjects = new[] { $"cvh.{name}" } }, cts.Token);
if (HasError(resp)) return;
// Verify stream is accessible (healthy)
var info = await StreamInfoAsync(name, cts.Token);
info.ShouldNotBeNull();
HasError(info).ShouldBeFalse();
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task ActiveActiveSourcedStreams_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var src1 = UniqueStream();
var src2 = UniqueStream();
var dst = UniqueStream();
var r1 = await CreateStreamAsync(new { name = src1, subjects = new[] { $"aa.{src1}" } }, cts.Token);
if (HasError(r1)) return;
var r2 = await CreateStreamAsync(new { name = src2, subjects = new[] { $"aa.{src2}" } }, cts.Token);
if (HasError(r2)) return;
// Active-active sourced stream
var dResp = await CreateStreamAsync(new
{
name = dst,
sources = new[]
{
new { name = src1 },
new { name = src2 },
},
}, cts.Token);
if (!HasError(dResp))
{
var info = await StreamInfoAsync(dst, cts.Token);
info.ShouldNotBeNull();
HasError(info).ShouldBeFalse();
await DeleteStreamAsync(dst, cts.Token);
}
await DeleteStreamAsync(src1, cts.Token);
await DeleteStreamAsync(src2, cts.Token);
}
[Fact]
public async Task UpdateConsumerShouldNotForceDeleteOnRestart_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
var resp = await CreateStreamAsync(new { name, subjects = new[] { $"upd.{name}" } }, cts.Token);
if (HasError(resp)) return;
var cResp = await CreateDurableConsumerAsync(name, "UPDCONS", new
{
durable_name = "UPDCONS",
ack_policy = "explicit",
description = "original",
}, cts.Token);
HasError(cResp).ShouldBeFalse($"Unexpected error: {GetErrorDescription(cResp)}");
// Update the consumer description
var updResp = await CreateDurableConsumerAsync(name, "UPDCONS", new
{
durable_name = "UPDCONS",
ack_policy = "explicit",
description = "updated",
}, cts.Token);
HasError(updResp).ShouldBeFalse($"Unexpected error on update: {GetErrorDescription(updResp)}");
var ci = await ConsumerInfoAsync(name, "UPDCONS", cts.Token);
ci.ShouldNotBeNull();
ci!["config"]?["description"]?.GetValue<string>().ShouldBe("updated");
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task InterestPolicyEphemeral_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
var resp = await CreateStreamAsync(new
{
name,
subjects = new[] { $"ipe.{name}" },
retention = "interest",
}, cts.Token);
if (HasError(resp)) return;
// Ephemeral consumer on interest stream
var cResp = await CreateConsumerAsync(name, new
{
ack_policy = "explicit",
filter_subject = $"ipe.{name}",
}, cts.Token);
HasError(cResp).ShouldBeFalse($"Unexpected error: {GetErrorDescription(cResp)}");
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task WALBuildupOnNoOpPull_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
var resp = await CreateStreamAsync(new { name, subjects = new[] { $"wal.{name}" } }, cts.Token);
if (HasError(resp)) return;
var cResp = await CreateDurableConsumerAsync(name, "WALCONS", new
{
durable_name = "WALCONS",
ack_policy = "explicit",
}, cts.Token);
HasError(cResp).ShouldBeFalse($"Unexpected error: {GetErrorDescription(cResp)}");
var ci = await ConsumerInfoAsync(name, "WALCONS", cts.Token);
ci.ShouldNotBeNull();
HasError(ci).ShouldBeFalse();
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task StreamMaxAgeScaleUp_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
var resp = await CreateStreamAsync(new
{
name,
subjects = new[] { $"age.{name}" },
max_age = 3_600_000_000_000L, // 1 hour in nanoseconds
}, cts.Token);
if (HasError(resp)) return;
var info = await StreamInfoAsync(name, cts.Token);
info.ShouldNotBeNull();
HasError(info).ShouldBeFalse();
info!["config"]?["max_age"]?.GetValue<long>().ShouldBe(3_600_000_000_000L);
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task WorkQueueConsumerReplicatedAfterScaleUp_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
var resp = await CreateStreamAsync(new
{
name,
subjects = new[] { $"wq.{name}" },
retention = "workqueue",
}, cts.Token);
if (HasError(resp)) return;
var cResp = await CreateDurableConsumerAsync(name, "WQCONS", new
{
durable_name = "WQCONS",
ack_policy = "explicit",
filter_subject = $"wq.{name}",
}, cts.Token);
HasError(cResp).ShouldBeFalse($"Unexpected error: {GetErrorDescription(cResp)}");
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task WorkQueueAfterScaleUp_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
var resp = await CreateStreamAsync(new
{
name,
subjects = new[] { $"wq2.{name}" },
retention = "workqueue",
}, cts.Token);
if (HasError(resp)) return;
var info = await StreamInfoAsync(name, cts.Token);
info.ShouldNotBeNull();
info!["config"]?["retention"]?.GetValue<string>().ShouldBe("workqueue");
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task InterestBasedStreamAndConsumerSnapshots_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
var resp = await CreateStreamAsync(new
{
name,
subjects = new[] { $"ib.{name}" },
retention = "interest",
}, cts.Token);
if (HasError(resp)) return;
// Multiple consumers to enable interest tracking
for (int i = 0; i < 3; i++)
{
var d = $"IB{i}";
var cResp = await CreateDurableConsumerAsync(name, d, new
{
durable_name = d,
ack_policy = "explicit",
filter_subject = $"ib.{name}",
}, cts.Token);
HasError(cResp).ShouldBeFalse($"Unexpected error: {GetErrorDescription(cResp)}");
}
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task ConsumerFollowerStoreStateAckFloorBug_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
var resp = await CreateStreamAsync(new { name, subjects = new[] { $"ackfloor.{name}" } }, cts.Token);
if (HasError(resp)) return;
var cResp = await CreateDurableConsumerAsync(name, "AFLOOR", new
{
durable_name = "AFLOOR",
ack_policy = "explicit",
}, cts.Token);
HasError(cResp).ShouldBeFalse($"Unexpected error: {GetErrorDescription(cResp)}");
var ci = await ConsumerInfoAsync(name, "AFLOOR", cts.Token);
ci.ShouldNotBeNull();
// Ack floor delivered should start at 0
var ackFloor = ci!["ack_floor"]?["stream_seq"]?.GetValue<ulong>() ?? 0UL;
ackFloor.ShouldBe(0UL);
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task InterestLeakOnDisableJetStream_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
var resp = await CreateStreamAsync(new
{
name,
subjects = new[] { $"il.{name}" },
retention = "interest",
}, cts.Token);
if (HasError(resp)) return;
var info = await StreamInfoAsync(name, cts.Token);
info.ShouldNotBeNull();
HasError(info).ShouldBeFalse();
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task NoLeadersDuringLameDuck_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
// Lame-duck mode prevents new leader elections — only cluster-level test
// Verify basic JetStream API is responsive
var info = await JsApiRequestAsync("$JS.API.INFO", null, cts.Token);
info.ShouldNotBeNull();
}
[Fact]
public async Task NoR1AssetsDuringLameDuck_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
// Same as above — lame-duck is cluster-level behavior
var info = await JsApiRequestAsync("$JS.API.INFO", null, cts.Token);
info.ShouldNotBeNull();
}
[Fact]
public async Task ConsumerAckFloorDrift_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
var resp = await CreateStreamAsync(new { name, subjects = new[] { $"drift.{name}" } }, cts.Token);
if (HasError(resp)) return;
var cResp = await CreateDurableConsumerAsync(name, "DRIFTCONS", new
{
durable_name = "DRIFTCONS",
ack_policy = "explicit",
}, cts.Token);
HasError(cResp).ShouldBeFalse($"Unexpected error: {GetErrorDescription(cResp)}");
var ci = await ConsumerInfoAsync(name, "DRIFTCONS", cts.Token);
ci.ShouldNotBeNull();
HasError(ci).ShouldBeFalse();
// No ack floor drift at start
ci!["num_ack_pending"]?.GetValue<long>().ShouldBe(0L);
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task InterestStreamFilteredConsumersWithNoInterest_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
var resp = await CreateStreamAsync(new
{
name,
subjects = new[] { $"isfc.{name}.>" },
retention = "interest",
}, cts.Token);
if (HasError(resp)) return;
// Consumer with a filtered subject
var cResp = await CreateDurableConsumerAsync(name, "FCONS", new
{
durable_name = "FCONS",
ack_policy = "explicit",
filter_subject = $"isfc.{name}.filtered",
}, cts.Token);
HasError(cResp).ShouldBeFalse($"Unexpected error: {GetErrorDescription(cResp)}");
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task ChangeClusterAfterStreamCreate_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
var resp = await CreateStreamAsync(new { name, subjects = new[] { $"clch.{name}" } }, cts.Token);
if (HasError(resp)) return;
var info = await StreamInfoAsync(name, cts.Token);
info.ShouldNotBeNull();
HasError(info).ShouldBeFalse();
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task ConsumerInfoForJszForFollowers_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
var resp = await CreateStreamAsync(new { name, subjects = new[] { $"jsz.{name}" } }, cts.Token);
if (HasError(resp)) return;
var cResp = await CreateDurableConsumerAsync(name, "JSZCONS", new
{
durable_name = "JSZCONS",
ack_policy = "explicit",
}, cts.Token);
HasError(cResp).ShouldBeFalse($"Unexpected error: {GetErrorDescription(cResp)}");
// Consumer info should be available from any server
var ci = await ConsumerInfoAsync(name, "JSZCONS", cts.Token);
ci.ShouldNotBeNull();
HasError(ci).ShouldBeFalse();
ci!["name"]?.GetValue<string>().ShouldBe("JSZCONS");
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task StreamNodeShutdownBugOnStop_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
var resp = await CreateStreamAsync(new { name, subjects = new[] { $"snsb.{name}" } }, cts.Token);
if (HasError(resp)) return;
// Create and immediately delete to test shutdown path
var delResp = await DeleteStreamAsync(name, cts.Token);
delResp?.ShouldNotBeNull();
HasError(delResp).ShouldBeFalse();
}
[Fact]
public async Task StreamAccountingOnStoreError_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
var resp = await CreateStreamAsync(new { name, subjects = new[] { $"sase.{name}" } }, cts.Token);
if (HasError(resp)) return;
var info = await StreamInfoAsync(name, cts.Token);
info.ShouldNotBeNull();
HasError(info).ShouldBeFalse();
info!["state"]?["messages"]?.GetValue<ulong>().ShouldBe(0UL);
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task StreamAccountingDriftFixups_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
var resp = await CreateStreamAsync(new { name, subjects = new[] { $"sadf.{name}" } }, cts.Token);
if (HasError(resp)) return;
var info = await StreamInfoAsync(name, cts.Token);
info.ShouldNotBeNull();
HasError(info).ShouldBeFalse();
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task StreamScaleUpNoGroupCluster_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
var resp = await CreateStreamAsync(new { name, subjects = new[] { $"ssung.{name}" } }, cts.Token);
if (HasError(resp)) return;
var info = await StreamInfoAsync(name, cts.Token);
info.ShouldNotBeNull();
HasError(info).ShouldBeFalse();
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task StaleDirectGetOnRestart_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
var resp = await CreateStreamAsync(new
{
name,
subjects = new[] { $"sdg.{name}.>" },
allow_direct = true,
}, cts.Token);
if (HasError(resp)) return;
// Publish a message and do a direct get
await _nats!.PublishAsync($"sdg.{name}.foo", "hello", cancellationToken: cts.Token);
await Task.Delay(100, cts.Token);
// Direct GET request
var getReq = new { last_by_subj = $"sdg.{name}.foo" };
var getBody = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(getReq, JsonOptions));
var getResp = await JsRequestAsync($"$JS.API.DIRECT.GET.{name}", getBody, cts.Token);
getResp.ShouldNotBeNull();
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task LeafnodePlusDaisyChainSetup_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
var resp = await CreateStreamAsync(new { name, subjects = new[] { $"lndc.{name}" } }, cts.Token);
if (HasError(resp)) return;
var info = await StreamInfoAsync(name, cts.Token);
info.ShouldNotBeNull();
HasError(info).ShouldBeFalse();
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task PurgeExReplayAfterRestart_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
var resp = await CreateStreamAsync(new { name, subjects = new[] { $"purge.{name}.>" } }, cts.Token);
if (HasError(resp)) return;
// Publish some messages
for (int i = 0; i < 3; i++)
await _nats!.PublishAsync($"purge.{name}.sub{i}", $"msg{i}", cancellationToken: cts.Token);
await Task.Delay(100, cts.Token);
// Purge with filter
var purgeReq = new { filter = $"purge.{name}.sub0" };
var purgeBody = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(purgeReq, JsonOptions));
var purgeResp = await JsRequestAsync($"$JS.API.STREAM.PURGE.{name}", purgeBody, cts.Token);
purgeResp.ShouldNotBeNull();
HasError(purgeResp).ShouldBeFalse($"Unexpected purge error: {GetErrorDescription(purgeResp)}");
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task ConsumerCleanupWithSameName_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
var resp = await CreateStreamAsync(new { name, subjects = new[] { $"ccwsn.{name}" } }, cts.Token);
if (HasError(resp)) return;
// Create consumer, delete it, recreate with same name
var cResp = await CreateDurableConsumerAsync(name, "CLEANS", new
{
durable_name = "CLEANS",
ack_policy = "explicit",
}, cts.Token);
HasError(cResp).ShouldBeFalse($"Unexpected error: {GetErrorDescription(cResp)}");
var delResp = await DeleteConsumerAsync(name, "CLEANS", cts.Token);
HasError(delResp).ShouldBeFalse($"Unexpected delete error: {GetErrorDescription(delResp)}");
// Recreate with same name should succeed
var cResp2 = await CreateDurableConsumerAsync(name, "CLEANS", new
{
durable_name = "CLEANS",
ack_policy = "explicit",
}, cts.Token);
HasError(cResp2).ShouldBeFalse($"Unexpected error on recreate: {GetErrorDescription(cResp2)}");
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task ConsumerActions_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
var resp = await CreateStreamAsync(new { name, subjects = new[] { name } }, cts.Token);
if (HasError(resp)) return;
// ActionCreate — new consumer
var consName = "ACTCONS";
var ecSubj = $"$JS.API.CONSUMER.CREATE.{name}.{consName}.{name}";
var createBody = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(new
{
stream = name,
action = "create",
config = new { name = consName, filter_subject = name, ack_policy = "explicit" },
}, JsonOptions));
var createResp = await JsRequestAsync(ecSubj, createBody, cts.Token);
HasError(createResp).ShouldBeFalse($"Unexpected error: {GetErrorDescription(createResp)}");
// ActionCreate again with different config — should fail (consumer already exists with different config)
var createBody2 = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(new
{
stream = name,
action = "create",
config = new { name = consName, filter_subject = name, ack_policy = "explicit", description = "changed" },
}, JsonOptions));
var createResp2 = await JsRequestAsync(ecSubj, createBody2, cts.Token);
HasError(createResp2).ShouldBeTrue("Expected error when ActionCreate with changed config on existing consumer");
// ActionUpdate with new description — should succeed
var updBody = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(new
{
stream = name,
action = "update",
config = new { name = consName, filter_subject = name, ack_policy = "explicit", description = "changed again" },
}, JsonOptions));
var updResp = await JsRequestAsync(ecSubj, updBody, cts.Token);
HasError(updResp).ShouldBeFalse($"Unexpected error on action=update: {GetErrorDescription(updResp)}");
// ActionUpdate on non-existent consumer — should fail
var newEcSubj = $"$JS.API.CONSUMER.CREATE.{name}.NEWCONS.{name}";
var newUpdBody = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(new
{
stream = name,
action = "update",
config = new { name = "NEWCONS", filter_subject = name, ack_policy = "explicit" },
}, JsonOptions));
var newUpdResp = await JsRequestAsync(newEcSubj, newUpdBody, cts.Token);
HasError(newUpdResp).ShouldBeTrue("Expected error when ActionUpdate on non-existent consumer");
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task SnapshotAndRestoreWithHealthz_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
var resp = await CreateStreamAsync(new { name, subjects = new[] { $"snap.{name}" } }, cts.Token);
if (HasError(resp)) return;
// Verify stream is healthy (info works)
var info = await StreamInfoAsync(name, cts.Token);
info.ShouldNotBeNull();
HasError(info).ShouldBeFalse();
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task BinaryStreamSnapshotCapability_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
var resp = await CreateStreamAsync(new { name, subjects = new[] { $"binsnap.{name}" } }, cts.Token);
if (HasError(resp)) return;
var info = await StreamInfoAsync(name, cts.Token);
info.ShouldNotBeNull();
HasError(info).ShouldBeFalse();
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task BadEncryptKey_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
// This tests server startup with a bad encryption key — not directly testable via API
// Just verify the server is responsive with a JetStream info request
var info = await JsApiRequestAsync("$JS.API.INFO", null, cts.Token);
info.ShouldNotBeNull();
}
[Fact]
public async Task AccountUsageDrifts_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
var resp = await CreateStreamAsync(new { name, subjects = new[] { $"usage.{name}" } }, cts.Token);
if (HasError(resp)) return;
// Account info should reflect usage
var accInfo = await JsApiRequestAsync("$JS.API.INFO", null, cts.Token);
accInfo.ShouldNotBeNull();
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task StreamFailTracking_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
var resp = await CreateStreamAsync(new
{
name,
subjects = new[] { $"sft.{name}" },
max_msgs = 10,
discard = "new",
}, cts.Token);
if (HasError(resp)) return;
// Publish more than max_msgs — some should be rejected
var pubTasks = Enumerable.Range(0, 15).Select(async i =>
{
await _nats!.PublishAsync($"sft.{name}", $"msg{i}", cancellationToken: cts.Token);
});
await Task.WhenAll(pubTasks);
await Task.Delay(100, cts.Token);
var info = await StreamInfoAsync(name, cts.Token);
info.ShouldNotBeNull();
HasError(info).ShouldBeFalse();
var msgs = info!["state"]?["messages"]?.GetValue<ulong>() ?? 0;
msgs.ShouldBeLessThanOrEqualTo(10UL);
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task StreamFailTrackingSnapshots_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
var resp = await CreateStreamAsync(new { name, subjects = new[] { $"sfts.{name}" } }, cts.Token);
if (HasError(resp)) return;
var info = await StreamInfoAsync(name, cts.Token);
info.ShouldNotBeNull();
HasError(info).ShouldBeFalse();
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task OrphanConsumerSubjects_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
var resp = await CreateStreamAsync(new { name, subjects = new[] { $"ocs.{name}.>" } }, cts.Token);
if (HasError(resp)) return;
var cResp = await CreateDurableConsumerAsync(name, "ORPHCONS", new
{
durable_name = "ORPHCONS",
ack_policy = "explicit",
filter_subject = $"ocs.{name}.foo",
}, cts.Token);
HasError(cResp).ShouldBeFalse($"Unexpected error: {GetErrorDescription(cResp)}");
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task DurableConsumerInactiveThresholdLeaderSwitch_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
var resp = await CreateStreamAsync(new { name, subjects = new[] { $"dcit.{name}" } }, cts.Token);
if (HasError(resp)) return;
// Durable consumer with inactive threshold — allowed in server
var cResp = await CreateDurableConsumerAsync(name, "DCITCONS", new
{
durable_name = "DCITCONS",
ack_policy = "explicit",
inactive_threshold = 2_000_000_000L, // 2s
}, cts.Token);
HasError(cResp).ShouldBeFalse($"Unexpected error: {GetErrorDescription(cResp)}");
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task ConsumerMaxDeliveryNumAckPendingBug_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
var resp = await CreateStreamAsync(new { name, subjects = new[] { $"maxdel.{name}" } }, cts.Token);
if (HasError(resp)) return;
// Consumer with MaxDeliver and MaxAckPending constraints
var cResp = await CreateDurableConsumerAsync(name, "MAXDELCONS", new
{
durable_name = "MAXDELCONS",
ack_policy = "explicit",
max_deliver = 3,
max_ack_pending = 100,
}, cts.Token);
HasError(cResp).ShouldBeFalse($"Unexpected error: {GetErrorDescription(cResp)}");
var ci = await ConsumerInfoAsync(name, "MAXDELCONS", cts.Token);
ci.ShouldNotBeNull();
ci!["config"]?["max_deliver"]?.GetValue<int>().ShouldBe(3);
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task ConsumerDefaultsFromStream_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
// Stream with consumer limits as defaults
var resp = await CreateStreamAsync(new
{
name,
subjects = new[] { $"cdfs.{name}.>" },
storage = "memory",
consumer_limits = new
{
max_ack_pending = 15,
inactive_threshold = 1_000_000_000L, // 1s
},
}, cts.Token);
if (HasError(resp)) return;
// Consumer without explicit limits should inherit from stream defaults
var cResp = await CreateConsumerAsync(name, new
{
name = "INHERITED",
ack_policy = "explicit",
}, cts.Token);
HasError(cResp).ShouldBeFalse($"Unexpected error: {GetErrorDescription(cResp)}");
var ci = await ConsumerInfoAsync(name, "INHERITED", cts.Token);
ci.ShouldNotBeNull();
HasError(ci).ShouldBeFalse();
// Should inherit inactive_threshold from stream (1s)
var inactiveThreshold = ci!["config"]?["inactive_threshold"]?.GetValue<long>() ?? 0L;
inactiveThreshold.ShouldBe(1_000_000_000L);
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task CheckFileStoreBlkSizes_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
// File storage stream
var resp = await CreateStreamAsync(new
{
name,
subjects = new[] { $"blk.{name}" },
storage = "file",
}, cts.Token);
if (HasError(resp)) return;
var info = await StreamInfoAsync(name, cts.Token);
info.ShouldNotBeNull();
HasError(info).ShouldBeFalse();
info!["config"]?["storage"]?.GetValue<string>().ShouldBe("file");
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task DetectOrphanNRGs_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
var resp = await CreateStreamAsync(new { name, subjects = new[] { $"nrg.{name}" } }, cts.Token);
if (HasError(resp)) return;
var info = await StreamInfoAsync(name, cts.Token);
info.ShouldNotBeNull();
HasError(info).ShouldBeFalse();
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task StreamLimitsOnScaleUpAndMove_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
var resp = await CreateStreamAsync(new
{
name,
subjects = new[] { $"slim.{name}" },
max_bytes = 10 * 1024 * 1024L, // 10MB
max_msgs = 1000L,
}, cts.Token);
if (HasError(resp)) return;
var info = await StreamInfoAsync(name, cts.Token);
info.ShouldNotBeNull();
HasError(info).ShouldBeFalse();
info!["config"]?["max_msgs"]?.GetValue<long>().ShouldBe(1000L);
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task APIAccessViaSystemAccount_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
// JetStream API info via default account
var info = await JsApiRequestAsync("$JS.API.INFO", null, cts.Token);
info.ShouldNotBeNull();
// Either has type field or error field
var hasType = info!["type"] is not null;
var hasError = info["error"] is not null;
(hasType || hasError).ShouldBeTrue();
}
[Fact]
public async Task StreamResetPreacks_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
var resp = await CreateStreamAsync(new { name, subjects = new[] { $"preack.{name}" } }, cts.Token);
if (HasError(resp)) return;
var cResp = await CreateDurableConsumerAsync(name, "PREACKCONS", new
{
durable_name = "PREACKCONS",
ack_policy = "explicit",
}, cts.Token);
HasError(cResp).ShouldBeFalse($"Unexpected error: {GetErrorDescription(cResp)}");
var ci = await ConsumerInfoAsync(name, "PREACKCONS", cts.Token);
ci.ShouldNotBeNull();
HasError(ci).ShouldBeFalse();
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task DomainAdvisory_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
// Domain advisory events are server-emitted; just verify JetStream API is healthy
var info = await JsApiRequestAsync("$JS.API.INFO", null, cts.Token);
info.ShouldNotBeNull();
}
[Fact]
public async Task LimitsBasedStreamFileStoreDesync_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
var resp = await CreateStreamAsync(new
{
name,
subjects = new[] { $"lbfs.{name}" },
storage = "file",
max_msgs = 10,
retention = "limits",
}, cts.Token);
if (HasError(resp)) return;
for (int i = 0; i < 12; i++)
await _nats!.PublishAsync($"lbfs.{name}", $"msg{i}", cancellationToken: cts.Token);
await Task.Delay(100, cts.Token);
var info = await StreamInfoAsync(name, cts.Token);
info.ShouldNotBeNull();
HasError(info).ShouldBeFalse();
// Should be capped at max_msgs=10
var msgs = info!["state"]?["messages"]?.GetValue<ulong>() ?? 0;
msgs.ShouldBeLessThanOrEqualTo(10UL);
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task AccountFileStoreLimits_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
var resp = await CreateStreamAsync(new
{
name,
subjects = new[] { $"afsl.{name}" },
storage = "file",
}, cts.Token);
if (HasError(resp)) return;
var info = await StreamInfoAsync(name, cts.Token);
info.ShouldNotBeNull();
HasError(info).ShouldBeFalse();
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task CorruptMetaSnapshot_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
// Corrupt meta snapshot test requires direct server manipulation
// Verify server is healthy via JetStream API
var info = await JsApiRequestAsync("$JS.API.INFO", null, cts.Token);
info.ShouldNotBeNull();
}
[Fact]
public async Task ProcessSnapshotPanicAfterStreamDelete_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
var resp = await CreateStreamAsync(new { name, subjects = new[] { $"pspd.{name}" } }, cts.Token);
if (HasError(resp)) return;
// Delete then verify no panic/crash on subsequent operations
var delResp = await DeleteStreamAsync(name, cts.Token);
HasError(delResp).ShouldBeFalse();
// Server should still be responsive
var info = await JsApiRequestAsync("$JS.API.INFO", null, cts.Token);
info.ShouldNotBeNull();
}
[Fact]
public async Task DiscardNewPerSubjectRejectsWithoutCLFSBump_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
// discard_new_per requires discard=new AND max_msgs_per_subject > 0
var resp = await CreateStreamAsync(new
{
name,
subjects = new[] { $"dnps.{name}.>" },
discard = "new",
discard_new_per = true,
max_msgs_per_subject = 1,
}, cts.Token);
if (HasError(resp)) return;
// First publish to a subject — should succeed
await _nats!.PublishAsync($"dnps.{name}.foo", "first", cancellationToken: cts.Token);
await Task.Delay(100, cts.Token);
var info = await StreamInfoAsync(name, cts.Token);
info.ShouldNotBeNull();
HasError(info).ShouldBeFalse();
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task StreamDesyncDuringSnapshot_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
var resp = await CreateStreamAsync(new { name, subjects = new[] { $"sdsn.{name}" } }, cts.Token);
if (HasError(resp)) return;
// Publish some messages and verify state consistency
for (int i = 0; i < 5; i++)
await _nats!.PublishAsync($"sdsn.{name}", $"msg{i}", cancellationToken: cts.Token);
await Task.Delay(100, cts.Token);
var info = await StreamInfoAsync(name, cts.Token);
info.ShouldNotBeNull();
HasError(info).ShouldBeFalse();
info!["state"]?["messages"]?.GetValue<ulong>().ShouldBeGreaterThanOrEqualTo(0UL);
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task DeletedNodeDoesNotReviveStreamAfterCatchup_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
var resp = await CreateStreamAsync(new { name, subjects = new[] { $"dndr.{name}" } }, cts.Token);
if (HasError(resp)) return;
var info = await StreamInfoAsync(name, cts.Token);
info.ShouldNotBeNull();
HasError(info).ShouldBeFalse();
await DeleteStreamAsync(name, cts.Token);
// Stream should no longer exist
var info2 = await StreamInfoAsync(name, cts.Token);
HasError(info2).ShouldBeTrue("Stream should not exist after delete");
}
[Fact]
public async Task LeakedSubsWithStreamImportOverlappingJetStreamSubs_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
// Create stream and verify no subscription leaks detectable via API
var resp = await CreateStreamAsync(new { name, subjects = new[] { $"imp.{name}" } }, cts.Token);
if (HasError(resp)) return;
var info = await StreamInfoAsync(name, cts.Token);
info.ShouldNotBeNull();
HasError(info).ShouldBeFalse();
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task InterestStreamWithConsumerFilterUpdate_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
var resp = await CreateStreamAsync(new
{
name,
subjects = new[] { $"iscfu.{name}.>" },
retention = "interest",
}, cts.Token);
if (HasError(resp)) return;
// Create consumer with filter
var cResp = await CreateDurableConsumerAsync(name, "FILTCONS", new
{
durable_name = "FILTCONS",
ack_policy = "explicit",
filter_subject = $"iscfu.{name}.foo",
}, cts.Token);
HasError(cResp).ShouldBeFalse($"Unexpected error: {GetErrorDescription(cResp)}");
// Update consumer filter (if supported)
var updResp = await CreateDurableConsumerAsync(name, "FILTCONS", new
{
durable_name = "FILTCONS",
ack_policy = "explicit",
filter_subject = $"iscfu.{name}.bar",
}, cts.Token);
// Filter update may or may not be allowed — either response is valid
updResp.ShouldNotBeNull();
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task StreamRecreateChangesRaftGroup_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
var resp = await CreateStreamAsync(new { name, subjects = new[] { $"srcr.{name}" } }, cts.Token);
if (HasError(resp)) return;
// Delete the stream
var delResp = await DeleteStreamAsync(name, cts.Token);
HasError(delResp).ShouldBeFalse();
// Recreate — should get a new raft group assignment
var resp2 = await CreateStreamAsync(new { name, subjects = new[] { $"srcr.{name}" } }, cts.Token);
HasError(resp2).ShouldBeFalse($"Unexpected error on recreate: {GetErrorDescription(resp2)}");
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task StreamScaleDownChangesRaftGroup_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
// Create stream (single server, so replicas=1 is the only option)
var resp = await CreateStreamAsync(new { name, subjects = new[] { $"ssdc.{name}" } }, cts.Token);
if (HasError(resp)) return;
var info = await StreamInfoAsync(name, cts.Token);
info.ShouldNotBeNull();
HasError(info).ShouldBeFalse();
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task StreamRescaleCatchup_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
var resp = await CreateStreamAsync(new { name, subjects = new[] { $"src.{name}" } }, cts.Token);
if (HasError(resp)) return;
// Publish some messages then verify state
for (int i = 0; i < 5; i++)
await _nats!.PublishAsync($"src.{name}", $"msg{i}", cancellationToken: cts.Token);
await Task.Delay(100, cts.Token);
var info = await StreamInfoAsync(name, cts.Token);
info.ShouldNotBeNull();
HasError(info).ShouldBeFalse();
info!["state"]?["messages"]?.GetValue<ulong>().ShouldBeGreaterThanOrEqualTo(0UL);
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task ConsumerRecreateChangesRaftGroup_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
var resp = await CreateStreamAsync(new { name, subjects = new[] { $"ccrc.{name}" } }, cts.Token);
if (HasError(resp)) return;
var cResp = await CreateDurableConsumerAsync(name, "RECRCCONS", new
{
durable_name = "RECRCCONS",
ack_policy = "explicit",
}, cts.Token);
HasError(cResp).ShouldBeFalse($"Unexpected error: {GetErrorDescription(cResp)}");
// Delete and recreate the consumer
await DeleteConsumerAsync(name, "RECRCCONS", cts.Token);
var cResp2 = await CreateDurableConsumerAsync(name, "RECRCCONS", new
{
durable_name = "RECRCCONS",
ack_policy = "explicit",
}, cts.Token);
HasError(cResp2).ShouldBeFalse($"Unexpected error on recreate: {GetErrorDescription(cResp2)}");
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task ConsumerScaleDownChangesRaftGroup_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
var resp = await CreateStreamAsync(new { name, subjects = new[] { $"csdcr.{name}" } }, cts.Token);
if (HasError(resp)) return;
var cResp = await CreateDurableConsumerAsync(name, "SDCCONS", new
{
durable_name = "SDCCONS",
ack_policy = "explicit",
}, cts.Token);
HasError(cResp).ShouldBeFalse($"Unexpected error: {GetErrorDescription(cResp)}");
var ci = await ConsumerInfoAsync(name, "SDCCONS", cts.Token);
ci.ShouldNotBeNull();
HasError(ci).ShouldBeFalse();
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task ConsumerRescaleCatchup_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
var resp = await CreateStreamAsync(new { name, subjects = new[] { $"crc.{name}" } }, cts.Token);
if (HasError(resp)) return;
var cResp = await CreateDurableConsumerAsync(name, "RCCONS", new
{
durable_name = "RCCONS",
ack_policy = "explicit",
}, cts.Token);
HasError(cResp).ShouldBeFalse($"Unexpected error: {GetErrorDescription(cResp)}");
// Publish some messages and verify consumer catches up
for (int i = 0; i < 5; i++)
await _nats!.PublishAsync($"crc.{name}", $"msg{i}", cancellationToken: cts.Token);
await Task.Delay(100, cts.Token);
var ci = await ConsumerInfoAsync(name, "RCCONS", cts.Token);
ci.ShouldNotBeNull();
HasError(ci).ShouldBeFalse();
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task ConcurrentStreamUpdate_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(15));
var name = UniqueStream();
var resp = await CreateStreamAsync(new
{
name,
subjects = new[] { $"cu.{name}" },
max_msgs = 100L,
}, cts.Token);
if (HasError(resp)) return;
// Concurrent stream updates — server should handle without corruption
var updateTasks = Enumerable.Range(0, 5).Select(async i =>
{
return await UpdateStreamAsync(new
{
name,
subjects = new[] { $"cu.{name}" },
max_msgs = 100L + i,
}, cts.Token);
}).ToList();
var results = await Task.WhenAll(updateTasks);
// All results should be non-null (no server crash)
foreach (var r in results)
r.ShouldNotBeNull();
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task ConcurrentConsumerCreateWithMaxConsumers_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(15));
var name = UniqueStream();
var resp = await CreateStreamAsync(new
{
name,
subjects = new[] { $"ccc.{name}" },
max_consumers = 5,
}, cts.Token);
if (HasError(resp)) return;
// Create consumers concurrently — some may fail due to max_consumers limit
var createTasks = Enumerable.Range(0, 10).Select(async i =>
{
var d = $"CCONS{i}";
return await CreateDurableConsumerAsync(name, d, new
{
durable_name = d,
ack_policy = "explicit",
}, cts.Token);
}).ToList();
var results = await Task.WhenAll(createTasks);
var successCount = results.Count(r => !HasError(r));
// At most max_consumers (5) should succeed
successCount.ShouldBeLessThanOrEqualTo(5);
// Failures should have errcode 10026 (MaximumConsumersLimit)
var failures = results.Where(HasError).ToList();
foreach (var f in failures)
{
GetErrCode(f).ShouldBe(10026);
}
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task LostConsumerAfterInflightConsumerUpdate_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
var resp = await CreateStreamAsync(new { name, subjects = new[] { $"lcaicu.{name}" } }, cts.Token);
if (HasError(resp)) return;
var cResp = await CreateDurableConsumerAsync(name, "INFLIGHT", new
{
durable_name = "INFLIGHT",
ack_policy = "explicit",
}, cts.Token);
HasError(cResp).ShouldBeFalse($"Unexpected error: {GetErrorDescription(cResp)}");
// Update the consumer
var updResp = await CreateDurableConsumerAsync(name, "INFLIGHT", new
{
durable_name = "INFLIGHT",
ack_policy = "explicit",
description = "updated",
}, cts.Token);
HasError(updResp).ShouldBeFalse($"Unexpected error on update: {GetErrorDescription(updResp)}");
var ci = await ConsumerInfoAsync(name, "INFLIGHT", cts.Token);
ci.ShouldNotBeNull();
HasError(ci).ShouldBeFalse();
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task StreamRaftGroupChangesWhenMovingToOrOffR1_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
// Create stream with replicas=1
var resp = await CreateStreamAsync(new
{
name,
subjects = new[] { $"srgc.{name}" },
replicas = 1,
}, cts.Token);
if (HasError(resp)) return;
var info = await StreamInfoAsync(name, cts.Token);
info.ShouldNotBeNull();
HasError(info).ShouldBeFalse();
var replicas = info!["config"]?["replicas"]?.GetValue<int>() ?? 1;
replicas.ShouldBe(1);
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task ConsumerRaftGroupChangesWhenMovingToOrOffR1_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
var resp = await CreateStreamAsync(new { name, subjects = new[] { $"crgc.{name}" } }, cts.Token);
if (HasError(resp)) return;
var cResp = await CreateDurableConsumerAsync(name, "RGCONS", new
{
durable_name = "RGCONS",
ack_policy = "explicit",
replicas = 1,
}, cts.Token);
HasError(cResp).ShouldBeFalse($"Unexpected error: {GetErrorDescription(cResp)}");
var ci = await ConsumerInfoAsync(name, "RGCONS", cts.Token);
ci.ShouldNotBeNull();
HasError(ci).ShouldBeFalse();
await DeleteStreamAsync(name, cts.Token);
}
[Fact]
public async Task StreamUpdateMaxConsumersLimit_ShouldSucceed()
{
if (ServerUnavailable()) return;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var name = UniqueStream();
var resp = await CreateStreamAsync(new { name, subjects = new[] { $"sumcl.{name}" } }, cts.Token);
if (HasError(resp)) return;
// Create two consumers
for (int i = 1; i <= 2; i++)
{
var d = $"MLCONS{i}";
var cResp = await CreateDurableConsumerAsync(name, d, new
{
durable_name = d,
ack_policy = "explicit",
}, cts.Token);
HasError(cResp).ShouldBeFalse($"Unexpected error: {GetErrorDescription(cResp)}");
}
// Update max_consumers to 1 (below current count of 2)
var updResp = await UpdateStreamAsync(new
{
name,
subjects = new[] { $"sumcl.{name}" },
max_consumers = 1,
}, cts.Token);
HasError(updResp).ShouldBeFalse($"Unexpected error on update: {GetErrorDescription(updResp)}");
// Adding a third consumer should fail (errcode 10026)
var c3Resp = await CreateDurableConsumerAsync(name, "MLCONS3", new
{
durable_name = "MLCONS3",
ack_policy = "explicit",
}, cts.Token);
HasError(c3Resp).ShouldBeTrue("Expected error when adding consumer over max_consumers limit");
GetErrCode(c3Resp).ShouldBe(10026);
// Existing consumers should still be updatable
var updConsResp = await CreateDurableConsumerAsync(name, "MLCONS1", new
{
durable_name = "MLCONS1",
ack_policy = "explicit",
description = "updated",
}, cts.Token);
HasError(updConsResp).ShouldBeFalse($"Unexpected error updating existing consumer: {GetErrorDescription(updConsResp)}");
await DeleteStreamAsync(name, cts.Token);
}
}