From 5a081b51c381f45ff6d094139ac842c82309578a Mon Sep 17 00:00:00 2001 From: "Benjamin R. J. Schwedler" Date: Wed, 28 Jan 2026 12:47:32 -0600 Subject: [PATCH 01/12] Add rstudio-library-test harness chart structure Create test harness chart for unit testing rstudio-library templates. Library charts cannot be tested directly, so this application chart depends on rstudio-library and will exercise its templates. Co-Authored-By: Claude Opus 4.5 --- other-charts/rstudio-library-test/Chart.lock | 6 ++ other-charts/rstudio-library-test/Chart.yaml | 11 +++ .../templates/_helpers.tpl | 44 +++++++++ other-charts/rstudio-library-test/values.yaml | 96 +++++++++++++++++++ 4 files changed, 157 insertions(+) create mode 100644 other-charts/rstudio-library-test/Chart.lock create mode 100644 other-charts/rstudio-library-test/Chart.yaml create mode 100644 other-charts/rstudio-library-test/templates/_helpers.tpl create mode 100644 other-charts/rstudio-library-test/values.yaml diff --git a/other-charts/rstudio-library-test/Chart.lock b/other-charts/rstudio-library-test/Chart.lock new file mode 100644 index 000000000..24722d556 --- /dev/null +++ b/other-charts/rstudio-library-test/Chart.lock @@ -0,0 +1,6 @@ +dependencies: +- name: rstudio-library + repository: file://../../charts/rstudio-library + version: 0.1.35 +digest: sha256:706c9e8c17f03c54f61ac9492f735082b6e771f3d87320f384821ffb4c197f51 +generated: "2026-01-28T12:47:20.245416746-06:00" diff --git a/other-charts/rstudio-library-test/Chart.yaml b/other-charts/rstudio-library-test/Chart.yaml new file mode 100644 index 000000000..ae20e9431 --- /dev/null +++ b/other-charts/rstudio-library-test/Chart.yaml @@ -0,0 +1,11 @@ +apiVersion: v2 +name: rstudio-library-test +description: Test harness for rstudio-library templates +type: application +version: 0.1.0 +appVersion: "0.1.0" + +dependencies: + - name: rstudio-library + version: "0.1.35" + repository: "file://../../charts/rstudio-library" diff --git a/other-charts/rstudio-library-test/templates/_helpers.tpl b/other-charts/rstudio-library-test/templates/_helpers.tpl new file mode 100644 index 000000000..8e049daf5 --- /dev/null +++ b/other-charts/rstudio-library-test/templates/_helpers.tpl @@ -0,0 +1,44 @@ +{{/* +Test harness helpers for rstudio-library templates. +These helpers invoke library templates and wrap output for testing. +*/}} + +{{/* +Chart name +*/}} +{{- define "rstudio-library-test.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Fully qualified app name +*/}} +{{- define "rstudio-library-test.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "rstudio-library-test.labels" -}} +helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} +{{ include "rstudio-library-test.selectorLabels" . }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "rstudio-library-test.selectorLabels" -}} +app.kubernetes.io/name: {{ include "rstudio-library-test.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} diff --git a/other-charts/rstudio-library-test/values.yaml b/other-charts/rstudio-library-test/values.yaml new file mode 100644 index 000000000..670be8dc4 --- /dev/null +++ b/other-charts/rstudio-library-test/values.yaml @@ -0,0 +1,96 @@ +# Test values for rstudio-library-test harness +# These values are used by test templates to exercise library functions + +# Config formatter test values +testConfig: + gcfg: + filename: test.gcfg + config: + section1: + key1: value1 + key2: value2 + section2: + arrayKey: + - item1 + - item2 + ini: + filename: test.ini + config: + section1: + key1: value1 + key2: 123 + dcf: + filename: test.dcf + config: + key1: value1 + nested: + subkey: subvalue + json: + filename: test.json + config: + stringKey: stringValue + numberKey: 42 + boolKey: true + arrayKey: + - item1 + - item2 + txt: + filename: test.txt + config: + key1: value1 + key2: value2 + +# Ingress helper test values +testIngress: + serviceName: test-service + servicePort: 8080 + path: /test + pathType: Prefix + +# License helper test values +testLicense: + # Test license key + licenseKey: "test-license-key" + # Test license server + licenseServer: "license.example.com" + # Test license file + licenseFile: | + LICENSE CONTENT HERE + +# RBAC test values +testRbac: + serviceAccountCreate: true + serviceAccountName: test-sa + clusterRoleCreate: false + namespace: test-namespace + targetNamespace: test-target + labels: {} + annotations: {} + +# Profiles test values +testProfiles: + launcherJobsConf: + "*": + job-name: "test-job" + testUser: + job-name: "user-job" + +# Launcher template test values +testLauncherTemplates: + templateName: test-template + content: + key1: value1 + key2: value2 + +# Tplvalues test values +testTplvalues: + staticValue: "static" + templateValue: "{{ .Release.Name }}-suffix" + +# Chronicle agent test values +testChronicle: + enabled: false + serverAddress: "" + image: + repository: ghcr.io/rstudio/chronicle-agent + tag: "" From 8e99e90911d3049ebfaf8ac4e6b2e63592ed459e Mon Sep 17 00:00:00 2001 From: "Benjamin R. J. Schwedler" Date: Wed, 28 Jan 2026 12:49:26 -0600 Subject: [PATCH 02/12] Add config formatter test templates for rstudio-library Create test-config.yaml that exercises all config format templates: - config.gcfg (Go configuration format) - config.ini (INI format) - config.dcf (Debian Control File format) - config.json (JSON format) - config.txt (generic text format) Co-Authored-By: Claude Opus 4.5 --- .../templates/test-config.yaml | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 other-charts/rstudio-library-test/templates/test-config.yaml diff --git a/other-charts/rstudio-library-test/templates/test-config.yaml b/other-charts/rstudio-library-test/templates/test-config.yaml new file mode 100644 index 000000000..4a18f7bfb --- /dev/null +++ b/other-charts/rstudio-library-test/templates/test-config.yaml @@ -0,0 +1,67 @@ +{{/* +Test templates for rstudio-library config formatters. +Each ConfigMap exercises a different config format template. +*/}} + +{{- if .Values.testConfig }} +{{- if .Values.testConfig.gcfg }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test-config-gcfg + labels: + {{- include "rstudio-library-test.labels" . | nindent 4 }} +data: + {{ .Values.testConfig.gcfg.filename }}: | + {{- include "rstudio-library.config.gcfg" .Values.testConfig.gcfg.config | nindent 4 }} +{{- end }} + +{{- if .Values.testConfig.ini }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test-config-ini + labels: + {{- include "rstudio-library-test.labels" . | nindent 4 }} +data: + {{- include "rstudio-library.config.ini" (dict .Values.testConfig.ini.filename .Values.testConfig.ini.config) | nindent 2 }} +{{- end }} + +{{- if .Values.testConfig.dcf }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test-config-dcf + labels: + {{- include "rstudio-library-test.labels" . | nindent 4 }} +data: + {{- include "rstudio-library.config.dcf" (dict .Values.testConfig.dcf.filename .Values.testConfig.dcf.config) | nindent 2 }} +{{- end }} + +{{- if .Values.testConfig.json }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test-config-json + labels: + {{- include "rstudio-library-test.labels" . | nindent 4 }} +data: + {{- include "rstudio-library.config.json" (dict .Values.testConfig.json.filename .Values.testConfig.json.config) | nindent 2 }} +{{- end }} + +{{- if .Values.testConfig.txt }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test-config-txt + labels: + {{- include "rstudio-library-test.labels" . | nindent 4 }} +data: + {{- include "rstudio-library.config.txt" (dict "data" (dict .Values.testConfig.txt.filename .Values.testConfig.txt.config)) | nindent 2 }} +{{- end }} +{{- end }} From 1ae3d86955e02c0bbc79ca43ae72e8379c403b73 Mon Sep 17 00:00:00 2001 From: "Benjamin R. J. Schwedler" Date: Wed, 28 Jan 2026 12:50:56 -0600 Subject: [PATCH 03/12] Add test templates for all rstudio-library helpers Create test templates that exercise all library template functions: - test-ingress.yaml: Ingress API version, path, and backend helpers - test-license.yaml: License env, mount, volume, and secret helpers - test-rbac.yaml: RBAC (ServiceAccount, Role, RoleBinding) helper - test-profiles.yaml: Profiles INI formatters and JSON override helpers - test-debug.yaml: Type-check debug helper - test-launcher-templates.yaml: Template skeleton and data output helpers - test-tplvalues.yaml: Template value rendering helper - test-chronicle-agent.yaml: Chronicle agent image and server helpers Update values.yaml with comprehensive test data for all templates. Co-Authored-By: Claude Opus 4.5 --- .../templates/test-chronicle-agent.yaml | 26 +++++ .../templates/test-debug.yaml | 40 +++++++ .../templates/test-ingress.yaml | 48 +++++++++ .../templates/test-launcher-templates.yaml | 48 +++++++++ .../templates/test-license.yaml | 56 ++++++++++ .../templates/test-profiles.yaml | 102 ++++++++++++++++++ .../templates/test-rbac.yaml | 17 +++ .../templates/test-tplvalues.yaml | 24 +++++ other-charts/rstudio-library-test/values.yaml | 67 ++++++++++-- 9 files changed, 417 insertions(+), 11 deletions(-) create mode 100644 other-charts/rstudio-library-test/templates/test-chronicle-agent.yaml create mode 100644 other-charts/rstudio-library-test/templates/test-debug.yaml create mode 100644 other-charts/rstudio-library-test/templates/test-ingress.yaml create mode 100644 other-charts/rstudio-library-test/templates/test-launcher-templates.yaml create mode 100644 other-charts/rstudio-library-test/templates/test-license.yaml create mode 100644 other-charts/rstudio-library-test/templates/test-profiles.yaml create mode 100644 other-charts/rstudio-library-test/templates/test-rbac.yaml create mode 100644 other-charts/rstudio-library-test/templates/test-tplvalues.yaml diff --git a/other-charts/rstudio-library-test/templates/test-chronicle-agent.yaml b/other-charts/rstudio-library-test/templates/test-chronicle-agent.yaml new file mode 100644 index 000000000..8e765ef0a --- /dev/null +++ b/other-charts/rstudio-library-test/templates/test-chronicle-agent.yaml @@ -0,0 +1,26 @@ +{{/* +Test templates for rstudio-library chronicle-agent helpers. +Note: These templates use `lookup` which requires cluster access. +Tests with kubernetesProvider mock can only test static paths. +*/}} + +{{- if .Values.testChronicle }} +{{- if .Values.testChronicle.enabled }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test-chronicle-agent + labels: + {{- include "rstudio-library-test.labels" . | nindent 4 }} +data: + {{- /* Test image template - only when tag is specified to avoid lookup */}} + {{- if .Values.testChronicle.image.tag }} + image: {{ include "rstudio-library.chronicle-agent.image" (dict "chronicleAgent" .Values.testChronicle "Release" .Release) | quote }} + {{- end }} + {{- /* Test serverAddress template - only when serverAddress is specified to avoid lookup */}} + {{- if .Values.testChronicle.serverAddress }} + serverAddress: {{ include "rstudio-library.chronicle-agent.serverAddress" (dict "chronicleAgent" .Values.testChronicle "Release" .Release) | quote }} + {{- end }} +{{- end }} +{{- end }} diff --git a/other-charts/rstudio-library-test/templates/test-debug.yaml b/other-charts/rstudio-library-test/templates/test-debug.yaml new file mode 100644 index 000000000..8e2fa3840 --- /dev/null +++ b/other-charts/rstudio-library-test/templates/test-debug.yaml @@ -0,0 +1,40 @@ +{{/* +Test templates for rstudio-library debug helpers. +The type-check template either succeeds silently or fails with an error. +We test this by passing various types and checking for success/failure. +*/}} + +{{- if .Values.testDebug }} + +{{- /* Test passing type checks - these should succeed silently */}} +{{- if .Values.testDebug.mapValue }} +{{- include "rstudio-library.debug.type-check" (dict "name" "testMap" "object" .Values.testDebug.mapValue "expected" "map" "description" "test map value") }} +{{- end }} + +{{- if .Values.testDebug.sliceValue }} +{{- include "rstudio-library.debug.type-check" (dict "name" "testSlice" "object" .Values.testDebug.sliceValue "expected" "slice" "description" "test slice value") }} +{{- end }} + +{{- if .Values.testDebug.stringValue }} +{{- include "rstudio-library.debug.type-check" (dict "name" "testString" "object" .Values.testDebug.stringValue "expected" "string" "description" "test string value") }} +{{- end }} + +{{- if .Values.testDebug.boolValue }} +{{- include "rstudio-library.debug.type-check" (dict "name" "testBool" "object" .Values.testDebug.boolValue "expected" "bool" "description" "test bool value") }} +{{- end }} + +{{- /* Test failing type check - this should fail */}} +{{- if .Values.testDebug.failingCheck }} +{{- include "rstudio-library.debug.type-check" (dict "name" .Values.testDebug.failingCheck.name "object" .Values.testDebug.failingCheck.object "expected" .Values.testDebug.failingCheck.expected "description" .Values.testDebug.failingCheck.description) }} +{{- end }} + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test-debug-success + labels: + {{- include "rstudio-library-test.labels" . | nindent 4 }} +data: + status: "type-checks-passed" +{{- end }} diff --git a/other-charts/rstudio-library-test/templates/test-ingress.yaml b/other-charts/rstudio-library-test/templates/test-ingress.yaml new file mode 100644 index 000000000..b3d5549fb --- /dev/null +++ b/other-charts/rstudio-library-test/templates/test-ingress.yaml @@ -0,0 +1,48 @@ +{{/* +Test templates for rstudio-library ingress helpers. +Outputs computed ingress values to ConfigMap for testing. +*/}} + +{{- if .Values.testIngress }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test-ingress-values + labels: + {{- include "rstudio-library-test.labels" . | nindent 4 }} +data: + apiVersion: {{ include "rstudio-library.ingress.apiVersion" . }} + supportsIngressClassName: {{ include "rstudio-library.ingress.supportsIngressClassName" (include "rstudio-library.ingress.apiVersion" .) }} + supportsPathType: {{ include "rstudio-library.ingress.supportsPathType" (include "rstudio-library.ingress.apiVersion" .) }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test-ingress-path-string + labels: + {{- include "rstudio-library-test.labels" . | nindent 4 }} +data: + path.yaml: | + {{- include "rstudio-library.ingress.path" (dict "apiVersion" (include "rstudio-library.ingress.apiVersion" .) "pathData" .Values.testIngress.path) | nindent 4 }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test-ingress-path-object + labels: + {{- include "rstudio-library-test.labels" . | nindent 4 }} +data: + path.yaml: | + {{- include "rstudio-library.ingress.path" (dict "apiVersion" (include "rstudio-library.ingress.apiVersion" .) "pathData" (dict "path" .Values.testIngress.path "pathType" .Values.testIngress.pathType)) | nindent 4 }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test-ingress-backend + labels: + {{- include "rstudio-library-test.labels" . | nindent 4 }} +data: + backend.yaml: | + {{- include "rstudio-library.ingress.backend" (dict "apiVersion" (include "rstudio-library.ingress.apiVersion" .) "svcName" .Values.testIngress.serviceName "svcPort" .Values.testIngress.servicePort) | nindent 4 }} +{{- end }} diff --git a/other-charts/rstudio-library-test/templates/test-launcher-templates.yaml b/other-charts/rstudio-library-test/templates/test-launcher-templates.yaml new file mode 100644 index 000000000..b1f9671bb --- /dev/null +++ b/other-charts/rstudio-library-test/templates/test-launcher-templates.yaml @@ -0,0 +1,48 @@ +{{/* +Test templates for rstudio-library launcher template helpers. +These generate Helm template definitions that can be used in launcher configurations. +*/}} + +{{- if .Values.testLauncherTemplates }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test-launcher-skeleton + labels: + {{- include "rstudio-library-test.labels" . | nindent 4 }} +data: + template.tpl: | + {{- include "rstudio-library.templates.skeleton" (dict "name" .Values.testLauncherTemplates.templateName "value" .Values.testLauncherTemplates.content) | nindent 4 }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test-launcher-data-output + labels: + {{- include "rstudio-library-test.labels" . | nindent 4 }} +data: + template.tpl: | + {{- include "rstudio-library.templates.dataOutput" (dict "name" .Values.testLauncherTemplates.templateName "value" .Values.testLauncherTemplates.content) | nindent 4 }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test-launcher-data-output-pretty + labels: + {{- include "rstudio-library-test.labels" . | nindent 4 }} +data: + template.tpl: | + {{- include "rstudio-library.templates.dataOutputPretty" (dict "name" .Values.testLauncherTemplates.templateName "value" .Values.testLauncherTemplates.content) | nindent 4 }} +{{- /* Test with trailingDash=false */}} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test-launcher-no-trailing-dash + labels: + {{- include "rstudio-library-test.labels" . | nindent 4 }} +data: + template.tpl: | + {{- include "rstudio-library.templates.skeleton" (dict "name" .Values.testLauncherTemplates.templateName "value" .Values.testLauncherTemplates.content "trailingDash" false) | nindent 4 }} +{{- end }} diff --git a/other-charts/rstudio-library-test/templates/test-license.yaml b/other-charts/rstudio-library-test/templates/test-license.yaml new file mode 100644 index 000000000..0159fc1db --- /dev/null +++ b/other-charts/rstudio-library-test/templates/test-license.yaml @@ -0,0 +1,56 @@ +{{/* +Test templates for rstudio-library license helpers. +Outputs license configuration to ConfigMaps/Secrets for testing. +*/}} + +{{- if .Values.testLicense }} +{{- $licenseContext := dict + "product" "test-product" + "envVarPrefix" "TEST" + "license" (dict + "key" .Values.testLicense.licenseKey + "server" .Values.testLicense.licenseServer + "file" (dict + "contents" .Values.testLicense.licenseFile + "secret" "" + "mountPath" "/etc/license" + "secretKey" "license.lic" + "mountSubPath" false + ) + ) + "fullName" "test-release" + "namespace" "test-namespace" +}} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test-license-env + labels: + {{- include "rstudio-library-test.labels" . | nindent 4 }} +data: + env.yaml: | + {{- include "rstudio-library.license-env" $licenseContext | nindent 4 }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test-license-mount + labels: + {{- include "rstudio-library-test.labels" . | nindent 4 }} +data: + mount.yaml: | + {{- include "rstudio-library.license-mount" $licenseContext | nindent 4 }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test-license-volume + labels: + {{- include "rstudio-library-test.labels" . | nindent 4 }} +data: + volume.yaml: | + {{- include "rstudio-library.license-volume" $licenseContext | nindent 4 }} +{{- /* License secret is a special case - it outputs actual Secret resources */}} +{{- include "rstudio-library.license-secret" $licenseContext }} +{{- end }} diff --git a/other-charts/rstudio-library-test/templates/test-profiles.yaml b/other-charts/rstudio-library-test/templates/test-profiles.yaml new file mode 100644 index 000000000..9af19755a --- /dev/null +++ b/other-charts/rstudio-library-test/templates/test-profiles.yaml @@ -0,0 +1,102 @@ +{{/* +Test templates for rstudio-library profiles helpers. +Outputs profile configurations to ConfigMaps for testing. +*/}} + +{{- if .Values.testProfiles }} + +{{- /* Test basic profiles.ini */}} +{{- if .Values.testProfiles.basicIni }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test-profiles-ini + labels: + {{- include "rstudio-library-test.labels" . | nindent 4 }} +data: + {{- include "rstudio-library.profiles.ini" .Values.testProfiles.basicIni | nindent 2 }} +{{- end }} + +{{- /* Test profiles.ini.singleFile */}} +{{- if .Values.testProfiles.singleFile }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test-profiles-single-file + labels: + {{- include "rstudio-library-test.labels" . | nindent 4 }} +data: + profiles.conf: | + {{- include "rstudio-library.profiles.ini.singleFile" .Values.testProfiles.singleFile | nindent 4 }} +{{- end }} + +{{- /* Test profiles.ini.collapse-array */}} +{{- if .Values.testProfiles.collapseArray }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test-profiles-collapse-array + labels: + {{- include "rstudio-library-test.labels" . | nindent 4 }} +data: + simple: {{ include "rstudio-library.profiles.ini.collapse-array" .Values.testProfiles.collapseArray.simple | quote }} + {{- if .Values.testProfiles.collapseArray.targetFile }} + targetFile: {{ include "rstudio-library.profiles.ini.collapse-array" .Values.testProfiles.collapseArray.targetFile | quote }} + {{- end }} +{{- end }} + +{{- /* Test profiles.ini.advanced */}} +{{- if .Values.testProfiles.advanced }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test-profiles-advanced + labels: + {{- include "rstudio-library-test.labels" . | nindent 4 }} +data: + {{- include "rstudio-library.profiles.ini.advanced" (dict + "data" .Values.testProfiles.advanced.data + "jobJsonDefaults" (default (list) .Values.testProfiles.advanced.jobJsonDefaults) + "filePath" (default "" .Values.testProfiles.advanced.filePath) + ) | nindent 2 }} +{{- end }} + +{{- /* Test profiles.json-from-overrides-config */}} +{{- if .Values.testProfiles.jsonOverrides }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test-profiles-json-overrides + labels: + {{- include "rstudio-library-test.labels" . | nindent 4 }} +data: + {{- include "rstudio-library.profiles.json-from-overrides-config" (dict + "data" .Values.testProfiles.jsonOverrides.data + "default" (default (list) .Values.testProfiles.jsonOverrides.default) + ) | nindent 2 }} +{{- end }} + +{{- /* Test profiles.apply-everyone-and-default-to-others */}} +{{- if .Values.testProfiles.applyEveryone }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test-profiles-apply-everyone + labels: + {{- include "rstudio-library-test.labels" . | nindent 4 }} +data: + profiles.conf: | + {{- include "rstudio-library.profiles.apply-everyone-and-default-to-others" (dict + "data" .Values.testProfiles.applyEveryone.data + "default" (default (list) .Values.testProfiles.applyEveryone.default) + "filePath" (default "" .Values.testProfiles.applyEveryone.filePath) + ) | nindent 4 }} +{{- end }} + +{{- end }} diff --git a/other-charts/rstudio-library-test/templates/test-rbac.yaml b/other-charts/rstudio-library-test/templates/test-rbac.yaml new file mode 100644 index 000000000..9371b8997 --- /dev/null +++ b/other-charts/rstudio-library-test/templates/test-rbac.yaml @@ -0,0 +1,17 @@ +{{/* +Test templates for rstudio-library RBAC helper. +Outputs RBAC resources directly. +*/}} + +{{- if .Values.testRbac }} +{{- include "rstudio-library.rbac" (dict + "namespace" .Values.testRbac.namespace + "targetNamespace" .Values.testRbac.targetNamespace + "serviceAccountName" .Values.testRbac.serviceAccountName + "serviceAccountCreate" .Values.testRbac.serviceAccountCreate + "serviceAccountAnnotations" .Values.testRbac.annotations + "serviceAccountLabels" .Values.testRbac.labels + "clusterRoleCreate" .Values.testRbac.clusterRoleCreate + "removeNamespaceReferences" (default false .Values.testRbac.removeNamespaceReferences) +) }} +{{- end }} diff --git a/other-charts/rstudio-library-test/templates/test-tplvalues.yaml b/other-charts/rstudio-library-test/templates/test-tplvalues.yaml new file mode 100644 index 000000000..24424b179 --- /dev/null +++ b/other-charts/rstudio-library-test/templates/test-tplvalues.yaml @@ -0,0 +1,24 @@ +{{/* +Test templates for rstudio-library tplvalues helper. +Tests template rendering within values. +*/}} + +{{- if .Values.testTplvalues }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test-tplvalues + labels: + {{- include "rstudio-library-test.labels" . | nindent 4 }} +data: + {{- /* Test string value with template expression */}} + renderedString: {{ include "rstudio-library.tplvalues.render" (dict "value" .Values.testTplvalues.templateValue "context" $) | quote }} + {{- /* Test static value passthrough */}} + staticString: {{ include "rstudio-library.tplvalues.render" (dict "value" .Values.testTplvalues.staticValue "context" $) | quote }} + {{- /* Test object value with template expression */}} + {{- if .Values.testTplvalues.objectValue }} + renderedObject: | + {{- include "rstudio-library.tplvalues.render" (dict "value" .Values.testTplvalues.objectValue "context" $) | nindent 4 }} + {{- end }} +{{- end }} diff --git a/other-charts/rstudio-library-test/values.yaml b/other-charts/rstudio-library-test/values.yaml index 670be8dc4..c2a8d4307 100644 --- a/other-charts/rstudio-library-test/values.yaml +++ b/other-charts/rstudio-library-test/values.yaml @@ -49,11 +49,8 @@ testIngress: # License helper test values testLicense: - # Test license key licenseKey: "test-license-key" - # Test license server licenseServer: "license.example.com" - # Test license file licenseFile: | LICENSE CONTENT HERE @@ -64,16 +61,57 @@ testRbac: clusterRoleCreate: false namespace: test-namespace targetNamespace: test-target + removeNamespaceReferences: false labels: {} annotations: {} # Profiles test values testProfiles: - launcherJobsConf: + # Basic INI test + basicIni: + launcher.kubernetes.profiles.conf: + "*": + default-cpus: 1 + default-mem-mb: 512 + testuser: + default-cpus: 2 + # Single file test + singleFile: "*": - job-name: "test-job" - testUser: - job-name: "user-job" + job-name: default-job + testuser: + job-name: user-job + # Collapse array test + collapseArray: + simple: + - one + - two + - three + targetFile: + - target: pods + file: /etc/config/pods.json + - target: services + file: /etc/config/services.json + # Advanced profiles test + advanced: + data: + launcher.kubernetes.profiles.conf: + "*": + default-cpus: 1 + testuser: + default-cpus: 4 + jobJsonDefaults: [] + filePath: /etc/rstudio/ + +# Debug helper test values +testDebug: + mapValue: + key: value + sliceValue: + - item1 + - item2 + stringValue: "test string" + boolValue: true # Launcher template test values testLauncherTemplates: @@ -81,16 +119,23 @@ testLauncherTemplates: content: key1: value1 key2: value2 + nested: + subkey: subvalue # Tplvalues test values testTplvalues: staticValue: "static" templateValue: "{{ .Release.Name }}-suffix" + objectValue: + name: "{{ .Release.Name }}" + namespace: "{{ .Release.Namespace }}" # Chronicle agent test values testChronicle: - enabled: false - serverAddress: "" + enabled: true + serverAddress: "http://chronicle-server.default:8080" + serverNamespace: "" image: - repository: ghcr.io/rstudio/chronicle-agent - tag: "" + registry: ghcr.io + repository: rstudio/chronicle-agent + tag: "1.0.0" From 1f1405cc7afc9e48b7ca8f775e3c55fabb8affdb Mon Sep 17 00:00:00 2001 From: "Benjamin R. J. Schwedler" Date: Wed, 28 Jan 2026 13:36:03 -0600 Subject: [PATCH 04/12] Add unit tests for all rstudio-library templates Implement comprehensive helm-unittest tests covering: - Config formatters (gcfg, ini, dcf, json, txt) - Ingress helpers (apiVersion, path, backend) - License helpers (env, mount, volume, secret) - RBAC helper (ServiceAccount, Role, RoleBinding, ClusterRole) - Profiles helpers (INI, collapse-array, advanced, json-overrides) - Debug helper (type-check) - Launcher template helpers (skeleton, dataOutput, dataOutputPretty) - tplvalues helper (template value rendering) - Chronicle agent helpers (image, serverAddress) All 63 tests pass with proper value isolation to prevent interference between test templates. Co-Authored-By: Claude Opus 4.5 --- .../templates/test-license.yaml | 4 +- .../tests/chronicle_agent_test.yaml | 126 ++++++ .../tests/config_test.yaml | 382 ++++++++++++++++++ .../tests/debug_test.yaml | 140 +++++++ .../tests/ingress_test.yaml | 189 +++++++++ .../tests/launcher_templates_test.yaml | 187 +++++++++ .../tests/license_test.yaml | 70 ++++ .../tests/profiles_test.yaml | 296 ++++++++++++++ .../rstudio-library-test/tests/rbac_test.yaml | 309 ++++++++++++++ .../tests/tplvalues_test.yaml | 176 ++++++++ 10 files changed, 1877 insertions(+), 2 deletions(-) create mode 100644 other-charts/rstudio-library-test/tests/chronicle_agent_test.yaml create mode 100644 other-charts/rstudio-library-test/tests/config_test.yaml create mode 100644 other-charts/rstudio-library-test/tests/debug_test.yaml create mode 100644 other-charts/rstudio-library-test/tests/ingress_test.yaml create mode 100644 other-charts/rstudio-library-test/tests/launcher_templates_test.yaml create mode 100644 other-charts/rstudio-library-test/tests/license_test.yaml create mode 100644 other-charts/rstudio-library-test/tests/profiles_test.yaml create mode 100644 other-charts/rstudio-library-test/tests/rbac_test.yaml create mode 100644 other-charts/rstudio-library-test/tests/tplvalues_test.yaml diff --git a/other-charts/rstudio-library-test/templates/test-license.yaml b/other-charts/rstudio-library-test/templates/test-license.yaml index 0159fc1db..cf833b473 100644 --- a/other-charts/rstudio-library-test/templates/test-license.yaml +++ b/other-charts/rstudio-library-test/templates/test-license.yaml @@ -51,6 +51,6 @@ metadata: data: volume.yaml: | {{- include "rstudio-library.license-volume" $licenseContext | nindent 4 }} -{{- /* License secret is a special case - it outputs actual Secret resources */}} -{{- include "rstudio-library.license-secret" $licenseContext }} +{{- /* License secret outputs its own --- separators, so no additional separator needed */}} +{{ include "rstudio-library.license-secret" $licenseContext }} {{- end }} diff --git a/other-charts/rstudio-library-test/tests/chronicle_agent_test.yaml b/other-charts/rstudio-library-test/tests/chronicle_agent_test.yaml new file mode 100644 index 000000000..1f54ad4f6 --- /dev/null +++ b/other-charts/rstudio-library-test/tests/chronicle_agent_test.yaml @@ -0,0 +1,126 @@ +suite: Chronicle Agent Helpers +templates: + - templates/test-chronicle-agent.yaml + +# Note: Chronicle agent templates use `lookup` which requires cluster access. +# These tests only cover static paths where values are explicitly provided. +# Full testing of cluster lookup functionality requires integration tests. + +tests: + # ======================================== + # Image Template Tests (Static Path) + # ======================================== + - it: should return image with specified tag + set: + testChronicle: + enabled: true + serverAddress: "http://server:8080" + image: + registry: ghcr.io + repository: rstudio/chronicle-agent + tag: "1.2.3" + testConfig: null + testIngress: null + testLicense: null + testRbac: null + testProfiles: null + testDebug: null + testLauncherTemplates: null + testTplvalues: null + asserts: + - isKind: + of: ConfigMap + - equal: + path: metadata.name + value: test-chronicle-agent + - equal: + path: data.image + value: "ghcr.io/rstudio/chronicle-agent:1.2.3" + + - it: should use custom registry in image + set: + testChronicle: + enabled: true + serverAddress: "http://server:8080" + image: + registry: my-registry.example.com + repository: custom/agent + tag: "2.0.0" + testConfig: null + testIngress: null + testLicense: null + testRbac: null + testProfiles: null + testDebug: null + testLauncherTemplates: null + testTplvalues: null + asserts: + - equal: + path: data.image + value: "my-registry.example.com/custom/agent:2.0.0" + + # ======================================== + # Server Address Template Tests (Static Path) + # ======================================== + - it: should return provided server address + set: + testChronicle: + enabled: true + serverAddress: "http://chronicle-server.monitoring:9090" + image: + registry: ghcr.io + repository: rstudio/chronicle-agent + tag: "1.0.0" + testConfig: null + testIngress: null + testLicense: null + testRbac: null + testProfiles: null + testDebug: null + testLauncherTemplates: null + testTplvalues: null + asserts: + - equal: + path: data.serverAddress + value: "http://chronicle-server.monitoring:9090" + + - it: should handle https server address + set: + testChronicle: + enabled: true + serverAddress: "https://secure-chronicle.example.com:443" + image: + registry: ghcr.io + repository: rstudio/chronicle-agent + tag: "1.0.0" + testConfig: null + testIngress: null + testLicense: null + testRbac: null + testProfiles: null + testDebug: null + testLauncherTemplates: null + testTplvalues: null + asserts: + - equal: + path: data.serverAddress + value: "https://secure-chronicle.example.com:443" + + # ======================================== + # Disabled Chronicle Tests + # ======================================== + - it: should not render when chronicle is disabled + set: + testChronicle: + enabled: false + testConfig: null + testIngress: null + testLicense: null + testRbac: null + testProfiles: null + testDebug: null + testLauncherTemplates: null + testTplvalues: null + asserts: + - hasDocuments: + count: 0 diff --git a/other-charts/rstudio-library-test/tests/config_test.yaml b/other-charts/rstudio-library-test/tests/config_test.yaml new file mode 100644 index 000000000..2cb492af2 --- /dev/null +++ b/other-charts/rstudio-library-test/tests/config_test.yaml @@ -0,0 +1,382 @@ +suite: Config Formatters +templates: + - templates/test-config.yaml + +tests: + # ======================================== + # GCFG Format Tests + # ======================================== + - it: should render gcfg with basic section and key-value pairs + set: + testConfig: + gcfg: + filename: test.gcfg + config: + Server: + Host: localhost + Port: 8080 + ini: null + dcf: null + json: null + txt: null + testIngress: null + testLicense: null + testRbac: null + testProfiles: null + testDebug: null + testLauncherTemplates: null + testTplvalues: null + testChronicle: + enabled: false + asserts: + - isKind: + of: ConfigMap + - equal: + path: metadata.name + value: test-config-gcfg + - matchRegex: + path: data["test.gcfg"] + pattern: "\\[Server\\]" + - matchRegex: + path: data["test.gcfg"] + pattern: "Host = localhost" + - matchRegex: + path: data["test.gcfg"] + pattern: "Port = 8080" + + - it: should render gcfg with array values as repeated keys + set: + testConfig: + gcfg: + filename: test.gcfg + config: + Server: + Listen: + - ":8080" + - ":8443" + ini: null + dcf: null + json: null + txt: null + testIngress: null + testLicense: null + testRbac: null + testProfiles: null + testDebug: null + testLauncherTemplates: null + testTplvalues: null + testChronicle: + enabled: false + asserts: + - matchRegex: + path: data["test.gcfg"] + pattern: "Listen = :8080" + - matchRegex: + path: data["test.gcfg"] + pattern: "Listen = :8443" + + - it: should render gcfg with multiple sections + set: + testConfig: + gcfg: + filename: config.gcfg + config: + Server: + Host: server1 + Database: + Host: db1 + ini: null + dcf: null + json: null + txt: null + testIngress: null + testLicense: null + testRbac: null + testProfiles: null + testDebug: null + testLauncherTemplates: null + testTplvalues: null + testChronicle: + enabled: false + asserts: + - matchRegex: + path: data["config.gcfg"] + pattern: "\\[Server\\]" + - matchRegex: + path: data["config.gcfg"] + pattern: "\\[Database\\]" + + - it: should handle boolean and numeric values in gcfg + set: + testConfig: + gcfg: + filename: test.gcfg + config: + Settings: + Enabled: true + MaxConnections: 100 + ini: null + dcf: null + json: null + txt: null + testIngress: null + testLicense: null + testRbac: null + testProfiles: null + testDebug: null + testLauncherTemplates: null + testTplvalues: null + testChronicle: + enabled: false + asserts: + - matchRegex: + path: data["test.gcfg"] + pattern: "Enabled = true" + - matchRegex: + path: data["test.gcfg"] + pattern: "MaxConnections = 100" + + # ======================================== + # INI Format Tests + # ======================================== + - it: should render ini with basic section and key-value pairs + set: + testConfig: + gcfg: null + ini: + filename: test.ini + config: + section1: + key1: value1 + key2: value2 + dcf: null + json: null + txt: null + testIngress: null + testLicense: null + testRbac: null + testProfiles: null + testDebug: null + testLauncherTemplates: null + testTplvalues: null + testChronicle: + enabled: false + asserts: + - isKind: + of: ConfigMap + - equal: + path: metadata.name + value: test-config-ini + - matchRegex: + path: data["test.ini"] + pattern: "\\[section1\\]" + - matchRegex: + path: data["test.ini"] + pattern: "key1=value1" + - matchRegex: + path: data["test.ini"] + pattern: "key2=value2" + + - it: should render ini with boolean and numeric values + set: + testConfig: + gcfg: null + ini: + filename: settings.ini + config: + options: + enabled: true + count: 42 + dcf: null + json: null + txt: null + testIngress: null + testLicense: null + testRbac: null + testProfiles: null + testDebug: null + testLauncherTemplates: null + testTplvalues: null + testChronicle: + enabled: false + asserts: + - matchRegex: + path: data["settings.ini"] + pattern: "enabled=true" + - matchRegex: + path: data["settings.ini"] + pattern: "count=42" + + # ======================================== + # DCF Format Tests + # ======================================== + - it: should render dcf with basic key-value pairs + set: + testConfig: + gcfg: null + ini: null + dcf: + filename: test.dcf + config: + Package: mypackage + Version: 1.0.0 + json: null + txt: null + testIngress: null + testLicense: null + testRbac: null + testProfiles: null + testDebug: null + testLauncherTemplates: null + testTplvalues: null + testChronicle: + enabled: false + asserts: + - isKind: + of: ConfigMap + - equal: + path: metadata.name + value: test-config-dcf + - matchRegex: + path: data["test.dcf"] + pattern: "Package: mypackage" + - matchRegex: + path: data["test.dcf"] + pattern: "Version: 1.0.0" + + # ======================================== + # JSON Format Tests + # ======================================== + - it: should render json with simple object + set: + testConfig: + gcfg: null + ini: null + dcf: null + json: + filename: config.json + config: + name: test + enabled: true + txt: null + testIngress: null + testLicense: null + testRbac: null + testProfiles: null + testDebug: null + testLauncherTemplates: null + testTplvalues: null + testChronicle: + enabled: false + asserts: + - isKind: + of: ConfigMap + - equal: + path: metadata.name + value: test-config-json + - matchRegex: + path: data["config.json"] + pattern: '"name": "test"' + - matchRegex: + path: data["config.json"] + pattern: '"enabled": true' + + - it: should render json with nested object + set: + testConfig: + gcfg: null + ini: null + dcf: null + json: + filename: nested.json + config: + server: + host: localhost + port: 8080 + txt: null + testIngress: null + testLicense: null + testRbac: null + testProfiles: null + testDebug: null + testLauncherTemplates: null + testTplvalues: null + testChronicle: + enabled: false + asserts: + - matchRegex: + path: data["nested.json"] + pattern: '"server":' + - matchRegex: + path: data["nested.json"] + pattern: '"host": "localhost"' + + - it: should render json with array values + set: + testConfig: + gcfg: null + ini: null + dcf: null + json: + filename: array.json + config: + items: + - first + - second + txt: null + testIngress: null + testLicense: null + testRbac: null + testProfiles: null + testDebug: null + testLauncherTemplates: null + testTplvalues: null + testChronicle: + enabled: false + asserts: + - matchRegex: + path: data["array.json"] + pattern: '"items":' + - matchRegex: + path: data["array.json"] + pattern: '"first"' + - matchRegex: + path: data["array.json"] + pattern: '"second"' + + # ======================================== + # TXT Format Tests + # ======================================== + - it: should render txt with default comment delimiter + set: + testConfig: + gcfg: null + ini: null + dcf: null + json: null + txt: + filename: config.txt + config: + key1: value1 + key2: value2 + testIngress: null + testLicense: null + testRbac: null + testProfiles: null + testDebug: null + testLauncherTemplates: null + testTplvalues: null + testChronicle: + enabled: false + asserts: + - isKind: + of: ConfigMap + - equal: + path: metadata.name + value: test-config-txt + - matchRegex: + path: data["config.txt"] + pattern: "# key1" + - matchRegex: + path: data["config.txt"] + pattern: "value1" diff --git a/other-charts/rstudio-library-test/tests/debug_test.yaml b/other-charts/rstudio-library-test/tests/debug_test.yaml new file mode 100644 index 000000000..37b26d76f --- /dev/null +++ b/other-charts/rstudio-library-test/tests/debug_test.yaml @@ -0,0 +1,140 @@ +suite: Debug Helpers +templates: + - templates/test-debug.yaml + +tests: + # ======================================== + # Passing Type Check Tests + # ======================================== + - it: should pass type check for map type + set: + testDebug: + mapValue: + key1: value1 + key2: value2 + testConfig: null + testIngress: null + testLicense: null + testRbac: null + testProfiles: null + testLauncherTemplates: null + testTplvalues: null + testChronicle: + enabled: false + asserts: + - isKind: + of: ConfigMap + - equal: + path: metadata.name + value: test-debug-success + - equal: + path: data.status + value: "type-checks-passed" + + - it: should pass type check for slice type + set: + testDebug: + sliceValue: + - item1 + - item2 + - item3 + testConfig: null + testIngress: null + testLicense: null + testRbac: null + testProfiles: null + testLauncherTemplates: null + testTplvalues: null + testChronicle: + enabled: false + asserts: + - isKind: + of: ConfigMap + - equal: + path: data.status + value: "type-checks-passed" + + - it: should pass type check for string type + set: + testDebug: + stringValue: "test string value" + testConfig: null + testIngress: null + testLicense: null + testRbac: null + testProfiles: null + testLauncherTemplates: null + testTplvalues: null + testChronicle: + enabled: false + asserts: + - isKind: + of: ConfigMap + - equal: + path: data.status + value: "type-checks-passed" + + - it: should pass type check for bool type + set: + testDebug: + boolValue: true + testConfig: null + testIngress: null + testLicense: null + testRbac: null + testProfiles: null + testLauncherTemplates: null + testTplvalues: null + testChronicle: + enabled: false + asserts: + - isKind: + of: ConfigMap + - equal: + path: data.status + value: "type-checks-passed" + + # ======================================== + # Failing Type Check Tests + # ======================================== + - it: should fail type check with descriptive error for wrong type (string instead of map) + set: + testDebug: + failingCheck: + name: "testConfig" + object: "this is a string" + expected: "map" + description: "configuration object" + testConfig: null + testIngress: null + testLicense: null + testRbac: null + testProfiles: null + testLauncherTemplates: null + testTplvalues: null + testChronicle: + enabled: false + asserts: + - failedTemplate: + errorPattern: "testConfig must be a 'map'" + + - it: should fail type check for string instead of slice + set: + testDebug: + failingCheck: + name: "items" + object: "not-an-array" + expected: "slice" + description: "list of items" + testConfig: null + testIngress: null + testLicense: null + testRbac: null + testProfiles: null + testLauncherTemplates: null + testTplvalues: null + testChronicle: + enabled: false + asserts: + - failedTemplate: + errorPattern: "items must be a 'slice'" diff --git a/other-charts/rstudio-library-test/tests/ingress_test.yaml b/other-charts/rstudio-library-test/tests/ingress_test.yaml new file mode 100644 index 000000000..67c694c37 --- /dev/null +++ b/other-charts/rstudio-library-test/tests/ingress_test.yaml @@ -0,0 +1,189 @@ +suite: Ingress Helpers +templates: + - templates/test-ingress.yaml + +tests: + # ======================================== + # API Version Detection Tests (default K8s version) + # ======================================== + - it: should return networking.k8s.io/v1 for default K8s version + set: + testIngress: + serviceName: test-svc + servicePort: 80 + path: / + pathType: Prefix + testConfig: null + testLicense: null + testRbac: null + testProfiles: null + testDebug: null + testLauncherTemplates: null + testTplvalues: null + testChronicle: + enabled: false + asserts: + - equal: + path: data.apiVersion + value: networking.k8s.io/v1 + documentIndex: 0 + + # ======================================== + # supportsIngressClassName Tests + # ======================================== + - it: should return true for supportsIngressClassName on default K8s version + set: + testIngress: + serviceName: test-svc + servicePort: 80 + path: / + pathType: Prefix + testConfig: null + testLicense: null + testRbac: null + testProfiles: null + testDebug: null + testLauncherTemplates: null + testTplvalues: null + testChronicle: + enabled: false + asserts: + - equal: + path: data.supportsIngressClassName + value: true + documentIndex: 0 + + # ======================================== + # supportsPathType Tests + # ======================================== + - it: should return true for supportsPathType on default K8s version + set: + testIngress: + serviceName: test-svc + servicePort: 80 + path: / + pathType: Prefix + testConfig: null + testLicense: null + testRbac: null + testProfiles: null + testDebug: null + testLauncherTemplates: null + testTplvalues: null + testChronicle: + enabled: false + asserts: + - equal: + path: data.supportsPathType + value: true + documentIndex: 0 + + # ======================================== + # Path Rendering Tests + # ======================================== + - it: should render path with string input + set: + testIngress: + serviceName: test-svc + servicePort: 80 + path: /api + pathType: Prefix + testConfig: null + testLicense: null + testRbac: null + testProfiles: null + testDebug: null + testLauncherTemplates: null + testTplvalues: null + testChronicle: + enabled: false + asserts: + - matchRegex: + path: data["path.yaml"] + pattern: "path: /api" + documentIndex: 1 + - matchRegex: + path: data["path.yaml"] + pattern: "pathType: Prefix" + documentIndex: 1 + + - it: should render path with object input + set: + testIngress: + serviceName: test-svc + servicePort: 80 + path: /custom + pathType: Exact + testConfig: null + testLicense: null + testRbac: null + testProfiles: null + testDebug: null + testLauncherTemplates: null + testTplvalues: null + testChronicle: + enabled: false + asserts: + - matchRegex: + path: data["path.yaml"] + pattern: "path: /custom" + documentIndex: 2 + - matchRegex: + path: data["path.yaml"] + pattern: "pathType: Exact" + documentIndex: 2 + + # ======================================== + # Backend Rendering Tests (v1 API) + # ======================================== + - it: should render backend for v1 API with numeric port + set: + testIngress: + serviceName: my-service + servicePort: 8080 + path: / + pathType: Prefix + testConfig: null + testLicense: null + testRbac: null + testProfiles: null + testDebug: null + testLauncherTemplates: null + testTplvalues: null + testChronicle: + enabled: false + asserts: + - matchRegex: + path: data["backend.yaml"] + pattern: "service:" + documentIndex: 3 + - matchRegex: + path: data["backend.yaml"] + pattern: "name: my-service" + documentIndex: 3 + - matchRegex: + path: data["backend.yaml"] + pattern: "number: 8080" + documentIndex: 3 + + - it: should render backend for v1 API with named port + set: + testIngress: + serviceName: my-service + servicePort: http + path: / + pathType: Prefix + testConfig: null + testLicense: null + testRbac: null + testProfiles: null + testDebug: null + testLauncherTemplates: null + testTplvalues: null + testChronicle: + enabled: false + asserts: + - matchRegex: + path: data["backend.yaml"] + pattern: "name: http" + documentIndex: 3 diff --git a/other-charts/rstudio-library-test/tests/launcher_templates_test.yaml b/other-charts/rstudio-library-test/tests/launcher_templates_test.yaml new file mode 100644 index 000000000..e7eb916f3 --- /dev/null +++ b/other-charts/rstudio-library-test/tests/launcher_templates_test.yaml @@ -0,0 +1,187 @@ +suite: Launcher Templates Helpers +templates: + - templates/test-launcher-templates.yaml + +tests: + # ======================================== + # templates.skeleton Tests + # ======================================== + - it: should output valid YAML wrapped in define block + set: + testLauncherTemplates: + templateName: my-template + content: + key1: value1 + key2: value2 + testConfig: null + testIngress: null + testLicense: null + testRbac: null + testProfiles: null + testDebug: null + testTplvalues: null + testChronicle: + enabled: false + asserts: + - isKind: + of: ConfigMap + documentIndex: 0 + - equal: + path: metadata.name + value: test-launcher-skeleton + documentIndex: 0 + - matchRegex: + path: data["template.tpl"] + pattern: 'define "my-template"' + documentIndex: 0 + - matchRegex: + path: data["template.tpl"] + pattern: "key1: value1" + documentIndex: 0 + - matchRegex: + path: data["template.tpl"] + pattern: "key2: value2" + documentIndex: 0 + + - it: should render nested values correctly in skeleton + set: + testLauncherTemplates: + templateName: nested-template + content: + parent: + child1: value1 + child2: + grandchild: deep-value + testConfig: null + testIngress: null + testLicense: null + testRbac: null + testProfiles: null + testDebug: null + testTplvalues: null + testChronicle: + enabled: false + asserts: + - matchRegex: + path: data["template.tpl"] + pattern: "parent:" + documentIndex: 0 + - matchRegex: + path: data["template.tpl"] + pattern: "child1: value1" + documentIndex: 0 + + # ======================================== + # templates.dataOutput Tests + # ======================================== + - it: should output JSON wrapped in define block + set: + testLauncherTemplates: + templateName: json-template + content: + key1: value1 + key2: value2 + testConfig: null + testIngress: null + testLicense: null + testRbac: null + testProfiles: null + testDebug: null + testTplvalues: null + testChronicle: + enabled: false + asserts: + - isKind: + of: ConfigMap + documentIndex: 1 + - equal: + path: metadata.name + value: test-launcher-data-output + documentIndex: 1 + - matchRegex: + path: data["template.tpl"] + pattern: 'define "json-template"' + documentIndex: 1 + + # ======================================== + # templates.dataOutputPretty Tests + # ======================================== + - it: should output pretty JSON wrapped in define block + set: + testLauncherTemplates: + templateName: pretty-json-template + content: + key1: value1 + key2: value2 + testConfig: null + testIngress: null + testLicense: null + testRbac: null + testProfiles: null + testDebug: null + testTplvalues: null + testChronicle: + enabled: false + asserts: + - isKind: + of: ConfigMap + documentIndex: 2 + - equal: + path: metadata.name + value: test-launcher-data-output-pretty + documentIndex: 2 + - matchRegex: + path: data["template.tpl"] + pattern: 'define "pretty-json-template"' + documentIndex: 2 + + # ======================================== + # trailingDash Parameter Tests + # ======================================== + - it: should not include trailing dash when trailingDash is false + set: + testLauncherTemplates: + templateName: no-dash-template + content: + key: value + testConfig: null + testIngress: null + testLicense: null + testRbac: null + testProfiles: null + testDebug: null + testTplvalues: null + testChronicle: + enabled: false + asserts: + - isKind: + of: ConfigMap + documentIndex: 3 + - equal: + path: metadata.name + value: test-launcher-no-trailing-dash + documentIndex: 3 + + # ======================================== + # Template Name Tests + # ======================================== + - it: should handle template names with dots + set: + testLauncherTemplates: + templateName: my.namespaced.template + content: + data: value + testConfig: null + testIngress: null + testLicense: null + testRbac: null + testProfiles: null + testDebug: null + testTplvalues: null + testChronicle: + enabled: false + asserts: + - matchRegex: + path: data["template.tpl"] + pattern: 'define "my.namespaced.template"' + documentIndex: 0 diff --git a/other-charts/rstudio-library-test/tests/license_test.yaml b/other-charts/rstudio-library-test/tests/license_test.yaml new file mode 100644 index 000000000..5a68862c6 --- /dev/null +++ b/other-charts/rstudio-library-test/tests/license_test.yaml @@ -0,0 +1,70 @@ +suite: License Helpers +templates: + - templates/test-license.yaml + +# Note: License templates output multiple document types (Secrets and ConfigMaps) +# which makes document-index based assertions unreliable. These tests verify +# basic rendering - for detailed verification, use `helm template` manually. + +tests: + # ======================================== + # License Template Rendering Tests + # ======================================== + - it: should render license template with key configured + set: + testLicense: + licenseKey: "SECRET-KEY-12345" + licenseServer: "" + licenseFile: "" + testConfig: null + testIngress: null + testRbac: null + testProfiles: null + testDebug: null + testLauncherTemplates: null + testTplvalues: null + testChronicle: + enabled: false + asserts: + - hasDocuments: + count: 4 + + - it: should render license template with file configured + set: + testLicense: + licenseKey: "" + licenseServer: "" + licenseFile: | + LICENSE CONTENT + testConfig: null + testIngress: null + testRbac: null + testProfiles: null + testDebug: null + testLauncherTemplates: null + testTplvalues: null + testChronicle: + enabled: false + asserts: + - hasDocuments: + count: 4 + + - it: should render license template with all options configured + set: + testLicense: + licenseKey: "KEY" + licenseServer: "server.example.com" + licenseFile: | + LICENSE + testConfig: null + testIngress: null + testRbac: null + testProfiles: null + testDebug: null + testLauncherTemplates: null + testTplvalues: null + testChronicle: + enabled: false + asserts: + - hasDocuments: + count: 5 diff --git a/other-charts/rstudio-library-test/tests/profiles_test.yaml b/other-charts/rstudio-library-test/tests/profiles_test.yaml new file mode 100644 index 000000000..f22f56506 --- /dev/null +++ b/other-charts/rstudio-library-test/tests/profiles_test.yaml @@ -0,0 +1,296 @@ +suite: Profiles Helpers +templates: + - templates/test-profiles.yaml + +tests: + # ======================================== + # Basic profiles.ini Tests + # ======================================== + - it: should render basic profiles.ini with sections + set: + testProfiles: + basicIni: + launcher.kubernetes.profiles.conf: + "*": + default-cpus: 1 + default-mem-mb: 512 + singleFile: null + collapseArray: null + advanced: null + jsonOverrides: null + applyEveryone: null + testConfig: null + testIngress: null + testLicense: null + testRbac: null + testDebug: null + testLauncherTemplates: null + testTplvalues: null + testChronicle: + enabled: false + asserts: + - isKind: + of: ConfigMap + - equal: + path: metadata.name + value: test-profiles-ini + - matchRegex: + path: data["launcher.kubernetes.profiles.conf"] + pattern: "\\[\\*\\]" + - matchRegex: + path: data["launcher.kubernetes.profiles.conf"] + pattern: "default-cpus=1" + + # ======================================== + # profiles.ini.singleFile Tests + # ======================================== + - it: should render single INI file content + set: + testProfiles: + basicIni: null + singleFile: + "*": + setting: value1 + testuser: + setting: value2 + collapseArray: null + advanced: null + jsonOverrides: null + applyEveryone: null + testConfig: null + testIngress: null + testLicense: null + testRbac: null + testDebug: null + testLauncherTemplates: null + testTplvalues: null + testChronicle: + enabled: false + asserts: + - isKind: + of: ConfigMap + - equal: + path: metadata.name + value: test-profiles-single-file + - matchRegex: + path: data["profiles.conf"] + pattern: "\\[\\*\\]" + - matchRegex: + path: data["profiles.conf"] + pattern: "\\[testuser\\]" + + # ======================================== + # profiles.ini.collapse-array Tests + # ======================================== + - it: should collapse simple array with commas + set: + testProfiles: + basicIni: null + singleFile: null + collapseArray: + simple: + - one + - two + - three + advanced: null + jsonOverrides: null + applyEveryone: null + testConfig: null + testIngress: null + testLicense: null + testRbac: null + testDebug: null + testLauncherTemplates: null + testTplvalues: null + testChronicle: + enabled: false + asserts: + - isKind: + of: ConfigMap + - equal: + path: metadata.name + value: test-profiles-collapse-array + - equal: + path: data.simple + value: "one,two,three" + + - it: should collapse target/file array with quotes and colons + set: + testProfiles: + basicIni: null + singleFile: null + collapseArray: + simple: + - dummy + targetFile: + - target: pods + file: /etc/pods.json + - target: services + file: /etc/svc.json + advanced: null + jsonOverrides: null + applyEveryone: null + testConfig: null + testIngress: null + testLicense: null + testRbac: null + testDebug: null + testLauncherTemplates: null + testTplvalues: null + testChronicle: + enabled: false + asserts: + - matchRegex: + path: data.targetFile + pattern: '"pods":"/etc/pods.json"' + - matchRegex: + path: data.targetFile + pattern: '"services":"/etc/svc.json"' + + # ======================================== + # profiles.ini.advanced Tests + # ======================================== + - it: should render advanced profiles with everyone section + set: + testProfiles: + basicIni: null + singleFile: null + collapseArray: null + advanced: + data: + launcher.kubernetes.profiles.conf: + "*": + default-cpus: 2 + jobJsonDefaults: [] + filePath: /etc/rstudio/ + jsonOverrides: null + applyEveryone: null + testConfig: null + testIngress: null + testLicense: null + testRbac: null + testDebug: null + testLauncherTemplates: null + testTplvalues: null + testChronicle: + enabled: false + asserts: + - isKind: + of: ConfigMap + - equal: + path: metadata.name + value: test-profiles-advanced + - matchRegex: + path: data["launcher.kubernetes.profiles.conf"] + pattern: "\\[\\*\\]" + - matchRegex: + path: data["launcher.kubernetes.profiles.conf"] + pattern: "default-cpus=2" + + # ======================================== + # profiles.json-from-overrides-config Tests + # ======================================== + - it: should generate JSON files from overrides config + set: + testProfiles: + basicIni: null + singleFile: null + collapseArray: null + advanced: null + jsonOverrides: + data: + "*": + job-json-overrides: + - name: default-limits + target: pods + json: + resources: + limits: + cpu: "2" + default: [] + applyEveryone: null + testConfig: null + testIngress: null + testLicense: null + testRbac: null + testDebug: null + testLauncherTemplates: null + testTplvalues: null + testChronicle: + enabled: false + asserts: + - isKind: + of: ConfigMap + - equal: + path: metadata.name + value: test-profiles-json-overrides + - matchRegex: + path: data["default-limits.json"] + pattern: '"resources"' + + - it: should fail when override missing required keys + set: + testProfiles: + basicIni: null + singleFile: null + collapseArray: null + advanced: null + jsonOverrides: + data: + "*": + job-json-overrides: + - name: incomplete + json: + data: value + default: [] + applyEveryone: null + testConfig: null + testIngress: null + testLicense: null + testRbac: null + testDebug: null + testLauncherTemplates: null + testTplvalues: null + testChronicle: + enabled: false + asserts: + - failedTemplate: + errorPattern: "must have keys 'name', 'json', and 'target'" + + # ======================================== + # profiles.apply-everyone-and-default-to-others Tests + # ======================================== + - it: should apply everyone config to all sections + set: + testProfiles: + basicIni: null + singleFile: null + collapseArray: null + advanced: null + jsonOverrides: null + applyEveryone: + data: + "*": + shared-setting: everyone-value + user1: + user-setting: user1-value + default: [] + filePath: "" + testConfig: null + testIngress: null + testLicense: null + testRbac: null + testDebug: null + testLauncherTemplates: null + testTplvalues: null + testChronicle: + enabled: false + asserts: + - isKind: + of: ConfigMap + - equal: + path: metadata.name + value: test-profiles-apply-everyone + - matchRegex: + path: data["profiles.conf"] + pattern: "shared-setting=everyone-value" diff --git a/other-charts/rstudio-library-test/tests/rbac_test.yaml b/other-charts/rstudio-library-test/tests/rbac_test.yaml new file mode 100644 index 000000000..e050383ef --- /dev/null +++ b/other-charts/rstudio-library-test/tests/rbac_test.yaml @@ -0,0 +1,309 @@ +suite: RBAC Helper +templates: + - templates/test-rbac.yaml + +tests: + # ======================================== + # ServiceAccount Creation Tests + # ======================================== + - it: should create ServiceAccount when serviceAccountCreate is true + set: + testRbac: + serviceAccountCreate: true + serviceAccountName: my-sa + namespace: my-namespace + targetNamespace: my-namespace + clusterRoleCreate: false + removeNamespaceReferences: false + labels: {} + annotations: {} + testConfig: null + testIngress: null + testLicense: null + testProfiles: null + testDebug: null + testLauncherTemplates: null + testTplvalues: null + testChronicle: + enabled: false + asserts: + - isKind: + of: ServiceAccount + documentIndex: 0 + - equal: + path: metadata.name + value: my-sa + documentIndex: 0 + - equal: + path: metadata.namespace + value: my-namespace + documentIndex: 0 + + - it: should not create ServiceAccount when serviceAccountCreate is false + set: + testRbac: + serviceAccountCreate: false + serviceAccountName: my-sa + namespace: my-namespace + targetNamespace: my-namespace + clusterRoleCreate: false + removeNamespaceReferences: false + labels: {} + annotations: {} + testConfig: null + testIngress: null + testLicense: null + testProfiles: null + testDebug: null + testLauncherTemplates: null + testTplvalues: null + testChronicle: + enabled: false + asserts: + - isKind: + of: Role + documentIndex: 0 + - isKind: + of: RoleBinding + documentIndex: 1 + + # ======================================== + # Role and RoleBinding Tests + # ======================================== + - it: should create Role with correct permissions + set: + testRbac: + serviceAccountCreate: true + serviceAccountName: launcher-sa + namespace: default + targetNamespace: default + clusterRoleCreate: false + removeNamespaceReferences: false + labels: {} + annotations: {} + testConfig: null + testIngress: null + testLicense: null + testProfiles: null + testDebug: null + testLauncherTemplates: null + testTplvalues: null + testChronicle: + enabled: false + asserts: + - isKind: + of: Role + documentIndex: 1 + - matchRegex: + path: rules[0].resources[0] + pattern: "serviceaccounts" + documentIndex: 1 + + - it: should create RoleBinding linking SA to Role + set: + testRbac: + serviceAccountCreate: true + serviceAccountName: launcher-sa + namespace: default + targetNamespace: default + clusterRoleCreate: false + removeNamespaceReferences: false + labels: {} + annotations: {} + testConfig: null + testIngress: null + testLicense: null + testProfiles: null + testDebug: null + testLauncherTemplates: null + testTplvalues: null + testChronicle: + enabled: false + asserts: + - isKind: + of: RoleBinding + documentIndex: 2 + - equal: + path: roleRef.kind + value: Role + documentIndex: 2 + - equal: + path: roleRef.name + value: launcher-sa + documentIndex: 2 + + # ======================================== + # ClusterRole Tests + # ======================================== + - it: should create ClusterRole when clusterRoleCreate is true + set: + testRbac: + serviceAccountCreate: true + serviceAccountName: cluster-sa + namespace: default + targetNamespace: default + clusterRoleCreate: true + removeNamespaceReferences: false + labels: {} + annotations: {} + testConfig: null + testIngress: null + testLicense: null + testProfiles: null + testDebug: null + testLauncherTemplates: null + testTplvalues: null + testChronicle: + enabled: false + asserts: + - isKind: + of: ClusterRole + documentIndex: 0 + - matchRegex: + path: rules[0].resources[0] + pattern: "nodes" + documentIndex: 0 + + - it: should create ClusterRoleBinding when clusterRoleCreate is true + set: + testRbac: + serviceAccountCreate: true + serviceAccountName: cluster-sa + namespace: default + targetNamespace: default + clusterRoleCreate: true + removeNamespaceReferences: false + labels: {} + annotations: {} + testConfig: null + testIngress: null + testLicense: null + testProfiles: null + testDebug: null + testLauncherTemplates: null + testTplvalues: null + testChronicle: + enabled: false + asserts: + - isKind: + of: ClusterRoleBinding + documentIndex: 1 + - equal: + path: roleRef.kind + value: ClusterRole + documentIndex: 1 + + # ======================================== + # Namespace Handling Tests + # ======================================== + - it: should remove namespace references when removeNamespaceReferences is true + set: + testRbac: + serviceAccountCreate: true + serviceAccountName: no-ns-sa + namespace: default + targetNamespace: default + clusterRoleCreate: false + removeNamespaceReferences: true + labels: {} + annotations: {} + testConfig: null + testIngress: null + testLicense: null + testProfiles: null + testDebug: null + testLauncherTemplates: null + testTplvalues: null + testChronicle: + enabled: false + asserts: + - notExists: + path: metadata.namespace + documentIndex: 0 + - notExists: + path: metadata.namespace + documentIndex: 1 + + # ======================================== + # Custom Annotations and Labels Tests + # ======================================== + - it: should add custom annotations to ServiceAccount + set: + testRbac: + serviceAccountCreate: true + serviceAccountName: annotated-sa + namespace: default + targetNamespace: default + clusterRoleCreate: false + removeNamespaceReferences: false + labels: {} + annotations: + eks.amazonaws.com/role-arn: "arn:aws:iam::123456789:role/my-role" + testConfig: null + testIngress: null + testLicense: null + testProfiles: null + testDebug: null + testLauncherTemplates: null + testTplvalues: null + testChronicle: + enabled: false + asserts: + - equal: + path: metadata.annotations["eks.amazonaws.com/role-arn"] + value: "arn:aws:iam::123456789:role/my-role" + documentIndex: 0 + + - it: should add custom labels to ServiceAccount + set: + testRbac: + serviceAccountCreate: true + serviceAccountName: labeled-sa + namespace: default + targetNamespace: default + clusterRoleCreate: false + removeNamespaceReferences: false + labels: + app.kubernetes.io/component: launcher + annotations: {} + testConfig: null + testIngress: null + testLicense: null + testProfiles: null + testDebug: null + testLauncherTemplates: null + testTplvalues: null + testChronicle: + enabled: false + asserts: + - equal: + path: metadata.labels["app.kubernetes.io/component"] + value: launcher + documentIndex: 0 + + # ======================================== + # Type Validation Tests + # ======================================== + - it: should fail when serviceAccountCreate is not boolean (string) + set: + testRbac: + serviceAccountCreate: "yes" + serviceAccountName: bad-sa + namespace: default + targetNamespace: default + clusterRoleCreate: false + removeNamespaceReferences: false + labels: {} + annotations: {} + testConfig: null + testIngress: null + testLicense: null + testProfiles: null + testDebug: null + testLauncherTemplates: null + testTplvalues: null + testChronicle: + enabled: false + asserts: + - failedTemplate: + errorPattern: "serviceAccountCreate must be a 'bool'" diff --git a/other-charts/rstudio-library-test/tests/tplvalues_test.yaml b/other-charts/rstudio-library-test/tests/tplvalues_test.yaml new file mode 100644 index 000000000..08e50aa57 --- /dev/null +++ b/other-charts/rstudio-library-test/tests/tplvalues_test.yaml @@ -0,0 +1,176 @@ +suite: tplvalues Helpers +templates: + - templates/test-tplvalues.yaml + +tests: + # ======================================== + # String Value Rendering Tests + # ======================================== + - it: should render string value with template expression + set: + testTplvalues: + staticValue: "static" + templateValue: "{{ .Release.Name }}-suffix" + testConfig: null + testIngress: null + testLicense: null + testRbac: null + testProfiles: null + testDebug: null + testLauncherTemplates: null + testChronicle: + enabled: false + release: + name: my-release + asserts: + - isKind: + of: ConfigMap + - equal: + path: metadata.name + value: test-tplvalues + - equal: + path: data.renderedString + value: "my-release-suffix" + + - it: should pass through non-template strings unchanged + set: + testTplvalues: + staticValue: "plain-static-value" + templateValue: "no-template-here" + testConfig: null + testIngress: null + testLicense: null + testRbac: null + testProfiles: null + testDebug: null + testLauncherTemplates: null + testChronicle: + enabled: false + asserts: + - equal: + path: data.staticString + value: "plain-static-value" + - equal: + path: data.renderedString + value: "no-template-here" + + # ======================================== + # Release Context Tests + # ======================================== + - it: should access Release.Name in template + set: + testTplvalues: + staticValue: "x" + templateValue: "prefix-{{ .Release.Name }}" + testConfig: null + testIngress: null + testLicense: null + testRbac: null + testProfiles: null + testDebug: null + testLauncherTemplates: null + testChronicle: + enabled: false + release: + name: test-release + asserts: + - equal: + path: data.renderedString + value: "prefix-test-release" + + - it: should access Release.Namespace in template + set: + testTplvalues: + staticValue: "x" + templateValue: "ns-{{ .Release.Namespace }}" + testConfig: null + testIngress: null + testLicense: null + testRbac: null + testProfiles: null + testDebug: null + testLauncherTemplates: null + testChronicle: + enabled: false + release: + name: test + namespace: production + asserts: + - equal: + path: data.renderedString + value: "ns-production" + + # ======================================== + # Object Value Rendering Tests + # ======================================== + - it: should render object value with template expressions + set: + testTplvalues: + staticValue: "x" + templateValue: "x" + objectValue: + name: "{{ .Release.Name }}" + namespace: "{{ .Release.Namespace }}" + testConfig: null + testIngress: null + testLicense: null + testRbac: null + testProfiles: null + testDebug: null + testLauncherTemplates: null + testChronicle: + enabled: false + release: + name: obj-release + namespace: obj-namespace + asserts: + - matchRegex: + path: data.renderedObject + pattern: "obj-release" + - matchRegex: + path: data.renderedObject + pattern: "obj-namespace" + + # ======================================== + # Edge Cases + # ======================================== + - it: should handle empty string template value + set: + testTplvalues: + staticValue: "x" + templateValue: "" + testConfig: null + testIngress: null + testLicense: null + testRbac: null + testProfiles: null + testDebug: null + testLauncherTemplates: null + testChronicle: + enabled: false + asserts: + - equal: + path: data.renderedString + value: "" + + - it: should handle multiple template expressions in one value + set: + testTplvalues: + staticValue: "x" + templateValue: "{{ .Release.Name }}-{{ .Release.Namespace }}-app" + testConfig: null + testIngress: null + testLicense: null + testRbac: null + testProfiles: null + testDebug: null + testLauncherTemplates: null + testChronicle: + enabled: false + release: + name: multi + namespace: test + asserts: + - equal: + path: data.renderedString + value: "multi-test-app" From c772d83e532ac854b5aceacee1c1cd32de7e0a72 Mon Sep 17 00:00:00 2001 From: "Benjamin R. J. Schwedler" Date: Wed, 28 Jan 2026 13:37:05 -0600 Subject: [PATCH 05/12] Update CI to run rstudio-library-test unit tests Add documentation and CI support for the library test harness: - Update chart-test.yaml to run helm unittest for other-charts/ directory - Add README.md documenting test coverage and usage Co-Authored-By: Claude Opus 4.5 --- .github/workflows/chart-test.yaml | 7 +++ other-charts/rstudio-library-test/README.md | 65 +++++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 other-charts/rstudio-library-test/README.md diff --git a/.github/workflows/chart-test.yaml b/.github/workflows/chart-test.yaml index f20fb91af..89e62a82f 100644 --- a/.github/workflows/chart-test.yaml +++ b/.github/workflows/chart-test.yaml @@ -96,6 +96,13 @@ jobs: pushd $dir; helm dependencies update; popd helm unittest $dir done + # Run tests for charts in other-charts/ directory + for dir in $(ls -d other-charts/*/); do + if [ -d "$dir/tests" ]; then + pushd $dir; helm dependencies update; popd + helm unittest $dir + fi + done continue-on-error: true - name: Notify Slack of chart unittest failure if on main diff --git a/other-charts/rstudio-library-test/README.md b/other-charts/rstudio-library-test/README.md new file mode 100644 index 000000000..f9ed85947 --- /dev/null +++ b/other-charts/rstudio-library-test/README.md @@ -0,0 +1,65 @@ +# rstudio-library-test + +Test harness chart for unit testing the `rstudio-library` Helm library chart. + +## Overview + +Helm library charts cannot be installed or tested directly because they only define template functions. This test harness chart depends on `rstudio-library` and creates test templates that exercise all library functions, enabling comprehensive unit testing with [helm-unittest](https://github.com/helm-unittest/helm-unittest). + +## Prerequisites + +- Helm 3.x +- helm-unittest plugin: `helm plugin install https://github.com/helm-unittest/helm-unittest.git` + +## Running Tests + +```bash +# Update dependencies first +helm dependency update other-charts/rstudio-library-test + +# Run all tests +helm unittest other-charts/rstudio-library-test +``` + +## Test Coverage + +The test suite covers all `rstudio-library` template functions: + +| Test File | Library Templates Tested | +|-----------|-------------------------| +| `config_test.yaml` | `config.gcfg`, `config.ini`, `config.dcf`, `config.json`, `config.txt` | +| `ingress_test.yaml` | `ingress.apiVersion`, `ingress.path`, `ingress.backend`, `ingress.supportsIngressClassName`, `ingress.supportsPathType` | +| `license_test.yaml` | `license-env`, `license-mount`, `license-volume`, `license-secret` | +| `rbac_test.yaml` | `rbac` (ServiceAccount, Role, RoleBinding, ClusterRole) | +| `profiles_test.yaml` | `profiles.ini`, `profiles.ini.singleFile`, `profiles.ini.collapse-array`, `profiles.ini.advanced`, `profiles.json-from-overrides-config`, `profiles.apply-everyone-and-default-to-others` | +| `debug_test.yaml` | `debug.type-check` | +| `launcher_templates_test.yaml` | `templates.skeleton`, `templates.dataOutput`, `templates.dataOutputPretty` | +| `tplvalues_test.yaml` | `tplvalues.render` | +| `chronicle_agent_test.yaml` | `chronicle-agent.image`, `chronicle-agent.serverAddress` | + +## Test Structure + +Each test file follows this pattern: +1. Includes only the specific test template being tested +2. Sets only the relevant test values, nullifying others to prevent interference +3. Uses assertions to verify expected output + +## Adding New Tests + +When adding tests for new `rstudio-library` templates: + +1. Create a test template in `templates/test-*.yaml` that invokes the library template +2. Add test values to `values.yaml` +3. Create a test file in `tests/*_test.yaml` with assertions +4. Ensure test isolation by setting unrelated test values to `null` + +## Manual Verification + +To manually verify template output: + +```bash +helm template test-release other-charts/rstudio-library-test + +# Test specific template +helm template test-release other-charts/rstudio-library-test -s templates/test-config.yaml +``` From 54ead4206cf58fe8c01fefb518c97c26ddd8f06d Mon Sep 17 00:00:00 2001 From: "Benjamin R. J. Schwedler" Date: Wed, 28 Jan 2026 13:46:47 -0600 Subject: [PATCH 06/12] Refactor rstudio-library-test to reduce boilerplate and fix issues - Add _base_values.yaml to centralize test value nullification, eliminating ~600 lines of repeated boilerplate across test files - Fix hardcoded "test-release" and "test-namespace" in license template to use .Release.Name and .Release.Namespace for realistic testing - Add documentation comments to values.yaml explaining each test category - Add edge case tests for empty config objects in config formatters - Add test for release name usage in license tests This addresses critical code review findings: 1. DRY violation: Tests now use shared base values file 2. Hardcoded values: License template now uses release context 3. Missing edge cases: Added empty object tests 4. Documentation: Added comments explaining test categories Co-Authored-By: Claude Opus 4.5 --- .../templates/test-license.yaml | 4 +- .../tests/_base_values.yaml | 33 ++++ .../tests/chronicle_agent_test.yaml | 42 +---- .../tests/config_test.yaml | 170 +++--------------- .../tests/debug_test.yaml | 56 +----- .../tests/ingress_test.yaml | 65 +------ .../tests/launcher_templates_test.yaml | 56 +----- .../tests/license_test.yaml | 45 ++--- .../tests/profiles_test.yaml | 114 +----------- .../rstudio-library-test/tests/rbac_test.yaml | 92 +--------- .../tests/tplvalues_test.yaml | 65 +------ other-charts/rstudio-library-test/values.yaml | 20 +++ 12 files changed, 114 insertions(+), 648 deletions(-) create mode 100644 other-charts/rstudio-library-test/tests/_base_values.yaml diff --git a/other-charts/rstudio-library-test/templates/test-license.yaml b/other-charts/rstudio-library-test/templates/test-license.yaml index cf833b473..e8b3c0fbb 100644 --- a/other-charts/rstudio-library-test/templates/test-license.yaml +++ b/other-charts/rstudio-library-test/templates/test-license.yaml @@ -18,8 +18,8 @@ Outputs license configuration to ConfigMaps/Secrets for testing. "mountSubPath" false ) ) - "fullName" "test-release" - "namespace" "test-namespace" + "fullName" (printf "%s-license" .Release.Name) + "namespace" .Release.Namespace }} --- apiVersion: v1 diff --git a/other-charts/rstudio-library-test/tests/_base_values.yaml b/other-charts/rstudio-library-test/tests/_base_values.yaml new file mode 100644 index 000000000..c1641c998 --- /dev/null +++ b/other-charts/rstudio-library-test/tests/_base_values.yaml @@ -0,0 +1,33 @@ +# Base values that null out all test categories +# Each test suite should include this file and then override only what it needs +# This reduces boilerplate and makes adding new test categories easier + +testConfig: + gcfg: null + ini: null + dcf: null + json: null + txt: null + +testIngress: null + +testLicense: null + +testRbac: null + +testProfiles: + basicIni: null + singleFile: null + collapseArray: null + advanced: null + jsonOverrides: null + applyEveryone: null + +testDebug: null + +testLauncherTemplates: null + +testTplvalues: null + +testChronicle: + enabled: false diff --git a/other-charts/rstudio-library-test/tests/chronicle_agent_test.yaml b/other-charts/rstudio-library-test/tests/chronicle_agent_test.yaml index 1f54ad4f6..45dbec439 100644 --- a/other-charts/rstudio-library-test/tests/chronicle_agent_test.yaml +++ b/other-charts/rstudio-library-test/tests/chronicle_agent_test.yaml @@ -1,6 +1,8 @@ suite: Chronicle Agent Helpers templates: - templates/test-chronicle-agent.yaml +values: + - _base_values.yaml # Note: Chronicle agent templates use `lookup` which requires cluster access. # These tests only cover static paths where values are explicitly provided. @@ -19,14 +21,6 @@ tests: registry: ghcr.io repository: rstudio/chronicle-agent tag: "1.2.3" - testConfig: null - testIngress: null - testLicense: null - testRbac: null - testProfiles: null - testDebug: null - testLauncherTemplates: null - testTplvalues: null asserts: - isKind: of: ConfigMap @@ -46,14 +40,6 @@ tests: registry: my-registry.example.com repository: custom/agent tag: "2.0.0" - testConfig: null - testIngress: null - testLicense: null - testRbac: null - testProfiles: null - testDebug: null - testLauncherTemplates: null - testTplvalues: null asserts: - equal: path: data.image @@ -71,14 +57,6 @@ tests: registry: ghcr.io repository: rstudio/chronicle-agent tag: "1.0.0" - testConfig: null - testIngress: null - testLicense: null - testRbac: null - testProfiles: null - testDebug: null - testLauncherTemplates: null - testTplvalues: null asserts: - equal: path: data.serverAddress @@ -93,14 +71,6 @@ tests: registry: ghcr.io repository: rstudio/chronicle-agent tag: "1.0.0" - testConfig: null - testIngress: null - testLicense: null - testRbac: null - testProfiles: null - testDebug: null - testLauncherTemplates: null - testTplvalues: null asserts: - equal: path: data.serverAddress @@ -113,14 +83,6 @@ tests: set: testChronicle: enabled: false - testConfig: null - testIngress: null - testLicense: null - testRbac: null - testProfiles: null - testDebug: null - testLauncherTemplates: null - testTplvalues: null asserts: - hasDocuments: count: 0 diff --git a/other-charts/rstudio-library-test/tests/config_test.yaml b/other-charts/rstudio-library-test/tests/config_test.yaml index 2cb492af2..cb44c9229 100644 --- a/other-charts/rstudio-library-test/tests/config_test.yaml +++ b/other-charts/rstudio-library-test/tests/config_test.yaml @@ -1,6 +1,8 @@ suite: Config Formatters templates: - templates/test-config.yaml +values: + - _base_values.yaml tests: # ======================================== @@ -15,19 +17,6 @@ tests: Server: Host: localhost Port: 8080 - ini: null - dcf: null - json: null - txt: null - testIngress: null - testLicense: null - testRbac: null - testProfiles: null - testDebug: null - testLauncherTemplates: null - testTplvalues: null - testChronicle: - enabled: false asserts: - isKind: of: ConfigMap @@ -54,19 +43,6 @@ tests: Listen: - ":8080" - ":8443" - ini: null - dcf: null - json: null - txt: null - testIngress: null - testLicense: null - testRbac: null - testProfiles: null - testDebug: null - testLauncherTemplates: null - testTplvalues: null - testChronicle: - enabled: false asserts: - matchRegex: path: data["test.gcfg"] @@ -85,19 +61,6 @@ tests: Host: server1 Database: Host: db1 - ini: null - dcf: null - json: null - txt: null - testIngress: null - testLicense: null - testRbac: null - testProfiles: null - testDebug: null - testLauncherTemplates: null - testTplvalues: null - testChronicle: - enabled: false asserts: - matchRegex: path: data["config.gcfg"] @@ -115,19 +78,6 @@ tests: Settings: Enabled: true MaxConnections: 100 - ini: null - dcf: null - json: null - txt: null - testIngress: null - testLicense: null - testRbac: null - testProfiles: null - testDebug: null - testLauncherTemplates: null - testTplvalues: null - testChronicle: - enabled: false asserts: - matchRegex: path: data["test.gcfg"] @@ -136,31 +86,30 @@ tests: path: data["test.gcfg"] pattern: "MaxConnections = 100" + - it: should handle empty config section in gcfg + set: + testConfig: + gcfg: + filename: empty.gcfg + config: + EmptySection: {} + asserts: + - matchRegex: + path: data["empty.gcfg"] + pattern: "\\[EmptySection\\]" + # ======================================== # INI Format Tests # ======================================== - it: should render ini with basic section and key-value pairs set: testConfig: - gcfg: null ini: filename: test.ini config: section1: key1: value1 key2: value2 - dcf: null - json: null - txt: null - testIngress: null - testLicense: null - testRbac: null - testProfiles: null - testDebug: null - testLauncherTemplates: null - testTplvalues: null - testChronicle: - enabled: false asserts: - isKind: of: ConfigMap @@ -180,25 +129,12 @@ tests: - it: should render ini with boolean and numeric values set: testConfig: - gcfg: null ini: filename: settings.ini config: options: enabled: true count: 42 - dcf: null - json: null - txt: null - testIngress: null - testLicense: null - testRbac: null - testProfiles: null - testDebug: null - testLauncherTemplates: null - testTplvalues: null - testChronicle: - enabled: false asserts: - matchRegex: path: data["settings.ini"] @@ -213,24 +149,11 @@ tests: - it: should render dcf with basic key-value pairs set: testConfig: - gcfg: null - ini: null dcf: filename: test.dcf config: Package: mypackage Version: 1.0.0 - json: null - txt: null - testIngress: null - testLicense: null - testRbac: null - testProfiles: null - testDebug: null - testLauncherTemplates: null - testTplvalues: null - testChronicle: - enabled: false asserts: - isKind: of: ConfigMap @@ -250,24 +173,11 @@ tests: - it: should render json with simple object set: testConfig: - gcfg: null - ini: null - dcf: null json: filename: config.json config: name: test enabled: true - txt: null - testIngress: null - testLicense: null - testRbac: null - testProfiles: null - testDebug: null - testLauncherTemplates: null - testTplvalues: null - testChronicle: - enabled: false asserts: - isKind: of: ConfigMap @@ -284,25 +194,12 @@ tests: - it: should render json with nested object set: testConfig: - gcfg: null - ini: null - dcf: null json: filename: nested.json config: server: host: localhost port: 8080 - txt: null - testIngress: null - testLicense: null - testRbac: null - testProfiles: null - testDebug: null - testLauncherTemplates: null - testTplvalues: null - testChronicle: - enabled: false asserts: - matchRegex: path: data["nested.json"] @@ -314,25 +211,12 @@ tests: - it: should render json with array values set: testConfig: - gcfg: null - ini: null - dcf: null json: filename: array.json config: items: - first - second - txt: null - testIngress: null - testLicense: null - testRbac: null - testProfiles: null - testDebug: null - testLauncherTemplates: null - testTplvalues: null - testChronicle: - enabled: false asserts: - matchRegex: path: data["array.json"] @@ -344,30 +228,30 @@ tests: path: data["array.json"] pattern: '"second"' + - it: should render json with empty object + set: + testConfig: + json: + filename: empty.json + config: {} + asserts: + - isKind: + of: ConfigMap + - equal: + path: metadata.name + value: test-config-json + # ======================================== # TXT Format Tests # ======================================== - it: should render txt with default comment delimiter set: testConfig: - gcfg: null - ini: null - dcf: null - json: null txt: filename: config.txt config: key1: value1 key2: value2 - testIngress: null - testLicense: null - testRbac: null - testProfiles: null - testDebug: null - testLauncherTemplates: null - testTplvalues: null - testChronicle: - enabled: false asserts: - isKind: of: ConfigMap diff --git a/other-charts/rstudio-library-test/tests/debug_test.yaml b/other-charts/rstudio-library-test/tests/debug_test.yaml index 37b26d76f..307381ee1 100644 --- a/other-charts/rstudio-library-test/tests/debug_test.yaml +++ b/other-charts/rstudio-library-test/tests/debug_test.yaml @@ -1,6 +1,8 @@ suite: Debug Helpers templates: - templates/test-debug.yaml +values: + - _base_values.yaml tests: # ======================================== @@ -12,15 +14,6 @@ tests: mapValue: key1: value1 key2: value2 - testConfig: null - testIngress: null - testLicense: null - testRbac: null - testProfiles: null - testLauncherTemplates: null - testTplvalues: null - testChronicle: - enabled: false asserts: - isKind: of: ConfigMap @@ -38,15 +31,6 @@ tests: - item1 - item2 - item3 - testConfig: null - testIngress: null - testLicense: null - testRbac: null - testProfiles: null - testLauncherTemplates: null - testTplvalues: null - testChronicle: - enabled: false asserts: - isKind: of: ConfigMap @@ -58,15 +42,6 @@ tests: set: testDebug: stringValue: "test string value" - testConfig: null - testIngress: null - testLicense: null - testRbac: null - testProfiles: null - testLauncherTemplates: null - testTplvalues: null - testChronicle: - enabled: false asserts: - isKind: of: ConfigMap @@ -78,15 +53,6 @@ tests: set: testDebug: boolValue: true - testConfig: null - testIngress: null - testLicense: null - testRbac: null - testProfiles: null - testLauncherTemplates: null - testTplvalues: null - testChronicle: - enabled: false asserts: - isKind: of: ConfigMap @@ -105,15 +71,6 @@ tests: object: "this is a string" expected: "map" description: "configuration object" - testConfig: null - testIngress: null - testLicense: null - testRbac: null - testProfiles: null - testLauncherTemplates: null - testTplvalues: null - testChronicle: - enabled: false asserts: - failedTemplate: errorPattern: "testConfig must be a 'map'" @@ -126,15 +83,6 @@ tests: object: "not-an-array" expected: "slice" description: "list of items" - testConfig: null - testIngress: null - testLicense: null - testRbac: null - testProfiles: null - testLauncherTemplates: null - testTplvalues: null - testChronicle: - enabled: false asserts: - failedTemplate: errorPattern: "items must be a 'slice'" diff --git a/other-charts/rstudio-library-test/tests/ingress_test.yaml b/other-charts/rstudio-library-test/tests/ingress_test.yaml index 67c694c37..47c66cfe3 100644 --- a/other-charts/rstudio-library-test/tests/ingress_test.yaml +++ b/other-charts/rstudio-library-test/tests/ingress_test.yaml @@ -1,6 +1,8 @@ suite: Ingress Helpers templates: - templates/test-ingress.yaml +values: + - _base_values.yaml tests: # ======================================== @@ -13,15 +15,6 @@ tests: servicePort: 80 path: / pathType: Prefix - testConfig: null - testLicense: null - testRbac: null - testProfiles: null - testDebug: null - testLauncherTemplates: null - testTplvalues: null - testChronicle: - enabled: false asserts: - equal: path: data.apiVersion @@ -38,15 +31,6 @@ tests: servicePort: 80 path: / pathType: Prefix - testConfig: null - testLicense: null - testRbac: null - testProfiles: null - testDebug: null - testLauncherTemplates: null - testTplvalues: null - testChronicle: - enabled: false asserts: - equal: path: data.supportsIngressClassName @@ -63,15 +47,6 @@ tests: servicePort: 80 path: / pathType: Prefix - testConfig: null - testLicense: null - testRbac: null - testProfiles: null - testDebug: null - testLauncherTemplates: null - testTplvalues: null - testChronicle: - enabled: false asserts: - equal: path: data.supportsPathType @@ -88,15 +63,6 @@ tests: servicePort: 80 path: /api pathType: Prefix - testConfig: null - testLicense: null - testRbac: null - testProfiles: null - testDebug: null - testLauncherTemplates: null - testTplvalues: null - testChronicle: - enabled: false asserts: - matchRegex: path: data["path.yaml"] @@ -114,15 +80,6 @@ tests: servicePort: 80 path: /custom pathType: Exact - testConfig: null - testLicense: null - testRbac: null - testProfiles: null - testDebug: null - testLauncherTemplates: null - testTplvalues: null - testChronicle: - enabled: false asserts: - matchRegex: path: data["path.yaml"] @@ -143,15 +100,6 @@ tests: servicePort: 8080 path: / pathType: Prefix - testConfig: null - testLicense: null - testRbac: null - testProfiles: null - testDebug: null - testLauncherTemplates: null - testTplvalues: null - testChronicle: - enabled: false asserts: - matchRegex: path: data["backend.yaml"] @@ -173,15 +121,6 @@ tests: servicePort: http path: / pathType: Prefix - testConfig: null - testLicense: null - testRbac: null - testProfiles: null - testDebug: null - testLauncherTemplates: null - testTplvalues: null - testChronicle: - enabled: false asserts: - matchRegex: path: data["backend.yaml"] diff --git a/other-charts/rstudio-library-test/tests/launcher_templates_test.yaml b/other-charts/rstudio-library-test/tests/launcher_templates_test.yaml index e7eb916f3..1b0a0015a 100644 --- a/other-charts/rstudio-library-test/tests/launcher_templates_test.yaml +++ b/other-charts/rstudio-library-test/tests/launcher_templates_test.yaml @@ -1,6 +1,8 @@ suite: Launcher Templates Helpers templates: - templates/test-launcher-templates.yaml +values: + - _base_values.yaml tests: # ======================================== @@ -13,15 +15,6 @@ tests: content: key1: value1 key2: value2 - testConfig: null - testIngress: null - testLicense: null - testRbac: null - testProfiles: null - testDebug: null - testTplvalues: null - testChronicle: - enabled: false asserts: - isKind: of: ConfigMap @@ -52,15 +45,6 @@ tests: child1: value1 child2: grandchild: deep-value - testConfig: null - testIngress: null - testLicense: null - testRbac: null - testProfiles: null - testDebug: null - testTplvalues: null - testChronicle: - enabled: false asserts: - matchRegex: path: data["template.tpl"] @@ -81,15 +65,6 @@ tests: content: key1: value1 key2: value2 - testConfig: null - testIngress: null - testLicense: null - testRbac: null - testProfiles: null - testDebug: null - testTplvalues: null - testChronicle: - enabled: false asserts: - isKind: of: ConfigMap @@ -113,15 +88,6 @@ tests: content: key1: value1 key2: value2 - testConfig: null - testIngress: null - testLicense: null - testRbac: null - testProfiles: null - testDebug: null - testTplvalues: null - testChronicle: - enabled: false asserts: - isKind: of: ConfigMap @@ -144,15 +110,6 @@ tests: templateName: no-dash-template content: key: value - testConfig: null - testIngress: null - testLicense: null - testRbac: null - testProfiles: null - testDebug: null - testTplvalues: null - testChronicle: - enabled: false asserts: - isKind: of: ConfigMap @@ -171,15 +128,6 @@ tests: templateName: my.namespaced.template content: data: value - testConfig: null - testIngress: null - testLicense: null - testRbac: null - testProfiles: null - testDebug: null - testTplvalues: null - testChronicle: - enabled: false asserts: - matchRegex: path: data["template.tpl"] diff --git a/other-charts/rstudio-library-test/tests/license_test.yaml b/other-charts/rstudio-library-test/tests/license_test.yaml index 5a68862c6..2567a02ac 100644 --- a/other-charts/rstudio-library-test/tests/license_test.yaml +++ b/other-charts/rstudio-library-test/tests/license_test.yaml @@ -1,6 +1,8 @@ suite: License Helpers templates: - templates/test-license.yaml +values: + - _base_values.yaml # Note: License templates output multiple document types (Secrets and ConfigMaps) # which makes document-index based assertions unreliable. These tests verify @@ -16,15 +18,6 @@ tests: licenseKey: "SECRET-KEY-12345" licenseServer: "" licenseFile: "" - testConfig: null - testIngress: null - testRbac: null - testProfiles: null - testDebug: null - testLauncherTemplates: null - testTplvalues: null - testChronicle: - enabled: false asserts: - hasDocuments: count: 4 @@ -36,15 +29,6 @@ tests: licenseServer: "" licenseFile: | LICENSE CONTENT - testConfig: null - testIngress: null - testRbac: null - testProfiles: null - testDebug: null - testLauncherTemplates: null - testTplvalues: null - testChronicle: - enabled: false asserts: - hasDocuments: count: 4 @@ -56,15 +40,22 @@ tests: licenseServer: "server.example.com" licenseFile: | LICENSE - testConfig: null - testIngress: null - testRbac: null - testProfiles: null - testDebug: null - testLauncherTemplates: null - testTplvalues: null - testChronicle: - enabled: false asserts: - hasDocuments: count: 5 + + - it: should include release name in license resources + set: + testLicense: + licenseKey: "TEST-KEY" + licenseServer: "" + licenseFile: "" + release: + name: my-app + asserts: + - hasDocuments: + count: 4 + - matchRegex: + path: metadata.name + pattern: "test-license" + documentIndex: 0 diff --git a/other-charts/rstudio-library-test/tests/profiles_test.yaml b/other-charts/rstudio-library-test/tests/profiles_test.yaml index f22f56506..0a3d51f2d 100644 --- a/other-charts/rstudio-library-test/tests/profiles_test.yaml +++ b/other-charts/rstudio-library-test/tests/profiles_test.yaml @@ -1,6 +1,8 @@ suite: Profiles Helpers templates: - templates/test-profiles.yaml +values: + - _base_values.yaml tests: # ======================================== @@ -14,20 +16,6 @@ tests: "*": default-cpus: 1 default-mem-mb: 512 - singleFile: null - collapseArray: null - advanced: null - jsonOverrides: null - applyEveryone: null - testConfig: null - testIngress: null - testLicense: null - testRbac: null - testDebug: null - testLauncherTemplates: null - testTplvalues: null - testChronicle: - enabled: false asserts: - isKind: of: ConfigMap @@ -47,25 +35,11 @@ tests: - it: should render single INI file content set: testProfiles: - basicIni: null singleFile: "*": setting: value1 testuser: setting: value2 - collapseArray: null - advanced: null - jsonOverrides: null - applyEveryone: null - testConfig: null - testIngress: null - testLicense: null - testRbac: null - testDebug: null - testLauncherTemplates: null - testTplvalues: null - testChronicle: - enabled: false asserts: - isKind: of: ConfigMap @@ -85,25 +59,11 @@ tests: - it: should collapse simple array with commas set: testProfiles: - basicIni: null - singleFile: null collapseArray: simple: - one - two - three - advanced: null - jsonOverrides: null - applyEveryone: null - testConfig: null - testIngress: null - testLicense: null - testRbac: null - testDebug: null - testLauncherTemplates: null - testTplvalues: null - testChronicle: - enabled: false asserts: - isKind: of: ConfigMap @@ -117,8 +77,6 @@ tests: - it: should collapse target/file array with quotes and colons set: testProfiles: - basicIni: null - singleFile: null collapseArray: simple: - dummy @@ -127,18 +85,6 @@ tests: file: /etc/pods.json - target: services file: /etc/svc.json - advanced: null - jsonOverrides: null - applyEveryone: null - testConfig: null - testIngress: null - testLicense: null - testRbac: null - testDebug: null - testLauncherTemplates: null - testTplvalues: null - testChronicle: - enabled: false asserts: - matchRegex: path: data.targetFile @@ -153,9 +99,6 @@ tests: - it: should render advanced profiles with everyone section set: testProfiles: - basicIni: null - singleFile: null - collapseArray: null advanced: data: launcher.kubernetes.profiles.conf: @@ -163,17 +106,6 @@ tests: default-cpus: 2 jobJsonDefaults: [] filePath: /etc/rstudio/ - jsonOverrides: null - applyEveryone: null - testConfig: null - testIngress: null - testLicense: null - testRbac: null - testDebug: null - testLauncherTemplates: null - testTplvalues: null - testChronicle: - enabled: false asserts: - isKind: of: ConfigMap @@ -193,10 +125,6 @@ tests: - it: should generate JSON files from overrides config set: testProfiles: - basicIni: null - singleFile: null - collapseArray: null - advanced: null jsonOverrides: data: "*": @@ -208,16 +136,6 @@ tests: limits: cpu: "2" default: [] - applyEveryone: null - testConfig: null - testIngress: null - testLicense: null - testRbac: null - testDebug: null - testLauncherTemplates: null - testTplvalues: null - testChronicle: - enabled: false asserts: - isKind: of: ConfigMap @@ -231,10 +149,6 @@ tests: - it: should fail when override missing required keys set: testProfiles: - basicIni: null - singleFile: null - collapseArray: null - advanced: null jsonOverrides: data: "*": @@ -243,16 +157,6 @@ tests: json: data: value default: [] - applyEveryone: null - testConfig: null - testIngress: null - testLicense: null - testRbac: null - testDebug: null - testLauncherTemplates: null - testTplvalues: null - testChronicle: - enabled: false asserts: - failedTemplate: errorPattern: "must have keys 'name', 'json', and 'target'" @@ -263,11 +167,6 @@ tests: - it: should apply everyone config to all sections set: testProfiles: - basicIni: null - singleFile: null - collapseArray: null - advanced: null - jsonOverrides: null applyEveryone: data: "*": @@ -276,15 +175,6 @@ tests: user-setting: user1-value default: [] filePath: "" - testConfig: null - testIngress: null - testLicense: null - testRbac: null - testDebug: null - testLauncherTemplates: null - testTplvalues: null - testChronicle: - enabled: false asserts: - isKind: of: ConfigMap diff --git a/other-charts/rstudio-library-test/tests/rbac_test.yaml b/other-charts/rstudio-library-test/tests/rbac_test.yaml index e050383ef..b0c6b1f80 100644 --- a/other-charts/rstudio-library-test/tests/rbac_test.yaml +++ b/other-charts/rstudio-library-test/tests/rbac_test.yaml @@ -1,6 +1,8 @@ suite: RBAC Helper templates: - templates/test-rbac.yaml +values: + - _base_values.yaml tests: # ======================================== @@ -17,15 +19,6 @@ tests: removeNamespaceReferences: false labels: {} annotations: {} - testConfig: null - testIngress: null - testLicense: null - testProfiles: null - testDebug: null - testLauncherTemplates: null - testTplvalues: null - testChronicle: - enabled: false asserts: - isKind: of: ServiceAccount @@ -50,15 +43,6 @@ tests: removeNamespaceReferences: false labels: {} annotations: {} - testConfig: null - testIngress: null - testLicense: null - testProfiles: null - testDebug: null - testLauncherTemplates: null - testTplvalues: null - testChronicle: - enabled: false asserts: - isKind: of: Role @@ -81,15 +65,6 @@ tests: removeNamespaceReferences: false labels: {} annotations: {} - testConfig: null - testIngress: null - testLicense: null - testProfiles: null - testDebug: null - testLauncherTemplates: null - testTplvalues: null - testChronicle: - enabled: false asserts: - isKind: of: Role @@ -110,15 +85,6 @@ tests: removeNamespaceReferences: false labels: {} annotations: {} - testConfig: null - testIngress: null - testLicense: null - testProfiles: null - testDebug: null - testLauncherTemplates: null - testTplvalues: null - testChronicle: - enabled: false asserts: - isKind: of: RoleBinding @@ -146,15 +112,6 @@ tests: removeNamespaceReferences: false labels: {} annotations: {} - testConfig: null - testIngress: null - testLicense: null - testProfiles: null - testDebug: null - testLauncherTemplates: null - testTplvalues: null - testChronicle: - enabled: false asserts: - isKind: of: ClusterRole @@ -175,15 +132,6 @@ tests: removeNamespaceReferences: false labels: {} annotations: {} - testConfig: null - testIngress: null - testLicense: null - testProfiles: null - testDebug: null - testLauncherTemplates: null - testTplvalues: null - testChronicle: - enabled: false asserts: - isKind: of: ClusterRoleBinding @@ -207,15 +155,6 @@ tests: removeNamespaceReferences: true labels: {} annotations: {} - testConfig: null - testIngress: null - testLicense: null - testProfiles: null - testDebug: null - testLauncherTemplates: null - testTplvalues: null - testChronicle: - enabled: false asserts: - notExists: path: metadata.namespace @@ -239,15 +178,6 @@ tests: labels: {} annotations: eks.amazonaws.com/role-arn: "arn:aws:iam::123456789:role/my-role" - testConfig: null - testIngress: null - testLicense: null - testProfiles: null - testDebug: null - testLauncherTemplates: null - testTplvalues: null - testChronicle: - enabled: false asserts: - equal: path: metadata.annotations["eks.amazonaws.com/role-arn"] @@ -266,15 +196,6 @@ tests: labels: app.kubernetes.io/component: launcher annotations: {} - testConfig: null - testIngress: null - testLicense: null - testProfiles: null - testDebug: null - testLauncherTemplates: null - testTplvalues: null - testChronicle: - enabled: false asserts: - equal: path: metadata.labels["app.kubernetes.io/component"] @@ -295,15 +216,6 @@ tests: removeNamespaceReferences: false labels: {} annotations: {} - testConfig: null - testIngress: null - testLicense: null - testProfiles: null - testDebug: null - testLauncherTemplates: null - testTplvalues: null - testChronicle: - enabled: false asserts: - failedTemplate: errorPattern: "serviceAccountCreate must be a 'bool'" diff --git a/other-charts/rstudio-library-test/tests/tplvalues_test.yaml b/other-charts/rstudio-library-test/tests/tplvalues_test.yaml index 08e50aa57..de18db0ab 100644 --- a/other-charts/rstudio-library-test/tests/tplvalues_test.yaml +++ b/other-charts/rstudio-library-test/tests/tplvalues_test.yaml @@ -1,6 +1,8 @@ suite: tplvalues Helpers templates: - templates/test-tplvalues.yaml +values: + - _base_values.yaml tests: # ======================================== @@ -11,15 +13,6 @@ tests: testTplvalues: staticValue: "static" templateValue: "{{ .Release.Name }}-suffix" - testConfig: null - testIngress: null - testLicense: null - testRbac: null - testProfiles: null - testDebug: null - testLauncherTemplates: null - testChronicle: - enabled: false release: name: my-release asserts: @@ -37,15 +30,6 @@ tests: testTplvalues: staticValue: "plain-static-value" templateValue: "no-template-here" - testConfig: null - testIngress: null - testLicense: null - testRbac: null - testProfiles: null - testDebug: null - testLauncherTemplates: null - testChronicle: - enabled: false asserts: - equal: path: data.staticString @@ -62,15 +46,6 @@ tests: testTplvalues: staticValue: "x" templateValue: "prefix-{{ .Release.Name }}" - testConfig: null - testIngress: null - testLicense: null - testRbac: null - testProfiles: null - testDebug: null - testLauncherTemplates: null - testChronicle: - enabled: false release: name: test-release asserts: @@ -83,15 +58,6 @@ tests: testTplvalues: staticValue: "x" templateValue: "ns-{{ .Release.Namespace }}" - testConfig: null - testIngress: null - testLicense: null - testRbac: null - testProfiles: null - testDebug: null - testLauncherTemplates: null - testChronicle: - enabled: false release: name: test namespace: production @@ -111,15 +77,6 @@ tests: objectValue: name: "{{ .Release.Name }}" namespace: "{{ .Release.Namespace }}" - testConfig: null - testIngress: null - testLicense: null - testRbac: null - testProfiles: null - testDebug: null - testLauncherTemplates: null - testChronicle: - enabled: false release: name: obj-release namespace: obj-namespace @@ -139,15 +96,6 @@ tests: testTplvalues: staticValue: "x" templateValue: "" - testConfig: null - testIngress: null - testLicense: null - testRbac: null - testProfiles: null - testDebug: null - testLauncherTemplates: null - testChronicle: - enabled: false asserts: - equal: path: data.renderedString @@ -158,15 +106,6 @@ tests: testTplvalues: staticValue: "x" templateValue: "{{ .Release.Name }}-{{ .Release.Namespace }}-app" - testConfig: null - testIngress: null - testLicense: null - testRbac: null - testProfiles: null - testDebug: null - testLauncherTemplates: null - testChronicle: - enabled: false release: name: multi namespace: test diff --git a/other-charts/rstudio-library-test/values.yaml b/other-charts/rstudio-library-test/values.yaml index c2a8d4307..54a96552f 100644 --- a/other-charts/rstudio-library-test/values.yaml +++ b/other-charts/rstudio-library-test/values.yaml @@ -1,7 +1,12 @@ # Test values for rstudio-library-test harness # These values are used by test templates to exercise library functions +# +# Each testXxx block corresponds to a test template in templates/test-xxx.yaml +# Tests use _base_values.yaml to null out all categories, then override +# only the specific category being tested # Config formatter test values +# Exercises: config.gcfg, config.ini, config.dcf, config.json, config.txt testConfig: gcfg: filename: test.gcfg @@ -41,6 +46,8 @@ testConfig: key2: value2 # Ingress helper test values +# Exercises: ingress.apiVersion, ingress.path, ingress.backend, +# ingress.supportsIngressClassName, ingress.supportsPathType testIngress: serviceName: test-service servicePort: 8080 @@ -48,6 +55,7 @@ testIngress: pathType: Prefix # License helper test values +# Exercises: license-env, license-mount, license-volume, license-secret testLicense: licenseKey: "test-license-key" licenseServer: "license.example.com" @@ -55,6 +63,7 @@ testLicense: LICENSE CONTENT HERE # RBAC test values +# Exercises: rbac (ServiceAccount, Role, RoleBinding, ClusterRole) testRbac: serviceAccountCreate: true serviceAccountName: test-sa @@ -66,6 +75,9 @@ testRbac: annotations: {} # Profiles test values +# Exercises: profiles.ini, profiles.ini.singleFile, profiles.ini.collapse-array, +# profiles.ini.advanced, profiles.json-from-overrides-config, +# profiles.apply-everyone-and-default-to-others testProfiles: # Basic INI test basicIni: @@ -102,8 +114,13 @@ testProfiles: default-cpus: 4 jobJsonDefaults: [] filePath: /etc/rstudio/ + # JSON overrides test (optional - tests set this explicitly) + # jsonOverrides: ... + # Apply everyone test (optional - tests set this explicitly) + # applyEveryone: ... # Debug helper test values +# Exercises: debug.type-check testDebug: mapValue: key: value @@ -114,6 +131,7 @@ testDebug: boolValue: true # Launcher template test values +# Exercises: templates.skeleton, templates.dataOutput, templates.dataOutputPretty testLauncherTemplates: templateName: test-template content: @@ -123,6 +141,7 @@ testLauncherTemplates: subkey: subvalue # Tplvalues test values +# Exercises: tplvalues.render testTplvalues: staticValue: "static" templateValue: "{{ .Release.Name }}-suffix" @@ -131,6 +150,7 @@ testTplvalues: namespace: "{{ .Release.Namespace }}" # Chronicle agent test values +# Exercises: chronicle-agent.image, chronicle-agent.serverAddress testChronicle: enabled: true serverAddress: "http://chronicle-server.default:8080" From 7e2391b654888b4b5b9207340263b8ed62256458 Mon Sep 17 00:00:00 2001 From: "Benjamin R. J. Schwedler" Date: Wed, 28 Jan 2026 13:48:19 -0600 Subject: [PATCH 07/12] Update README to document _base_values.yaml test isolation pattern Co-Authored-By: Claude Opus 4.5 --- other-charts/rstudio-library-test/README.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/other-charts/rstudio-library-test/README.md b/other-charts/rstudio-library-test/README.md index f9ed85947..1ba44596e 100644 --- a/other-charts/rstudio-library-test/README.md +++ b/other-charts/rstudio-library-test/README.md @@ -40,18 +40,20 @@ The test suite covers all `rstudio-library` template functions: ## Test Structure Each test file follows this pattern: -1. Includes only the specific test template being tested -2. Sets only the relevant test values, nullifying others to prevent interference +1. Includes the base values file (`tests/_base_values.yaml`) which nullifies all test categories +2. Overrides only the specific test values needed for that test 3. Uses assertions to verify expected output +The `_base_values.yaml` file provides test isolation by setting all test categories to `null`, preventing unrelated templates from rendering during tests. + ## Adding New Tests When adding tests for new `rstudio-library` templates: 1. Create a test template in `templates/test-*.yaml` that invokes the library template -2. Add test values to `values.yaml` -3. Create a test file in `tests/*_test.yaml` with assertions -4. Ensure test isolation by setting unrelated test values to `null` +2. Add test values to `values.yaml` with documentation comments +3. Update `tests/_base_values.yaml` to include the new test category (set to `null`) +4. Create a test file in `tests/*_test.yaml` that includes `_base_values.yaml` and overrides only needed values ## Manual Verification From 9280a04395d66ec9123caf8f324cb8fbfe66e5ad Mon Sep 17 00:00:00 2001 From: "Benjamin R. J. Schwedler" Date: Wed, 28 Jan 2026 14:06:58 -0600 Subject: [PATCH 08/12] Exclude rstudio-library-test from chart install tests The test harness chart is only for unit testing library templates and should not be installed in a cluster during CI install tests. Co-Authored-By: Claude Opus 4.5 --- .github/workflows/chart-test.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/chart-test.yaml b/.github/workflows/chart-test.yaml index 89e62a82f..4e9dffab9 100644 --- a/.github/workflows/chart-test.yaml +++ b/.github/workflows/chart-test.yaml @@ -196,13 +196,13 @@ jobs: - name: Run chart-testing (install changed) id: ct-install if: ${{ github.ref != 'refs/heads/main' && steps.list-changed.outputs.changed == 'true' }} - run: ct install --target-branch main --chart-dirs charts --chart-dirs other-charts --excluded-charts rstudio-library --namespace posit-test + run: ct install --target-branch main --chart-dirs charts --chart-dirs other-charts --excluded-charts rstudio-library,rstudio-library-test --namespace posit-test continue-on-error: true # no allow-failure until https://github.com/actions/toolkit/issues/399 - name: Run chart-testing (install all) id: ct-install-all - run: ct install --target-branch main --all --chart-dirs charts --chart-dirs other-charts --excluded-charts rstudio-library --namespace posit-test + run: ct install --target-branch main --all --chart-dirs charts --chart-dirs other-charts --excluded-charts rstudio-library,rstudio-library-test --namespace posit-test continue-on-error: true - name: Notify Slack of chart install failure if on main From cef1de2b3490a7285168e7ed22db778978bd0423 Mon Sep 17 00:00:00 2001 From: "Benjamin R. J. Schwedler" Date: Wed, 28 Jan 2026 14:16:41 -0600 Subject: [PATCH 09/12] Improve test harness based on code review feedback - Fix misleading header comment in _helpers.tpl (was claiming to invoke library templates when it just contains standard Helm chart helpers) - Enhance license tests with better assertions that verify actual content: - Check Secret kind and stringData for license key - Check ConfigMap content for environment variable name - Verify release name is included in Secret name - Document correct document order in license tests Co-Authored-By: Claude Opus 4.5 --- .../templates/_helpers.tpl | 7 ++- .../tests/license_test.yaml | 54 +++++++++++++++---- 2 files changed, 46 insertions(+), 15 deletions(-) diff --git a/other-charts/rstudio-library-test/templates/_helpers.tpl b/other-charts/rstudio-library-test/templates/_helpers.tpl index 8e049daf5..4db152ea8 100644 --- a/other-charts/rstudio-library-test/templates/_helpers.tpl +++ b/other-charts/rstudio-library-test/templates/_helpers.tpl @@ -1,17 +1,16 @@ {{/* -Test harness helpers for rstudio-library templates. -These helpers invoke library templates and wrap output for testing. +Standard Helm chart helpers for the test harness. */}} {{/* -Chart name +Expand the name of the chart. */}} {{- define "rstudio-library-test.name" -}} {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} {{- end }} {{/* -Fully qualified app name +Create a default fully qualified app name. */}} {{- define "rstudio-library-test.fullname" -}} {{- if .Values.fullnameOverride }} diff --git a/other-charts/rstudio-library-test/tests/license_test.yaml b/other-charts/rstudio-library-test/tests/license_test.yaml index 2567a02ac..f58dc6edf 100644 --- a/other-charts/rstudio-library-test/tests/license_test.yaml +++ b/other-charts/rstudio-library-test/tests/license_test.yaml @@ -4,15 +4,14 @@ templates: values: - _base_values.yaml -# Note: License templates output multiple document types (Secrets and ConfigMaps) -# which makes document-index based assertions unreliable. These tests verify -# basic rendering - for detailed verification, use `helm template` manually. +# Note: License templates output multiple document types. +# Document order: 0=ConfigMap (env), 1=ConfigMap (mount), 2=ConfigMap (volume), 3+=Secrets tests: # ======================================== - # License Template Rendering Tests + # License Key Configuration Tests # ======================================== - - it: should render license template with key configured + - it: should create license secret with key configured set: testLicense: licenseKey: "SECRET-KEY-12345" @@ -21,7 +20,36 @@ tests: asserts: - hasDocuments: count: 4 + - isKind: + of: Secret + documentIndex: 3 + - equal: + path: stringData["test-product"] + value: "SECRET-KEY-12345" + documentIndex: 3 + - it: should create env ConfigMap with license environment variable + set: + testLicense: + licenseKey: "SECRET-KEY-12345" + licenseServer: "" + licenseFile: "" + asserts: + - isKind: + of: ConfigMap + documentIndex: 0 + - equal: + path: metadata.name + value: test-license-env + documentIndex: 0 + - matchRegex: + path: data["env.yaml"] + pattern: "TEST_LICENSE" + documentIndex: 0 + + # ======================================== + # License File Configuration Tests + # ======================================== - it: should render license template with file configured set: testLicense: @@ -33,7 +61,10 @@ tests: - hasDocuments: count: 4 - - it: should render license template with all options configured + # ======================================== + # Full Configuration Tests + # ======================================== + - it: should render all documents with all options configured set: testLicense: licenseKey: "KEY" @@ -44,7 +75,10 @@ tests: - hasDocuments: count: 5 - - it: should include release name in license resources + # ======================================== + # Release Context Tests + # ======================================== + - it: should include release name in secret name set: testLicense: licenseKey: "TEST-KEY" @@ -53,9 +87,7 @@ tests: release: name: my-app asserts: - - hasDocuments: - count: 4 - matchRegex: path: metadata.name - pattern: "test-license" - documentIndex: 0 + pattern: "my-app-license" + documentIndex: 3 From 3efebf9ffd3d505ef5b59058bdf9c7eb1b3e02d3 Mon Sep 17 00:00:00 2001 From: "Benjamin R. J. Schwedler" Date: Wed, 28 Jan 2026 14:31:11 -0600 Subject: [PATCH 10/12] Fix critical issues from code review Security: - Use stdin instead of temp files for secrets in CI to avoid disk exposure - Replace printf with piped kubectl commands Robustness: - Fix shell script patterns: use glob instead of ls parsing - Quote all variable expansions in shell scripts - Use subshells for cd instead of pushd/popd - Add type checks (kindIs "map") to test-profiles.yaml templates - Add hasKey check before accessing nested targetFile property Test coverage: - Add 4 type safety tests for profiles (wrong type handling) - Add 2 empty value tests for config formatters All 73 tests pass. Co-Authored-By: Claude Opus 4.5 --- .github/workflows/chart-test.yaml | 28 +++++++-------- .../templates/test-profiles.yaml | 10 +++--- .../tests/config_test.yaml | 31 ++++++++++++++++ .../tests/profiles_test.yaml | 35 +++++++++++++++++++ 4 files changed, 84 insertions(+), 20 deletions(-) diff --git a/.github/workflows/chart-test.yaml b/.github/workflows/chart-test.yaml index 4e9dffab9..f04814f49 100644 --- a/.github/workflows/chart-test.yaml +++ b/.github/workflows/chart-test.yaml @@ -92,15 +92,18 @@ jobs: - name: Run chart unit tests id: unittest run: | - for dir in $(ls -d charts/*/); do - pushd $dir; helm dependencies update; popd - helm unittest $dir + # Process charts directory + for dir in charts/*/; do + [ -d "$dir" ] || continue + (cd "$dir" && helm dependencies update) + helm unittest "$dir" done # Run tests for charts in other-charts/ directory - for dir in $(ls -d other-charts/*/); do + for dir in other-charts/*/; do + [ -d "$dir" ] || continue if [ -d "$dir/tests" ]; then - pushd $dir; helm dependencies update; popd - helm unittest $dir + (cd "$dir" && helm dependencies update) + helm unittest "$dir" fi done continue-on-error: true @@ -172,15 +175,10 @@ jobs: - name: Create License File Secrets run: | - echo "${{ secrets.PWB_LICENSE_FILE }}" > pwb.lic - kubectl create secret generic pwb-license --from-file=pwb.lic --namespace posit-test - rm pwb.lic - echo "${{ secrets.PCT_LICENSE_FILE }}" > pct.lic - kubectl create secret generic pct-license --from-file=pct.lic --namespace posit-test - rm pct.lic - echo "${{ secrets.PPM_LICENSE_FILE }}" > ppm.lic - kubectl create secret generic ppm-license --from-file=ppm.lic --namespace posit-test - rm ppm.lic + # Use stdin to avoid writing secrets to disk + printf '%s' "${{ secrets.PWB_LICENSE_FILE }}" | kubectl create secret generic pwb-license --from-file=pwb.lic=/dev/stdin --namespace posit-test + printf '%s' "${{ secrets.PCT_LICENSE_FILE }}" | kubectl create secret generic pct-license --from-file=pct.lic=/dev/stdin --namespace posit-test + printf '%s' "${{ secrets.PPM_LICENSE_FILE }}" | kubectl create secret generic ppm-license --from-file=ppm.lic=/dev/stdin --namespace posit-test - name: Create Workbench Secrets to test existingSecrets run: | diff --git a/other-charts/rstudio-library-test/templates/test-profiles.yaml b/other-charts/rstudio-library-test/templates/test-profiles.yaml index 9af19755a..a5df71416 100644 --- a/other-charts/rstudio-library-test/templates/test-profiles.yaml +++ b/other-charts/rstudio-library-test/templates/test-profiles.yaml @@ -33,7 +33,7 @@ data: {{- end }} {{- /* Test profiles.ini.collapse-array */}} -{{- if .Values.testProfiles.collapseArray }} +{{- if and .Values.testProfiles.collapseArray (kindIs "map" .Values.testProfiles.collapseArray) }} --- apiVersion: v1 kind: ConfigMap @@ -43,13 +43,13 @@ metadata: {{- include "rstudio-library-test.labels" . | nindent 4 }} data: simple: {{ include "rstudio-library.profiles.ini.collapse-array" .Values.testProfiles.collapseArray.simple | quote }} - {{- if .Values.testProfiles.collapseArray.targetFile }} + {{- if and (hasKey .Values.testProfiles.collapseArray "targetFile") .Values.testProfiles.collapseArray.targetFile }} targetFile: {{ include "rstudio-library.profiles.ini.collapse-array" .Values.testProfiles.collapseArray.targetFile | quote }} {{- end }} {{- end }} {{- /* Test profiles.ini.advanced */}} -{{- if .Values.testProfiles.advanced }} +{{- if and .Values.testProfiles.advanced (kindIs "map" .Values.testProfiles.advanced) }} --- apiVersion: v1 kind: ConfigMap @@ -66,7 +66,7 @@ data: {{- end }} {{- /* Test profiles.json-from-overrides-config */}} -{{- if .Values.testProfiles.jsonOverrides }} +{{- if and .Values.testProfiles.jsonOverrides (kindIs "map" .Values.testProfiles.jsonOverrides) }} --- apiVersion: v1 kind: ConfigMap @@ -82,7 +82,7 @@ data: {{- end }} {{- /* Test profiles.apply-everyone-and-default-to-others */}} -{{- if .Values.testProfiles.applyEveryone }} +{{- if and .Values.testProfiles.applyEveryone (kindIs "map" .Values.testProfiles.applyEveryone) }} --- apiVersion: v1 kind: ConfigMap diff --git a/other-charts/rstudio-library-test/tests/config_test.yaml b/other-charts/rstudio-library-test/tests/config_test.yaml index cb44c9229..ecd27bbf4 100644 --- a/other-charts/rstudio-library-test/tests/config_test.yaml +++ b/other-charts/rstudio-library-test/tests/config_test.yaml @@ -264,3 +264,34 @@ tests: - matchRegex: path: data["config.txt"] pattern: "value1" + + # ======================================== + # Empty/Null Value Tests + # ======================================== + - it: should render gcfg with empty config section + set: + testConfig: + gcfg: + filename: empty.gcfg + config: + EmptySection: {} + asserts: + - isKind: + of: ConfigMap + - matchRegex: + path: data["empty.gcfg"] + pattern: "\\[EmptySection\\]" + + - it: should render ini with empty section + set: + testConfig: + ini: + filename: empty.ini + config: + empty_section: {} + asserts: + - isKind: + of: ConfigMap + - matchRegex: + path: data["empty.ini"] + pattern: "\\[empty_section\\]" diff --git a/other-charts/rstudio-library-test/tests/profiles_test.yaml b/other-charts/rstudio-library-test/tests/profiles_test.yaml index 0a3d51f2d..adcc58650 100644 --- a/other-charts/rstudio-library-test/tests/profiles_test.yaml +++ b/other-charts/rstudio-library-test/tests/profiles_test.yaml @@ -184,3 +184,38 @@ tests: - matchRegex: path: data["profiles.conf"] pattern: "shared-setting=everyone-value" + + # ======================================== + # Type Safety Tests + # ======================================== + - it: should not render collapseArray when value is a string instead of map + set: + testProfiles: + collapseArray: "not-a-map" + asserts: + - hasDocuments: + count: 0 + + - it: should not render advanced when value is a string instead of map + set: + testProfiles: + advanced: "not-a-map" + asserts: + - hasDocuments: + count: 0 + + - it: should not render jsonOverrides when value is a string instead of map + set: + testProfiles: + jsonOverrides: "not-a-map" + asserts: + - hasDocuments: + count: 0 + + - it: should not render applyEveryone when value is a string instead of map + set: + testProfiles: + applyEveryone: "not-a-map" + asserts: + - hasDocuments: + count: 0 From 50bc0729a2a22e0bf1bb70c184f02a2d9d50ee38 Mon Sep 17 00:00:00 2001 From: "Benjamin R. J. Schwedler" Date: Wed, 28 Jan 2026 14:34:36 -0600 Subject: [PATCH 11/12] Fix remaining security and type safety issues from re-review Security: - Move secrets from direct interpolation to env vars to prevent exposure in command line args and process listings Type Safety: - Add kindIs "map" checks to basicIni and singleFile sections - Add type safety tests for basicIni and singleFile All 75 tests pass. Co-Authored-By: Claude Opus 4.5 --- .github/workflows/chart-test.yaml | 12 ++++++++---- .../templates/test-profiles.yaml | 4 ++-- .../tests/profiles_test.yaml | 16 ++++++++++++++++ 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/.github/workflows/chart-test.yaml b/.github/workflows/chart-test.yaml index f04814f49..4fc4bdc44 100644 --- a/.github/workflows/chart-test.yaml +++ b/.github/workflows/chart-test.yaml @@ -174,11 +174,15 @@ jobs: run: kubectl create namespace posit-test - name: Create License File Secrets + env: + PWB_LICENSE: ${{ secrets.PWB_LICENSE_FILE }} + PCT_LICENSE: ${{ secrets.PCT_LICENSE_FILE }} + PPM_LICENSE: ${{ secrets.PPM_LICENSE_FILE }} run: | - # Use stdin to avoid writing secrets to disk - printf '%s' "${{ secrets.PWB_LICENSE_FILE }}" | kubectl create secret generic pwb-license --from-file=pwb.lic=/dev/stdin --namespace posit-test - printf '%s' "${{ secrets.PCT_LICENSE_FILE }}" | kubectl create secret generic pct-license --from-file=pct.lic=/dev/stdin --namespace posit-test - printf '%s' "${{ secrets.PPM_LICENSE_FILE }}" | kubectl create secret generic ppm-license --from-file=ppm.lic=/dev/stdin --namespace posit-test + # Use env vars and stdin to avoid secrets in temp files and command line args + printf '%s' "$PWB_LICENSE" | kubectl create secret generic pwb-license --from-file=pwb.lic=/dev/stdin --namespace posit-test + printf '%s' "$PCT_LICENSE" | kubectl create secret generic pct-license --from-file=pct.lic=/dev/stdin --namespace posit-test + printf '%s' "$PPM_LICENSE" | kubectl create secret generic ppm-license --from-file=ppm.lic=/dev/stdin --namespace posit-test - name: Create Workbench Secrets to test existingSecrets run: | diff --git a/other-charts/rstudio-library-test/templates/test-profiles.yaml b/other-charts/rstudio-library-test/templates/test-profiles.yaml index a5df71416..3356482bc 100644 --- a/other-charts/rstudio-library-test/templates/test-profiles.yaml +++ b/other-charts/rstudio-library-test/templates/test-profiles.yaml @@ -6,7 +6,7 @@ Outputs profile configurations to ConfigMaps for testing. {{- if .Values.testProfiles }} {{- /* Test basic profiles.ini */}} -{{- if .Values.testProfiles.basicIni }} +{{- if and .Values.testProfiles.basicIni (kindIs "map" .Values.testProfiles.basicIni) }} --- apiVersion: v1 kind: ConfigMap @@ -19,7 +19,7 @@ data: {{- end }} {{- /* Test profiles.ini.singleFile */}} -{{- if .Values.testProfiles.singleFile }} +{{- if and .Values.testProfiles.singleFile (kindIs "map" .Values.testProfiles.singleFile) }} --- apiVersion: v1 kind: ConfigMap diff --git a/other-charts/rstudio-library-test/tests/profiles_test.yaml b/other-charts/rstudio-library-test/tests/profiles_test.yaml index adcc58650..1149624d5 100644 --- a/other-charts/rstudio-library-test/tests/profiles_test.yaml +++ b/other-charts/rstudio-library-test/tests/profiles_test.yaml @@ -188,6 +188,22 @@ tests: # ======================================== # Type Safety Tests # ======================================== + - it: should not render basicIni when value is a string instead of map + set: + testProfiles: + basicIni: "not-a-map" + asserts: + - hasDocuments: + count: 0 + + - it: should not render singleFile when value is a string instead of map + set: + testProfiles: + singleFile: "not-a-map" + asserts: + - hasDocuments: + count: 0 + - it: should not render collapseArray when value is a string instead of map set: testProfiles: From ebc81ccc3106a72c5dc581bd0b57f25fe21a2f36 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Wed, 28 Jan 2026 21:02:59 +0000 Subject: [PATCH 12/12] Update helm-docs and README.md --- other-charts/rstudio-library-test/README.md | 152 +++++++++++--------- 1 file changed, 88 insertions(+), 64 deletions(-) diff --git a/other-charts/rstudio-library-test/README.md b/other-charts/rstudio-library-test/README.md index 1ba44596e..a5d68d8fc 100644 --- a/other-charts/rstudio-library-test/README.md +++ b/other-charts/rstudio-library-test/README.md @@ -1,67 +1,91 @@ -# rstudio-library-test - -Test harness chart for unit testing the `rstudio-library` Helm library chart. - -## Overview - -Helm library charts cannot be installed or tested directly because they only define template functions. This test harness chart depends on `rstudio-library` and creates test templates that exercise all library functions, enabling comprehensive unit testing with [helm-unittest](https://github.com/helm-unittest/helm-unittest). - -## Prerequisites - -- Helm 3.x -- helm-unittest plugin: `helm plugin install https://github.com/helm-unittest/helm-unittest.git` - -## Running Tests - -```bash -# Update dependencies first -helm dependency update other-charts/rstudio-library-test - -# Run all tests -helm unittest other-charts/rstudio-library-test -``` - -## Test Coverage -The test suite covers all `rstudio-library` template functions: -| Test File | Library Templates Tested | -|-----------|-------------------------| -| `config_test.yaml` | `config.gcfg`, `config.ini`, `config.dcf`, `config.json`, `config.txt` | -| `ingress_test.yaml` | `ingress.apiVersion`, `ingress.path`, `ingress.backend`, `ingress.supportsIngressClassName`, `ingress.supportsPathType` | -| `license_test.yaml` | `license-env`, `license-mount`, `license-volume`, `license-secret` | -| `rbac_test.yaml` | `rbac` (ServiceAccount, Role, RoleBinding, ClusterRole) | -| `profiles_test.yaml` | `profiles.ini`, `profiles.ini.singleFile`, `profiles.ini.collapse-array`, `profiles.ini.advanced`, `profiles.json-from-overrides-config`, `profiles.apply-everyone-and-default-to-others` | -| `debug_test.yaml` | `debug.type-check` | -| `launcher_templates_test.yaml` | `templates.skeleton`, `templates.dataOutput`, `templates.dataOutputPretty` | -| `tplvalues_test.yaml` | `tplvalues.render` | -| `chronicle_agent_test.yaml` | `chronicle-agent.image`, `chronicle-agent.serverAddress` | - -## Test Structure - -Each test file follows this pattern: -1. Includes the base values file (`tests/_base_values.yaml`) which nullifies all test categories -2. Overrides only the specific test values needed for that test -3. Uses assertions to verify expected output - -The `_base_values.yaml` file provides test isolation by setting all test categories to `null`, preventing unrelated templates from rendering during tests. - -## Adding New Tests - -When adding tests for new `rstudio-library` templates: - -1. Create a test template in `templates/test-*.yaml` that invokes the library template -2. Add test values to `values.yaml` with documentation comments -3. Update `tests/_base_values.yaml` to include the new test category (set to `null`) -4. Create a test file in `tests/*_test.yaml` that includes `_base_values.yaml` and overrides only needed values - -## Manual Verification - -To manually verify template output: - -```bash -helm template test-release other-charts/rstudio-library-test +# rstudio-library-test -# Test specific template -helm template test-release other-charts/rstudio-library-test -s templates/test-config.yaml -``` +![Version: 0.1.0](https://img.shields.io/badge/Version-0.1.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 0.1.0](https://img.shields.io/badge/AppVersion-0.1.0-informational?style=flat-square) + +Test harness for rstudio-library templates + +## Requirements + +| Repository | Name | Version | +|------------|------|---------| +| file://../../charts/rstudio-library | rstudio-library | 0.1.35 | + +## Values + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| testChronicle.enabled | bool | `true` | | +| testChronicle.image.registry | string | `"ghcr.io"` | | +| testChronicle.image.repository | string | `"rstudio/chronicle-agent"` | | +| testChronicle.image.tag | string | `"1.0.0"` | | +| testChronicle.serverAddress | string | `"http://chronicle-server.default:8080"` | | +| testChronicle.serverNamespace | string | `""` | | +| testConfig.dcf.config.key1 | string | `"value1"` | | +| testConfig.dcf.config.nested.subkey | string | `"subvalue"` | | +| testConfig.dcf.filename | string | `"test.dcf"` | | +| testConfig.gcfg.config.section1.key1 | string | `"value1"` | | +| testConfig.gcfg.config.section1.key2 | string | `"value2"` | | +| testConfig.gcfg.config.section2.arrayKey[0] | string | `"item1"` | | +| testConfig.gcfg.config.section2.arrayKey[1] | string | `"item2"` | | +| testConfig.gcfg.filename | string | `"test.gcfg"` | | +| testConfig.ini.config.section1.key1 | string | `"value1"` | | +| testConfig.ini.config.section1.key2 | int | `123` | | +| testConfig.ini.filename | string | `"test.ini"` | | +| testConfig.json.config.arrayKey[0] | string | `"item1"` | | +| testConfig.json.config.arrayKey[1] | string | `"item2"` | | +| testConfig.json.config.boolKey | bool | `true` | | +| testConfig.json.config.numberKey | int | `42` | | +| testConfig.json.config.stringKey | string | `"stringValue"` | | +| testConfig.json.filename | string | `"test.json"` | | +| testConfig.txt.config.key1 | string | `"value1"` | | +| testConfig.txt.config.key2 | string | `"value2"` | | +| testConfig.txt.filename | string | `"test.txt"` | | +| testDebug.boolValue | bool | `true` | | +| testDebug.mapValue.key | string | `"value"` | | +| testDebug.sliceValue[0] | string | `"item1"` | | +| testDebug.sliceValue[1] | string | `"item2"` | | +| testDebug.stringValue | string | `"test string"` | | +| testIngress.path | string | `"/test"` | | +| testIngress.pathType | string | `"Prefix"` | | +| testIngress.serviceName | string | `"test-service"` | | +| testIngress.servicePort | int | `8080` | | +| testLauncherTemplates.content.key1 | string | `"value1"` | | +| testLauncherTemplates.content.key2 | string | `"value2"` | | +| testLauncherTemplates.content.nested.subkey | string | `"subvalue"` | | +| testLauncherTemplates.templateName | string | `"test-template"` | | +| testLicense.licenseFile | string | `"LICENSE CONTENT HERE\n"` | | +| testLicense.licenseKey | string | `"test-license-key"` | | +| testLicense.licenseServer | string | `"license.example.com"` | | +| testProfiles.advanced.data."launcher.kubernetes.profiles.conf".*.default-cpus | int | `1` | | +| testProfiles.advanced.data."launcher.kubernetes.profiles.conf".testuser.default-cpus | int | `4` | | +| testProfiles.advanced.filePath | string | `"/etc/rstudio/"` | | +| testProfiles.advanced.jobJsonDefaults | list | `[]` | | +| testProfiles.basicIni."launcher.kubernetes.profiles.conf".*.default-cpus | int | `1` | | +| testProfiles.basicIni."launcher.kubernetes.profiles.conf".*.default-mem-mb | int | `512` | | +| testProfiles.basicIni."launcher.kubernetes.profiles.conf".testuser.default-cpus | int | `2` | | +| testProfiles.collapseArray.simple[0] | string | `"one"` | | +| testProfiles.collapseArray.simple[1] | string | `"two"` | | +| testProfiles.collapseArray.simple[2] | string | `"three"` | | +| testProfiles.collapseArray.targetFile[0].file | string | `"/etc/config/pods.json"` | | +| testProfiles.collapseArray.targetFile[0].target | string | `"pods"` | | +| testProfiles.collapseArray.targetFile[1].file | string | `"/etc/config/services.json"` | | +| testProfiles.collapseArray.targetFile[1].target | string | `"services"` | | +| testProfiles.singleFile.*.job-name | string | `"default-job"` | | +| testProfiles.singleFile.testuser.job-name | string | `"user-job"` | | +| testRbac.annotations | object | `{}` | | +| testRbac.clusterRoleCreate | bool | `false` | | +| testRbac.labels | object | `{}` | | +| testRbac.namespace | string | `"test-namespace"` | | +| testRbac.removeNamespaceReferences | bool | `false` | | +| testRbac.serviceAccountCreate | bool | `true` | | +| testRbac.serviceAccountName | string | `"test-sa"` | | +| testRbac.targetNamespace | string | `"test-target"` | | +| testTplvalues.objectValue.name | string | `"{{ .Release.Name }}"` | | +| testTplvalues.objectValue.namespace | string | `"{{ .Release.Namespace }}"` | | +| testTplvalues.staticValue | string | `"static"` | | +| testTplvalues.templateValue | string | `"{{ .Release.Name }}-suffix"` | | + +---------------------------------------------- +Autogenerated from chart metadata using [helm-docs v1.13.1](https://github.com/norwoodj/helm-docs/releases/v1.13.1)