Stream lifecycle, publish/ack, consumer delivery, retention policy, API endpoints, cluster formation, and leader failover tests ported from Go nats-server reference. 1006 total tests passing.
151 lines
6.0 KiB
C#
151 lines
6.0 KiB
C#
// Port of Go tests from golang/nats-server/server/jetstream_test.go
|
|
// TestJetStreamPubAck, TestJetStreamPublishDeDupe, TestJetStreamPublishExpect
|
|
|
|
using NATS.Server.JetStream;
|
|
using NATS.Server.JetStream.Models;
|
|
using NATS.Server.JetStream.Publish;
|
|
|
|
namespace NATS.Server.Tests.JetStream;
|
|
|
|
public class PublishAckParityTests
|
|
{
|
|
// Go ref: TestJetStreamPubAck (jetstream_test.go:354)
|
|
// Verifies that each published message returns a PubAck with the correct stream
|
|
// name and a monotonically incrementing sequence number.
|
|
[Fact]
|
|
public async Task PubAck_stream_name_and_incrementing_seq_are_returned()
|
|
{
|
|
await using var fixture = await JetStreamApiFixture.StartWithStreamAsync("PUBACK", "foo");
|
|
|
|
for (var i = 1UL; i <= 5UL; i++)
|
|
{
|
|
var ack = await fixture.PublishAndGetAckAsync("foo", "HELLO");
|
|
ack.Stream.ShouldBe("PUBACK");
|
|
ack.Seq.ShouldBe(i);
|
|
ack.ErrorCode.ShouldBeNull();
|
|
}
|
|
}
|
|
|
|
// Go ref: TestJetStreamPublishDeDupe (jetstream_test.go:2657) — first block
|
|
// When the same Nats-Msg-Id is published twice within the duplicate window the
|
|
// server returns the original sequence and does not store a second message.
|
|
[Fact]
|
|
public async Task Duplicate_msgid_within_window_returns_same_sequence()
|
|
{
|
|
var streamManager = new StreamManager();
|
|
streamManager.CreateOrUpdate(new StreamConfig
|
|
{
|
|
Name = "DEDUPE",
|
|
Subjects = ["foo.*"],
|
|
DuplicateWindowMs = 2_000,
|
|
}).Error.ShouldBeNull();
|
|
|
|
var publisher = new JetStreamPublisher(streamManager);
|
|
|
|
// First publish — should store at seq 1
|
|
publisher.TryCaptureWithOptions("foo.1", "Hello DeDupe!"u8.ToArray(),
|
|
new PublishOptions { MsgId = "AA" }, out var first).ShouldBeTrue();
|
|
first.ErrorCode.ShouldBeNull();
|
|
first.Seq.ShouldBe(1UL);
|
|
|
|
// Second publish — same MsgId within window, should return the original seq
|
|
publisher.TryCaptureWithOptions("foo.1", "Hello DeDupe!"u8.ToArray(),
|
|
new PublishOptions { MsgId = "AA" }, out var second).ShouldBeTrue();
|
|
second.Seq.ShouldBe(first.Seq);
|
|
|
|
// Stream should still contain only one message
|
|
var state = await streamManager.GetStateAsync("DEDUPE", default);
|
|
state.Messages.ShouldBe(1UL);
|
|
}
|
|
|
|
// Go ref: TestJetStreamPublishDeDupe (jetstream_test.go:2728) — window-expiry block
|
|
// After the duplicate window has elapsed the same MsgId is treated as a new publish
|
|
// and gets a new, higher sequence number.
|
|
[Fact]
|
|
public async Task Duplicate_msgid_after_window_expiry_creates_new_message()
|
|
{
|
|
var streamManager = new StreamManager();
|
|
streamManager.CreateOrUpdate(new StreamConfig
|
|
{
|
|
Name = "DEDUPE2",
|
|
Subjects = ["bar.*"],
|
|
DuplicateWindowMs = 30,
|
|
}).Error.ShouldBeNull();
|
|
|
|
var publisher = new JetStreamPublisher(streamManager);
|
|
|
|
publisher.TryCaptureWithOptions("bar.1", "first"u8.ToArray(),
|
|
new PublishOptions { MsgId = "M1" }, out var first).ShouldBeTrue();
|
|
first.ErrorCode.ShouldBeNull();
|
|
|
|
// Wait for the duplicate window to expire
|
|
await Task.Delay(60);
|
|
|
|
// Same MsgId after window — should be treated as a new message
|
|
publisher.TryCaptureWithOptions("bar.1", "after-window"u8.ToArray(),
|
|
new PublishOptions { MsgId = "M1" }, out var third).ShouldBeTrue();
|
|
third.ErrorCode.ShouldBeNull();
|
|
third.Seq.ShouldBeGreaterThan(first.Seq);
|
|
|
|
// Both messages should now be stored
|
|
var state = await streamManager.GetStateAsync("DEDUPE2", default);
|
|
state.Messages.ShouldBe(2UL);
|
|
}
|
|
|
|
// Go ref: TestJetStreamPublishDeDupe (jetstream_test.go:2716) — four-distinct-ids block
|
|
// Multiple distinct MsgIds within the window are all stored as separate messages.
|
|
[Fact]
|
|
public async Task Distinct_msgids_within_window_each_stored_as_separate_message()
|
|
{
|
|
var streamManager = new StreamManager();
|
|
streamManager.CreateOrUpdate(new StreamConfig
|
|
{
|
|
Name = "DEDUPED",
|
|
Subjects = ["foo.*"],
|
|
DuplicateWindowMs = 2_000,
|
|
}).Error.ShouldBeNull();
|
|
|
|
var publisher = new JetStreamPublisher(streamManager);
|
|
var ids = new[] { "AA", "BB", "CC", "ZZ" };
|
|
|
|
for (var i = 0; i < ids.Length; i++)
|
|
{
|
|
publisher.TryCaptureWithOptions($"foo.{i + 1}", "Hello DeDupe!"u8.ToArray(),
|
|
new PublishOptions { MsgId = ids[i] }, out var ack).ShouldBeTrue();
|
|
ack.ErrorCode.ShouldBeNull();
|
|
ack.Seq.ShouldBe((ulong)(i + 1));
|
|
}
|
|
|
|
var state = await streamManager.GetStateAsync("DEDUPED", default);
|
|
state.Messages.ShouldBe(4UL);
|
|
|
|
// Re-sending the same MsgIds must NOT increase the message count
|
|
foreach (var id in ids)
|
|
{
|
|
publisher.TryCaptureWithOptions("foo.1", "Hello DeDupe!"u8.ToArray(),
|
|
new PublishOptions { MsgId = id }, out _).ShouldBeTrue();
|
|
}
|
|
|
|
state = await streamManager.GetStateAsync("DEDUPED", default);
|
|
state.Messages.ShouldBe(4UL);
|
|
}
|
|
|
|
// Go ref: TestJetStreamPublishExpect (jetstream_test.go:2817) — expected-last-seq block
|
|
// Publishing with an ExpectedLastSeq that does not match the current last sequence
|
|
// of the stream must return error code 10071.
|
|
[Fact]
|
|
public async Task Expected_last_seq_mismatch_returns_error_code_10071()
|
|
{
|
|
await using var fixture = await JetStreamApiFixture.StartWithStreamAsync("EXPECT", "foo.*");
|
|
|
|
// Publish one message so the stream has last seq = 1
|
|
var first = await fixture.PublishAndGetAckAsync("foo.bar", "HELLO");
|
|
first.Seq.ShouldBe(1UL);
|
|
first.ErrorCode.ShouldBeNull();
|
|
|
|
// Expect last seq = 10 — this must fail because actual is 1
|
|
var bad = await fixture.PublishWithExpectedLastSeqAsync("foo.bar", "HELLO", expectedLastSeq: 10);
|
|
bad.ErrorCode.ShouldBe(10071);
|
|
}
|
|
}
|