// Copyright 2020-2026 Project Capsule Authors // SPDX-License-Identifier: Apache-2.0 package template_test import ( "sync" "testing" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" tpl "github.com/projectcapsule/capsule/pkg/template" ) func TestRequiresFastTemplate(t *testing.T) { t.Parallel() tests := []struct { name string input string expected bool }{ { name: "no braces", input: "plain text with no template markers", expected: false, }, { name: "only opening braces", input: "value with {{ but no closing", expected: false, }, { name: "only closing braces", input: "value with }} but no opening", expected: false, }, { name: "proper template expression", input: "hello {{ .Name }}", expected: true, }, { name: "multiple template expressions", input: "{{ .A }} and {{ .B }}", expected: true, }, { name: "braces without spaces", input: "{{.Value}}", expected: true, }, { name: "empty string", input: "", expected: false, }, { name: "only opening and closing braces but separated", input: "text {{ middle }} end", expected: true, }, { name: "single braces not considered template", input: "{ value }", expected: false, }, { name: "nested braces", input: "{{ {{ .Nested }} }}", expected: true, }, } for _, tt := range tests { tt := tt // capture range variable t.Run(tt.name, func(t *testing.T) { t.Parallel() got := tpl.RequiresFastTemplate(tt.input) if got != tt.expected { t.Fatalf( "RequiresFastTemplate(%q) = %v, expected %v", tt.input, got, tt.expected, ) } }) } } func TestTemplateForTenantAndNamespace_ReplacesPlaceholders(t *testing.T) { tplContext := map[string]string{ "tenant.name": "tenant-a", "namespace": "ns-1", } got := tpl.FastTemplate( "tenant={{tenant.name}}, ns={{namespace}}", tplContext, ) want := "tenant=tenant-a, ns=ns-1" if got != want { t.Fatalf("expected %q, got %q", want, got) } } func TestTemplateForTenantAndNamespace_ReplacesPlaceholdersSpaces(t *testing.T) { tplContext := map[string]string{ "tenant.name": "tenant-a", "namespace": "ns-1", } got := tpl.FastTemplate( "tenant={{ tenant.name }}, ns={{ namespace }}", tplContext, ) want := "tenant=tenant-a, ns=ns-1" if got != want { t.Fatalf("expected %q, got %q", want, got) } } func TestTemplateForTenantAndNamespace_OnlyTenant(t *testing.T) { tplContext := map[string]string{ "tenant.name": "tenant-x", "namespace": "ns-y", } got := tpl.FastTemplate("T={{tenant.name}}", tplContext) want := "T=tenant-x" if got != want { t.Fatalf("expected %q, got %q", want, got) } } func TestTemplateForTenantAndNamespace_OnlyNamespace(t *testing.T) { tplContext := map[string]string{ "tenant.name": "tenant-x", "namespace": "ns-y", } got := tpl.FastTemplate("N={{namespace}}", tplContext) want := "N=ns-y" if got != want { t.Fatalf("expected %q, got %q", want, got) } } func TestTemplateForTenantAndNamespace_NoDelimiters_ReturnsInput(t *testing.T) { tplContext := map[string]string{ "tenant.name": "tenant-a", "namespace": "ns-1", } in := "plain-value-without-templates" got := tpl.FastTemplate(in, tplContext) if got != in { t.Fatalf("expected %q, got %q", in, got) } } func TestTemplateForTenantAndNamespace_UnknownKeyBecomesEmpty(t *testing.T) { tplContext := map[string]string{ "tenant.name": "tenant-a", "namespace": "ns-1", } got := tpl.FastTemplate("X={{unknown.key}}", tplContext) want := "X=" if got != want { t.Fatalf("expected %q, got %q", want, got) } } func TestTemplateForTenantAndNamespaceMap_ReplacesPlaceholders(t *testing.T) { tplContext := map[string]string{ "tenant.name": "tenant-a", "namespace": "ns-1", } orig := map[string]string{ "key1": "tenant={{tenant.name}}, ns={{namespace}}", "key2": "plain-value", } out := tpl.FastTemplateMap(orig, tplContext) // output is templated if got := out["key1"]; got != "tenant=tenant-a, ns=ns-1" { t.Fatalf("key1: expected %q, got %q", "tenant=tenant-a, ns=ns-1", got) } if got := out["key2"]; got != "plain-value" { t.Fatalf("key2: expected %q, got %q", "plain-value", got) } // input map must remain unchanged (new behavior) if got := orig["key1"]; got != "tenant={{tenant.name}}, ns={{namespace}}" { t.Fatalf("input map must not be mutated; key1 got %q", got) } } func TestTemplateForTenantAndNamespaceMap_ReplacesPlaceholdersSpaces(t *testing.T) { tplContext := map[string]string{ "tenant.name": "tenant-a", "namespace": "ns-1", } orig := map[string]string{ "key1": "tenant={{ tenant.name }}, ns={{ namespace }}", "key2": "plain-value", } out := tpl.FastTemplateMap(orig, tplContext) if got := out["key1"]; got != "tenant=tenant-a, ns=ns-1" { t.Fatalf("key1: expected %q, got %q", "tenant=tenant-a, ns=ns-1", got) } if got := out["key2"]; got != "plain-value" { t.Fatalf("key2: expected %q, got %q", "plain-value", got) } // input map must remain unchanged if got := orig["key1"]; got != "tenant={{ tenant.name }}, ns={{ namespace }}" { t.Fatalf("input map must not be mutated; key1 got %q", got) } } func TestTemplateForTenantAndNamespaceMap_TransformsValuesWithDelimiters(t *testing.T) { tplContext := map[string]string{ "tenant.name": "tenant-a", "namespace": "ns-1", } orig := map[string]string{ "t1": "hello {{tenant.name}}", "t2": "namespace {{namespace}}", "t3": "static", } out := tpl.FastTemplateMap(orig, tplContext) if got := out["t1"]; got != "hello tenant-a" { t.Fatalf("t1: expected %q, got %q", "hello tenant-a", got) } if got := out["t2"]; got != "namespace ns-1" { t.Fatalf("t2: expected %q, got %q", "namespace ns-1", got) } if got := out["t3"]; got != "static" { t.Fatalf("t3: expected %q, got %q", "static", got) } } func TestTemplateForTenantAndNamespaceMap_MixedKeys(t *testing.T) { tplContext := map[string]string{ "tenant.name": "tenant-x", "namespace": "ns-x", } orig := map[string]string{ "onlyTenant": "T={{ tenant.name }}", "onlyNS": "N={{ namespace }}", "none": "static", } out := tpl.FastTemplateMap(orig, tplContext) if got := out["onlyTenant"]; got != "T=tenant-x" { t.Fatalf("onlyTenant: expected %q, got %q", "T=tenant-x", got) } if got := out["onlyNS"]; got != "N=ns-x" { t.Fatalf("onlyNS: expected %q, got %q", "N=ns-x", got) } if got := out["none"]; got != "static" { t.Fatalf("none: expected %q, got %q", "static", got) } } func TestTemplateForTenantAndNamespaceMap_UnknownKeyBecomesEmpty(t *testing.T) { tplContext := map[string]string{ "tenant.name": "tenant-a", "namespace": "ns-1", } orig := map[string]string{ "unknown": "X={{ unknown.key }}", } out := tpl.FastTemplateMap(orig, tplContext) if got := out["unknown"]; got != "X=" { t.Fatalf("unknown: expected %q, got %q", "X=", got) } } func TestTemplateForTenantAndNamespaceMap_EmptyOrNilInput(t *testing.T) { tplContext := map[string]string{ "tenant.name": "tenant-a", "namespace": "ns-1", } // nil map outNil := tpl.FastTemplateMap(nil, tplContext) if outNil == nil { t.Fatalf("expected non-nil map for nil input") } if len(outNil) != 0 { t.Fatalf("expected empty map for nil input, got %v", outNil) } // empty map outEmpty := tpl.FastTemplateMap(map[string]string{}, tplContext) if outEmpty == nil || len(outEmpty) != 0 { t.Fatalf("expected empty map, got %v", outEmpty) } } // Concurrency test: should never panic with "concurrent map writes" // Run with: go test -race ./... func TestTemplateForTenantAndNamespaceMap_Concurrency(t *testing.T) { tplContext := map[string]string{ "tenant.name": "tenant-a", "namespace": "ns-1", } // Shared input map across goroutines (this used to be unsafe if the function mutated in-place) shared := map[string]string{ "k1": "tenant={{tenant.name}}", "k2": "ns={{namespace}}", "k3": "static", } const goroutines = 50 const iterations = 200 var wg sync.WaitGroup wg.Add(goroutines) for i := 0; i < goroutines; i++ { go func() { defer wg.Done() for j := 0; j < iterations; j++ { out := tpl.FastTemplateMap(shared, tplContext) // sanity checks if out["k1"] != "tenant=tenant-a" { t.Errorf("unexpected k1: %q", out["k1"]) return } if out["k2"] != "ns=ns-1" { t.Errorf("unexpected k2: %q", out["k2"]) return } if out["k3"] != "static" { t.Errorf("unexpected k3: %q", out["k3"]) return } } }() } wg.Wait() // verify input map was not mutated if shared["k1"] != "tenant={{tenant.name}}" { t.Fatalf("input map mutated under concurrency: k1=%q", shared["k1"]) } if shared["k2"] != "ns={{namespace}}" { t.Fatalf("input map mutated under concurrency: k2=%q", shared["k2"]) } } func TestFastTemplateLabelSelector(t *testing.T) { t.Parallel() t.Run("nil selector returns nil, nil", func(t *testing.T) { t.Parallel() got, err := tpl.FastTemplateLabelSelector(nil, map[string]string{"x": "y"}) if err != nil { t.Fatalf("expected err=nil, got %v", err) } if got != nil { t.Fatalf("expected selector=nil, got %#v", got) } }) t.Run("does not mutate input (deep copy)", func(t *testing.T) { t.Parallel() in := &metav1.LabelSelector{ MatchLabels: map[string]string{ "created-by": "{{ controller }}", }, MatchExpressions: []metav1.LabelSelectorRequirement{ { Key: "{{ key }}", Operator: metav1.LabelSelectorOpIn, Values: []string{"{{ v1 }}", "{{ v2 }}"}, }, }, } ctx := map[string]string{ "controller": "capsule", "key": "env", "v1": "prod", "v2": "staging", } orig := in.DeepCopy() got, err := tpl.FastTemplateLabelSelector(in, ctx) if err != nil { t.Fatalf("expected err=nil, got %v", err) } if got == nil { t.Fatalf("expected non-nil selector") } // Input must remain unchanged if in.MatchLabels["created-by"] != orig.MatchLabels["created-by"] { t.Fatalf("input was mutated: MatchLabels value changed from %q to %q", orig.MatchLabels["created-by"], in.MatchLabels["created-by"]) } if in.MatchExpressions[0].Key != orig.MatchExpressions[0].Key { t.Fatalf("input was mutated: MatchExpressions[0].Key changed from %q to %q", orig.MatchExpressions[0].Key, in.MatchExpressions[0].Key) } if in.MatchExpressions[0].Values[0] != orig.MatchExpressions[0].Values[0] || in.MatchExpressions[0].Values[1] != orig.MatchExpressions[0].Values[1] { t.Fatalf("input was mutated: MatchExpressions[0].Values changed from %#v to %#v", orig.MatchExpressions[0].Values, in.MatchExpressions[0].Values) } // Output should be templated if got.MatchLabels["created-by"] != "capsule" { t.Fatalf("expected templated MatchLabels[created-by]=capsule, got %q", got.MatchLabels["created-by"]) } if got.MatchExpressions[0].Key != "env" { t.Fatalf("expected templated MatchExpressions[0].Key=env, got %q", got.MatchExpressions[0].Key) } if len(got.MatchExpressions[0].Values) != 2 || got.MatchExpressions[0].Values[0] != "prod" || got.MatchExpressions[0].Values[1] != "staging" { t.Fatalf("expected templated values [prod staging], got %#v", got.MatchExpressions[0].Values) } }) t.Run("templates matchLabels keys and values via FastTemplateMap", func(t *testing.T) { t.Parallel() in := &metav1.LabelSelector{ MatchLabels: map[string]string{ "{{ k1 }}": "{{ v1 }}", "static": "{{ v2 }}", }, } ctx := map[string]string{ "k1": "app", "v1": "demo", "v2": "x", } got, err := tpl.FastTemplateLabelSelector(in, ctx) if err != nil { t.Fatalf("expected err=nil, got %v", err) } if got == nil { t.Fatalf("expected non-nil selector") } if _, ok := got.MatchLabels["app"]; !ok { t.Fatalf("expected templated key 'app' to exist; got keys: %#v", got.MatchLabels) } if got.MatchLabels["app"] != "demo" { t.Fatalf("expected MatchLabels[app]=demo, got %q", got.MatchLabels["app"]) } if got.MatchLabels["static"] != "x" { t.Fatalf("expected MatchLabels[static]=x, got %q", got.MatchLabels["static"]) } }) t.Run("templates matchExpressions key and values", func(t *testing.T) { t.Parallel() in := &metav1.LabelSelector{ MatchExpressions: []metav1.LabelSelectorRequirement{ { Key: "tier-{{ t }}", Operator: metav1.LabelSelectorOpIn, Values: []string{"{{ a }}", "{{ b }}"}, }, }, } ctx := map[string]string{"t": "id", "a": "gold", "b": "silver"} got, err := tpl.FastTemplateLabelSelector(in, ctx) if err != nil { t.Fatalf("expected err=nil, got %v", err) } if got.MatchExpressions[0].Key != "tier-id" { t.Fatalf("expected key=tier-id, got %q", got.MatchExpressions[0].Key) } if got.MatchExpressions[0].Values[0] != "gold" || got.MatchExpressions[0].Values[1] != "silver" { t.Fatalf("expected values [gold silver], got %#v", got.MatchExpressions[0].Values) } }) t.Run("returns error when templating produces invalid selector (empty key)", func(t *testing.T) { t.Parallel() // After templating, Key becomes empty which is invalid for a selector. in := &metav1.LabelSelector{ MatchExpressions: []metav1.LabelSelectorRequirement{ { Key: "{{ missing }}", Operator: metav1.LabelSelectorOpExists, }, }, } got, err := tpl.FastTemplateLabelSelector(in, map[string]string{}) if err == nil { t.Fatalf("expected error, got nil (selector=%#v)", got) } }) t.Run("key overwrite risk: two templated keys collapse into one without error", func(t *testing.T) { t.Parallel() // This test documents current behavior (no collision protection). // Both keys template to "app". The resulting map will have a single entry. in := &metav1.LabelSelector{ MatchLabels: map[string]string{ "{{ k1 }}": "v1", "{{ k2 }}": "v2", }, } ctx := map[string]string{"k1": "app", "k2": "app"} got, err := tpl.FastTemplateLabelSelector(in, ctx) if err != nil { t.Fatalf("expected err=nil, got %v", err) } if got == nil { t.Fatalf("expected non-nil selector") } // Only one key should remain due to collision overwrite behavior. if len(got.MatchLabels) != 1 { t.Fatalf("expected 1 key after collision, got %d (%#v)", len(got.MatchLabels), got.MatchLabels) } if _, ok := got.MatchLabels["app"]; !ok { t.Fatalf("expected final key 'app' to exist, got %#v", got.MatchLabels) } // We intentionally do NOT assert which value wins since map iteration order is randomized. // This is exactly the risk you mentioned; the test makes it visible. }) }