Repository: kubernetes-sigs/external-dns (language: go)

Issue:
Error: template: external-dns/templates/deployment.yaml:105:24: executing "external-dns/templates/deployment.yaml" at <eq .Values.txtPrefix "">: error calling eq: invalid type for comparison
<!-- Please use this template while reporting a bug and provide as much info as possible. Not doing so may result in your bug not being addressed in a timely manner. Thanks!

-->

**What happened**:
I tried to install Helm chart, failed with an error
> Error: template: external-dns/templates/deployment.yaml:105:24: executing "external-dns/templates/deployment.yaml" at <eq .Values.txtPrefix "">: error calling eq: invalid type for comparison

**What you expected to happen**:
Helm chart is installed

**How to reproduce it (as minimally and precisely as possible)**:
`helm upgrade --install external-dns external-dns/external-dns --version 1.15.0`



Discussion / hints:
What is the helm version?

Fix patch:
diff --git a/Makefile b/Makefile
index f81221dd52..e3e1a995ff 100644
--- a/Makefile
+++ b/Makefile
@@ -184,3 +184,11 @@ pre-commit-validate:
 #? help: Get more info on available commands
 help: Makefile
 	@sed -n 's/^#?//p' $< | column -t -s ':' |  sort | sed -e 's/^/ /'
+
+#? helm-test: Run unit tests
+helm-test:
+	scripts/helm-tools.sh --helm-unittest
+
+#? helm-template: Run helm template
+helm-template:
+	scripts/helm-tools.sh --helm-template
diff --git a/charts/external-dns/.helmignore b/charts/external-dns/.helmignore
index 596d9bae68..e951b6fb71 100644
--- a/charts/external-dns/.helmignore
+++ b/charts/external-dns/.helmignore
@@ -24,3 +24,4 @@
 ci/
 schema/
 .schema.yaml
+tests/
diff --git a/charts/external-dns/CHANGELOG.md b/charts/external-dns/CHANGELOG.md
index d1e974f196..574992f153 100644
--- a/charts/external-dns/CHANGELOG.md
+++ b/charts/external-dns/CHANGELOG.md
@@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ### Added
 
+- Added helm testing framework `helm plugin unittest`. ([#5137](https://github.com/kubernetes-sigs/external-dns/pull/5137)) _@ivankatliarchuk_
 - Added ability to generate schema with `helm plugin schema`. ([#5075](https://github.com/kubernetes-sigs/external-dns/pull/5075)) _@ivankatliarchuk_
 - Added `docs/contributing/dev-guide.md#helm-values` guide. ([#5075](https://github.com/kubernetes-sigs/external-dns/pull/5075)) _@ivankatliarchuk_
 
diff --git a/charts/external-dns/values.schema.json b/charts/external-dns/values.schema.json
index 47db1df6b5..648c5c8825 100644
--- a/charts/external-dns/values.schema.json
+++ b/charts/external-dns/values.schema.json
@@ -497,10 +497,16 @@
           "type": "boolean"
         },
         "mountPath": {
-          "type": "null"
+          "type": [
+            "string",
+            "null"
+          ]
         },
         "subPath": {
-          "type": "null"
+          "type": [
+            "string",
+            "null"
+          ]
         }
       },
       "type": "object"
diff --git a/charts/external-dns/values.yaml b/charts/external-dns/values.yaml
index 0bb99a759e..e1a299c43c 100644
--- a/charts/external-dns/values.yaml
+++ b/charts/external-dns/values.yaml
@@ -302,8 +302,8 @@ secretConfiguration:
   # -- If `true`, create a `Secret` to store sensitive provider configuration (**DEPRECATED**).
   enabled: false
   # -- Mount path for the `Secret`, this can be templated.
-  mountPath:
+  mountPath:  # @schema type:[string, null]; default: null
   # -- Sub-path for mounting the `Secret`, this can be templated.
-  subPath:
+  subPath:  # @schema type:[string, null]; default: null
   # -- `Secret` data.
   data: {}
diff --git a/docs/contributing/dev-guide.md b/docs/contributing/dev-guide.md
index 0ba8c4cb85..8f220ce353 100644
--- a/docs/contributing/dev-guide.md
+++ b/docs/contributing/dev-guide.md
@@ -244,7 +244,13 @@ This helm chart comes with a JSON schema generated from values with [helm schema
 ❯❯ scripts/helm-tools.sh --docs
 ```
 
-6. Add an entry to the chart [CHANGELOG.md](../../charts/external-dns/CHANGELOG.md) under `## UNRELEASED` section and `open` pull request
+6. Run helm unittets.
+
+```sh
+❯❯ make helm-test
+```
+
+7. Add an entry to the chart [CHANGELOG.md](../../charts/external-dns/CHANGELOG.md) under `## UNRELEASED` section and `open` pull request
 
 ## Deploy with kubernetes manifests
 
diff --git a/scripts/helm-tools.sh b/scripts/helm-tools.sh
index f723eb7411..0d635a6cae 100755
--- a/scripts/helm-tools.sh
+++ b/scripts/helm-tools.sh
@@ -14,6 +14,8 @@ set -e
 # scripts/helm-tools.sh --schema
 # scripts/helm-tools.sh --lint
 # scripts/helm-tools.sh --docs
+# scripts/helm-tools.sh --helm-template
+# scripts/helm-tools.sh --helm-unittest
 
 show_help() {
 cat << EOF
@@ -26,6 +28,8 @@ Usage: $(basename "$0") <options>
     -i, --install       Install required tooling
     -l, --lint          Lint chart
     -s, --schema        Generate schema
+    --helm-unittest     Run helm unittest(s)
+    --helm-template     Run helm template
     --show-docs         Show available documentation
 EOF
 }
@@ -34,8 +38,13 @@ install() {
   if [[ -x $(which helm) ]]; then
       echo "installing https://github.com/losisin/helm-values-schema-json.git plugin"
       helm plugin install https://github.com/losisin/helm-values-schema-json.git | true
+      helm plugin update schema
       helm plugin list | grep "schema"
 
+      helm plugin install https://github.com/helm-unittest/helm-unittest.git | true
+      helm plugin update unittest
+      helm plugin list | grep "unittest"
+
       echo "installing helm-docs"
       go install github.com/norwoodj/helm-docs/cmd/helm-docs@latest | true
 
@@ -84,6 +93,16 @@ helm_docs() {
   helm-docs
 }
 
+helm_unittest() {
+  helm unittest -f 'tests/*_test.yaml' --color charts/external-dns
+}
+
+helm_template() {
+  helm template external-dns charts/external-dns \
+		--output-dir _scratch \
+		-n kube-system
+}
+
 show_docs() {
   open "https://github.com/losisin/helm-values-schema-json?tab=readme-ov-file"
 }
@@ -93,6 +112,12 @@ function main() {
     --show-docs)
       show_docs
       ;;
+    --helm-unittest)
+      helm_unittest
+      ;;
+    --helm-template)
+      helm_unittest
+      ;;
     -d|--diff)
       diff_schema
       ;;


Test patch:
diff --git a/.github/workflows/lint-test-chart.yaml b/.github/workflows/lint-test-chart.yaml
index fbc3d86ad4..14514f69a7 100644
--- a/.github/workflows/lint-test-chart.yaml
+++ b/.github/workflows/lint-test-chart.yaml
@@ -58,6 +58,12 @@ jobs:
             exit 1
           fi
 
+      - name: Run Helm Unit Tests
+        run: |
+          set -euo pipefail
+          helm plugin install https://github.com/helm-unittest/helm-unittest.git >/dev/null 2>&1
+          helm unittest -f 'tests/*_test.yaml' --color charts/external-dns
+
       - name: Install Artifact Hub CLI
         uses: action-stars/install-tool-from-github-release@ece2623611b240002e0dd73a0d685505733122f6 # v0.2.4
         with:
diff --git a/charts/external-dns/tests/common-metadata_test.yaml b/charts/external-dns/tests/common-metadata_test.yaml
new file mode 100644
index 0000000000..3a976b4277
--- /dev/null
+++ b/charts/external-dns/tests/common-metadata_test.yaml
@@ -0,0 +1,29 @@
+suite: All resources contains metadata
+templates:
+  - "*.yaml"
+release:
+  name: external-dns
+chart:
+  version: "1.15.0"
+  appVersion: "0.15.0"
+tests:
+  - it: "should contain labels metadata in each template"
+    set:
+      secretConfiguration:
+        enabled: true
+        mountPath: "/etc/kubernetes/"
+      serviceMonitor:
+        enabled: true
+    asserts:
+      - exists:
+          path: metadata.labels
+      - isNotEmpty:
+          path: metadata.labels
+      - equal:
+          path: metadata.labels
+          value:
+            helm.sh/chart: external-dns-1.15.0
+            app.kubernetes.io/name: external-dns
+            app.kubernetes.io/instance: external-dns
+            app.kubernetes.io/version: "0.15.0"
+            app.kubernetes.io/managed-by: Helm
diff --git a/charts/external-dns/tests/deployment-config_test.yaml b/charts/external-dns/tests/deployment-config_test.yaml
new file mode 100644
index 0000000000..4f7268ef68
--- /dev/null
+++ b/charts/external-dns/tests/deployment-config_test.yaml
@@ -0,0 +1,80 @@
+suite: Deployment configuration
+templates:
+  - deployment.yaml
+release:
+  namespace: default
+tests:
+  - it: should provide expected defaults
+    asserts:
+      - isKind:
+            of: Deployment
+      - hasDocuments:
+          count: 1
+      - equal:
+          path: spec.replicas
+          value: 1
+      - equal:
+          path: spec.strategy.type
+          value: Recreate
+      - equal:
+          path: metadata.namespace
+          value: default
+      - notExists:
+          path: spec.template.spec.automountServiceAccountToken
+
+  - it: should provide expected defaults for securityContext
+    asserts:
+      - isSubset:
+          path: spec.template.spec.containers[?(@.name == "external-dns")]
+          content:
+            securityContext:
+              allowPrivilegeEscalation: false
+              capabilities:
+                drop:
+                  - ALL
+              privileged: false
+              readOnlyRootFilesystem: true
+              runAsGroup: 65532
+              runAsNonRoot: true
+              runAsUser: 65532
+
+  - it: should provide expected defaults for liveness and readiness
+    asserts:
+      - isSubset:
+          path: spec.template.spec.containers[?(@.name == "external-dns")]
+          content:
+            readinessProbe:
+              failureThreshold: 6
+              httpGet:
+                path: /healthz
+                port: http
+              initialDelaySeconds: 5
+              periodSeconds: 10
+              successThreshold: 1
+              timeoutSeconds: 5
+            livenessProbe:
+              failureThreshold: 2
+              httpGet:
+                path: /healthz
+                port: http
+              initialDelaySeconds: 10
+              periodSeconds: 10
+              successThreshold: 1
+              timeoutSeconds: 5
+
+  - it: should have service account set
+    release:
+      name: test
+    asserts:
+      - equal:
+          path: spec.template.spec.serviceAccountName
+          value: test-external-dns
+
+  - it: should have not be able to change replicas when specified via values
+    set:
+      deployment:
+        replicas: 3
+    asserts:
+      - equal:
+          path: spec.replicas
+          value: 1
diff --git a/charts/external-dns/tests/deployment-flags_test.yaml b/charts/external-dns/tests/deployment-flags_test.yaml
new file mode 100644
index 0000000000..38a9567ff4
--- /dev/null
+++ b/charts/external-dns/tests/deployment-flags_test.yaml
@@ -0,0 +1,119 @@
+suite: Deployment flags configurations
+templates:
+  - deployment.yaml
+release:
+  namespace: default
+tests:
+  - it: should provide expected default flags
+    asserts:
+      # - matchSnapshot: {}
+      - exists :
+          path: spec.template.spec.containers[?(@.name == "external-dns")]
+      - equal :
+          path: spec.template.spec.containers[?(@.name == "external-dns")].args
+          value:
+            - --log-level=info
+            - --log-format=text
+            - --interval=1m
+            - --source=service
+            - --source=ingress
+            - --policy=upsert-only
+            - --registry=txt
+            - --provider=aws
+
+  - it: should configure txtPrefix and ignore txtSuffix if not empty
+    set:
+      provider:
+        name: cloudflare
+      txtPrefix: "test-prefix"
+      txtSuffix: "test-suffix"
+      txtOwnerId: "testing"
+    asserts:
+      - exists :
+          path: spec.template.spec.containers[?(@.name == "external-dns")]
+      - equal :
+          path: spec.template.spec.containers[?(@.name == "external-dns")].args
+          value:
+            - --log-level=info
+            - --log-format=text
+            - --interval=1m
+            - --source=service
+            - --source=ingress
+            - --policy=upsert-only
+            - --registry=txt
+            - --txt-owner-id=testing
+            - --txt-prefix=test-prefix
+            - --provider=cloudflare
+      - notContains:
+          path: spec.template.spec.containers[?(@.name == "external-dns")].args
+          content: "--txt-suffix=test-suffix"
+
+  - it: should configure 'txtSuffix' when not empty with 'txtPrefix' empty
+    set:
+      txtPrefix: ""
+      txtSuffix: "test-suffix"
+    asserts:
+      - exists :
+          path: spec.template.spec.containers[?(@.name == "external-dns")]
+      - equal :
+          path: spec.template.spec.containers[?(@.name == "external-dns")].args
+          value:
+            - --log-level=info
+            - --log-format=text
+            - --interval=1m
+            - --source=service
+            - --source=ingress
+            - --policy=upsert-only
+            - --registry=txt
+            - --txt-suffix=test-suffix
+            - --provider=aws
+      - notContains:
+          path: spec.template.spec.containers[?(@.name == "external-dns")].args
+          content: "--txt-prefix=test-prefix"
+
+  - it: should be able configure multiple sources
+    set:
+      sources:
+        - fake
+        - crd
+    asserts:
+      - contains:
+          path: spec.template.spec.containers[?(@.name == "external-dns")].args
+          content: "--source=fake"
+      - contains:
+          path: spec.template.spec.containers[?(@.name == "external-dns")].args
+          content: "--source=crd"
+
+  - it: should be able to configure in single namespace
+    set:
+      namespaced: true
+    asserts:
+      - contains:
+          path: spec.template.spec.containers[?(@.name == "external-dns")].args
+          content: "--namespace=default"
+
+  - it: should manage multiple zones with 'extraArgs'
+    set:
+      extraArgs:
+        - --zone-id-filter=/hostedzone/Z00001
+        - --zone-id-filter=/hostedzone/Z00002
+        - --zone-id-filter=/hostedzone/Z00003
+        - --zone-id-filter=/hostedzone/Z00004
+        - --zone-id-filter=/hostedzone/Z00005
+    asserts:
+      - equal:
+          path: spec.template.spec.containers[?(@.name == "external-dns")].args
+          value:
+            - --log-level=info
+            - --log-format=text
+            - --interval=1m
+            - --source=service
+            - --source=ingress
+            - --policy=upsert-only
+            - --registry=txt
+            - --provider=aws
+            - --zone-id-filter=/hostedzone/Z00001
+            - --zone-id-filter=/hostedzone/Z00002
+            - --zone-id-filter=/hostedzone/Z00003
+            - --zone-id-filter=/hostedzone/Z00004
+            - --zone-id-filter=/hostedzone/Z00005
diff --git a/charts/external-dns/tests/deployment-scheduling_test.yaml b/charts/external-dns/tests/deployment-scheduling_test.yaml
new file mode 100644
index 0000000000..849bb17ab6
--- /dev/null
+++ b/charts/external-dns/tests/deployment-scheduling_test.yaml
@@ -0,0 +1,118 @@
+suite: Deployment scheduling configuration
+templates:
+  - deployment.yaml
+release:
+  namespace: default
+tests:
+  - it: should not provide defaults for affinities or tolerations
+    asserts:
+      - isKind:
+          of: Deployment
+      - notExists:
+          path: spec.template.spec.tolerations
+      - notExists:
+          path: spec.template.spec.affinity
+
+  - it: should provide support for custom affinities
+    set:
+      affinity:
+        nodeAffinity:
+          requiredDuringSchedulingIgnoredDuringExecution:
+            nodeSelectorTerms:
+            - matchExpressions:
+              - key: topology.kubernetes.io/zone
+                operator: In
+                values:
+                - zoneA
+                - zoneB
+    asserts:
+      - equal:
+          path: spec.template.spec.affinity
+          value:
+            nodeAffinity:
+              requiredDuringSchedulingIgnoredDuringExecution:
+                nodeSelectorTerms:
+                - matchExpressions:
+                  - key: topology.kubernetes.io/zone
+                    operator: In
+                    values:
+                    - zoneA
+                    - zoneB
+
+  - it: should provide support for configuring node and pod affinities
+    set:
+      affinity:
+        podAntiAffinity:
+          requiredDuringSchedulingIgnoredDuringExecution:
+            - topologyKey: kubernetes.io/hostname
+              labelSelector:
+                matchExpressions:
+                - key: app
+                  operator: In
+                  values:
+                  - test-pod-scheduling-config
+        nodeAffinity:
+          requiredDuringSchedulingIgnoredDuringExecution:
+            nodeSelectorTerms:
+              - matchExpressions:
+                - key: datadog-agent
+                  operator: In
+                  values:
+                  - standard
+                - key: iam/scope
+                  operator: In
+                  values:
+                  - namespace
+    asserts:
+      - equal:
+          path: spec.template.spec.affinity
+          value:
+            nodeAffinity:
+              requiredDuringSchedulingIgnoredDuringExecution:
+                nodeSelectorTerms:
+                  - matchExpressions:
+                      - key: datadog-agent
+                        operator: In
+                        values:
+                          - standard
+                      - key: iam/scope
+                        operator: In
+                        values:
+                          - namespace
+            podAntiAffinity:
+              requiredDuringSchedulingIgnoredDuringExecution:
+                - labelSelector:
+                    matchExpressions:
+                      - key: app
+                        operator: In
+                        values:
+                          - test-pod-scheduling-config
+                  topologyKey: kubernetes.io/hostname
+
+  - it: should provide support for configuring tolerations
+    set:
+      tolerations:
+        - key: "key1"
+          operator: "Equal"
+          value: "value1"
+          effect: "NoSchedule"
+        - key: iam/scope
+          operator: Equal
+          value: namespace
+          effect: NoExecute
+    asserts:
+      - notExists:
+          path: spec.template.spec.affinity
+      - exists:
+          path: spec.template.spec.tolerations
+      - equal:
+          path: spec.template.spec.tolerations
+          value:
+            - effect: NoSchedule
+              key: key1
+              operator: Equal
+              value: value1
+            - effect: NoExecute
+              key: iam/scope
+              operator: Equal
+              value: namespace
diff --git a/charts/external-dns/tests/notes_test.yaml b/charts/external-dns/tests/notes_test.yaml
new file mode 100644
index 0000000000..c165ca2d84
--- /dev/null
+++ b/charts/external-dns/tests/notes_test.yaml
@@ -0,0 +1,21 @@
+suite: NOTES.txt output
+templates:
+  - NOTES.txt
+chart:
+  appVersion: 0.15.0
+  version: 1.15.0
+tests:
+  - it: should display message
+    set:
+      rbac:
+        namespaced: true
+    asserts:
+      - equalRaw:
+          value: |
+            ***********************************************************************
+            * External DNS                                                        *
+            ***********************************************************************
+              Chart version: 1.15.0
+              App version:   0.15.0
+              Image tag:     registry.k8s.io/external-dns/external-dns:v0.15.0
+            ***********************************************************************
diff --git a/charts/external-dns/tests/rbac_test.yaml b/charts/external-dns/tests/rbac_test.yaml
new file mode 100644
index 0000000000..fd1b8cfbc8
--- /dev/null
+++ b/charts/external-dns/tests/rbac_test.yaml
@@ -0,0 +1,155 @@
+suite: RBAC configuration
+templates:
+  - clusterrole.yaml
+  - clusterrolebinding.yaml
+  - serviceaccount.yaml
+release:
+  name: rbac
+tests:
+  - it: should create default RBAC related objects
+    asserts:
+      - isKind:
+          of: ClusterRole
+        template: clusterrole.yaml
+      - equal:
+          path: metadata.name
+          value: rbac-external-dns
+        template: clusterrole.yaml
+      - isKind:
+          of: ClusterRoleBinding
+        template: clusterrolebinding.yaml
+      - equal:
+          path: metadata.name
+          value: rbac-external-dns-viewer
+        template: clusterrolebinding.yaml
+      - isKind:
+          of: ServiceAccount
+        template: serviceaccount.yaml
+      - equal:
+          path: metadata.name
+          value: rbac-external-dns
+        template: serviceaccount.yaml
+
+  - it: should create default RBAC rules
+    asserts:
+      - template: clusterrole.yaml
+        equal:
+          path: rules
+          value:
+            - apiGroups: [""]
+              resources: ["nodes"]
+              verbs: ["list", "watch"]
+            - apiGroups: [""]
+              resources: ["pods"]
+              verbs: ["get", "watch", "list"]
+            - apiGroups: [""]
+              resources: ["services","endpoints"]
+              verbs: ["get","watch","list"]
+            - apiGroups: ["extensions","networking.k8s.io"]
+              resources: ["ingresses"]
+              verbs: ["get","watch","list"]
+
+  - it: should create default RBAC rules for 'ambassador-host'
+    set:
+      sources:
+        - ambassador-host
+    asserts:
+      - template: clusterrole.yaml
+        equal:
+          path: rules
+          value:
+            - apiGroups: ["getambassador.io"]
+              resources: ["hosts","ingresses"]
+              verbs: ["get","watch","list"]
+
+  - it: should create default RBAC rules for 'crd' and 'traefik-proxy'
+    set:
+      sources:
+        - crd
+        - traefik-proxy
+    asserts:
+      - template: clusterrole.yaml
+        equal:
+          path: rules
+          value:
+            - apiGroups: ["externaldns.k8s.io"]
+              resources: ["dnsendpoints"]
+              verbs: ["get","watch","list"]
+            - apiGroups: ["externaldns.k8s.io"]
+              resources: ["dnsendpoints/status"]
+              verbs: ["*"]
+            - apiGroups: ["traefik.containo.us", "traefik.io"]
+              resources: ["ingressroutes", "ingressroutetcps", "ingressrouteudps"]
+              verbs: ["get","watch","list"]
+
+  - it: should create default RBAC rules for 'f5' when 'f5-virtualserver' is set
+    set:
+      sources:
+        - f5-virtualserver
+    asserts:
+      - template: clusterrole.yaml
+        equal:
+          path: rules
+          value:
+            - apiGroups: ["cis.f5.com"]
+              resources: ["virtualservers", "transportservers"]
+              verbs: ["get","watch","list"]
+
+  - it: should create default RBAC rules for 'f5' when 'f5-transportserver' is set
+    set:
+      sources:
+        - f5-transportserver
+    asserts:
+      - template: clusterrole.yaml
+        equal:
+          path: rules
+          value:
+            - apiGroups: ["cis.f5.com"]
+              resources: ["virtualservers", "transportservers"]
+              verbs: ["get","watch","list"]
+
+  - it: should create default RBAC rules for 'gateway-api' with source 'gateway-httproute'
+    set:
+      sources:
+        - gateway-httproute
+    asserts:
+      - template: clusterrole.yaml
+        equal:
+          path: rules
+          value:
+            - apiGroups: ["gateway.networking.k8s.io"]
+              resources: ["gateways"]
+              verbs: ["get","watch","list"]
+            - apiGroups: [""]
+              resources: ["namespaces"]
+              verbs: ["get","watch","list"]
+            - apiGroups: ["gateway.networking.k8s.io"]
+              resources: ["httproutes"]
+              verbs: ["get","watch","list"]
+
+  - it: should create default RBAC rules for 'gateway-api' with sources 'tlsroute,tcproute,udproute'
+    set:
+      sources:
+        - gateway-tlsroute
+        - gateway-tcproute
+        - gateway-udproute
+    asserts:
+      - template: clusterrole.yaml
+        equal:
+          path: rules
+          value:
+            - apiGroups: ["gateway.networking.k8s.io"]
+              resources: ["gateways"]
+              verbs: ["get","watch","list"]
+            - apiGroups: [""]
+              resources: ["namespaces"]
+              verbs: ["get","watch","list"]
+            - apiGroups: ["gateway.networking.k8s.io"]
+              resources: ["tlsroutes"]
+              verbs: ["get","watch","list"]
+            - apiGroups: ["gateway.networking.k8s.io"]
+              resources: ["tcproutes"]
+              verbs: ["get","watch","list"]
+            - apiGroups: ["gateway.networking.k8s.io"]
+              resources: ["udproutes"]
+              verbs: ["get","watch","list"]
diff --git a/charts/external-dns/tests/secret_test.yaml b/charts/external-dns/tests/secret_test.yaml
new file mode 100644
index 0000000000..67d8456b3a
--- /dev/null
+++ b/charts/external-dns/tests/secret_test.yaml
@@ -0,0 +1,42 @@
+suite: Secret configuration
+templates:
+  - "secret.yaml"
+  - "deployment.yaml"
+tests:
+  - it: should not provide secret by default
+    template: "secret.yaml"
+    asserts:
+      - hasDocuments:
+          count: 0
+
+  - it: should configure Azure secret
+    release:
+      name: under-test
+    set:
+      secretConfiguration:
+        enabled: true
+        mountPath: "/etc/kubernetes/"
+        data:
+          azure.json: |
+            {
+              "tenantId": "00000000-0000-0000-0000-000000000000",
+              "subscriptionId": "00000000-0000-0000-0000-000000000000",
+              "resourceGroup": "networking",
+              "useWorkloadIdentityExtension": true
+            }
+    asserts:
+      - hasDocuments:
+          count: 1
+      - exists:
+          path: data["azure.json"]
+        template: "secret.yaml"
+      - exists:
+          path: spec.template.spec.volumes
+        template: "deployment.yaml"
+      - equal:
+          path: spec.template.spec.volumes
+          value:
+            - name: secrets
+              secret:
+                secretName: under-test-external-dns
+        template: "deployment.yaml"
diff --git a/charts/external-dns/tests/serviceaccount_test.yaml b/charts/external-dns/tests/serviceaccount_test.yaml
new file mode 100644
index 0000000000..e86ae1d117
--- /dev/null
+++ b/charts/external-dns/tests/serviceaccount_test.yaml
@@ -0,0 +1,61 @@
+suite: ServiceAccount configuration
+templates:
+  - serviceaccount.yaml
+release:
+  namespace: default
+tests:
+  - it: should provide a single service account by default
+    asserts:
+      - isKind:
+            of: ServiceAccount
+      - hasDocuments:
+          count: 1
+      - equal:
+          path: automountServiceAccountToken
+          value: null
+
+  - it: should provide a way to disable service account
+    set:
+      serviceAccount:
+        create: false
+    asserts:
+      - hasDocuments:
+          count: 0
+
+  - it: should support annotations without template variables
+    set:
+      serviceAccount:
+        annotations:
+          eks.amazonaws.com/role-arn: arn:aws:iam::123456789:role/external-dns-role
+          eks.amazonaws.com/sts-regional-endpoints: "true"
+    asserts:
+      - hasDocuments:
+          count: 1
+      - equal:
+          path: metadata.annotations
+          value:
+            eks.amazonaws.com/role-arn: arn:aws:iam::123456789:role/external-dns-role
+            eks.amazonaws.com/sts-regional-endpoints: "true"
+
+  - it: should support annotations with template variables
+    release:
+      name: v1
+    set:
+      account: 9876-dynamic-account-id
+      serviceAccount:
+        annotations:
+          eks.amazonaws.com/role-arn: arn:aws:iam::{{ .Values.account }}:role/external-dns-role
+          eks.amazonaws.com/sts-regional-endpoints: "true"
+          tags.k8s.io/service: service-{{ include "external-dns.fullname" . }}
+    asserts:
+      - hasDocuments:
+          count: 1
+      - equal:
+          path: metadata.annotations
+          value:
+            eks.amazonaws.com/role-arn: arn:aws:iam::9876-dynamic-account-id:role/external-dns-role
+            eks.amazonaws.com/sts-regional-endpoints: "true"
+            tags.k8s.io/service: service-v1-external-dns
+      - isType:
+          path: metadata.annotations["eks.amazonaws.com/sts-regional-endpoints"]
+          type: string