using NATS.Server.Subscriptions; namespace NATS.Server.Tests; public class SubListTests { private static Subscription MakeSub(string subject, string? queue = null, string sid = "1") => new() { Subject = subject, Queue = queue, Sid = sid }; [Fact] public void Insert_and_match_literal_subject() { var sl = new SubList(); var sub = MakeSub("foo.bar"); sl.Insert(sub); var r = sl.Match("foo.bar"); r.PlainSubs.ShouldHaveSingleItem(); r.PlainSubs[0].ShouldBeSameAs(sub); r.QueueSubs.ShouldBeEmpty(); } [Fact] public void Match_returns_empty_for_no_match() { var sl = new SubList(); sl.Insert(MakeSub("foo.bar")); var r = sl.Match("foo.baz"); r.PlainSubs.ShouldBeEmpty(); } [Fact] public void Match_partial_wildcard() { var sl = new SubList(); var sub = MakeSub("foo.*"); sl.Insert(sub); sl.Match("foo.bar").PlainSubs.ShouldHaveSingleItem(); sl.Match("foo.baz").PlainSubs.ShouldHaveSingleItem(); sl.Match("foo.bar.baz").PlainSubs.ShouldBeEmpty(); } [Fact] public void Match_full_wildcard() { var sl = new SubList(); var sub = MakeSub("foo.>"); sl.Insert(sub); sl.Match("foo.bar").PlainSubs.ShouldHaveSingleItem(); sl.Match("foo.bar.baz").PlainSubs.ShouldHaveSingleItem(); sl.Match("foo").PlainSubs.ShouldBeEmpty(); } [Fact] public void Match_root_full_wildcard() { var sl = new SubList(); sl.Insert(MakeSub(">")); sl.Match("foo").PlainSubs.ShouldHaveSingleItem(); sl.Match("foo.bar").PlainSubs.ShouldHaveSingleItem(); sl.Match("foo.bar.baz").PlainSubs.ShouldHaveSingleItem(); } [Fact] public void Match_collects_multiple_subs() { var sl = new SubList(); sl.Insert(MakeSub("foo.bar", sid: "1")); sl.Insert(MakeSub("foo.*", sid: "2")); sl.Insert(MakeSub("foo.>", sid: "3")); sl.Insert(MakeSub(">", sid: "4")); var r = sl.Match("foo.bar"); r.PlainSubs.Length.ShouldBe(4); } [Fact] public void Remove_subscription() { var sl = new SubList(); var sub = MakeSub("foo.bar"); sl.Insert(sub); sl.Match("foo.bar").PlainSubs.ShouldHaveSingleItem(); sl.Remove(sub); sl.Match("foo.bar").PlainSubs.ShouldBeEmpty(); } [Fact] public void Queue_group_subscriptions() { var sl = new SubList(); sl.Insert(MakeSub("foo.bar", queue: "workers", sid: "1")); sl.Insert(MakeSub("foo.bar", queue: "workers", sid: "2")); sl.Insert(MakeSub("foo.bar", queue: "loggers", sid: "3")); var r = sl.Match("foo.bar"); r.PlainSubs.ShouldBeEmpty(); r.QueueSubs.Length.ShouldBe(2); // 2 queue groups } [Fact] public void Count_tracks_subscriptions() { var sl = new SubList(); sl.Count.ShouldBe(0u); sl.Insert(MakeSub("foo", sid: "1")); sl.Insert(MakeSub("bar", sid: "2")); sl.Count.ShouldBe(2u); sl.Remove(MakeSub("foo", sid: "1")); // Remove by reference won't work — we need the same instance } [Fact] public void Count_tracks_with_same_instance() { var sl = new SubList(); var sub = MakeSub("foo"); sl.Insert(sub); sl.Count.ShouldBe(1u); sl.Remove(sub); sl.Count.ShouldBe(0u); } [Fact] public void Cache_invalidation_on_insert() { var sl = new SubList(); sl.Insert(MakeSub("foo.bar", sid: "1")); // Prime the cache var r1 = sl.Match("foo.bar"); r1.PlainSubs.ShouldHaveSingleItem(); // Insert a wildcard that matches — cache should be invalidated sl.Insert(MakeSub("foo.*", sid: "2")); var r2 = sl.Match("foo.bar"); r2.PlainSubs.Length.ShouldBe(2); } [Fact] public void Match_partial_wildcard_at_different_levels() { var sl = new SubList(); sl.Insert(MakeSub("*.bar.baz", sid: "1")); sl.Insert(MakeSub("foo.*.baz", sid: "2")); sl.Insert(MakeSub("foo.bar.*", sid: "3")); var r = sl.Match("foo.bar.baz"); r.PlainSubs.Length.ShouldBe(3); } [Fact] public void Stats_returns_correct_values() { var sl = new SubList(); sl.Insert(MakeSub("foo.bar", sid: "1")); sl.Insert(MakeSub("foo.baz", sid: "2")); sl.Match("foo.bar"); sl.Match("foo.bar"); // cache hit var stats = sl.Stats(); stats.NumSubs.ShouldBe(2u); stats.NumInserts.ShouldBe(2ul); stats.NumMatches.ShouldBe(2ul); stats.CacheHitRate.ShouldBeGreaterThan(0.0); } [Fact] public void HasInterest_returns_true_when_subscribers_exist() { var sl = new SubList(); sl.Insert(MakeSub("foo.bar")); sl.HasInterest("foo.bar").ShouldBeTrue(); sl.HasInterest("foo.baz").ShouldBeFalse(); } [Fact] public void HasInterest_with_wildcards() { var sl = new SubList(); sl.Insert(MakeSub("foo.*")); sl.HasInterest("foo.bar").ShouldBeTrue(); sl.HasInterest("bar.baz").ShouldBeFalse(); } [Fact] public void NumInterest_counts_subscribers() { var sl = new SubList(); sl.Insert(MakeSub("foo.bar", sid: "1")); sl.Insert(MakeSub("foo.*", sid: "2")); sl.Insert(MakeSub("foo.bar", queue: "q1", sid: "3")); var (np, nq) = sl.NumInterest("foo.bar"); np.ShouldBe(2); // foo.bar + foo.* nq.ShouldBe(1); // queue sub } [Fact] public void RemoveBatch_removes_all() { var sl = new SubList(); var sub1 = MakeSub("foo.bar", sid: "1"); var sub2 = MakeSub("foo.baz", sid: "2"); var sub3 = MakeSub("bar.qux", sid: "3"); sl.Insert(sub1); sl.Insert(sub2); sl.Insert(sub3); sl.Count.ShouldBe(3u); sl.RemoveBatch([sub1, sub2]); sl.Count.ShouldBe(1u); sl.Match("foo.bar").PlainSubs.ShouldBeEmpty(); sl.Match("bar.qux").PlainSubs.ShouldHaveSingleItem(); } [Fact] public void All_returns_every_subscription() { var sl = new SubList(); var sub1 = MakeSub("foo.bar", sid: "1"); var sub2 = MakeSub("foo.*", sid: "2"); var sub3 = MakeSub("bar.>", queue: "q", sid: "3"); sl.Insert(sub1); sl.Insert(sub2); sl.Insert(sub3); var all = sl.All(); all.Count.ShouldBe(3); all.ShouldContain(sub1); all.ShouldContain(sub2); all.ShouldContain(sub3); } [Fact] public void ReverseMatch_finds_patterns_matching_literal() { var sl = new SubList(); var sub1 = MakeSub("foo.bar", sid: "1"); var sub2 = MakeSub("foo.*", sid: "2"); var sub3 = MakeSub("foo.>", sid: "3"); var sub4 = MakeSub("bar.baz", sid: "4"); sl.Insert(sub1); sl.Insert(sub2); sl.Insert(sub3); sl.Insert(sub4); var result = sl.ReverseMatch("foo.bar"); result.PlainSubs.Length.ShouldBe(3); // foo.bar, foo.*, foo.> result.PlainSubs.ShouldContain(sub1); result.PlainSubs.ShouldContain(sub2); result.PlainSubs.ShouldContain(sub3); } [Fact] public void Generation_ID_invalidates_cache() { var sl = new SubList(); sl.Insert(MakeSub("foo.bar", sid: "1")); // Prime cache var r1 = sl.Match("foo.bar"); r1.PlainSubs.Length.ShouldBe(1); // Insert another sub (bumps generation) sl.Insert(MakeSub("foo.bar", sid: "2")); // Cache should be invalidated by generation mismatch var r2 = sl.Match("foo.bar"); r2.PlainSubs.Length.ShouldBe(2); } }