Files
natsdotnet/tests/NATS.Server.JetStream.Tests/JetStream/PublishAckParityTests.cs
Joseph Doherty 78b4bc2486 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.
2026-03-12 15:58:10 -04:00

152 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;
using NATS.Server.TestUtilities;
namespace NATS.Server.JetStream.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);
}
}