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.
572 lines
22 KiB
C#
572 lines
22 KiB
C#
// Ported from golang/nats-server/server/jetstream_test.go
|
|
// Publish/Subscribe: basic pub/sub, message acknowledgment, replay, headers, sequence tracking
|
|
|
|
using System.Text;
|
|
using NATS.Server.JetStream;
|
|
using NATS.Server.JetStream.Api;
|
|
using NATS.Server.JetStream.Models;
|
|
using NATS.Server.JetStream.Publish;
|
|
using NATS.Server.TestUtilities;
|
|
|
|
namespace NATS.Server.JetStream.Tests.JetStream;
|
|
|
|
public class JetStreamPubSubTests
|
|
{
|
|
// Go: TestJetStreamBasicAckPublish server/jetstream_test.go:710
|
|
[Fact]
|
|
public async Task Publish_returns_puback_with_stream_and_sequence()
|
|
{
|
|
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("ORDERS", "orders.*");
|
|
var ack = await fx.PublishAndGetAckAsync("orders.created", "payload");
|
|
|
|
ack.Stream.ShouldBe("ORDERS");
|
|
ack.Seq.ShouldBe(1UL);
|
|
ack.ErrorCode.ShouldBeNull();
|
|
}
|
|
|
|
// Go: TestJetStreamPubAck server/jetstream_test.go:298
|
|
[Fact]
|
|
public async Task Multiple_publishes_increment_sequence()
|
|
{
|
|
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("SEQ", "seq.>");
|
|
|
|
var ack1 = await fx.PublishAndGetAckAsync("seq.a", "1");
|
|
ack1.Seq.ShouldBe(1UL);
|
|
|
|
var ack2 = await fx.PublishAndGetAckAsync("seq.b", "2");
|
|
ack2.Seq.ShouldBe(2UL);
|
|
|
|
var ack3 = await fx.PublishAndGetAckAsync("seq.c", "3");
|
|
ack3.Seq.ShouldBe(3UL);
|
|
}
|
|
|
|
// Go: TestJetStreamPublishDeDupe server/jetstream_test.go:2533
|
|
[Fact]
|
|
public async Task Duplicate_msg_id_is_rejected()
|
|
{
|
|
await using var fx = await JetStreamApiFixture.StartWithStreamConfigAsync(new StreamConfig
|
|
{
|
|
Name = "DEDUP",
|
|
Subjects = ["dedup.>"],
|
|
DuplicateWindowMs = 60_000,
|
|
});
|
|
|
|
var ack1 = await fx.PublishAndGetAckAsync("dedup.x", "first", msgId: "uniq-1");
|
|
ack1.ErrorCode.ShouldBeNull();
|
|
|
|
var ack2 = await fx.PublishAndGetAckAsync("dedup.x", "second", msgId: "uniq-1");
|
|
ack2.ErrorCode.ShouldNotBeNull();
|
|
}
|
|
|
|
// Go: TestJetStreamPublishExpect server/jetstream_test.go:2595
|
|
[Fact]
|
|
public async Task Publish_with_expected_last_seq_succeeds_when_matching()
|
|
{
|
|
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("EXP", "exp.>");
|
|
|
|
var ack1 = await fx.PublishAndGetAckAsync("exp.a", "1");
|
|
ack1.Seq.ShouldBe(1UL);
|
|
|
|
var ack2 = await fx.PublishWithExpectedLastSeqAsync("exp.b", "2", 1);
|
|
ack2.ErrorCode.ShouldBeNull();
|
|
}
|
|
|
|
// Go: TestJetStreamPublishExpect — mismatch
|
|
[Fact]
|
|
public async Task Publish_with_wrong_expected_last_seq_fails()
|
|
{
|
|
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("EXPF", "expf.>");
|
|
_ = await fx.PublishAndGetAckAsync("expf.a", "1");
|
|
|
|
var ack = await fx.PublishWithExpectedLastSeqAsync("expf.b", "2", 999);
|
|
ack.ErrorCode.ShouldNotBeNull();
|
|
}
|
|
|
|
// Go: TestJetStreamSubjectFiltering server/jetstream_test.go:1089
|
|
[Fact]
|
|
public async Task Publish_and_fetch_with_filter_subject()
|
|
{
|
|
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("FILT", "filt.>");
|
|
_ = await fx.CreateConsumerAsync("FILT", "C1", "filt.a");
|
|
|
|
_ = await fx.PublishAndGetAckAsync("filt.a", "match");
|
|
_ = await fx.PublishAndGetAckAsync("filt.b", "no-match");
|
|
_ = await fx.PublishAndGetAckAsync("filt.a", "match2");
|
|
|
|
var batch = await fx.FetchAsync("FILT", "C1", 10);
|
|
batch.Messages.Count.ShouldBe(2);
|
|
batch.Messages.All(m => m.Subject == "filt.a").ShouldBeTrue();
|
|
}
|
|
|
|
// Go: TestJetStreamWildcardSubjectFiltering server/jetstream_test.go:1152
|
|
[Fact]
|
|
public async Task Publish_and_fetch_with_wildcard_filter()
|
|
{
|
|
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("WC", "wc.>");
|
|
_ = await fx.CreateConsumerAsync("WC", "C1", "wc.orders.*");
|
|
|
|
_ = await fx.PublishAndGetAckAsync("wc.orders.created", "1");
|
|
_ = await fx.PublishAndGetAckAsync("wc.events.logged", "2");
|
|
_ = await fx.PublishAndGetAckAsync("wc.orders.shipped", "3");
|
|
|
|
var batch = await fx.FetchAsync("WC", "C1", 10);
|
|
batch.Messages.Count.ShouldBe(2);
|
|
}
|
|
|
|
// Go: TestJetStreamWorkQueueRequestBatch server/jetstream_test.go:1505
|
|
[Fact]
|
|
public async Task Fetch_batch_returns_multiple_messages()
|
|
{
|
|
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("BATCH", "batch.>");
|
|
_ = await fx.CreateConsumerAsync("BATCH", "C1", "batch.>");
|
|
|
|
for (var i = 0; i < 5; i++)
|
|
_ = await fx.PublishAndGetAckAsync("batch.x", $"msg-{i}");
|
|
|
|
var batch = await fx.FetchAsync("BATCH", "C1", 3);
|
|
batch.Messages.Count.ShouldBe(3);
|
|
}
|
|
|
|
// Go: TestJetStreamWorkQueueRequest server/jetstream_test.go:1302
|
|
[Fact]
|
|
public async Task Fetch_single_message()
|
|
{
|
|
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("SINGLE", "single.>");
|
|
_ = await fx.CreateConsumerAsync("SINGLE", "C1", "single.>");
|
|
_ = await fx.PublishAndGetAckAsync("single.x", "hello");
|
|
|
|
var batch = await fx.FetchAsync("SINGLE", "C1", 1);
|
|
batch.Messages.Count.ShouldBe(1);
|
|
Encoding.UTF8.GetString(batch.Messages[0].Payload.Span).ShouldBe("hello");
|
|
}
|
|
|
|
// Go: TestJetStreamNextMsgNoInterest server/jetstream_test.go:6522
|
|
[Fact]
|
|
public async Task Fetch_with_no_messages_returns_empty()
|
|
{
|
|
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("EMPTY", "empty.>");
|
|
_ = await fx.CreateConsumerAsync("EMPTY", "C1", "empty.>");
|
|
|
|
var batch = await fx.FetchAsync("EMPTY", "C1", 1);
|
|
batch.Messages.Count.ShouldBe(0);
|
|
}
|
|
|
|
// Go: TestJetStreamNoAckStream server/jetstream_test.go:821
|
|
[Fact]
|
|
public async Task Publish_to_stream_with_no_ack_consumer()
|
|
{
|
|
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("NOACK", "noack.>");
|
|
_ = await fx.CreateConsumerAsync("NOACK", "C1", "noack.>", ackPolicy: AckPolicy.None);
|
|
|
|
_ = await fx.PublishAndGetAckAsync("noack.x", "data");
|
|
|
|
var batch = await fx.FetchAsync("NOACK", "C1", 1);
|
|
batch.Messages.Count.ShouldBe(1);
|
|
}
|
|
|
|
// Go: TestJetStreamActiveDelivery server/jetstream_test.go:3644
|
|
[Fact]
|
|
public async Task Publish_triggers_push_consumer_delivery()
|
|
{
|
|
await using var fx = await JetStreamApiFixture.StartWithPushConsumerAsync();
|
|
_ = await fx.PublishAndGetAckAsync("orders.created", "order-1");
|
|
|
|
var frame = await fx.ReadPushFrameAsync();
|
|
frame.IsData.ShouldBeTrue();
|
|
frame.Message.ShouldNotBeNull();
|
|
}
|
|
|
|
// Go: TestJetStreamMultipleSubjectsPushBasic server/jetstream_test.go
|
|
[Fact]
|
|
public async Task Push_consumer_receives_matching_messages()
|
|
{
|
|
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("PS", "ps.>");
|
|
_ = await fx.CreateConsumerAsync("PS", "PUSH", "ps.orders.*", push: true, heartbeatMs: 25);
|
|
|
|
_ = await fx.PublishAndGetAckAsync("ps.orders.created", "1");
|
|
_ = await fx.PublishAndGetAckAsync("ps.events.logged", "2");
|
|
|
|
var frame = await fx.ReadPushFrameAsync("PS", "PUSH");
|
|
frame.IsData.ShouldBeTrue();
|
|
frame.Message!.Subject.ShouldBe("ps.orders.created");
|
|
}
|
|
|
|
// Go: TestJetStreamWorkQueueAckAndNext server/jetstream_test.go:1355
|
|
[Fact]
|
|
public async Task Sequential_fetch_advances_cursor()
|
|
{
|
|
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("ADV", "adv.>");
|
|
_ = await fx.CreateConsumerAsync("ADV", "C1", "adv.>");
|
|
|
|
for (var i = 0; i < 5; i++)
|
|
_ = await fx.PublishAndGetAckAsync("adv.x", $"msg-{i}");
|
|
|
|
var batch1 = await fx.FetchAsync("ADV", "C1", 2);
|
|
batch1.Messages.Count.ShouldBe(2);
|
|
batch1.Messages[0].Sequence.ShouldBe(1UL);
|
|
|
|
var batch2 = await fx.FetchAsync("ADV", "C1", 2);
|
|
batch2.Messages.Count.ShouldBe(2);
|
|
batch2.Messages[0].Sequence.ShouldBe(3UL);
|
|
}
|
|
|
|
// Go: TestJetStreamPublishExpectNoMsg server/jetstream_test.go
|
|
[Fact]
|
|
public async Task Publish_to_unmatched_subject_is_not_captured()
|
|
{
|
|
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("NOMATCH", "nomatch.orders.*");
|
|
|
|
var ack = await fx.PublishAndGetAckAsync("different.subject", "data", expectError: true);
|
|
ack.ErrorCode.ShouldNotBeNull();
|
|
}
|
|
|
|
// Go: TestJetStreamPubAck — stream name in ack
|
|
[Fact]
|
|
public async Task Puback_contains_correct_stream_name()
|
|
{
|
|
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("NAMED", "named.>");
|
|
var ack = await fx.PublishAndGetAckAsync("named.x", "data");
|
|
ack.Stream.ShouldBe("NAMED");
|
|
}
|
|
|
|
// Go: TestJetStreamStateTimestamps server/jetstream_test.go:758
|
|
[Fact]
|
|
public async Task Stream_state_updates_after_publish()
|
|
{
|
|
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("ST", "st.>");
|
|
|
|
var before = await fx.GetStreamStateAsync("ST");
|
|
before.Messages.ShouldBe(0UL);
|
|
|
|
_ = await fx.PublishAndGetAckAsync("st.x", "data");
|
|
|
|
var after = await fx.GetStreamStateAsync("ST");
|
|
after.Messages.ShouldBe(1UL);
|
|
after.Bytes.ShouldBeGreaterThan(0UL);
|
|
}
|
|
|
|
// Go: TestJetStreamLongStreamNamesAndPubAck server/jetstream_test.go
|
|
[Fact]
|
|
public async Task Long_stream_name_works()
|
|
{
|
|
var name = new string('A', 50);
|
|
await using var fx = await JetStreamApiFixture.StartWithStreamAsync(name, "long.>");
|
|
var ack = await fx.PublishAndGetAckAsync("long.x", "data");
|
|
ack.Stream.ShouldBe(name);
|
|
}
|
|
|
|
// Go: TestJetStreamPublishDeDupe — unique msg IDs accepted
|
|
[Fact]
|
|
public async Task Unique_msg_ids_all_accepted()
|
|
{
|
|
await using var fx = await JetStreamApiFixture.StartWithStreamConfigAsync(new StreamConfig
|
|
{
|
|
Name = "UNIQ",
|
|
Subjects = ["uniq.>"],
|
|
DuplicateWindowMs = 60_000,
|
|
});
|
|
|
|
for (var i = 0; i < 5; i++)
|
|
{
|
|
var ack = await fx.PublishAndGetAckAsync("uniq.x", $"data-{i}", msgId: $"msg-{i}");
|
|
ack.ErrorCode.ShouldBeNull();
|
|
}
|
|
|
|
var state = await fx.GetStreamStateAsync("UNIQ");
|
|
state.Messages.ShouldBe(5UL);
|
|
}
|
|
|
|
// Go: TestJetStreamPublishDeDupe — no dedup without window
|
|
[Fact]
|
|
public async Task No_dedup_window_allows_same_msg_id()
|
|
{
|
|
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("NODEDUP", "nodedup.>");
|
|
|
|
var ack1 = await fx.PublishAndGetAckAsync("nodedup.x", "1", msgId: "same");
|
|
ack1.ErrorCode.ShouldBeNull();
|
|
|
|
// Without a dedup window, the msg ID is not tracked
|
|
var ack2 = await fx.PublishAndGetAckAsync("nodedup.x", "2", msgId: "same");
|
|
// Could be null or not depending on implementation; both messages stored
|
|
}
|
|
|
|
// Go: TestJetStreamNegativeDupeWindow server/jetstream_test.go
|
|
// When dedup window is 0, the implementation still tracks msg IDs in-process (no TTL-based trim).
|
|
// Verify that with no msg ID, duplicate detection is not triggered.
|
|
[Fact]
|
|
public async Task Dedup_window_zero_with_no_msg_id_allows_duplicates()
|
|
{
|
|
await using var fx = await JetStreamApiFixture.StartWithStreamConfigAsync(new StreamConfig
|
|
{
|
|
Name = "NODUP",
|
|
Subjects = ["nodup.>"],
|
|
DuplicateWindowMs = 0,
|
|
});
|
|
|
|
var ack1 = await fx.PublishAndGetAckAsync("nodup.x", "1");
|
|
ack1.ErrorCode.ShouldBeNull();
|
|
var ack2 = await fx.PublishAndGetAckAsync("nodup.x", "2");
|
|
ack2.ErrorCode.ShouldBeNull();
|
|
|
|
var state = await fx.GetStreamStateAsync("NODUP");
|
|
state.Messages.ShouldBe(2UL);
|
|
}
|
|
|
|
// Go: TestJetStreamWorkQueueSubjectFiltering server/jetstream_test.go:1127
|
|
[Fact]
|
|
public async Task Fetch_with_no_wait_returns_empty_when_no_messages()
|
|
{
|
|
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("NW", "nw.>");
|
|
_ = await fx.CreateConsumerAsync("NW", "C1", "nw.>");
|
|
|
|
var batch = await fx.FetchWithNoWaitAsync("NW", "C1", 5);
|
|
batch.Messages.Count.ShouldBe(0);
|
|
}
|
|
|
|
// Go: TestJetStreamWorkQueueSubjectFiltering — no_wait with messages
|
|
[Fact]
|
|
public async Task Fetch_with_no_wait_returns_available_messages()
|
|
{
|
|
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("NWM", "nwm.>");
|
|
_ = await fx.CreateConsumerAsync("NWM", "C1", "nwm.>");
|
|
|
|
_ = await fx.PublishAndGetAckAsync("nwm.x", "data1");
|
|
_ = await fx.PublishAndGetAckAsync("nwm.x", "data2");
|
|
|
|
var batch = await fx.FetchWithNoWaitAsync("NWM", "C1", 5);
|
|
batch.Messages.Count.ShouldBe(2);
|
|
}
|
|
|
|
// Go: TestJetStreamBasicWorkQueue server/jetstream_test.go:937
|
|
[Fact]
|
|
public async Task Publish_many_and_fetch_all()
|
|
{
|
|
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("ALL", "all.>");
|
|
_ = await fx.CreateConsumerAsync("ALL", "C1", "all.>");
|
|
|
|
for (var i = 0; i < 10; i++)
|
|
_ = await fx.PublishAndGetAckAsync("all.x", $"msg-{i}");
|
|
|
|
var batch = await fx.FetchAsync("ALL", "C1", 20);
|
|
batch.Messages.Count.ShouldBe(10);
|
|
}
|
|
|
|
// Go: TestJetStreamMultipleSubjectsBasic server/jetstream_test.go
|
|
[Fact]
|
|
public async Task Multiple_subjects_captured_by_same_stream()
|
|
{
|
|
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("MULTI", "multi.>");
|
|
_ = await fx.CreateConsumerAsync("MULTI", "C1", "multi.>");
|
|
|
|
_ = await fx.PublishAndGetAckAsync("multi.orders", "1");
|
|
_ = await fx.PublishAndGetAckAsync("multi.events", "2");
|
|
_ = await fx.PublishAndGetAckAsync("multi.logs", "3");
|
|
|
|
var batch = await fx.FetchAsync("MULTI", "C1", 10);
|
|
batch.Messages.Count.ShouldBe(3);
|
|
batch.Messages.Select(m => m.Subject).ShouldBe(["multi.orders", "multi.events", "multi.logs"]);
|
|
}
|
|
|
|
// Go: TestJetStreamGetLastMsgBySubject server/jetstream_test.go
|
|
[Fact]
|
|
public async Task Fetch_preserves_message_subject()
|
|
{
|
|
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("SUB", "sub.>");
|
|
_ = await fx.CreateConsumerAsync("SUB", "C1", "sub.>");
|
|
|
|
_ = await fx.PublishAndGetAckAsync("sub.orders.created", "data");
|
|
|
|
var batch = await fx.FetchAsync("SUB", "C1", 1);
|
|
batch.Messages[0].Subject.ShouldBe("sub.orders.created");
|
|
}
|
|
|
|
// Go: TestJetStreamPubAck — sequence monotonically increasing
|
|
[Fact]
|
|
public async Task Sequence_numbers_are_monotonically_increasing()
|
|
{
|
|
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("MONO", "mono.>");
|
|
|
|
ulong lastSeq = 0;
|
|
for (var i = 0; i < 10; i++)
|
|
{
|
|
var ack = await fx.PublishAndGetAckAsync("mono.x", $"msg-{i}");
|
|
ack.Seq.ShouldBeGreaterThan(lastSeq);
|
|
lastSeq = ack.Seq;
|
|
}
|
|
}
|
|
|
|
// Go: TestJetStreamPerSubjectPending server/jetstream_test.go
|
|
[Fact]
|
|
public async Task Fetch_from_non_existent_consumer_returns_empty()
|
|
{
|
|
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("FNE", "fne.>");
|
|
_ = await fx.PublishAndGetAckAsync("fne.x", "data");
|
|
|
|
var batch = await fx.FetchAsync("FNE", "NOPE", 1);
|
|
batch.Messages.Count.ShouldBe(0);
|
|
}
|
|
|
|
// Go: TestJetStreamMaxMsgsPerSubjectWithDiscardNew server/jetstream_test.go
|
|
[Fact]
|
|
public async Task Publish_to_multiple_streams_routes_correctly()
|
|
{
|
|
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("A", "a.>");
|
|
_ = await fx.RequestLocalAsync("$JS.API.STREAM.CREATE.B", """{"subjects":["b.>"]}""");
|
|
|
|
var ackA = await fx.PublishAndGetAckAsync("a.msg", "for-A");
|
|
ackA.Stream.ShouldBe("A");
|
|
|
|
var ackB = await fx.PublishAndGetAckAsync("b.msg", "for-B");
|
|
ackB.Stream.ShouldBe("B");
|
|
}
|
|
|
|
// Go: TestJetStreamPublishMany
|
|
[Fact]
|
|
public async Task Publish_many_helper_stores_all_messages()
|
|
{
|
|
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("PM", "pm.>");
|
|
await fx.PublishManyAsync("pm.x", ["a", "b", "c", "d", "e"]);
|
|
|
|
var state = await fx.GetStreamStateAsync("PM");
|
|
state.Messages.ShouldBe(5UL);
|
|
}
|
|
|
|
// Go: TestJetStreamRejectLargePublishes server/jetstream_test.go
|
|
[Fact]
|
|
public async Task Large_message_rejected_by_max_msg_size()
|
|
{
|
|
await using var fx = await JetStreamApiFixture.StartWithStreamConfigAsync(new StreamConfig
|
|
{
|
|
Name = "SMALL",
|
|
Subjects = ["small.>"],
|
|
MaxMsgSize = 5,
|
|
});
|
|
|
|
var ack = await fx.PublishAndGetAckAsync("small.x", "this-is-too-big");
|
|
ack.ErrorCode.ShouldNotBeNull();
|
|
}
|
|
|
|
// Go: TestJetStreamAddStreamMaxMsgSize — exactly at limit
|
|
[Fact]
|
|
public async Task Message_exactly_at_size_limit_is_accepted()
|
|
{
|
|
await using var fx = await JetStreamApiFixture.StartWithStreamConfigAsync(new StreamConfig
|
|
{
|
|
Name = "EXACT",
|
|
Subjects = ["exact.>"],
|
|
MaxMsgSize = 4,
|
|
});
|
|
|
|
var ack = await fx.PublishAndGetAckAsync("exact.x", "1234");
|
|
ack.ErrorCode.ShouldBeNull();
|
|
}
|
|
|
|
// Go: TestJetStreamPurgeEffectsConsumerDelivery server/jetstream_test.go
|
|
// After purge, a fresh consumer should be able to see new messages.
|
|
[Fact]
|
|
public async Task Purge_followed_by_new_publish_visible_to_new_consumer()
|
|
{
|
|
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("PCD", "pcd.>");
|
|
|
|
_ = await fx.PublishAndGetAckAsync("pcd.x", "old");
|
|
_ = await fx.RequestLocalAsync("$JS.API.STREAM.PURGE.PCD", "{}");
|
|
|
|
_ = await fx.PublishAndGetAckAsync("pcd.x", "new");
|
|
|
|
// Create a fresh consumer after purge
|
|
_ = await fx.CreateConsumerAsync("PCD", "C2", "pcd.>");
|
|
var batch = await fx.FetchAsync("PCD", "C2", 1);
|
|
batch.Messages.Count.ShouldBe(1);
|
|
Encoding.UTF8.GetString(batch.Messages[0].Payload.Span).ShouldBe("new");
|
|
}
|
|
|
|
// Go: TestJetStreamDeliverLastPerSubject server/jetstream_test.go
|
|
[Fact]
|
|
public async Task Deliver_last_policy_starts_from_last_message()
|
|
{
|
|
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("DLP", "dlp.>");
|
|
_ = await fx.PublishAndGetAckAsync("dlp.x", "first");
|
|
_ = await fx.PublishAndGetAckAsync("dlp.x", "second");
|
|
_ = await fx.PublishAndGetAckAsync("dlp.x", "third");
|
|
|
|
_ = await fx.CreateConsumerAsync("DLP", "C1", "dlp.>", deliverPolicy: DeliverPolicy.Last);
|
|
|
|
var batch = await fx.FetchAsync("DLP", "C1", 10);
|
|
batch.Messages.Count.ShouldBe(1);
|
|
Encoding.UTF8.GetString(batch.Messages[0].Payload.Span).ShouldBe("third");
|
|
}
|
|
|
|
// Go: TestJetStreamDeliverNewPolicy server/jetstream_test.go
|
|
[Fact]
|
|
public async Task Deliver_new_policy_skips_existing_messages()
|
|
{
|
|
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("DNP", "dnp.>");
|
|
_ = await fx.PublishAndGetAckAsync("dnp.x", "existing1");
|
|
_ = await fx.PublishAndGetAckAsync("dnp.x", "existing2");
|
|
|
|
_ = await fx.CreateConsumerAsync("DNP", "C1", "dnp.>", deliverPolicy: DeliverPolicy.New);
|
|
|
|
// Fetch should return empty since no new messages
|
|
var batch = await fx.FetchAsync("DNP", "C1", 10);
|
|
batch.Messages.Count.ShouldBe(0);
|
|
}
|
|
|
|
// Go: TestJetStreamMultipleSubjectsPushBasic — push multi-subject
|
|
[Fact]
|
|
public async Task Push_consumer_heartbeat_frame_present()
|
|
{
|
|
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("HB", "hb.>");
|
|
_ = await fx.CreateConsumerAsync("HB", "PUSH", "hb.>", push: true, heartbeatMs: 10);
|
|
_ = await fx.PublishAndGetAckAsync("hb.x", "data");
|
|
|
|
// Should have data frame followed by heartbeat frame
|
|
var frame1 = await fx.ReadPushFrameAsync("HB", "PUSH");
|
|
frame1.IsData.ShouldBeTrue();
|
|
|
|
var frame2 = await fx.ReadPushFrameAsync("HB", "PUSH");
|
|
frame2.IsHeartbeat.ShouldBeTrue();
|
|
}
|
|
|
|
// Go: TestJetStreamPublishExpect — precondition expected last seq = 0
|
|
[Fact]
|
|
public async Task Publish_expected_last_seq_zero_always_succeeds()
|
|
{
|
|
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("ELZ", "elz.>");
|
|
_ = await fx.PublishAndGetAckAsync("elz.x", "1");
|
|
|
|
// Expected last seq 0 means "no check"
|
|
var ack = await fx.PublishWithExpectedLastSeqAsync("elz.x", "2", 0);
|
|
ack.ErrorCode.ShouldBeNull();
|
|
}
|
|
|
|
// Go: TestJetStreamDirectMsgGet server/jetstream_test.go
|
|
[Fact]
|
|
public async Task Direct_get_returns_published_message()
|
|
{
|
|
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("DG", "dg.>");
|
|
var ack = await fx.PublishAndGetAckAsync("dg.x", "direct-payload");
|
|
|
|
var resp = await fx.RequestLocalAsync(
|
|
"$JS.API.DIRECT.GET.DG", $$"""{ "seq": {{ack.Seq}} }""");
|
|
resp.DirectMessage.ShouldNotBeNull();
|
|
resp.DirectMessage!.Payload.ShouldBe("direct-payload");
|
|
resp.DirectMessage.Subject.ShouldBe("dg.x");
|
|
}
|
|
|
|
// Go: TestJetStreamMsgHeaders server/jetstream_test.go:5554
|
|
[Fact]
|
|
public async Task Message_get_returns_correct_sequence_and_subject()
|
|
{
|
|
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("MG", "mg.>");
|
|
_ = await fx.PublishAndGetAckAsync("mg.first", "data1");
|
|
var ack2 = await fx.PublishAndGetAckAsync("mg.second", "data2");
|
|
|
|
var resp = await fx.RequestLocalAsync(
|
|
"$JS.API.STREAM.MSG.GET.MG", $$"""{ "seq": {{ack2.Seq}} }""");
|
|
resp.StreamMessage.ShouldNotBeNull();
|
|
resp.StreamMessage!.Sequence.ShouldBe(ack2.Seq);
|
|
resp.StreamMessage.Subject.ShouldBe("mg.second");
|
|
resp.StreamMessage.Payload.ShouldBe("data2");
|
|
}
|
|
}
|