From 7d23ad16827ebec69fec7d56dafe9133e8bf89c8 Mon Sep 17 00:00:00 2001 From: "Hen Schwartz (EXT-Nokia)" Date: Mon, 9 Jun 2025 14:54:14 +0300 Subject: [PATCH] test(ws): Ensure test files exist in backend for any executed code #381 Signed-off-by: Hen Schwartz (EXT-Nokia) --- .../internal/models/health_check/types.go | 7 + .../models/workspacekinds/suite_test.go | 288 +++++++++ .../internal/models/workspaces/funcs_test.go | 574 ++++++++++++++++++ .../internal/models/workspaces/suite_test.go | 29 + .../repositories/health_check/repo.go | 11 +- .../repositories/health_check/suite_test.go | 63 ++ 6 files changed, 963 insertions(+), 9 deletions(-) create mode 100644 workspaces/backend/internal/models/workspacekinds/suite_test.go create mode 100644 workspaces/backend/internal/models/workspaces/funcs_test.go create mode 100644 workspaces/backend/internal/models/workspaces/suite_test.go create mode 100644 workspaces/backend/internal/repositories/health_check/suite_test.go diff --git a/workspaces/backend/internal/models/health_check/types.go b/workspaces/backend/internal/models/health_check/types.go index 8f33a4093..1317cb220 100644 --- a/workspaces/backend/internal/models/health_check/types.go +++ b/workspaces/backend/internal/models/health_check/types.go @@ -31,3 +31,10 @@ const ( ServiceStatusHealthy ServiceStatus = "Healthy" ServiceStatusUnhealthy ServiceStatus = "Unhealthy" ) + +func NewHealthCheck(status ServiceStatus, version string) HealthCheck { + return HealthCheck{ + Status: status, + SystemInfo: SystemInfo{Version: version}, + } +} diff --git a/workspaces/backend/internal/models/workspacekinds/suite_test.go b/workspaces/backend/internal/models/workspacekinds/suite_test.go new file mode 100644 index 000000000..3357bc617 --- /dev/null +++ b/workspaces/backend/internal/models/workspacekinds/suite_test.go @@ -0,0 +1,288 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package workspacekinds_test + +import ( + "testing" + + kubefloworgv1beta1 "github.com/kubeflow/notebooks/workspaces/controller/api/v1beta1" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" + + workspacekinds "github.com/kubeflow/notebooks/workspaces/backend/internal/models/workspacekinds" +) + +func TestWorkspaceKinds(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "WorkspaceKinds Suite") +} + +type workspaceKindTestCase struct { + name string + workspaceKind *kubefloworgv1beta1.WorkspaceKind + expectedName string + expectedDisplayName string + expectedDescription string + expectedDeprecated bool + expectedDeprecationMsg string + expectedHidden bool + expectedIconURL string + expectedLogoURL string + expectedPodLabels map[string]string + expectedPodAnnotations map[string]string + additionalValidations func(result workspacekinds.WorkspaceKind) +} + +var _ = Describe("WorkspaceKind Types", func() { + Describe("NewWorkspaceKindModelFromWorkspaceKind", func() { + DescribeTable("should create WorkspaceKind models correctly", + func(tc workspaceKindTestCase) { + result := workspacekinds.NewWorkspaceKindModelFromWorkspaceKind(tc.workspaceKind) + + // Basic assertions that apply to all test cases + Expect(result).ToNot(BeNil()) + Expect(result.Name).To(Equal(tc.expectedName)) + Expect(result.DisplayName).To(Equal(tc.expectedDisplayName)) + Expect(result.Description).To(Equal(tc.expectedDescription)) + Expect(result.Deprecated).To(Equal(tc.expectedDeprecated)) + Expect(result.DeprecationMessage).To(Equal(tc.expectedDeprecationMsg)) + Expect(result.Hidden).To(Equal(tc.expectedHidden)) + Expect(result.Icon.URL).To(Equal(tc.expectedIconURL)) + Expect(result.Logo.URL).To(Equal(tc.expectedLogoURL)) + + // Pod metadata assertions + if tc.expectedPodLabels != nil { + for key, value := range tc.expectedPodLabels { + Expect(result.PodTemplate.PodMetadata.Labels).To(HaveKeyWithValue(key, value)) + } + } + if tc.expectedPodAnnotations != nil { + for key, value := range tc.expectedPodAnnotations { + Expect(result.PodTemplate.PodMetadata.Annotations).To(HaveKeyWithValue(key, value)) + } + } + + // Run any additional custom validations + if tc.additionalValidations != nil { + tc.additionalValidations(result) + } + }, + + Entry("complete WorkspaceKind with all fields", workspaceKindTestCase{ + name: "complete WorkspaceKind", + workspaceKind: &kubefloworgv1beta1.WorkspaceKind{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-workspacekind", + Namespace: "kubeflow", + Labels: map[string]string{ + "app": "workspacekind", + "version": "v1", + }, + Annotations: map[string]string{ + "description": "Complete workspacekind", + }, + }, + Spec: kubefloworgv1beta1.WorkspaceKindSpec{ + Spawner: kubefloworgv1beta1.WorkspaceKindSpawner{ + DisplayName: "Complete Workspacekind", + Description: "A complete test workspacekind with all features", + Deprecated: ptr.To(false), + Hidden: ptr.To(false), + Icon: kubefloworgv1beta1.WorkspaceKindIcon{ + Url: ptr.To("/workspaces/backend/api/v1/workspacekinds/test-workspacekind/assets/icon"), + }, + Logo: kubefloworgv1beta1.WorkspaceKindIcon{ + Url: ptr.To("/workspaces/backend/api/v1/workspacekinds/test-workspacekind/assets/logo"), + }, + }, + PodTemplate: kubefloworgv1beta1.WorkspaceKindPodTemplate{ + PodMetadata: &kubefloworgv1beta1.WorkspaceKindPodMetadata{ + Labels: map[string]string{ + "app": "test-workspacekind", + }, + Annotations: map[string]string{ + "annotation-key": "annotation-value", + }, + }, + }, + }, + }, + expectedName: "test-workspacekind", + expectedDisplayName: "Complete Workspacekind", + expectedDescription: "A complete test workspacekind with all features", + expectedDeprecated: false, + expectedDeprecationMsg: "", + expectedHidden: false, + expectedIconURL: "/workspaces/backend/api/v1/workspacekinds/test-workspacekind/assets/icon", + expectedLogoURL: "/workspaces/backend/api/v1/workspacekinds/test-workspacekind/assets/logo", + expectedPodLabels: map[string]string{"app": "test-workspacekind"}, + expectedPodAnnotations: map[string]string{"annotation-key": "annotation-value"}, + }), + + Entry("empty strings in required fields", workspaceKindTestCase{ + name: "empty strings in required fields", + workspaceKind: &kubefloworgv1beta1.WorkspaceKind{ + ObjectMeta: metav1.ObjectMeta{ + Name: "empty-fields-workspace", + }, + Spec: kubefloworgv1beta1.WorkspaceKindSpec{ + Spawner: kubefloworgv1beta1.WorkspaceKindSpawner{ + DisplayName: "", + Description: "", + }, + }, + }, + expectedName: "empty-fields-workspace", + expectedDisplayName: "", + expectedDescription: "", + expectedDeprecated: false, + expectedDeprecationMsg: "", + expectedHidden: false, + expectedIconURL: "/workspaces/backend/api/v1/workspacekinds/empty-fields-workspace/assets/icon", + expectedLogoURL: "/workspaces/backend/api/v1/workspacekinds/empty-fields-workspace/assets/logo", + }), + + Entry("special characters in names", workspaceKindTestCase{ + name: "special characters in names", + workspaceKind: &kubefloworgv1beta1.WorkspaceKind{ + ObjectMeta: metav1.ObjectMeta{ + Name: "special-chars_workspace-123", + }, + Spec: kubefloworgv1beta1.WorkspaceKindSpec{ + Spawner: kubefloworgv1beta1.WorkspaceKindSpawner{ + DisplayName: "Special-Chars_Workspace 123!", + Description: "Workspace with special characters in name", + }, + }, + }, + expectedName: "special-chars_workspace-123", + expectedDisplayName: "Special-Chars_Workspace 123!", + expectedDescription: "Workspace with special characters in name", + expectedDeprecated: false, + expectedDeprecationMsg: "", + expectedHidden: false, + expectedIconURL: "/workspaces/backend/api/v1/workspacekinds/special-chars_workspace-123/assets/icon", + expectedLogoURL: "/workspaces/backend/api/v1/workspacekinds/special-chars_workspace-123/assets/logo", + }), + + Entry("long descriptions", workspaceKindTestCase{ + name: "long descriptions", + workspaceKind: &kubefloworgv1beta1.WorkspaceKind{ + ObjectMeta: metav1.ObjectMeta{ + Name: "long-description-workspace", + }, + Spec: kubefloworgv1beta1.WorkspaceKindSpec{ + Spawner: kubefloworgv1beta1.WorkspaceKindSpawner{ + DisplayName: "Long Description Workspace", + Description: "This is a very long description that contains multiple sentences. " + + "It describes a workspace kind that has many features and capabilities. " + + "The description should be preserved exactly as provided without truncation. " + + "This tests the ability to handle larger text fields in the workspace kind model.", + }, + }, + }, + expectedName: "long-description-workspace", + expectedDisplayName: "Long Description Workspace", + expectedDescription: "This is a very long description that contains multiple sentences. " + + "It describes a workspace kind that has many features and capabilities. " + + "The description should be preserved exactly as provided without truncation. " + + "This tests the ability to handle larger text fields in the workspace kind model.", + expectedDeprecated: false, + expectedDeprecationMsg: "", + expectedHidden: false, + expectedIconURL: "/workspaces/backend/api/v1/workspacekinds/long-description-workspace/assets/icon", + expectedLogoURL: "/workspaces/backend/api/v1/workspacekinds/long-description-workspace/assets/logo", + additionalValidations: func(result workspacekinds.WorkspaceKind) { + Expect(len(result.Description)).To(BeNumerically(">", 200)) + }, + }), + + Entry("deprecated WorkspaceKind", workspaceKindTestCase{ + name: "deprecated WorkspaceKind", + workspaceKind: &kubefloworgv1beta1.WorkspaceKind{ + ObjectMeta: metav1.ObjectMeta{ + Name: "deprecated-workspace", + }, + Spec: kubefloworgv1beta1.WorkspaceKindSpec{ + Spawner: kubefloworgv1beta1.WorkspaceKindSpawner{ + DisplayName: "Deprecated Workspace", + Description: "This workspace kind is deprecated", + Deprecated: ptr.To(true), + DeprecationMessage: ptr.To("Use new-workspace instead"), + }, + }, + }, + expectedName: "deprecated-workspace", + expectedDisplayName: "Deprecated Workspace", + expectedDescription: "This workspace kind is deprecated", + expectedDeprecated: true, + expectedDeprecationMsg: "Use new-workspace instead", + expectedHidden: false, + expectedIconURL: "/workspaces/backend/api/v1/workspacekinds/deprecated-workspace/assets/icon", + expectedLogoURL: "/workspaces/backend/api/v1/workspacekinds/deprecated-workspace/assets/logo", + }), + + Entry("hidden WorkspaceKind", workspaceKindTestCase{ + name: "hidden WorkspaceKind", + workspaceKind: &kubefloworgv1beta1.WorkspaceKind{ + ObjectMeta: metav1.ObjectMeta{ + Name: "hidden-workspace", + }, + Spec: kubefloworgv1beta1.WorkspaceKindSpec{ + Spawner: kubefloworgv1beta1.WorkspaceKindSpawner{ + DisplayName: "Hidden Workspace", + Description: "This workspace kind is hidden from UI", + Hidden: ptr.To(true), + }, + }, + }, + expectedName: "hidden-workspace", + expectedDisplayName: "Hidden Workspace", + expectedDescription: "This workspace kind is hidden from UI", + expectedDeprecated: false, + expectedDeprecationMsg: "", + expectedHidden: true, + expectedIconURL: "/workspaces/backend/api/v1/workspacekinds/hidden-workspace/assets/icon", + expectedLogoURL: "/workspaces/backend/api/v1/workspacekinds/hidden-workspace/assets/logo", + }), + + Entry("minimal WorkspaceKind with only required fields", workspaceKindTestCase{ + name: "minimal WorkspaceKind", + workspaceKind: &kubefloworgv1beta1.WorkspaceKind{ + ObjectMeta: metav1.ObjectMeta{ + Name: "minimal-workspace", + }, + Spec: kubefloworgv1beta1.WorkspaceKindSpec{ + Spawner: kubefloworgv1beta1.WorkspaceKindSpawner{ + DisplayName: "Minimal Workspace", + }, + }, + }, + expectedName: "minimal-workspace", + expectedDisplayName: "Minimal Workspace", + expectedDescription: "", + expectedDeprecated: false, + expectedDeprecationMsg: "", + expectedHidden: false, + expectedIconURL: "/workspaces/backend/api/v1/workspacekinds/minimal-workspace/assets/icon", + expectedLogoURL: "/workspaces/backend/api/v1/workspacekinds/minimal-workspace/assets/logo", + }), + ) + }) +}) diff --git a/workspaces/backend/internal/models/workspaces/funcs_test.go b/workspaces/backend/internal/models/workspaces/funcs_test.go new file mode 100644 index 000000000..50d0b4e64 --- /dev/null +++ b/workspaces/backend/internal/models/workspaces/funcs_test.go @@ -0,0 +1,574 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package workspaces_test + +import ( + "time" + + kubefloworgv1beta1 "github.com/kubeflow/notebooks/workspaces/controller/api/v1beta1" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" + + workspaces "github.com/kubeflow/notebooks/workspaces/backend/internal/models/workspaces" +) + +// Test constants +const ( + testWorkspaceName = "test-workspace" + testNamespace = "test-namespace" + testWorkspaceKindName = "jupyter-notebook" + testUID = "test-uid" + testHomePVC = "home-pvc" + testHomeMountPath = "/home/jovyan" + testDataPVC1 = "data-pvc-1" + testDataPVC2 = "data-pvc-2" + testDataMountPath1 = "/data1" + testDataMountPath2 = "/data2" + testImageConfigID = "default-image" + testPodConfigID = "default-pod" + testDesiredImageID = "desired-image" + testDesiredPodID = "desired-pod" + testOldImageID = "old-image" + testOldPodID = "old-pod" + testNewImageID = "new-image" + testNewPodID = "new-pod" + testStateMessage = "Running successfully" + testAppLabel = "test-app" + testAnnotationKey = "annotation-key" + testAnnotationValue = "annotation-value" + testEnvironmentLabel = "environment" + testPythonValue = "python" + testSizeLabel = "size" + testSmallValue = "small" + testJupyterPortID = "jupyter" + testJupyterDisplayName = "Jupyter" +) + +// Test data structures +type WorkspaceBuilder struct { + workspace *kubefloworgv1beta1.Workspace +} + +type WorkspaceKindBuilder struct { + workspaceKind *kubefloworgv1beta1.WorkspaceKind +} + +type TestExpectations struct { + Name string + Namespace string + WorkspaceKindName string + WorkspaceKindMissing bool + State workspaces.WorkspaceState + StateMessage string + DeferUpdates bool + Paused bool + PendingRestart bool + HomePVCName string + HomeMountPath string + HomeReadOnly bool + DataVolumeCount int + ImageConfigID string + PodConfigID string + ServiceCount int + Labels map[string]string + Annotations map[string]string +} + +// Workspace builder functions +func NewWorkspaceBuilder() *WorkspaceBuilder { + return &WorkspaceBuilder{ + workspace: &kubefloworgv1beta1.Workspace{ + ObjectMeta: metav1.ObjectMeta{ + Name: testWorkspaceName, + Namespace: testNamespace, + }, + Spec: kubefloworgv1beta1.WorkspaceSpec{ + Kind: testWorkspaceKindName, + DeferUpdates: ptr.To(false), + Paused: ptr.To(false), + }, + Status: kubefloworgv1beta1.WorkspaceStatus{ + State: kubefloworgv1beta1.WorkspaceStateRunning, + StateMessage: testStateMessage, + PendingRestart: false, + }, + }, + } +} + +func (wb *WorkspaceBuilder) WithName(name string) *WorkspaceBuilder { + wb.workspace.Name = name + return wb +} + +func (wb *WorkspaceBuilder) WithNamespace(namespace string) *WorkspaceBuilder { + wb.workspace.Namespace = namespace + return wb +} + +func (wb *WorkspaceBuilder) WithKind(kind string) *WorkspaceBuilder { + wb.workspace.Spec.Kind = kind + return wb +} + +func (wb *WorkspaceBuilder) WithState(state kubefloworgv1beta1.WorkspaceState) *WorkspaceBuilder { + wb.workspace.Status.State = state + return wb +} + +func (wb *WorkspaceBuilder) WithStateMessage(message string) *WorkspaceBuilder { + wb.workspace.Status.StateMessage = message + return wb +} + +func (wb *WorkspaceBuilder) WithPodMetadata(labels, annotations map[string]string) *WorkspaceBuilder { + wb.workspace.Spec.PodTemplate.PodMetadata = &kubefloworgv1beta1.WorkspacePodMetadata{ + Labels: labels, + Annotations: annotations, + } + return wb +} + +func (wb *WorkspaceBuilder) WithHomeVolume(pvcName string) *WorkspaceBuilder { + wb.workspace.Spec.PodTemplate.Volumes.Home = ptr.To(pvcName) + return wb +} + +func (wb *WorkspaceBuilder) WithoutHomeVolume() *WorkspaceBuilder { + wb.workspace.Spec.PodTemplate.Volumes.Home = nil + return wb +} + +func (wb *WorkspaceBuilder) WithDataVolumes(volumes []kubefloworgv1beta1.PodVolumeMount) *WorkspaceBuilder { + wb.workspace.Spec.PodTemplate.Volumes.Data = volumes + return wb +} + +func (wb *WorkspaceBuilder) WithImageConfig(imageConfig string) *WorkspaceBuilder { + wb.workspace.Spec.PodTemplate.Options.ImageConfig = imageConfig + return wb +} + +func (wb *WorkspaceBuilder) WithPodConfig(podConfig string) *WorkspaceBuilder { + wb.workspace.Spec.PodTemplate.Options.PodConfig = podConfig + return wb +} + +func (wb *WorkspaceBuilder) WithDesiredImageConfig(desired string) *WorkspaceBuilder { + wb.workspace.Status.PodTemplateOptions.ImageConfig.Desired = desired + return wb +} + +func (wb *WorkspaceBuilder) WithDesiredPodConfig(desired string) *WorkspaceBuilder { + wb.workspace.Status.PodTemplateOptions.PodConfig.Desired = desired + return wb +} + +func (wb *WorkspaceBuilder) WithImageRedirectChain(chain []kubefloworgv1beta1.WorkspacePodOptionRedirectStep) *WorkspaceBuilder { + wb.workspace.Status.PodTemplateOptions.ImageConfig.RedirectChain = chain + return wb +} + +func (wb *WorkspaceBuilder) WithPodRedirectChain(chain []kubefloworgv1beta1.WorkspacePodOptionRedirectStep) *WorkspaceBuilder { + wb.workspace.Status.PodTemplateOptions.PodConfig.RedirectChain = chain + return wb +} + +func (wb *WorkspaceBuilder) WithActivity(lastActivity, lastUpdate int64) *WorkspaceBuilder { + wb.workspace.Status.Activity = kubefloworgv1beta1.WorkspaceActivity{ + LastActivity: lastActivity, + LastUpdate: lastUpdate, + } + return wb +} + +func (wb *WorkspaceBuilder) Build() *kubefloworgv1beta1.Workspace { + return wb.workspace +} + +// WorkspaceKind builder functions +func NewWorkspaceKindBuilder() *WorkspaceKindBuilder { + return &WorkspaceKindBuilder{ + workspaceKind: &kubefloworgv1beta1.WorkspaceKind{ + ObjectMeta: metav1.ObjectMeta{ + Name: testWorkspaceKindName, + UID: types.UID(testUID), + }, + Spec: kubefloworgv1beta1.WorkspaceKindSpec{ + Spawner: kubefloworgv1beta1.WorkspaceKindSpawner{ + DisplayName: "Jupyter Notebook", + Description: "A Jupyter notebook workspace", + }, + PodTemplate: kubefloworgv1beta1.WorkspaceKindPodTemplate{ + VolumeMounts: kubefloworgv1beta1.WorkspaceKindVolumeMounts{ + Home: testHomeMountPath, + }, + }, + }, + }, + } +} + +func (wkb *WorkspaceKindBuilder) WithName(name string) *WorkspaceKindBuilder { + wkb.workspaceKind.Name = name + return wkb +} + +func (wkb *WorkspaceKindBuilder) WithUID(uid string) *WorkspaceKindBuilder { + wkb.workspaceKind.UID = types.UID(uid) + return wkb +} + +func (wkb *WorkspaceKindBuilder) WithoutUID() *WorkspaceKindBuilder { + wkb.workspaceKind.UID = "" + return wkb +} + +func (wkb *WorkspaceKindBuilder) WithHomeMountPath(path string) *WorkspaceKindBuilder { + wkb.workspaceKind.Spec.PodTemplate.VolumeMounts.Home = path + return wkb +} + +func (wkb *WorkspaceKindBuilder) WithImageConfigs(configs []kubefloworgv1beta1.ImageConfigValue) *WorkspaceKindBuilder { + wkb.workspaceKind.Spec.PodTemplate.Options.ImageConfig.Values = configs + return wkb +} + +func (wkb *WorkspaceKindBuilder) WithPodConfigs(configs []kubefloworgv1beta1.PodConfigValue) *WorkspaceKindBuilder { + wkb.workspaceKind.Spec.PodTemplate.Options.PodConfig.Values = configs + return wkb +} + +func (wkb *WorkspaceKindBuilder) Build() *kubefloworgv1beta1.WorkspaceKind { + return wkb.workspaceKind +} + +// Helper functions for creating common test data +func createDefaultDataVolumes() []kubefloworgv1beta1.PodVolumeMount { + return []kubefloworgv1beta1.PodVolumeMount{ + { + PVCName: testDataPVC1, + MountPath: testDataMountPath1, + ReadOnly: ptr.To(false), + }, + { + PVCName: testDataPVC2, + MountPath: testDataMountPath2, + ReadOnly: ptr.To(true), + }, + } +} + +func createDefaultPodMetadata() (map[string]string, map[string]string) { + labels := map[string]string{testAppLabel: testAppLabel} + annotations := map[string]string{testAnnotationKey: testAnnotationValue} + return labels, annotations +} + +func createDefaultImageConfigs() []kubefloworgv1beta1.ImageConfigValue { + return []kubefloworgv1beta1.ImageConfigValue{ + { + Id: testImageConfigID, + Spawner: kubefloworgv1beta1.OptionSpawnerInfo{ + DisplayName: "Default Image", + Description: ptr.To("Default Jupyter image"), + Labels: []kubefloworgv1beta1.OptionSpawnerLabel{ + {Key: testEnvironmentLabel, Value: testPythonValue}, + }, + }, + Spec: kubefloworgv1beta1.ImageConfigSpec{ + Ports: []kubefloworgv1beta1.ImagePort{ + { + Id: testJupyterPortID, + DisplayName: testJupyterDisplayName, + Protocol: kubefloworgv1beta1.ImagePortProtocolHTTP, + }, + }, + }, + }, + { + Id: testOldImageID, + Spawner: kubefloworgv1beta1.OptionSpawnerInfo{ + DisplayName: "Old Image", + Description: ptr.To("Deprecated image"), + }, + Redirect: &kubefloworgv1beta1.OptionRedirect{ + To: testNewImageID, + Message: &kubefloworgv1beta1.RedirectMessage{ + Text: "This image has been deprecated", + Level: kubefloworgv1beta1.RedirectMessageLevelWarning, + }, + }, + }, + } +} + +func createDefaultPodConfigs() []kubefloworgv1beta1.PodConfigValue { + return []kubefloworgv1beta1.PodConfigValue{ + { + Id: testPodConfigID, + Spawner: kubefloworgv1beta1.OptionSpawnerInfo{ + DisplayName: "Default Pod Config", + Description: ptr.To("Default pod configuration"), + Labels: []kubefloworgv1beta1.OptionSpawnerLabel{ + {Key: testSizeLabel, Value: testSmallValue}, + }, + }, + }, + { + Id: testOldPodID, + Spawner: kubefloworgv1beta1.OptionSpawnerInfo{ + DisplayName: "Old Pod Config", + Description: ptr.To("Deprecated pod config"), + }, + Redirect: &kubefloworgv1beta1.OptionRedirect{ + To: testNewPodID, + Message: &kubefloworgv1beta1.RedirectMessage{ + Text: "This pod config has been deprecated", + Level: kubefloworgv1beta1.RedirectMessageLevelDanger, + }, + }, + }, + } +} + +func createDefaultRedirectChain() []kubefloworgv1beta1.WorkspacePodOptionRedirectStep { + return []kubefloworgv1beta1.WorkspacePodOptionRedirectStep{ + {Source: testOldImageID, Target: testNewImageID}, + } +} + +// Assertion helper functions +func expectBasicWorkspaceFields(result *workspaces.Workspace, expectations *TestExpectations) { + Expect(result.Name).To(Equal(expectations.Name)) + Expect(result.Namespace).To(Equal(expectations.Namespace)) + Expect(result.WorkspaceKind.Name).To(Equal(expectations.WorkspaceKindName)) + Expect(result.WorkspaceKind.Missing).To(Equal(expectations.WorkspaceKindMissing)) + Expect(result.State).To(Equal(expectations.State)) + Expect(result.StateMessage).To(Equal(expectations.StateMessage)) + Expect(result.DeferUpdates).To(Equal(expectations.DeferUpdates)) + Expect(result.Paused).To(Equal(expectations.Paused)) + Expect(result.PendingRestart).To(Equal(expectations.PendingRestart)) +} + +func expectHomeVolume(result *workspaces.Workspace, expectations *TestExpectations) { + if expectations.HomePVCName == "" { + Expect(result.PodTemplate.Volumes.Home).To(BeNil()) + } else { + Expect(result.PodTemplate.Volumes.Home).ToNot(BeNil()) + Expect(result.PodTemplate.Volumes.Home.PVCName).To(Equal(expectations.HomePVCName)) + Expect(result.PodTemplate.Volumes.Home.MountPath).To(Equal(expectations.HomeMountPath)) + Expect(result.PodTemplate.Volumes.Home.ReadOnly).To(Equal(expectations.HomeReadOnly)) + } +} + +func expectDataVolumes(result *workspaces.Workspace, expectations *TestExpectations) { + Expect(result.PodTemplate.Volumes.Data).To(HaveLen(expectations.DataVolumeCount)) +} + +func expectPodMetadata(result *workspaces.Workspace, expectations *TestExpectations) { + if expectations.Labels != nil { + for key, value := range expectations.Labels { + Expect(result.PodTemplate.PodMetadata.Labels).To(HaveKeyWithValue(key, value)) + } + } + if expectations.Annotations != nil { + for key, value := range expectations.Annotations { + Expect(result.PodTemplate.PodMetadata.Annotations).To(HaveKeyWithValue(key, value)) + } + } +} + +func expectServices(result *workspaces.Workspace, expectations *TestExpectations) { + if expectations.ServiceCount == 0 { + Expect(result.Services).To(BeNil()) + } else { + Expect(result.Services).To(HaveLen(expectations.ServiceCount)) + } +} + +func validateWorkspaceModel(result *workspaces.Workspace, expectations *TestExpectations) { + expectBasicWorkspaceFields(result, expectations) + expectHomeVolume(result, expectations) + expectDataVolumes(result, expectations) + expectPodMetadata(result, expectations) + expectServices(result, expectations) +} + +var _ = Describe("Workspace Functions", func() { + var testTime = metav1.NewTime(time.Now()) + + Describe("NewWorkspaceModelFromWorkspace", func() { + DescribeTable("should create workspace models correctly", + func( + workspaceBuilder func() *kubefloworgv1beta1.Workspace, + workspaceKindBuilder func() *kubefloworgv1beta1.WorkspaceKind, + expectations TestExpectations, + ) { + workspace := workspaceBuilder() + workspaceKind := workspaceKindBuilder() + + result := workspaces.NewWorkspaceModelFromWorkspace(workspace, workspaceKind) + validateWorkspaceModel(&result, &expectations) + }, + + Entry("complete workspace with matching workspace kind", + func() *kubefloworgv1beta1.Workspace { + labels, annotations := createDefaultPodMetadata() + return NewWorkspaceBuilder(). + WithPodMetadata(labels, annotations). + WithHomeVolume(testHomePVC). + WithDataVolumes(createDefaultDataVolumes()). + WithImageConfig(testImageConfigID). + WithPodConfig(testPodConfigID). + WithDesiredImageConfig(testDesiredImageID). + WithDesiredPodConfig(testDesiredPodID). + WithImageRedirectChain(createDefaultRedirectChain()). + WithPodRedirectChain(createDefaultRedirectChain()). + WithActivity(testTime.Unix(), testTime.Unix()). + Build() + }, + func() *kubefloworgv1beta1.WorkspaceKind { + return NewWorkspaceKindBuilder(). + WithImageConfigs(createDefaultImageConfigs()). + WithPodConfigs(createDefaultPodConfigs()). + Build() + }, + TestExpectations{ + Name: testWorkspaceName, + Namespace: testNamespace, + WorkspaceKindName: testWorkspaceKindName, + WorkspaceKindMissing: false, + State: workspaces.WorkspaceStateRunning, + StateMessage: testStateMessage, + DeferUpdates: false, + Paused: false, + PendingRestart: false, + HomePVCName: testHomePVC, + HomeMountPath: testHomeMountPath, + HomeReadOnly: false, + DataVolumeCount: 2, + ServiceCount: 1, + Labels: map[string]string{testAppLabel: testAppLabel}, + Annotations: map[string]string{testAnnotationKey: testAnnotationValue}, + }, + ), + + Entry("workspace with nil workspace kind", + func() *kubefloworgv1beta1.Workspace { + return NewWorkspaceBuilder(). + WithHomeVolume(testHomePVC). + Build() + }, + func() *kubefloworgv1beta1.WorkspaceKind { + return nil + }, + TestExpectations{ + Name: testWorkspaceName, + Namespace: testNamespace, + WorkspaceKindName: testWorkspaceKindName, + WorkspaceKindMissing: true, + State: workspaces.WorkspaceStateRunning, + StateMessage: testStateMessage, + HomePVCName: testHomePVC, + HomeMountPath: workspaces.UnknownHomeMountPath, + HomeReadOnly: false, + DataVolumeCount: 0, + ServiceCount: 0, + }, + ), + + Entry("workspace kind without UID", + func() *kubefloworgv1beta1.Workspace { + return NewWorkspaceBuilder(). + WithHomeVolume(testHomePVC). + Build() + }, + func() *kubefloworgv1beta1.WorkspaceKind { + return NewWorkspaceKindBuilder(). + WithoutUID(). + Build() + }, + TestExpectations{ + Name: testWorkspaceName, + Namespace: testNamespace, + WorkspaceKindName: testWorkspaceKindName, + WorkspaceKindMissing: true, + State: workspaces.WorkspaceStateRunning, + StateMessage: testStateMessage, + HomePVCName: testHomePVC, + HomeMountPath: workspaces.UnknownHomeMountPath, + HomeReadOnly: false, + DataVolumeCount: 0, + ServiceCount: 0, + }, + ), + + Entry("workspace without home volume", + func() *kubefloworgv1beta1.Workspace { + return NewWorkspaceBuilder(). + WithoutHomeVolume(). + Build() + }, + func() *kubefloworgv1beta1.WorkspaceKind { + return NewWorkspaceKindBuilder().Build() + }, + TestExpectations{ + Name: testWorkspaceName, + Namespace: testNamespace, + WorkspaceKindName: testWorkspaceKindName, + WorkspaceKindMissing: false, + State: workspaces.WorkspaceStateRunning, + StateMessage: testStateMessage, + HomePVCName: "", // Empty means nil home volume + DataVolumeCount: 0, + ServiceCount: 0, + }, + ), + ) + + DescribeTable("should map workspace states correctly", + func(inputState kubefloworgv1beta1.WorkspaceState, expectedState workspaces.WorkspaceState) { + workspace := NewWorkspaceBuilder().WithState(inputState).Build() + workspaceKind := NewWorkspaceKindBuilder().Build() + + result := workspaces.NewWorkspaceModelFromWorkspace(workspace, workspaceKind) + Expect(result.State).To(Equal(expectedState)) + }, + Entry("Running", kubefloworgv1beta1.WorkspaceStateRunning, workspaces.WorkspaceStateRunning), + Entry("Terminating", kubefloworgv1beta1.WorkspaceStateTerminating, workspaces.WorkspaceStateTerminating), + Entry("Paused", kubefloworgv1beta1.WorkspaceStatePaused, workspaces.WorkspaceStatePaused), + Entry("Pending", kubefloworgv1beta1.WorkspaceStatePending, workspaces.WorkspaceStatePending), + Entry("Error", kubefloworgv1beta1.WorkspaceStateError, workspaces.WorkspaceStateError), + Entry("Unknown", kubefloworgv1beta1.WorkspaceStateUnknown, workspaces.WorkspaceStateUnknown), + ) + + Context("error cases", func() { + It("should panic when workspace kind name doesn't match", func() { + workspace := NewWorkspaceBuilder().Build() + workspaceKind := NewWorkspaceKindBuilder().WithName("different-kind").Build() + + Expect(func() { + workspaces.NewWorkspaceModelFromWorkspace(workspace, workspaceKind) + }).To(Panic()) + }) + }) + }) +}) diff --git a/workspaces/backend/internal/models/workspaces/suite_test.go b/workspaces/backend/internal/models/workspaces/suite_test.go new file mode 100644 index 000000000..b36ee1036 --- /dev/null +++ b/workspaces/backend/internal/models/workspaces/suite_test.go @@ -0,0 +1,29 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package workspaces_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestWorkspaces(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Workspaces Suite") +} diff --git a/workspaces/backend/internal/repositories/health_check/repo.go b/workspaces/backend/internal/repositories/health_check/repo.go index 3d77c0fad..efa82c561 100644 --- a/workspaces/backend/internal/repositories/health_check/repo.go +++ b/workspaces/backend/internal/repositories/health_check/repo.go @@ -27,14 +27,7 @@ func NewHealthCheckRepository() *HealthCheckRepository { } func (r *HealthCheckRepository) HealthCheck(version string) (models.HealthCheck, error) { - - var res = models.HealthCheck{ - // TODO: implement actual health check logic - Status: models.ServiceStatusHealthy, - SystemInfo: models.SystemInfo{ - Version: version, - }, - } - + // TODO: implement actual health check logic + res := models.NewHealthCheck(models.ServiceStatusHealthy, version) return res, nil } diff --git a/workspaces/backend/internal/repositories/health_check/suite_test.go b/workspaces/backend/internal/repositories/health_check/suite_test.go new file mode 100644 index 000000000..6b014d0cd --- /dev/null +++ b/workspaces/backend/internal/repositories/health_check/suite_test.go @@ -0,0 +1,63 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package health_check_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + models "github.com/kubeflow/notebooks/workspaces/backend/internal/models/health_check" + repo "github.com/kubeflow/notebooks/workspaces/backend/internal/repositories" + health_check "github.com/kubeflow/notebooks/workspaces/backend/internal/repositories/health_check" +) + +func TestHealthCheckRepositories(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "HealthCheck Repositories Suite") +} + +var _ = Describe("HealthCheckRepository", func() { + var ( + repos *repo.Repositories + healthRepo *health_check.HealthCheckRepository + ) + + BeforeEach(func() { + repos = repo.NewRepositories(nil) + healthRepo = repos.HealthCheck + }) + + Context("Repository Initialization", func() { + It("should initialize with non-nil repository", func() { + Expect(repos).ToNot(BeNil()) + Expect(healthRepo).ToNot(BeNil()) + Expect(healthRepo).To(BeAssignableToTypeOf(&health_check.HealthCheckRepository{})) + }) + }) + + Context("HealthCheck Functionality", func() { + It("should return healthy status with valid version", func() { + version := "1.0.0" + result, err := healthRepo.HealthCheck(version) + Expect(err).ToNot(HaveOccurred()) + Expect(result.Status).To(Equal(models.ServiceStatusHealthy)) + Expect(result.SystemInfo.Version).To(Equal(version)) + }) + }) +})