Files
capsule/pkg/template/fast_test.go
Oliver Bähler a42d910ba1 fix(controller): template concurrency (#1802)
Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>
2025-12-19 08:14:37 +01:00

291 lines
7.4 KiB
Go

// Copyright 2020-2025 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package template_test
import (
"sync"
"testing"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
tpl "github.com/projectcapsule/capsule/pkg/template"
)
func newTenant(name string) *capsulev1beta2.Tenant {
return &capsulev1beta2.Tenant{
ObjectMeta: metav1.ObjectMeta{Name: name},
}
}
func newNamespace(name string) *v1.Namespace {
return &v1.Namespace{
ObjectMeta: metav1.ObjectMeta{Name: name},
}
}
func TestTemplateForTenantAndNamespace_ReplacesPlaceholders(t *testing.T) {
tnt := newTenant("tenant-a")
ns := newNamespace("ns-1")
got := tpl.TemplateForTenantAndNamespace(
"tenant={{tenant.name}}, ns={{namespace}}",
tnt,
ns,
)
want := "tenant=tenant-a, ns=ns-1"
if got != want {
t.Fatalf("expected %q, got %q", want, got)
}
}
func TestTemplateForTenantAndNamespace_ReplacesPlaceholdersSpaces(t *testing.T) {
tnt := newTenant("tenant-a")
ns := newNamespace("ns-1")
got := tpl.TemplateForTenantAndNamespace(
"tenant={{ tenant.name }}, ns={{ namespace }}",
tnt,
ns,
)
want := "tenant=tenant-a, ns=ns-1"
if got != want {
t.Fatalf("expected %q, got %q", want, got)
}
}
func TestTemplateForTenantAndNamespace_OnlyTenant(t *testing.T) {
tnt := newTenant("tenant-x")
ns := newNamespace("ns-y")
got := tpl.TemplateForTenantAndNamespace("T={{tenant.name}}", tnt, ns)
want := "T=tenant-x"
if got != want {
t.Fatalf("expected %q, got %q", want, got)
}
}
func TestTemplateForTenantAndNamespace_OnlyNamespace(t *testing.T) {
tnt := newTenant("tenant-x")
ns := newNamespace("ns-y")
got := tpl.TemplateForTenantAndNamespace("N={{namespace}}", tnt, ns)
want := "N=ns-y"
if got != want {
t.Fatalf("expected %q, got %q", want, got)
}
}
func TestTemplateForTenantAndNamespace_NoDelimiters_ReturnsInput(t *testing.T) {
tnt := newTenant("tenant-a")
ns := newNamespace("ns-1")
in := "plain-value-without-templates"
got := tpl.TemplateForTenantAndNamespace(in, tnt, ns)
if got != in {
t.Fatalf("expected %q, got %q", in, got)
}
}
func TestTemplateForTenantAndNamespace_UnknownKeyBecomesEmpty(t *testing.T) {
tnt := newTenant("tenant-a")
ns := newNamespace("ns-1")
got := tpl.TemplateForTenantAndNamespace("X={{unknown.key}}", tnt, ns)
want := "X="
if got != want {
t.Fatalf("expected %q, got %q", want, got)
}
}
func TestTemplateForTenantAndNamespaceMap_ReplacesPlaceholders(t *testing.T) {
tnt := newTenant("tenant-a")
ns := newNamespace("ns-1")
orig := map[string]string{
"key1": "tenant={{tenant.name}}, ns={{namespace}}",
"key2": "plain-value",
}
out := tpl.TemplateForTenantAndNamespaceMap(orig, tnt, ns)
// 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) {
tnt := newTenant("tenant-a")
ns := newNamespace("ns-1")
orig := map[string]string{
"key1": "tenant={{ tenant.name }}, ns={{ namespace }}",
"key2": "plain-value",
}
out := tpl.TemplateForTenantAndNamespaceMap(orig, tnt, ns)
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) {
tnt := newTenant("tenant-a")
ns := newNamespace("ns-1")
orig := map[string]string{
"t1": "hello {{tenant.name}}",
"t2": "namespace {{namespace}}",
"t3": "static",
}
out := tpl.TemplateForTenantAndNamespaceMap(orig, tnt, ns)
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) {
tnt := newTenant("tenant-x")
ns := newNamespace("ns-x")
orig := map[string]string{
"onlyTenant": "T={{ tenant.name }}",
"onlyNS": "N={{ namespace }}",
"none": "static",
}
out := tpl.TemplateForTenantAndNamespaceMap(orig, tnt, ns)
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) {
tnt := newTenant("tenant-a")
ns := newNamespace("ns-1")
orig := map[string]string{
"unknown": "X={{ unknown.key }}",
}
out := tpl.TemplateForTenantAndNamespaceMap(orig, tnt, ns)
if got := out["unknown"]; got != "X=" {
t.Fatalf("unknown: expected %q, got %q", "X=", got)
}
}
func TestTemplateForTenantAndNamespaceMap_EmptyOrNilInput(t *testing.T) {
tnt := newTenant("tenant-a")
ns := newNamespace("ns-1")
// nil map
outNil := tpl.TemplateForTenantAndNamespaceMap(nil, tnt, ns)
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.TemplateForTenantAndNamespaceMap(map[string]string{}, tnt, ns)
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) {
tnt := newTenant("tenant-a")
ns := newNamespace("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.TemplateForTenantAndNamespaceMap(shared, tnt, ns)
// 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"])
}
}