using NATS.Server.Subscriptions; namespace NATS.Server.Tests; public class SubjectTransformTests { [Fact] public void WildcardReplacement_SingleToken() { // foo.* -> bar.{{wildcard(1)}} var transform = SubjectTransform.Create("foo.*", "bar.{{wildcard(1)}}"); transform.ShouldNotBeNull(); transform.Apply("foo.baz").ShouldBe("bar.baz"); } [Fact] public void DollarSyntax_ReversesOrder() { // foo.*.* -> bar.$2.$1 reverses captured tokens var transform = SubjectTransform.Create("foo.*.*", "bar.$2.$1"); transform.ShouldNotBeNull(); transform.Apply("foo.A.B").ShouldBe("bar.B.A"); } [Fact] public void DollarSyntax_MultipleWildcardPositions() { // foo.*.bar.*.baz -> req.$2.$1 var transform = SubjectTransform.Create("foo.*.bar.*.baz", "req.$2.$1"); transform.ShouldNotBeNull(); transform.Apply("foo.A.bar.B.baz").ShouldBe("req.B.A"); } [Fact] public void WildcardFunction_MultiplePositions() { // foo.*.bar.*.baz -> req.{{wildcard(2)}}.{{wildcard(1)}} var transform = SubjectTransform.Create("foo.*.bar.*.baz", "req.{{wildcard(2)}}.{{wildcard(1)}}"); transform.ShouldNotBeNull(); transform.Apply("foo.A.bar.B.baz").ShouldBe("req.B.A"); } [Fact] public void FullWildcardCapture_MultiToken() { // baz.> -> my.pre.> captures multi-token remainder var transform = SubjectTransform.Create("baz.>", "my.pre.>"); transform.ShouldNotBeNull(); transform.Apply("baz.1.2.3").ShouldBe("my.pre.1.2.3"); } [Fact] public void FullWildcardCapture_FooBar() { // baz.> -> foo.bar.> var transform = SubjectTransform.Create("baz.>", "foo.bar.>"); transform.ShouldNotBeNull(); transform.Apply("baz.1.2.3").ShouldBe("foo.bar.1.2.3"); } [Fact] public void NoMatch_ReturnsNull() { var transform = SubjectTransform.Create("foo.*", "bar.$1"); transform.ShouldNotBeNull(); transform.Apply("baz.qux").ShouldBeNull(); } [Fact] public void NoMatch_WrongTokenCount() { var transform = SubjectTransform.Create("foo.*", "bar.$1"); transform.ShouldNotBeNull(); transform.Apply("foo.a.b").ShouldBeNull(); } [Fact] public void PartitionFunction_DeterministicResult() { // Partition should produce deterministic 0..N-1 results var transform = SubjectTransform.Create("*", "bar.{{partition(10)}}"); transform.ShouldNotBeNull(); // FNV-1a of "foo" mod 10 = 3 transform.Apply("foo").ShouldBe("bar.3"); // FNV-1a of "baz" mod 10 = 0 transform.Apply("baz").ShouldBe("bar.0"); // FNV-1a of "qux" mod 10 = 9 transform.Apply("qux").ShouldBe("bar.9"); } [Fact] public void PartitionFunction_ZeroBuckets() { var transform = SubjectTransform.Create("*", "bar.{{partition(0)}}"); transform.ShouldNotBeNull(); transform.Apply("baz").ShouldBe("bar.0"); } [Fact] public void PartitionFunction_WithTokenIndexes() { // partition(10, 1, 2) hashes concatenation of wildcard 1 and wildcard 2 // For source *.*: wildcard 1 -> pos 0 ("foo"), wildcard 2 -> pos 1 ("bar") // Key = "foobar" (no separator), FNV-1a("foobar") % 10 = 0 var transform = SubjectTransform.Create("*.*", "bar.{{partition(10,1,2)}}"); transform.ShouldNotBeNull(); transform.Apply("foo.bar").ShouldBe("bar.0"); } [Fact] public void PartitionFunction_WithSpecificToken() { // partition(10, 0) with wildcard source: in Go, wildcard index 0 silently // maps to source position 0 (Go map zero-value behavior). We match this. var transform = SubjectTransform.Create("*", "bar.{{partition(10, 0)}}"); transform.ShouldNotBeNull(); transform.Apply("foo").ShouldBe("bar.3"); } [Fact] public void PartitionFunction_ShorthandNoWildcardsInSource() { // When source has no wildcards, partition(n) hashes the full subject var transform = SubjectTransform.Create("foo.bar", "baz.{{partition(10)}}"); transform.ShouldNotBeNull(); transform.Apply("foo.bar").ShouldBe("baz.6"); } [Fact] public void PartitionFunction_ShorthandWithWildcards() { // partition(10) with wildcards hashes all subject tokens joined var transform = SubjectTransform.Create("*.*", "bar.{{partition(10)}}"); transform.ShouldNotBeNull(); transform.Apply("foo.bar").ShouldBe("bar.6"); } [Fact] public void SplitFunction_BasicDelimiter() { // events.a-b-c with split(1,-) -> split.a.b.c var transform = SubjectTransform.Create("*", "{{split(1,-)}}"); transform.ShouldNotBeNull(); transform.Apply("abc-def--ghi-").ShouldBe("abc.def.ghi"); } [Fact] public void SplitFunction_LeadingDelimiter() { var transform = SubjectTransform.Create("*", "{{split(1,-)}}"); transform.ShouldNotBeNull(); transform.Apply("-abc-def--ghi-").ShouldBe("abc.def.ghi"); } [Fact] public void LeftFunction_BasicTrim() { // data.abcdef with left(1,3) -> prefix.abc var transform = SubjectTransform.Create("*", "prefix.{{left(1,3)}}"); transform.ShouldNotBeNull(); transform.Apply("abcdef").ShouldBe("prefix.abc"); } [Fact] public void LeftFunction_LenExceedsToken() { var transform = SubjectTransform.Create("*", "{{left(1,6)}}"); transform.ShouldNotBeNull(); // When len exceeds token length, return full token transform.Apply("1234").ShouldBe("1234"); } [Fact] public void LeftFunction_SingleChar() { var transform = SubjectTransform.Create("*", "{{left(1,1)}}"); transform.ShouldNotBeNull(); transform.Apply("1234").ShouldBe("1"); } [Fact] public void RightFunction_BasicTrim() { // data.abcdef with right(1,3) -> suffix.def var transform = SubjectTransform.Create("*", "suffix.{{right(1,3)}}"); transform.ShouldNotBeNull(); transform.Apply("abcdef").ShouldBe("suffix.def"); } [Fact] public void RightFunction_LenExceedsToken() { var transform = SubjectTransform.Create("*", "{{right(1,6)}}"); transform.ShouldNotBeNull(); transform.Apply("1234").ShouldBe("1234"); } [Fact] public void RightFunction_SingleChar() { var transform = SubjectTransform.Create("*", "{{right(1,1)}}"); transform.ShouldNotBeNull(); transform.Apply("1234").ShouldBe("4"); } [Fact] public void RightFunction_ThreeChars() { var transform = SubjectTransform.Create("*", "{{right(1,3)}}"); transform.ShouldNotBeNull(); transform.Apply("1234").ShouldBe("234"); } [Fact] public void SplitFromLeft_BasicSplit() { // data.abcdef with splitFromLeft(1,3) -> parts.abc.def var transform = SubjectTransform.Create("*", "{{splitFromLeft(1,3)}}"); transform.ShouldNotBeNull(); transform.Apply("12345").ShouldBe("123.45"); } [Fact] public void SplitFromRight_BasicSplit() { // data.abcdef with splitFromRight(1,3) -> parts.abc.def var transform = SubjectTransform.Create("*", "{{SplitFromRight(1,3)}}"); transform.ShouldNotBeNull(); transform.Apply("12345").ShouldBe("12.345"); } [Fact] public void SliceFromLeft_BasicSlice() { // data.abcdef with sliceFromLeft(1,2) -> chunks.ab.cd.ef var transform = SubjectTransform.Create("*", "{{SliceFromLeft(1,3)}}"); transform.ShouldNotBeNull(); transform.Apply("1234567890").ShouldBe("123.456.789.0"); } [Fact] public void SliceFromRight_BasicSlice() { // data.abcdef with sliceFromRight(1,2) -> chunks.ab.cd.ef var transform = SubjectTransform.Create("*", "{{SliceFromRight(1,3)}}"); transform.ShouldNotBeNull(); transform.Apply("1234567890").ShouldBe("1.234.567.890"); } [Fact] public void LiteralPassthrough_NoWildcards() { // Literal source with no wildcards: exact match, returns dest var transform = SubjectTransform.Create("foo", "bar"); transform.ShouldNotBeNull(); transform.Apply("foo").ShouldBe("bar"); } [Fact] public void LiteralPassthrough_NoMatchOnDifferentSubject() { var transform = SubjectTransform.Create("foo", "bar"); transform.ShouldNotBeNull(); transform.Apply("baz").ShouldBeNull(); } [Fact] public void InvalidSource_ReturnsNull() { // foo.. is not a valid subject SubjectTransform.Create("foo..", "bar").ShouldBeNull(); } [Fact] public void InvalidSource_EmptyToken() { SubjectTransform.Create(".foo", "bar").ShouldBeNull(); } [Fact] public void WildcardIndexOutOfRange_ReturnsNull() { // Source has 1 wildcard but dest references $2 SubjectTransform.Create("foo.*", "bar.$2").ShouldBeNull(); } [Fact] public void DestinationWithWildcard_ReturnsNull() { // Wildcards not allowed in destination (pwc) SubjectTransform.Create("foo.*", "bar.*").ShouldBeNull(); } [Fact] public void FwcMismatch_ReturnsNull() { // If source has >, dest must also have > SubjectTransform.Create("foo.*", "bar.$1.>").ShouldBeNull(); SubjectTransform.Create("foo.>", "bar.baz").ShouldBeNull(); } [Fact] public void UnknownFunction_ReturnsNull() { SubjectTransform.Create("foo.*", "foo.{{unimplemented(1)}}").ShouldBeNull(); } [Fact] public void SingleWildcardCapture_ExpandedToBarPrefix() { var transform = SubjectTransform.Create("*", "foo.bar.$1"); transform.ShouldNotBeNull(); transform.Apply("foo").ShouldBe("foo.bar.foo"); } [Fact] public void ComboTransform_SplitAndSplitFromLeft() { // Combo: split + splitFromLeft var transform = SubjectTransform.Create("*.*", "{{split(2,-)}}.{{splitfromleft(1,2)}}"); transform.ShouldNotBeNull(); transform.Apply("foo.-abc-def--ghij-").ShouldBe("abc.def.ghij.fo.o"); } [Fact] public void PartitionFunction_NoWildcardSource_FullSubjectHash() { // foo.baz -> qux.{{partition(10)}} var transform = SubjectTransform.Create("foo.baz", "qux.{{partition(10)}}"); transform.ShouldNotBeNull(); transform.Apply("foo.baz").ShouldBe("qux.4"); } [Fact] public void PartitionFunction_NoWildcardSource_TestSubject() { var transform = SubjectTransform.Create("test.subject", "result.{{partition(5)}}"); transform.ShouldNotBeNull(); transform.Apply("test.subject").ShouldBe("result.0"); } [Fact] public void WildcardFunction_CaseInsensitive() { // Function names are case-insensitive (e.g. Wildcard, wildcard, WILDCARD) var transform = SubjectTransform.Create("foo.*", "bar.{{Wildcard(1)}}"); transform.ShouldNotBeNull(); transform.Apply("foo.test").ShouldBe("bar.test"); } [Fact] public void SplitFromLeft_CaseInsensitive() { var transform = SubjectTransform.Create("*", "{{splitfromleft(1,1)}}"); transform.ShouldNotBeNull(); // Single char split from left pos 1: "ab" -> "a.b" } [Fact] public void NotEnoughTokensInDest_PartitionWithMissingArgs() { SubjectTransform.Create("foo.*", "foo.{{partition()}}").ShouldBeNull(); } [Fact] public void WildcardFunctionBadArg_ReturnsNull() { SubjectTransform.Create("foo.*", "foo.{{wildcard(foo)}}").ShouldBeNull(); } [Fact] public void WildcardFunctionNoArgs_ReturnsNull() { SubjectTransform.Create("foo.*", "foo.{{wildcard()}}").ShouldBeNull(); } [Fact] public void WildcardFunctionTooManyArgs_ReturnsNull() { SubjectTransform.Create("foo.*", "foo.{{wildcard(1,2)}}").ShouldBeNull(); } [Fact] public void BadMustacheFormat_ReturnsNull() { SubjectTransform.Create("foo.*", "foo.{{ wildcard5) }}").ShouldBeNull(); } [Fact] public void NoWildcardSource_TransformFunctionNotAllowed() { // When source has no wildcards, only partition and random functions are allowed SubjectTransform.Create("foo", "bla.{{wildcard(1)}}").ShouldBeNull(); } }