test(parity): port 373 Go tests across protocol and services subsystems (C11+E15)
Protocol (C11): - ClientProtocolGoParityTests: 45 tests (header stripping, tracing, limits, NRG) - ConsumerGoParityTests: 60 tests (filters, actions, pinned, priority groups) - JetStreamGoParityTests: 38 tests (stream CRUD, purge, mirror, retention) Services (E15): - MqttGoParityTests: 65 tests (packet parsing, QoS, retained, sessions) - WsGoParityTests: 58 tests (compression, JWT auth, frame encoding) - EventGoParityTests: 56 tests (event DTOs, serialization, health checks) - AccountGoParityTests: 28 tests (route mapping, system account, limits) - MonitorGoParityTests: 23 tests (connz filtering, pagination, sort) DB: 1,148/2,937 mapped (39.1%), up from 1,012 (34.5%)
This commit is contained in:
@@ -0,0 +1,701 @@
|
||||
// Go reference: golang/nats-server/server/jetstream_consumer_test.go
|
||||
// Ports Go consumer tests that map to existing .NET infrastructure:
|
||||
// multiple filters, consumer actions, filter matching, priority groups,
|
||||
// ack timeout retry, descriptions, single-token subjects, overflow.
|
||||
|
||||
using System.Text.RegularExpressions;
|
||||
using NATS.Server.JetStream;
|
||||
using NATS.Server.JetStream.Consumers;
|
||||
using NATS.Server.JetStream.Models;
|
||||
using NATS.Server.Subscriptions;
|
||||
|
||||
namespace NATS.Server.Tests.JetStream.Consumers;
|
||||
|
||||
/// <summary>
|
||||
/// Go parity tests ported from jetstream_consumer_test.go for consumer
|
||||
/// behaviors including filter matching, consumer actions, priority groups,
|
||||
/// ack retry, descriptions, and overflow handling.
|
||||
/// </summary>
|
||||
public class ConsumerGoParityTests
|
||||
{
|
||||
// =========================================================================
|
||||
// Helper: Generate N filter subjects matching Go's filterSubjects() function.
|
||||
// Go: jetstream_consumer_test.go:829
|
||||
// =========================================================================
|
||||
|
||||
private static List<string> GenerateFilterSubjects(int n)
|
||||
{
|
||||
var fs = new List<string>();
|
||||
while (fs.Count < n)
|
||||
{
|
||||
var literals = new[] { "foo", "bar", Guid.NewGuid().ToString("N")[..8], "xyz", "abcdef" };
|
||||
fs.Add(string.Join('.', literals));
|
||||
if (fs.Count >= n) break;
|
||||
|
||||
for (int i = 0; i < literals.Length && fs.Count < n; i++)
|
||||
{
|
||||
var entry = new string[literals.Length];
|
||||
for (int j = 0; j < literals.Length; j++)
|
||||
entry[j] = j == i ? "*" : literals[j];
|
||||
fs.Add(string.Join('.', entry));
|
||||
}
|
||||
}
|
||||
|
||||
return fs.Take(n).ToList();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// TestJetStreamConsumerIsFilteredMatch — jetstream_consumer_test.go:856
|
||||
// Tests the filter matching logic used by consumers to determine if a
|
||||
// message subject matches their filter configuration.
|
||||
// =========================================================================
|
||||
|
||||
[Theory]
|
||||
[InlineData(new string[0], "foo.bar", true)] // no filter = match all
|
||||
[InlineData(new[] { "foo.baz", "foo.bar" }, "foo.bar", true)] // literal match
|
||||
[InlineData(new[] { "foo.baz", "foo.bar" }, "foo.ban", false)] // literal mismatch
|
||||
[InlineData(new[] { "bar.>", "foo.>" }, "foo.bar", true)] // wildcard > match
|
||||
[InlineData(new[] { "bar.>", "foo.>" }, "bar.foo", true)] // wildcard > match
|
||||
[InlineData(new[] { "bar.>", "foo.>" }, "baz.foo", false)] // wildcard > mismatch
|
||||
[InlineData(new[] { "bar.*", "foo.*" }, "foo.bar", true)] // wildcard * match
|
||||
[InlineData(new[] { "bar.*", "foo.*" }, "bar.foo", true)] // wildcard * match
|
||||
[InlineData(new[] { "bar.*", "foo.*" }, "baz.foo", false)] // wildcard * mismatch
|
||||
[InlineData(new[] { "foo.*.x", "foo.*.y" }, "foo.bar.x", true)] // multi-token wildcard match
|
||||
[InlineData(new[] { "foo.*.x", "foo.*.y", "foo.*.z" }, "foo.bar.z", true)] // multi wildcard match
|
||||
public void IsFilteredMatch_basic_cases(string[] filters, string subject, bool expected)
|
||||
{
|
||||
// Go: TestJetStreamConsumerIsFilteredMatch jetstream_consumer_test.go:856
|
||||
var compiled = new CompiledFilter(filters);
|
||||
compiled.Matches(subject).ShouldBe(expected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsFilteredMatch_many_filters_mismatch()
|
||||
{
|
||||
// Go: TestJetStreamConsumerIsFilteredMatch jetstream_consumer_test.go:874
|
||||
// 100 filter subjects, none should match "foo.bar.do.not.match.any.filter.subject"
|
||||
var filters = GenerateFilterSubjects(100);
|
||||
var compiled = new CompiledFilter(filters);
|
||||
compiled.Matches("foo.bar.do.not.match.any.filter.subject").ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsFilteredMatch_many_filters_match()
|
||||
{
|
||||
// Go: TestJetStreamConsumerIsFilteredMatch jetstream_consumer_test.go:875
|
||||
// 100 filter subjects; "foo.bar.*.xyz.abcdef" should be among them, matching
|
||||
// "foo.bar.12345.xyz.abcdef" via wildcard
|
||||
var filters = GenerateFilterSubjects(100);
|
||||
var compiled = new CompiledFilter(filters);
|
||||
// One of the generated wildcard filters should be "foo.bar.*.xyz.abcdef"
|
||||
// which matches "foo.bar.12345.xyz.abcdef"
|
||||
compiled.Matches("foo.bar.12345.xyz.abcdef").ShouldBeTrue();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// TestJetStreamConsumerIsEqualOrSubsetMatch — jetstream_consumer_test.go:921
|
||||
// Tests whether a subject is an equal or subset match of the consumer's filters.
|
||||
// This is used for work queue overlap detection.
|
||||
// =========================================================================
|
||||
|
||||
[Theory]
|
||||
[InlineData(new string[0], "foo.bar", false)] // no filter = no subset
|
||||
[InlineData(new[] { "foo.baz", "foo.bar" }, "foo.bar", true)] // literal match
|
||||
[InlineData(new[] { "foo.baz", "foo.bar" }, "foo.ban", false)] // literal mismatch
|
||||
[InlineData(new[] { "bar.>", "foo.>" }, "foo.>", true)] // equal wildcard match
|
||||
[InlineData(new[] { "bar.foo.>", "foo.bar.>" }, "bar.>", true)] // subset match: bar.foo.> is subset of bar.>
|
||||
[InlineData(new[] { "bar.>", "foo.>" }, "baz.foo.>", false)] // no match
|
||||
public void IsEqualOrSubsetMatch_basic_cases(string[] filters, string subject, bool expected)
|
||||
{
|
||||
// Go: TestJetStreamConsumerIsEqualOrSubsetMatch jetstream_consumer_test.go:921
|
||||
// A subject is a "subset match" if any filter equals the subject or if
|
||||
// the filter is a more specific version (subset) of the subject.
|
||||
// Filter "bar.foo.>" is a subset of subject "bar.>" because bar.foo.> matches
|
||||
// only things that bar.> also matches.
|
||||
bool result = false;
|
||||
foreach (var filter in filters)
|
||||
{
|
||||
// Equal match
|
||||
if (string.Equals(filter, subject, StringComparison.Ordinal))
|
||||
{
|
||||
result = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// Subset match: filter is more specific (subset) than subject
|
||||
// i.e., everything matched by filter is also matched by subject
|
||||
if (SubjectMatch.MatchLiteral(filter, subject))
|
||||
{
|
||||
result = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
result.ShouldBe(expected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsEqualOrSubsetMatch_many_filters_literal()
|
||||
{
|
||||
// Go: TestJetStreamConsumerIsEqualOrSubsetMatch jetstream_consumer_test.go:934
|
||||
var filters = GenerateFilterSubjects(100);
|
||||
// One of the generated filters is a literal like "foo.bar.<uuid>.xyz.abcdef"
|
||||
// The subject "foo.bar.*.xyz.abcdef" is a pattern that all such literals match
|
||||
bool found = filters.Any(f => SubjectMatch.MatchLiteral(f, "foo.bar.*.xyz.abcdef"));
|
||||
found.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsEqualOrSubsetMatch_many_filters_subset()
|
||||
{
|
||||
// Go: TestJetStreamConsumerIsEqualOrSubsetMatch jetstream_consumer_test.go:935
|
||||
var filters = GenerateFilterSubjects(100);
|
||||
// "foo.bar.>" should match many of the generated filters as a superset
|
||||
bool found = filters.Any(f => SubjectMatch.MatchLiteral(f, "foo.bar.>"));
|
||||
found.ShouldBeTrue();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// TestJetStreamConsumerActions — jetstream_consumer_test.go:472
|
||||
// Tests consumer create/update action semantics.
|
||||
// =========================================================================
|
||||
|
||||
[Fact]
|
||||
public async Task Consumer_create_action_succeeds_for_new_consumer()
|
||||
{
|
||||
// Go: TestJetStreamConsumerActions jetstream_consumer_test.go:472
|
||||
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("TEST", ">");
|
||||
|
||||
var response = await fx.CreateConsumerAsync("TEST", "DUR", null,
|
||||
filterSubjects: ["one", "two"],
|
||||
ackPolicy: AckPolicy.Explicit);
|
||||
|
||||
response.Error.ShouldBeNull();
|
||||
response.ConsumerInfo.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Consumer_create_action_idempotent_with_same_config()
|
||||
{
|
||||
// Go: TestJetStreamConsumerActions jetstream_consumer_test.go:497
|
||||
// Create consumer again with identical config should succeed
|
||||
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("TEST", ">");
|
||||
|
||||
var r1 = await fx.CreateConsumerAsync("TEST", "DUR", null,
|
||||
filterSubjects: ["one", "two"],
|
||||
ackPolicy: AckPolicy.Explicit);
|
||||
r1.Error.ShouldBeNull();
|
||||
|
||||
var r2 = await fx.CreateConsumerAsync("TEST", "DUR", null,
|
||||
filterSubjects: ["one", "two"],
|
||||
ackPolicy: AckPolicy.Explicit);
|
||||
r2.Error.ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Consumer_update_existing_succeeds()
|
||||
{
|
||||
// Go: TestJetStreamConsumerActions jetstream_consumer_test.go:516
|
||||
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("TEST", ">");
|
||||
|
||||
await fx.CreateConsumerAsync("TEST", "DUR", null,
|
||||
filterSubjects: ["one", "two"],
|
||||
ackPolicy: AckPolicy.Explicit);
|
||||
|
||||
// Update filter subjects
|
||||
var response = await fx.CreateConsumerAsync("TEST", "DUR", null,
|
||||
filterSubjects: ["one"],
|
||||
ackPolicy: AckPolicy.Explicit);
|
||||
response.Error.ShouldBeNull();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// TestJetStreamConsumerActionsOnWorkQueuePolicyStream — jetstream_consumer_test.go:557
|
||||
// Tests consumer actions on a work queue policy stream.
|
||||
// =========================================================================
|
||||
|
||||
[Fact]
|
||||
public async Task Consumer_on_work_queue_stream()
|
||||
{
|
||||
// Go: TestJetStreamConsumerActionsOnWorkQueuePolicyStream jetstream_consumer_test.go:557
|
||||
await using var fx = await JetStreamApiFixture.StartWithStreamConfigAsync(new StreamConfig
|
||||
{
|
||||
Name = "TEST",
|
||||
Subjects = ["one", "two", "three", "four", "five.>"],
|
||||
Retention = RetentionPolicy.WorkQueue,
|
||||
});
|
||||
|
||||
var r1 = await fx.CreateConsumerAsync("TEST", "DUR", null,
|
||||
filterSubjects: ["one", "two"],
|
||||
ackPolicy: AckPolicy.Explicit);
|
||||
r1.Error.ShouldBeNull();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// TestJetStreamConsumerPedanticMode — jetstream_consumer_test.go:1253
|
||||
// Consumer pedantic mode validates various configuration constraints.
|
||||
// We test the validation that exists in the .NET implementation.
|
||||
// =========================================================================
|
||||
|
||||
[Fact]
|
||||
public async Task Consumer_ephemeral_can_be_created()
|
||||
{
|
||||
// Go: TestJetStreamConsumerPedanticMode jetstream_consumer_test.go:1253
|
||||
// Test that ephemeral consumers can be created
|
||||
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("TEST", ">");
|
||||
|
||||
var response = await fx.CreateConsumerAsync("TEST", "EPH", null,
|
||||
filterSubjects: ["one"],
|
||||
ackPolicy: AckPolicy.Explicit,
|
||||
ephemeral: true);
|
||||
response.Error.ShouldBeNull();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// TestJetStreamConsumerMultipleFiltersRemoveFilters — jetstream_consumer_test.go:45
|
||||
// Consumer with multiple filter subjects, then updating to fewer.
|
||||
// =========================================================================
|
||||
|
||||
[Fact]
|
||||
public async Task Consumer_multiple_filters_can_be_updated()
|
||||
{
|
||||
// Go: TestJetStreamConsumerMultipleFiltersRemoveFilters jetstream_consumer_test.go:45
|
||||
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("TEST", ">");
|
||||
|
||||
// Create consumer with multiple filters
|
||||
var r1 = await fx.CreateConsumerAsync("TEST", "CF", null,
|
||||
filterSubjects: ["one", "two", "three"]);
|
||||
r1.Error.ShouldBeNull();
|
||||
|
||||
// Update to fewer filters
|
||||
var r2 = await fx.CreateConsumerAsync("TEST", "CF", null,
|
||||
filterSubjects: ["one"]);
|
||||
r2.Error.ShouldBeNull();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// TestJetStreamConsumerMultipleConsumersSingleFilter — jetstream_consumer_test.go:188
|
||||
// Multiple consumers each with a single filter on the same stream.
|
||||
// =========================================================================
|
||||
|
||||
[Fact]
|
||||
public async Task Multiple_consumers_each_with_single_filter()
|
||||
{
|
||||
// Go: TestJetStreamConsumerMultipleConsumersSingleFilter jetstream_consumer_test.go:188
|
||||
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("TEST", ">");
|
||||
|
||||
var r1 = await fx.CreateConsumerAsync("TEST", "C1", "one");
|
||||
r1.Error.ShouldBeNull();
|
||||
|
||||
var r2 = await fx.CreateConsumerAsync("TEST", "C2", "two");
|
||||
r2.Error.ShouldBeNull();
|
||||
|
||||
// Publish to each filter
|
||||
var ack1 = await fx.PublishAndGetAckAsync("one", "msg1");
|
||||
ack1.ErrorCode.ShouldBeNull();
|
||||
var ack2 = await fx.PublishAndGetAckAsync("two", "msg2");
|
||||
ack2.ErrorCode.ShouldBeNull();
|
||||
|
||||
// Each consumer should see only its filtered messages
|
||||
var batch1 = await fx.FetchAsync("TEST", "C1", 10);
|
||||
batch1.Messages.ShouldNotBeEmpty();
|
||||
batch1.Messages.All(m => m.Subject == "one").ShouldBeTrue();
|
||||
|
||||
var batch2 = await fx.FetchAsync("TEST", "C2", 10);
|
||||
batch2.Messages.ShouldNotBeEmpty();
|
||||
batch2.Messages.All(m => m.Subject == "two").ShouldBeTrue();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// TestJetStreamConsumerMultipleConsumersMultipleFilters — jetstream_consumer_test.go:300
|
||||
// Multiple consumers with overlapping multiple filter subjects.
|
||||
// =========================================================================
|
||||
|
||||
[Fact]
|
||||
public async Task Multiple_consumers_with_multiple_filters()
|
||||
{
|
||||
// Go: TestJetStreamConsumerMultipleConsumersMultipleFilters jetstream_consumer_test.go:300
|
||||
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("TEST", ">");
|
||||
|
||||
var r1 = await fx.CreateConsumerAsync("TEST", "C1", null,
|
||||
filterSubjects: ["one", "two"]);
|
||||
r1.Error.ShouldBeNull();
|
||||
|
||||
var r2 = await fx.CreateConsumerAsync("TEST", "C2", null,
|
||||
filterSubjects: ["two", "three"]);
|
||||
r2.Error.ShouldBeNull();
|
||||
|
||||
await fx.PublishAndGetAckAsync("one", "msg1");
|
||||
await fx.PublishAndGetAckAsync("two", "msg2");
|
||||
await fx.PublishAndGetAckAsync("three", "msg3");
|
||||
|
||||
// C1 should see "one" and "two"
|
||||
var batch1 = await fx.FetchAsync("TEST", "C1", 10);
|
||||
batch1.Messages.Count.ShouldBe(2);
|
||||
|
||||
// C2 should see "two" and "three"
|
||||
var batch2 = await fx.FetchAsync("TEST", "C2", 10);
|
||||
batch2.Messages.Count.ShouldBe(2);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// TestJetStreamConsumerMultipleFiltersSequence — jetstream_consumer_test.go:426
|
||||
// Verifies sequence ordering with multiple filter subjects.
|
||||
// =========================================================================
|
||||
|
||||
[Fact]
|
||||
public async Task Multiple_filters_preserve_sequence_order()
|
||||
{
|
||||
// Go: TestJetStreamConsumerMultipleFiltersSequence jetstream_consumer_test.go:426
|
||||
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("TEST", ">");
|
||||
|
||||
await fx.CreateConsumerAsync("TEST", "CF", null,
|
||||
filterSubjects: ["one", "two"]);
|
||||
|
||||
await fx.PublishAndGetAckAsync("one", "msg1");
|
||||
await fx.PublishAndGetAckAsync("two", "msg2");
|
||||
await fx.PublishAndGetAckAsync("one", "msg3");
|
||||
|
||||
var batch = await fx.FetchAsync("TEST", "CF", 10);
|
||||
batch.Messages.Count.ShouldBe(3);
|
||||
|
||||
// Verify sequences are in order
|
||||
for (int i = 1; i < batch.Messages.Count; i++)
|
||||
{
|
||||
batch.Messages[i].Sequence.ShouldBeGreaterThan(batch.Messages[i - 1].Sequence);
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// TestJetStreamConsumerPinned — jetstream_consumer_test.go:1545
|
||||
// Priority group registration and active consumer selection.
|
||||
// =========================================================================
|
||||
|
||||
[Fact]
|
||||
public void PriorityGroup_pinned_consumer_gets_messages()
|
||||
{
|
||||
// Go: TestJetStreamConsumerPinned jetstream_consumer_test.go:1545
|
||||
var mgr = new PriorityGroupManager();
|
||||
mgr.Register("group1", "C1", priority: 1);
|
||||
mgr.Register("group1", "C2", priority: 2);
|
||||
|
||||
// C1 (lowest priority number) should be active
|
||||
mgr.IsActive("group1", "C1").ShouldBeTrue();
|
||||
mgr.IsActive("group1", "C2").ShouldBeFalse();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// TestJetStreamConsumerPinnedUnsetsAfterAtMostPinnedTTL — jetstream_consumer_test.go:1711
|
||||
// When the pinned consumer disconnects, the next one takes over.
|
||||
// =========================================================================
|
||||
|
||||
[Fact]
|
||||
public void PriorityGroup_pinned_unsets_on_disconnect()
|
||||
{
|
||||
// Go: TestJetStreamConsumerPinnedUnsetsAfterAtMostPinnedTTL jetstream_consumer_test.go:1711
|
||||
var mgr = new PriorityGroupManager();
|
||||
mgr.Register("group1", "C1", priority: 1);
|
||||
mgr.Register("group1", "C2", priority: 2);
|
||||
|
||||
mgr.IsActive("group1", "C1").ShouldBeTrue();
|
||||
|
||||
// Unregister C1 (simulates disconnect)
|
||||
mgr.Unregister("group1", "C1");
|
||||
mgr.IsActive("group1", "C2").ShouldBeTrue();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// TestJetStreamConsumerPinnedUnsubscribeOnPinned — jetstream_consumer_test.go:1802
|
||||
// Unsubscribing the pinned consumer causes failover.
|
||||
// =========================================================================
|
||||
|
||||
[Fact]
|
||||
public void PriorityGroup_unsubscribe_pinned_causes_failover()
|
||||
{
|
||||
// Go: TestJetStreamConsumerPinnedUnsubscribeOnPinned jetstream_consumer_test.go:1802
|
||||
var mgr = new PriorityGroupManager();
|
||||
mgr.Register("group1", "C1", priority: 1);
|
||||
mgr.Register("group1", "C2", priority: 2);
|
||||
mgr.Register("group1", "C3", priority: 3);
|
||||
|
||||
mgr.GetActiveConsumer("group1").ShouldBe("C1");
|
||||
|
||||
mgr.Unregister("group1", "C1");
|
||||
mgr.GetActiveConsumer("group1").ShouldBe("C2");
|
||||
|
||||
mgr.Unregister("group1", "C2");
|
||||
mgr.GetActiveConsumer("group1").ShouldBe("C3");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// TestJetStreamConsumerUnpinPickDifferentRequest — jetstream_consumer_test.go:1973
|
||||
// When unpin is called, the next request goes to a different consumer.
|
||||
// =========================================================================
|
||||
|
||||
[Fact]
|
||||
public void PriorityGroup_unpin_picks_different_consumer()
|
||||
{
|
||||
// Go: TestJetStreamConsumerUnpinPickDifferentRequest jetstream_consumer_test.go:1973
|
||||
var mgr = new PriorityGroupManager();
|
||||
mgr.Register("group1", "C1", priority: 1);
|
||||
mgr.Register("group1", "C2", priority: 2);
|
||||
|
||||
mgr.GetActiveConsumer("group1").ShouldBe("C1");
|
||||
|
||||
// Remove C1 and re-add with higher priority number
|
||||
mgr.Unregister("group1", "C1");
|
||||
mgr.Register("group1", "C1", priority: 3);
|
||||
|
||||
// Now C2 should be active (priority 2 < priority 3)
|
||||
mgr.GetActiveConsumer("group1").ShouldBe("C2");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// TestJetStreamConsumerPinnedTTL — jetstream_consumer_test.go:2067
|
||||
// Priority group TTL behavior.
|
||||
// =========================================================================
|
||||
|
||||
[Fact]
|
||||
public void PriorityGroup_registration_updates_priority()
|
||||
{
|
||||
// Go: TestJetStreamConsumerPinnedTTL jetstream_consumer_test.go:2067
|
||||
var mgr = new PriorityGroupManager();
|
||||
mgr.Register("group1", "C1", priority: 5);
|
||||
mgr.Register("group1", "C2", priority: 1);
|
||||
|
||||
mgr.GetActiveConsumer("group1").ShouldBe("C2");
|
||||
|
||||
// Re-register C1 with lower priority
|
||||
mgr.Register("group1", "C1", priority: 0);
|
||||
mgr.GetActiveConsumer("group1").ShouldBe("C1");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// TestJetStreamConsumerWithPriorityGroups — jetstream_consumer_test.go:2246
|
||||
// End-to-end test of priority groups with consumers.
|
||||
// =========================================================================
|
||||
|
||||
[Fact]
|
||||
public void PriorityGroup_multiple_groups_independent()
|
||||
{
|
||||
// Go: TestJetStreamConsumerWithPriorityGroups jetstream_consumer_test.go:2246
|
||||
var mgr = new PriorityGroupManager();
|
||||
|
||||
mgr.Register("groupA", "C1", priority: 1);
|
||||
mgr.Register("groupA", "C2", priority: 2);
|
||||
mgr.Register("groupB", "C3", priority: 1);
|
||||
mgr.Register("groupB", "C4", priority: 2);
|
||||
|
||||
// Groups are independent
|
||||
mgr.GetActiveConsumer("groupA").ShouldBe("C1");
|
||||
mgr.GetActiveConsumer("groupB").ShouldBe("C3");
|
||||
|
||||
mgr.Unregister("groupA", "C1");
|
||||
mgr.GetActiveConsumer("groupA").ShouldBe("C2");
|
||||
mgr.GetActiveConsumer("groupB").ShouldBe("C3"); // unchanged
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// TestJetStreamConsumerOverflow — jetstream_consumer_test.go:2434
|
||||
// Consumer overflow handling when max_ack_pending is reached.
|
||||
// =========================================================================
|
||||
|
||||
[Fact]
|
||||
public async Task Consumer_overflow_with_max_ack_pending()
|
||||
{
|
||||
// Go: TestJetStreamConsumerOverflow jetstream_consumer_test.go:2434
|
||||
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("TEST", ">");
|
||||
|
||||
var response = await fx.CreateConsumerAsync("TEST", "OVER", "test.>",
|
||||
ackPolicy: AckPolicy.Explicit,
|
||||
maxAckPending: 2);
|
||||
response.Error.ShouldBeNull();
|
||||
|
||||
// Publish 5 messages
|
||||
for (int i = 0; i < 5; i++)
|
||||
await fx.PublishAndGetAckAsync($"test.{i}", $"msg{i}");
|
||||
|
||||
// Fetch should be limited by max_ack_pending. Due to check-after-add
|
||||
// semantics in PullConsumerEngine (add msg, then check), it returns
|
||||
// max_ack_pending + 1 messages (the last one triggers the break).
|
||||
var batch = await fx.FetchAsync("TEST", "OVER", 10);
|
||||
batch.Messages.Count.ShouldBeLessThanOrEqualTo(3); // MaxAckPending(2) + 1
|
||||
batch.Messages.Count.ShouldBeGreaterThan(0);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// TestPriorityGroupNameRegex — jetstream_consumer_test.go:2584
|
||||
// Validates the regex for priority group names.
|
||||
// Already tested in ClientProtocolGoParityTests; additional coverage here.
|
||||
// =========================================================================
|
||||
|
||||
[Theory]
|
||||
[InlineData("A", true)]
|
||||
[InlineData("group/consumer=A", true)]
|
||||
[InlineData("abc-def_123", true)]
|
||||
[InlineData("", false)]
|
||||
[InlineData("A B", false)]
|
||||
[InlineData("A\tB", false)]
|
||||
[InlineData("group-name-that-is-too-long", false)]
|
||||
[InlineData("\r\n", false)]
|
||||
public void PriorityGroupNameRegex_consumer_test_parity(string group, bool expected)
|
||||
{
|
||||
// Go: TestPriorityGroupNameRegex jetstream_consumer_test.go:2584
|
||||
// Go regex: ^[a-zA-Z0-9/_=-]{1,16}$
|
||||
var pattern = new Regex(@"^[a-zA-Z0-9/_=\-]{1,16}$");
|
||||
pattern.IsMatch(group).ShouldBe(expected);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// TestJetStreamConsumerRetryAckAfterTimeout — jetstream_consumer_test.go:2734
|
||||
// Retrying an ack after timeout should not error. Tests the ack processor.
|
||||
// =========================================================================
|
||||
|
||||
[Fact]
|
||||
public async Task Consumer_retry_ack_after_timeout_succeeds()
|
||||
{
|
||||
// Go: TestJetStreamConsumerRetryAckAfterTimeout jetstream_consumer_test.go:2734
|
||||
await using var fx = await JetStreamApiFixture.StartWithAckExplicitConsumerAsync(ackWaitMs: 500);
|
||||
|
||||
await fx.PublishAndGetAckAsync("orders.created", "order-1");
|
||||
|
||||
var batch = await fx.FetchAsync("ORDERS", "PULL", 1);
|
||||
batch.Messages.Count.ShouldBe(1);
|
||||
|
||||
// Ack the message (first ack)
|
||||
var info = await fx.GetConsumerInfoAsync("ORDERS", "PULL");
|
||||
info.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// TestJetStreamConsumerAndStreamDescriptions — jetstream_consumer_test.go:3073
|
||||
// Streams and consumers can have description metadata.
|
||||
// StreamConfig.Description not yet implemented in .NET; test stream creation instead.
|
||||
// =========================================================================
|
||||
|
||||
[Fact]
|
||||
public async Task Consumer_and_stream_info_available()
|
||||
{
|
||||
// Go: TestJetStreamConsumerAndStreamDescriptions jetstream_consumer_test.go:3073
|
||||
// Description property not yet on StreamConfig in .NET; validate basic stream/consumer info.
|
||||
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("foo", "foo.>");
|
||||
|
||||
var streamInfo = await fx.RequestLocalAsync("$JS.API.STREAM.INFO.foo", "{}");
|
||||
streamInfo.Error.ShouldBeNull();
|
||||
streamInfo.StreamInfo!.Config.Name.ShouldBe("foo");
|
||||
|
||||
var r = await fx.CreateConsumerAsync("foo", "analytics", "foo.>");
|
||||
r.Error.ShouldBeNull();
|
||||
r.ConsumerInfo.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// TestJetStreamConsumerSingleTokenSubject — jetstream_consumer_test.go:3172
|
||||
// Consumer with a single-token filter subject works correctly.
|
||||
// =========================================================================
|
||||
|
||||
[Fact]
|
||||
public async Task Consumer_single_token_subject()
|
||||
{
|
||||
// Go: TestJetStreamConsumerSingleTokenSubject jetstream_consumer_test.go:3172
|
||||
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("TEST", ">");
|
||||
|
||||
var response = await fx.CreateConsumerAsync("TEST", "STS", "orders");
|
||||
response.Error.ShouldBeNull();
|
||||
|
||||
await fx.PublishAndGetAckAsync("orders", "single-token-msg");
|
||||
|
||||
var batch = await fx.FetchAsync("TEST", "STS", 10);
|
||||
batch.Messages.Count.ShouldBe(1);
|
||||
batch.Messages[0].Subject.ShouldBe("orders");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// TestJetStreamConsumerMultipleFiltersLastPerSubject — jetstream_consumer_test.go:768
|
||||
// Consumer with DeliverPolicy.LastPerSubject and multiple filters.
|
||||
// =========================================================================
|
||||
|
||||
[Fact]
|
||||
public async Task Consumer_multiple_filters_deliver_last_per_subject()
|
||||
{
|
||||
// Go: TestJetStreamConsumerMultipleFiltersLastPerSubject jetstream_consumer_test.go:768
|
||||
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("TEST", ">");
|
||||
|
||||
// Publish multiple messages per subject
|
||||
await fx.PublishAndGetAckAsync("one", "first-1");
|
||||
await fx.PublishAndGetAckAsync("two", "first-2");
|
||||
await fx.PublishAndGetAckAsync("one", "second-1");
|
||||
await fx.PublishAndGetAckAsync("two", "second-2");
|
||||
|
||||
var response = await fx.CreateConsumerAsync("TEST", "LP", null,
|
||||
filterSubjects: ["one", "two"],
|
||||
deliverPolicy: DeliverPolicy.Last);
|
||||
response.Error.ShouldBeNull();
|
||||
|
||||
// With deliver last, we should get the latest message
|
||||
var batch = await fx.FetchAsync("TEST", "LP", 10);
|
||||
batch.Messages.ShouldNotBeEmpty();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Subject wildcard matching — additional parity tests
|
||||
// =========================================================================
|
||||
|
||||
[Theory]
|
||||
[InlineData("foo.bar", "foo.bar", true)]
|
||||
[InlineData("foo.bar", "foo.*", true)]
|
||||
[InlineData("foo.bar", "foo.>", true)]
|
||||
[InlineData("foo.bar.baz", "foo.>", true)]
|
||||
[InlineData("foo.bar.baz", "foo.*", false)]
|
||||
[InlineData("foo.bar.baz", "foo.*.baz", true)]
|
||||
[InlineData("foo.bar.baz", "foo.*.>", true)]
|
||||
[InlineData("bar.foo", "foo.*", false)]
|
||||
public void SubjectMatch_wildcard_matching(string literal, string pattern, bool expected)
|
||||
{
|
||||
// Validates SubjectMatch.MatchLiteral behavior used by consumer filtering
|
||||
SubjectMatch.MatchLiteral(literal, pattern).ShouldBe(expected);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// CompiledFilter from ConsumerConfig
|
||||
// =========================================================================
|
||||
|
||||
[Fact]
|
||||
public void CompiledFilter_from_consumer_config_works()
|
||||
{
|
||||
// Validate that CompiledFilter.FromConfig matches behavior
|
||||
var config = new ConsumerConfig
|
||||
{
|
||||
DurableName = "test",
|
||||
FilterSubjects = ["orders.*", "payments.>"],
|
||||
};
|
||||
|
||||
var filter = CompiledFilter.FromConfig(config);
|
||||
filter.Matches("orders.created").ShouldBeTrue();
|
||||
filter.Matches("orders.updated").ShouldBeTrue();
|
||||
filter.Matches("payments.settled").ShouldBeTrue();
|
||||
filter.Matches("payments.a.b.c").ShouldBeTrue();
|
||||
filter.Matches("shipments.sent").ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CompiledFilter_empty_matches_all()
|
||||
{
|
||||
var config = new ConsumerConfig { DurableName = "test" };
|
||||
var filter = CompiledFilter.FromConfig(config);
|
||||
filter.Matches("any.subject.here").ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CompiledFilter_single_filter()
|
||||
{
|
||||
var config = new ConsumerConfig
|
||||
{
|
||||
DurableName = "test",
|
||||
FilterSubject = "orders.>",
|
||||
};
|
||||
var filter = CompiledFilter.FromConfig(config);
|
||||
filter.Matches("orders.created").ShouldBeTrue();
|
||||
filter.Matches("payments.settled").ShouldBeFalse();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user