// Copyright 2023-2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using Shouldly; using ZB.MOM.NatsNet.Server.Internal; namespace ZB.MOM.NatsNet.Server.Tests.Internal; /// /// Tests for . /// Mirrors server/subject_transform_test.go: /// TestPlaceHolderIndex (ID 2958), TestSubjectTransformHelpers (ID 2959), /// TestSubjectTransforms (ID 2960), /// TestSubjectTransformDoesntPanicTransformingMissingToken (ID 2961). /// public sealed class SubjectTransformTests { [Fact] public void PlaceHolderIndex_ShouldParseAllFunctionTypes() { // Mirror: TestPlaceHolderIndex // $1 — old style var (tt, idxs, intArg, _, err) = SubjectTransform.IndexPlaceHolders("$1"); err.ShouldBeNull(); tt.ShouldBe(TransformType.Wildcard); idxs.Length.ShouldBe(1); idxs[0].ShouldBe(1); intArg.ShouldBe(-1); // {{partition(10,1,2,3)}} (tt, idxs, intArg, _, err) = SubjectTransform.IndexPlaceHolders("{{partition(10,1,2,3)}}"); err.ShouldBeNull(); tt.ShouldBe(TransformType.Partition); idxs.ShouldBe([1, 2, 3]); intArg.ShouldBe(10); // {{ Partition (10,1,2,3) }} (with spaces) (tt, idxs, intArg, _, err) = SubjectTransform.IndexPlaceHolders("{{ Partition (10,1,2,3) }}"); err.ShouldBeNull(); tt.ShouldBe(TransformType.Partition); idxs.ShouldBe([1, 2, 3]); intArg.ShouldBe(10); // {{wildcard(2)}} (tt, idxs, intArg, _, err) = SubjectTransform.IndexPlaceHolders("{{wildcard(2)}}"); err.ShouldBeNull(); tt.ShouldBe(TransformType.Wildcard); idxs.Length.ShouldBe(1); idxs[0].ShouldBe(2); intArg.ShouldBe(-1); // {{SplitFromLeft(2,1)}} int pos; (tt, idxs, pos, _, err) = SubjectTransform.IndexPlaceHolders("{{SplitFromLeft(2,1)}}"); err.ShouldBeNull(); tt.ShouldBe(TransformType.SplitFromLeft); idxs.Length.ShouldBe(1); idxs[0].ShouldBe(2); pos.ShouldBe(1); // {{SplitFromRight(3,2)}} (tt, idxs, pos, _, err) = SubjectTransform.IndexPlaceHolders("{{SplitFromRight(3,2)}}"); err.ShouldBeNull(); tt.ShouldBe(TransformType.SplitFromRight); idxs.Length.ShouldBe(1); idxs[0].ShouldBe(3); pos.ShouldBe(2); // {{SliceFromLeft(2,2)}} (tt, idxs, var sliceSize, _, err) = SubjectTransform.IndexPlaceHolders("{{SliceFromLeft(2,2)}}"); err.ShouldBeNull(); tt.ShouldBe(TransformType.SliceFromLeft); idxs.Length.ShouldBe(1); idxs[0].ShouldBe(2); sliceSize.ShouldBe(2); // {{Left(3,2)}} (tt, idxs, pos, _, err) = SubjectTransform.IndexPlaceHolders("{{Left(3,2)}}"); err.ShouldBeNull(); tt.ShouldBe(TransformType.Left); idxs.Length.ShouldBe(1); idxs[0].ShouldBe(3); pos.ShouldBe(2); // {{Right(3,2)}} (tt, idxs, pos, _, err) = SubjectTransform.IndexPlaceHolders("{{Right(3,2)}}"); err.ShouldBeNull(); tt.ShouldBe(TransformType.Right); idxs.Length.ShouldBe(1); idxs[0].ShouldBe(3); pos.ShouldBe(2); } [Fact] public void SubjectTransformHelpers_ShouldTokenizeAndUntokenize() { // Mirror: TestSubjectTransformHelpers // transformUntokenize — no placeholders var (filter, phs) = SubjectTransform.TransformUntokenize("bar"); filter.ShouldBe("bar"); phs.Length.ShouldBe(0); // transformUntokenize — dollar-sign placeholders (filter, phs) = SubjectTransform.TransformUntokenize("foo.$2.$1"); filter.ShouldBe("foo.*.*"); phs.ShouldBe(["$2", "$1"]); // transformUntokenize — mustache placeholders (filter, phs) = SubjectTransform.TransformUntokenize("foo.{{wildcard(2)}}.{{wildcard(1)}}"); filter.ShouldBe("foo.*.*"); phs.ShouldBe(["{{wildcard(2)}}", "{{wildcard(1)}}"]); // Strict reverse transform. var (tr, err) = SubjectTransform.NewStrict("foo.*.*", "bar.$2.{{Wildcard(1)}}"); err.ShouldBeNull($"NewStrict failed: {err?.Message}"); tr.ShouldNotBeNull(); var subject = "foo.b.a"; var transformed = tr!.TransformSubject(subject); var reverse = tr.Reverse(); reverse.ShouldNotBeNull("reverse should not be null"); reverse!.TransformSubject(transformed).ShouldBe(subject, "reverse of transform should return original subject"); } [Fact] public void SubjectTransforms_ShouldValidateAndTransformCorrectly() { // Mirror: TestSubjectTransforms void ShouldErr(string src, string dest, bool strict) { var (t, e) = SubjectTransform.NewWithStrict(src, dest, strict); t.ShouldBeNull($"expected error but got transform for src={src}, dest={dest}"); var isValid = ReferenceEquals(e, ServerErrors.ErrBadSubject) || (e is MappingDestinationException mde && mde.Is(ServerErrors.ErrInvalidMappingDestination)); isValid.ShouldBeTrue( $"Expected ErrBadSubject or ErrInvalidMappingDestination for src={src}, dest={dest}, got: {e}"); } ShouldErr("foo..", "bar", false); ShouldErr("foo.*", "bar.*", false); ShouldErr("foo.*", "bar.$2", false); ShouldErr("foo.*", "bar.$1.>", false); ShouldErr("foo.>", "bar.baz", false); ShouldErr("foo.*.*", "bar.$2", true); ShouldErr("foo.*", "foo.$foo", true); ShouldErr("foo.*", "bar.{{Partition(2,1)}}", true); ShouldErr("foo.*", "foo.{{wildcard(2)}}", false); ShouldErr("foo.*", "foo.{{unimplemented(1)}}", false); ShouldErr("foo.*", "foo.{{partition()}}", false); ShouldErr("foo.*", "foo.{{random()}}", false); ShouldErr("foo.*", "foo.{{wildcard(foo)}}", false); ShouldErr("foo.*", "foo.{{wildcard()}}", false); ShouldErr("foo.*", "foo.{{wildcard(1,2)}}", false); ShouldErr("foo.*", "foo.{{ wildcard5) }}", false); ShouldErr("foo.*", "foo.{{splitLeft(2,2}}", false); ShouldErr("foo", "bla.{{wildcard(1)}}", false); ShouldErr("foo.*", $"foo.{{{{partition({(long)int.MaxValue + 1})}}}}", false); ShouldErr("foo.*", $"foo.{{{{random({(long)int.MaxValue + 1})}}}}", false); SubjectTransform? ShouldBeOK(string src, string dest, bool strict) { var (tr, err) = SubjectTransform.NewWithStrict(src, dest, strict); err.ShouldBeNull($"Got error {err} for src={src}, dest={dest}"); return tr; } ShouldBeOK("foo.*", "bar.{{Wildcard(1)}}", true); ShouldBeOK("foo.*.*", "bar.$2", false); ShouldBeOK("foo.*.*", "bar.{{wildcard(1)}}", false); ShouldBeOK("foo.*.*", "bar.{{partition(1)}}", false); ShouldBeOK("foo.*.*", "bar.{{random(5)}}", false); ShouldBeOK("foo", "bar", false); ShouldBeOK("foo.*.bar.*.baz", "req.$2.$1", false); ShouldBeOK("baz.>", "mybaz.>", false); ShouldBeOK("*", "{{splitfromleft(1,1)}}", false); ShouldBeOK("", "prefix.>", false); ShouldBeOK("*.*", "{{partition(10,1,2)}}", false); ShouldBeOK("foo.*.*", "foo.{{wildcard(1)}}.{{wildcard(2)}}.{{partition(5,1,2)}}", false); ShouldBeOK("foo.*", $"foo.{{{{partition({int.MaxValue})}}}}", false); ShouldBeOK("foo.*", $"foo.{{{{random({int.MaxValue})}}}}", false); ShouldBeOK("foo.bar", $"foo.{{{{random({int.MaxValue})}}}}", false); void ShouldMatch(string src, string dest, string sample, params string[] expected) { var tr = ShouldBeOK(src, dest, false); if (tr == null) return; var (s, err2) = tr.Match(sample); err2.ShouldBeNull($"Match error: {err2}"); expected.ShouldContain(s, $"Transform {src}→{dest} on '{sample}', got '{s}'"); } ShouldMatch("", "prefix.>", "foo", "prefix.foo"); ShouldMatch("foo", "", "foo", "foo"); ShouldMatch("foo", "bar", "foo", "bar"); ShouldMatch("foo.*.bar.*.baz", "req.$2.$1", "foo.A.bar.B.baz", "req.B.A"); ShouldMatch("foo.*.bar.*.baz", "req.{{wildcard(2)}}.{{wildcard(1)}}", "foo.A.bar.B.baz", "req.B.A"); ShouldMatch("baz.>", "my.pre.>", "baz.1.2.3", "my.pre.1.2.3"); ShouldMatch("baz.>", "foo.bar.>", "baz.1.2.3", "foo.bar.1.2.3"); ShouldMatch("*", "foo.bar.$1", "foo", "foo.bar.foo"); ShouldMatch("*", "{{splitfromleft(1,3)}}", "12345", "123.45"); ShouldMatch("*", "{{SplitFromRight(1,3)}}", "12345", "12.345"); ShouldMatch("*", "{{SliceFromLeft(1,3)}}", "1234567890", "123.456.789.0"); ShouldMatch("*", "{{SliceFromRight(1,3)}}", "1234567890", "1.234.567.890"); ShouldMatch("*", "{{split(1,-)}}", "-abc-def--ghi-", "abc.def.ghi"); ShouldMatch("*", "{{split(1,-)}}", "abc-def--ghi-", "abc.def.ghi"); ShouldMatch("*.*", "{{split(2,-)}}.{{splitfromleft(1,2)}}", "foo.-abc-def--ghij-", "abc.def.ghij.fo.o"); ShouldMatch("*", "{{right(1,1)}}", "1234", "4"); ShouldMatch("*", "{{right(1,3)}}", "1234", "234"); ShouldMatch("*", "{{right(1,6)}}", "1234", "1234"); ShouldMatch("*", "{{left(1,1)}}", "1234", "1"); ShouldMatch("*", "{{left(1,3)}}", "1234", "123"); ShouldMatch("*", "{{left(1,6)}}", "1234", "1234"); ShouldMatch("*", "bar.{{partition(0)}}", "baz", "bar.0"); ShouldMatch("*", "bar.{{partition(10, 0)}}", "foo", "bar.3"); ShouldMatch("*.*", "bar.{{partition(10)}}", "foo.bar", "bar.6"); ShouldMatch("*", "bar.{{partition(10)}}", "foo", "bar.3"); ShouldMatch("*", "bar.{{partition(10)}}", "baz", "bar.0"); ShouldMatch("*", "bar.{{partition(10)}}", "qux", "bar.9"); ShouldMatch("*", "bar.{{random(0)}}", "qux", "bar.0"); // random(6) — any value 0–5 is acceptable. for (var i = 0; i < 100; i++) ShouldMatch("*", "bar.{{random(6)}}", "qux", "bar.0", "bar.1", "bar.2", "bar.3", "bar.4", "bar.5"); ShouldBeOK("foo.bar", "baz.{{partition(10)}}", false); ShouldMatch("foo.bar", "baz.{{partition(10)}}", "foo.bar", "baz.6"); ShouldMatch("foo.baz", "qux.{{partition(10)}}", "foo.baz", "qux.4"); ShouldMatch("test.subject", "result.{{partition(5)}}", "test.subject", "result.0"); } [Fact] public void TransformTokenizedSubject_ShouldNotPanicOnMissingToken() { // Mirror: TestSubjectTransformDoesntPanicTransformingMissingToken var (tr, err) = SubjectTransform.New("foo.*", "one.two.{{wildcard(1)}}"); err.ShouldBeNull(); tr.ShouldNotBeNull(); // Should not throw even when the token at index 1 is missing. var result = tr!.TransformTokenizedSubject(["foo"]); result.ShouldBe("one.two."); } }