refactor: extract NATS.Server.JetStream.Tests project

Move 225 JetStream-related test files from NATS.Server.Tests into a
dedicated NATS.Server.JetStream.Tests project. This includes root-level
JetStream*.cs files, storage test files (FileStore, MemStore,
StreamStoreContract), and the full JetStream/ subfolder tree (Api,
Cluster, Consumers, MirrorSource, Snapshots, Storage, Streams).

Updated all namespaces, added InternalsVisibleTo, registered in the
solution file, and added the JETSTREAM_INTEGRATION_MATRIX define.
This commit is contained in:
Joseph Doherty
2026-03-12 15:58:10 -04:00
parent 36b9dfa654
commit 78b4bc2486
228 changed files with 253 additions and 227 deletions

View File

@@ -0,0 +1,193 @@
// Go reference: jetstream_api.go:1200-1350 — stream purge supports options: subject filter,
// sequence cutoff, and keep-last-N. Combinations like filter+keep allow keeping the last N
// messages per matching subject.
using System.Text;
using NATS.Server.JetStream;
using NATS.Server.JetStream.Api;
namespace NATS.Server.JetStream.Tests.JetStream.Api;
public class StreamPurgeOptionsTests
{
private static JetStreamApiRouter CreateRouterWithStream(string streamName, string subjectPattern, out StreamManager streamManager)
{
streamManager = new StreamManager();
var consumerManager = new ConsumerManager();
var router = new JetStreamApiRouter(streamManager, consumerManager);
var payload = Encoding.UTF8.GetBytes($$$"""{"name":"{{{streamName}}}","subjects":["{{{subjectPattern}}}"]}""");
var result = router.Route($"$JS.API.STREAM.CREATE.{streamName}", payload);
result.Error.ShouldBeNull();
return router;
}
private static async Task PublishAsync(StreamManager streamManager, string subject, string payload)
{
var stream = streamManager.FindBySubject(subject);
stream.ShouldNotBeNull();
await stream.Store.AppendAsync(subject, Encoding.UTF8.GetBytes(payload), default);
}
/// <summary>
/// Purge with no options removes all messages and returns the count.
/// Go reference: jetstream_api.go — basic purge with empty request body.
/// </summary>
[Fact]
public async Task Purge_NoOptions_RemovesAll()
{
var router = CreateRouterWithStream("TEST", "test.>", out var sm);
await PublishAsync(sm, "test.a", "1");
await PublishAsync(sm, "test.b", "2");
await PublishAsync(sm, "test.c", "3");
var result = router.Route("$JS.API.STREAM.PURGE.TEST", Encoding.UTF8.GetBytes("{}"));
result.Error.ShouldBeNull();
result.Success.ShouldBeTrue();
result.Purged.ShouldBe(3UL);
var state = await sm.GetStateAsync("TEST", default);
state.Messages.ShouldBe(0UL);
}
/// <summary>
/// Purge with a subject filter removes only messages matching the pattern.
/// Go reference: jetstream_api.go:1200-1350 — filter option.
/// </summary>
[Fact]
public async Task Purge_WithSubjectFilter_RemovesOnlyMatching()
{
var router = CreateRouterWithStream("TEST", ">", out var sm);
await PublishAsync(sm, "orders.a", "1");
await PublishAsync(sm, "orders.b", "2");
await PublishAsync(sm, "logs.x", "3");
await PublishAsync(sm, "orders.c", "4");
var payload = Encoding.UTF8.GetBytes("""{"filter":"orders.*"}""");
var result = router.Route("$JS.API.STREAM.PURGE.TEST", payload);
result.Error.ShouldBeNull();
result.Success.ShouldBeTrue();
result.Purged.ShouldBe(3UL);
var state = await sm.GetStateAsync("TEST", default);
state.Messages.ShouldBe(1UL);
}
/// <summary>
/// Purge with seq option removes all messages with sequence strictly less than the given value.
/// Go reference: jetstream_api.go:1200-1350 — seq option.
/// </summary>
[Fact]
public async Task Purge_WithSeq_RemovesBelowSequence()
{
var router = CreateRouterWithStream("TEST", "test.>", out var sm);
await PublishAsync(sm, "test.a", "1"); // seq 1
await PublishAsync(sm, "test.b", "2"); // seq 2
await PublishAsync(sm, "test.c", "3"); // seq 3
await PublishAsync(sm, "test.d", "4"); // seq 4
await PublishAsync(sm, "test.e", "5"); // seq 5
// Remove all messages with seq < 4 (i.e., sequences 1, 2, 3).
var payload = Encoding.UTF8.GetBytes("""{"seq":4}""");
var result = router.Route("$JS.API.STREAM.PURGE.TEST", payload);
result.Error.ShouldBeNull();
result.Success.ShouldBeTrue();
result.Purged.ShouldBe(3UL);
var state = await sm.GetStateAsync("TEST", default);
state.Messages.ShouldBe(2UL);
}
/// <summary>
/// Purge with keep option retains the last N messages globally.
/// Go reference: jetstream_api.go:1200-1350 — keep option.
/// </summary>
[Fact]
public async Task Purge_WithKeep_KeepsLastN()
{
var router = CreateRouterWithStream("TEST", "test.>", out var sm);
await PublishAsync(sm, "test.a", "1"); // seq 1
await PublishAsync(sm, "test.b", "2"); // seq 2
await PublishAsync(sm, "test.c", "3"); // seq 3
await PublishAsync(sm, "test.d", "4"); // seq 4
await PublishAsync(sm, "test.e", "5"); // seq 5
// Keep the last 2 messages (seq 4, 5); purge 1, 2, 3.
var payload = Encoding.UTF8.GetBytes("""{"keep":2}""");
var result = router.Route("$JS.API.STREAM.PURGE.TEST", payload);
result.Error.ShouldBeNull();
result.Success.ShouldBeTrue();
result.Purged.ShouldBe(3UL);
var state = await sm.GetStateAsync("TEST", default);
state.Messages.ShouldBe(2UL);
}
/// <summary>
/// Purge with both filter and keep retains the last N messages per matching subject.
/// Go reference: jetstream_api.go:1200-1350 — filter+keep combination.
/// </summary>
[Fact]
public async Task Purge_FilterAndKeep_KeepsLastNPerFilter()
{
var router = CreateRouterWithStream("TEST", ">", out var sm);
// Publish multiple messages on two subjects.
await PublishAsync(sm, "orders.a", "o1"); // seq 1
await PublishAsync(sm, "orders.a", "o2"); // seq 2
await PublishAsync(sm, "orders.a", "o3"); // seq 3
await PublishAsync(sm, "logs.x", "l1"); // seq 4 — not matching filter
await PublishAsync(sm, "orders.b", "ob1"); // seq 5
await PublishAsync(sm, "orders.b", "ob2"); // seq 6
// Keep last 1 per matching subject "orders.*".
// orders.a has 3 msgs -> keep seq 3, purge seq 1, 2
// orders.b has 2 msgs -> keep seq 6, purge seq 5
// logs.x is unaffected (does not match filter)
var payload = Encoding.UTF8.GetBytes("""{"filter":"orders.*","keep":1}""");
var result = router.Route("$JS.API.STREAM.PURGE.TEST", payload);
result.Error.ShouldBeNull();
result.Success.ShouldBeTrue();
result.Purged.ShouldBe(3UL);
var state = await sm.GetStateAsync("TEST", default);
// Remaining: orders.a seq 3, logs.x seq 4, orders.b seq 6 = 3 messages
state.Messages.ShouldBe(3UL);
}
/// <summary>
/// Purge on a non-existent stream returns a 404 not-found error.
/// Go reference: jetstream_api.go — stream not found.
/// </summary>
[Fact]
public void Purge_InvalidStream_ReturnsNotFound()
{
var streamManager = new StreamManager();
var consumerManager = new ConsumerManager();
var router = new JetStreamApiRouter(streamManager, consumerManager);
var result = router.Route("$JS.API.STREAM.PURGE.NONEXISTENT", Encoding.UTF8.GetBytes("{}"));
result.Error.ShouldNotBeNull();
result.Error!.Code.ShouldBe(404);
}
/// <summary>
/// Purge on an empty stream returns success with zero purged count.
/// Go reference: jetstream_api.go — purge on empty stream.
/// </summary>
[Fact]
public void Purge_EmptyStream_ReturnsZeroPurged()
{
var router = CreateRouterWithStream("TEST", "test.>", out _);
var result = router.Route("$JS.API.STREAM.PURGE.TEST", Encoding.UTF8.GetBytes("{}"));
result.Error.ShouldBeNull();
result.Success.ShouldBeTrue();
result.Purged.ShouldBe(0UL);
}
}