Move 25 gateway-related test files from NATS.Server.Tests into a dedicated NATS.Server.Gateways.Tests project. Update namespaces, replace private ReadUntilAsync with SocketTestHelper from TestUtilities, inline TestServerFactory usage, add InternalsVisibleTo, and register the project in the solution file. All 261 tests pass.
152 lines
5.5 KiB
C#
152 lines
5.5 KiB
C#
using NATS.Server.Gateways;
|
|
|
|
namespace NATS.Server.Gateways.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();
|
|
}
|
|
}
|