diff --git a/DOCUMENT.md b/DOCUMENT.md index 328c4eb82..75f700779 100644 --- a/DOCUMENT.md +++ b/DOCUMENT.md @@ -114,7 +114,7 @@ Available assertion types are listed below: | `notEqual` | **path**: *string*. The `set` path to assert.
**value**: *any*. The value expected not to be. | Assert the value of specified **path** NOT equal to the **value**. |
notEqual:
path: metadata.name
value: my-deploy
| | `matchRegex` | **path**: *string*. The `set` path to assert, the value must be a *string*.
**pattern**: *string*. The regex pattern to match (without quoting `/`). | Assert the value of specified **path** match **pattern**. |
matchRegex:
path: metadata.name
pattern: -my-chart$
| | `notMatchRegex` | **path**: *string*. The `set` path to assert, the value must be a *string*.
**pattern**: *string*. The regex pattern NOT to match (without quoting `/`). | Assert the value of specified **path** NOT match **pattern**. |
notMatchRegex:
path: metadata.name
pattern: -my-chat$
| -| `contains` | **path**: *string*. The `set` path to assert, the value must be an *array*.
**content**: *any*. The content to be contained. | Assert the array as the value of specified **path** contains the **content**. |
contains:
path: spec.ports
content:
name: web
port: 80
targetPort: 80
protocle:TCP
| +| `contains` | **path**: *string*. The `set` path to assert, the value must be an *array*.
**content**: *any*. The content to be contained.
**count**: *int, optional*. The count of content to be contained.
**any**: *bool, optional*. ignores any other values within the found content. | Assert the array as the value of specified **path** contains the **content**. |
contains:
path: spec.ports
content:
name: web
port: 80
targetPort: 80
protocle:TCP
| | `notContains` | **path**: *string*. The `set` path to assert, the value must be an *array*.
**content**: *any*. The content NOT to be contained. | Assert the array as the value of specified **path** NOT contains the **content**. |
notContains:
path: spec.ports
content:
name: server
port: 80
targetPort: 80
protocle: TCP
| | `isNull` | **path**: *string*. The `set` path to assert. | Assert the value of specified **path** is `null`. |
isNull:
path: spec.strategy
| | `isNotNull` | **path**: *string*. The `set` path to assert. | Assert the value of specified **path** is NOT `null`. |
isNotNull:
path: spec.replicas
| diff --git a/Makefile b/Makefile index a420c7595..b1504e7eb 100644 --- a/Makefile +++ b/Makefile @@ -16,8 +16,12 @@ install: bootstrap build .PHONY: hookInstall hookInstall: bootstrap build +.PHONY: unittest +unittest: + go test ./unittest/... -v -cover + .PHONY: build -build: +build: unittest go build -o untt -ldflags $(LDFLAGS) ./main.go .PHONY: dist diff --git a/__fixtures__/v2/basic/tests/__snapshot__/notes_test.yaml.snap b/__fixtures__/v2/basic/tests/__snapshot__/notes_test.yaml.snap new file mode 100644 index 000000000..2ca380109 --- /dev/null +++ b/__fixtures__/v2/basic/tests/__snapshot__/notes_test.yaml.snap @@ -0,0 +1,20 @@ +should pass the notes file with ingress enabled: + 1: | + | + 1. Get the application URL by running these commands: + http://chart-example.local +should pass the notes file with service type LoadBalancer: + 1: | + | + 1. Get the application URL by running these commands: + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get svc -w RELEASE-NAME-basic' + export SERVICE_IP=$(kubectl get svc --namespace NAMESPACE RELEASE-NAME-basic -o jsonpath='{.status.loadBalancer.ingress[0].ip}') + echo http://$SERVICE_IP:9999 +should pass the notes file with service type NodePort: + 1: | + | + 1. Get the application URL by running these commands: + export NODE_PORT=$(kubectl get --namespace NAMESPACE -o jsonpath="{.spec.ports[0].nodePort}" services RELEASE-NAME-basic) + export NODE_IP=$(kubectl get nodes --namespace NAMESPACE -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT diff --git a/__fixtures__/v2/basic/tests/notes_test.yaml b/__fixtures__/v2/basic/tests/notes_test.yaml new file mode 100644 index 000000000..3e950e9d6 --- /dev/null +++ b/__fixtures__/v2/basic/tests/notes_test.yaml @@ -0,0 +1,35 @@ +suite: test notes +templates: + - NOTES.txt +tests: + - it: should pass the notes file with ingress enabled + set: + ingress.enabled: true + asserts: + - equalRaw: + value: | + 1. Get the application URL by running these commands: + http://chart-example.local + - matchSnapshotRaw: {} + + - it: should pass the notes file with service type NodePort + set: + service.type: NodePort + asserts: + - equalRaw: + value: | + 1. Get the application URL by running these commands: + export NODE_PORT=$(kubectl get --namespace NAMESPACE -o jsonpath="{.spec.ports[0].nodePort}" services RELEASE-NAME-basic) + export NODE_IP=$(kubectl get nodes --namespace NAMESPACE -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT + - matchSnapshotRaw: {} + + - it: should pass the notes file with service type LoadBalancer + set: + service.type: LoadBalancer + service.externalPort: 9999 + asserts: + - matchRegexRaw: + pattern: http://\$SERVICE_IP:9999 + - matchSnapshotRaw: {} + \ No newline at end of file diff --git a/__fixtures__/v2/basic/tests_failed/notes_test.yaml b/__fixtures__/v2/basic/tests_failed/notes_test.yaml new file mode 100644 index 000000000..2c6cbccf5 --- /dev/null +++ b/__fixtures__/v2/basic/tests_failed/notes_test.yaml @@ -0,0 +1,31 @@ +suite: test notes +templates: + - NOTES.txt +tests: + - it: should pass the notes file with ingress enabled + set: + ingress.enabled: true + asserts: + - notEqualRaw: + value: | + 1. Get the application URL by running these commands: + http://chart-example.local + + - it: should pass the notes file with service type NodePort + set: + service.type: NodePort + asserts: + - notEqualRaw: + value: | + 1. Get the application URL by running these commands: + export NODE_PORT=$(kubectl get --namespace NAMESPACE -o jsonpath="{.spec.ports[0].nodePort}" services MY-RELEASE) + export NODE_IP=$(kubectl get nodes --namespace NAMESPACE -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT + + - it: should pass the notes file with service type LoadBalancer + set: + service.type: LoadBalancer + service.externalPort: 9999 + asserts: + - matchRegexRaw: + pattern: http://\$SERVICE_IP:80 diff --git a/__fixtures__/v2/with-subchart/charts/child-chart/tests/__snapshot__/notes_test.yaml.snap b/__fixtures__/v2/with-subchart/charts/child-chart/tests/__snapshot__/notes_test.yaml.snap new file mode 100644 index 000000000..95055e673 --- /dev/null +++ b/__fixtures__/v2/with-subchart/charts/child-chart/tests/__snapshot__/notes_test.yaml.snap @@ -0,0 +1,20 @@ +should pass the notes file with ingress enabled: + 1: | + | + 1. Get the application URL by running these commands: + http://chart-example.local +should pass the notes file with service type LoadBalancer: + 1: | + | + 1. Get the application URL by running these commands: + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get svc -w RELEASE-NAME-child-chart' + export SERVICE_IP=$(kubectl get svc --namespace NAMESPACE RELEASE-NAME-child-chart -o jsonpath='{.status.loadBalancer.ingress[0].ip}') + echo http://$SERVICE_IP:9999 +should pass the notes file with service type NodePort: + 1: | + | + 1. Get the application URL by running these commands: + export NODE_PORT=$(kubectl get --namespace NAMESPACE -o jsonpath="{.spec.ports[0].nodePort}" services RELEASE-NAME-child-chart) + export NODE_IP=$(kubectl get nodes --namespace NAMESPACE -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT diff --git a/__fixtures__/v2/with-subchart/charts/child-chart/tests/notes_test.yaml b/__fixtures__/v2/with-subchart/charts/child-chart/tests/notes_test.yaml new file mode 100644 index 000000000..dafb3ca0d --- /dev/null +++ b/__fixtures__/v2/with-subchart/charts/child-chart/tests/notes_test.yaml @@ -0,0 +1,35 @@ +suite: test notes +templates: + - NOTES.txt +tests: + - it: should pass the notes file with ingress enabled + set: + ingress.enabled: true + asserts: + - equalRaw: + value: | + 1. Get the application URL by running these commands: + http://chart-example.local + - matchSnapshotRaw: {} + + - it: should pass the notes file with service type NodePort + set: + service.type: NodePort + asserts: + - equalRaw: + value: | + 1. Get the application URL by running these commands: + export NODE_PORT=$(kubectl get --namespace NAMESPACE -o jsonpath="{.spec.ports[0].nodePort}" services RELEASE-NAME-child-chart) + export NODE_IP=$(kubectl get nodes --namespace NAMESPACE -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT + - matchSnapshotRaw: {} + + - it: should pass the notes file with service type LoadBalancer + set: + service.type: LoadBalancer + service.externalPort: 9999 + asserts: + - matchRegexRaw: + pattern: http://\$SERVICE_IP:9999 + - matchSnapshotRaw: {} + \ No newline at end of file diff --git a/__fixtures__/v2/with-subchart/templates/NOTES.txt b/__fixtures__/v2/with-subchart/templates/NOTES.txt index 30e62529b..f0cd1009c 100644 --- a/__fixtures__/v2/with-subchart/templates/NOTES.txt +++ b/__fixtures__/v2/with-subchart/templates/NOTES.txt @@ -4,16 +4,16 @@ http://{{ . }} {{- end }} {{- else if contains "NodePort" .Values.service.type }} - export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ template "parant-chart.fullname" . }}) + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ template "parent-chart.fullname" . }}) export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") echo http://$NODE_IP:$NODE_PORT {{- else if contains "LoadBalancer" .Values.service.type }} NOTE: It may take a few minutes for the LoadBalancer IP to be available. - You can watch the status of by running 'kubectl get svc -w {{ template "parant-chart.fullname" . }}' - export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ template "parant-chart.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}') + You can watch the status of by running 'kubectl get svc -w {{ template "parent-chart.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ template "parent-chart.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}') echo http://$SERVICE_IP:{{ .Values.service.externalPort }} {{- else if contains "ClusterIP" .Values.service.type }} - export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app={{ template "parant-chart.name" . }},release={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app={{ template "parent-chart.name" . }},release={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") echo "Visit http://127.0.0.1:8080 to use your application" kubectl port-forward $POD_NAME 8080:{{ .Values.service.internalPort }} {{- end }} diff --git a/__fixtures__/v2/with-subchart/tests/__snapshot__/notes_test.yaml.snap b/__fixtures__/v2/with-subchart/tests/__snapshot__/notes_test.yaml.snap new file mode 100644 index 000000000..1a2110db0 --- /dev/null +++ b/__fixtures__/v2/with-subchart/tests/__snapshot__/notes_test.yaml.snap @@ -0,0 +1,20 @@ +should pass the notes file with ingress enabled: + 1: | + | + 1. Get the application URL by running these commands: + http://chart-example.local +should pass the notes file with service type LoadBalancer: + 1: | + | + 1. Get the application URL by running these commands: + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get svc -w RELEASE-NAME-parent-chart' + export SERVICE_IP=$(kubectl get svc --namespace NAMESPACE RELEASE-NAME-parent-chart -o jsonpath='{.status.loadBalancer.ingress[0].ip}') + echo http://$SERVICE_IP:9999 +should pass the notes file with service type NodePort: + 1: | + | + 1. Get the application URL by running these commands: + export NODE_PORT=$(kubectl get --namespace NAMESPACE -o jsonpath="{.spec.ports[0].nodePort}" services RELEASE-NAME-parent-chart) + export NODE_IP=$(kubectl get nodes --namespace NAMESPACE -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT diff --git a/__fixtures__/v2/with-subchart/tests/notes_test.yaml b/__fixtures__/v2/with-subchart/tests/notes_test.yaml new file mode 100644 index 000000000..8a34e865c --- /dev/null +++ b/__fixtures__/v2/with-subchart/tests/notes_test.yaml @@ -0,0 +1,35 @@ +suite: test notes +templates: + - NOTES.txt +tests: + - it: should pass the notes file with ingress enabled + set: + ingress.enabled: true + asserts: + - equalRaw: + value: | + 1. Get the application URL by running these commands: + http://chart-example.local + - matchSnapshotRaw: {} + + - it: should pass the notes file with service type NodePort + set: + service.type: NodePort + asserts: + - equalRaw: + value: | + 1. Get the application URL by running these commands: + export NODE_PORT=$(kubectl get --namespace NAMESPACE -o jsonpath="{.spec.ports[0].nodePort}" services RELEASE-NAME-parent-chart) + export NODE_IP=$(kubectl get nodes --namespace NAMESPACE -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT + - matchSnapshotRaw: {} + + - it: should pass the notes file with service type LoadBalancer + set: + service.type: LoadBalancer + service.externalPort: 9999 + asserts: + - matchRegexRaw: + pattern: http://\$SERVICE_IP:9999 + - matchSnapshotRaw: {} + \ No newline at end of file diff --git a/__fixtures__/v2/with-subfolder/.helmignore b/__fixtures__/v2/with-subfolder/.helmignore new file mode 100644 index 000000000..dadf20295 --- /dev/null +++ b/__fixtures__/v2/with-subfolder/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj + +tests diff --git a/__fixtures__/v2/with-subfolder/Chart.yaml b/__fixtures__/v2/with-subfolder/Chart.yaml new file mode 100644 index 000000000..0d1f15e99 --- /dev/null +++ b/__fixtures__/v2/with-subfolder/Chart.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +description: A advanced example chart to demonstrate unittest plugin +name: with-subfolder +version: 0.1.0 diff --git a/__fixtures__/v2/with-subfolder/templates/NOTES.txt b/__fixtures__/v2/with-subfolder/templates/NOTES.txt new file mode 100644 index 000000000..f3c81c022 --- /dev/null +++ b/__fixtures__/v2/with-subfolder/templates/NOTES.txt @@ -0,0 +1,19 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range .Values.ingress.hosts }} + http://{{ . }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ template "with-subfolder.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get svc -w {{ template "with-subfolder.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ template "with-subfolder.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}') + echo http://$SERVICE_IP:{{ .Values.service.externalPort }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app={{ template "with-subfolder.name" . }},release={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl port-forward $POD_NAME 8080:{{ .Values.service.internalPort }} +{{- end }} diff --git a/__fixtures__/v2/with-subfolder/templates/_helpers.tpl b/__fixtures__/v2/with-subfolder/templates/_helpers.tpl new file mode 100644 index 000000000..21c49b484 --- /dev/null +++ b/__fixtures__/v2/with-subfolder/templates/_helpers.tpl @@ -0,0 +1,16 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Expand the name of the chart. +*/}} +{{- define "with-subfolder.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +*/}} +{{- define "with-subfolder.fullname" -}} +{{- $name := default .Chart.Name .Values.nameOverride -}} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} diff --git a/__fixtures__/v2/with-subfolder/templates/db/deployment.yaml b/__fixtures__/v2/with-subfolder/templates/db/deployment.yaml new file mode 100644 index 000000000..054e55003 --- /dev/null +++ b/__fixtures__/v2/with-subfolder/templates/db/deployment.yaml @@ -0,0 +1,42 @@ +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: {{ template "with-subfolder.fullname" . }}-db + labels: + app: {{ template "with-subfolder.name" . }} + chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +spec: + replicas: 1 + template: + metadata: + labels: + app: {{ template "with-subfolder.name" . }} + release: {{ .Release.Name }} + annotations: + some_template: | + --- + apiVersion: ... + this: is test for old separator workaround bug + spec: + containers: + - name: {{ .Chart.Name }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - containerPort: {{ .Values.service.dbPort }} + livenessProbe: + httpGet: + path: / + port: {{ .Values.service.internalPort }} + readinessProbe: + httpGet: + path: / + port: {{ .Values.service.internalPort }} + resources: +{{ toYaml .Values.resources | indent 12 }} + {{- if .Values.nodeSelector }} + nodeSelector: +{{ toYaml .Values.nodeSelector | indent 8 }} + {{- end }} diff --git a/__fixtures__/v2/with-subfolder/templates/webserver/deployment.yaml b/__fixtures__/v2/with-subfolder/templates/webserver/deployment.yaml new file mode 100644 index 000000000..bf8a0d155 --- /dev/null +++ b/__fixtures__/v2/with-subfolder/templates/webserver/deployment.yaml @@ -0,0 +1,38 @@ +--- +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: {{ template "with-subfolder.fullname" . }} + labels: + app: {{ template "with-subfolder.name" . }} + chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +spec: + replicas: {{ .Values.replicaCount }} + template: + metadata: + labels: + app: {{ template "with-subfolder.name" . }} + release: {{ .Release.Name }} + spec: + containers: + - name: {{ .Chart.Name }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - containerPort: {{ .Values.service.internalPort }} + livenessProbe: + httpGet: + path: / + port: {{ .Values.service.internalPort }} + readinessProbe: + httpGet: + path: / + port: {{ .Values.service.internalPort }} + resources: +{{ toYaml .Values.resources | indent 12 }} + {{- if .Values.nodeSelector }} + nodeSelector: +{{ toYaml .Values.nodeSelector | indent 8 }} + {{- end }} diff --git a/__fixtures__/v2/with-subfolder/templates/webserver/ingress.yaml b/__fixtures__/v2/with-subfolder/templates/webserver/ingress.yaml new file mode 100644 index 000000000..5f48d8e1c --- /dev/null +++ b/__fixtures__/v2/with-subfolder/templates/webserver/ingress.yaml @@ -0,0 +1,32 @@ +{{- if .Values.ingress.enabled -}} +{{- $serviceName := include "with-subfolder.fullname" . -}} +{{- $servicePort := .Values.service.externalPort -}} +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: {{ template "with-subfolder.fullname" . }} + labels: + app: {{ template "with-subfolder.name" . }} + chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} + annotations: + {{- range $key, $value := .Values.ingress.annotations }} + {{ $key }}: {{ $value | quote }} + {{- end }} +spec: + rules: + {{- range $host := .Values.ingress.hosts }} + - host: {{ $host }} + http: + paths: + - path: / + backend: + serviceName: {{ $serviceName }} + servicePort: {{ $servicePort }} + {{- end -}} + {{- if .Values.ingress.tls }} + tls: +{{ toYaml .Values.ingress.tls | indent 4 }} + {{- end -}} +{{- end -}} diff --git a/__fixtures__/v2/with-subfolder/templates/webserver/service.yaml b/__fixtures__/v2/with-subfolder/templates/webserver/service.yaml new file mode 100644 index 000000000..db0b551db --- /dev/null +++ b/__fixtures__/v2/with-subfolder/templates/webserver/service.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ template "with-subfolder.fullname" . }} + labels: + app: {{ template "with-subfolder.name" . }} + chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.externalPort }} + targetPort: {{ .Values.service.internalPort }} + protocol: TCP + name: {{ .Values.service.name }} + selector: + app: {{ template "with-subfolder.name" . }} + release: {{ .Release.Name }} diff --git a/__fixtures__/v2/with-subfolder/tests/__snapshot__/deployment_test.yaml.snap b/__fixtures__/v2/with-subfolder/tests/__snapshot__/deployment_test.yaml.snap new file mode 100644 index 000000000..d90e2b624 --- /dev/null +++ b/__fixtures__/v2/with-subfolder/tests/__snapshot__/deployment_test.yaml.snap @@ -0,0 +1,52 @@ +should pass all kinds of assertion for both deployments: + 1: | + replicas: 1 + template: + metadata: + annotations: + some_template: | + --- + apiVersion: ... + this: is test for old separator workaround bug + labels: + app: with-subfolder + release: RELEASE-NAME + spec: + containers: + - image: apache:latest + imagePullPolicy: Always + livenessProbe: + httpGet: + path: / + port: 80 + name: with-subfolder + ports: + - containerPort: null + readinessProbe: + httpGet: + path: / + port: 80 + resources: {} + 2: | + replicas: 1 + template: + metadata: + labels: + app: with-subfolder + release: RELEASE-NAME + spec: + containers: + - image: apache:latest + imagePullPolicy: Always + livenessProbe: + httpGet: + path: / + port: 80 + name: with-subfolder + ports: + - containerPort: 80 + readinessProbe: + httpGet: + path: / + port: 80 + resources: {} diff --git a/__fixtures__/v2/with-subfolder/tests/__snapshot__/notes_test.yaml.snap b/__fixtures__/v2/with-subfolder/tests/__snapshot__/notes_test.yaml.snap new file mode 100644 index 000000000..01d006a12 --- /dev/null +++ b/__fixtures__/v2/with-subfolder/tests/__snapshot__/notes_test.yaml.snap @@ -0,0 +1,20 @@ +should pass the notes file with ingress enabled: + 1: | + | + 1. Get the application URL by running these commands: + http://chart-example.local +should pass the notes file with service type LoadBalancer: + 1: | + | + 1. Get the application URL by running these commands: + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get svc -w RELEASE-NAME-with-subfolder' + export SERVICE_IP=$(kubectl get svc --namespace NAMESPACE RELEASE-NAME-with-subfolder -o jsonpath='{.status.loadBalancer.ingress[0].ip}') + echo http://$SERVICE_IP:9999 +should pass the notes file with service type NodePort: + 1: | + | + 1. Get the application URL by running these commands: + export NODE_PORT=$(kubectl get --namespace NAMESPACE -o jsonpath="{.spec.ports[0].nodePort}" services RELEASE-NAME-with-subfolder) + export NODE_IP=$(kubectl get nodes --namespace NAMESPACE -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT diff --git a/__fixtures__/v2/with-subfolder/tests/deployment_test.yaml b/__fixtures__/v2/with-subfolder/tests/deployment_test.yaml new file mode 100644 index 000000000..1bc9027d2 --- /dev/null +++ b/__fixtures__/v2/with-subfolder/tests/deployment_test.yaml @@ -0,0 +1,67 @@ +suite: test deployment +templates: + - db/deployment.yaml + - webserver/deployment.yaml +tests: + - it: should pass all kinds of assertion for both deployments + values: + - ./values/image.yaml + asserts: + - equal: + path: spec.template.spec.containers[0].image + value: apache:latest + - notEqual: + path: spec.template.spec.containers[0].image + value: nginx:stable + - matchRegex: + path: metadata.name + pattern: ^.*-subfolder.*$ + - notMatchRegex: + path: metadata.name + pattern: ^.*-foobar$ + - isNull: + path: spec.template.nodeSelector + - isNotNull: + path: spec.template + - isEmpty: + path: spec.template.spec.containers[0].resources + - isNotEmpty: + path: spec.template.spec.containers[0] + - isKind: + of: Deployment + - isAPIVersion: + of: extensions/v1beta1 + - hasDocuments: + count: 1 + - matchSnapshot: + path: spec + - it: should pass all kinds of assertion for webserver deployment + values: + - ./values/image.yaml + template: webserver/deployment.yaml + set: + service.internalPort: 8080 + asserts: + - contains: + path: spec.template.spec.containers[0].ports + content: + containerPort: 8080 + - notContains: + path: spec.template.spec.containers[0].ports + content: + containerPort: 80 + - it: should pass all kinds of assertion for db deployment + values: + - ./values/image.yaml + template: db/deployment.yaml + set: + service.dbPort: 8080 + asserts: + - contains: + path: spec.template.spec.containers[0].ports + content: + containerPort: 8080 + - notContains: + path: spec.template.spec.containers[0].ports + content: + containerPort: 80 diff --git a/__fixtures__/v2/with-subfolder/tests/ingress_test.yaml b/__fixtures__/v2/with-subfolder/tests/ingress_test.yaml new file mode 100644 index 000000000..a83dddb85 --- /dev/null +++ b/__fixtures__/v2/with-subfolder/tests/ingress_test.yaml @@ -0,0 +1,73 @@ +suite: test ingress +templates: + - webserver/ingress.yaml +tests: + - it: should render nothing if not enabled + asserts: + - hasDocuments: + count: 0 + + - it: should render Ingress right if enabled + set: + ingress.enabled: true + service.externalPort: 12345 + release: + name: my-release + asserts: + - hasDocuments: + count: 1 + - isKind: + of: Ingress + - contains: + path: spec.rules[0].http.paths + content: + path: / + backend: + serviceName: my-release-with-subfolder + servicePort: 12345 + - isNull: + path: spec.tls + + - it: should set annotations if given + set: + ingress.enabled: true + ingress.annotations: + kubernetes.io/ingress.class: nginx + kubernetes.io/tls-acme: "true" + ingress.kubernetes.io/rewrite-target: / + asserts: + - equal: + path: metadata.annotations + value: + kubernetes.io/ingress.class: nginx + kubernetes.io/tls-acme: "true" + ingress.kubernetes.io/rewrite-target: / + + - it: should set annotations if given and verify the specific values. + set: + ingress.enabled: true + ingress.annotations: + kubernetes.io/ingress.class: nginx + kubernetes.io/tls-acme: "true" + ingress.kubernetes.io/rewrite-target: / + asserts: + - equal: + path: metadata.annotations.[kubernetes.io/ingress.class] + value: nginx + - equal: + path: metadata.annotations.[kubernetes.io/tls-acme] + value: "true" + - equal: + path: metadata.annotations.[ingress.kubernetes.io/rewrite-target] + value: / + + - it: should set tls if given + set: + ingress.enabled: true + ingress.tls: + - secretName: my-tls-secret + asserts: + - equal: + path: spec.tls + value: + - secretName: my-tls-secret diff --git a/__fixtures__/v2/with-subfolder/tests/notes_test.yaml b/__fixtures__/v2/with-subfolder/tests/notes_test.yaml new file mode 100644 index 000000000..76e642863 --- /dev/null +++ b/__fixtures__/v2/with-subfolder/tests/notes_test.yaml @@ -0,0 +1,34 @@ +suite: test notes +templates: + - NOTES.txt +tests: + - it: should pass the notes file with ingress enabled + set: + ingress.enabled: true + asserts: + - equalRaw: + value: | + 1. Get the application URL by running these commands: + http://chart-example.local + - matchSnapshotRaw: {} + + - it: should pass the notes file with service type NodePort + set: + service.type: NodePort + asserts: + - equalRaw: + value: | + 1. Get the application URL by running these commands: + export NODE_PORT=$(kubectl get --namespace NAMESPACE -o jsonpath="{.spec.ports[0].nodePort}" services RELEASE-NAME-with-subfolder) + export NODE_IP=$(kubectl get nodes --namespace NAMESPACE -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT + - matchSnapshotRaw: {} + + - it: should pass the notes file with service type LoadBalancer + set: + service.type: LoadBalancer + service.externalPort: 9999 + asserts: + - matchRegexRaw: + pattern: http://\$SERVICE_IP:9999 + - matchSnapshotRaw: {} diff --git a/__fixtures__/v2/with-subfolder/tests/service_test.yaml b/__fixtures__/v2/with-subfolder/tests/service_test.yaml new file mode 100644 index 000000000..bd39d27af --- /dev/null +++ b/__fixtures__/v2/with-subfolder/tests/service_test.yaml @@ -0,0 +1,42 @@ +suite: test service +templates: + - webserver/service.yaml +tests: + - it: should pass + release: + name: my-release + asserts: + - contains: + path: spec.ports + content: + port: 80 + targetPort: 80 + protocol: TCP + name: nginx + - equal: + path: spec.type + value: ClusterIP + - equal: + path: spec.selector + value: + app: with-subfolder + release: my-release + + - it: should render right if values given + set: + service: + type: NodePort + internalPort: 1234 + externalPort: 4321 + name: cool-service + asserts: + - contains: + path: spec.ports + content: + port: 4321 + targetPort: 1234 + protocol: TCP + name: cool-service + - equal: + path: spec.type + value: NodePort diff --git a/__fixtures__/v2/with-subfolder/tests/values/image.yaml b/__fixtures__/v2/with-subfolder/tests/values/image.yaml new file mode 100644 index 000000000..2514e3940 --- /dev/null +++ b/__fixtures__/v2/with-subfolder/tests/values/image.yaml @@ -0,0 +1,4 @@ +image: + repository: apache + tag: latest + pullPolicy: Always diff --git a/__fixtures__/v2/with-subfolder/values.yaml b/__fixtures__/v2/with-subfolder/values.yaml new file mode 100644 index 000000000..45d1817af --- /dev/null +++ b/__fixtures__/v2/with-subfolder/values.yaml @@ -0,0 +1,38 @@ +# Default values for basic. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. +replicaCount: 1 +image: + repository: nginx + tag: stable + pullPolicy: IfNotPresent +service: + name: nginx + type: ClusterIP + externalPort: 80 + internalPort: 80 +ingress: + class: foo + enabled: false + # Used to create an Ingress record. + hosts: + - chart-example.local + annotations: + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + tls: + # Secrets must be manually created in the namespace. + # - secretName: chart-example-tls + # hosts: + # - chart-example.local +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi diff --git a/__fixtures__/v3/basic/tests/__snapshot__/notes_test.yaml.snap b/__fixtures__/v3/basic/tests/__snapshot__/notes_test.yaml.snap new file mode 100644 index 000000000..2ca380109 --- /dev/null +++ b/__fixtures__/v3/basic/tests/__snapshot__/notes_test.yaml.snap @@ -0,0 +1,20 @@ +should pass the notes file with ingress enabled: + 1: | + | + 1. Get the application URL by running these commands: + http://chart-example.local +should pass the notes file with service type LoadBalancer: + 1: | + | + 1. Get the application URL by running these commands: + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get svc -w RELEASE-NAME-basic' + export SERVICE_IP=$(kubectl get svc --namespace NAMESPACE RELEASE-NAME-basic -o jsonpath='{.status.loadBalancer.ingress[0].ip}') + echo http://$SERVICE_IP:9999 +should pass the notes file with service type NodePort: + 1: | + | + 1. Get the application URL by running these commands: + export NODE_PORT=$(kubectl get --namespace NAMESPACE -o jsonpath="{.spec.ports[0].nodePort}" services RELEASE-NAME-basic) + export NODE_IP=$(kubectl get nodes --namespace NAMESPACE -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT diff --git a/__fixtures__/v3/basic/tests/notes_test.yaml b/__fixtures__/v3/basic/tests/notes_test.yaml new file mode 100644 index 000000000..3e950e9d6 --- /dev/null +++ b/__fixtures__/v3/basic/tests/notes_test.yaml @@ -0,0 +1,35 @@ +suite: test notes +templates: + - NOTES.txt +tests: + - it: should pass the notes file with ingress enabled + set: + ingress.enabled: true + asserts: + - equalRaw: + value: | + 1. Get the application URL by running these commands: + http://chart-example.local + - matchSnapshotRaw: {} + + - it: should pass the notes file with service type NodePort + set: + service.type: NodePort + asserts: + - equalRaw: + value: | + 1. Get the application URL by running these commands: + export NODE_PORT=$(kubectl get --namespace NAMESPACE -o jsonpath="{.spec.ports[0].nodePort}" services RELEASE-NAME-basic) + export NODE_IP=$(kubectl get nodes --namespace NAMESPACE -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT + - matchSnapshotRaw: {} + + - it: should pass the notes file with service type LoadBalancer + set: + service.type: LoadBalancer + service.externalPort: 9999 + asserts: + - matchRegexRaw: + pattern: http://\$SERVICE_IP:9999 + - matchSnapshotRaw: {} + \ No newline at end of file diff --git a/__fixtures__/v3/basic/tests_failed/notes_test.yaml b/__fixtures__/v3/basic/tests_failed/notes_test.yaml new file mode 100644 index 000000000..2c6cbccf5 --- /dev/null +++ b/__fixtures__/v3/basic/tests_failed/notes_test.yaml @@ -0,0 +1,31 @@ +suite: test notes +templates: + - NOTES.txt +tests: + - it: should pass the notes file with ingress enabled + set: + ingress.enabled: true + asserts: + - notEqualRaw: + value: | + 1. Get the application URL by running these commands: + http://chart-example.local + + - it: should pass the notes file with service type NodePort + set: + service.type: NodePort + asserts: + - notEqualRaw: + value: | + 1. Get the application URL by running these commands: + export NODE_PORT=$(kubectl get --namespace NAMESPACE -o jsonpath="{.spec.ports[0].nodePort}" services MY-RELEASE) + export NODE_IP=$(kubectl get nodes --namespace NAMESPACE -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT + + - it: should pass the notes file with service type LoadBalancer + set: + service.type: LoadBalancer + service.externalPort: 9999 + asserts: + - matchRegexRaw: + pattern: http://\$SERVICE_IP:80 diff --git a/__fixtures__/v3/with-subchart/charts/child-chart/tests/__snapshot__/notes_test.yaml.snap b/__fixtures__/v3/with-subchart/charts/child-chart/tests/__snapshot__/notes_test.yaml.snap new file mode 100644 index 000000000..95055e673 --- /dev/null +++ b/__fixtures__/v3/with-subchart/charts/child-chart/tests/__snapshot__/notes_test.yaml.snap @@ -0,0 +1,20 @@ +should pass the notes file with ingress enabled: + 1: | + | + 1. Get the application URL by running these commands: + http://chart-example.local +should pass the notes file with service type LoadBalancer: + 1: | + | + 1. Get the application URL by running these commands: + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get svc -w RELEASE-NAME-child-chart' + export SERVICE_IP=$(kubectl get svc --namespace NAMESPACE RELEASE-NAME-child-chart -o jsonpath='{.status.loadBalancer.ingress[0].ip}') + echo http://$SERVICE_IP:9999 +should pass the notes file with service type NodePort: + 1: | + | + 1. Get the application URL by running these commands: + export NODE_PORT=$(kubectl get --namespace NAMESPACE -o jsonpath="{.spec.ports[0].nodePort}" services RELEASE-NAME-child-chart) + export NODE_IP=$(kubectl get nodes --namespace NAMESPACE -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT diff --git a/__fixtures__/v3/with-subchart/charts/child-chart/tests/notes_test.yaml b/__fixtures__/v3/with-subchart/charts/child-chart/tests/notes_test.yaml new file mode 100644 index 000000000..dafb3ca0d --- /dev/null +++ b/__fixtures__/v3/with-subchart/charts/child-chart/tests/notes_test.yaml @@ -0,0 +1,35 @@ +suite: test notes +templates: + - NOTES.txt +tests: + - it: should pass the notes file with ingress enabled + set: + ingress.enabled: true + asserts: + - equalRaw: + value: | + 1. Get the application URL by running these commands: + http://chart-example.local + - matchSnapshotRaw: {} + + - it: should pass the notes file with service type NodePort + set: + service.type: NodePort + asserts: + - equalRaw: + value: | + 1. Get the application URL by running these commands: + export NODE_PORT=$(kubectl get --namespace NAMESPACE -o jsonpath="{.spec.ports[0].nodePort}" services RELEASE-NAME-child-chart) + export NODE_IP=$(kubectl get nodes --namespace NAMESPACE -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT + - matchSnapshotRaw: {} + + - it: should pass the notes file with service type LoadBalancer + set: + service.type: LoadBalancer + service.externalPort: 9999 + asserts: + - matchRegexRaw: + pattern: http://\$SERVICE_IP:9999 + - matchSnapshotRaw: {} + \ No newline at end of file diff --git a/__fixtures__/v3/with-subchart/templates/NOTES.txt b/__fixtures__/v3/with-subchart/templates/NOTES.txt index 30e62529b..f0cd1009c 100644 --- a/__fixtures__/v3/with-subchart/templates/NOTES.txt +++ b/__fixtures__/v3/with-subchart/templates/NOTES.txt @@ -4,16 +4,16 @@ http://{{ . }} {{- end }} {{- else if contains "NodePort" .Values.service.type }} - export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ template "parant-chart.fullname" . }}) + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ template "parent-chart.fullname" . }}) export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") echo http://$NODE_IP:$NODE_PORT {{- else if contains "LoadBalancer" .Values.service.type }} NOTE: It may take a few minutes for the LoadBalancer IP to be available. - You can watch the status of by running 'kubectl get svc -w {{ template "parant-chart.fullname" . }}' - export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ template "parant-chart.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}') + You can watch the status of by running 'kubectl get svc -w {{ template "parent-chart.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ template "parent-chart.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}') echo http://$SERVICE_IP:{{ .Values.service.externalPort }} {{- else if contains "ClusterIP" .Values.service.type }} - export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app={{ template "parant-chart.name" . }},release={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app={{ template "parent-chart.name" . }},release={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") echo "Visit http://127.0.0.1:8080 to use your application" kubectl port-forward $POD_NAME 8080:{{ .Values.service.internalPort }} {{- end }} diff --git a/__fixtures__/v3/with-subchart/tests/__snapshot__/notes_test.yaml.snap b/__fixtures__/v3/with-subchart/tests/__snapshot__/notes_test.yaml.snap new file mode 100644 index 000000000..1a2110db0 --- /dev/null +++ b/__fixtures__/v3/with-subchart/tests/__snapshot__/notes_test.yaml.snap @@ -0,0 +1,20 @@ +should pass the notes file with ingress enabled: + 1: | + | + 1. Get the application URL by running these commands: + http://chart-example.local +should pass the notes file with service type LoadBalancer: + 1: | + | + 1. Get the application URL by running these commands: + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get svc -w RELEASE-NAME-parent-chart' + export SERVICE_IP=$(kubectl get svc --namespace NAMESPACE RELEASE-NAME-parent-chart -o jsonpath='{.status.loadBalancer.ingress[0].ip}') + echo http://$SERVICE_IP:9999 +should pass the notes file with service type NodePort: + 1: | + | + 1. Get the application URL by running these commands: + export NODE_PORT=$(kubectl get --namespace NAMESPACE -o jsonpath="{.spec.ports[0].nodePort}" services RELEASE-NAME-parent-chart) + export NODE_IP=$(kubectl get nodes --namespace NAMESPACE -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT diff --git a/__fixtures__/v3/with-subchart/tests/notes_test.yaml b/__fixtures__/v3/with-subchart/tests/notes_test.yaml new file mode 100644 index 000000000..8a34e865c --- /dev/null +++ b/__fixtures__/v3/with-subchart/tests/notes_test.yaml @@ -0,0 +1,35 @@ +suite: test notes +templates: + - NOTES.txt +tests: + - it: should pass the notes file with ingress enabled + set: + ingress.enabled: true + asserts: + - equalRaw: + value: | + 1. Get the application URL by running these commands: + http://chart-example.local + - matchSnapshotRaw: {} + + - it: should pass the notes file with service type NodePort + set: + service.type: NodePort + asserts: + - equalRaw: + value: | + 1. Get the application URL by running these commands: + export NODE_PORT=$(kubectl get --namespace NAMESPACE -o jsonpath="{.spec.ports[0].nodePort}" services RELEASE-NAME-parent-chart) + export NODE_IP=$(kubectl get nodes --namespace NAMESPACE -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT + - matchSnapshotRaw: {} + + - it: should pass the notes file with service type LoadBalancer + set: + service.type: LoadBalancer + service.externalPort: 9999 + asserts: + - matchRegexRaw: + pattern: http://\$SERVICE_IP:9999 + - matchSnapshotRaw: {} + \ No newline at end of file diff --git a/__fixtures__/v3/with-subfolder/.helmignore b/__fixtures__/v3/with-subfolder/.helmignore new file mode 100644 index 000000000..dadf20295 --- /dev/null +++ b/__fixtures__/v3/with-subfolder/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj + +tests diff --git a/__fixtures__/v3/with-subfolder/Chart.yaml b/__fixtures__/v3/with-subfolder/Chart.yaml new file mode 100644 index 000000000..49d878e22 --- /dev/null +++ b/__fixtures__/v3/with-subfolder/Chart.yaml @@ -0,0 +1,4 @@ +apiVersion: v2 +description: A advanced example chart to demonstrate unittest plugin +name: with-subfolder +version: 0.1.0 diff --git a/__fixtures__/v3/with-subfolder/templates/NOTES.txt b/__fixtures__/v3/with-subfolder/templates/NOTES.txt new file mode 100644 index 000000000..f3c81c022 --- /dev/null +++ b/__fixtures__/v3/with-subfolder/templates/NOTES.txt @@ -0,0 +1,19 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range .Values.ingress.hosts }} + http://{{ . }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ template "with-subfolder.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get svc -w {{ template "with-subfolder.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ template "with-subfolder.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}') + echo http://$SERVICE_IP:{{ .Values.service.externalPort }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app={{ template "with-subfolder.name" . }},release={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl port-forward $POD_NAME 8080:{{ .Values.service.internalPort }} +{{- end }} diff --git a/__fixtures__/v3/with-subfolder/templates/_helpers.tpl b/__fixtures__/v3/with-subfolder/templates/_helpers.tpl new file mode 100644 index 000000000..21c49b484 --- /dev/null +++ b/__fixtures__/v3/with-subfolder/templates/_helpers.tpl @@ -0,0 +1,16 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Expand the name of the chart. +*/}} +{{- define "with-subfolder.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +*/}} +{{- define "with-subfolder.fullname" -}} +{{- $name := default .Chart.Name .Values.nameOverride -}} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} diff --git a/__fixtures__/v3/with-subfolder/templates/db/deployment.yaml b/__fixtures__/v3/with-subfolder/templates/db/deployment.yaml new file mode 100644 index 000000000..054e55003 --- /dev/null +++ b/__fixtures__/v3/with-subfolder/templates/db/deployment.yaml @@ -0,0 +1,42 @@ +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: {{ template "with-subfolder.fullname" . }}-db + labels: + app: {{ template "with-subfolder.name" . }} + chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +spec: + replicas: 1 + template: + metadata: + labels: + app: {{ template "with-subfolder.name" . }} + release: {{ .Release.Name }} + annotations: + some_template: | + --- + apiVersion: ... + this: is test for old separator workaround bug + spec: + containers: + - name: {{ .Chart.Name }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - containerPort: {{ .Values.service.dbPort }} + livenessProbe: + httpGet: + path: / + port: {{ .Values.service.internalPort }} + readinessProbe: + httpGet: + path: / + port: {{ .Values.service.internalPort }} + resources: +{{ toYaml .Values.resources | indent 12 }} + {{- if .Values.nodeSelector }} + nodeSelector: +{{ toYaml .Values.nodeSelector | indent 8 }} + {{- end }} diff --git a/__fixtures__/v3/with-subfolder/templates/webserver/deployment.yaml b/__fixtures__/v3/with-subfolder/templates/webserver/deployment.yaml new file mode 100644 index 000000000..bf8a0d155 --- /dev/null +++ b/__fixtures__/v3/with-subfolder/templates/webserver/deployment.yaml @@ -0,0 +1,38 @@ +--- +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: {{ template "with-subfolder.fullname" . }} + labels: + app: {{ template "with-subfolder.name" . }} + chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +spec: + replicas: {{ .Values.replicaCount }} + template: + metadata: + labels: + app: {{ template "with-subfolder.name" . }} + release: {{ .Release.Name }} + spec: + containers: + - name: {{ .Chart.Name }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - containerPort: {{ .Values.service.internalPort }} + livenessProbe: + httpGet: + path: / + port: {{ .Values.service.internalPort }} + readinessProbe: + httpGet: + path: / + port: {{ .Values.service.internalPort }} + resources: +{{ toYaml .Values.resources | indent 12 }} + {{- if .Values.nodeSelector }} + nodeSelector: +{{ toYaml .Values.nodeSelector | indent 8 }} + {{- end }} diff --git a/__fixtures__/v3/with-subfolder/templates/webserver/ingress.yaml b/__fixtures__/v3/with-subfolder/templates/webserver/ingress.yaml new file mode 100644 index 000000000..5f48d8e1c --- /dev/null +++ b/__fixtures__/v3/with-subfolder/templates/webserver/ingress.yaml @@ -0,0 +1,32 @@ +{{- if .Values.ingress.enabled -}} +{{- $serviceName := include "with-subfolder.fullname" . -}} +{{- $servicePort := .Values.service.externalPort -}} +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: {{ template "with-subfolder.fullname" . }} + labels: + app: {{ template "with-subfolder.name" . }} + chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} + annotations: + {{- range $key, $value := .Values.ingress.annotations }} + {{ $key }}: {{ $value | quote }} + {{- end }} +spec: + rules: + {{- range $host := .Values.ingress.hosts }} + - host: {{ $host }} + http: + paths: + - path: / + backend: + serviceName: {{ $serviceName }} + servicePort: {{ $servicePort }} + {{- end -}} + {{- if .Values.ingress.tls }} + tls: +{{ toYaml .Values.ingress.tls | indent 4 }} + {{- end -}} +{{- end -}} diff --git a/__fixtures__/v3/with-subfolder/templates/webserver/service.yaml b/__fixtures__/v3/with-subfolder/templates/webserver/service.yaml new file mode 100644 index 000000000..db0b551db --- /dev/null +++ b/__fixtures__/v3/with-subfolder/templates/webserver/service.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ template "with-subfolder.fullname" . }} + labels: + app: {{ template "with-subfolder.name" . }} + chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.externalPort }} + targetPort: {{ .Values.service.internalPort }} + protocol: TCP + name: {{ .Values.service.name }} + selector: + app: {{ template "with-subfolder.name" . }} + release: {{ .Release.Name }} diff --git a/__fixtures__/v3/with-subfolder/tests/__snapshot__/deployment_test.yaml.snap b/__fixtures__/v3/with-subfolder/tests/__snapshot__/deployment_test.yaml.snap new file mode 100644 index 000000000..d90e2b624 --- /dev/null +++ b/__fixtures__/v3/with-subfolder/tests/__snapshot__/deployment_test.yaml.snap @@ -0,0 +1,52 @@ +should pass all kinds of assertion for both deployments: + 1: | + replicas: 1 + template: + metadata: + annotations: + some_template: | + --- + apiVersion: ... + this: is test for old separator workaround bug + labels: + app: with-subfolder + release: RELEASE-NAME + spec: + containers: + - image: apache:latest + imagePullPolicy: Always + livenessProbe: + httpGet: + path: / + port: 80 + name: with-subfolder + ports: + - containerPort: null + readinessProbe: + httpGet: + path: / + port: 80 + resources: {} + 2: | + replicas: 1 + template: + metadata: + labels: + app: with-subfolder + release: RELEASE-NAME + spec: + containers: + - image: apache:latest + imagePullPolicy: Always + livenessProbe: + httpGet: + path: / + port: 80 + name: with-subfolder + ports: + - containerPort: 80 + readinessProbe: + httpGet: + path: / + port: 80 + resources: {} diff --git a/__fixtures__/v3/with-subfolder/tests/__snapshot__/notes_test.yaml.snap b/__fixtures__/v3/with-subfolder/tests/__snapshot__/notes_test.yaml.snap new file mode 100644 index 000000000..01d006a12 --- /dev/null +++ b/__fixtures__/v3/with-subfolder/tests/__snapshot__/notes_test.yaml.snap @@ -0,0 +1,20 @@ +should pass the notes file with ingress enabled: + 1: | + | + 1. Get the application URL by running these commands: + http://chart-example.local +should pass the notes file with service type LoadBalancer: + 1: | + | + 1. Get the application URL by running these commands: + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get svc -w RELEASE-NAME-with-subfolder' + export SERVICE_IP=$(kubectl get svc --namespace NAMESPACE RELEASE-NAME-with-subfolder -o jsonpath='{.status.loadBalancer.ingress[0].ip}') + echo http://$SERVICE_IP:9999 +should pass the notes file with service type NodePort: + 1: | + | + 1. Get the application URL by running these commands: + export NODE_PORT=$(kubectl get --namespace NAMESPACE -o jsonpath="{.spec.ports[0].nodePort}" services RELEASE-NAME-with-subfolder) + export NODE_IP=$(kubectl get nodes --namespace NAMESPACE -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT diff --git a/__fixtures__/v3/with-subfolder/tests/deployment_test.yaml b/__fixtures__/v3/with-subfolder/tests/deployment_test.yaml new file mode 100644 index 000000000..1bc9027d2 --- /dev/null +++ b/__fixtures__/v3/with-subfolder/tests/deployment_test.yaml @@ -0,0 +1,67 @@ +suite: test deployment +templates: + - db/deployment.yaml + - webserver/deployment.yaml +tests: + - it: should pass all kinds of assertion for both deployments + values: + - ./values/image.yaml + asserts: + - equal: + path: spec.template.spec.containers[0].image + value: apache:latest + - notEqual: + path: spec.template.spec.containers[0].image + value: nginx:stable + - matchRegex: + path: metadata.name + pattern: ^.*-subfolder.*$ + - notMatchRegex: + path: metadata.name + pattern: ^.*-foobar$ + - isNull: + path: spec.template.nodeSelector + - isNotNull: + path: spec.template + - isEmpty: + path: spec.template.spec.containers[0].resources + - isNotEmpty: + path: spec.template.spec.containers[0] + - isKind: + of: Deployment + - isAPIVersion: + of: extensions/v1beta1 + - hasDocuments: + count: 1 + - matchSnapshot: + path: spec + - it: should pass all kinds of assertion for webserver deployment + values: + - ./values/image.yaml + template: webserver/deployment.yaml + set: + service.internalPort: 8080 + asserts: + - contains: + path: spec.template.spec.containers[0].ports + content: + containerPort: 8080 + - notContains: + path: spec.template.spec.containers[0].ports + content: + containerPort: 80 + - it: should pass all kinds of assertion for db deployment + values: + - ./values/image.yaml + template: db/deployment.yaml + set: + service.dbPort: 8080 + asserts: + - contains: + path: spec.template.spec.containers[0].ports + content: + containerPort: 8080 + - notContains: + path: spec.template.spec.containers[0].ports + content: + containerPort: 80 diff --git a/__fixtures__/v3/with-subfolder/tests/ingress_test.yaml b/__fixtures__/v3/with-subfolder/tests/ingress_test.yaml new file mode 100644 index 000000000..a83dddb85 --- /dev/null +++ b/__fixtures__/v3/with-subfolder/tests/ingress_test.yaml @@ -0,0 +1,73 @@ +suite: test ingress +templates: + - webserver/ingress.yaml +tests: + - it: should render nothing if not enabled + asserts: + - hasDocuments: + count: 0 + + - it: should render Ingress right if enabled + set: + ingress.enabled: true + service.externalPort: 12345 + release: + name: my-release + asserts: + - hasDocuments: + count: 1 + - isKind: + of: Ingress + - contains: + path: spec.rules[0].http.paths + content: + path: / + backend: + serviceName: my-release-with-subfolder + servicePort: 12345 + - isNull: + path: spec.tls + + - it: should set annotations if given + set: + ingress.enabled: true + ingress.annotations: + kubernetes.io/ingress.class: nginx + kubernetes.io/tls-acme: "true" + ingress.kubernetes.io/rewrite-target: / + asserts: + - equal: + path: metadata.annotations + value: + kubernetes.io/ingress.class: nginx + kubernetes.io/tls-acme: "true" + ingress.kubernetes.io/rewrite-target: / + + - it: should set annotations if given and verify the specific values. + set: + ingress.enabled: true + ingress.annotations: + kubernetes.io/ingress.class: nginx + kubernetes.io/tls-acme: "true" + ingress.kubernetes.io/rewrite-target: / + asserts: + - equal: + path: metadata.annotations.[kubernetes.io/ingress.class] + value: nginx + - equal: + path: metadata.annotations.[kubernetes.io/tls-acme] + value: "true" + - equal: + path: metadata.annotations.[ingress.kubernetes.io/rewrite-target] + value: / + + - it: should set tls if given + set: + ingress.enabled: true + ingress.tls: + - secretName: my-tls-secret + asserts: + - equal: + path: spec.tls + value: + - secretName: my-tls-secret diff --git a/__fixtures__/v3/with-subfolder/tests/notes_test.yaml b/__fixtures__/v3/with-subfolder/tests/notes_test.yaml new file mode 100644 index 000000000..2fc6e2808 --- /dev/null +++ b/__fixtures__/v3/with-subfolder/tests/notes_test.yaml @@ -0,0 +1,32 @@ +suite: test notes +templates: + - NOTES.txt +tests: + - it: should pass the notes file with ingress enabled + set: + ingress.enabled: true + asserts: + - equalRaw: + value: | + 1. Get the application URL by running these commands: + http://chart-example.local + - matchSnapshotRaw: {} + - it: should pass the notes file with service type NodePort + set: + service.type: NodePort + asserts: + - equalRaw: + value: | + 1. Get the application URL by running these commands: + export NODE_PORT=$(kubectl get --namespace NAMESPACE -o jsonpath="{.spec.ports[0].nodePort}" services RELEASE-NAME-with-subfolder) + export NODE_IP=$(kubectl get nodes --namespace NAMESPACE -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT + - matchSnapshotRaw: {} + - it: should pass the notes file with service type LoadBalancer + set: + service.type: LoadBalancer + service.externalPort: 9999 + asserts: + - matchRegexRaw: + pattern: http://\$SERVICE_IP:9999 + - matchSnapshotRaw: {} diff --git a/__fixtures__/v3/with-subfolder/tests/service_test.yaml b/__fixtures__/v3/with-subfolder/tests/service_test.yaml new file mode 100644 index 000000000..bd39d27af --- /dev/null +++ b/__fixtures__/v3/with-subfolder/tests/service_test.yaml @@ -0,0 +1,42 @@ +suite: test service +templates: + - webserver/service.yaml +tests: + - it: should pass + release: + name: my-release + asserts: + - contains: + path: spec.ports + content: + port: 80 + targetPort: 80 + protocol: TCP + name: nginx + - equal: + path: spec.type + value: ClusterIP + - equal: + path: spec.selector + value: + app: with-subfolder + release: my-release + + - it: should render right if values given + set: + service: + type: NodePort + internalPort: 1234 + externalPort: 4321 + name: cool-service + asserts: + - contains: + path: spec.ports + content: + port: 4321 + targetPort: 1234 + protocol: TCP + name: cool-service + - equal: + path: spec.type + value: NodePort diff --git a/__fixtures__/v3/with-subfolder/tests/values/image.yaml b/__fixtures__/v3/with-subfolder/tests/values/image.yaml new file mode 100644 index 000000000..2514e3940 --- /dev/null +++ b/__fixtures__/v3/with-subfolder/tests/values/image.yaml @@ -0,0 +1,4 @@ +image: + repository: apache + tag: latest + pullPolicy: Always diff --git a/__fixtures__/v3/with-subfolder/values.yaml b/__fixtures__/v3/with-subfolder/values.yaml new file mode 100644 index 000000000..45d1817af --- /dev/null +++ b/__fixtures__/v3/with-subfolder/values.yaml @@ -0,0 +1,38 @@ +# Default values for basic. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. +replicaCount: 1 +image: + repository: nginx + tag: stable + pullPolicy: IfNotPresent +service: + name: nginx + type: ClusterIP + externalPort: 80 + internalPort: 80 +ingress: + class: foo + enabled: false + # Used to create an Ingress record. + hosts: + - chart-example.local + annotations: + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + tls: + # Secrets must be manually created in the namespace. + # - secretName: chart-example-tls + # hosts: + # - chart-example.local +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi diff --git a/unittest/.snapshots/TestV2RunJobWithInvalidValuesFile b/unittest/.snapshots/TestV2RunJobWithInvalidValuesFile deleted file mode 100644 index 2f40ecf1f..000000000 --- a/unittest/.snapshots/TestV2RunJobWithInvalidValuesFile +++ /dev/null @@ -1,30 +0,0 @@ -(*unittest.TestJobResult)({ - DisplayName: (string) (len=11) "should work", - Index: (int) 0, - Passed: (bool) false, - ExecError: (error) , - AssertsResult: ([]*unittest.AssertionResult) (len=1) { - (*unittest.AssertionResult)({ - Index: (int) 0, - FailInfo: ([]string) (len=12) { - (string) (len=41) "Template:\tbasic/templates/deployment.yaml", - (string) (len=19) "Path:\tmetadata.name", - (string) (len=9) "Expected:", - (string) (len=23) "\tRELEASE-NAME-mary-jane", - (string) (len=7) "Actual:", - (string) (len=19) "\tRELEASE-NAME-basic", - (string) (len=5) "Diff:", - (string) (len=13) "\t--- Expected", - (string) (len=11) "\t+++ Actual", - (string) (len=16) "\t@@ -1,2 +1,2 @@", - (string) (len=24) "\t-RELEASE-NAME-mary-jane", - (string) (len=20) "\t+RELEASE-NAME-basic" - }, - Passed: (bool) false, - AssertType: (string) (len=5) "equal", - Not: (bool) false, - CustomInfo: (string) "" - }) - }, - Duration: (time.Duration) 0s -}) diff --git a/unittest/.snapshots/TestV2RunJobWithNOTESTemplateOk b/unittest/.snapshots/TestV2RunJobWithNOTESTemplateOk new file mode 100644 index 000000000..6339785e5 --- /dev/null +++ b/unittest/.snapshots/TestV2RunJobWithNOTESTemplateOk @@ -0,0 +1,27 @@ +(*unittest.TestJobResult)({ + DisplayName: (string) (len=11) "should work", + Index: (int) 0, + Passed: (bool) true, + ExecError: (error) , + AssertsResult: ([]*unittest.AssertionResult) (len=2) { + (*unittest.AssertionResult)({ + Index: (int) 0, + FailInfo: ([]string) { + }, + Passed: (bool) true, + AssertType: (string) (len=8) "equalRaw", + Not: (bool) false, + CustomInfo: (string) "" + }), + (*unittest.AssertionResult)({ + Index: (int) 1, + FailInfo: ([]string) { + }, + Passed: (bool) true, + AssertType: (string) (len=13) "matchRegexRaw", + Not: (bool) false, + CustomInfo: (string) "" + }) + }, + Duration: (time.Duration) 0s +}) diff --git a/unittest/.snapshots/TestV2RunSuiteWithSubfolderWhenPass b/unittest/.snapshots/TestV2RunSuiteWithSubfolderWhenPass new file mode 100644 index 000000000..50470ac56 --- /dev/null +++ b/unittest/.snapshots/TestV2RunSuiteWithSubfolderWhenPass @@ -0,0 +1,41 @@ +(*unittest.TestSuiteResult)({ + DisplayName: (string) (len=15) "test suite name", + FilePath: (string) "", + Passed: (bool) true, + ExecError: (error) , + TestsResult: ([]*unittest.TestJobResult) (len=1) { + (*unittest.TestJobResult)({ + DisplayName: (string) (len=11) "should pass", + Index: (int) 0, + Passed: (bool) true, + ExecError: (error) , + AssertsResult: ([]*unittest.AssertionResult) (len=2) { + (*unittest.AssertionResult)({ + Index: (int) 0, + FailInfo: ([]string) { + }, + Passed: (bool) true, + AssertType: (string) (len=5) "equal", + Not: (bool) false, + CustomInfo: (string) "" + }), + (*unittest.AssertionResult)({ + Index: (int) 1, + FailInfo: ([]string) { + }, + Passed: (bool) true, + AssertType: (string) (len=13) "matchSnapshot", + Not: (bool) false, + CustomInfo: (string) "" + }) + }, + Duration: (time.Duration) 0s + }) + }, + SnapshotCounting: (struct { Total uint; Failed uint; Created uint; Vanished uint }) { + Total: (uint) 2, + Failed: (uint) 0, + Created: (uint) 2, + Vanished: (uint) 0 + } +}) diff --git a/unittest/.snapshots/TestV2RunnerOkWithFailedTests b/unittest/.snapshots/TestV2RunnerOkWithFailedTests index a0c095428..076c8eef1 100644 --- a/unittest/.snapshots/TestV2RunnerOkWithFailedTests +++ b/unittest/.snapshots/TestV2RunnerOkWithFailedTests @@ -279,6 +279,28 @@ @@ -1,2 +1,2 @@ -- secretName: my-tls-secret +null + FAIL test notes ../__fixtures__/v2/basic/tests_failed/notes_test.yaml + - should pass the notes file with ingress enabled + + - asserts[0] `notEqualRaw` fail + + Template: basic/templates/NOTES.txt + Expected NOT to equal: + | + 1. Get the application URL by running these commands: + http://chart-example.local + + - should pass the notes file with service type LoadBalancer + + - asserts[0] `matchRegexRaw` fail + + Template: basic/templates/NOTES.txt + Expected to match: http:///$SERVICE_IP:80 + Actual: 1. Get the application URL by running these commands: + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get svc -w RELEASE-NAME-basic' + export SERVICE_IP=$(kubectl get svc --namespace NAMESPACE RELEASE-NAME-basic -o jsonpath='{.status.loadBalancer.ingress[0].ip}') + echo http://$SERVICE_IP:9999 FAIL test service ../__fixtures__/v2/basic/tests_failed/service_test.yaml - should failed @@ -344,8 +366,8 @@ Charts: 1 failed, 0 passed, 1 total -Test Suites: 3 failed, 0 passed, 3 total -Tests: 7 failed, 0 passed, 7 total +Test Suites: 4 failed, 0 passed, 4 total +Tests: 9 failed, 1 passed, 10 total Snapshot: 2 passed, 2 total Time: XX.XXXms diff --git a/unittest/.snapshots/TestV2RunnerOkWithPassedTests b/unittest/.snapshots/TestV2RunnerOkWithPassedTests index 8ec871a67..03e1af958 100644 --- a/unittest/.snapshots/TestV2RunnerOkWithPassedTests +++ b/unittest/.snapshots/TestV2RunnerOkWithPassedTests @@ -4,13 +4,14 @@ PASS test deployment ../__fixtures__/v2/basic/tests/deployment_test.yaml PASS test ingress ../__fixtures__/v2/basic/tests/ingress_test.yaml + PASS test notes ../__fixtures__/v2/basic/tests/notes_test.yaml PASS test service ../__fixtures__/v2/basic/tests/service_test.yaml Charts: 1 passed, 1 total -Test Suites: 3 passed, 3 total -Tests: 8 passed, 8 total -Snapshot: 1 passed, 1 total +Test Suites: 4 passed, 4 total +Tests: 11 passed, 11 total +Snapshot: 4 passed, 4 total Time: XX.XXXms diff --git a/unittest/.snapshots/TestV2RunnerOkWithSubSubfolder b/unittest/.snapshots/TestV2RunnerOkWithSubSubfolder new file mode 100644 index 000000000..b61c06743 --- /dev/null +++ b/unittest/.snapshots/TestV2RunnerOkWithSubSubfolder @@ -0,0 +1,17 @@ + +### Chart [ with-subfolder ] ../__fixtures__/v2/with-subfolder + + + PASS test deployment ../__fixtures__/v2/with-subfolder/tests/deployment_test.yaml + PASS test ingress ../__fixtures__/v2/with-subfolder/tests/ingress_test.yaml + PASS test notes ../__fixtures__/v2/with-subfolder/tests/notes_test.yaml + PASS test service ../__fixtures__/v2/with-subfolder/tests/service_test.yaml + + +Charts: 1 passed, 1 total +Test Suites: 4 passed, 4 total +Tests: 13 passed, 13 total +Snapshot: 5 passed, 5 total +Time: XX.XXXms + + diff --git a/unittest/.snapshots/TestV2RunnerWithTestsInSubchart b/unittest/.snapshots/TestV2RunnerWithTestsInSubchart index bdaee8026..4427d9403 100644 --- a/unittest/.snapshots/TestV2RunnerWithTestsInSubchart +++ b/unittest/.snapshots/TestV2RunnerWithTestsInSubchart @@ -6,14 +6,16 @@ PASS test deployment ../__fixtures__/v2/with-subchart/tests/deployment_test.yaml PASS test ingress ../__fixtures__/v2/with-subchart/charts/child-chart/tests/ingress_test.yaml PASS test ingress ../__fixtures__/v2/with-subchart/tests/ingress_test.yaml + PASS test notes ../__fixtures__/v2/with-subchart/charts/child-chart/tests/notes_test.yaml + PASS test notes ../__fixtures__/v2/with-subchart/tests/notes_test.yaml PASS test service ../__fixtures__/v2/with-subchart/charts/child-chart/tests/service_test.yaml PASS test service ../__fixtures__/v2/with-subchart/tests/service_test.yaml Charts: 1 passed, 1 total -Test Suites: 6 passed, 6 total -Tests: 14 passed, 14 total -Snapshot: 2 passed, 2 total +Test Suites: 8 passed, 8 total +Tests: 20 passed, 20 total +Snapshot: 8 passed, 8 total Time: XX.XXXms diff --git a/unittest/.snapshots/TestV2RunnerWithTestsInSubchartButFlagFalse b/unittest/.snapshots/TestV2RunnerWithTestsInSubchartButFlagFalse index d04474fdc..33a4d6b5a 100644 --- a/unittest/.snapshots/TestV2RunnerWithTestsInSubchartButFlagFalse +++ b/unittest/.snapshots/TestV2RunnerWithTestsInSubchartButFlagFalse @@ -4,13 +4,14 @@ PASS test deployment ../__fixtures__/v2/with-subchart/tests/deployment_test.yaml PASS test ingress ../__fixtures__/v2/with-subchart/tests/ingress_test.yaml + PASS test notes ../__fixtures__/v2/with-subchart/tests/notes_test.yaml PASS test service ../__fixtures__/v2/with-subchart/tests/service_test.yaml Charts: 1 passed, 1 total -Test Suites: 3 passed, 3 total -Tests: 7 passed, 7 total -Snapshot: 1 passed, 1 total +Test Suites: 4 passed, 4 total +Tests: 10 passed, 10 total +Snapshot: 4 passed, 4 total Time: XX.XXXms diff --git a/unittest/.snapshots/TestV3RunSuiteWithSubfolderWhenPass b/unittest/.snapshots/TestV3RunSuiteWithSubfolderWhenPass new file mode 100644 index 000000000..50470ac56 --- /dev/null +++ b/unittest/.snapshots/TestV3RunSuiteWithSubfolderWhenPass @@ -0,0 +1,41 @@ +(*unittest.TestSuiteResult)({ + DisplayName: (string) (len=15) "test suite name", + FilePath: (string) "", + Passed: (bool) true, + ExecError: (error) , + TestsResult: ([]*unittest.TestJobResult) (len=1) { + (*unittest.TestJobResult)({ + DisplayName: (string) (len=11) "should pass", + Index: (int) 0, + Passed: (bool) true, + ExecError: (error) , + AssertsResult: ([]*unittest.AssertionResult) (len=2) { + (*unittest.AssertionResult)({ + Index: (int) 0, + FailInfo: ([]string) { + }, + Passed: (bool) true, + AssertType: (string) (len=5) "equal", + Not: (bool) false, + CustomInfo: (string) "" + }), + (*unittest.AssertionResult)({ + Index: (int) 1, + FailInfo: ([]string) { + }, + Passed: (bool) true, + AssertType: (string) (len=13) "matchSnapshot", + Not: (bool) false, + CustomInfo: (string) "" + }) + }, + Duration: (time.Duration) 0s + }) + }, + SnapshotCounting: (struct { Total uint; Failed uint; Created uint; Vanished uint }) { + Total: (uint) 2, + Failed: (uint) 0, + Created: (uint) 2, + Vanished: (uint) 0 + } +}) diff --git a/unittest/.snapshots/TestV3RunnerOkWithFailedTests b/unittest/.snapshots/TestV3RunnerOkWithFailedTests index 811480e3f..e02df3399 100644 --- a/unittest/.snapshots/TestV3RunnerOkWithFailedTests +++ b/unittest/.snapshots/TestV3RunnerOkWithFailedTests @@ -279,6 +279,28 @@ @@ -1,2 +1,2 @@ -- secretName: my-tls-secret +null + FAIL test notes ../__fixtures__/v3/basic/tests_failed/notes_test.yaml + - should pass the notes file with ingress enabled + + - asserts[0] `notEqualRaw` fail + + Template: basic/templates/NOTES.txt + Expected NOT to equal: + | + 1. Get the application URL by running these commands: + http://chart-example.local + + - should pass the notes file with service type LoadBalancer + + - asserts[0] `matchRegexRaw` fail + + Template: basic/templates/NOTES.txt + Expected to match: http:///$SERVICE_IP:80 + Actual: 1. Get the application URL by running these commands: + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get svc -w RELEASE-NAME-basic' + export SERVICE_IP=$(kubectl get svc --namespace NAMESPACE RELEASE-NAME-basic -o jsonpath='{.status.loadBalancer.ingress[0].ip}') + echo http://$SERVICE_IP:9999 FAIL test service ../__fixtures__/v3/basic/tests_failed/service_test.yaml - should failed @@ -344,8 +366,8 @@ Charts: 1 failed, 0 passed, 1 total -Test Suites: 3 failed, 0 passed, 3 total -Tests: 7 failed, 0 passed, 7 total +Test Suites: 4 failed, 0 passed, 4 total +Tests: 9 failed, 1 passed, 10 total Snapshot: 2 passed, 2 total Time: XX.XXXms diff --git a/unittest/.snapshots/TestV3RunnerOkWithPassedTests b/unittest/.snapshots/TestV3RunnerOkWithPassedTests index 6044f5d5d..e3ef6599f 100644 --- a/unittest/.snapshots/TestV3RunnerOkWithPassedTests +++ b/unittest/.snapshots/TestV3RunnerOkWithPassedTests @@ -4,13 +4,14 @@ PASS test deployment ../__fixtures__/v3/basic/tests/deployment_test.yaml PASS test ingress ../__fixtures__/v3/basic/tests/ingress_test.yaml + PASS test notes ../__fixtures__/v3/basic/tests/notes_test.yaml PASS test service ../__fixtures__/v3/basic/tests/service_test.yaml Charts: 1 passed, 1 total -Test Suites: 3 passed, 3 total -Tests: 8 passed, 8 total -Snapshot: 1 passed, 1 total +Test Suites: 4 passed, 4 total +Tests: 11 passed, 11 total +Snapshot: 4 passed, 4 total Time: XX.XXXms diff --git a/unittest/.snapshots/TestV3RunnerOkWithSubSubfolder b/unittest/.snapshots/TestV3RunnerOkWithSubSubfolder new file mode 100644 index 000000000..7f00e9e26 --- /dev/null +++ b/unittest/.snapshots/TestV3RunnerOkWithSubSubfolder @@ -0,0 +1,17 @@ + +### Chart [ with-subfolder ] ../__fixtures__/v3/with-subfolder + + + PASS test deployment ../__fixtures__/v3/with-subfolder/tests/deployment_test.yaml + PASS test ingress ../__fixtures__/v3/with-subfolder/tests/ingress_test.yaml + PASS test notes ../__fixtures__/v3/with-subfolder/tests/notes_test.yaml + PASS test service ../__fixtures__/v3/with-subfolder/tests/service_test.yaml + + +Charts: 1 passed, 1 total +Test Suites: 4 passed, 4 total +Tests: 13 passed, 13 total +Snapshot: 5 passed, 5 total +Time: XX.XXXms + + diff --git a/unittest/.snapshots/TestV3RunnerWithTestsInSubchart b/unittest/.snapshots/TestV3RunnerWithTestsInSubchart index f331e7212..a2e4172a0 100644 --- a/unittest/.snapshots/TestV3RunnerWithTestsInSubchart +++ b/unittest/.snapshots/TestV3RunnerWithTestsInSubchart @@ -6,14 +6,16 @@ PASS test deployment ../__fixtures__/v3/with-subchart/tests/deployment_test.yaml PASS test ingress ../__fixtures__/v3/with-subchart/charts/child-chart/tests/ingress_test.yaml PASS test ingress ../__fixtures__/v3/with-subchart/tests/ingress_test.yaml + PASS test notes ../__fixtures__/v3/with-subchart/charts/child-chart/tests/notes_test.yaml + PASS test notes ../__fixtures__/v3/with-subchart/tests/notes_test.yaml PASS test service ../__fixtures__/v3/with-subchart/charts/child-chart/tests/service_test.yaml PASS test service ../__fixtures__/v3/with-subchart/tests/service_test.yaml Charts: 1 passed, 1 total -Test Suites: 6 passed, 6 total -Tests: 14 passed, 14 total -Snapshot: 2 passed, 2 total +Test Suites: 8 passed, 8 total +Tests: 20 passed, 20 total +Snapshot: 8 passed, 8 total Time: XX.XXXms diff --git a/unittest/.snapshots/TestV3RunnerWithTestsInSubchartButFlagFalse b/unittest/.snapshots/TestV3RunnerWithTestsInSubchartButFlagFalse index 603d4ff3b..76f4fcb73 100644 --- a/unittest/.snapshots/TestV3RunnerWithTestsInSubchartButFlagFalse +++ b/unittest/.snapshots/TestV3RunnerWithTestsInSubchartButFlagFalse @@ -4,13 +4,14 @@ PASS test deployment ../__fixtures__/v3/with-subchart/tests/deployment_test.yaml PASS test ingress ../__fixtures__/v3/with-subchart/tests/ingress_test.yaml + PASS test notes ../__fixtures__/v3/with-subchart/tests/notes_test.yaml PASS test service ../__fixtures__/v3/with-subchart/tests/service_test.yaml Charts: 1 passed, 1 total -Test Suites: 3 passed, 3 total -Tests: 7 passed, 7 total -Snapshot: 1 passed, 1 total +Test Suites: 4 passed, 4 total +Tests: 10 passed, 10 total +Snapshot: 4 passed, 4 total Time: XX.XXXms diff --git a/unittest/assertion.go b/unittest/assertion.go index f880db044..36267aafa 100644 --- a/unittest/assertion.go +++ b/unittest/assertion.go @@ -144,18 +144,25 @@ type assertTypeDef struct { } var assertTypeMapping = map[string]assertTypeDef{ - "matchSnapshot": {reflect.TypeOf(validators.MatchSnapshotValidator{}), false}, - "equal": {reflect.TypeOf(validators.EqualValidator{}), false}, - "notEqual": {reflect.TypeOf(validators.EqualValidator{}), true}, - "matchRegex": {reflect.TypeOf(validators.MatchRegexValidator{}), false}, - "notMatchRegex": {reflect.TypeOf(validators.MatchRegexValidator{}), true}, - "contains": {reflect.TypeOf(validators.ContainsValidator{}), false}, - "notContains": {reflect.TypeOf(validators.ContainsValidator{}), true}, - "isNull": {reflect.TypeOf(validators.IsNullValidator{}), false}, - "isNotNull": {reflect.TypeOf(validators.IsNullValidator{}), true}, - "isEmpty": {reflect.TypeOf(validators.IsEmptyValidator{}), false}, - "isNotEmpty": {reflect.TypeOf(validators.IsEmptyValidator{}), true}, - "isKind": {reflect.TypeOf(validators.IsKindValidator{}), false}, - "isAPIVersion": {reflect.TypeOf(validators.IsAPIVersionValidator{}), false}, - "hasDocuments": {reflect.TypeOf(validators.HasDocumentsValidator{}), false}, + "matchSnapshot": {reflect.TypeOf(validators.MatchSnapshotValidator{}), false}, + "matchSnapshotRaw": {reflect.TypeOf(validators.MatchSnapshotRawValidator{}), false}, + "equal": {reflect.TypeOf(validators.EqualValidator{}), false}, + "notEqual": {reflect.TypeOf(validators.EqualValidator{}), true}, + "equalRaw": {reflect.TypeOf(validators.EqualRawValidator{}), false}, + "notEqualRaw": {reflect.TypeOf(validators.EqualRawValidator{}), true}, + "matchRegex": {reflect.TypeOf(validators.MatchRegexValidator{}), false}, + "notMatchRegex": {reflect.TypeOf(validators.MatchRegexValidator{}), true}, + "matchRegexRaw": {reflect.TypeOf(validators.MatchRegexRawValidator{}), false}, + "notMatchRegexRaw": {reflect.TypeOf(validators.MatchRegexRawValidator{}), true}, + "contains": {reflect.TypeOf(validators.ContainsValidator{}), false}, + "notContains": {reflect.TypeOf(validators.ContainsValidator{}), true}, + "isNull": {reflect.TypeOf(validators.IsNullValidator{}), false}, + "isNotNull": {reflect.TypeOf(validators.IsNullValidator{}), true}, + "isEmpty": {reflect.TypeOf(validators.IsEmptyValidator{}), false}, + "isNotEmpty": {reflect.TypeOf(validators.IsEmptyValidator{}), true}, + "isKind": {reflect.TypeOf(validators.IsKindValidator{}), false}, + "isAPIVersion": {reflect.TypeOf(validators.IsAPIVersionValidator{}), false}, + "hasDocuments": {reflect.TypeOf(validators.HasDocumentsValidator{}), false}, + "isSubset": {reflect.TypeOf(validators.IsSubsetValidator{}), false}, + "isNotSubset": {reflect.TypeOf(validators.IsSubsetValidator{}), true}, } diff --git a/unittest/assertion_test.go b/unittest/assertion_test.go index 2eb5aac87..ecdcee64c 100644 --- a/unittest/assertion_test.go +++ b/unittest/assertion_test.go @@ -10,12 +10,42 @@ import ( "gopkg.in/yaml.v2" ) +func validateSucceededTestAssertions( + t *testing.T, + assertionsYAML string, + assertionCount int, + renderedMap map[string][]common.K8sManifest) { + + assertions := make([]Assertion, assertionCount) + err := yaml.Unmarshal([]byte(assertionsYAML), &assertions) + + a := assert.New(t) + a.Nil(err) + + for idx, assertion := range assertions { + result := assertion.Assert(renderedMap, fakeSnapshotComparer(true), &AssertionResult{Index: idx}) + a.Equal(&AssertionResult{ + Index: idx, + FailInfo: []string{}, + Passed: true, + AssertType: assertion.AssertType, + Not: false, + CustomInfo: "", + }, result) + } + +} + func TestAssertionUnmarshaledFromYAML(t *testing.T) { assertionsYAML := ` - equal: - notEqual: +- equalRaw: +- notEqualRaw: - matchRegex: - notMatchRegex: +- matchRegexRaw: +- notMatchRegexRaw: - contains: - notContains: - isNull: @@ -25,10 +55,12 @@ func TestAssertionUnmarshaledFromYAML(t *testing.T) { - isKind: - isAPIVersion: - hasDocuments: +- isSubset: ` - assertionsAsMap := make([]map[string]interface{}, 13) + + assertionsAsMap := make([]map[string]interface{}, 18) yaml.Unmarshal([]byte(assertionsYAML), &assertionsAsMap) - assertions := make([]Assertion, 13) + assertions := make([]Assertion, 18) yaml.Unmarshal([]byte(assertionsYAML), &assertions) a := assert.New(t) @@ -45,10 +77,18 @@ func TestAssertionUnmarshaledFromYAMLWithNotTrue(t *testing.T) { not: true - notEqual: not: true +- equalRaw: + not: true +- notEqualRaw: + not: true - matchRegex: not: true - notMatchRegex: not: true +- matchRegexRaw: + not: true +- notMatchRegexRaw: + not: true - contains: not: true - notContains: @@ -67,8 +107,10 @@ func TestAssertionUnmarshaledFromYAMLWithNotTrue(t *testing.T) { not: true - hasDocuments: not: true +- isSubset: + not: true ` - assertions := make([]Assertion, 13) + assertions := make([]Assertion, 18) yaml.Unmarshal([]byte(assertionsYAML), &assertions) a := assert.New(t) @@ -80,27 +122,36 @@ func TestAssertionUnmarshaledFromYAMLWithNotTrue(t *testing.T) { func TestReverseAssertionTheSameAsOriginalOneWithNotTrue(t *testing.T) { assertionsYAML := ` - equal: - not: true + not: true - notEqual: +- equalRaw: + not: true +- notEqualRaw: - matchRegex: - not: true + not: true - notMatchRegex: +- matchRegexRaw: + not: true +- notMatchRegexRaw: - contains: - not: true + not: true - notContains: - isNull: - not: true + not: true - isNotNull: - isEmpty: - not: true + not: true - isNotEmpty: +- isSubset: + not: true +- isNotSubset: ` - assertions := make([]Assertion, 10) + assertions := make([]Assertion, 15) yaml.Unmarshal([]byte(assertionsYAML), &assertions) a := assert.New(t) for idx := 0; idx < len(assertions); idx += 2 { - a.Equal(assertions[idx], assertions[idx+1]) + a.Equal(assertions[idx].Not, !assertions[idx+1].Not) } } @@ -118,6 +169,8 @@ kind: Fake apiVersion: v123 a: b c: [d] +e: + f: g ` manifest := common.K8sManifest{} yaml.Unmarshal([]byte(manifestDoc), &manifest) @@ -173,24 +226,41 @@ c: [d] count: 1 - template: t.yaml matchSnapshot: {} +- template: t.yaml + isSubset: + path: e + content: + f: g ` - assertions := make([]Assertion, 13) - err := yaml.Unmarshal([]byte(assertionsYAML), &assertions) - - a := assert.New(t) - a.Nil(err) + validateSucceededTestAssertions(t, assertionsYAML, 14, renderedMap) +} - for idx, assertion := range assertions { - result := assertion.Assert(renderedMap, fakeSnapshotComparer(true), &AssertionResult{Index: idx}) - a.Equal(&AssertionResult{ - Index: idx, - FailInfo: []string{}, - Passed: true, - AssertType: assertion.AssertType, - Not: false, - CustomInfo: "", - }, result) +func TestAssertionRawAssertWhenOk(t *testing.T) { + manifest := common.K8sManifest{common.RAW: "NOTES.txt"} + renderedMap := map[string][]common.K8sManifest{ + "t.yaml": {manifest}, } + + assertionsYAML := ` +- template: t.yaml + equalRaw: + value: NOTES.txt +- template: t.yaml + notEqualRaw: + value: UNNOTES.txt +- template: t.yaml + matchRegexRaw: + pattern: NOTES.txt +- template: t.yaml + notMatchRegexRaw: + pattern: UNNOTES.txt +- template: t.yaml + hasDocuments: + count: 1 +- template: t.yaml + matchSnapshot: {} +` + validateSucceededTestAssertions(t, assertionsYAML, 5, renderedMap) } func TestAssertionAssertWhenTemplateNotExisted(t *testing.T) { diff --git a/unittest/common/types.go b/unittest/common/types.go index 2acd701a7..768a32cc6 100644 --- a/unittest/common/types.go +++ b/unittest/common/types.go @@ -2,3 +2,5 @@ package common // K8sManifest type for rendered manifest unmarshaled to type K8sManifest map[string]interface{} + +const RAW string = "raw" diff --git a/unittest/test_job.go b/unittest/test_job.go index a6e660f87..8bc0a5ec2 100644 --- a/unittest/test_job.go +++ b/unittest/test_job.go @@ -261,9 +261,9 @@ func (t *TestJob) parseManifestsFromOutputOfFiles(outputOfFiles map[string]strin manifestsOfFiles := make(map[string][]common.K8sManifest) for file, rendered := range outputOfFiles { - decoder := yaml.NewDecoder(strings.NewReader(rendered)) if filepath.Ext(file) == ".yaml" { + decoder := yaml.NewDecoder(strings.NewReader(rendered)) manifests := make([]common.K8sManifest, 0) for { @@ -283,6 +283,17 @@ func (t *TestJob) parseManifestsFromOutputOfFiles(outputOfFiles map[string]strin manifestsOfFiles[file] = manifests } + + if filepath.Ext(file) == ".txt" { + manifests := make([]common.K8sManifest, 0) + manifest := make(common.K8sManifest) + manifest[common.RAW] = rendered + + if len(manifest) > 0 { + manifests = append(manifests, manifest) + } + manifestsOfFiles[file] = manifests + } } return manifestsOfFiles, nil diff --git a/unittest/test_job_test.go b/unittest/test_job_test.go index 0a9e4f2e4..9c9e8e4c5 100644 --- a/unittest/test_job_test.go +++ b/unittest/test_job_test.go @@ -89,6 +89,34 @@ asserts: a.Equal(2, len(testResult.AssertsResult)) } +func TestV2RunJobWithNOTESTemplateOk(t *testing.T) { + c, _ := v2util.Load("../__fixtures__/v2/basic") + manifest := ` +it: should work +template: NOTES.txt +asserts: + - equalRaw: + value: | + 1. Get the application URL by running these commands: + export POD_NAME=$(kubectl get pods --namespace NAMESPACE -l "app=basic,release=RELEASE-NAME" -o jsonpath="{.items[0].metadata.name}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl port-forward $POD_NAME 8080:80 + - matchRegexRaw: + pattern: ^1. Get the application URL by running these commands +` + var tj TestJob + yaml.Unmarshal([]byte(manifest), &tj) + + testResult := tj.RunV2(c, &snapshot.Cache{}, &TestJobResult{}) + + a := assert.New(t) + cupaloy.SnapshotT(t, makeTestJobResultSnapshotable(testResult)) + + a.Nil(testResult.ExecError) + a.True(testResult.Passed) + a.Equal(2, len(testResult.AssertsResult)) +} + func TestV2RunJobWithTestJobTemplateOk(t *testing.T) { c, _ := v2util.Load("../__fixtures__/v2/basic") manifest := ` diff --git a/unittest/test_runner.go b/unittest/test_runner.go index 61520d8d8..b1a4aeb96 100644 --- a/unittest/test_runner.go +++ b/unittest/test_runner.go @@ -155,7 +155,6 @@ func (tr *TestRunner) getTestSuites(chartPath, chartRoute string) ([]*TestSuite, FilePath: file, ExecError: err, }) - continue } resultSuites = append(resultSuites, suite) } @@ -221,6 +220,7 @@ func (tr *TestRunner) runV2SuitesOfChart(suites []*TestSuite, chart *v2chart.Cha FilePath: suite.definitionFile, ExecError: err, }) + chartPassed = false continue } @@ -245,6 +245,7 @@ func (tr *TestRunner) runV3SuitesOfChart(suites []*TestSuite, chart *v3chart.Cha FilePath: suite.definitionFile, ExecError: err, }) + chartPassed = false continue } diff --git a/unittest/test_runner_test.go b/unittest/test_runner_test.go index 43a9e0ca9..f58c62f4d 100644 --- a/unittest/test_runner_test.go +++ b/unittest/test_runner_test.go @@ -65,7 +65,7 @@ func TestV2RunnerOkWithPassedTests(t *testing.T) { }, } passed := runner.RunV2([]string{"../__fixtures__/v2/basic"}) - assert.True(t, passed) + assert.True(t, passed, buffer.String()) cupaloy.SnapshotT(t, makeOutputSnapshotable(buffer.String())...) } @@ -78,7 +78,20 @@ func TestV2RunnerOkWithFailedTests(t *testing.T) { }, } passed := runner.RunV2([]string{"../__fixtures__/v2/basic"}) - assert.False(t, passed) + assert.False(t, passed, buffer.String()) + cupaloy.SnapshotT(t, makeOutputSnapshotable(buffer.String())...) +} + +func TestV2RunnerOkWithSubSubfolder(t *testing.T) { + buffer := new(bytes.Buffer) + runner := TestRunner{ + Printer: NewPrinter(buffer, nil), + Config: TestConfig{ + TestFiles: []string{"tests/*_test.yaml"}, + }, + } + passed := runner.RunV2([]string{"../__fixtures__/v2/with-subfolder"}) + assert.True(t, passed, buffer.String()) cupaloy.SnapshotT(t, makeOutputSnapshotable(buffer.String())...) } @@ -92,7 +105,7 @@ func TestV2RunnerWithTestsInSubchart(t *testing.T) { }, } passed := runner.RunV2([]string{"../__fixtures__/v2/with-subchart"}) - assert.True(t, passed) + assert.True(t, passed, buffer.String()) cupaloy.SnapshotT(t, makeOutputSnapshotable(buffer.String())...) } @@ -106,7 +119,7 @@ func TestV2RunnerWithTestsInSubchartButFlagFalse(t *testing.T) { }, } passed := runner.RunV2([]string{"../__fixtures__/v2/with-subchart"}) - assert.True(t, passed) + assert.True(t, passed, buffer.String()) cupaloy.SnapshotT(t, makeOutputSnapshotable(buffer.String())...) } @@ -119,7 +132,7 @@ func TestV3RunnerOkWithPassedTests(t *testing.T) { }, } passed := runner.RunV3([]string{"../__fixtures__/v3/basic"}) - assert.True(t, passed) + assert.True(t, passed, buffer.String()) cupaloy.SnapshotT(t, makeOutputSnapshotable(buffer.String())...) } @@ -132,7 +145,20 @@ func TestV3RunnerOkWithFailedTests(t *testing.T) { }, } passed := runner.RunV3([]string{"../__fixtures__/v3/basic"}) - assert.False(t, passed) + assert.False(t, passed, buffer.String()) + cupaloy.SnapshotT(t, makeOutputSnapshotable(buffer.String())...) +} + +func TestV3RunnerOkWithSubSubfolder(t *testing.T) { + buffer := new(bytes.Buffer) + runner := TestRunner{ + Printer: NewPrinter(buffer, nil), + Config: TestConfig{ + TestFiles: []string{"tests/*_test.yaml"}, + }, + } + passed := runner.RunV3([]string{"../__fixtures__/v3/with-subfolder"}) + assert.True(t, passed, buffer.String()) cupaloy.SnapshotT(t, makeOutputSnapshotable(buffer.String())...) } @@ -146,7 +172,7 @@ func TestV3RunnerWithTestsInSubchart(t *testing.T) { }, } passed := runner.RunV3([]string{"../__fixtures__/v3/with-subchart"}) - assert.True(t, passed) + assert.True(t, passed, buffer.String()) cupaloy.SnapshotT(t, makeOutputSnapshotable(buffer.String())...) } @@ -160,6 +186,6 @@ func TestV3RunnerWithTestsInSubchartButFlagFalse(t *testing.T) { }, } passed := runner.RunV3([]string{"../__fixtures__/v3/with-subchart"}) - assert.True(t, passed) + assert.True(t, passed, buffer.String()) cupaloy.SnapshotT(t, makeOutputSnapshotable(buffer.String())...) } diff --git a/unittest/test_suite.go b/unittest/test_suite.go index 70390f75a..abe1478d5 100644 --- a/unittest/test_suite.go +++ b/unittest/test_suite.go @@ -127,7 +127,9 @@ func (s *TestSuite) prepareV2Chart(targetChart *v2chart.Chart) (*v2chart.Chart, for _, fileName := range s.Templates { found := false for _, template := range targetChart.Templates { - if filepath.Base(template.Name) == fileName { + // Within templates unix separators are always used. + relativeFilePath := strings.Join([]string{"templates", fileName}, "/") + if template.Name == relativeFilePath { filteredTemplate = append(filteredTemplate, template) found = true break @@ -169,7 +171,9 @@ func (s *TestSuite) prepareV3Chart(targetChart *v3chart.Chart) (*v3chart.Chart, for _, fileName := range s.Templates { found := false for _, template := range targetChart.Templates { - if filepath.Base(template.Name) == fileName { + // Within templates unix separators are always used. + relativeFilePath := strings.Join([]string{"templates", fileName}, "/") + if template.Name == relativeFilePath { filteredTemplate = append(filteredTemplate, template) found = true break diff --git a/unittest/test_suite_test.go b/unittest/test_suite_test.go index 256ec3669..0138b60f4 100644 --- a/unittest/test_suite_test.go +++ b/unittest/test_suite_test.go @@ -53,9 +53,20 @@ func TestV2ParseTestSuiteFileOk(t *testing.T) { suite, err := ParseTestSuiteFile("../__fixtures__/v2/basic/tests/deployment_test.yaml", "basic") a.Nil(err) - a.Equal(suite.Name, "test deployment") - a.Equal(suite.Templates, []string{"deployment.yaml"}) - a.Equal(suite.Tests[0].Name, "should pass all kinds of assertion") + a.Equal("test deployment", suite.Name) + a.Equal([]string{"deployment.yaml"}, suite.Templates) + a.Equal("should pass all kinds of assertion", suite.Tests[0].Name) +} + +func TestV2ParseTestSuiteFileInSubfolderOk(t *testing.T) { + a := assert.New(t) + suite, err := ParseTestSuiteFile("../__fixtures__/v2/with-subfolder/tests/service_test.yaml", "with-subfolder") + + a.Nil(err) + a.Equal("test service", suite.Name) + a.Equal([]string{"webserver/service.yaml"}, suite.Templates) + a.Equal("should pass", suite.Tests[0].Name) + a.Equal("should render right if values given", suite.Tests[1].Name) } func TestV2RunSuiteWithMultipleTemplatesWhenPass(t *testing.T) { @@ -142,6 +153,30 @@ tests: validateTestResultAndSnapshots(t, suiteResult, false, "test suite name", 1, 0, 0, 0, 0) } +func TestV2RunSuiteWithSubfolderWhenPass(t *testing.T) { + c, _ := v2util.Load("../__fixtures__/v2/with-subfolder") + suiteDoc := ` +suite: test suite name +templates: + - db/deployment.yaml + - webserver/deployment.yaml +tests: + - it: should pass + asserts: + - equal: + path: kind + value: Deployment + - matchSnapshot: {} +` + testSuite := TestSuite{} + yaml.Unmarshal([]byte(suiteDoc), &testSuite) + + cache, _ := snapshot.CreateSnapshotOfSuite(path.Join(tmpdir, "my_test.yaml"), false) + suiteResult := testSuite.RunV2(c, cache, &TestSuiteResult{}) + + validateTestResultAndSnapshots(t, suiteResult, true, "test suite name", 1, 2, 2, 0, 0) +} + func TestV3ParseTestSuiteFileOk(t *testing.T) { a := assert.New(t) suite, err := ParseTestSuiteFile("../__fixtures__/v3/basic/tests/deployment_test.yaml", "basic") @@ -235,3 +270,27 @@ tests: validateTestResultAndSnapshots(t, suiteResult, false, "test suite name", 1, 0, 0, 0, 0) } + +func TestV3RunSuiteWithSubfolderWhenPass(t *testing.T) { + c, _ := loader.Load("../__fixtures__/v3/with-subfolder") + suiteDoc := ` +suite: test suite name +templates: + - db/deployment.yaml + - webserver/deployment.yaml +tests: + - it: should pass + asserts: + - equal: + path: kind + value: Deployment + - matchSnapshot: {} +` + testSuite := TestSuite{} + yaml.Unmarshal([]byte(suiteDoc), &testSuite) + + cache, _ := snapshot.CreateSnapshotOfSuite(path.Join(tmpdir, "my_test.yaml"), false) + suiteResult := testSuite.RunV3(c, cache, &TestSuiteResult{}) + + validateTestResultAndSnapshots(t, suiteResult, true, "test suite name", 1, 2, 2, 0, 0) +} diff --git a/unittest/validators/common.go b/unittest/validators/common.go index 65dffa4db..4cb397954 100644 --- a/unittest/validators/common.go +++ b/unittest/validators/common.go @@ -79,6 +79,13 @@ func diff(expected string, actual string) string { return diff } +// uniform the content with correct line-endings +func uniformContent(content interface{}) string { + // All decoded content uses LF + actual := fmt.Sprintf("%v", content) + return strings.ReplaceAll(actual, "\r\n", "\n") +} + const errorFormat = ` Error: %s diff --git a/unittest/validators/common_test.go b/unittest/validators/common_test.go index 3806a6dac..a98102898 100644 --- a/unittest/validators/common_test.go +++ b/unittest/validators/common_test.go @@ -2,6 +2,8 @@ package validators_test import ( "github.com/lrills/helm-unittest/unittest/common" + "github.com/lrills/helm-unittest/unittest/snapshot" + "github.com/stretchr/testify/mock" yaml "gopkg.in/yaml.v2" ) @@ -10,3 +12,12 @@ func makeManifest(doc string) common.K8sManifest { yaml.Unmarshal([]byte(doc), &manifest) return manifest } + +type mockSnapshotComparer struct { + mock.Mock +} + +func (m *mockSnapshotComparer) CompareToSnapshot(content interface{}) *snapshot.CompareResult { + args := m.Called(content) + return args.Get(0).(*snapshot.CompareResult) +} diff --git a/unittest/validators/contains_validator.go b/unittest/validators/contains_validator.go index c25c036b2..81590826e 100644 --- a/unittest/validators/contains_validator.go +++ b/unittest/validators/contains_validator.go @@ -13,6 +13,8 @@ import ( type ContainsValidator struct { Path string Content interface{} + Count *int + Any bool } func (v ContainsValidator) failInfo(actual interface{}, index int, not bool) []string { @@ -36,6 +38,33 @@ Actual: ) } +func (v ContainsValidator) validateContent(actual []interface{}) (bool, int) { + found := false + validateFoundCount := 0 + + for _, ele := range actual { + // When any enabled, only the key is validated + if v.Any { + if subset, ok := ele.(map[interface{}]interface{}); ok { + for key, value := range subset { + ele := map[interface{}]interface{}{key: value} + if reflect.DeepEqual(ele, v.Content) { + found = true + validateFoundCount++ + } + } + } + } + + if !v.Any && reflect.DeepEqual(ele, v.Content) { + found = true + validateFoundCount++ + } + } + + return found, validateFoundCount +} + // Validate implement Validatable func (v ContainsValidator) Validate(context *ValidateContext) (bool, []string) { manifests, err := context.getManifests() @@ -56,20 +85,30 @@ func (v ContainsValidator) Validate(context *ValidateContext) (bool, []string) { } if actual, ok := actual.([]interface{}); ok { - found := false - for _, ele := range actual { - if reflect.DeepEqual(ele, v.Content) { - found = true - } - } - if found == context.Negative { + found, validateFoundCount := v.validateContent(actual) + + if v.Count == nil && found == context.Negative { validateSuccess = validateSuccess && false errorMessage := v.failInfo(actual, idx, context.Negative) validateErrors = append(validateErrors, errorMessage...) continue } + if v.Count != nil && *v.Count != validateFoundCount && found == !context.Negative { + actualYAML, _ := yaml.Marshal(actual) + validateSuccess = validateSuccess && false + errorMessage := splitInfof(errorFormat, idx, fmt.Sprintf( + "expect count %d in '%s' to be in array, got %d:\n%s", + *v.Count, + v.Path, + validateFoundCount, + string(actualYAML), + )) + validateErrors = append(validateErrors, errorMessage...) + continue + } + validateSuccess = validateSuccess && true continue } diff --git a/unittest/validators/contains_validator_test.go b/unittest/validators/contains_validator_test.go index a415994e4..c59ba61de 100644 --- a/unittest/validators/contains_validator_test.go +++ b/unittest/validators/contains_validator_test.go @@ -14,6 +14,8 @@ a: b: - c: hello world - d: foo bar + - e: bar + - e: bar ` func TestContainsValidatorWhenOk(t *testing.T) { @@ -22,6 +24,8 @@ func TestContainsValidatorWhenOk(t *testing.T) { validator := ContainsValidator{ "a.b", map[interface{}]interface{}{"d": "foo bar"}, + nil, + false, } pass, diff := validator.Validate(&ValidateContext{ Docs: []common.K8sManifest{manifest}, @@ -31,10 +35,75 @@ func TestContainsValidatorWhenOk(t *testing.T) { assert.Equal(t, []string{}, diff) } +func TestContainsValidatorWithAnyWhenOk(t *testing.T) { + docToTestContainsAny := ` +a: + b: + - name: VALUE1 + value: bla + - name: VALUE2 + value: bla2 +` + manifest := makeManifest(docToTestContainsAny) + + validator := ContainsValidator{ + "a.b", + map[interface{}]interface{}{"name": "VALUE1"}, + nil, + true, + } + pass, diff := validator.Validate(&ValidateContext{ + Docs: []common.K8sManifest{manifest}, + }) + + assert.True(t, pass) + assert.Equal(t, []string{}, diff) +} + +func TestContainsValidatorWithAnyWhenNotFoundOk(t *testing.T) { + docToTestContainsAny := ` +a: + b: + - name: VALUE1 + value: bla + - name: VALUE2 + value: bla2 +` + manifest := makeManifest(docToTestContainsAny) + + validator := ContainsValidator{ + "a.b", + map[interface{}]interface{}{"name": "VALUE3"}, + nil, + true, + } + pass, diff := validator.Validate(&ValidateContext{ + Docs: []common.K8sManifest{manifest}, + }) + + assert.False(t, pass) + assert.Equal(t, []string{ + "DocumentIndex: 0", + "Path: a.b", + "Expected to contain:", + " - name: VALUE3", + "Actual:", + " - name: VALUE1", + " value: bla", + " - name: VALUE2", + " value: bla2", + }, diff) +} + func TestContainsValidatorWhenNegativeAndOk(t *testing.T) { manifest := makeManifest(docToTestContains) - validator := ContainsValidator{"a.b", map[interface{}]interface{}{"d": "hello bar"}} + validator := ContainsValidator{ + "a.b", + map[interface{}]interface{}{"d": "hello bar"}, + nil, + false, + } pass, diff := validator.Validate(&ValidateContext{ Docs: []common.K8sManifest{manifest}, Negative: true, @@ -50,6 +119,8 @@ func TestContainsValidatorWhenFail(t *testing.T) { validator := ContainsValidator{ "a.b", map[interface{}]interface{}{"e": "bar bar"}, + nil, + false, } pass, diff := validator.Validate(&ValidateContext{ Docs: []common.K8sManifest{manifest}, @@ -64,22 +135,26 @@ func TestContainsValidatorWhenFail(t *testing.T) { "Actual:", " - c: hello world", " - d: foo bar", + " - e: bar", + " - e: bar", }, diff) } func TestContainsValidatorMultiManifestWhenFail(t *testing.T) { manifest1 := makeManifest(docToTestContains) - var docToTestContains = ` + extraDoc := ` a: b: - c: hello world ` - manifest2 := makeManifest(docToTestContains) + manifest2 := makeManifest(extraDoc) manifests := []common.K8sManifest{manifest1, manifest2} validator := ContainsValidator{ "a.b", map[interface{}]interface{}{"d": "foo bar"}, + nil, + false, } pass, diff := validator.Validate(&ValidateContext{ Docs: manifests, @@ -104,6 +179,8 @@ func TestContainsValidatorMultiManifestWhenBothFail(t *testing.T) { validator := ContainsValidator{ "a.b", map[interface{}]interface{}{"e": "foo bar"}, + nil, + false, } pass, diff := validator.Validate(&ValidateContext{ Docs: manifests, @@ -119,6 +196,8 @@ func TestContainsValidatorMultiManifestWhenBothFail(t *testing.T) { "Actual:", " - c: hello world", " - d: foo bar", + " - e: bar", + " - e: bar", "DocumentIndex: 1", "Path: a.b", "Expected to contain:", @@ -126,6 +205,8 @@ func TestContainsValidatorMultiManifestWhenBothFail(t *testing.T) { "Actual:", " - c: hello world", " - d: foo bar", + " - e: bar", + " - e: bar", }, diff) } @@ -135,6 +216,8 @@ func TestContainsValidatorWhenNegativeAndFail(t *testing.T) { validator := ContainsValidator{ "a.b", map[interface{}]interface{}{"d": "foo bar"}, + nil, + false, } pass, diff := validator.Validate(&ValidateContext{ Docs: []common.K8sManifest{manifest}, @@ -150,6 +233,8 @@ func TestContainsValidatorWhenNegativeAndFail(t *testing.T) { "Actual:", " - c: hello world", " - d: foo bar", + " - e: bar", + " - e: bar", }, diff) } @@ -162,7 +247,12 @@ a: ` manifest := makeManifest(manifestDocNotArray) - validator := ContainsValidator{"a.b", common.K8sManifest{"d": "foo bar"}} + validator := ContainsValidator{ + "a.b", + common.K8sManifest{"d": "foo bar"}, + nil, + false, + } pass, diff := validator.Validate(&ValidateContext{ Docs: []common.K8sManifest{manifest}, }) @@ -180,7 +270,12 @@ a: func TestContainsValidatorWhenInvalidIndex(t *testing.T) { manifest := makeManifest(docToTestContains) - validator := ContainsValidator{"a.b", common.K8sManifest{"d": "foo bar"}} + validator := ContainsValidator{ + "a.b", + common.K8sManifest{"d": "foo bar"}, + nil, + false, + } pass, diff := validator.Validate(&ValidateContext{ Docs: []common.K8sManifest{manifest}, Index: 2, @@ -196,7 +291,12 @@ func TestContainsValidatorWhenInvalidIndex(t *testing.T) { func TestContainsValidatorWhenInvalidPath(t *testing.T) { manifest := makeManifest(docToTestContains) - validator := ContainsValidator{"a.b.e", common.K8sManifest{"d": "foo bar"}} + validator := ContainsValidator{ + "a.b.e", + common.K8sManifest{"e": "bar"}, + nil, + false, + } pass, diff := validator.Validate(&ValidateContext{ Docs: []common.K8sManifest{manifest}, }) @@ -208,5 +308,73 @@ func TestContainsValidatorWhenInvalidPath(t *testing.T) { " can't get [\"e\"] from a non map type:", " - c: hello world", " - d: foo bar", + " - e: bar", + " - e: bar", + }, diff) +} + +func TestContainsValidatorWhenMultipleTimesInArray(t *testing.T) { + manifest := makeManifest(docToTestContains) + + counter := new(int) + *counter = 2 + validator := ContainsValidator{ + "a.b", + map[interface{}]interface{}{"e": "bar"}, + counter, + false, + } + pass, diff := validator.Validate(&ValidateContext{ + Docs: []common.K8sManifest{manifest}, + }) + + assert.True(t, pass) + assert.Equal(t, []string{}, diff) +} + +func TestContainsValidatorInverseWhenNotMultipleTimesInArray(t *testing.T) { + manifest := makeManifest(docToTestContains) + + counter := new(int) + *counter = 1 + validator := ContainsValidator{ + "a.b", + map[interface{}]interface{}{"e": "bar"}, + counter, + false, + } + pass, diff := validator.Validate(&ValidateContext{ + Docs: []common.K8sManifest{manifest}, + Negative: true, + }) + + assert.True(t, pass) + assert.Equal(t, []string{}, diff) +} + +func TestContainsValidatorWhenNotMultipleTimesInArray(t *testing.T) { + manifest := makeManifest(docToTestContains) + + counter := new(int) + *counter = 1 + validator := ContainsValidator{ + "a.b", + map[interface{}]interface{}{"e": "bar"}, + counter, + false, + } + pass, diff := validator.Validate(&ValidateContext{ + Docs: []common.K8sManifest{manifest}, + }) + + assert.False(t, pass) + assert.Equal(t, []string{ + "DocumentIndex: 0", + "Error:", + " expect count 1 in 'a.b' to be in array, got 2:", + " - c: hello world", + " - d: foo bar", + " - e: bar", + " - e: bar", }, diff) } diff --git a/unittest/validators/equal_raw_validator.go b/unittest/validators/equal_raw_validator.go new file mode 100644 index 000000000..4e7394b25 --- /dev/null +++ b/unittest/validators/equal_raw_validator.go @@ -0,0 +1,67 @@ +package validators + +import ( + "reflect" + + "github.com/lrills/helm-unittest/unittest/common" +) + +// EqualRawValidator validate whether the raw value equal to Value +type EqualRawValidator struct { + Value string +} + +func (a EqualRawValidator) failInfo(actual interface{}, not bool) []string { + var notAnnotation string + if not { + notAnnotation = " NOT to equal" + } + failFormat := ` +Expected` + notAnnotation + `: +%s` + + expectedYAML := common.TrustedMarshalYAML(a.Value) + if not { + return splitInfof(failFormat, -1, expectedYAML) + } + + actualYAML := common.TrustedMarshalYAML(actual) + return splitInfof( + failFormat+` +Actual: +%s +Diff: +%s +`, + -1, + expectedYAML, + actualYAML, + diff(expectedYAML, actualYAML), + ) +} + +// Validate implement Validatable +func (a EqualRawValidator) Validate(context *ValidateContext) (bool, []string) { + manifests, err := context.getManifests() + if err != nil { + return false, splitInfof(errorFormat, -1, err.Error()) + } + + validateSuccess := true + validateErrors := make([]string, 0) + + for _, manifest := range manifests { + actual := uniformContent(manifest[common.RAW]) + + if reflect.DeepEqual(a.Value, actual) == context.Negative { + validateSuccess = validateSuccess && false + errorMessage := a.failInfo(actual, context.Negative) + validateErrors = append(validateErrors, errorMessage...) + continue + } + + validateSuccess = validateSuccess && true + } + + return validateSuccess, validateErrors +} diff --git a/unittest/validators/equal_raw_validator_test.go b/unittest/validators/equal_raw_validator_test.go new file mode 100644 index 000000000..5ccac43da --- /dev/null +++ b/unittest/validators/equal_raw_validator_test.go @@ -0,0 +1,93 @@ +package validators_test + +import ( + "testing" + + . "github.com/lrills/helm-unittest/unittest/validators" + + "github.com/lrills/helm-unittest/unittest/common" + "github.com/stretchr/testify/assert" +) + +var docToTestEqualRaw = ` +raw: This is a NOTES.txt document. +` + +func TestEqualRawValidatorWhenOk(t *testing.T) { + manifest := makeManifest(docToTestEqualRaw) + validator := EqualRawValidator{"This is a NOTES.txt document."} + + pass, diff := validator.Validate(&ValidateContext{ + Docs: []common.K8sManifest{manifest}, + }) + + assert.True(t, pass) + assert.Equal(t, []string{}, diff) +} + +func TestEqualRawValidatorWhenNegativeAndOk(t *testing.T) { + manifest := makeManifest(docToTestEqualRaw) + + validator := EqualRawValidator{"Invalid text."} + pass, diff := validator.Validate(&ValidateContext{ + Docs: []common.K8sManifest{manifest}, + Negative: true, + }) + + assert.True(t, pass) + assert.Equal(t, []string{}, diff) +} + +func TestEqualRawValidatorWhenFail(t *testing.T) { + manifest := makeManifest(docToTestEqualRaw) + + validator := EqualRawValidator{"Invalid text."} + pass, diff := validator.Validate(&ValidateContext{ + Docs: []common.K8sManifest{manifest}, + }) + + assert.False(t, pass) + assert.Equal(t, []string{ + "Expected:", + " Invalid text.", + "Actual:", + " This is a NOTES.txt document.", + "Diff:", + " --- Expected", + " +++ Actual", + " @@ -1,2 +1,2 @@", + " -Invalid text.", + " +This is a NOTES.txt document.", + }, diff) +} + +func TestEqualRawValidatorWhenNegativeAndFail(t *testing.T) { + manifest := makeManifest(docToTestEqualRaw) + + v := EqualRawValidator{"This is a NOTES.txt document."} + pass, diff := v.Validate(&ValidateContext{ + Docs: []common.K8sManifest{manifest}, + Negative: true, + }) + + assert.False(t, pass) + assert.Equal(t, []string{ + "Expected NOT to equal:", + " This is a NOTES.txt document.", + }, diff) +} + +func TestEqualRawValidatorWhenInvalidIndex(t *testing.T) { + manifest := makeManifest(docToTestEqualRaw) + validator := EqualRawValidator{"Invalid text."} + pass, diff := validator.Validate(&ValidateContext{ + Docs: []common.K8sManifest{manifest}, + Index: 2, + }) + + assert.False(t, pass) + assert.Equal(t, []string{ + "Error:", + " documentIndex 2 out of range", + }, diff) +} diff --git a/unittest/validators/is_subset_validator.go b/unittest/validators/is_subset_validator.go new file mode 100644 index 000000000..4df14de12 --- /dev/null +++ b/unittest/validators/is_subset_validator.go @@ -0,0 +1,90 @@ +package validators + +import ( + "fmt" + "reflect" + + "github.com/lrills/helm-unittest/unittest/common" + "github.com/lrills/helm-unittest/unittest/valueutils" + yaml "gopkg.in/yaml.v2" +) + +// IsSubsetValidator validate whether value of Path contains Content +type IsSubsetValidator struct { + Path string + Content interface{} +} + +func (v IsSubsetValidator) failInfo(actual interface{}, index int, not bool) []string { + var notAnnotation string + if not { + notAnnotation = " NOT" + } + containsFailFormat := ` +Path:%s +Expected` + notAnnotation + ` to contain: +%s +Actual: +%s +` + return splitInfof( + containsFailFormat, + index, + v.Path, + common.TrustedMarshalYAML(v.Content), + common.TrustedMarshalYAML(actual), + ) +} + +// Validate implement Validatable +func (v IsSubsetValidator) Validate(context *ValidateContext) (bool, []string) { + manifests, err := context.getManifests() + if err != nil { + return false, splitInfof(errorFormat, -1, err.Error()) + } + + validateSuccess := true + validateErrors := make([]string, 0) + + for idx, manifest := range manifests { + actual, err := valueutils.GetValueOfSetPath(manifest, v.Path) + if err != nil { + validateSuccess = validateSuccess && false + errorMessage := splitInfof(errorFormat, idx, err.Error()) + validateErrors = append(validateErrors, errorMessage...) + continue + } + + if actual, ok := actual.(map[interface{}]interface{}); ok { + found := false + + for key, value := range actual { + ele := map[interface{}]interface{}{key: value} + if reflect.DeepEqual(ele, v.Content) { + found = true + } + } + + if found == context.Negative { + validateSuccess = validateSuccess && false + errorMessage := v.failInfo(actual, idx, context.Negative) + validateErrors = append(validateErrors, errorMessage...) + continue + } + + validateSuccess = validateSuccess && true + continue + } + + actualYAML, _ := yaml.Marshal(actual) + validateSuccess = validateSuccess && false + errorMessage := splitInfof(errorFormat, idx, fmt.Sprintf( + "expect '%s' to be an object, got:\n%s", + v.Path, + string(actualYAML), + )) + validateErrors = append(validateErrors, errorMessage...) + } + + return validateSuccess, validateErrors +} diff --git a/unittest/validators/is_subset_validator_test.go b/unittest/validators/is_subset_validator_test.go new file mode 100644 index 000000000..97b04e830 --- /dev/null +++ b/unittest/validators/is_subset_validator_test.go @@ -0,0 +1,212 @@ +package validators_test + +import ( + "testing" + + . "github.com/lrills/helm-unittest/unittest/validators" + + "github.com/lrills/helm-unittest/unittest/common" + "github.com/stretchr/testify/assert" +) + +var docToTestIsSubset = ` +a: + b: + c: hello world + d: foo bar +` + +func TestIsSubsetValidatorWhenOk(t *testing.T) { + manifest := makeManifest(docToTestIsSubset) + + validator := IsSubsetValidator{ + "a.b", + map[interface{}]interface{}{"d": "foo bar"}} + + pass, diff := validator.Validate(&ValidateContext{ + Docs: []common.K8sManifest{manifest}, + }) + + assert.True(t, pass) + assert.Equal(t, []string{}, diff) +} + +func TestIsSubsetValidatorWhenNegativeAndOk(t *testing.T) { + manifest := makeManifest(docToTestIsSubset) + + validator := IsSubsetValidator{ + "a.b", + map[interface{}]interface{}{"d": "hello bar"}} + pass, diff := validator.Validate(&ValidateContext{ + Docs: []common.K8sManifest{manifest}, + Negative: true, + }) + + assert.True(t, pass) + assert.Equal(t, []string{}, diff) +} + +func TestIsSubsetValidatorWhenFail(t *testing.T) { + manifest := makeManifest(docToTestIsSubset) + + validator := IsSubsetValidator{ + "a.b", + map[interface{}]interface{}{"e": "bar bar"}, + } + pass, diff := validator.Validate(&ValidateContext{ + Docs: []common.K8sManifest{manifest}, + }) + + assert.False(t, pass) + assert.Equal(t, []string{ + "DocumentIndex: 0", + "Path: a.b", + "Expected to contain:", + " e: bar bar", + "Actual:", + " c: hello world", + " d: foo bar", + }, diff) +} + +func TestIsSubsetValidatorMultiManifestWhenFail(t *testing.T) { + manifest1 := makeManifest(docToTestIsSubset) + extraDoc := ` +a: + b: + c: hello world +` + manifest2 := makeManifest(extraDoc) + manifests := []common.K8sManifest{manifest1, manifest2} + + validator := IsSubsetValidator{ + "a.b", + map[interface{}]interface{}{"d": "foo bar"}, + } + pass, diff := validator.Validate(&ValidateContext{ + Docs: manifests, + Index: -1, + }) + + assert.False(t, pass) + assert.Equal(t, []string{ + "DocumentIndex: 1", + "Path: a.b", + "Expected to contain:", + " d: foo bar", + "Actual:", + " c: hello world", + }, diff) +} + +func TestIsSubsetValidatorMultiManifestWhenBothFail(t *testing.T) { + manifest1 := makeManifest(docToTestIsSubset) + manifests := []common.K8sManifest{manifest1, manifest1} + + validator := IsSubsetValidator{ + "a.b", + map[interface{}]interface{}{"e": "foo bar"}, + } + pass, diff := validator.Validate(&ValidateContext{ + Docs: manifests, + Index: -1, + }) + + assert.False(t, pass) + assert.Equal(t, []string{ + "DocumentIndex: 0", + "Path: a.b", + "Expected to contain:", + " e: foo bar", + "Actual:", + " c: hello world", + " d: foo bar", + "DocumentIndex: 1", + "Path: a.b", + "Expected to contain:", + " e: foo bar", + "Actual:", + " c: hello world", + " d: foo bar", + }, diff) +} + +func TestIsSubsetValidatorWhenNegativeAndFail(t *testing.T) { + manifest := makeManifest(docToTestIsSubset) + + validator := IsSubsetValidator{ + "a.b", + map[interface{}]interface{}{"d": "foo bar"}, + } + pass, diff := validator.Validate(&ValidateContext{ + Docs: []common.K8sManifest{manifest}, + Negative: true, + }) + + assert.False(t, pass) + assert.Equal(t, []string{ + "DocumentIndex: 0", + "Path: a.b", + "Expected NOT to contain:", + " d: foo bar", + "Actual:", + " c: hello world", + " d: foo bar", + }, diff) +} + +func TestIsSubsetValidatorWhenInvalidIndex(t *testing.T) { + manifest := makeManifest(docToTestIsSubset) + + validator := IsSubsetValidator{"a.b", common.K8sManifest{"d": "foo bar"}} + pass, diff := validator.Validate(&ValidateContext{ + Docs: []common.K8sManifest{manifest}, + Index: 2, + }) + + assert.False(t, pass) + assert.Equal(t, []string{ + "Error:", + " documentIndex 2 out of range", + }, diff) +} + +func TestIsSubsetValidatorWhenNotAnObject(t *testing.T) { + manifestDocNotObject := ` +a: + b: + c: hello world + d: foo bar +` + manifest := makeManifest(manifestDocNotObject) + + validator := IsSubsetValidator{"a.b.c", common.K8sManifest{"d": "foo bar"}} + pass, diff := validator.Validate(&ValidateContext{ + Docs: []common.K8sManifest{manifest}, + }) + + assert.False(t, pass) + assert.Equal(t, []string{ + "DocumentIndex: 0", + "Error:", + " expect 'a.b.c' to be an object, got:", + " hello world", + }, diff) +} + +func TestIsSubsetValidatorWhenInvalidPath(t *testing.T) { + manifest := makeManifest("a::error") + + validator := IsSubsetValidator{"a.b", common.K8sManifest{"d": "foo bar"}} + pass, diff := validator.Validate(&ValidateContext{ + Docs: []common.K8sManifest{manifest}, + }) + + assert.False(t, pass) + assert.Equal(t, []string{ + "DocumentIndex: 0", + "Error:", + " can't get [\"b\"] from a non map type:", + " null", + }, diff) +} diff --git a/unittest/validators/match_regex_raw_validator.go b/unittest/validators/match_regex_raw_validator.go new file mode 100644 index 000000000..f5436131a --- /dev/null +++ b/unittest/validators/match_regex_raw_validator.go @@ -0,0 +1,58 @@ +package validators + +import ( + "regexp" + + "github.com/lrills/helm-unittest/unittest/common" +) + +// MatchRegexRawValidator validate value of Path match Pattern +type MatchRegexRawValidator struct { + Pattern string +} + +func (v MatchRegexRawValidator) failInfo(actual string, not bool) []string { + var notAnnotation = "" + if not { + notAnnotation = " NOT" + } + regexFailFormat := ` +Expected` + notAnnotation + ` to match:%s +Actual:%s +` + return splitInfof(regexFailFormat, -1, v.Pattern, actual) +} + +// Validate implement Validatable +func (v MatchRegexRawValidator) Validate(context *ValidateContext) (bool, []string) { + manifests, err := context.getManifests() + if err != nil { + return false, splitInfof(errorFormat, -1, err.Error()) + } + + validateSuccess := true + validateErrors := make([]string, 0) + + for _, manifest := range manifests { + actual := uniformContent(manifest[common.RAW]) + + p, err := regexp.Compile(v.Pattern) + if err != nil { + validateSuccess = validateSuccess && false + errorMessage := splitInfof(errorFormat, -1, err.Error()) + validateErrors = append(validateErrors, errorMessage...) + break + } + + if p.MatchString(actual) == context.Negative { + validateSuccess = validateSuccess && false + errorMessage := v.failInfo(actual, context.Negative) + validateErrors = append(validateErrors, errorMessage...) + continue + } + + validateSuccess = validateSuccess && true + } + + return validateSuccess, validateErrors +} diff --git a/unittest/validators/match_regex_raw_validator_test.go b/unittest/validators/match_regex_raw_validator_test.go new file mode 100644 index 000000000..009df13b3 --- /dev/null +++ b/unittest/validators/match_regex_raw_validator_test.go @@ -0,0 +1,99 @@ +package validators_test + +import ( + "testing" + + . "github.com/lrills/helm-unittest/unittest/validators" + + "github.com/lrills/helm-unittest/unittest/common" + "github.com/stretchr/testify/assert" +) + +var docToTestMatchRegexRaw = ` +raw: | + This is a NOTES.txt document. +` + +func TestMatchRegexRawValidatorWhenOk(t *testing.T) { + manifest := makeManifest(docToTestMatchRegexRaw) + + validator := MatchRegexRawValidator{"^This"} + pass, diff := validator.Validate(&ValidateContext{ + Docs: []common.K8sManifest{manifest}, + }) + + assert.True(t, pass) + assert.Equal(t, []string{}, diff) +} + +func TestMatchRegexRawValidatorWhenNegativeAndOk(t *testing.T) { + manifest := makeManifest(docToTestMatchRegexRaw) + + validator := MatchRegexRawValidator{"^foo"} + pass, diff := validator.Validate(&ValidateContext{ + Docs: []common.K8sManifest{manifest}, + Negative: true, + }) + assert.True(t, pass) + assert.Equal(t, []string{}, diff) +} + +func TestMatchRegexRawValidatorWhenRegexCompileFail(t *testing.T) { + manifest := common.K8sManifest{"raw": ""} + + validator := MatchRegexRawValidator{"+"} + pass, diff := validator.Validate(&ValidateContext{ + Docs: []common.K8sManifest{manifest}, + }) + assert.False(t, pass) + assert.Equal(t, []string{ + "Error:", + " error parsing regexp: missing argument to repetition operator: `+`", + }, diff) +} + +func TestMatchRegexRawValidatorWhenMatchFail(t *testing.T) { + manifest := makeManifest(docToTestMatchRegexRaw) + + validator := MatchRegexRawValidator{"^foo"} + pass, diff := validator.Validate(&ValidateContext{ + Docs: []common.K8sManifest{manifest}, + }) + assert.False(t, pass) + assert.Equal(t, []string{ + "Expected to match: ^foo", + "Actual: This is a NOTES.txt document.", + }, diff) +} + +func TestMatchRegexRawValidatorWhenNegativeAndMatchFail(t *testing.T) { + manifest := makeManifest(docToTestMatchRegexRaw) + + validator := MatchRegexRawValidator{"^This"} + pass, diff := validator.Validate(&ValidateContext{ + Docs: []common.K8sManifest{manifest}, + Negative: true, + }) + + assert.False(t, pass) + assert.Equal(t, []string{ + "Expected NOT to match: ^This", + "Actual: This is a NOTES.txt document.", + }, diff) +} + +func TestMatchRegexRawValidatorWhenInvalidIndex(t *testing.T) { + manifest := makeManifest(docToTestMatchRegexRaw) + + validator := MatchRegexRawValidator{"^This"} + pass, diff := validator.Validate(&ValidateContext{ + Docs: []common.K8sManifest{manifest}, + Index: 2, + }) + + assert.False(t, pass) + assert.Equal(t, []string{ + "Error:", + " documentIndex 2 out of range", + }, diff) +} diff --git a/unittest/validators/snapshot_raw_validator.go b/unittest/validators/snapshot_raw_validator.go new file mode 100644 index 000000000..623e930a8 --- /dev/null +++ b/unittest/validators/snapshot_raw_validator.go @@ -0,0 +1,57 @@ +package validators + +import ( + "strconv" + + "github.com/lrills/helm-unittest/unittest/common" + "github.com/lrills/helm-unittest/unittest/snapshot" +) + +// MatchSnapshotRawValidator validate snapshot of value of Path the same as cached +type MatchSnapshotRawValidator struct{} + +func (v MatchSnapshotRawValidator) failInfo(compared *snapshot.CompareResult, not bool) []string { + var notAnnotation = "" + if not { + notAnnotation = " NOT" + } + snapshotFailFormat := ` +Expected` + notAnnotation + ` to match snapshot ` + strconv.Itoa(int(compared.Index)) + `: +%s +` + var infoToShow string + if not { + infoToShow = compared.CachedSnapshot + } else { + infoToShow = diff(compared.CachedSnapshot, compared.NewSnapshot) + } + return splitInfof(snapshotFailFormat, -1, infoToShow) +} + +// Validate implement Validatable +func (v MatchSnapshotRawValidator) Validate(context *ValidateContext) (bool, []string) { + manifests, err := context.getManifests() + if err != nil { + return false, splitInfof(errorFormat, -1, err.Error()) + } + + validateSuccess := true + validateErrors := make([]string, 0) + + for _, manifest := range manifests { + actual := uniformContent(manifest[common.RAW]) + + result := context.CompareToSnapshot(actual) + + if result.Passed == context.Negative { + validateSuccess = validateSuccess && false + errorMessage := v.failInfo(result, context.Negative) + validateErrors = append(validateErrors, errorMessage...) + continue + } + + validateSuccess = validateSuccess == true + } + + return validateSuccess, validateErrors +} diff --git a/unittest/validators/snapshot_raw_validator_test.go b/unittest/validators/snapshot_raw_validator_test.go new file mode 100644 index 000000000..5e3e9b165 --- /dev/null +++ b/unittest/validators/snapshot_raw_validator_test.go @@ -0,0 +1,125 @@ +package validators_test + +import ( + "testing" + + "github.com/lrills/helm-unittest/unittest/common" + "github.com/lrills/helm-unittest/unittest/snapshot" + . "github.com/lrills/helm-unittest/unittest/validators" + "github.com/stretchr/testify/assert" +) + +func TestSnapshotRawValidatorWhenOk(t *testing.T) { + data := common.K8sManifest{common.RAW: "b"} + validator := MatchSnapshotRawValidator{} + + mockComparer := new(mockSnapshotComparer) + mockComparer.On("CompareToSnapshot", "b").Return(&snapshot.CompareResult{ + Passed: true, + }) + + pass, diff := validator.Validate(&ValidateContext{ + Docs: []common.K8sManifest{data}, + SnapshotComparer: mockComparer, + }) + + assert.True(t, pass) + assert.Equal(t, []string{}, diff) + + mockComparer.AssertExpectations(t) +} + +func TestSnapshotRawValidatorWhenNegativeAndOk(t *testing.T) { + data := common.K8sManifest{common.RAW: "b"} + validator := MatchSnapshotRawValidator{} + + mockComparer := new(mockSnapshotComparer) + mockComparer.On("CompareToSnapshot", "b").Return(&snapshot.CompareResult{ + Passed: false, + CachedSnapshot: "b\n", + NewSnapshot: "x\n", + }) + + pass, diff := validator.Validate(&ValidateContext{ + Negative: true, + Docs: []common.K8sManifest{data}, + SnapshotComparer: mockComparer, + }) + + assert.True(t, pass) + assert.Equal(t, []string{}, diff) + + mockComparer.AssertExpectations(t) +} + +func TestSnapshotRawValidatorWhenFail(t *testing.T) { + data := common.K8sManifest{common.RAW: "b"} + validator := MatchSnapshotRawValidator{} + + mockComparer := new(mockSnapshotComparer) + mockComparer.On("CompareToSnapshot", "b").Return(&snapshot.CompareResult{ + Passed: false, + CachedSnapshot: "b\n", + NewSnapshot: "x\n", + }) + + pass, diff := validator.Validate(&ValidateContext{ + Docs: []common.K8sManifest{data}, + SnapshotComparer: mockComparer, + }) + + assert.False(t, pass) + assert.Equal(t, []string{ + "Expected to match snapshot 0:", + " --- Expected", + " +++ Actual", + " @@ -1,2 +1,2 @@", + " -b", + " +x", + }, diff) + + mockComparer.AssertExpectations(t) +} + +func TestSnapshotRawValidatorWhenNegativeAndFail(t *testing.T) { + data := common.K8sManifest{common.RAW: "b"} + validator := MatchSnapshotRawValidator{} + + cached := "b\n" + mockComparer := new(mockSnapshotComparer) + mockComparer.On("CompareToSnapshot", "b").Return(&snapshot.CompareResult{ + Passed: true, + CachedSnapshot: cached, + NewSnapshot: cached, + }) + + pass, diff := validator.Validate(&ValidateContext{ + Negative: true, + Docs: []common.K8sManifest{data}, + SnapshotComparer: mockComparer, + }) + + assert.False(t, pass) + assert.Equal(t, []string{ + "Expected NOT to match snapshot 0:", + " b", + }, diff) + + mockComparer.AssertExpectations(t) +} + +func TestSnapshotRawValidatorWhenInvalidIndex(t *testing.T) { + data := common.K8sManifest{common.RAW: "b"} + + validator := MatchSnapshotRawValidator{} + pass, diff := validator.Validate(&ValidateContext{ + Docs: []common.K8sManifest{data}, + Index: 2, + }) + + assert.False(t, pass) + assert.Equal(t, []string{ + "Error:", + " documentIndex 2 out of range", + }, diff) +} diff --git a/unittest/validators/snapshot_validator_test.go b/unittest/validators/snapshot_validator_test.go index 6df040f30..05b201c51 100644 --- a/unittest/validators/snapshot_validator_test.go +++ b/unittest/validators/snapshot_validator_test.go @@ -7,18 +7,8 @@ import ( "github.com/lrills/helm-unittest/unittest/snapshot" . "github.com/lrills/helm-unittest/unittest/validators" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" ) -type mockSnapshotComparer struct { - mock.Mock -} - -func (m *mockSnapshotComparer) CompareToSnapshot(content interface{}) *snapshot.CompareResult { - args := m.Called(content) - return args.Get(0).(*snapshot.CompareResult) -} - func TestSnapshotValidatorWhenOk(t *testing.T) { data := common.K8sManifest{"a": "b"} validator := MatchSnapshotValidator{Path: "a"}