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.
152 lines
5.5 KiB
C#
152 lines
5.5 KiB
C#
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();
|
|
}
|
|
}
|