Repository: nats-io/nats-server
Pull request #5617 (2024-07-04 21:32:14)

Improve error reporting for subject transforms in streams
Improve error reporting for bad subject mapping transform sources and destinations.

Add checking of subject transforms in stream configuration for mirroring subject transform and stream subject transform.

Add missing JS API Errors:
JSMirrorInvalidTransformDestination
JSStreamTransformInvalidSource
JSStreamTransformInvalidDestination



Patch:
diff --git a/server/accounts.go b/server/accounts.go
index aca96b5d..05a78154 100644
--- a/server/accounts.go
+++ b/server/accounts.go
@@ -660,7 +660,7 @@ func (a *Account) AddWeightedMappings(src string, dests ...*MapDest) error {
 		if tw[d.Cluster] > 100 {
 			return fmt.Errorf("total weight needs to be <= 100")
 		}
-		err := ValidateMappingDestination(d.Subject)
+		err := ValidateMapping(src, d.Subject)
 		if err != nil {
 			return err
 		}
diff --git a/server/errors.go b/server/errors.go
index 8efa7ac0..5a91140d 100644
--- a/server/errors.go
+++ b/server/errors.go
@@ -203,7 +203,7 @@ var (
 	ErrInvalidMappingDestination = errors.New("invalid mapping destination")
 
 	// ErrInvalidMappingDestinationSubject is used to error on a bad transform destination mapping
-	ErrInvalidMappingDestinationSubject = fmt.Errorf("%w: invalid subject", ErrInvalidMappingDestination)
+	ErrInvalidMappingDestinationSubject = fmt.Errorf("%w: invalid transform", ErrInvalidMappingDestination)
 
 	// ErrMappingDestinationNotUsingAllWildcards is used to error on a transform destination not using all of the token wildcards
 	ErrMappingDestinationNotUsingAllWildcards = fmt.Errorf("%w: not using all of the token wildcard(s)", ErrInvalidMappingDestination)
diff --git a/server/errors.json b/server/errors.json
index 79602466..039eda78 100644
--- a/server/errors.json
+++ b/server/errors.json
@@ -1433,7 +1433,7 @@
     "constant": "JSSourceInvalidSubjectFilter",
     "code": 400,
     "error_code": 10145,
-    "description": "source subject filter is invalid",
+    "description": "source transform source: {err}",
     "comment": "",
     "help": "",
     "url": "",
@@ -1443,7 +1443,7 @@
     "constant": "JSSourceInvalidTransformDestination",
     "code": 400,
     "error_code": 10146,
-    "description": "source transform destination is invalid",
+    "description": "source transform: {err}",
     "comment": "",
     "help": "",
     "url": "",
@@ -1493,7 +1493,7 @@
     "constant": "JSMirrorInvalidSubjectFilter",
     "code": 400,
     "error_code": 10151,
-    "description": "mirror subject filter is invalid",
+    "description": "mirror transform source: {err}",
     "comment": "",
     "help": "",
     "url": "",
@@ -1518,5 +1518,35 @@
     "help": "",
     "url": "",
     "deprecates": ""
+  },
+  {
+    "constant": "JSMirrorInvalidTransformDestination",
+    "code": 400,
+    "error_code": 10154,
+    "description": "mirror transform: {err}",
+    "comment": "",
+    "help": "",
+    "url": "",
+    "deprecates": ""
+  },
+  {
+    "constant": "JSStreamTransformInvalidSource",
+    "code": 400,
+    "error_code": 10155,
+    "description": "stream transform source: {err}",
+    "comment": "",
+    "help": "",
+    "url": "",
+    "deprecates": ""
+  },
+  {
+    "constant": "JSStreamTransformInvalidDestination",
+    "code": 400,
+    "error_code": 10156,
+    "description": "stream transform: {err}",
+    "comment": "",
+    "help": "",
+    "url": "",
+    "deprecates": ""
   }
 ]
diff --git a/server/jetstream_errors_generated.go b/server/jetstream_errors_generated.go
index 1543eeeb..4c6b594c 100644
--- a/server/jetstream_errors_generated.go
+++ b/server/jetstream_errors_generated.go
@@ -236,9 +236,12 @@ const (
 	// JSMirrorInvalidStreamName mirrored stream name is invalid
 	JSMirrorInvalidStreamName ErrorIdentifier = 10142
 
-	// JSMirrorInvalidSubjectFilter mirror subject filter is invalid
+	// JSMirrorInvalidSubjectFilter mirror transform source: {err}
 	JSMirrorInvalidSubjectFilter ErrorIdentifier = 10151
 
+	// JSMirrorInvalidTransformDestination mirror transform: {err}
+	JSMirrorInvalidTransformDestination ErrorIdentifier = 10154
+
 	// JSMirrorMaxMessageSizeTooBigErr stream mirror must have max message size >= source
 	JSMirrorMaxMessageSizeTooBigErr ErrorIdentifier = 10030
 
@@ -308,10 +311,10 @@ const (
 	// JSSourceInvalidStreamName sourced stream name is invalid
 	JSSourceInvalidStreamName ErrorIdentifier = 10141
 
-	// JSSourceInvalidSubjectFilter source subject filter is invalid
+	// JSSourceInvalidSubjectFilter source transform source: {err}
 	JSSourceInvalidSubjectFilter ErrorIdentifier = 10145
 
-	// JSSourceInvalidTransformDestination source transform destination is invalid
+	// JSSourceInvalidTransformDestination source transform: {err}
 	JSSourceInvalidTransformDestination ErrorIdentifier = 10146
 
 	// JSSourceMaxMessageSizeTooBigErr stream source must have max message size >= target
@@ -446,6 +449,12 @@ const (
 	// JSStreamTemplateNotFoundErr template not found
 	JSStreamTemplateNotFoundErr ErrorIdentifier = 10068
 
+	// JSStreamTransformInvalidDestination stream transform: {err}
+	JSStreamTransformInvalidDestination ErrorIdentifier = 10156
+
+	// JSStreamTransformInvalidSource stream transform source: {err}
+	JSStreamTransformInvalidSource ErrorIdentifier = 10155
+
 	// JSStreamUpdateErrF Generic stream update error string ({err})
 	JSStreamUpdateErrF ErrorIdentifier = 10069
 
@@ -541,7 +550,8 @@ var (
 		JSMemoryResourcesExceededErr:               {Code: 500, ErrCode: 10028, Description: "insufficient memory resources available"},
 		JSMirrorConsumerSetupFailedErrF:            {Code: 500, ErrCode: 10029, Description: "{err}"},
 		JSMirrorInvalidStreamName:                  {Code: 400, ErrCode: 10142, Description: "mirrored stream name is invalid"},
-		JSMirrorInvalidSubjectFilter:               {Code: 400, ErrCode: 10151, Description: "mirror subject filter is invalid"},
+		JSMirrorInvalidSubjectFilter:               {Code: 400, ErrCode: 10151, Description: "mirror transform source: {err}"},
+		JSMirrorInvalidTransformDestination:        {Code: 400, ErrCode: 10154, Description: "mirror transform: {err}"},
 		JSMirrorMaxMessageSizeTooBigErr:            {Code: 400, ErrCode: 10030, Description: "stream mirror must have max message size >= source"},
 		JSMirrorMultipleFiltersNotAllowed:          {Code: 400, ErrCode: 10150, Description: "mirror with multiple subject transforms cannot also have a single subject filter"},
 		JSMirrorOverlappingSubjectFilters:          {Code: 400, ErrCode: 10152, Description: "mirror subject filters can not overlap"},
@@ -565,8 +575,8 @@ var (
 		JSSourceConsumerSetupFailedErrF:            {Code: 500, ErrCode: 10045, Description: "{err}"},
 		JSSourceDuplicateDetected:                  {Code: 400, ErrCode: 10140, Description: "duplicate source configuration detected"},
 		JSSourceInvalidStreamName:                  {Code: 400, ErrCode: 10141, Description: "sourced stream name is invalid"},
-		JSSourceInvalidSubjectFilter:               {Code: 400, ErrCode: 10145, Description: "source subject filter is invalid"},
-		JSSourceInvalidTransformDestination:        {Code: 400, ErrCode: 10146, Description: "source transform destination is invalid"},
+		JSSourceInvalidSubjectFilter:               {Code: 400, ErrCode: 10145, Description: "source transform source: {err}"},
+		JSSourceInvalidTransformDestination:        {Code: 400, ErrCode: 10146, Description: "source transform: {err}"},
 		JSSourceMaxMessageSizeTooBigErr:            {Code: 400, ErrCode: 10046, Description: "stream source must have max message size >= target"},
 		JSSourceMultipleFiltersNotAllowed:          {Code: 400, ErrCode: 10144, Description: "source with multiple subject transforms cannot also have a single subject filter"},
 		JSSourceOverlappingSubjectFilters:          {Code: 400, ErrCode: 10147, Description: "source filters can not overlap"},
@@ -611,6 +621,8 @@ var (
 		JSStreamTemplateCreateErrF:                 {Code: 500, ErrCode: 10066, Description: "{err}"},
 		JSStreamTemplateDeleteErrF:                 {Code: 500, ErrCode: 10067, Description: "{err}"},
 		JSStreamTemplateNotFoundErr:                {Code: 404, ErrCode: 10068, Description: "template not found"},
+		JSStreamTransformInvalidDestination:        {Code: 400, ErrCode: 10156, Description: "stream transform: {err}"},
+		JSStreamTransformInvalidSource:             {Code: 400, ErrCode: 10155, Description: "stream transform source: {err}"},
 		JSStreamUpdateErrF:                         {Code: 500, ErrCode: 10069, Description: "{err}"},
 		JSStreamWrongLastMsgIDErrF:                 {Code: 400, ErrCode: 10070, Description: "wrong last msg ID: {id}"},
 		JSStreamWrongLastSequenceErrF:              {Code: 400, ErrCode: 10071, Description: "wrong last sequence: {seq}"},
@@ -1483,14 +1495,36 @@ func NewJSMirrorInvalidStreamNameError(opts ...ErrorOption) *ApiError {
 	return ApiErrors[JSMirrorInvalidStreamName]
 }
 
-// NewJSMirrorInvalidSubjectFilterError creates a new JSMirrorInvalidSubjectFilter error: "mirror subject filter is invalid"
-func NewJSMirrorInvalidSubjectFilterError(opts ...ErrorOption) *ApiError {
+// NewJSMirrorInvalidSubjectFilterError creates a new JSMirrorInvalidSubjectFilter error: "mirror transform source: {err}"
+func NewJSMirrorInvalidSubjectFilterError(err error, opts ...ErrorOption) *ApiError {
+	eopts := parseOpts(opts)
+	if ae, ok := eopts.err.(*ApiError); ok {
+		return ae
+	}
+
+	e := ApiErrors[JSMirrorInvalidSubjectFilter]
+	args := e.toReplacerArgs([]interface{}{"{err}", err})
+	return &ApiError{
+		Code:        e.Code,
+		ErrCode:     e.ErrCode,
+		Description: strings.NewReplacer(args...).Replace(e.Description),
+	}
+}
+
+// NewJSMirrorInvalidTransformDestinationError creates a new JSMirrorInvalidTransformDestination error: "mirror transform: {err}"
+func NewJSMirrorInvalidTransformDestinationError(err error, opts ...ErrorOption) *ApiError {
 	eopts := parseOpts(opts)
 	if ae, ok := eopts.err.(*ApiError); ok {
 		return ae
 	}
 
-	return ApiErrors[JSMirrorInvalidSubjectFilter]
+	e := ApiErrors[JSMirrorInvalidTransformDestination]
+	args := e.toReplacerArgs([]interface{}{"{err}", err})
+	return &ApiError{
+		Code:        e.Code,
+		ErrCode:     e.ErrCode,
+		Description: strings.NewReplacer(args...).Replace(e.Description),
+	}
 }
 
 // NewJSMirrorMaxMessageSizeTooBigError creates a new JSMirrorMaxMessageSizeTooBigErr error: "stream mirror must have max message size >= source"
@@ -1747,24 +1781,36 @@ func NewJSSourceInvalidStreamNameError(opts ...ErrorOption) *ApiError {
 	return ApiErrors[JSSourceInvalidStreamName]
 }
 
-// NewJSSourceInvalidSubjectFilterError creates a new JSSourceInvalidSubjectFilter error: "source subject filter is invalid"
-func NewJSSourceInvalidSubjectFilterError(opts ...ErrorOption) *ApiError {
+// NewJSSourceInvalidSubjectFilterError creates a new JSSourceInvalidSubjectFilter error: "source transform source: {err}"
+func NewJSSourceInvalidSubjectFilterError(err error, opts ...ErrorOption) *ApiError {
 	eopts := parseOpts(opts)
 	if ae, ok := eopts.err.(*ApiError); ok {
 		return ae
 	}
 
-	return ApiErrors[JSSourceInvalidSubjectFilter]
+	e := ApiErrors[JSSourceInvalidSubjectFilter]
+	args := e.toReplacerArgs([]interface{}{"{err}", err})
+	return &ApiError{
+		Code:        e.Code,
+		ErrCode:     e.ErrCode,
+		Description: strings.NewReplacer(args...).Replace(e.Description),
+	}
 }
 
-// NewJSSourceInvalidTransformDestinationError creates a new JSSourceInvalidTransformDestination error: "source transform destination is invalid"
-func NewJSSourceInvalidTransformDestinationError(opts ...ErrorOption) *ApiError {
+// NewJSSourceInvalidTransformDestinationError creates a new JSSourceInvalidTransformDestination error: "source transform: {err}"
+func NewJSSourceInvalidTransformDestinationError(err error, opts ...ErrorOption) *ApiError {
 	eopts := parseOpts(opts)
 	if ae, ok := eopts.err.(*ApiError); ok {
 		return ae
 	}
 
-	return ApiErrors[JSSourceInvalidTransformDestination]
+	e := ApiErrors[JSSourceInvalidTransformDestination]
+	args := e.toReplacerArgs([]interface{}{"{err}", err})
+	return &ApiError{
+		Code:        e.Code,
+		ErrCode:     e.ErrCode,
+		Description: strings.NewReplacer(args...).Replace(e.Description),
+	}
 }
 
 // NewJSSourceMaxMessageSizeTooBigError creates a new JSSourceMaxMessageSizeTooBigErr error: "stream source must have max message size >= target"
@@ -2315,6 +2361,38 @@ func NewJSStreamTemplateNotFoundError(opts ...ErrorOption) *ApiError {
 	return ApiErrors[JSStreamTemplateNotFoundErr]
 }
 
+// NewJSStreamTransformInvalidDestinationError creates a new JSStreamTransformInvalidDestination error: "stream transform: {err}"
+func NewJSStreamTransformInvalidDestinationError(err error, opts ...ErrorOption) *ApiError {
+	eopts := parseOpts(opts)
+	if ae, ok := eopts.err.(*ApiError); ok {
+		return ae
+	}
+
+	e := ApiErrors[JSStreamTransformInvalidDestination]
+	args := e.toReplacerArgs([]interface{}{"{err}", err})
+	return &ApiError{
+		Code:        e.Code,
+		ErrCode:     e.ErrCode,
+		Description: strings.NewReplacer(args...).Replace(e.Description),
+	}
+}
+
+// NewJSStreamTransformInvalidSourceError creates a new JSStreamTransformInvalidSource error: "stream transform source: {err}"
+func NewJSStreamTransformInvalidSourceError(err error, opts ...ErrorOption) *ApiError {
+	eopts := parseOpts(opts)
+	if ae, ok := eopts.err.(*ApiError); ok {
+		return ae
+	}
+
+	e := ApiErrors[JSStreamTransformInvalidSource]
+	args := e.toReplacerArgs([]interface{}{"{err}", err})
+	return &ApiError{
+		Code:        e.Code,
+		ErrCode:     e.ErrCode,
+		Description: strings.NewReplacer(args...).Replace(e.Description),
+	}
+}
+
 // NewJSStreamUpdateError creates a new JSStreamUpdateErrF error: "{err}"
 func NewJSStreamUpdateError(err error, opts ...ErrorOption) *ApiError {
 	eopts := parseOpts(opts)
diff --git a/server/stream.go b/server/stream.go
index 8bbcc9aa..545a1dac 100644
--- a/server/stream.go
+++ b/server/stream.go
@@ -1306,9 +1306,15 @@ func (s *Server) checkStreamCfg(config *StreamConfig, acc *Account) (StreamConfi
 		}
 		// Check subject filters overlap.
 		for outer, tr := range cfg.Mirror.SubjectTransforms {
-			if !IsValidSubject(tr.Source) {
-				return StreamConfig{}, NewJSMirrorInvalidSubjectFilterError()
+			if tr.Source != _EMPTY_ && !IsValidSubject(tr.Source) {
+				return StreamConfig{}, NewJSMirrorInvalidSubjectFilterError(fmt.Errorf("%w %s", ErrBadSubject, tr.Source))
 			}
+
+			err := ValidateMapping(tr.Source, tr.Destination)
+			if err != nil {
+				return StreamConfig{}, NewJSMirrorInvalidTransformDestinationError(err)
+			}
+
 			for inner, innertr := range cfg.Mirror.SubjectTransforms {
 				if inner != outer && SubjectsCollide(tr.Source, innertr.Source) {
 					return StreamConfig{}, NewJSMirrorOverlappingSubjectFiltersError()
@@ -1350,10 +1356,10 @@ func (s *Server) checkStreamCfg(config *StreamConfig, acc *Account) (StreamConfi
 			if cfg.Mirror.External.DeliverPrefix != _EMPTY_ {
 				deliveryPrefixes = append(deliveryPrefixes, cfg.Mirror.External.DeliverPrefix)
 			}
+
 			if cfg.Mirror.External.ApiPrefix != _EMPTY_ {
 				apiPrefixes = append(apiPrefixes, cfg.Mirror.External.ApiPrefix)
 			}
-
 		}
 	}
 
@@ -1386,17 +1392,18 @@ func (s *Server) checkStreamCfg(config *StreamConfig, acc *Account) (StreamConfi
 			}
 
 			for _, tr := range src.SubjectTransforms {
-				err := ValidateMappingDestination(tr.Destination)
+				if tr.Source != _EMPTY_ && !IsValidSubject(tr.Source) {
+					return StreamConfig{}, NewJSSourceInvalidSubjectFilterError(fmt.Errorf("%w %s", ErrBadSubject, tr.Source))
+				}
+
+				err := ValidateMapping(tr.Source, tr.Destination)
 				if err != nil {
-					return StreamConfig{}, NewJSSourceInvalidTransformDestinationError()
+					return StreamConfig{}, NewJSSourceInvalidTransformDestinationError(err)
 				}
 			}
 
 			// Check subject filters overlap.
 			for outer, tr := range src.SubjectTransforms {
-				if !IsValidSubject(tr.Source) {
-					return StreamConfig{}, NewJSSourceInvalidSubjectFilterError()
-				}
 				for inner, innertr := range src.SubjectTransforms {
 					if inner != outer && subjectIsSubsetMatch(tr.Source, innertr.Source) {
 						return StreamConfig{}, NewJSSourceOverlappingSubjectFiltersError()
@@ -1577,6 +1584,18 @@ func (s *Server) checkStreamCfg(config *StreamConfig, acc *Account) (StreamConfi
 		}
 	}
 
+	// Check the subject transform if any
+	if cfg.SubjectTransform != nil {
+		if cfg.SubjectTransform.Source != _EMPTY_ && !IsValidSubject(cfg.SubjectTransform.Source) {
+			return StreamConfig{}, NewJSStreamTransformInvalidSourceError(fmt.Errorf("%w %s", ErrBadSubject, cfg.SubjectTransform.Source))
+		}
+
+		err := ValidateMapping(cfg.SubjectTransform.Source, cfg.SubjectTransform.Destination)
+		if err != nil {
+			return StreamConfig{}, NewJSStreamTransformInvalidDestinationError(err)
+		}
+	}
+
 	return cfg, nil
 }
 
diff --git a/server/sublist.go b/server/sublist.go
index 0000ad9f..c0e7a181 100644
--- a/server/sublist.go
+++ b/server/sublist.go
@@ -1225,12 +1225,12 @@ func isValidLiteralSubject(tokens []string) bool {
 	return true
 }
 
-// ValidateMappingDestination returns nil error if the subject is a valid subject mapping destination subject
-func ValidateMappingDestination(subject string) error {
-	if subject == _EMPTY_ {
+// ValidateMapping returns nil error if the subject is a valid subject mapping destination subject
+func ValidateMapping(src string, dest string) error {
+	if dest == _EMPTY_ {
 		return nil
 	}
-	subjectTokens := strings.Split(subject, tsep)
+	subjectTokens := strings.Split(dest, tsep)
 	sfwc := false
 	for _, t := range subjectTokens {
 		length := len(t)
@@ -1238,6 +1238,7 @@ func ValidateMappingDestination(subject string) error {
 			return &mappingDestinationErr{t, ErrInvalidMappingDestinationSubject}
 		}
 
+		// if it looks like it contains a mapping function, it should be a valid mapping function
 		if length > 4 && t[0] == '{' && t[1] == '{' && t[length-2] == '}' && t[length-1] == '}' {
 			if !partitionMappingFunctionRegEx.MatchString(t) &&
 				!wildcardMappingFunctionRegEx.MatchString(t) &&
@@ -1258,7 +1259,10 @@ func ValidateMappingDestination(subject string) error {
 			return ErrInvalidMappingDestinationSubject
 		}
 	}
-	return nil
+
+	// Finally, verify that the transform can actually be created from the source and destination
+	_, err := NewSubjectTransform(src, dest)
+	return err
 }
 
 // Will check tokens and report back if the have any partial or full wildcards.


Test patch:
diff --git a/server/jetstream_test.go b/server/jetstream_test.go
index d153255a..4cc3730d 100644
--- a/server/jetstream_test.go
+++ b/server/jetstream_test.go
@@ -11577,7 +11577,7 @@ func TestJetStreamMirrorBasics(t *testing.T) {
 		Name:    "MBAD",
 		Storage: FileStorage,
 		Mirror:  &StreamSource{Name: "S1", SubjectTransforms: []SubjectTransformConfig{{Source: "*", Destination: "{{wildcard(2)}}"}}},
-	}, ApiErrors[JSStreamCreateErrF].ErrCode)
+	}, ApiErrors[JSMirrorInvalidTransformDestination].ErrCode)
 
 	createStreamServerStreamConfig(&StreamConfig{
 		Name:    "MBAD",
@@ -23760,6 +23760,67 @@ func TestJetStreamBadSubjectMappingStream(t *testing.T) {
 			},
 		},
 	})
+	require_Error(t, err, NewJSStreamUpdateError(errors.New("nats: source transform: invalid mapping destination: too many arguments passed to the function in {{wildcard(1)}}{{split(3,1)}}")))
 
-	require_Error(t, err, NewJSStreamUpdateError(errors.New("unable to get subject transform for source: invalid mapping destination: too many arguments passed to the function in {{wildcard(1)}}{{split(3,1)}}")))
+	_, err = js.UpdateStream(&nats.StreamConfig{
+		Name: "test",
+		Sources: []*nats.StreamSource{
+			{
+				Name: "mapping",
+				SubjectTransforms: []nats.SubjectTransformConfig{
+					{
+						Source:      "events.>.*",
+						Destination: "events.{{split(1,1)}}",
+					},
+				},
+			},
+		},
+	})
+	require_Error(t, err, NewJSStreamUpdateError(errors.New("nats: source transform source: invalid subject events.>.*")))
+
+	_, err = js.UpdateStream(&nats.StreamConfig{
+		Name: "test",
+		Mirror: &nats.StreamSource{
+			Name: "mapping",
+			SubjectTransforms: []nats.SubjectTransformConfig{
+				{
+					Source:      "events.*",
+					Destination: "events.{{split(3,1)}}",
+				},
+			},
+		},
+	})
+	require_Error(t, err, NewJSStreamUpdateError(errors.New("nats: mirror transform: invalid mapping destination: wildcard index out of range in {{split(3,1)}}: [3]")))
+
+	_, err = js.UpdateStream(&nats.StreamConfig{
+		Name: "test",
+		Mirror: &nats.StreamSource{
+			Name: "mapping",
+			SubjectTransforms: []nats.SubjectTransformConfig{
+				{
+					Source:      "events.>.*",
+					Destination: "events.{{split(1,1)}}",
+				},
+			},
+		},
+	})
+	require_Error(t, err, NewJSStreamUpdateError(errors.New("nats: mirror transform source: invalid subject events.>.*")))
+
+	_, err = js.UpdateStream(&nats.StreamConfig{
+		Name: "test",
+		SubjectTransform: &nats.SubjectTransformConfig{
+			Source:      "events.*",
+			Destination: "events.{{split(3,1)}}",
+		},
+	})
+	require_Error(t, err, NewJSStreamUpdateError(errors.New("nats: stream transform: invalid mapping destination: wildcard index out of range in {{split(3,1)}}: [3]")))
+
+	_, err = js.UpdateStream(&nats.StreamConfig{
+		Name: "test",
+		SubjectTransform: &nats.SubjectTransformConfig{
+			Source:      "events.>.*",
+			Destination: "events.{{split(1,1)}}",
+		},
+	})
+	require_Error(t, err, NewJSStreamUpdateError(errors.New("nats: stream transform source: invalid subject events.>.*")))
 }
diff --git a/server/sublist_test.go b/server/sublist_test.go
index 966401b5..e7d78b7b 100644
--- a/server/sublist_test.go
+++ b/server/sublist_test.go
@@ -679,24 +679,24 @@ func TestSubjectIsLiteral(t *testing.T) {
 }
 
 func TestValidateDestinationSubject(t *testing.T) {
-	checkError(ValidateMappingDestination("foo"), nil, t)
-	checkError(ValidateMappingDestination("foo.bar"), nil, t)
-	checkError(ValidateMappingDestination("*"), nil, t)
-	checkError(ValidateMappingDestination(">"), nil, t)
-	checkError(ValidateMappingDestination("foo.*"), nil, t)
-	checkError(ValidateMappingDestination("foo.>"), nil, t)
-	checkError(ValidateMappingDestination("foo.*.>"), nil, t)
-	checkError(ValidateMappingDestination("foo.*.bar"), nil, t)
-	checkError(ValidateMappingDestination("foo.bar.>"), nil, t)
-	checkError(ValidateMappingDestination("foo.{{wildcard(1)}}"), nil, t)
-	checkError(ValidateMappingDestination("foo.{{ wildcard(1) }}"), nil, t)
-	checkError(ValidateMappingDestination("foo.{{wildcard( 1 )}}"), nil, t)
-	checkError(ValidateMappingDestination("foo.{{partition(2,1)}}"), nil, t)
-	checkError(ValidateMappingDestination("foo.{{SplitFromLeft(2,1)}}"), nil, t)
-	checkError(ValidateMappingDestination("foo.{{SplitFromRight(2,1)}}"), nil, t)
-	checkError(ValidateMappingDestination("foo.{{unknown(1)}}"), ErrInvalidMappingDestination, t)
-	checkError(ValidateMappingDestination("foo..}"), ErrInvalidMappingDestination, t)
-	checkError(ValidateMappingDestination("foo. bar}"), ErrInvalidMappingDestinationSubject, t)
+	checkError(ValidateMapping("bar", "foo"), nil, t)
+	checkError(ValidateMapping("foo", "foo.bar"), nil, t)
+	checkError(ValidateMapping("*", "{{wildcard(1)}}"), nil, t)
+	checkError(ValidateMapping("foo.>", ">"), nil, t)
+	checkError(ValidateMapping("foo.*", "bar.{{wildcard(1)}}"), nil, t)
+	checkError(ValidateMapping("foo.>", "bar.>"), nil, t)
+	checkError(ValidateMapping("foo.*.>", "bar.{{wildcard(1)}}.>"), nil, t)
+	checkError(ValidateMapping("foo.*.bar", "bar.{{wildcard(1)}}.foo"), nil, t)
+	checkError(ValidateMapping("foo.bar.>", "foo.bar.foo.>"), nil, t)
+	checkError(ValidateMapping("*", "foo.{{wildcard(1)}}"), nil, t)
+	checkError(ValidateMapping("*", "foo.{{ wildcard(1) }}"), nil, t)
+	checkError(ValidateMapping("*", "foo.{{wildcard( 1 )}}"), nil, t)
+	checkError(ValidateMapping("*", "foo.{{partition(2,1)}}"), nil, t)
+	checkError(ValidateMapping("*.*", "foo.{{SplitFromLeft(2,1)}}"), nil, t)
+	checkError(ValidateMapping("*.*", "foo.{{SplitFromRight(2,1)}}"), nil, t)
+	checkError(ValidateMapping("*", "foo.{{unknown(1)}}"), ErrInvalidMappingDestination, t)
+	checkError(ValidateMapping("foo", "foo..}"), ErrInvalidMappingDestination, t)
+	checkError(ValidateMapping("foo", "foo. bar}"), ErrInvalidMappingDestinationSubject, t)
 
 }