using NATS.Server.Gateways; namespace NATS.Server.Tests.Gateways; /// /// 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. /// 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(); } }