Files
natsdotnet/tests/NATS.Server.Gateways.Tests/Gateways/ReplyMapperFullTests.cs
Joseph Doherty 9972b74bc3 refactor: extract NATS.Server.Gateways.Tests project
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.
2026-03-12 15:10:50 -04:00

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();
}
}