diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml new file mode 100644 index 0000000..de80258 --- /dev/null +++ b/.github/workflows/unit-tests.yml @@ -0,0 +1,43 @@ +# +# Copyright (c) 2020-2024 Red Hat, Inc. +# This program and the accompanying materials are made +# available under the terms of the Eclipse Public License 2.0 +# which is available at https://www.eclipse.org/legal/epl-2.0/ +# +# SPDX-License-Identifier: EPL-2.0 +# +name: Unit Tests & Coverage + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + unit-tests: + name: Run unit tests + runs-on: ubuntu-24.04 + permissions: + id-token: write + + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + + - name: Run unit tests with coverage + run: make test-coverage + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v6 + with: + use_oidc: true + flags: unit-tests + files: ./coverage.out + fail_ci_if_error: false diff --git a/.gitignore b/.gitignore index 0a3d36b..b2c318c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .idea _output generated +coverage.out diff --git a/Makefile b/Makefile index 86e6d65..6cc47f0 100644 --- a/Makefile +++ b/Makefile @@ -18,6 +18,17 @@ include build/makefiles/version.mk all: help +.PHONY: test test-coverage + +### test: Run unit tests +test: + go test -v ./pkg/... + +### test-coverage: Run unit tests with coverage report +test-coverage: + go test -v -coverprofile=coverage.out -covermode=atomic ./pkg/... + go tool cover -func=coverage.out + _print_vars: echo "Current env vars:" echo " WTO_IMG=$(WTO_IMG)" diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..a0ecadb --- /dev/null +++ b/codecov.yml @@ -0,0 +1,34 @@ +codecov: + require_ci_to_pass: yes + +coverage: + precision: 2 + round: down + range: "50...100" + + status: + project: + default: + target: auto + threshold: 1% + patch: + default: + target: auto + threshold: 1% + +ignore: + - "vendor/**" + - "build/**" + - "hack/**" + - "docs/**" + - "manifests/**" + - "metadata/**" + - "dev-scripts/**" + +flags: + unit-tests: + carryforward: true + +comment: + layout: "reach,diff,flags,files" + behavior: default diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go new file mode 100644 index 0000000..baf9270 --- /dev/null +++ b/pkg/config/config_test.go @@ -0,0 +1,91 @@ +// +// Copyright (c) 2021-2024 Red Hat, Inc. +// This program and the accompanying materials are made +// available under the terms of the Eclipse Public License 2.0 +// which is available at https://www.eclipse.org/legal/epl-2.0/ +// +// SPDX-License-Identifier: EPL-2.0 +// + +package config + +import ( + "testing" +) + +func TestGetDefaultToolingImage(t *testing.T) { + tests := []struct { + name string + envVal string + want string + wantErr bool + }{ + { + name: "returns image when env var is set", + envVal: "quay.io/wto/web-terminal-tooling:latest", + want: "quay.io/wto/web-terminal-tooling:latest", + wantErr: false, + }, + { + name: "returns error when env var is unset", + envVal: "", + want: "", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Setenv(toolingImageEnvVar, tt.envVal) + got, err := GetDefaultToolingImage() + if (err != nil) != tt.wantErr { + t.Errorf("GetDefaultToolingImage() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("GetDefaultToolingImage() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestGetDefaultExecImage(t *testing.T) { + tests := []struct { + name string + envVal string + want string + wantErr bool + }{ + { + name: "returns image when env var is set", + envVal: "quay.io/wto/web-terminal-exec:latest", + want: "quay.io/wto/web-terminal-exec:latest", + wantErr: false, + }, + { + name: "returns error when env var is unset", + envVal: "", + want: "", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Setenv(execImageEnvVar, tt.envVal) + got, err := GetDefaultExecImage() + if (err != nil) != tt.wantErr { + t.Errorf("GetDefaultExecImage() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("GetDefaultExecImage() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestGetNamespaceReturnsErrorOutsideCluster(t *testing.T) { + _, err := GetNamespace() + if err == nil { + t.Fatal("expected error when service account namespace file does not exist") + } +} diff --git a/pkg/webterminal/exec_test.go b/pkg/webterminal/exec_test.go new file mode 100644 index 0000000..30a2dfd --- /dev/null +++ b/pkg/webterminal/exec_test.go @@ -0,0 +1,157 @@ +// +// Copyright (c) 2021-2024 Red Hat, Inc. +// This program and the accompanying materials are made +// available under the terms of the Eclipse Public License 2.0 +// which is available at https://www.eclipse.org/legal/epl-2.0/ +// +// SPDX-License-Identifier: EPL-2.0 +// + +package webterminal + +import ( + "testing" + + dw "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/redhat-developer/web-terminal-operator/pkg/config" +) + +func makeExecTemplate(image string) *dw.DevWorkspaceTemplate { + return &dw.DevWorkspaceTemplate{ + ObjectMeta: v1.ObjectMeta{ + Name: config.ExecTemplateName, + Namespace: "test-ns", + }, + Spec: dw.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: dw.DevWorkspaceTemplateSpecContent{ + Components: []dw.Component{ + { + Name: config.ExecTemplateName, + ComponentUnion: dw.ComponentUnion{ + Container: &dw.ContainerComponent{ + Container: dw.Container{ + Image: image, + }, + }, + }, + }, + }, + }, + }, + } +} + +func TestHandleUnmanagedExecState(t *testing.T) { + const defaultImage = "quay.io/wto/web-terminal-exec:latest" + + tests := []struct { + name string + specImage string + clusterImage string + expectedImage string + }{ + { + name: "same image returns cluster unchanged", + specImage: defaultImage, + clusterImage: defaultImage, + expectedImage: defaultImage, + }, + { + name: "same repo different tag updates image", + specImage: defaultImage, + clusterImage: "quay.io/wto/web-terminal-exec:old", + expectedImage: defaultImage, + }, + { + name: "same repo different digest updates image", + specImage: defaultImage, + clusterImage: "quay.io/wto/web-terminal-exec@sha256:abc123", + expectedImage: defaultImage, + }, + { + name: "different repo does not update", + specImage: defaultImage, + clusterImage: "quay.io/other/custom-exec:latest", + expectedImage: "quay.io/other/custom-exec:latest", + }, + { + name: "empty cluster image returns cluster unchanged", + specImage: defaultImage, + clusterImage: "", + expectedImage: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Setenv("RELATED_IMAGE_web_terminal_exec", tt.specImage) + + spec := makeExecTemplate(tt.specImage) + cluster := makeExecTemplate(tt.clusterImage) + + result := handleUnmanagedExecState(spec, cluster) + + var resultImage string + for _, component := range result.Spec.Components { + if component.Name == config.ExecTemplateName && component.Container != nil { + resultImage = component.Container.Image + } + } + if resultImage != tt.expectedImage { + t.Errorf("expected image %q, got %q", tt.expectedImage, resultImage) + } + }) + } +} + +func TestHandleUnmanagedExecStateEnvUnset(t *testing.T) { + t.Setenv("RELATED_IMAGE_web_terminal_exec", "") + + spec := makeExecTemplate("any-image") + cluster := makeExecTemplate("quay.io/wto/web-terminal-exec:old") + + result := handleUnmanagedExecState(spec, cluster) + + var resultImage string + for _, component := range result.Spec.Components { + if component.Name == config.ExecTemplateName && component.Container != nil { + resultImage = component.Container.Image + } + } + if resultImage != "quay.io/wto/web-terminal-exec:old" { + t.Errorf("expected cluster image unchanged when env var unset, got %q", resultImage) + } +} + +func TestGetSpecExecTemplate(t *testing.T) { + const testImage = "quay.io/wto/web-terminal-exec:test" + t.Setenv("RELATED_IMAGE_web_terminal_exec", testImage) + + dwt, err := getSpecExecTemplate("test-namespace") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if dwt.Name != config.ExecTemplateName { + t.Errorf("expected name %q, got %q", config.ExecTemplateName, dwt.Name) + } + if dwt.Namespace != "test-namespace" { + t.Errorf("expected namespace %q, got %q", "test-namespace", dwt.Namespace) + } + if len(dwt.Spec.Components) != 1 { + t.Fatalf("expected 1 component, got %d", len(dwt.Spec.Components)) + } + if dwt.Spec.Components[0].Container.Image != testImage { + t.Errorf("expected image %q, got %q", testImage, dwt.Spec.Components[0].Container.Image) + } +} + +func TestGetSpecExecTemplateEnvUnset(t *testing.T) { + t.Setenv("RELATED_IMAGE_web_terminal_exec", "") + + _, err := getSpecExecTemplate("test-namespace") + if err == nil { + t.Error("expected error when env var is unset") + } +} diff --git a/pkg/webterminal/tooling_test.go b/pkg/webterminal/tooling_test.go new file mode 100644 index 0000000..07b5b7e --- /dev/null +++ b/pkg/webterminal/tooling_test.go @@ -0,0 +1,157 @@ +// +// Copyright (c) 2021-2024 Red Hat, Inc. +// This program and the accompanying materials are made +// available under the terms of the Eclipse Public License 2.0 +// which is available at https://www.eclipse.org/legal/epl-2.0/ +// +// SPDX-License-Identifier: EPL-2.0 +// + +package webterminal + +import ( + "testing" + + dw "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/redhat-developer/web-terminal-operator/pkg/config" +) + +func makeToolingTemplate(image string) *dw.DevWorkspaceTemplate { + return &dw.DevWorkspaceTemplate{ + ObjectMeta: v1.ObjectMeta{ + Name: config.ToolingTemplateName, + Namespace: "test-ns", + }, + Spec: dw.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: dw.DevWorkspaceTemplateSpecContent{ + Components: []dw.Component{ + { + Name: config.ToolingTemplateName, + ComponentUnion: dw.ComponentUnion{ + Container: &dw.ContainerComponent{ + Container: dw.Container{ + Image: image, + }, + }, + }, + }, + }, + }, + }, + } +} + +func TestHandleUnmanagedToolingState(t *testing.T) { + const defaultImage = "quay.io/wto/web-terminal-tooling:latest" + + tests := []struct { + name string + specImage string + clusterImage string + expectedImage string + }{ + { + name: "same image returns cluster unchanged", + specImage: defaultImage, + clusterImage: defaultImage, + expectedImage: defaultImage, + }, + { + name: "same repo different tag updates image", + specImage: defaultImage, + clusterImage: "quay.io/wto/web-terminal-tooling:old", + expectedImage: defaultImage, + }, + { + name: "same repo different digest updates image", + specImage: defaultImage, + clusterImage: "quay.io/wto/web-terminal-tooling@sha256:abc123", + expectedImage: defaultImage, + }, + { + name: "different repo does not update", + specImage: defaultImage, + clusterImage: "quay.io/other/custom-tooling:latest", + expectedImage: "quay.io/other/custom-tooling:latest", + }, + { + name: "empty cluster image returns cluster unchanged", + specImage: defaultImage, + clusterImage: "", + expectedImage: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Setenv("RELATED_IMAGE_web_terminal_tooling", tt.specImage) + + spec := makeToolingTemplate(tt.specImage) + cluster := makeToolingTemplate(tt.clusterImage) + + result := handleUnmanagedToolingState(spec, cluster) + + var resultImage string + for _, component := range result.Spec.Components { + if component.Name == config.ToolingTemplateName && component.Container != nil { + resultImage = component.Container.Image + } + } + if resultImage != tt.expectedImage { + t.Errorf("expected image %q, got %q", tt.expectedImage, resultImage) + } + }) + } +} + +func TestHandleUnmanagedToolingStateEnvUnset(t *testing.T) { + t.Setenv("RELATED_IMAGE_web_terminal_tooling", "") + + spec := makeToolingTemplate("any-image") + cluster := makeToolingTemplate("quay.io/wto/web-terminal-tooling:old") + + result := handleUnmanagedToolingState(spec, cluster) + + var resultImage string + for _, component := range result.Spec.Components { + if component.Name == config.ToolingTemplateName && component.Container != nil { + resultImage = component.Container.Image + } + } + if resultImage != "quay.io/wto/web-terminal-tooling:old" { + t.Errorf("expected cluster image unchanged when env var unset, got %q", resultImage) + } +} + +func TestGetSpecToolingTemplate(t *testing.T) { + const testImage = "quay.io/wto/web-terminal-tooling:test" + t.Setenv("RELATED_IMAGE_web_terminal_tooling", testImage) + + dwt, err := getSpecToolingTemplate("test-namespace") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if dwt.Name != config.ToolingTemplateName { + t.Errorf("expected name %q, got %q", config.ToolingTemplateName, dwt.Name) + } + if dwt.Namespace != "test-namespace" { + t.Errorf("expected namespace %q, got %q", "test-namespace", dwt.Namespace) + } + if len(dwt.Spec.Components) != 1 { + t.Fatalf("expected 1 component, got %d", len(dwt.Spec.Components)) + } + if dwt.Spec.Components[0].Container.Image != testImage { + t.Errorf("expected image %q, got %q", testImage, dwt.Spec.Components[0].Container.Image) + } +} + +func TestGetSpecToolingTemplateEnvUnset(t *testing.T) { + t.Setenv("RELATED_IMAGE_web_terminal_tooling", "") + + _, err := getSpecToolingTemplate("test-namespace") + if err == nil { + t.Error("expected error when env var is unset") + } +}