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:
@@ -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.");
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user