diff --git a/pkg/templates/templating.go b/pkg/templates/templating.go index 27d23a6620..24c280c3cb 100644 --- a/pkg/templates/templating.go +++ b/pkg/templates/templating.go @@ -53,7 +53,18 @@ func ReplacePlaceHoldersVariables(template string, dico map[string]string, rawEv parts := keys.ParamsRe.FindStringSubmatch(s) key := strings.TrimSpace(parts[1]) if strings.HasPrefix(key, "body") || strings.HasPrefix(key, "headers") || strings.HasPrefix(key, "files") { - if rawEvent != nil && headers != nil { + // Check specific requirements for each prefix + canEvaluate := false + switch { + case strings.HasPrefix(key, "body") && rawEvent != nil: + canEvaluate = true + case strings.HasPrefix(key, "headers") && headers != nil: + canEvaluate = true + case strings.HasPrefix(key, "files"): + canEvaluate = true // files evaluation doesn't depend on rawEvent or headers + } + + if canEvaluate { // convert headers to map[string]string headerMap := make(map[string]string) for k, v := range headers { diff --git a/pkg/templates/templating_test.go b/pkg/templates/templating_test.go index 3f5633af82..29770d0b49 100644 --- a/pkg/templates/templating_test.go +++ b/pkg/templates/templating_test.go @@ -2,9 +2,11 @@ package templates import ( "net/http" + "strings" "testing" "github.com/google/go-cmp/cmp" + "gotest.tools/v3/assert" ) func TestReplacePlaceHoldersVariables(t *testing.T) { @@ -72,7 +74,280 @@ func TestReplacePlaceHoldersVariables(t *testing.T) { headers: http.Header{}, rawEvent: map[string]string{}, }, + { + name: "Test with nil rawEvent and headers", + template: `body: {{ body.hello }}`, + expected: `body: {{ body.hello }}`, + dicto: map[string]string{}, + changedFiles: map[string]any{}, + headers: nil, + rawEvent: nil, + }, + { + name: "Test with nil headers only", + template: `body: {{ body.hello }}`, + expected: `body: world`, + dicto: map[string]string{}, + changedFiles: map[string]any{}, + headers: nil, + rawEvent: map[string]string{ + "hello": "world", + }, + }, + { + name: "Test CEL with numeric value", + template: `count: {{ body.count }}`, + expected: `count: 42`, + dicto: map[string]string{}, + changedFiles: map[string]any{}, + headers: http.Header{}, + rawEvent: map[string]any{ + "count": 42, + }, + }, + { + name: "Test CEL with boolean value", + template: `enabled: {{ body.enabled }}`, + expected: `enabled: true`, + dicto: map[string]string{}, + changedFiles: map[string]any{}, + headers: http.Header{}, + rawEvent: map[string]any{ + "enabled": true, + }, + }, + { + name: "Test CEL with invalid key", + template: `invalid: {{ body.nonexistent }}`, + expected: `invalid: {{ body.nonexistent }}`, + dicto: map[string]string{}, + changedFiles: map[string]any{}, + headers: http.Header{}, + rawEvent: map[string]any{ + "existing": "value", + }, + }, + { + name: "Test with file prefix but nil rawEvent", + template: `file: {{ files.modified }}`, + expected: `file: true`, + dicto: map[string]string{}, + changedFiles: map[string]any{ + "modified": "true", + }, + headers: http.Header{}, + rawEvent: nil, + }, + { + name: "Test with header prefix but nil rawEvent", + template: `header: {{ headers.Accept }}`, + expected: `header: application/json`, + dicto: map[string]string{}, + changedFiles: map[string]any{}, + headers: http.Header{ + "Accept": []string{"application/json"}, + }, + rawEvent: nil, + }, + { + name: "Test placeholder not found in dico", + template: `missing: {{ missing_key }}`, + expected: `missing: {{ missing_key }}`, + dicto: map[string]string{ + "existing_key": "value", + }, + changedFiles: map[string]any{}, + headers: http.Header{}, + rawEvent: nil, + }, + { + name: "Test multiple placeholders mixed", + template: `dico: {{ revision }}, body: {{ body.hello }}, header: {{ headers.Test }}`, + expected: `dico: main, body: world, header: value`, + dicto: map[string]string{ + "revision": "main", + }, + changedFiles: map[string]any{}, + headers: http.Header{ + "Test": []string{"value"}, + }, + rawEvent: map[string]any{ + "hello": "world", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ReplacePlaceHoldersVariables(tt.template, tt.dicto, tt.rawEvent, tt.headers, tt.changedFiles) + if d := cmp.Diff(got, tt.expected); d != "" { + t.Fatalf("-got, +want: %v", d) + } + }) } +} + +func TestReplacePlaceHoldersVariablesJSONOutput(t *testing.T) { + tests := []struct { + name string + template string + dicto map[string]string + headers http.Header + changedFiles map[string]any + rawEvent any + checkFunc func(t *testing.T, result string) + }{ + { + name: "CEL array serialization", + template: `items: {{ body.items }}`, + dicto: map[string]string{}, + changedFiles: map[string]any{}, + headers: http.Header{}, + rawEvent: map[string]any{ + "items": []string{"item1", "item2"}, + }, + checkFunc: func(t *testing.T, result string) { + assert.Assert(t, strings.Contains(result, `"item1"`), "should contain item1") + assert.Assert(t, strings.Contains(result, `"item2"`), "should contain item2") + assert.Assert(t, strings.HasPrefix(result, "items: ["), "should start with 'items: ['") + assert.Assert(t, strings.HasSuffix(result, "]"), "should end with ']'") + }, + }, + { + name: "CEL object serialization", + template: `config: {{ body.config }}`, + dicto: map[string]string{}, + changedFiles: map[string]any{}, + headers: http.Header{}, + rawEvent: map[string]any{ + "config": map[string]any{ + "name": "test", + "value": "123", + }, + }, + checkFunc: func(t *testing.T, result string) { + assert.Assert(t, strings.Contains(result, `"name"`), "should contain name key") + assert.Assert(t, strings.Contains(result, `"test"`), "should contain test value") + assert.Assert(t, strings.Contains(result, `"value"`), "should contain value key") + assert.Assert(t, strings.Contains(result, `"123"`), "should contain 123 value") + assert.Assert(t, strings.HasPrefix(result, "config: {"), "should start with 'config: {'") + assert.Assert(t, strings.HasSuffix(result, "}"), "should end with '}'") + }, + }, + { + name: "CEL double value", + template: `price: {{ body.price }}`, + dicto: map[string]string{}, + changedFiles: map[string]any{}, + headers: http.Header{}, + rawEvent: map[string]any{ + "price": 42.5, + }, + checkFunc: func(t *testing.T, result string) { + assert.Assert(t, strings.Contains(result, "42.5"), "should contain double value") + assert.Assert(t, strings.HasPrefix(result, "price: "), "should start with 'price: '") + }, + }, + { + name: "CEL int value", + template: `age: {{ body.age }}`, + dicto: map[string]string{}, + changedFiles: map[string]any{}, + headers: http.Header{}, + rawEvent: map[string]any{ + "age": int64(25), + }, + checkFunc: func(t *testing.T, result string) { + assert.Assert(t, strings.Contains(result, "25"), "should contain int value") + assert.Assert(t, strings.HasPrefix(result, "age: "), "should start with 'age: '") + }, + }, + { + name: "CEL bytes value", + template: `data: {{ body.data }}`, + dicto: map[string]string{}, + changedFiles: map[string]any{}, + headers: http.Header{}, + rawEvent: map[string]any{ + "data": []byte("hello"), + }, + checkFunc: func(t *testing.T, result string) { + assert.Assert(t, strings.HasPrefix(result, "data: "), "should start with 'data: '") + // Bytes might be base64 encoded or handled differently + assert.Assert(t, len(result) > len("data: "), "should have some data content") + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ReplacePlaceHoldersVariables(tt.template, tt.dicto, tt.rawEvent, tt.headers, tt.changedFiles) + tt.checkFunc(t, got) + }) + } +} + +func TestReplacePlaceHoldersVariablesEdgeCases(t *testing.T) { + tests := []struct { + name string + template string + dicto map[string]string + headers http.Header + changedFiles map[string]any + rawEvent any + expected string + }{ + { + name: "CEL expression with complex nested access", + template: `nested: {{ body.deep.nested.value }}`, + dicto: map[string]string{}, + changedFiles: map[string]any{}, + headers: http.Header{}, + rawEvent: map[string]any{ + "deep": map[string]any{ + "nested": map[string]any{ + "value": "found", + }, + }, + }, + expected: `nested: found`, + }, + { + name: "CEL with nil in nested structure", + template: `null: {{ body.value }}`, + dicto: map[string]string{}, + changedFiles: map[string]any{}, + headers: http.Header{}, + rawEvent: map[string]any{ + "value": nil, + }, + expected: `null: `, + }, + { + name: "Multi-level headers access", + template: `user-agent: {{ headers["User-Agent"] }}`, + dicto: map[string]string{}, + changedFiles: map[string]any{}, + headers: http.Header{ + "User-Agent": []string{"test-agent/1.0"}, + }, + rawEvent: map[string]any{}, + expected: `user-agent: test-agent/1.0`, + }, + { + name: "Files with nested structure", + template: `file-info: {{ files.metadata.size }}`, + dicto: map[string]string{}, + changedFiles: map[string]any{ + "metadata": map[string]any{ + "size": 1024, + }, + }, + headers: http.Header{}, + rawEvent: nil, + expected: `file-info: 1024`, + }, + } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := ReplacePlaceHoldersVariables(tt.template, tt.dicto, tt.rawEvent, tt.headers, tt.changedFiles)