// 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; using NATS.Server.TestUtilities; namespace NATS.Server.JetStream.Tests.JetStream.Consumers; /// /// 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. /// public class ConsumerGoParityTests { // ========================================================================= // Helper: Generate N filter subjects matching Go's filterSubjects() function. // Go: jetstream_consumer_test.go:829 // ========================================================================= private static List GenerateFilterSubjects(int n) { var fs = new List(); 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..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(); } }