refactor: rename remaining tests to NATS.Server.Core.Tests
- Rename tests/NATS.Server.Tests -> tests/NATS.Server.Core.Tests - Update solution file, InternalsVisibleTo, and csproj references - Remove JETSTREAM_INTEGRATION_MATRIX and NATS.NKeys from csproj (moved to JetStream.Tests and Auth.Tests) - Update all namespaces from NATS.Server.Tests.* to NATS.Server.Core.Tests.* - Replace private GetFreePort/ReadUntilAsync helpers with TestUtilities calls - Fix stale namespace in Transport.Tests/NetworkingGoParityTests.cs
This commit is contained in:
396
tests/NATS.Server.Core.Tests/SubjectTransformTests.cs
Normal file
396
tests/NATS.Server.Core.Tests/SubjectTransformTests.cs
Normal file
@@ -0,0 +1,396 @@
|
||||
using NATS.Server.Subscriptions;
|
||||
|
||||
namespace NATS.Server.Core.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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user