feat(networking): expand gateway reply mapper and add leaf solicited connections (D4+D5)
D4: Add hash segment support to ReplyMapper (_GR_.{cluster}.{hash}.{reply}),
FNV-1a ComputeReplyHash, TryExtractClusterId/Hash, legacy format compat.
D5: Add ConnectSolicitedAsync with exponential backoff (1s-60s cap),
JetStreamDomain propagation in LEAF handshake, LeafNodeOptions.JetStreamDomain.
This commit is contained in:
151
tests/NATS.Server.Tests/Gateways/ReplyMapperFullTests.cs
Normal file
151
tests/NATS.Server.Tests/Gateways/ReplyMapperFullTests.cs
Normal file
@@ -0,0 +1,151 @@
|
||||
using NATS.Server.Gateways;
|
||||
|
||||
namespace NATS.Server.Tests.Gateways;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for the expanded ReplyMapper with hash support.
|
||||
/// Covers new format (_GR_.{clusterId}.{hash}.{reply}), legacy format (_GR_.{clusterId}.{reply}),
|
||||
/// cluster/hash extraction, and FNV-1a hash determinism.
|
||||
/// Go reference: gateway.go:2000-2100, gateway.go:340-380.
|
||||
/// </summary>
|
||||
public class ReplyMapperFullTests
|
||||
{
|
||||
// Go: gateway.go — replyPfx includes cluster hash + server hash segments
|
||||
[Fact]
|
||||
public void ToGatewayReply_WithHash_IncludesHashSegment()
|
||||
{
|
||||
var result = ReplyMapper.ToGatewayReply("_INBOX.abc123", "clusterA", 42);
|
||||
|
||||
result.ShouldNotBeNull();
|
||||
result.ShouldBe("_GR_.clusterA.42._INBOX.abc123");
|
||||
}
|
||||
|
||||
// Go: gateway.go — hash is deterministic based on reply subject
|
||||
[Fact]
|
||||
public void ToGatewayReply_AutoHash_IsDeterministic()
|
||||
{
|
||||
var result1 = ReplyMapper.ToGatewayReply("_INBOX.xyz", "cluster1");
|
||||
var result2 = ReplyMapper.ToGatewayReply("_INBOX.xyz", "cluster1");
|
||||
|
||||
result1.ShouldNotBeNull();
|
||||
result2.ShouldNotBeNull();
|
||||
result1.ShouldBe(result2);
|
||||
|
||||
// Should contain the hash segment between cluster and reply
|
||||
result1!.ShouldStartWith("_GR_.cluster1.");
|
||||
result1.ShouldEndWith("._INBOX.xyz");
|
||||
|
||||
// Parse the hash segment
|
||||
var afterPrefix = result1["_GR_.cluster1.".Length..];
|
||||
var dotIdx = afterPrefix.IndexOf('.');
|
||||
dotIdx.ShouldBeGreaterThan(0);
|
||||
var hashStr = afterPrefix[..dotIdx];
|
||||
long.TryParse(hashStr, out var hash).ShouldBeTrue();
|
||||
hash.ShouldBeGreaterThan(0);
|
||||
}
|
||||
|
||||
// Go: handleGatewayReply — strips _GR_ prefix + cluster + hash to restore original
|
||||
[Fact]
|
||||
public void TryRestoreGatewayReply_WithHash_RestoresOriginal()
|
||||
{
|
||||
var hash = ReplyMapper.ComputeReplyHash("reply.subject");
|
||||
var mapped = ReplyMapper.ToGatewayReply("reply.subject", "clusterB", hash);
|
||||
|
||||
var success = ReplyMapper.TryRestoreGatewayReply(mapped, out var restored);
|
||||
|
||||
success.ShouldBeTrue();
|
||||
restored.ShouldBe("reply.subject");
|
||||
}
|
||||
|
||||
// Go: handleGatewayReply — legacy $GR. and old _GR_ formats without hash
|
||||
[Fact]
|
||||
public void TryRestoreGatewayReply_LegacyNoHash_StillWorks()
|
||||
{
|
||||
// Legacy format: _GR_.{clusterId}.{reply} (no hash segment)
|
||||
// The reply itself starts with a non-numeric character, so it won't be mistaken for a hash.
|
||||
var legacyReply = "_GR_.clusterX.my.reply.subject";
|
||||
|
||||
var success = ReplyMapper.TryRestoreGatewayReply(legacyReply, out var restored);
|
||||
|
||||
success.ShouldBeTrue();
|
||||
restored.ShouldBe("my.reply.subject");
|
||||
}
|
||||
|
||||
// Go: handleGatewayReply — nested _GR_ prefixes from multi-hop gateways
|
||||
[Fact]
|
||||
public void TryRestoreGatewayReply_NestedPrefixes_UnwrapsAll()
|
||||
{
|
||||
// Inner: _GR_.cluster1.{hash}.original.reply
|
||||
var hash1 = ReplyMapper.ComputeReplyHash("original.reply");
|
||||
var inner = ReplyMapper.ToGatewayReply("original.reply", "cluster1", hash1);
|
||||
|
||||
// Outer: _GR_.cluster2.{hash2}.{inner}
|
||||
var hash2 = ReplyMapper.ComputeReplyHash(inner!);
|
||||
var outer = ReplyMapper.ToGatewayReply(inner, "cluster2", hash2);
|
||||
|
||||
var success = ReplyMapper.TryRestoreGatewayReply(outer, out var restored);
|
||||
|
||||
success.ShouldBeTrue();
|
||||
restored.ShouldBe("original.reply");
|
||||
}
|
||||
|
||||
// Go: gateway.go — cluster hash extraction for routing decisions
|
||||
[Fact]
|
||||
public void TryExtractClusterId_ValidReply_ExtractsId()
|
||||
{
|
||||
var mapped = ReplyMapper.ToGatewayReply("test.reply", "myCluster", 999);
|
||||
|
||||
var success = ReplyMapper.TryExtractClusterId(mapped, out var clusterId);
|
||||
|
||||
success.ShouldBeTrue();
|
||||
clusterId.ShouldBe("myCluster");
|
||||
}
|
||||
|
||||
// Go: gateway.go — hash extraction for reply deduplication
|
||||
[Fact]
|
||||
public void TryExtractHash_ValidReply_ExtractsHash()
|
||||
{
|
||||
var mapped = ReplyMapper.ToGatewayReply("inbox.abc", "clusterZ", 12345);
|
||||
|
||||
var success = ReplyMapper.TryExtractHash(mapped, out var hash);
|
||||
|
||||
success.ShouldBeTrue();
|
||||
hash.ShouldBe(12345);
|
||||
}
|
||||
|
||||
// Go: getGWHash — hash must be deterministic for same input
|
||||
[Fact]
|
||||
public void ComputeReplyHash_Deterministic()
|
||||
{
|
||||
var hash1 = ReplyMapper.ComputeReplyHash("_INBOX.test123");
|
||||
var hash2 = ReplyMapper.ComputeReplyHash("_INBOX.test123");
|
||||
|
||||
hash1.ShouldBe(hash2);
|
||||
hash1.ShouldBeGreaterThan(0);
|
||||
}
|
||||
|
||||
// Go: getGWHash — different inputs should produce different hashes
|
||||
[Fact]
|
||||
public void ComputeReplyHash_DifferentInputs_DifferentHashes()
|
||||
{
|
||||
var hash1 = ReplyMapper.ComputeReplyHash("_INBOX.aaa");
|
||||
var hash2 = ReplyMapper.ComputeReplyHash("_INBOX.bbb");
|
||||
var hash3 = ReplyMapper.ComputeReplyHash("reply.subject.1");
|
||||
|
||||
hash1.ShouldNotBe(hash2);
|
||||
hash1.ShouldNotBe(hash3);
|
||||
hash2.ShouldNotBe(hash3);
|
||||
}
|
||||
|
||||
// Go: isGWRoutedReply — plain subjects should not match gateway prefix
|
||||
[Fact]
|
||||
public void HasGatewayReplyPrefix_PlainSubject_ReturnsFalse()
|
||||
{
|
||||
ReplyMapper.HasGatewayReplyPrefix("foo.bar").ShouldBeFalse();
|
||||
ReplyMapper.HasGatewayReplyPrefix("_INBOX.test").ShouldBeFalse();
|
||||
ReplyMapper.HasGatewayReplyPrefix(null).ShouldBeFalse();
|
||||
ReplyMapper.HasGatewayReplyPrefix("").ShouldBeFalse();
|
||||
ReplyMapper.HasGatewayReplyPrefix("_GR_").ShouldBeFalse(); // No trailing dot
|
||||
ReplyMapper.HasGatewayReplyPrefix("_GR_.cluster.reply").ShouldBeTrue();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user