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,95 @@
using Microsoft.Extensions.Logging.Abstractions;
using NATS.Server.Configuration;
namespace NATS.Server.Tests.Gateways;
public class GatewayConnectionDirectionParityBatch2Tests
{
[Fact]
public async Task Gateway_manager_tracks_inbound_and_outbound_connection_sets()
{
var a = await StartServerAsync(MakeGatewayOptions("GW-A"));
var b = await StartServerAsync(MakeGatewayOptions("GW-B", a.Server.GatewayListen));
try
{
await WaitForCondition(() =>
a.Server.NumInboundGateways() == 1 &&
b.Server.NumOutboundGateways() == 1,
10000);
a.Server.NumInboundGateways().ShouldBe(1);
a.Server.NumOutboundGateways().ShouldBe(0);
b.Server.NumOutboundGateways().ShouldBe(1);
b.Server.NumInboundGateways().ShouldBe(0);
var aManager = a.Server.GatewayManager;
var bManager = b.Server.GatewayManager;
aManager.ShouldNotBeNull();
bManager.ShouldNotBeNull();
aManager!.HasInbound(b.Server.ServerId).ShouldBeTrue();
bManager!.HasInbound(a.Server.ServerId).ShouldBeFalse();
bManager.GetOutboundGatewayConnection(a.Server.ServerId).ShouldNotBeNull();
bManager.GetOutboundGatewayConnection("does-not-exist").ShouldBeNull();
aManager.GetInboundGatewayConnections().Count.ShouldBe(1);
aManager.GetOutboundGatewayConnections().Count.ShouldBe(0);
bManager.GetOutboundGatewayConnections().Count.ShouldBe(1);
bManager.GetInboundGatewayConnections().Count.ShouldBe(0);
}
finally
{
await DisposeServers(a, b);
}
}
private static NatsOptions MakeGatewayOptions(string gatewayName, string? remote = null)
{
return new NatsOptions
{
Host = "127.0.0.1",
Port = 0,
Gateway = new GatewayOptions
{
Name = gatewayName,
Host = "127.0.0.1",
Port = 0,
Remotes = remote is null ? [] : [remote],
},
};
}
private static async Task<(NatsServer Server, CancellationTokenSource Cts)> StartServerAsync(NatsOptions opts)
{
var server = new NatsServer(opts, NullLoggerFactory.Instance);
var cts = new CancellationTokenSource();
_ = server.StartAsync(cts.Token);
await server.WaitForReadyAsync();
return (server, cts);
}
private static async Task DisposeServers(params (NatsServer Server, CancellationTokenSource Cts)[] servers)
{
foreach (var (server, cts) in servers)
{
await cts.CancelAsync();
server.Dispose();
cts.Dispose();
}
}
private static async Task WaitForCondition(Func<bool> predicate, int timeoutMs)
{
using var cts = new CancellationTokenSource(timeoutMs);
while (!cts.IsCancellationRequested)
{
if (predicate())
return;
await Task.Yield();
}
throw new TimeoutException("Condition not met.");
}
}

View File

@@ -0,0 +1,61 @@
using NATS.Server.Configuration;
namespace NATS.Server.Tests.Gateways;
public class GatewayRemoteConfigParityBatch3Tests
{
[Fact]
public void RemoteGatewayOptions_tracks_connection_attempts_and_implicit_flag()
{
var cfg = new RemoteGatewayOptions { Name = "GW-B", Implicit = true };
cfg.IsImplicit().ShouldBeTrue();
cfg.GetConnAttempts().ShouldBe(0);
cfg.BumpConnAttempts().ShouldBe(1);
cfg.BumpConnAttempts().ShouldBe(2);
cfg.GetConnAttempts().ShouldBe(2);
cfg.ResetConnAttempts();
cfg.GetConnAttempts().ShouldBe(0);
}
[Fact]
public void RemoteGatewayOptions_add_and_update_urls_normalize_and_deduplicate()
{
var cfg = new RemoteGatewayOptions();
cfg.AddUrls(["127.0.0.1:7222", "nats://127.0.0.1:7222", "nats://127.0.0.1:7223"]);
cfg.Urls.Count.ShouldBe(2);
cfg.Urls.ShouldContain("nats://127.0.0.1:7222");
cfg.Urls.ShouldContain("nats://127.0.0.1:7223");
cfg.UpdateUrls(
configuredUrls: ["127.0.0.1:7333"],
discoveredUrls: ["nats://127.0.0.1:7334", "127.0.0.1:7333"]);
cfg.Urls.Count.ShouldBe(2);
cfg.Urls.ShouldContain("nats://127.0.0.1:7333");
cfg.Urls.ShouldContain("nats://127.0.0.1:7334");
}
[Fact]
public void RemoteGatewayOptions_save_tls_hostname_and_get_urls_helpers()
{
var cfg = new RemoteGatewayOptions
{
Urls = ["127.0.0.1:7444", "nats://localhost:7445"],
};
cfg.SaveTlsHostname("nats://gw.example.net:7522");
cfg.TlsName.ShouldBe("gw.example.net");
var urlStrings = cfg.GetUrlsAsStrings();
urlStrings.Count.ShouldBe(2);
urlStrings.ShouldContain("nats://127.0.0.1:7444");
urlStrings.ShouldContain("nats://localhost:7445");
var urls = cfg.GetUrls();
urls.Count.ShouldBe(2);
urls.ShouldContain(u => u.Authority == "127.0.0.1:7444");
urls.ShouldContain(u => u.Authority == "localhost:7445");
}
}

View File

@@ -0,0 +1,104 @@
using NATS.Server.Configuration;
using NATS.Server.Gateways;
namespace NATS.Server.Tests.Gateways;
public class GatewayReplyAndConfigParityBatch1Tests
{
[Fact]
public void HasGatewayReplyPrefix_accepts_new_and_old_prefixes()
{
ReplyMapper.HasGatewayReplyPrefix("_GR_.clusterA.reply").ShouldBeTrue();
ReplyMapper.HasGatewayReplyPrefix("$GR.clusterA.reply").ShouldBeTrue();
ReplyMapper.HasGatewayReplyPrefix("_INBOX.reply").ShouldBeFalse();
}
[Fact]
public void IsGatewayRoutedSubject_reports_old_prefix_flag()
{
ReplyMapper.IsGatewayRoutedSubject("_GR_.C1.r", out var newPrefixOldFlag).ShouldBeTrue();
newPrefixOldFlag.ShouldBeFalse();
ReplyMapper.IsGatewayRoutedSubject("$GR.C1.r", out var oldPrefixOldFlag).ShouldBeTrue();
oldPrefixOldFlag.ShouldBeTrue();
}
[Fact]
public void TryRestoreGatewayReply_handles_old_prefix_format()
{
ReplyMapper.TryRestoreGatewayReply("$GR.clusterA.reply.one", out var restored).ShouldBeTrue();
restored.ShouldBe("reply.one");
}
[Fact]
public void GatewayHash_helpers_are_deterministic_and_expected_length()
{
var hash1 = ReplyMapper.ComputeGatewayHash("east");
var hash2 = ReplyMapper.ComputeGatewayHash("east");
var oldHash1 = ReplyMapper.ComputeOldGatewayHash("east");
var oldHash2 = ReplyMapper.ComputeOldGatewayHash("east");
hash1.ShouldBe(hash2);
oldHash1.ShouldBe(oldHash2);
hash1.Length.ShouldBe(ReplyMapper.GatewayHashLen);
oldHash1.Length.ShouldBe(ReplyMapper.OldGatewayHashLen);
}
[Fact]
public void Legacy_prefixed_reply_extracts_cluster_and_not_hash()
{
ReplyMapper.TryExtractClusterId("$GR.clusterB.inbox.reply", out var cluster).ShouldBeTrue();
cluster.ShouldBe("clusterB");
ReplyMapper.TryExtractHash("$GR.clusterB.inbox.reply", out _).ShouldBeFalse();
}
[Fact]
public void RemoteGatewayOptions_clone_deep_copies_url_list()
{
var original = new RemoteGatewayOptions
{
Name = "gw-west",
Urls = ["nats://127.0.0.1:7522", "nats://127.0.0.1:7523"],
};
var clone = original.Clone();
clone.ShouldNotBeSameAs(original);
clone.Name.ShouldBe(original.Name);
clone.Urls.ShouldBe(original.Urls);
clone.Urls.Add("nats://127.0.0.1:7524");
original.Urls.Count.ShouldBe(2);
}
[Fact]
public void ValidateGatewayOptions_checks_required_fields()
{
GatewayManager.ValidateGatewayOptions(new GatewayOptions
{
Name = "gw",
Host = "127.0.0.1",
Port = 7222,
Remotes = ["127.0.0.1:8222"],
}, out var error).ShouldBeTrue();
error.ShouldBeNull();
GatewayManager.ValidateGatewayOptions(new GatewayOptions { Port = 7222 }, out error).ShouldBeFalse();
error.ShouldNotBeNull();
error.ShouldContain("name");
GatewayManager.ValidateGatewayOptions(new GatewayOptions { Name = "gw", Port = -1 }, out error).ShouldBeFalse();
error.ShouldNotBeNull();
error.ShouldContain("0-65535");
GatewayManager.ValidateGatewayOptions(new GatewayOptions { Name = "gw", Port = 7222, Remotes = [""] }, out error).ShouldBeFalse();
error.ShouldNotBeNull();
error.ShouldContain("cannot be empty");
}
[Fact]
public void Gateway_tls_warning_constant_is_present()
{
GatewayManager.GatewayTlsInsecureWarning.ShouldNotBeNullOrWhiteSpace();
GatewayManager.GatewayTlsInsecureWarning.ShouldContain("TLS");
}
}

View File

@@ -0,0 +1,37 @@
using Microsoft.Extensions.Logging.Abstractions;
using NATS.Server.Configuration;
namespace NATS.Server.Tests.Gateways;
public class GatewayServerAccessorParityBatch4Tests
{
[Fact]
public void Gateway_address_url_and_name_accessors_reflect_gateway_options()
{
using var server = new NatsServer(
new NatsOptions
{
Gateway = new GatewayOptions
{
Name = "gw-a",
Host = "127.0.0.1",
Port = 7222,
},
},
NullLoggerFactory.Instance);
server.GatewayAddr().ShouldBe("127.0.0.1:7222");
server.GetGatewayURL().ShouldBe("127.0.0.1:7222");
server.GetGatewayName().ShouldBe("gw-a");
}
[Fact]
public void Gateway_accessors_return_null_when_gateway_is_not_configured()
{
using var server = new NatsServer(new NatsOptions(), NullLoggerFactory.Instance);
server.GatewayAddr().ShouldBeNull();
server.GetGatewayURL().ShouldBeNull();
server.GetGatewayName().ShouldBeNull();
}
}