Fix E2E test gaps and add comprehensive E2E + parity test suites

- Fix pull consumer fetch: send original stream subject in HMSG (not inbox)
  so NATS client distinguishes data messages from control messages
- Fix MaxAge expiry: add background timer in StreamManager for periodic pruning
- Fix JetStream wire format: Go-compatible anonymous objects with string enums,
  proper offset-based pagination for stream/consumer list APIs
- Add 42 E2E black-box tests (core messaging, auth, TLS, accounts, JetStream)
- Add ~1000 parity tests across all subsystems (gaps closure)
- Update gap inventory docs to reflect implementation status
This commit is contained in:
Joseph Doherty
2026-03-12 14:09:23 -04:00
parent 79c1ee8776
commit c30e67a69d
226 changed files with 17801 additions and 709 deletions

View File

@@ -0,0 +1,46 @@
using NATS.Server.Subscriptions;
namespace NATS.Server.Tests.Subscriptions;
public class SubListCtorAndNotificationParityTests
{
[Fact]
public void Constructor_with_enableCache_false_disables_cache()
{
var subList = new SubList(enableCache: false);
subList.CacheEnabled().ShouldBeFalse();
}
[Fact]
public void NewSublistNoCache_factory_disables_cache()
{
var subList = SubList.NewSublistNoCache();
subList.CacheEnabled().ShouldBeFalse();
}
[Fact]
public void RegisterNotification_emits_true_on_first_interest_and_false_on_last_interest()
{
var subList = new SubList();
var notifications = new List<bool>();
subList.RegisterNotification(v => notifications.Add(v));
var sub = new Subscription { Subject = "foo", Sid = "1" };
subList.Insert(sub);
subList.Remove(sub);
notifications.ShouldBe([true, false]);
}
[Fact]
public void SubjectMatch_alias_helpers_match_existing_behavior()
{
SubjectMatch.SubjectHasWildcard("foo.*").ShouldBeTrue();
SubjectMatch.SubjectHasWildcard("foo.bar").ShouldBeFalse();
SubjectMatch.IsValidLiteralSubject("foo.bar").ShouldBeTrue();
SubjectMatch.IsValidLiteralSubject("foo.*").ShouldBeFalse();
}
}

View File

@@ -0,0 +1,130 @@
using NATS.Server;
using NATS.Server.Auth;
using NATS.Server.Protocol;
using NATS.Server.Subscriptions;
namespace NATS.Server.Tests.Subscriptions;
public class SubListParityBatch2Tests
{
[Fact]
public void RegisterQueueNotification_tracks_first_and_last_exact_queue_interest()
{
var subList = new SubList();
var notifications = new List<bool>();
Action<bool> callback = hasInterest => notifications.Add(hasInterest);
subList.RegisterQueueNotification("foo.bar", "q", callback).ShouldBeTrue();
notifications.ShouldBe([false]);
var sub1 = new Subscription { Subject = "foo.bar", Queue = "q", Sid = "1" };
var sub2 = new Subscription { Subject = "foo.bar", Queue = "q", Sid = "2" };
subList.Insert(sub1);
subList.Insert(sub2);
notifications.ShouldBe([false, true]);
subList.Remove(sub1);
notifications.ShouldBe([false, true]);
subList.Remove(sub2);
notifications.ShouldBe([false, true, false]);
subList.ClearQueueNotification("foo.bar", "q", callback).ShouldBeTrue();
}
[Fact]
public void UpdateRemoteQSub_updates_queue_weight_for_match_remote()
{
var subList = new SubList();
var original = new RemoteSubscription("foo.bar", "q", "R1", Account: "A", QueueWeight: 1);
subList.ApplyRemoteSub(original);
subList.MatchRemote("A", "foo.bar").Count.ShouldBe(1);
subList.UpdateRemoteQSub(original with { QueueWeight = 3 });
subList.MatchRemote("A", "foo.bar").Count.ShouldBe(3);
}
[Fact]
public void SubListStats_Add_aggregates_stats_like_go()
{
var stats = new SubListStats
{
NumSubs = 1,
NumCache = 2,
NumInserts = 3,
NumRemoves = 4,
NumMatches = 10,
MaxFanout = 5,
TotalFanout = 8,
CacheEntries = 2,
CacheHits = 6,
};
stats.Add(new SubListStats
{
NumSubs = 2,
NumCache = 3,
NumInserts = 4,
NumRemoves = 5,
NumMatches = 30,
MaxFanout = 9,
TotalFanout = 12,
CacheEntries = 3,
CacheHits = 15,
});
stats.NumSubs.ShouldBe((uint)3);
stats.NumCache.ShouldBe((uint)5);
stats.NumInserts.ShouldBe((ulong)7);
stats.NumRemoves.ShouldBe((ulong)9);
stats.NumMatches.ShouldBe((ulong)40);
stats.MaxFanout.ShouldBe((uint)9);
stats.AvgFanout.ShouldBe(4.0);
stats.CacheHitRate.ShouldBe(0.525);
}
[Fact]
public void NumLevels_returns_max_trie_depth()
{
var subList = new SubList();
subList.Insert(new Subscription { Subject = "foo.bar.baz", Sid = "1" });
subList.Insert(new Subscription { Subject = "foo.bar", Sid = "2" });
subList.NumLevels().ShouldBe(3);
}
[Fact]
public void LocalSubs_filters_non_local_kinds_and_optionally_includes_leaf()
{
var subList = new SubList();
subList.Insert(new Subscription { Subject = "foo.a", Sid = "1", Client = new TestClient(ClientKind.Client) });
subList.Insert(new Subscription { Subject = "foo.b", Sid = "2", Client = new TestClient(ClientKind.Router) });
subList.Insert(new Subscription { Subject = "foo.c", Sid = "3", Client = new TestClient(ClientKind.System) });
subList.Insert(new Subscription { Subject = "foo.d", Sid = "4", Client = new TestClient(ClientKind.Leaf) });
var local = subList.LocalSubs(includeLeafHubs: false).Select(s => s.Sid).OrderBy(x => x).ToArray();
local.ShouldBe(["1", "3"]);
var withLeaf = subList.LocalSubs(includeLeafHubs: true).Select(s => s.Sid).OrderBy(x => x).ToArray();
withLeaf.ShouldBe(["1", "3", "4"]);
}
private sealed class TestClient(ClientKind kind) : INatsClient
{
public ulong Id => 1;
public ClientKind Kind => kind;
public Account? Account => null;
public ClientOptions? ClientOpts => null;
public ClientPermissions? Permissions => null;
public void SendMessage(string subject, string sid, string? replyTo, ReadOnlyMemory<byte> headers, ReadOnlyMemory<byte> payload)
{
}
public bool QueueOutbound(ReadOnlyMemory<byte> data) => true;
public void RemoveSubscription(string sid)
{
}
}
}

View File

@@ -0,0 +1,40 @@
using NATS.Server.Subscriptions;
namespace NATS.Server.Tests.Subscriptions;
public class SubjectSubsetMatchParityBatch1Tests
{
[Theory]
[InlineData("foo.bar", "foo.bar", true)]
[InlineData("foo.bar", "foo.*", true)]
[InlineData("foo.bar", "foo.>", true)]
[InlineData("foo.bar", "*.*", true)]
[InlineData("foo.bar", ">", true)]
[InlineData("foo.bar", "foo.baz", false)]
[InlineData("foo.bar.baz", "foo.*", false)]
public void SubjectMatchesFilter_matches_go_subset_behavior(string subject, string filter, bool expected)
{
SubjectMatch.SubjectMatchesFilter(subject, filter).ShouldBe(expected);
}
[Fact]
public void SubjectIsSubsetMatch_uses_subject_tokens_against_test_pattern()
{
SubjectMatch.SubjectIsSubsetMatch("foo.*", "foo.*").ShouldBeTrue();
SubjectMatch.SubjectIsSubsetMatch("foo.*", "foo.bar").ShouldBeFalse();
}
[Fact]
public void IsSubsetMatch_tokenizes_test_subject_and_delegates_to_tokenized_matcher()
{
SubjectMatch.IsSubsetMatch(["foo", "bar"], "foo.*").ShouldBeTrue();
SubjectMatch.IsSubsetMatch(["foo", "bar"], "foo.baz").ShouldBeFalse();
}
[Fact]
public void IsSubsetMatchTokenized_handles_fwc_and_rejects_empty_tokens_like_go()
{
SubjectMatch.IsSubsetMatchTokenized(["foo", "bar"], ["foo", ">"]).ShouldBeTrue();
SubjectMatch.IsSubsetMatchTokenized(["foo", "bar"], ["foo", ""]).ShouldBeFalse();
}
}

View File

@@ -0,0 +1,83 @@
using NATS.Server.Subscriptions;
namespace NATS.Server.Tests.Subscriptions;
public class SubjectTransformParityBatch3Tests
{
[Fact]
public void ValidateMapping_accepts_supported_templates_and_rejects_invalid_templates()
{
SubjectTransform.ValidateMapping("dest.$1").ShouldBeTrue();
SubjectTransform.ValidateMapping("dest.{{partition(10)}}").ShouldBeTrue();
SubjectTransform.ValidateMapping("dest.{{random(5)}}").ShouldBeTrue();
SubjectTransform.ValidateMapping("dest.*").ShouldBeFalse();
SubjectTransform.ValidateMapping("dest.{{wildcard()}}").ShouldBeFalse();
}
[Fact]
public void NewSubjectTransformStrict_requires_all_source_wildcards_to_be_used()
{
SubjectTransform.NewSubjectTransformWithStrict("foo.*.*", "bar.$1", strict: true).ShouldBeNull();
SubjectTransform.NewSubjectTransformWithStrict("foo.*.*", "bar.$1", strict: false).ShouldNotBeNull();
}
[Fact]
public void NewSubjectTransformStrict_accepts_when_all_source_wildcards_are_used()
{
var transform = SubjectTransform.NewSubjectTransformStrict("foo.*.*", "bar.$2.$1");
transform.ShouldNotBeNull();
transform.Apply("foo.A.B").ShouldBe("bar.B.A");
}
[Fact]
public void Random_transform_function_returns_bucket_in_range()
{
var transform = SubjectTransform.Create("*", "rand.{{random(3)}}");
transform.ShouldNotBeNull();
for (var i = 0; i < 20; i++)
{
var output = transform.Apply("foo");
output.ShouldNotBeNull();
var parts = output!.Split('.');
parts.Length.ShouldBe(2);
int.TryParse(parts[1], out var bucket).ShouldBeTrue();
bucket.ShouldBeGreaterThanOrEqualTo(0);
bucket.ShouldBeLessThan(3);
}
}
[Fact]
public void TransformTokenize_and_transformUntokenize_round_trip_wildcards()
{
var tokenized = SubjectTransform.TransformTokenize("foo.*.*");
tokenized.ShouldBe("foo.$1.$2");
var untokenized = SubjectTransform.TransformUntokenize(tokenized);
untokenized.ShouldBe("foo.*.*");
}
[Fact]
public void Reverse_produces_inverse_transform_for_reordered_wildcards()
{
var forward = SubjectTransform.Create("foo.*.*", "bar.$2.$1");
forward.ShouldNotBeNull();
var reverse = forward.Reverse();
reverse.ShouldNotBeNull();
var mapped = forward.Apply("foo.A.B");
mapped.ShouldBe("bar.B.A");
reverse.Apply(mapped!).ShouldBe("foo.A.B");
}
[Fact]
public void TransformSubject_applies_transform_without_source_match_guard()
{
var transform = SubjectTransform.Create("foo.*", "bar.$1");
transform.ShouldNotBeNull();
transform.TransformSubject("baz.qux").ShouldBe("bar.qux");
}
}