// Copyright 2020-2025 Project Capsule Authors // SPDX-License-Identifier: Apache-2.0 package template_test import ( "strings" "testing" "github.com/projectcapsule/capsule/pkg/template" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) // Adjust these if your MissingKeyOption constants are named differently. var ( missingKeyErr = template.MissingKeyOption("error") missingKeyZero = template.MissingKeyOption("zero") ) func mustOne(t *testing.T, items []*unstructured.Unstructured) *unstructured.Unstructured { t.Helper() if len(items) != 1 { t.Fatalf("expected 1 item, got %d", len(items)) } return items[0] } func TestRenderUnstructuredItems_SingleYAMLDocument(t *testing.T) { ctx := template.ReferenceContext{"name": "cm-1"} tpl := ` apiVersion: v1 kind: ConfigMap metadata: name: {{ .name }} data: x: y ` items, err := template.RenderUnstructuredItems(ctx, missingKeyErr, tpl) if err != nil { t.Fatalf("expected no error, got: %v", err) } u := mustOne(t, items) if u.GetAPIVersion() != "v1" { t.Fatalf("expected apiVersion=v1, got %q", u.GetAPIVersion()) } if u.GetKind() != "ConfigMap" { t.Fatalf("expected kind=ConfigMap, got %q", u.GetKind()) } if u.GetName() != "cm-1" { t.Fatalf("expected name=cm-1, got %q", u.GetName()) } } func TestRenderUnstructuredItems_MultiDoc_SkipsEmptyWhitespaceAndNullDocs(t *testing.T) { tpl := ` --- apiVersion: v1 kind: Namespace metadata: name: ns-1 --- # empty doc --- # whitespace doc --- null --- apiVersion: v1 kind: ConfigMap metadata: name: cm-2 ` items, err := template.RenderUnstructuredItems(template.ReferenceContext{}, missingKeyErr, tpl) if err != nil { t.Fatalf("expected no error, got: %v", err) } if len(items) != 2 { t.Fatalf("expected 2 items, got %d", len(items)) } if items[0].GetKind() != "Namespace" || items[0].GetName() != "ns-1" { t.Fatalf("unexpected first object: kind=%q name=%q", items[0].GetKind(), items[0].GetName()) } if items[1].GetKind() != "ConfigMap" || items[1].GetName() != "cm-2" { t.Fatalf("unexpected second object: kind=%q name=%q", items[1].GetKind(), items[1].GetName()) } } func TestRenderUnstructuredItems_SkipsObjectMissingBothKindAndAPIVersion(t *testing.T) { tpl := ` metadata: name: skipped --- apiVersion: v1 kind: ConfigMap metadata: name: kept ` items, err := template.RenderUnstructuredItems(template.ReferenceContext{}, missingKeyErr, tpl) if err != nil { t.Fatalf("expected no error, got: %v", err) } if len(items) != 1 { t.Fatalf("expected 1 item, got %d", len(items)) } if items[0].GetName() != "kept" { t.Fatalf("expected kept object, got name=%q", items[0].GetName()) } } func TestRenderUnstructuredItems_DoesNotSkipIfOnlyOneOfKindOrAPIVersionPresent(t *testing.T) { tpl := ` apiVersion: v1 metadata: name: only-apiversion --- kind: ConfigMap metadata: name: only-kind ` items, err := template.RenderUnstructuredItems(template.ReferenceContext{}, missingKeyErr, tpl) if err != nil { t.Fatalf("expected no error, got: %v", err) } if len(items) != 2 { t.Fatalf("expected 2 items, got %d", len(items)) } if items[0].GetAPIVersion() != "v1" || items[0].GetName() != "only-apiversion" { t.Fatalf("unexpected first object: apiVersion=%q name=%q", items[0].GetAPIVersion(), items[0].GetName()) } if items[1].GetKind() != "ConfigMap" || items[1].GetName() != "only-kind" { t.Fatalf("unexpected second object: kind=%q name=%q", items[1].GetKind(), items[1].GetName()) } } func TestRenderUnstructuredItems_JSONDocument(t *testing.T) { tpl := `{"apiVersion":"v1","kind":"ConfigMap","metadata":{"name":"cm-json"}}` items, err := template.RenderUnstructuredItems(template.ReferenceContext{}, missingKeyErr, tpl) if err != nil { t.Fatalf("expected no error, got: %v", err) } u := mustOne(t, items) if u.GetKind() != "ConfigMap" || u.GetName() != "cm-json" { t.Fatalf("unexpected object: kind=%q name=%q", u.GetKind(), u.GetName()) } } func TestRenderUnstructuredItems_TemplateParseError(t *testing.T) { tpl := ` apiVersion: v1 kind: ConfigMap metadata: name: {{ .name ` _, err := template.RenderUnstructuredItems(template.ReferenceContext{"name": "x"}, missingKeyErr, tpl) if err == nil { t.Fatalf("expected parse error, got nil") } } func TestRenderUnstructuredItems_MissingKey_ErrorMode(t *testing.T) { tpl := ` apiVersion: v1 kind: ConfigMap metadata: name: {{ .doesNotExist }} ` _, err := template.RenderUnstructuredItems(template.ReferenceContext{}, missingKeyErr, tpl) if err == nil { t.Fatalf("expected execute error for missing key, got nil") } } func TestRenderUnstructuredItems_MissingKey_ZeroMode_AllowsRender(t *testing.T) { tpl := ` apiVersion: v1 kind: ConfigMap metadata: name: {{ .doesNotExist }} ` items, err := template.RenderUnstructuredItems(template.ReferenceContext{}, missingKeyZero, tpl) if err != nil { t.Fatalf("expected no error, got: %v", err) } u := mustOne(t, items) if u.GetKind() != "ConfigMap" { t.Fatalf("expected kind=ConfigMap, got %q", u.GetKind()) } } func TestRenderUnstructuredItems_MalformedYAML_ReturnsDecodeError(t *testing.T) { tpl := ` apiVersion: v1 kind: ConfigMap metadata: name: cm data: a: b c: d ` _, err := template.RenderUnstructuredItems(template.ReferenceContext{}, missingKeyErr, tpl) if err == nil { t.Fatalf("expected decode error, got nil") } if !strings.Contains(err.Error(), "decode yaml") { t.Fatalf("expected error to contain %q, got: %v", "decode yaml", err) } } func TestRenderUnstructuredItems_SequenceRoot_IsError(t *testing.T) { tpl := ` - apiVersion: v1 kind: ConfigMap metadata: name: cm ` _, err := template.RenderUnstructuredItems(template.ReferenceContext{}, missingKeyErr, tpl) if err == nil { t.Fatalf("expected decode error for sequence root, got nil") } if !strings.Contains(err.Error(), "decode yaml") { t.Fatalf("expected error to contain %q, got: %v", "decode yaml", err) } } func TestRenderUnstructuredItems_ScalarRoot_IsError(t *testing.T) { tpl := `just-a-string` _, err := template.RenderUnstructuredItems(template.ReferenceContext{}, missingKeyErr, tpl) if err == nil { t.Fatalf("expected decode error for scalar root, got nil") } if !strings.Contains(err.Error(), "decode yaml") { t.Fatalf("expected error to contain %q, got: %v", "decode yaml", err) } } func TestRenderUnstructuredItems_WhitespaceOnly_IsError(t *testing.T) { tpl := "\n \n\t\n" _, err := template.RenderUnstructuredItems(template.ReferenceContext{}, missingKeyErr, tpl) if err == nil { t.Fatalf("expected decode error for scalar root, got nil") } if !strings.Contains(err.Error(), "decode yaml") { t.Fatalf("expected error to contain %q, got: %v", "decode yaml", err) } } func TestRenderUnstructuredItems_ContextNestedTypes_RenderOK(t *testing.T) { ctx := template.ReferenceContext{ "outer": map[string]any{ "inner": "v", }, "list": []any{"a", "b"}, } tpl := ` apiVersion: v1 kind: ConfigMap metadata: name: cm-{{ index .list 0 }} data: x: {{ .outer.inner }} ` items, err := template.RenderUnstructuredItems(ctx, missingKeyErr, tpl) if err != nil { t.Fatalf("expected no error, got: %v", err) } u := mustOne(t, items) if u.GetName() != "cm-a" { t.Fatalf("expected name=cm-a, got %q", u.GetName()) } } func TestReferenceContext_String_MarshalUnmarshalRoundTrip(t *testing.T) { ctx := template.ReferenceContext{ "a": "b", "n": 1, "m": map[string]any{"x": "y"}, } s, err := ctx.String() if err != nil { t.Fatalf("expected no error, got: %v", err) } if !strings.Contains(s, `"a":"b"`) { t.Fatalf("expected JSON to contain %q, got %q", `"a":"b"`, s) } } func TestRenderUnstructuredItems_MultiYAML_AllValid(t *testing.T) { ctx := template.ReferenceContext{"ns": "ns-1"} tpl := ` apiVersion: v1 kind: Namespace metadata: name: {{ .ns }} --- apiVersion: v1 kind: ConfigMap metadata: name: cm-1 namespace: {{ .ns }} data: k: v --- apiVersion: v1 kind: Secret metadata: name: s-1 namespace: {{ .ns }} type: Opaque stringData: a: b ` items, err := template.RenderUnstructuredItems(ctx, missingKeyErr, tpl) if err != nil { t.Fatalf("expected no error, got: %v", err) } if len(items) != 3 { t.Fatalf("expected 3 items, got %d", len(items)) } if items[0].GetKind() != "Namespace" || items[0].GetName() != "ns-1" { t.Fatalf("unexpected item0: kind=%q name=%q", items[0].GetKind(), items[0].GetName()) } if items[1].GetKind() != "ConfigMap" || items[1].GetName() != "cm-1" || items[1].GetNamespace() != "ns-1" { t.Fatalf("unexpected item1: kind=%q name=%q ns=%q", items[1].GetKind(), items[1].GetName(), items[1].GetNamespace()) } if items[2].GetKind() != "Secret" || items[2].GetName() != "s-1" || items[2].GetNamespace() != "ns-1" { t.Fatalf("unexpected item2: kind=%q name=%q ns=%q", items[2].GetKind(), items[2].GetName(), items[2].GetNamespace()) } } func TestRenderUnstructuredItems_MultiJSON_NewlineDelimited(t *testing.T) { // YAMLOrJSONDecoder supports multiple JSON objects if separated in the stream (e.g. NDJSON). tpl := ` {"apiVersion":"v1","kind":"ConfigMap","metadata":{"name":"cm-a"}} {"apiVersion":"v1","kind":"ConfigMap","metadata":{"name":"cm-b"}} {"apiVersion":"v1","kind":"Namespace","metadata":{"name":"ns-c"}} ` items, err := template.RenderUnstructuredItems(template.ReferenceContext{}, missingKeyErr, tpl) if err != nil { t.Fatalf("expected no error, got: %v", err) } if len(items) != 3 { t.Fatalf("expected 3 items, got %d", len(items)) } if items[0].GetName() != "cm-a" || items[0].GetKind() != "ConfigMap" { t.Fatalf("unexpected item0: kind=%q name=%q", items[0].GetKind(), items[0].GetName()) } if items[1].GetName() != "cm-b" || items[1].GetKind() != "ConfigMap" { t.Fatalf("unexpected item1: kind=%q name=%q", items[1].GetKind(), items[1].GetName()) } if items[2].GetName() != "ns-c" || items[2].GetKind() != "Namespace" { t.Fatalf("unexpected item2: kind=%q name=%q", items[2].GetKind(), items[2].GetName()) } } func TestRenderUnstructuredItems_MixedYAMLAndJSON_AllValid(t *testing.T) { // Decoder supports YAML and JSON in same stream. tpl := ` apiVersion: v1 kind: Namespace metadata: name: ns-1 --- {"apiVersion":"v1","kind":"ConfigMap","metadata":{"name":"cm-1","namespace":"ns-1"}} --- apiVersion: v1 kind: Secret metadata: name: s-1 namespace: ns-1 type: Opaque stringData: a: b ` items, err := template.RenderUnstructuredItems(template.ReferenceContext{}, missingKeyErr, tpl) if err != nil { t.Fatalf("expected no error, got: %v", err) } if len(items) != 3 { t.Fatalf("expected 3 items, got %d", len(items)) } if items[0].GetKind() != "Namespace" || items[0].GetName() != "ns-1" { t.Fatalf("unexpected item0: kind=%q name=%q", items[0].GetKind(), items[0].GetName()) } if items[1].GetKind() != "ConfigMap" || items[1].GetName() != "cm-1" || items[1].GetNamespace() != "ns-1" { t.Fatalf("unexpected item1: kind=%q name=%q ns=%q", items[1].GetKind(), items[1].GetName(), items[1].GetNamespace()) } if items[2].GetKind() != "Secret" || items[2].GetName() != "s-1" || items[2].GetNamespace() != "ns-1" { t.Fatalf("unexpected item2: kind=%q name=%q ns=%q", items[2].GetKind(), items[2].GetName(), items[2].GetNamespace()) } } func TestRenderUnstructuredItems_MultiDocs_EmptyMapAndNullAreSkipped(t *testing.T) { tpl := ` {} --- null --- apiVersion: v1 kind: ConfigMap metadata: name: cm-1 --- {} # another empty doc --- apiVersion: v1 kind: Namespace metadata: name: ns-1 ` items, err := template.RenderUnstructuredItems(template.ReferenceContext{}, missingKeyErr, tpl) if err != nil { t.Fatalf("expected no error, got: %v", err) } if len(items) != 2 { t.Fatalf("expected 2 items, got %d", len(items)) } if items[0].GetKind() != "ConfigMap" || items[0].GetName() != "cm-1" { t.Fatalf("unexpected item0: kind=%q name=%q", items[0].GetKind(), items[0].GetName()) } if items[1].GetKind() != "Namespace" || items[1].GetName() != "ns-1" { t.Fatalf("unexpected item1: kind=%q name=%q", items[1].GetKind(), items[1].GetName()) } }