From f1ba8908c5b684d8d866ca9b96515d013b20d52b Mon Sep 17 00:00:00 2001 From: OliverTrautvetter <66372584+OliverTrautvetter@users.noreply.github.com> Date: Thu, 12 Mar 2026 17:35:06 +0100 Subject: [PATCH 1/4] refactor: streamline file path handling in generate commands - Use RepoRoot-relative paths instead of joining with current directory - Introduce clientFactory pattern for lazy client creation in generate docker - Format OpenAPI client code and fix float formatting --- api/openapi_client/client.go | 172 +++++++++++++----------- cli/cmd/generate_docker.go | 27 ++-- cli/cmd/generate_docker_test.go | 6 +- cli/cmd/generate_images.go | 5 +- cli/cmd/generate_images_test.go | 4 +- cli/cmd/generate_kubernetes.go | 4 +- cli/cmd/generate_kubernetes_test.go | 6 +- hack/split-pr.sh | 200 ++++++++++++++++++++++++++++ pkg/exporter/exporter.go | 10 +- pkg/exporter/exporter_test.go | 16 +-- 10 files changed, 326 insertions(+), 124 deletions(-) create mode 100755 hack/split-pr.sh diff --git a/api/openapi_client/client.go b/api/openapi_client/client.go index 85a2f0e..7f3f140 100644 --- a/api/openapi_client/client.go +++ b/api/openapi_client/client.go @@ -9,7 +9,6 @@ API version: 0.1.0 // Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. - package openapi_client import ( @@ -33,14 +32,13 @@ import ( "strings" "time" "unicode/utf8" - ) var ( JsonCheck = regexp.MustCompile(`(?i:(?:application|text)/(?:[^;]+\+)?json)`) XmlCheck = regexp.MustCompile(`(?i:(?:application|text)/(?:[^;]+\+)?xml)`) queryParamSplit = regexp.MustCompile(`(^|&)([^&]+)`) - queryDescape = strings.NewReplacer( "%5B", "[", "%5D", "]" ) + queryDescape = strings.NewReplacer("%5B", "[", "%5D", "]") ) // APIClient manages communication with the Codesphere Public API API v0.1.0 @@ -139,23 +137,35 @@ func typeCheckParameter(obj interface{}, expected string, name string) error { return nil } -func parameterValueToString( obj interface{}, key string ) string { +func parameterValueToString(obj interface{}, key string) string { if reflect.TypeOf(obj).Kind() != reflect.Ptr { if actualObj, ok := obj.(interface{ GetActualInstanceValue() interface{} }); ok { - return fmt.Sprintf("%v", actualObj.GetActualInstanceValue()) + return formatValue(actualObj.GetActualInstanceValue()) } - return fmt.Sprintf("%v", obj) + return formatValue(obj) } - var param,ok = obj.(MappedNullable) + var param, ok = obj.(MappedNullable) if !ok { return "" } - dataMap,err := param.ToMap() + dataMap, err := param.ToMap() if err != nil { return "" } - return fmt.Sprintf("%v", dataMap[key]) + return formatValue(dataMap[key]) +} + +// formatValue converts a value to string, avoiding scientific notation for floats +func formatValue(obj interface{}) string { + switch v := obj.(type) { + case float32: + return fmt.Sprintf("%.0f", v) + case float64: + return fmt.Sprintf("%.0f", v) + default: + return fmt.Sprintf("%v", obj) + } } // parameterAddToHeaderOrQuery adds the provided object to the request header or url query @@ -167,85 +177,85 @@ func parameterAddToHeaderOrQuery(headerOrQueryParams interface{}, keyPrefix stri value = "null" } else { switch v.Kind() { - case reflect.Invalid: - value = "invalid" + case reflect.Invalid: + value = "invalid" - case reflect.Struct: - if t,ok := obj.(MappedNullable); ok { - dataMap,err := t.ToMap() - if err != nil { - return - } - parameterAddToHeaderOrQuery(headerOrQueryParams, keyPrefix, dataMap, style, collectionType) - return - } - if t, ok := obj.(time.Time); ok { - parameterAddToHeaderOrQuery(headerOrQueryParams, keyPrefix, t.Format(time.RFC3339Nano), style, collectionType) - return - } - value = v.Type().String() + " value" - case reflect.Slice: - var indValue = reflect.ValueOf(obj) - if indValue == reflect.ValueOf(nil) { + case reflect.Struct: + if t, ok := obj.(MappedNullable); ok { + dataMap, err := t.ToMap() + if err != nil { return } - var lenIndValue = indValue.Len() - for i:=0;i/dev/null 2>&1; then + echo "ERROR: Branch '$branch' already exists. Delete it first or pick a different name." + exit 1 + fi +done + +STASHED=0 +if ! git diff --quiet || ! git diff --cached --quiet; then + echo "Stashing uncommitted changes on $ORIGINAL_BRANCH..." + git stash push --include-untracked -m "pr-split-auto-stash" + STASHED=1 +fi + +cleanup() { + echo "" + echo "Returning to original branch..." + git checkout "$ORIGINAL_BRANCH" 2>/dev/null || true + if [ "$STASHED" = "1" ]; then + echo "Restoring stashed changes..." + git stash pop 2>/dev/null || true + fi +} +trap cleanup EXIT + +############################################################################### +# PR 1: refactor/split-integration-tests +############################################################################### +echo "" +echo "=============================================" +echo " Creating PR 1: $PR1_BRANCH" +echo "=============================================" + +git checkout main +git checkout -b "$PR1_BRANCH" + +# --- Commit 1: Generate command fixes + OpenAPI client cleanup --------------- +echo "" +echo "[PR1 · Commit 1/3] Generate command and OpenAPI client changes..." + +git checkout "$ORIGINAL_BRANCH" -- \ + api/openapi_client/client.go \ + cli/cmd/generate_docker.go \ + cli/cmd/generate_docker_test.go \ + cli/cmd/generate_images.go \ + cli/cmd/generate_images_test.go \ + cli/cmd/generate_kubernetes.go \ + cli/cmd/generate_kubernetes_test.go \ + pkg/exporter/exporter.go \ + pkg/exporter/exporter_test.go + +go mod tidy +git add -A +git commit -m "refactor: streamline file path handling in generate commands + +- Use RepoRoot-relative paths instead of joining with current directory +- Introduce clientFactory pattern for lazy client creation in generate docker +- Format OpenAPI client code and fix float formatting" + +echo " ✓ Verifying compilation..." +go build ./... + +# --- Commit 2: Split integration tests (delete + create in SAME commit) ------ +echo "" +echo "[PR1 · Commit 2/3] Split integration tests into labeled files..." + +# Delete the old monolithic test file +git rm int/integration_test.go + +# Check out every split file + util changes from the original branch +git checkout "$ORIGINAL_BRANCH" -- \ + int/curl_test.go \ + int/error_handling_test.go \ + int/git_test.go \ + int/int_suite_test.go \ + int/list_test.go \ + int/log_test.go \ + int/monitor_test.go \ + int/pipeline_test.go \ + int/version_help_test.go \ + int/wakeup_test.go \ + int/workspace_test.go \ + int/util/constants.go \ + int/util/test_helpers.go \ + int/util/workspace.go + +go mod tidy +git add -A +git commit -m "refactor: split integration tests into labeled files + +Split the monolithic integration_test.go into individual files, each +with a Ginkgo label for selective execution via matrix CI: + + curl_test.go (curl), error_handling_test.go (error-handling), + git_test.go (git), list_test.go (list), log_test.go (log), + monitor_test.go (local), pipeline_test.go (pipeline), + version_help_test.go (local), wakeup_test.go (wakeup), + workspace_test.go (workspace) + +Extract shared constants and helpers into int/util/." + +echo " ✓ Verifying compilation..." +go build ./... + +# --- Commit 3: Workflow + Makefile ------------------------------------------- +echo "" +echo "[PR1 · Commit 3/3] Workflow matrix strategy and Makefile targets..." + +git checkout "$ORIGINAL_BRANCH" -- \ + .github/workflows/integration-test.yml \ + Makefile + +git add -A +git commit -m "refactor: add matrix strategy to integration test workflow + +- Split CI into build, test (parallel per-label matrix), and cleanup jobs +- Define label list once; derive matrix and unlabeled filter dynamically +- Add unique run-name with github.run_id to avoid parallel conflicts +- Fetch latest hadolint and kubeconform versions at install time +- Add per-label Makefile targets (test-int-workspace, test-int-list, etc.)" + +############################################################################### +# PR 2: feat/kubernetes-export-tests +############################################################################### +echo "" +echo "=============================================" +echo " Creating PR 2: $PR2_BRANCH" +echo "=============================================" + +git checkout -b "$PR2_BRANCH" + +echo "" +echo "[PR2 · Commit 1/1] Kubernetes export integration tests..." + +git checkout "$ORIGINAL_BRANCH" -- \ + int/export_kubernetes_test.go \ + int/util/linters.go + +go mod tidy +git add -A +git commit -m "feat: add integration tests for kubernetes export + +Add comprehensive tests for generate docker, kubernetes, and images +commands. Tests validate generated Dockerfiles, shell scripts, and K8s +manifests (Deployments, Services, Ingress) by unmarshalling into typed +structs and running linters (hadolint, shellcheck, kubeconform) when +available. + +Add int/util/linters.go with a single runLinter function that handles +tool discovery, skip-on-missing behavior, and failure reporting." + +echo " ✓ Verifying compilation..." +go build ./... + +############################################################################### +# Summary +############################################################################### +echo "" +echo "=============================================" +echo " Done!" +echo "=============================================" +echo "" +echo "Branches created:" +echo " PR 1: $PR1_BRANCH (3 commits on top of main)" +echo " PR 2: $PR2_BRANCH (1 commit on top of PR 1)" +echo "" +echo "Original branch '$ORIGINAL_BRANCH' is untouched." +echo "" +echo "Verify:" +echo " git log --oneline main..$PR1_BRANCH" +echo " git log --oneline $PR1_BRANCH..$PR2_BRANCH" +echo "" +echo "When ready:" +echo " git push -u origin $PR1_BRANCH" +echo " git push -u origin $PR2_BRANCH" diff --git a/pkg/exporter/exporter.go b/pkg/exporter/exporter.go index c7cb21d..821d41b 100644 --- a/pkg/exporter/exporter.go +++ b/pkg/exporter/exporter.go @@ -58,13 +58,11 @@ func (e *ExporterService) ReadYmlFile(path string) (*ci.CiYml, error) { } func (e *ExporterService) GetExportDir() string { - return filepath.Join(e.repoRoot, e.outputPath) - + return e.outputPath } func (e *ExporterService) GetKubernetesDir() string { - return filepath.Join(e.repoRoot, e.outputPath, "kubernetes") - + return filepath.Join(e.outputPath, "kubernetes") } // ExportDockerArtifacts exports Docker artifacts based on the provided input path, output path, base image, and environment variables. @@ -90,9 +88,6 @@ func (e *ExporterService) ExportDockerArtifacts() error { if err != nil { return fmt.Errorf("error creating dockerfile for service %s: %w", serviceName, err) } - log.Println(e.outputPath) - log.Println(e.GetExportDir()) - log.Println(filepath.Join(e.GetExportDir(), serviceName)) err = e.fs.WriteFile(filepath.Join(e.GetExportDir(), serviceName), "Dockerfile", dockerfile, e.force) if err != nil { return fmt.Errorf("error writing dockerfile for service %s: %w", serviceName, err) @@ -239,7 +234,6 @@ func (e *ExporterService) ExportImages(ctx context.Context, registry string, ima // CreateImageTag creates a Docker image tag from the registry, image prefix and service name. // It returns the full image tag in the format: /-:latest. func (e *ExporterService) CreateImageTag(registry string, imagePrefix string, serviceName string) (string, error) { - log.Println(imagePrefix) if imagePrefix == "" { tag, err := url.JoinPath(registry, fmt.Sprintf("%s:latest", serviceName)) if err != nil { diff --git a/pkg/exporter/exporter_test.go b/pkg/exporter/exporter_test.go index d2613c3..e0b5bd8 100644 --- a/pkg/exporter/exporter_test.go +++ b/pkg/exporter/exporter_test.go @@ -71,20 +71,20 @@ var _ = Describe("GenerateDockerfile", func() { err = e.ExportDockerArtifacts() Expect(err).To(Not(HaveOccurred())) - Expect(memoryFs.DirExists("workspace-repo/export")).To(BeTrue()) - Expect(memoryFs.FileExists("workspace-repo/export/docker-compose.yml")).To(BeTrue()) + Expect(memoryFs.DirExists("./export")).To(BeTrue()) + Expect(memoryFs.FileExists("./export/docker-compose.yml")).To(BeTrue()) - Expect(memoryFs.DirExists("workspace-repo/export/frontend")).To(BeTrue()) - Expect(memoryFs.FileExists("workspace-repo/export/frontend/Dockerfile")).To(BeTrue()) - Expect(memoryFs.FileExists("workspace-repo/export/frontend/entrypoint.sh")).To(BeTrue()) + Expect(memoryFs.DirExists("./export/frontend")).To(BeTrue()) + Expect(memoryFs.FileExists("./export/frontend/Dockerfile")).To(BeTrue()) + Expect(memoryFs.FileExists("./export/frontend/entrypoint.sh")).To(BeTrue()) err = e.ExportKubernetesArtifacts("registry", "image", mock.Anything, mock.Anything, mock.Anything, mock.Anything) Expect(err).To(Not(HaveOccurred())) - Expect(memoryFs.DirExists("workspace-repo/export/kubernetes")).To(BeTrue()) - Expect(memoryFs.FileExists("workspace-repo/export/kubernetes/ingress.yml")).To(BeTrue()) + Expect(memoryFs.DirExists("./export/kubernetes")).To(BeTrue()) + Expect(memoryFs.FileExists("./export/kubernetes/ingress.yml")).To(BeTrue()) - Expect(memoryFs.FileExists("workspace-repo/export/kubernetes/service-frontend.yml")).To(BeTrue()) + Expect(memoryFs.FileExists("./export/kubernetes/service-frontend.yml")).To(BeTrue()) }) }) }) From ac864c151a79ff0926f97bbc2ac0e68c3433a455 Mon Sep 17 00:00:00 2001 From: OliverTrautvetter <66372584+OliverTrautvetter@users.noreply.github.com> Date: Thu, 12 Mar 2026 17:35:13 +0100 Subject: [PATCH 2/4] refactor: split integration tests into labeled files Split the monolithic integration_test.go into individual files, each with a Ginkgo label for selective execution via matrix CI: curl_test.go (curl), error_handling_test.go (error-handling), git_test.go (git), list_test.go (list), log_test.go (log), monitor_test.go (local), pipeline_test.go (pipeline), version_help_test.go (local), wakeup_test.go (wakeup), workspace_test.go (workspace) Extract shared constants and helpers into int/util/. --- int/curl_test.go | 145 ++++ int/error_handling_test.go | 37 + int/git_test.go | 52 ++ int/int_suite_test.go | 14 +- int/integration_test.go | 1343 ------------------------------------ int/list_test.go | 124 ++++ int/log_test.go | 53 ++ int/monitor_test.go | 350 ++++++++++ int/pipeline_test.go | 52 ++ int/util/constants.go | 29 + int/util/test_helpers.go | 16 +- int/util/workspace.go | 34 +- int/version_help_test.go | 115 +++ int/wakeup_test.go | 146 ++++ int/workspace_test.go | 229 ++++++ 15 files changed, 1373 insertions(+), 1366 deletions(-) create mode 100644 int/curl_test.go create mode 100644 int/error_handling_test.go create mode 100644 int/git_test.go delete mode 100644 int/integration_test.go create mode 100644 int/list_test.go create mode 100644 int/log_test.go create mode 100644 int/monitor_test.go create mode 100644 int/pipeline_test.go create mode 100644 int/util/constants.go create mode 100644 int/version_help_test.go create mode 100644 int/wakeup_test.go create mode 100644 int/workspace_test.go diff --git a/int/curl_test.go b/int/curl_test.go new file mode 100644 index 0000000..a81daac --- /dev/null +++ b/int/curl_test.go @@ -0,0 +1,145 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + +package int_test + +import ( + "fmt" + "log" + "os" + "time" + + intutil "github.com/codesphere-cloud/cs-go/int/util" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Curl Workspace Integration Tests", Label("curl"), func() { + var ( + teamId string + workspaceName string + workspaceId string + ) + + BeforeEach(func() { + teamId, _ = intutil.FailIfMissingEnvVars() + workspaceName = intutil.NewWorkspaceName("curl") + }) + + AfterEach(func() { + if workspaceId != "" { + By(fmt.Sprintf("Cleaning up: deleting workspace %s (ID: %s)", workspaceName, workspaceId)) + intutil.CleanupWorkspace(workspaceId) + workspaceId = "" + } + }) + + Context("Curl Command", func() { + BeforeEach(func() { + By("Creating a workspace for curl testing") + workspaceId = intutil.CreateTestWorkspace(teamId, workspaceName) + + By("Waiting for workspace to be fully provisioned") + time.Sleep(intutil.PostCreateWaitTime) + }) + + It("should send authenticated request to workspace", func() { + By("Sending curl request to workspace root") + output := intutil.RunCommand( + "curl", "/", + "-w", workspaceId, + "--", "-k", "-s", "-o", "/dev/null", "-w", "%{http_code}", + ) + log.Printf("Curl workspace output: %s\n", output) + + Expect(output).To(ContainSubstring("Sending request to workspace")) + Expect(output).To(ContainSubstring(workspaceId)) + }) + + It("should support custom paths", func() { + By("Sending curl request to custom path") + output, exitCode := intutil.RunCommandWithExitCode( + "curl", "/api/health", + "-w", workspaceId, + "--", "-k", "-s", "-o", "/dev/null", "-w", "%{http_code}", + ) + log.Printf("Curl with custom path output: %s (exit code: %d)\n", output, exitCode) + + Expect(output).To(ContainSubstring("Sending request to workspace")) + }) + + It("should pass through curl arguments", func() { + By("Sending HEAD request using curl -I flag") + output := intutil.RunCommand( + "curl", "/", + "-w", workspaceId, + "--", "-k", "-I", + ) + log.Printf("Curl with -I flag output: %s\n", output) + + Expect(output).To(ContainSubstring("Sending request to workspace")) + }) + + It("should work with workspace ID from environment variable", func() { + By("Setting CS_WORKSPACE_ID environment variable") + originalWsId := os.Getenv("CS_WORKSPACE_ID") + _ = os.Setenv("CS_WORKSPACE_ID", workspaceId) + defer func() { _ = os.Setenv("CS_WORKSPACE_ID", originalWsId) }() + + By("Sending curl request using environment variable") + output := intutil.RunCommand( + "curl", "/", + "--", "-k", "-s", "-o", "/dev/null", "-w", "%{http_code}", + ) + log.Printf("Curl with env var output: %s\n", output) + + Expect(output).To(ContainSubstring("Sending request to workspace")) + Expect(output).To(ContainSubstring(workspaceId)) + }) + }) + + Context("Curl Error Handling", func() { + It("should fail when workspace ID is missing", func() { + By("Attempting to curl without workspace ID") + intutil.WithClearedWorkspaceEnv(func() { + output, exitCode := intutil.RunCommandWithExitCode("curl", "/") + log.Printf("Curl without workspace ID output: %s (exit code: %d)\n", output, exitCode) + + Expect(exitCode).NotTo(Equal(0)) + Expect(output).To(Or( + ContainSubstring("workspace"), + ContainSubstring("required"), + ContainSubstring("not set"), + )) + }) + }) + + It("should require path argument", func() { + By("Attempting to curl without path") + output, exitCode := intutil.RunCommandWithExitCode( + "curl", + "-w", "1234", + ) + log.Printf("Curl without path output: %s (exit code: %d)\n", output, exitCode) + + Expect(exitCode).NotTo(Equal(0)) + Expect(output).To(Or( + ContainSubstring("path"), + ContainSubstring("required"), + ContainSubstring("argument"), + )) + }) + }) + + Context("Curl Command Help", func() { + It("should display help information", func() { + By("Running curl --help") + output := intutil.RunCommand("curl", "--help") + log.Printf("Curl help output: %s\n", output) + + Expect(output).To(ContainSubstring("Send authenticated HTTP requests")) + Expect(output).To(ContainSubstring("--timeout")) + Expect(output).To(ContainSubstring("-w, --workspace")) + }) + }) +}) diff --git a/int/error_handling_test.go b/int/error_handling_test.go new file mode 100644 index 0000000..477324e --- /dev/null +++ b/int/error_handling_test.go @@ -0,0 +1,37 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + +package int_test + +import ( + "fmt" + "log" + + intutil "github.com/codesphere-cloud/cs-go/int/util" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Command Error Handling Tests", Label("error-handling"), func() { + It("should fail gracefully with non-existent workspace for all commands", func() { + testCases := []struct { + commandName string + args []string + }{ + {"open workspace", []string{"open", "workspace", "-w", intutil.NonExistentWorkspaceId}}, + {"log", []string{"log", "-w", intutil.NonExistentWorkspaceId}}, + {"start pipeline", []string{"start", "pipeline", "-w", intutil.NonExistentWorkspaceId}}, + {"git pull", []string{"git", "pull", "-w", intutil.NonExistentWorkspaceId}}, + {"set-env", []string{"set-env", "-w", intutil.NonExistentWorkspaceId, "TEST_VAR=test"}}, + {"wake-up", []string{"wake-up", "-w", intutil.NonExistentWorkspaceId}}, + {"curl", []string{"curl", "/", "-w", intutil.NonExistentWorkspaceId}}, + } + + for _, tc := range testCases { + By(fmt.Sprintf("Testing %s with non-existent workspace", tc.commandName)) + output, exitCode := intutil.RunCommandWithExitCode(tc.args...) + log.Printf("%s non-existent workspace output: %s (exit code: %d)\n", tc.commandName, output, exitCode) + Expect(exitCode).NotTo(Equal(0)) + } + }) +}) diff --git a/int/git_test.go b/int/git_test.go new file mode 100644 index 0000000..33ffdf1 --- /dev/null +++ b/int/git_test.go @@ -0,0 +1,52 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + +package int_test + +import ( + "fmt" + "log" + + intutil "github.com/codesphere-cloud/cs-go/int/util" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Git Pull Integration Tests", Label("git"), func() { + var ( + teamId string + workspaceName string + workspaceId string + ) + + BeforeEach(func() { + teamId, _ = intutil.FailIfMissingEnvVars() + workspaceName = intutil.NewWorkspaceName("git") + }) + + AfterEach(func() { + if workspaceId != "" { + By(fmt.Sprintf("Cleaning up: deleting workspace %s (ID: %s)", workspaceName, workspaceId)) + intutil.CleanupWorkspace(workspaceId) + workspaceId = "" + } + }) + + Context("Git Pull Command", func() { + BeforeEach(func() { + By("Creating a workspace") + workspaceId = intutil.CreateTestWorkspace(teamId, workspaceName) + }) + + It("should execute git pull command", func() { + By("Running git pull") + output, exitCode := intutil.RunCommandWithExitCode( + "git", "pull", + "-w", workspaceId, + ) + log.Printf("Git pull output: %s (exit code: %d)\n", output, exitCode) + + Expect(output).NotTo(BeEmpty()) + }) + }) +}) diff --git a/int/int_suite_test.go b/int/int_suite_test.go index 6b56b70..6682a41 100644 --- a/int/int_suite_test.go +++ b/int/int_suite_test.go @@ -26,19 +26,7 @@ var _ = AfterSuite(func() { GinkgoWriter.Println("Running global cleanup for any orphaned test workspaces...") - prefixes := []string{ - "cli-git-test-", - "cli-pipeline-test-", - "cli-log-test-", - "cli-open-test-", - "cli-setenv-test-", - "cli-edge-test-", - "cli-very-long-workspace-name-test-", - "cli-wakeup-test-", - "cli-curl-test-", - } - - for _, prefix := range prefixes { + for _, prefix := range intutil.WorkspaceNamePrefixes { intutil.CleanupAllWorkspacesInTeam(teamId, prefix) } diff --git a/int/integration_test.go b/int/integration_test.go deleted file mode 100644 index f29352a..0000000 --- a/int/integration_test.go +++ /dev/null @@ -1,1343 +0,0 @@ -// Copyright (c) Codesphere Inc. -// SPDX-License-Identifier: Apache-2.0 - -package int_test - -import ( - "bytes" - "context" - "fmt" - "io" - "log" - "net/http" - "os" - "os/exec" - "path/filepath" - "strings" - "time" - - intutil "github.com/codesphere-cloud/cs-go/int/util" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -var _ = Describe("cs monitor", func() { - var ( - certsDir string - tempDir string - caCertPath string - serverCertPath string - serverKeyPath string - monitorListenPort int - targetServerPort int - targetServer *http.Server - monitorCmdProcess *exec.Cmd - testHttpClient *http.Client - monitorOutputBuf *bytes.Buffer - targetServerOutputBuf *bytes.Buffer - ) - - BeforeEach(func() { - var err error - tempDir, err = os.MkdirTemp("", "e2e-tls-monitor-test-") - Expect(err).NotTo(HaveOccurred()) - certsDir = filepath.Join(tempDir, "certs") - - monitorListenPort, err = intutil.GetEphemeralPort() - Expect(err).NotTo(HaveOccurred()) - targetServerPort, err = intutil.GetEphemeralPort() - Expect(err).NotTo(HaveOccurred()) - - testHttpClient = &http.Client{ - Timeout: 10 * time.Second, - } - - monitorOutputBuf = new(bytes.Buffer) - targetServerOutputBuf = new(bytes.Buffer) - }) - - AfterEach(func() { - if monitorCmdProcess != nil && monitorCmdProcess.Process != nil { - log.Printf("Terminating monitor process (PID: %d). Output:\n%s\n", monitorCmdProcess.Process.Pid, monitorOutputBuf.String()) - _ = monitorCmdProcess.Process.Kill() - _, _ = monitorCmdProcess.Process.Wait() - } - - Expect(os.RemoveAll(tempDir)).NotTo(HaveOccurred()) - }) - - Context("Healthcheck forwarding", func() { - AfterEach(func() { - if targetServer != nil { - log.Printf("Terminating HTTP(S) server. Output:\n%s\n", targetServerOutputBuf.String()) - shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 1*time.Second) - defer shutdownCancel() - _ = targetServer.Shutdown(shutdownCtx) - } - }) - It("should start a Go HTTP server, and proxy successfully", func() { - var err error - - By("Starting Go HTTPS server with generated certs") - targetServer, err = intutil.StartTestHttpServer(targetServerPort) - Expect(err).NotTo(HaveOccurred()) - intutil.WaitForPort(fmt.Sprintf("127.0.0.1:%d", targetServerPort), 10*time.Second) - log.Printf("Go HTTPS server started on port %d.\n", targetServerPort) - - By("Running 'cs monitor' command with --forward and --insecure-skip-verify") - intutil.RunCommandInBackground(monitorOutputBuf, - "monitor", - "--forward", fmt.Sprintf("http://127.0.0.1:%d/", targetServerPort), - "--address", fmt.Sprintf(":%d", monitorListenPort), - "--", "sleep", "60s", - ) - intutil.WaitForPort(fmt.Sprintf("127.0.0.1:%d", monitorListenPort), 10*time.Second) - - By("Making request to monitor proxy to verify successful forwarding") - resp, err := testHttpClient.Get(fmt.Sprintf("http://127.0.0.1:%d/", monitorListenPort)) - Expect(err).NotTo(HaveOccurred()) - defer func() { _ = resp.Body.Close() }() - - Expect(resp.StatusCode).To(Equal(http.StatusOK)) - bodyBytes, err := io.ReadAll(resp.Body) - Expect(err).NotTo(HaveOccurred()) - Expect(string(bodyBytes)).To(Equal("OK (HTTP)")) - - log.Printf("Monitor output after request:\n%s\n", monitorOutputBuf.String()) - }) - - It("should start a Go HTTPS server with generated certs, run monitor with --insecure-skip-verify, and proxy successfully", func() { - By("Generating TLS certificates") - var err error - caCertPath, serverCertPath, serverKeyPath, err = intutil.GenerateTLSCerts( - certsDir, - "localhost", - []string{"localhost", "127.0.0.1"}, - ) - Expect(err).NotTo(HaveOccurred()) - Expect(caCertPath).To(BeAnExistingFile()) - Expect(serverCertPath).To(BeAnExistingFile()) - Expect(serverKeyPath).To(BeAnExistingFile()) - - By("Starting Go HTTPS server with generated certs") - targetServer, err = intutil.StartTestHttpsServer(targetServerPort, serverCertPath, serverKeyPath) - Expect(err).NotTo(HaveOccurred()) - intutil.WaitForPort(fmt.Sprintf("127.0.0.1:%d", targetServerPort), 10*time.Second) - log.Printf("Go HTTPS server started on port %d.\n", targetServerPort) - - By("Running 'cs monitor' command with --forward and --insecure-skip-verify") - intutil.RunCommandInBackground(monitorOutputBuf, - "monitor", - "--forward", fmt.Sprintf("https://127.0.0.1:%d/", targetServerPort), - "--address", fmt.Sprintf(":%d", monitorListenPort), - "--insecure-skip-verify", - "--", "sleep", "60s", - ) - intutil.WaitForPort(fmt.Sprintf("127.0.0.1:%d", monitorListenPort), 10*time.Second) - - By("Making request to monitor proxy to verify successful forwarding") - resp, err := testHttpClient.Get(fmt.Sprintf("http://127.0.0.1:%d/", monitorListenPort)) - Expect(err).NotTo(HaveOccurred()) - defer func() { _ = resp.Body.Close() }() - - Expect(resp.StatusCode).To(Equal(http.StatusOK)) - bodyBytes, err := io.ReadAll(resp.Body) - Expect(err).NotTo(HaveOccurred()) - Expect(string(bodyBytes)).To(Equal("OK (HTTPS)")) - - log.Printf("Monitor output after request:\n%s\n", monitorOutputBuf.String()) - }) - - It("should get an error for an invalid HTTPS certificate without --insecure-skip-verify or --ca-cert-file", func() { - By("Generating TLS certificates in Go") - var err error - caCertPath, serverCertPath, serverKeyPath, err = intutil.GenerateTLSCerts( - certsDir, - "localhost", - []string{"localhost", "127.0.0.1"}, - ) - Expect(err).NotTo(HaveOccurred()) - Expect(caCertPath).To(BeAnExistingFile()) - Expect(serverCertPath).To(BeAnExistingFile()) - Expect(serverKeyPath).To(BeAnExistingFile()) - - By("Starting Go HTTPS server with generated certs") - targetServer, err = intutil.StartTestHttpsServer(targetServerPort, serverCertPath, serverKeyPath) - Expect(err).NotTo(HaveOccurred()) - intutil.WaitForPort(fmt.Sprintf("127.0.0.1:%d", targetServerPort), 10*time.Second) - log.Printf("Go HTTPS server started on port %d.\n", targetServerPort) - - By("Running 'cs monitor' command without TLS bypass/trust") - intutil.RunCommandInBackground( - monitorOutputBuf, - "monitor", - "--address", fmt.Sprintf(":%d", monitorListenPort), - "--forward", fmt.Sprintf("https://127.0.0.1:%d/", targetServerPort), - "--", "sleep", "60s", - ) - - intutil.WaitForPort(fmt.Sprintf("127.0.0.1:%d", monitorListenPort), 10*time.Second) - log.Printf("Monitor command started on port %d. Initial output:\n%s\n", monitorListenPort, monitorOutputBuf.String()) - - By("Making request to monitor proxy and expecting a Bad Gateway error") - resp, err := testHttpClient.Get(fmt.Sprintf("http://127.0.0.1:%d/", monitorListenPort)) - Expect(err).NotTo(HaveOccurred()) - defer func() { _ = resp.Body.Close() }() - - Expect(resp.StatusCode).To(Equal(http.StatusBadGateway)) - bodyBytes, err := io.ReadAll(resp.Body) - Expect(err).NotTo(HaveOccurred()) - Expect(string(bodyBytes)).To(ContainSubstring("Error forwarding request")) - Expect(string(bodyBytes)).To(ContainSubstring("tls: failed to verify certificate")) - - log.Printf("Monitor output after request:\n%s\n", monitorOutputBuf.String()) - }) - - It("should forward to an HTTPS target with --ca-cert-file and return 200 OK", func() { - By("Generating TLS certificates in Go") - var err error - caCertPath, serverCertPath, serverKeyPath, err = intutil.GenerateTLSCerts( - certsDir, - "localhost", - []string{"localhost", "127.0.0.1"}, - ) - Expect(err).NotTo(HaveOccurred()) - Expect(caCertPath).To(BeAnExistingFile()) - Expect(serverCertPath).To(BeAnExistingFile()) - Expect(serverKeyPath).To(BeAnExistingFile()) - - By("Starting Go HTTPS server with generated certs") - targetServer, err = intutil.StartTestHttpsServer(targetServerPort, serverCertPath, serverKeyPath) - Expect(err).NotTo(HaveOccurred()) - intutil.WaitForPort(fmt.Sprintf("127.0.0.1:%d", targetServerPort), 10*time.Second) - log.Printf("Go HTTPS server started on port %d.\n", targetServerPort) - - By("Running 'cs monitor' command with --ca-cert-file") - intutil.RunCommandInBackground(monitorOutputBuf, - "monitor", - "--address", fmt.Sprintf(":%d", monitorListenPort), - "--forward", fmt.Sprintf("https://127.0.0.1:%d/", targetServerPort), - "--ca-cert-file", caCertPath, - "--", - "sleep", "60s", - ) - intutil.WaitForPort(fmt.Sprintf("127.0.0.1:%d", monitorListenPort), 10*time.Second) - log.Printf("Monitor command started on port %d. Initial output:\n%s\n", monitorListenPort, monitorOutputBuf.String()) - - By("Making request to monitor proxy to verify successful forwarding") - resp, err := testHttpClient.Get(fmt.Sprintf("http://127.0.0.1:%d/", monitorListenPort)) - Expect(err).NotTo(HaveOccurred()) - defer func() { _ = resp.Body.Close() }() - - Expect(resp.StatusCode).To(Equal(http.StatusOK)) - bodyBytes, err := io.ReadAll(resp.Body) - Expect(err).NotTo(HaveOccurred()) - Expect(string(bodyBytes)).To(Equal("OK (HTTPS)")) - - log.Printf("Monitor output after request:\n%s\n", monitorOutputBuf.String()) - }) - }) - - Context("Prometheus Metrics Endpoint", func() { - It("should expose Prometheus metrics when no forward is specified", func() { - By("Running 'cs monitor' command without forwarding (metrics only)") - intutil.RunCommandInBackground(monitorOutputBuf, - "monitor", - "--address", fmt.Sprintf(":%d", monitorListenPort), - "--", "sleep", "60s", - ) - intutil.WaitForPort(fmt.Sprintf("127.0.0.1:%d", monitorListenPort), 10*time.Second) - log.Printf("Monitor command started on port %d. Initial output:\n%s\n", monitorListenPort, monitorOutputBuf.String()) - - By("Making a request to the monitor's metrics endpoint") - resp, err := testHttpClient.Get(fmt.Sprintf("http://127.0.0.1:%d/metrics", monitorListenPort)) - Expect(err).NotTo(HaveOccurred()) - defer func() { _ = resp.Body.Close() }() - - Expect(resp.StatusCode).To(Equal(http.StatusOK)) - bodyBytes, err := io.ReadAll(resp.Body) - Expect(err).NotTo(HaveOccurred()) - Expect(string(bodyBytes)).To(ContainSubstring("cs_monitor_restarts_total")) - log.Printf("Monitor output after metrics request:\n%s\n", monitorOutputBuf.String()) - }) - - It("should redirect root to /metrics", func() { - By("Running 'cs monitor' command without forwarding (metrics only)") - intutil.RunCommandInBackground(monitorOutputBuf, - "monitor", - "--address", fmt.Sprintf(":%d", monitorListenPort), - "--", "sleep", "60s", - ) - intutil.WaitForPort(fmt.Sprintf("127.0.0.1:%d", monitorListenPort), 10*time.Second) - log.Printf("Monitor command started on port %d. Initial output:\n%s\n", monitorListenPort, monitorOutputBuf.String()) - - By("Making a request to the monitor's root endpoint and expecting a redirect") - client := &http.Client{ - CheckRedirect: func(req *http.Request, via []*http.Request) error { - return http.ErrUseLastResponse - }, - Timeout: 5 * time.Second, - } - resp, err := client.Get(fmt.Sprintf("http://127.0.0.1:%d/", monitorListenPort)) - Expect(err).NotTo(HaveOccurred()) - defer func() { _ = resp.Body.Close() }() - - Expect(resp.StatusCode).To(Equal(http.StatusMovedPermanently)) - Expect(resp.Header.Get("Location")).To(Equal("/metrics")) - log.Printf("Monitor output after redirect request:\n%s\n", monitorOutputBuf.String()) - }) - }) - - Context("Command Execution and Restart Logic", func() { - It("should execute the command once if it succeeds", func() { - monitorCmdProcess = intutil.RunCommandInBackground(monitorOutputBuf, - "monitor", - "--address", fmt.Sprintf(":%d", monitorListenPort), - "--max-restarts", "0", - "--", "true", - ) - - Eventually(monitorCmdProcess.Wait, "5s").Should(Succeed(), "Monitor process should exit successfully") - - output := monitorOutputBuf.String() - Expect(output).To(ContainSubstring("command exited")) - Expect(output).To(ContainSubstring("returnCode=0")) - Expect(output).To(ContainSubstring("maximum number of restarts reached, exiting")) - Expect(strings.Count(output, "command exited")).To(Equal(1), "Command should have executed only once") - }) - - It("should restart the command if it exits with non-zero code quickly", func() { - - monitorCmdProcess = intutil.RunCommandInBackground(monitorOutputBuf, - "monitor", - "--address", fmt.Sprintf(":%d", monitorListenPort), - "--max-restarts", "1", - "--", "bash", "-c", "echo FAKE_OUTPUT;exit 1", - ) - - Eventually(monitorCmdProcess.Wait, "15s").Should(Succeed(), "Monitor process should exit after restarts") - - output := monitorOutputBuf.String() - Expect(output).To(ContainSubstring("command exited")) - Expect(output).To(ContainSubstring("returnCode=1")) - Expect(output).To(ContainSubstring("command exited with non-zero code in less than 1 second. Waiting 5 seconds before next restart")) - Expect(output).To(ContainSubstring("cs monitor: restarting")) - Expect(output).To(ContainSubstring("maximum number of restarts reached, exiting")) - Expect(strings.Count(output, "FAKE_OUTPUT")).To(Equal(3), "Command should have executed twice") - }) - - It("should stop command runner on context cancellation", func() { - By("Running 'cs monitor' command with infinite restarts") - monitorCmdProcess = intutil.RunCommandInBackground(monitorOutputBuf, - "monitor", - "--address", fmt.Sprintf(":%d", monitorListenPort), - "--max-restarts", "-1", - "--", "sleep", "10s", - ) - Eventually(func() string { return monitorOutputBuf.String() }, "5s").Should(ContainSubstring("starting monitor")) - - By("Stopping command execution") - err := monitorCmdProcess.Process.Signal(os.Interrupt) - Expect(err).NotTo(HaveOccurred()) - _, _ = monitorCmdProcess.Process.Wait() - - output := monitorOutputBuf.String() - Expect(output).To(ContainSubstring("initiating graceful shutdown...")) - Expect(output).To(ContainSubstring("stopping command runner.")) - Expect(output).NotTo(ContainSubstring("error executing command")) - }) - }) -}) - -var _ = Describe("Open Workspace Integration Tests", func() { - var ( - teamId string - workspaceName string - workspaceId string - ) - - BeforeEach(func() { - teamId, _ = intutil.SkipIfMissingEnvVars() - workspaceName = fmt.Sprintf("cli-open-test-%d", time.Now().Unix()) - }) - - AfterEach(func() { - if workspaceId != "" { - By(fmt.Sprintf("Cleaning up: deleting workspace %s (ID: %s)", workspaceName, workspaceId)) - intutil.CleanupWorkspace(workspaceId) - workspaceId = "" - } - }) - - Context("Open Workspace Command", func() { - BeforeEach(func() { - By("Creating a workspace") - output := intutil.RunCommand( - "create", "workspace", workspaceName, - "-t", teamId, - "-p", "8", - "--timeout", "15m", - ) - log.Printf("Create workspace output: %s\n", output) - - Expect(output).To(ContainSubstring("Workspace created")) - workspaceId = intutil.ExtractWorkspaceId(output) - Expect(workspaceId).NotTo(BeEmpty()) - }) - - It("should open workspace successfully", func() { - By("Opening the workspace") - output := intutil.RunCommand( - "open", "workspace", - "-w", workspaceId, - ) - log.Printf("Open workspace output: %s\n", output) - - Expect(output).To(ContainSubstring("Opening workspace")) - Expect(output).To(ContainSubstring(workspaceId)) - }) - }) - - Context("Open Workspace Error Handling", func() { - It("should fail when workspace ID is missing", func() { - By("Attempting to open workspace without ID") - originalWsId := os.Getenv("CS_WORKSPACE_ID") - originalWsIdFallback := os.Getenv("WORKSPACE_ID") - _ = os.Unsetenv("CS_WORKSPACE_ID") - _ = os.Unsetenv("WORKSPACE_ID") - defer func() { - _ = os.Setenv("CS_WORKSPACE_ID", originalWsId) - _ = os.Setenv("WORKSPACE_ID", originalWsIdFallback) - }() - - output, exitCode := intutil.RunCommandWithExitCode( - "open", "workspace", - ) - log.Printf("Open without workspace ID output: %s (exit code: %d)\n", output, exitCode) - Expect(exitCode).NotTo(Equal(0)) - Expect(output).To(Or( - ContainSubstring("workspace"), - ContainSubstring("required"), - )) - }) - }) -}) - -var _ = Describe("Workspace Edge Cases and Advanced Operations", func() { - var ( - teamId string - workspaceName string - workspaceId string - ) - - BeforeEach(func() { - teamId, _ = intutil.SkipIfMissingEnvVars() - workspaceName = fmt.Sprintf("cli-edge-test-%d", time.Now().Unix()) - }) - - AfterEach(func() { - if workspaceId != "" { - By(fmt.Sprintf("Cleaning up: deleting workspace %s (ID: %s)", workspaceName, workspaceId)) - intutil.CleanupWorkspace(workspaceId) - workspaceId = "" - } - }) - - Context("Workspace Creation Edge Cases", func() { - It("should create a workspace with a very long name", func() { - longName := fmt.Sprintf("cli-very-long-workspace-name-test-%d", time.Now().Unix()) - By("Creating a workspace with a long name") - output := intutil.RunCommand( - "create", "workspace", longName, - "-t", teamId, - "-p", "8", - "--timeout", "15m", - ) - log.Printf("Create workspace with long name output: %s\n", output) - - if output != "" && !strings.Contains(output, "error") { - Expect(output).To(ContainSubstring("Workspace created")) - workspaceId = intutil.ExtractWorkspaceId(output) - } - }) - - It("should handle creation timeout gracefully", func() { - By("Creating a workspace with very short timeout") - output, exitCode := intutil.RunCommandWithExitCode( - "create", "workspace", workspaceName, - "-t", teamId, - "-p", "8", - "--timeout", "1s", - ) - log.Printf("Create with short timeout output: %s (exit code: %d)\n", output, exitCode) - - if exitCode != 0 { - Expect(output).To(Or( - ContainSubstring("timeout"), - ContainSubstring("timed out"), - )) - } else if strings.Contains(output, "Workspace created") { - workspaceId = intutil.ExtractWorkspaceId(output) - } - }) - }) - - Context("Exec Command Edge Cases", func() { - BeforeEach(func() { - By("Creating a workspace") - output := intutil.RunCommand( - "create", "workspace", workspaceName, - "-t", teamId, - "-p", "8", - "--timeout", "15m", - ) - Expect(output).To(ContainSubstring("Workspace created")) - workspaceId = intutil.ExtractWorkspaceId(output) - Expect(workspaceId).NotTo(BeEmpty()) - }) - - It("should execute commands with multiple arguments", func() { - By("Executing a command with multiple arguments") - output := intutil.RunCommand( - "exec", - "-w", workspaceId, - "--", - "sh", "-c", "echo test1 && echo test2", - ) - log.Printf("Exec with multiple args output: %s\n", output) - Expect(output).To(ContainSubstring("test1")) - Expect(output).To(ContainSubstring("test2")) - }) - - It("should handle commands that output to stderr", func() { - By("Executing a command that writes to stderr") - output := intutil.RunCommand( - "exec", - "-w", workspaceId, - "--", - "sh", "-c", "echo error message >&2", - ) - log.Printf("Exec with stderr output: %s\n", output) - Expect(output).To(ContainSubstring("error message")) - }) - - It("should handle commands with exit codes", func() { - By("Executing a command that exits with non-zero code") - output, exitCode := intutil.RunCommandWithExitCode( - "exec", - "-w", workspaceId, - "--", - "sh", "-c", "exit 42", - ) - log.Printf("Exec with exit code output: %s (exit code: %d)\n", output, exitCode) - }) - - It("should execute long-running commands", func() { - By("Executing a command that takes a few seconds") - output := intutil.RunCommand( - "exec", - "-w", workspaceId, - "--", - "sh", "-c", "sleep 2 && echo completed", - ) - log.Printf("Exec long-running command output: %s\n", output) - Expect(output).To(ContainSubstring("completed")) - }) - }) - - Context("Workspace Deletion Edge Cases", func() { - It("should prevent deletion without confirmation when not forced", func() { - By("Creating a workspace") - output := intutil.RunCommand( - "create", "workspace", workspaceName, - "-t", teamId, - "-p", "8", - "--timeout", "15m", - ) - Expect(output).To(ContainSubstring("Workspace created")) - workspaceId = intutil.ExtractWorkspaceId(output) - Expect(workspaceId).NotTo(BeEmpty()) - - By("Attempting to delete without --yes flag") - output = intutil.RunCommand( - "delete", "workspace", - "-w", workspaceId, - "--yes", - ) - log.Printf("Delete with confirmation output: %s\n", output) - Expect(output).To(ContainSubstring("deleted")) - workspaceId = "" - }) - - It("should fail gracefully when deleting already deleted workspace", func() { - By("Creating and deleting a workspace") - output := intutil.RunCommand( - "create", "workspace", workspaceName, - "-t", teamId, - "-p", "8", - "--timeout", "15m", - ) - Expect(output).To(ContainSubstring("Workspace created")) - tempWsId := intutil.ExtractWorkspaceId(output) - - output = intutil.RunCommand( - "delete", "workspace", - "-w", tempWsId, - "--yes", - ) - Expect(output).To(ContainSubstring("deleted")) - - By("Attempting to delete the same workspace again") - output, exitCode := intutil.RunCommandWithExitCode( - "delete", "workspace", - "-w", tempWsId, - "--yes", - ) - log.Printf("Delete already deleted workspace output: %s (exit code: %d)\n", output, exitCode) - Expect(exitCode).NotTo(Equal(0)) - Expect(output).To(Or( - ContainSubstring("error"), - ContainSubstring("failed"), - ContainSubstring("not found"), - )) - }) - }) -}) - -var _ = Describe("Version and Help Tests", func() { - Context("Version Command", func() { - It("should display version information", func() { - By("Running version command") - output := intutil.RunCommand("version") - log.Printf("Version output: %s\n", output) - - Expect(output).To(Or( - ContainSubstring("version"), - ContainSubstring("Version"), - MatchRegexp(`\d+\.\d+\.\d+`), - )) - }) - }) - - Context("Help Commands", func() { - It("should display main help", func() { - By("Running help command") - output := intutil.RunCommand("--help") - log.Printf("Help output length: %d\n", len(output)) - - Expect(output).To(ContainSubstring("Usage:")) - Expect(output).To(ContainSubstring("Available Commands:")) - }) - - It("should display help for all subcommands", func() { - testCases := []struct { - command []string - shouldMatch string - }{ - {[]string{"create", "--help"}, "workspace"}, - {[]string{"exec", "--help"}, "exec"}, - {[]string{"log", "--help"}, "log"}, - {[]string{"start", "pipeline", "--help"}, "pipeline"}, - {[]string{"git", "pull", "--help"}, "pull"}, - {[]string{"set-env", "--help"}, "set-env"}, - } - - for _, tc := range testCases { - By(fmt.Sprintf("Testing %v", tc.command)) - output := intutil.RunCommand(tc.command...) - Expect(output).To(ContainSubstring("Usage:")) - Expect(output).To(ContainSubstring(tc.shouldMatch)) - } - }) - }) - - Context("Invalid Commands", func() { - It("should handle unknown commands gracefully", func() { - By("Running unknown command") - output, exitCode := intutil.RunCommandWithExitCode("unknowncommand") - log.Printf("Unknown command output: %s (exit code: %d)\n", output, exitCode) - - Expect(exitCode).NotTo(Equal(0)) - Expect(output).To(Or( - ContainSubstring("unknown command"), - ContainSubstring("Error:"), - )) - }) - - It("should suggest similar commands for typos", func() { - By("Running misspelled command") - output, exitCode := intutil.RunCommandWithExitCode("listt") - log.Printf("Typo command output: %s (exit code: %d)\n", output, exitCode) - - Expect(exitCode).NotTo(Equal(0)) - lowerOutput := strings.ToLower(output) - Expect(lowerOutput).To(Or( - ContainSubstring("unknown"), - ContainSubstring("error"), - ContainSubstring("did you mean"), - )) - }) - }) - - Context("Global Flags", func() { - It("should accept all global flags", func() { - By("Testing --api flag") - output := intutil.RunCommand( - "--api", "https://example.com/api", - "list", "teams", - ) - Expect(output).NotTo(ContainSubstring("unknown flag")) - - By("Testing --verbose flag") - output = intutil.RunCommand( - "--verbose", - "list", "plans", - ) - Expect(output).NotTo(ContainSubstring("unknown flag")) - - By("Testing -v shorthand") - output = intutil.RunCommand( - "-v", - "list", "baseimages", - ) - Expect(output).NotTo(ContainSubstring("unknown flag")) - }) - }) -}) - -var _ = Describe("List Command Tests", func() { - var teamId string - - BeforeEach(func() { - teamId, _ = intutil.SkipIfMissingEnvVars() - }) - - Context("List Workspaces", func() { - It("should list all workspaces in team with proper formatting", func() { - By("Listing workspaces") - output := intutil.RunCommand("list", "workspaces", "-t", teamId) - log.Printf("List workspaces output length: %d\n", len(output)) - - Expect(output).To(ContainSubstring("TEAM ID")) - Expect(output).To(ContainSubstring("ID")) - Expect(output).To(ContainSubstring("NAME")) - }) - }) - - Context("List Plans", func() { - It("should list all available plans", func() { - By("Listing plans") - output := intutil.RunCommand("list", "plans") - log.Printf("List plans output: %s\n", output) - - Expect(output).To(ContainSubstring("ID")) - Expect(output).To(ContainSubstring("NAME")) - Expect(output).To(Or( - ContainSubstring("Micro"), - ContainSubstring("Free"), - )) - }) - - It("should show plan details like CPU and RAM", func() { - By("Listing plans with details") - output := intutil.RunCommand("list", "plans") - log.Printf("Plan details output length: %d\n", len(output)) - - Expect(output).To(ContainSubstring("CPU")) - Expect(output).To(ContainSubstring("RAM")) - }) - }) - - Context("List Base Images", func() { - It("should list available base images", func() { - By("Listing base images") - output := intutil.RunCommand("list", "baseimages") - log.Printf("List baseimages output: %s\n", output) - - Expect(output).To(ContainSubstring("ID")) - Expect(output).To(ContainSubstring("NAME")) - }) - - It("should show Ubuntu images", func() { - By("Checking for Ubuntu in base images") - output := intutil.RunCommand("list", "baseimages") - - Expect(output).To(ContainSubstring("Ubuntu")) - }) - }) - - Context("List Teams", func() { - It("should list teams user has access to", func() { - By("Listing teams") - output := intutil.RunCommand("list", "teams") - log.Printf("List teams output: %s\n", output) - - Expect(output).To(ContainSubstring("ID")) - Expect(output).To(ContainSubstring("NAME")) - Expect(output).To(ContainSubstring(teamId)) - }) - - It("should show team role", func() { - By("Checking team roles") - output := intutil.RunCommand("list", "teams") - - Expect(output).To(Or( - ContainSubstring("Admin"), - ContainSubstring("Member"), - ContainSubstring("ROLE"), - )) - }) - }) - - Context("List Error Handling", func() { - It("should handle missing or invalid list subcommand", func() { - By("Running list without subcommand") - output, exitCode := intutil.RunCommandWithExitCode("list") - log.Printf("List without subcommand output: %s (exit code: %d)\n", output, exitCode) - Expect(output).To(Or( - ContainSubstring("Available Commands:"), - ContainSubstring("Usage:"), - )) - - By("Running list with invalid subcommand") - output, _ = intutil.RunCommandWithExitCode("list", "invalid") - log.Printf("List invalid output (first 200 chars): %s\n", output[:min(200, len(output))]) - Expect(output).To(Or( - ContainSubstring("Available Commands:"), - ContainSubstring("Usage:"), - )) - }) - - It("should require team ID for workspace listing when not set globally", func() { - By("Listing workspaces without team ID in specific contexts") - output := intutil.RunCommand("list", "workspaces", "-t", teamId) - - Expect(output).NotTo(BeEmpty()) - }) - }) -}) - -var _ = Describe("Command Error Handling Tests", func() { - It("should fail gracefully with non-existent workspace for all commands", func() { - testCases := []struct { - commandName string - args []string - }{ - {"open workspace", []string{"open", "workspace", "-w", "99999999"}}, - {"log", []string{"log", "-w", "99999999"}}, - {"start pipeline", []string{"start", "pipeline", "-w", "99999999"}}, - {"git pull", []string{"git", "pull", "-w", "99999999"}}, - {"set-env", []string{"set-env", "-w", "99999999", "TEST_VAR=test"}}, - {"wake-up", []string{"wake-up", "-w", "99999999"}}, - {"curl", []string{"curl", "/", "-w", "99999999"}}, - } - - for _, tc := range testCases { - By(fmt.Sprintf("Testing %s with non-existent workspace", tc.commandName)) - output, exitCode := intutil.RunCommandWithExitCode(tc.args...) - log.Printf("%s non-existent workspace output: %s (exit code: %d)\n", tc.commandName, output, exitCode) - Expect(exitCode).NotTo(Equal(0)) - } - }) -}) - -var _ = Describe("Log Command Integration Tests", func() { - var ( - teamId string - workspaceName string - workspaceId string - ) - - BeforeEach(func() { - teamId, _ = intutil.SkipIfMissingEnvVars() - workspaceName = fmt.Sprintf("cli-log-test-%d", time.Now().Unix()) - }) - - AfterEach(func() { - if workspaceId != "" { - By(fmt.Sprintf("Cleaning up: deleting workspace %s (ID: %s)", workspaceName, workspaceId)) - intutil.CleanupWorkspace(workspaceId) - workspaceId = "" - } - }) - - Context("Log Command", func() { - BeforeEach(func() { - By("Creating a workspace") - output := intutil.RunCommand( - "create", "workspace", workspaceName, - "-t", teamId, - "-p", "8", - "--timeout", "15m", - ) - Expect(output).To(ContainSubstring("Workspace created")) - workspaceId = intutil.ExtractWorkspaceId(output) - Expect(workspaceId).NotTo(BeEmpty()) - }) - - It("should retrieve logs from workspace", func() { - By("Getting logs from workspace") - output, exitCode := intutil.RunCommandWithExitCode( - "log", - "-w", workspaceId, - ) - log.Printf("Log command output (first 500 chars): %s... (exit code: %d)\n", - output[:min(500, len(output))], exitCode) - - Expect(exitCode).To(Or(Equal(0), Equal(1))) - }) - }) -}) - -var _ = Describe("Start Pipeline Integration Tests", func() { - var ( - teamId string - workspaceName string - workspaceId string - ) - - BeforeEach(func() { - teamId, _ = intutil.SkipIfMissingEnvVars() - workspaceName = fmt.Sprintf("cli-pipeline-test-%d", time.Now().Unix()) - }) - - AfterEach(func() { - if workspaceId != "" { - By(fmt.Sprintf("Cleaning up: deleting workspace %s (ID: %s)", workspaceName, workspaceId)) - intutil.CleanupWorkspace(workspaceId) - workspaceId = "" - } - }) - - Context("Start Pipeline Command", func() { - BeforeEach(func() { - By("Creating a workspace") - output := intutil.RunCommand( - "create", "workspace", workspaceName, - "-t", teamId, - "-p", "8", - "--timeout", "15m", - ) - Expect(output).To(ContainSubstring("Workspace created")) - workspaceId = intutil.ExtractWorkspaceId(output) - Expect(workspaceId).NotTo(BeEmpty()) - }) - - It("should start pipeline successfully", func() { - By("Starting pipeline") - output, exitCode := intutil.RunCommandWithExitCode( - "start", "pipeline", - "-w", workspaceId, - ) - log.Printf("Start pipeline output: %s (exit code: %d)\n", output, exitCode) - - Expect(output).NotTo(BeEmpty()) - }) - }) -}) - -var _ = Describe("Git Pull Integration Tests", func() { - var ( - teamId string - workspaceName string - workspaceId string - ) - - BeforeEach(func() { - teamId, _ = intutil.SkipIfMissingEnvVars() - workspaceName = fmt.Sprintf("cli-git-test-%d", time.Now().Unix()) - }) - - AfterEach(func() { - if workspaceId != "" { - By(fmt.Sprintf("Cleaning up: deleting workspace %s (ID: %s)", workspaceName, workspaceId)) - intutil.CleanupWorkspace(workspaceId) - workspaceId = "" - } - }) - - Context("Git Pull Command", func() { - BeforeEach(func() { - By("Creating a workspace") - output := intutil.RunCommand( - "create", "workspace", workspaceName, - "-t", teamId, - "-p", "8", - "--timeout", "15m", - ) - Expect(output).To(ContainSubstring("Workspace created")) - workspaceId = intutil.ExtractWorkspaceId(output) - Expect(workspaceId).NotTo(BeEmpty()) - }) - - It("should execute git pull command", func() { - By("Running git pull") - output, exitCode := intutil.RunCommandWithExitCode( - "git", "pull", - "-w", workspaceId, - ) - log.Printf("Git pull output: %s (exit code: %d)\n", output, exitCode) - - Expect(output).NotTo(BeEmpty()) - }) - }) -}) - -var _ = Describe("Wake Up Workspace Integration Tests", func() { - var ( - teamId string - workspaceName string - workspaceId string - ) - - BeforeEach(func() { - teamId, _ = intutil.SkipIfMissingEnvVars() - workspaceName = fmt.Sprintf("cli-wakeup-test-%d", time.Now().Unix()) - }) - - AfterEach(func() { - if workspaceId != "" { - By(fmt.Sprintf("Cleaning up: deleting workspace %s (ID: %s)", workspaceName, workspaceId)) - intutil.CleanupWorkspace(workspaceId) - workspaceId = "" - } - }) - - Context("Wake Up Command", func() { - BeforeEach(func() { - By("Creating a workspace for wake-up testing") - output := intutil.RunCommand( - "create", "workspace", workspaceName, - "-t", teamId, - "-p", "8", - "--timeout", "15m", - ) - fmt.Printf("Create workspace output: %s\n", output) - - Expect(output).To(ContainSubstring("Workspace created")) - workspaceId = intutil.ExtractWorkspaceId(output) - Expect(workspaceId).NotTo(BeEmpty()) - - By("Waiting for workspace to be fully provisioned") - time.Sleep(5 * time.Second) - }) - - It("should wake up workspace successfully", func() { - By("Waking up the workspace") - output := intutil.RunCommand( - "wake-up", - "-w", workspaceId, - ) - fmt.Printf("Wake up workspace output: %s\n", output) - - Expect(output).To(Or( - ContainSubstring("Waking up workspace"), - // The workspace might already be running - ContainSubstring("is already running"), - )) - Expect(output).To(ContainSubstring(workspaceId)) - }) - - It("should respect custom timeout", func() { - By("Waking up workspace with custom timeout") - output, exitCode := intutil.RunCommandWithExitCode( - "wake-up", - "-w", workspaceId, - "--timeout", "5s", - ) - fmt.Printf("Wake up with timeout output: %s (exit code: %d)\n", output, exitCode) - - Expect(output).To(Or( - ContainSubstring("Waking up workspace"), - // The workspace might already be running - ContainSubstring("is already running"), - )) - Expect(exitCode).To(Equal(0)) - }) - - It("should work with workspace ID from environment variable", func() { - By("Setting CS_WORKSPACE_ID environment variable") - originalWsId := os.Getenv("CS_WORKSPACE_ID") - _ = os.Setenv("CS_WORKSPACE_ID", workspaceId) - defer func() { _ = os.Setenv("CS_WORKSPACE_ID", originalWsId) }() - - By("Waking up workspace using environment variable") - output := intutil.RunCommand("wake-up") - fmt.Printf("Wake up with env var output: %s\n", output) - - Expect(output).To(Or( - ContainSubstring("Waking up workspace"), - // The workspace might already be running - ContainSubstring("is already running"), - )) - Expect(output).To(ContainSubstring(workspaceId)) - }) - }) - - Context("Wake Up Error Handling", func() { - It("should fail when workspace ID is missing", func() { - By("Attempting to wake up workspace without ID") - originalWsId := os.Getenv("CS_WORKSPACE_ID") - originalWsIdFallback := os.Getenv("WORKSPACE_ID") - _ = os.Unsetenv("CS_WORKSPACE_ID") - _ = os.Unsetenv("WORKSPACE_ID") - defer func() { - _ = os.Setenv("CS_WORKSPACE_ID", originalWsId) - _ = os.Setenv("WORKSPACE_ID", originalWsIdFallback) - }() - - output, exitCode := intutil.RunCommandWithExitCode("wake-up") - fmt.Printf("Wake up without workspace ID output: %s (exit code: %d)\n", output, exitCode) - - Expect(exitCode).NotTo(Equal(0)) - Expect(output).To(Or( - ContainSubstring("workspace"), - ContainSubstring("required"), - ContainSubstring("not set"), - )) - }) - - It("should fail gracefully with non-existent workspace", func() { - By("Attempting to wake up non-existent workspace") - output, exitCode := intutil.RunCommandWithExitCode( - "wake-up", - "-w", "99999999", - ) - fmt.Printf("Wake up non-existent workspace output: %s (exit code: %d)\n", output, exitCode) - - Expect(exitCode).NotTo(Equal(0)) - Expect(output).To(Or( - ContainSubstring("failed to get workspace"), - ContainSubstring("not found"), - ContainSubstring("404"), - )) - }) - - It("should handle workspace without dev domain gracefully", func() { - By("Creating a workspace (which might not have dev domain configured)") - output := intutil.RunCommand( - "create", "workspace", workspaceName, - "-t", teamId, - "-p", "8", - "--timeout", "15m", - ) - fmt.Printf("Create workspace output: %s\n", output) - - Expect(output).To(ContainSubstring("Workspace created")) - workspaceId = intutil.ExtractWorkspaceId(output) - Expect(workspaceId).NotTo(BeEmpty()) - - By("Attempting to wake up the workspace") - wakeupOutput, wakeupExitCode := intutil.RunCommandWithExitCode( - "wake-up", - "-w", workspaceId, - ) - fmt.Printf("Wake up workspace output: %s (exit code: %d)\n", wakeupOutput, wakeupExitCode) - - if wakeupExitCode != 0 { - Expect(wakeupOutput).To(Or( - ContainSubstring("development domain"), - ContainSubstring("dev domain"), - ContainSubstring("failed to wake up"), - )) - } - }) - }) - - Context("Wake Up Command Help", func() { - It("should display help information", func() { - By("Running wake-up --help") - output := intutil.RunCommand("wake-up", "--help") - fmt.Printf("Wake up help output: %s\n", output) - - Expect(output).To(ContainSubstring("Wake up an on-demand workspace")) - Expect(output).To(ContainSubstring("--timeout")) - Expect(output).To(ContainSubstring("-w, --workspace")) - }) - }) -}) - -var _ = Describe("Curl Workspace Integration Tests", func() { - var ( - teamId string - workspaceName string - workspaceId string - ) - - BeforeEach(func() { - teamId, _ = intutil.SkipIfMissingEnvVars() - workspaceName = fmt.Sprintf("cli-curl-test-%d", time.Now().Unix()) - }) - - AfterEach(func() { - if workspaceId != "" { - By(fmt.Sprintf("Cleaning up: deleting workspace %s (ID: %s)", workspaceName, workspaceId)) - intutil.CleanupWorkspace(workspaceId) - workspaceId = "" - } - }) - - Context("Curl Command", func() { - BeforeEach(func() { - By("Creating a workspace for curl testing") - output := intutil.RunCommand( - "create", "workspace", workspaceName, - "-t", teamId, - "-p", "8", - "--timeout", "15m", - ) - fmt.Printf("Create workspace output: %s\n", output) - - Expect(output).To(ContainSubstring("Workspace created")) - workspaceId = intutil.ExtractWorkspaceId(output) - Expect(workspaceId).NotTo(BeEmpty()) - - By("Waiting for workspace to be fully provisioned") - time.Sleep(5 * time.Second) - }) - - It("should send authenticated request to workspace", func() { - By("Sending curl request to workspace root") - output := intutil.RunCommand( - "curl", "/", - "-w", workspaceId, - "--", "-k", "-s", "-o", "/dev/null", "-w", "%{http_code}", - ) - fmt.Printf("Curl workspace output: %s\n", output) - - Expect(output).To(ContainSubstring("Sending request to workspace")) - Expect(output).To(ContainSubstring(workspaceId)) - }) - - It("should support custom paths", func() { - By("Sending curl request to custom path") - output, exitCode := intutil.RunCommandWithExitCode( - "curl", "/api/health", - "-w", workspaceId, - "--", "-k", "-s", "-o", "/dev/null", "-w", "%{http_code}", - ) - fmt.Printf("Curl with custom path output: %s (exit code: %d)\n", output, exitCode) - - Expect(output).To(ContainSubstring("Sending request to workspace")) - }) - - It("should pass through curl arguments", func() { - By("Sending HEAD request using curl -I flag") - output := intutil.RunCommand( - "curl", "/", - "-w", workspaceId, - "--", "-k", "-I", - ) - fmt.Printf("Curl with -I flag output: %s\n", output) - - Expect(output).To(ContainSubstring("Sending request to workspace")) - }) - - It("should work with workspace ID from environment variable", func() { - By("Setting CS_WORKSPACE_ID environment variable") - originalWsId := os.Getenv("CS_WORKSPACE_ID") - _ = os.Setenv("CS_WORKSPACE_ID", workspaceId) - defer func() { _ = os.Setenv("CS_WORKSPACE_ID", originalWsId) }() - - By("Sending curl request using environment variable") - output := intutil.RunCommand( - "curl", "/", - "--", "-k", "-s", "-o", "/dev/null", "-w", "%{http_code}", - ) - fmt.Printf("Curl with env var output: %s\n", output) - - Expect(output).To(ContainSubstring("Sending request to workspace")) - Expect(output).To(ContainSubstring(workspaceId)) - }) - }) - - Context("Curl Error Handling", func() { - It("should fail when workspace ID is missing", func() { - By("Attempting to curl without workspace ID") - originalWsId := os.Getenv("CS_WORKSPACE_ID") - originalWsIdFallback := os.Getenv("WORKSPACE_ID") - _ = os.Unsetenv("CS_WORKSPACE_ID") - _ = os.Unsetenv("WORKSPACE_ID") - defer func() { - _ = os.Setenv("CS_WORKSPACE_ID", originalWsId) - _ = os.Setenv("WORKSPACE_ID", originalWsIdFallback) - }() - - output, exitCode := intutil.RunCommandWithExitCode("curl", "/") - fmt.Printf("Curl without workspace ID output: %s (exit code: %d)\n", output, exitCode) - - Expect(exitCode).NotTo(Equal(0)) - Expect(output).To(Or( - ContainSubstring("workspace"), - ContainSubstring("required"), - ContainSubstring("not set"), - )) - }) - - It("should fail gracefully with non-existent workspace", func() { - By("Attempting to curl non-existent workspace") - output, exitCode := intutil.RunCommandWithExitCode( - "curl", "/", - "-w", "99999999", - ) - fmt.Printf("Curl non-existent workspace output: %s (exit code: %d)\n", output, exitCode) - - Expect(exitCode).NotTo(Equal(0)) - Expect(output).To(Or( - ContainSubstring("failed to get workspace"), - ContainSubstring("not found"), - ContainSubstring("404"), - )) - }) - - It("should require path argument", func() { - By("Attempting to curl without path") - output, exitCode := intutil.RunCommandWithExitCode( - "curl", - "-w", "1234", - ) - fmt.Printf("Curl without path output: %s (exit code: %d)\n", output, exitCode) - - Expect(exitCode).NotTo(Equal(0)) - Expect(output).To(Or( - ContainSubstring("path"), - ContainSubstring("required"), - ContainSubstring("argument"), - )) - }) - }) - - Context("Curl Command Help", func() { - It("should display help information", func() { - By("Running curl --help") - output := intutil.RunCommand("curl", "--help") - fmt.Printf("Curl help output: %s\n", output) - - Expect(output).To(ContainSubstring("Send authenticated HTTP requests")) - Expect(output).To(ContainSubstring("--timeout")) - Expect(output).To(ContainSubstring("-w, --workspace")) - }) - }) -}) - -var _ = Describe("Command Error Handling Tests", func() { - It("should fail gracefully with non-existent workspace for all commands", func() { - testCases := []struct { - commandName string - args []string - }{ - {"open workspace", []string{"open", "workspace", "-w", "99999999"}}, - {"log", []string{"log", "-w", "99999999"}}, - {"start pipeline", []string{"start", "pipeline", "-w", "99999999"}}, - {"git pull", []string{"git", "pull", "-w", "99999999"}}, - {"set-env", []string{"set-env", "-w", "99999999", "TEST_VAR=test"}}, - } - - for _, tc := range testCases { - By(fmt.Sprintf("Testing %s with non-existent workspace", tc.commandName)) - output, exitCode := intutil.RunCommandWithExitCode(tc.args...) - log.Printf("%s non-existent workspace output: %s (exit code: %d)\n", tc.commandName, output, exitCode) - Expect(exitCode).NotTo(Equal(0)) - } - }) -}) diff --git a/int/list_test.go b/int/list_test.go new file mode 100644 index 0000000..f0392ad --- /dev/null +++ b/int/list_test.go @@ -0,0 +1,124 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + +package int_test + +import ( + "log" + + intutil "github.com/codesphere-cloud/cs-go/int/util" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("List Command Tests", Label("list"), func() { + var teamId string + + BeforeEach(func() { + teamId, _ = intutil.FailIfMissingEnvVars() + }) + + Context("List Workspaces", func() { + It("should list all workspaces in team with proper formatting", func() { + By("Listing workspaces") + output := intutil.RunCommand("list", "workspaces", "-t", teamId) + log.Printf("List workspaces output length: %d\n", len(output)) + + Expect(output).To(ContainSubstring("TEAM ID")) + Expect(output).To(ContainSubstring("ID")) + Expect(output).To(ContainSubstring("NAME")) + }) + }) + + Context("List Plans", func() { + It("should list all available plans", func() { + By("Listing plans") + output := intutil.RunCommand("list", "plans") + log.Printf("List plans output: %s\n", output) + + Expect(output).To(ContainSubstring("ID")) + Expect(output).To(ContainSubstring("NAME")) + Expect(output).To(Or( + ContainSubstring("Micro"), + ContainSubstring("Free"), + )) + }) + + It("should show plan details like CPU and RAM", func() { + By("Listing plans with details") + output := intutil.RunCommand("list", "plans") + log.Printf("Plan details output length: %d\n", len(output)) + + Expect(output).To(ContainSubstring("CPU")) + Expect(output).To(ContainSubstring("RAM")) + }) + }) + + Context("List Base Images", func() { + It("should list available base images", func() { + By("Listing base images") + output := intutil.RunCommand("list", "baseimages") + log.Printf("List baseimages output: %s\n", output) + + Expect(output).To(ContainSubstring("ID")) + Expect(output).To(ContainSubstring("NAME")) + }) + + It("should show Ubuntu images", func() { + By("Checking for Ubuntu in base images") + output := intutil.RunCommand("list", "baseimages") + + Expect(output).To(ContainSubstring("Ubuntu")) + }) + }) + + Context("List Teams", func() { + It("should list teams user has access to", func() { + By("Listing teams") + output := intutil.RunCommand("list", "teams") + log.Printf("List teams output: %s\n", output) + + Expect(output).To(ContainSubstring("ID")) + Expect(output).To(ContainSubstring("NAME")) + Expect(output).To(ContainSubstring(teamId)) + }) + + It("should show team role", func() { + By("Checking team roles") + output := intutil.RunCommand("list", "teams") + + Expect(output).To(Or( + ContainSubstring("Admin"), + ContainSubstring("Member"), + ContainSubstring("ROLE"), + )) + }) + }) + + Context("List Error Handling", func() { + It("should handle missing or invalid list subcommand", func() { + By("Running list without subcommand") + output, exitCode := intutil.RunCommandWithExitCode("list") + log.Printf("List without subcommand output: %s (exit code: %d)\n", output, exitCode) + Expect(output).To(Or( + ContainSubstring("Available Commands:"), + ContainSubstring("Usage:"), + )) + + By("Running list with invalid subcommand") + output, _ = intutil.RunCommandWithExitCode("list", "invalid") + log.Printf("List invalid output (first 200 chars): %s\n", output[:min(200, len(output))]) + Expect(output).To(Or( + ContainSubstring("Available Commands:"), + ContainSubstring("Usage:"), + )) + }) + + It("should require team ID for workspace listing when not set globally", func() { + By("Listing workspaces without team ID in specific contexts") + output := intutil.RunCommand("list", "workspaces", "-t", teamId) + + Expect(output).NotTo(BeEmpty()) + }) + }) +}) diff --git a/int/log_test.go b/int/log_test.go new file mode 100644 index 0000000..acbff6c --- /dev/null +++ b/int/log_test.go @@ -0,0 +1,53 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + +package int_test + +import ( + "fmt" + "log" + + intutil "github.com/codesphere-cloud/cs-go/int/util" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Log Command Integration Tests", Label("log"), func() { + var ( + teamId string + workspaceName string + workspaceId string + ) + + BeforeEach(func() { + teamId, _ = intutil.FailIfMissingEnvVars() + workspaceName = intutil.NewWorkspaceName("log") + }) + + AfterEach(func() { + if workspaceId != "" { + By(fmt.Sprintf("Cleaning up: deleting workspace %s (ID: %s)", workspaceName, workspaceId)) + intutil.CleanupWorkspace(workspaceId) + workspaceId = "" + } + }) + + Context("Log Command", func() { + BeforeEach(func() { + By("Creating a workspace") + workspaceId = intutil.CreateTestWorkspace(teamId, workspaceName) + }) + + It("should retrieve logs from workspace", func() { + By("Getting logs from workspace") + output, exitCode := intutil.RunCommandWithExitCode( + "log", + "-w", workspaceId, + ) + log.Printf("Log command output (first 500 chars): %s... (exit code: %d)\n", + output[:min(500, len(output))], exitCode) + + Expect(exitCode).To(Or(Equal(0), Equal(1))) + }) + }) +}) diff --git a/int/monitor_test.go b/int/monitor_test.go new file mode 100644 index 0000000..8468e44 --- /dev/null +++ b/int/monitor_test.go @@ -0,0 +1,350 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + +package int_test + +import ( + "bytes" + "context" + "fmt" + "io" + "log" + "net/http" + "os" + "os/exec" + "path/filepath" + "strings" + "time" + + intutil "github.com/codesphere-cloud/cs-go/int/util" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("cs monitor", Label("local"), func() { + var ( + certsDir string + tempDir string + caCertPath string + serverCertPath string + serverKeyPath string + monitorListenPort int + targetServerPort int + targetServer *http.Server + monitorCmdProcess *exec.Cmd + testHttpClient *http.Client + monitorOutputBuf *bytes.Buffer + targetServerOutputBuf *bytes.Buffer + ) + + BeforeEach(func() { + var err error + tempDir, err = os.MkdirTemp("", "e2e-tls-monitor-test-") + Expect(err).NotTo(HaveOccurred()) + certsDir = filepath.Join(tempDir, "certs") + + monitorListenPort, err = intutil.GetEphemeralPort() + Expect(err).NotTo(HaveOccurred()) + targetServerPort, err = intutil.GetEphemeralPort() + Expect(err).NotTo(HaveOccurred()) + + testHttpClient = &http.Client{ + Timeout: 10 * time.Second, + } + + monitorOutputBuf = new(bytes.Buffer) + targetServerOutputBuf = new(bytes.Buffer) + }) + + AfterEach(func() { + if monitorCmdProcess != nil && monitorCmdProcess.Process != nil { + log.Printf("Terminating monitor process (PID: %d). Output:\n%s\n", monitorCmdProcess.Process.Pid, monitorOutputBuf.String()) + _ = monitorCmdProcess.Process.Kill() + _, _ = monitorCmdProcess.Process.Wait() + } + + Expect(os.RemoveAll(tempDir)).NotTo(HaveOccurred()) + }) + + Context("Healthcheck forwarding", func() { + AfterEach(func() { + if targetServer != nil { + log.Printf("Terminating HTTP(S) server. Output:\n%s\n", targetServerOutputBuf.String()) + shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 1*time.Second) + defer shutdownCancel() + _ = targetServer.Shutdown(shutdownCtx) + } + }) + + It("should start a Go HTTP server, and proxy successfully", func() { + var err error + + By("Starting Go HTTPS server with generated certs") + targetServer, err = intutil.StartTestHttpServer(targetServerPort) + Expect(err).NotTo(HaveOccurred()) + intutil.WaitForPort(fmt.Sprintf("127.0.0.1:%d", targetServerPort), 10*time.Second) + log.Printf("Go HTTPS server started on port %d.\n", targetServerPort) + + By("Running 'cs monitor' command with --forward and --insecure-skip-verify") + intutil.RunCommandInBackground(monitorOutputBuf, + "monitor", + "--forward", fmt.Sprintf("http://127.0.0.1:%d/", targetServerPort), + "--address", fmt.Sprintf(":%d", monitorListenPort), + "--", "sleep", "60s", + ) + intutil.WaitForPort(fmt.Sprintf("127.0.0.1:%d", monitorListenPort), 10*time.Second) + + By("Making request to monitor proxy to verify successful forwarding") + resp, err := testHttpClient.Get(fmt.Sprintf("http://127.0.0.1:%d/", monitorListenPort)) + Expect(err).NotTo(HaveOccurred()) + defer func() { _ = resp.Body.Close() }() + + Expect(resp.StatusCode).To(Equal(http.StatusOK)) + bodyBytes, err := io.ReadAll(resp.Body) + Expect(err).NotTo(HaveOccurred()) + Expect(string(bodyBytes)).To(Equal("OK (HTTP)")) + + log.Printf("Monitor output after request:\n%s\n", monitorOutputBuf.String()) + }) + + It("should start a Go HTTPS server with generated certs, run monitor with --insecure-skip-verify, and proxy successfully", func() { + By("Generating TLS certificates") + var err error + caCertPath, serverCertPath, serverKeyPath, err = intutil.GenerateTLSCerts( + certsDir, + "localhost", + []string{"localhost", "127.0.0.1"}, + ) + Expect(err).NotTo(HaveOccurred()) + Expect(caCertPath).To(BeAnExistingFile()) + Expect(serverCertPath).To(BeAnExistingFile()) + Expect(serverKeyPath).To(BeAnExistingFile()) + + By("Starting Go HTTPS server with generated certs") + targetServer, err = intutil.StartTestHttpsServer(targetServerPort, serverCertPath, serverKeyPath) + Expect(err).NotTo(HaveOccurred()) + intutil.WaitForPort(fmt.Sprintf("127.0.0.1:%d", targetServerPort), 10*time.Second) + log.Printf("Go HTTPS server started on port %d.\n", targetServerPort) + + By("Running 'cs monitor' command with --forward and --insecure-skip-verify") + intutil.RunCommandInBackground(monitorOutputBuf, + "monitor", + "--forward", fmt.Sprintf("https://127.0.0.1:%d/", targetServerPort), + "--address", fmt.Sprintf(":%d", monitorListenPort), + "--insecure-skip-verify", + "--", "sleep", "60s", + ) + intutil.WaitForPort(fmt.Sprintf("127.0.0.1:%d", monitorListenPort), 10*time.Second) + + By("Making request to monitor proxy to verify successful forwarding") + resp, err := testHttpClient.Get(fmt.Sprintf("http://127.0.0.1:%d/", monitorListenPort)) + Expect(err).NotTo(HaveOccurred()) + defer func() { _ = resp.Body.Close() }() + + Expect(resp.StatusCode).To(Equal(http.StatusOK)) + bodyBytes, err := io.ReadAll(resp.Body) + Expect(err).NotTo(HaveOccurred()) + Expect(string(bodyBytes)).To(Equal("OK (HTTPS)")) + + log.Printf("Monitor output after request:\n%s\n", monitorOutputBuf.String()) + }) + + It("should get an error for an invalid HTTPS certificate without --insecure-skip-verify or --ca-cert-file", func() { + By("Generating TLS certificates in Go") + var err error + caCertPath, serverCertPath, serverKeyPath, err = intutil.GenerateTLSCerts( + certsDir, + "localhost", + []string{"localhost", "127.0.0.1"}, + ) + Expect(err).NotTo(HaveOccurred()) + Expect(caCertPath).To(BeAnExistingFile()) + Expect(serverCertPath).To(BeAnExistingFile()) + Expect(serverKeyPath).To(BeAnExistingFile()) + + By("Starting Go HTTPS server with generated certs") + targetServer, err = intutil.StartTestHttpsServer(targetServerPort, serverCertPath, serverKeyPath) + Expect(err).NotTo(HaveOccurred()) + intutil.WaitForPort(fmt.Sprintf("127.0.0.1:%d", targetServerPort), 10*time.Second) + log.Printf("Go HTTPS server started on port %d.\n", targetServerPort) + + By("Running 'cs monitor' command without TLS bypass/trust") + intutil.RunCommandInBackground( + monitorOutputBuf, + "monitor", + "--address", fmt.Sprintf(":%d", monitorListenPort), + "--forward", fmt.Sprintf("https://127.0.0.1:%d/", targetServerPort), + "--", "sleep", "60s", + ) + + intutil.WaitForPort(fmt.Sprintf("127.0.0.1:%d", monitorListenPort), 10*time.Second) + log.Printf("Monitor command started on port %d. Initial output:\n%s\n", monitorListenPort, monitorOutputBuf.String()) + + By("Making request to monitor proxy and expecting a Bad Gateway error") + resp, err := testHttpClient.Get(fmt.Sprintf("http://127.0.0.1:%d/", monitorListenPort)) + Expect(err).NotTo(HaveOccurred()) + defer func() { _ = resp.Body.Close() }() + + Expect(resp.StatusCode).To(Equal(http.StatusBadGateway)) + bodyBytes, err := io.ReadAll(resp.Body) + Expect(err).NotTo(HaveOccurred()) + Expect(string(bodyBytes)).To(ContainSubstring("Error forwarding request")) + Expect(string(bodyBytes)).To(ContainSubstring("tls: failed to verify certificate")) + + log.Printf("Monitor output after request:\n%s\n", monitorOutputBuf.String()) + }) + + It("should forward to an HTTPS target with --ca-cert-file and return 200 OK", func() { + By("Generating TLS certificates in Go") + var err error + caCertPath, serverCertPath, serverKeyPath, err = intutil.GenerateTLSCerts( + certsDir, + "localhost", + []string{"localhost", "127.0.0.1"}, + ) + Expect(err).NotTo(HaveOccurred()) + Expect(caCertPath).To(BeAnExistingFile()) + Expect(serverCertPath).To(BeAnExistingFile()) + Expect(serverKeyPath).To(BeAnExistingFile()) + + By("Starting Go HTTPS server with generated certs") + targetServer, err = intutil.StartTestHttpsServer(targetServerPort, serverCertPath, serverKeyPath) + Expect(err).NotTo(HaveOccurred()) + intutil.WaitForPort(fmt.Sprintf("127.0.0.1:%d", targetServerPort), 10*time.Second) + log.Printf("Go HTTPS server started on port %d.\n", targetServerPort) + + By("Running 'cs monitor' command with --ca-cert-file") + intutil.RunCommandInBackground(monitorOutputBuf, + "monitor", + "--address", fmt.Sprintf(":%d", monitorListenPort), + "--forward", fmt.Sprintf("https://127.0.0.1:%d/", targetServerPort), + "--ca-cert-file", caCertPath, + "--", + "sleep", "60s", + ) + intutil.WaitForPort(fmt.Sprintf("127.0.0.1:%d", monitorListenPort), 10*time.Second) + log.Printf("Monitor command started on port %d. Initial output:\n%s\n", monitorListenPort, monitorOutputBuf.String()) + + By("Making request to monitor proxy to verify successful forwarding") + resp, err := testHttpClient.Get(fmt.Sprintf("http://127.0.0.1:%d/", monitorListenPort)) + Expect(err).NotTo(HaveOccurred()) + defer func() { _ = resp.Body.Close() }() + + Expect(resp.StatusCode).To(Equal(http.StatusOK)) + bodyBytes, err := io.ReadAll(resp.Body) + Expect(err).NotTo(HaveOccurred()) + Expect(string(bodyBytes)).To(Equal("OK (HTTPS)")) + + log.Printf("Monitor output after request:\n%s\n", monitorOutputBuf.String()) + }) + }) + + Context("Prometheus Metrics Endpoint", func() { + It("should expose Prometheus metrics when no forward is specified", func() { + By("Running 'cs monitor' command without forwarding (metrics only)") + monitorCmdProcess = intutil.RunCommandInBackground(monitorOutputBuf, + "monitor", + "--address", fmt.Sprintf(":%d", monitorListenPort), + "--", "sleep", "60s", + ) + intutil.WaitForPort(fmt.Sprintf("127.0.0.1:%d", monitorListenPort), 10*time.Second) + log.Printf("Monitor command started on port %d. Initial output:\n%s\n", monitorListenPort, monitorOutputBuf.String()) + + By("Making a request to the monitor's metrics endpoint") + resp, err := testHttpClient.Get(fmt.Sprintf("http://127.0.0.1:%d/metrics", monitorListenPort)) + Expect(err).NotTo(HaveOccurred()) + defer func() { _ = resp.Body.Close() }() + + Expect(resp.StatusCode).To(Equal(http.StatusOK)) + bodyBytes, err := io.ReadAll(resp.Body) + Expect(err).NotTo(HaveOccurred()) + Expect(string(bodyBytes)).To(ContainSubstring("cs_monitor_restarts_total")) + log.Printf("Monitor output after metrics request:\n%s\n", monitorOutputBuf.String()) + }) + + It("should redirect root to /metrics", func() { + By("Running 'cs monitor' command without forwarding (metrics only)") + monitorCmdProcess = intutil.RunCommandInBackground(monitorOutputBuf, + "monitor", + "--address", fmt.Sprintf(":%d", monitorListenPort), + "--", "sleep", "60s", + ) + intutil.WaitForPort(fmt.Sprintf("127.0.0.1:%d", monitorListenPort), 10*time.Second) + log.Printf("Monitor command started on port %d. Initial output:\n%s\n", monitorListenPort, monitorOutputBuf.String()) + + By("Making a request to the monitor's root endpoint and expecting a redirect") + client := &http.Client{ + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + Timeout: 5 * time.Second, + } + resp, err := client.Get(fmt.Sprintf("http://127.0.0.1:%d/", monitorListenPort)) + Expect(err).NotTo(HaveOccurred()) + defer func() { _ = resp.Body.Close() }() + + Expect(resp.StatusCode).To(Equal(http.StatusMovedPermanently)) + Expect(resp.Header.Get("Location")).To(Equal("/metrics")) + log.Printf("Monitor output after redirect request:\n%s\n", monitorOutputBuf.String()) + }) + }) + + Context("Command Execution and Restart Logic", func() { + It("should execute the command once if it succeeds", func() { + monitorCmdProcess = intutil.RunCommandInBackground(monitorOutputBuf, + "monitor", + "--address", fmt.Sprintf(":%d", monitorListenPort), + "--max-restarts", "0", + "--", "true", + ) + + Eventually(monitorCmdProcess.Wait, "5s").Should(Succeed(), "Monitor process should exit successfully") + + output := monitorOutputBuf.String() + Expect(output).To(ContainSubstring("command exited")) + Expect(output).To(ContainSubstring("returnCode=0")) + Expect(output).To(ContainSubstring("maximum number of restarts reached, exiting")) + Expect(strings.Count(output, "command exited")).To(Equal(1), "Command should have executed only once") + }) + + It("should restart the command if it exits with non-zero code quickly", func() { + monitorCmdProcess = intutil.RunCommandInBackground(monitorOutputBuf, + "monitor", + "--address", fmt.Sprintf(":%d", monitorListenPort), + "--max-restarts", "1", + "--", "bash", "-c", "echo FAKE_OUTPUT;exit 1", + ) + + Eventually(monitorCmdProcess.Wait, "15s").Should(Succeed(), "Monitor process should exit after restarts") + + output := monitorOutputBuf.String() + Expect(output).To(ContainSubstring("command exited")) + Expect(output).To(ContainSubstring("returnCode=1")) + Expect(output).To(ContainSubstring("command exited with non-zero code in less than 1 second. Waiting 5 seconds before next restart")) + Expect(output).To(ContainSubstring("cs monitor: restarting")) + Expect(output).To(ContainSubstring("maximum number of restarts reached, exiting")) + Expect(strings.Count(output, "FAKE_OUTPUT")).To(Equal(3), "Command should have executed twice") + }) + + It("should stop command runner on context cancellation", func() { + By("Running 'cs monitor' command with infinite restarts") + monitorCmdProcess = intutil.RunCommandInBackground(monitorOutputBuf, + "monitor", + "--address", fmt.Sprintf(":%d", monitorListenPort), + "--max-restarts", "-1", + "--", "sleep", "10s", + ) + Eventually(func() string { return monitorOutputBuf.String() }, "5s").Should(ContainSubstring("starting monitor")) + + By("Stopping command execution") + err := monitorCmdProcess.Process.Signal(os.Interrupt) + Expect(err).NotTo(HaveOccurred()) + _, _ = monitorCmdProcess.Process.Wait() + + output := monitorOutputBuf.String() + Expect(output).To(ContainSubstring("initiating graceful shutdown...")) + Expect(output).To(ContainSubstring("stopping command runner.")) + Expect(output).NotTo(ContainSubstring("error executing command")) + }) + }) +}) diff --git a/int/pipeline_test.go b/int/pipeline_test.go new file mode 100644 index 0000000..f2c26c4 --- /dev/null +++ b/int/pipeline_test.go @@ -0,0 +1,52 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + +package int_test + +import ( + "fmt" + "log" + + intutil "github.com/codesphere-cloud/cs-go/int/util" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Start Pipeline Integration Tests", Label("pipeline"), func() { + var ( + teamId string + workspaceName string + workspaceId string + ) + + BeforeEach(func() { + teamId, _ = intutil.FailIfMissingEnvVars() + workspaceName = intutil.NewWorkspaceName("pipeline") + }) + + AfterEach(func() { + if workspaceId != "" { + By(fmt.Sprintf("Cleaning up: deleting workspace %s (ID: %s)", workspaceName, workspaceId)) + intutil.CleanupWorkspace(workspaceId) + workspaceId = "" + } + }) + + Context("Start Pipeline Command", func() { + BeforeEach(func() { + By("Creating a workspace") + workspaceId = intutil.CreateTestWorkspace(teamId, workspaceName) + }) + + It("should start pipeline successfully", func() { + By("Starting pipeline") + output, exitCode := intutil.RunCommandWithExitCode( + "start", "pipeline", + "-w", workspaceId, + ) + log.Printf("Start pipeline output: %s (exit code: %d)\n", output, exitCode) + + Expect(output).NotTo(BeEmpty()) + }) + }) +}) diff --git a/int/util/constants.go b/int/util/constants.go new file mode 100644 index 0000000..c366de4 --- /dev/null +++ b/int/util/constants.go @@ -0,0 +1,29 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + +package util + +import "time" + +const ( + DefaultPlanId = "8" + DefaultCreateTimeout = "15m" + NonExistentWorkspaceId = "99999999" + WorkspaceCreatedOutput = "Workspace created" +) + +var PostCreateWaitTime = 5 * time.Second + +// WorkspaceNamePrefixes contains all workspace name prefixes used by integration tests. +// This is used for global cleanup in AfterSuite to catch orphaned workspaces. +var WorkspaceNamePrefixes = []string{ + "cli-git-test-", + "cli-pipeline-test-", + "cli-log-test-", + "cli-open-test-", + "cli-setenv-test-", + "cli-edge-test-", + "cli-very-long-workspace-name-test-", + "cli-wakeup-test-", + "cli-curl-test-", +} diff --git a/int/util/test_helpers.go b/int/util/test_helpers.go index 5c40c11..4b2b0ed 100644 --- a/int/util/test_helpers.go +++ b/int/util/test_helpers.go @@ -9,7 +9,7 @@ import ( ginkgo "github.com/onsi/ginkgo/v2" ) -func SkipIfMissingEnvVars() (teamId, token string) { +func FailIfMissingEnvVars() (teamId, token string) { teamId = os.Getenv("CS_TEAM_ID") if teamId == "" { ginkgo.Fail("CS_TEAM_ID environment variable not set") @@ -22,3 +22,17 @@ func SkipIfMissingEnvVars() (teamId, token string) { return teamId, token } + +// WithClearedWorkspaceEnv temporarily unsets CS_WORKSPACE_ID and WORKSPACE_ID, +// calls fn, then restores the original values. +func WithClearedWorkspaceEnv(fn func()) { + originalWsId := os.Getenv("CS_WORKSPACE_ID") + originalWsIdFallback := os.Getenv("WORKSPACE_ID") + _ = os.Unsetenv("CS_WORKSPACE_ID") + _ = os.Unsetenv("WORKSPACE_ID") + defer func() { + _ = os.Setenv("CS_WORKSPACE_ID", originalWsId) + _ = os.Setenv("WORKSPACE_ID", originalWsIdFallback) + }() + fn() +} diff --git a/int/util/workspace.go b/int/util/workspace.go index c6269cc..ae8ef7a 100644 --- a/int/util/workspace.go +++ b/int/util/workspace.go @@ -5,6 +5,7 @@ package util import ( "bytes" + "fmt" "log" "os" "os/exec" @@ -13,8 +14,32 @@ import ( "time" "github.com/codesphere-cloud/cs-go/api" + ginkgo "github.com/onsi/ginkgo/v2" + "github.com/onsi/gomega" ) +// CreateTestWorkspace creates a workspace with standard settings and returns the workspace ID. +// It fails the test if creation fails or no workspace ID is returned. +func CreateTestWorkspace(teamId, workspaceName string) string { + ginkgo.GinkgoHelper() + output := RunCommand( + "create", "workspace", workspaceName, + "-t", teamId, + "-p", DefaultPlanId, + "--timeout", DefaultCreateTimeout, + ) + log.Printf("Create workspace output: %s\n", output) + gomega.Expect(output).To(gomega.ContainSubstring(WorkspaceCreatedOutput)) + workspaceId := ExtractWorkspaceId(output) + gomega.Expect(workspaceId).NotTo(gomega.BeEmpty()) + return workspaceId +} + +// NewWorkspaceName generates a unique workspace name with the given prefix. +func NewWorkspaceName(prefix string) string { + return fmt.Sprintf("cli-%s-test-%d", prefix, time.Now().Unix()) +} + func CheckBillingStatus(teamId string) (bool, string) { testName := "billing-check-temp" output, exitCode := RunCommandWithExitCode( @@ -127,15 +152,6 @@ func ExtractTeamId(output string) string { return "" } -func ContainsAny(s string, substrings []string) bool { - for _, substring := range substrings { - if strings.Contains(s, substring) { - return true - } - } - return false -} - func CleanupTeam(teamId string) { if teamId == "" { return diff --git a/int/version_help_test.go b/int/version_help_test.go new file mode 100644 index 0000000..5bda646 --- /dev/null +++ b/int/version_help_test.go @@ -0,0 +1,115 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + +package int_test + +import ( + "fmt" + "log" + "strings" + + intutil "github.com/codesphere-cloud/cs-go/int/util" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Version and Help Tests", Label("local"), func() { + Context("Version Command", func() { + It("should display version information", func() { + By("Running version command") + output := intutil.RunCommand("version") + log.Printf("Version output: %s\n", output) + + Expect(output).To(Or( + ContainSubstring("version"), + ContainSubstring("Version"), + MatchRegexp(`\d+\.\d+\.\d+`), + )) + }) + }) + + Context("Help Commands", func() { + It("should display main help", func() { + By("Running help command") + output := intutil.RunCommand("--help") + log.Printf("Help output length: %d\n", len(output)) + + Expect(output).To(ContainSubstring("Usage:")) + Expect(output).To(ContainSubstring("Available Commands:")) + }) + + It("should display help for all subcommands", func() { + testCases := []struct { + command []string + shouldMatch string + }{ + {[]string{"create", "--help"}, "workspace"}, + {[]string{"exec", "--help"}, "exec"}, + {[]string{"log", "--help"}, "log"}, + {[]string{"start", "pipeline", "--help"}, "pipeline"}, + {[]string{"git", "pull", "--help"}, "pull"}, + {[]string{"set-env", "--help"}, "set-env"}, + } + + for _, tc := range testCases { + By(fmt.Sprintf("Testing %v", tc.command)) + output := intutil.RunCommand(tc.command...) + Expect(output).To(ContainSubstring("Usage:")) + Expect(output).To(ContainSubstring(tc.shouldMatch)) + } + }) + }) + + Context("Invalid Commands", func() { + It("should handle unknown commands gracefully", func() { + By("Running unknown command") + output, exitCode := intutil.RunCommandWithExitCode("unknowncommand") + log.Printf("Unknown command output: %s (exit code: %d)\n", output, exitCode) + + Expect(exitCode).NotTo(Equal(0)) + Expect(output).To(Or( + ContainSubstring("unknown command"), + ContainSubstring("Error:"), + )) + }) + + It("should suggest similar commands for typos", func() { + By("Running misspelled command") + output, exitCode := intutil.RunCommandWithExitCode("listt") + log.Printf("Typo command output: %s (exit code: %d)\n", output, exitCode) + + Expect(exitCode).NotTo(Equal(0)) + lowerOutput := strings.ToLower(output) + Expect(lowerOutput).To(Or( + ContainSubstring("unknown"), + ContainSubstring("error"), + ContainSubstring("did you mean"), + )) + }) + }) + + Context("Global Flags", func() { + It("should accept all global flags", func() { + By("Testing --api flag") + output := intutil.RunCommand( + "--api", "https://example.com/api", + "list", "teams", + ) + Expect(output).NotTo(ContainSubstring("unknown flag")) + + By("Testing --verbose flag") + output = intutil.RunCommand( + "--verbose", + "list", "plans", + ) + Expect(output).NotTo(ContainSubstring("unknown flag")) + + By("Testing -v shorthand") + output = intutil.RunCommand( + "-v", + "list", "baseimages", + ) + Expect(output).NotTo(ContainSubstring("unknown flag")) + }) + }) +}) diff --git a/int/wakeup_test.go b/int/wakeup_test.go new file mode 100644 index 0000000..8a66f70 --- /dev/null +++ b/int/wakeup_test.go @@ -0,0 +1,146 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + +package int_test + +import ( + "fmt" + "log" + "os" + "time" + + intutil "github.com/codesphere-cloud/cs-go/int/util" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Wake Up Workspace Integration Tests", Label("wakeup"), func() { + var ( + teamId string + workspaceName string + workspaceId string + ) + + BeforeEach(func() { + teamId, _ = intutil.FailIfMissingEnvVars() + workspaceName = intutil.NewWorkspaceName("wakeup") + }) + + AfterEach(func() { + if workspaceId != "" { + By(fmt.Sprintf("Cleaning up: deleting workspace %s (ID: %s)", workspaceName, workspaceId)) + intutil.CleanupWorkspace(workspaceId) + workspaceId = "" + } + }) + + Context("Wake Up Command", func() { + BeforeEach(func() { + By("Creating a workspace for wake-up testing") + workspaceId = intutil.CreateTestWorkspace(teamId, workspaceName) + + By("Waiting for workspace to be fully provisioned") + time.Sleep(intutil.PostCreateWaitTime) + }) + + It("should wake up workspace successfully", func() { + By("Waking up the workspace") + output := intutil.RunCommand( + "wake-up", + "-w", workspaceId, + ) + log.Printf("Wake up workspace output: %s\n", output) + + Expect(output).To(Or( + ContainSubstring("Waking up workspace"), + // The workspace might already be running + ContainSubstring("is already running"), + )) + Expect(output).To(ContainSubstring(workspaceId)) + }) + + It("should respect custom timeout", func() { + By("Waking up workspace with custom timeout") + output, exitCode := intutil.RunCommandWithExitCode( + "wake-up", + "-w", workspaceId, + "--timeout", "5s", + ) + log.Printf("Wake up with timeout output: %s (exit code: %d)\n", output, exitCode) + + Expect(output).To(Or( + ContainSubstring("Waking up workspace"), + // The workspace might already be running + ContainSubstring("is already running"), + )) + Expect(exitCode).To(Equal(0)) + }) + + It("should work with workspace ID from environment variable", func() { + By("Setting CS_WORKSPACE_ID environment variable") + originalWsId := os.Getenv("CS_WORKSPACE_ID") + _ = os.Setenv("CS_WORKSPACE_ID", workspaceId) + defer func() { _ = os.Setenv("CS_WORKSPACE_ID", originalWsId) }() + + By("Waking up workspace using environment variable") + output := intutil.RunCommand("wake-up") + log.Printf("Wake up with env var output: %s\n", output) + + Expect(output).To(Or( + ContainSubstring("Waking up workspace"), + // The workspace might already be running + ContainSubstring("is already running"), + )) + Expect(output).To(ContainSubstring(workspaceId)) + }) + }) + + Context("Wake Up Error Handling", func() { + It("should fail when workspace ID is missing", func() { + By("Attempting to wake up workspace without ID") + intutil.WithClearedWorkspaceEnv(func() { + output, exitCode := intutil.RunCommandWithExitCode("wake-up") + log.Printf("Wake up without workspace ID output: %s (exit code: %d)\n", output, exitCode) + + Expect(exitCode).NotTo(Equal(0)) + Expect(output).To(Or( + ContainSubstring("workspace"), + ContainSubstring("required"), + ContainSubstring("not set"), + )) + }) + }) + + It("should handle workspace without dev domain gracefully", func() { + By("Creating a workspace (which might not have dev domain configured)") + workspaceId = intutil.CreateTestWorkspace(teamId, workspaceName) + + By("Attempting to wake up the workspace") + wakeupOutput, wakeupExitCode := intutil.RunCommandWithExitCode( + "wake-up", + "-w", workspaceId, + ) + log.Printf("Wake up workspace output: %s (exit code: %d)\n", wakeupOutput, wakeupExitCode) + + if wakeupExitCode != 0 { + Expect(wakeupOutput).To(Or( + ContainSubstring("development domain"), + ContainSubstring("dev domain"), + ContainSubstring("failed to wake up"), + )) + } + }) + }) + + Context("Wake Up Command Help", func() { + It("should display help information", func() { + By("Running wake-up --help") + output := intutil.RunCommand("wake-up", "--help") + log.Printf("Wake up help output: %s\n", output) + + Expect(output).To(ContainSubstring("Wake up an on-demand workspace")) + Expect(output).To(ContainSubstring("--timeout")) + Expect(output).To(ContainSubstring("-w, --workspace")) + }) + }) +}) diff --git a/int/workspace_test.go b/int/workspace_test.go new file mode 100644 index 0000000..11ed927 --- /dev/null +++ b/int/workspace_test.go @@ -0,0 +1,229 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + +package int_test + +import ( + "fmt" + "log" + "strings" + + intutil "github.com/codesphere-cloud/cs-go/int/util" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Open Workspace Integration Tests", Label("workspace"), func() { + var ( + teamId string + workspaceName string + workspaceId string + ) + + BeforeEach(func() { + teamId, _ = intutil.FailIfMissingEnvVars() + workspaceName = intutil.NewWorkspaceName("open") + }) + + AfterEach(func() { + if workspaceId != "" { + By(fmt.Sprintf("Cleaning up: deleting workspace %s (ID: %s)", workspaceName, workspaceId)) + intutil.CleanupWorkspace(workspaceId) + workspaceId = "" + } + }) + + Context("Open Workspace Command", func() { + BeforeEach(func() { + By("Creating a workspace") + workspaceId = intutil.CreateTestWorkspace(teamId, workspaceName) + }) + + It("should open workspace successfully", func() { + By("Opening the workspace") + output := intutil.RunCommand( + "open", "workspace", + "-w", workspaceId, + ) + log.Printf("Open workspace output: %s\n", output) + + Expect(output).To(ContainSubstring("Opening workspace")) + Expect(output).To(ContainSubstring(workspaceId)) + }) + }) + + Context("Open Workspace Error Handling", func() { + It("should fail when workspace ID is missing", func() { + By("Attempting to open workspace without ID") + intutil.WithClearedWorkspaceEnv(func() { + output, exitCode := intutil.RunCommandWithExitCode( + "open", "workspace", + ) + log.Printf("Open without workspace ID output: %s (exit code: %d)\n", output, exitCode) + Expect(exitCode).NotTo(Equal(0)) + Expect(output).To(Or( + ContainSubstring("workspace"), + ContainSubstring("required"), + )) + }) + }) + }) +}) + +var _ = Describe("Workspace Edge Cases and Advanced Operations", Label("workspace"), func() { + var ( + teamId string + workspaceName string + workspaceId string + ) + + BeforeEach(func() { + teamId, _ = intutil.FailIfMissingEnvVars() + workspaceName = intutil.NewWorkspaceName("edge") + }) + + AfterEach(func() { + if workspaceId != "" { + By(fmt.Sprintf("Cleaning up: deleting workspace %s (ID: %s)", workspaceName, workspaceId)) + intutil.CleanupWorkspace(workspaceId) + workspaceId = "" + } + }) + + Context("Workspace Creation Edge Cases", func() { + It("should create a workspace with a very long name", func() { + longName := intutil.NewWorkspaceName("very-long-workspace-name") + By("Creating a workspace with a long name") + output := intutil.RunCommand( + "create", "workspace", longName, + "-t", teamId, + "-p", intutil.DefaultPlanId, + "--timeout", intutil.DefaultCreateTimeout, + ) + log.Printf("Create workspace with long name output: %s\n", output) + + if output != "" && !strings.Contains(output, "error") { + Expect(output).To(ContainSubstring(intutil.WorkspaceCreatedOutput)) + workspaceId = intutil.ExtractWorkspaceId(output) + } + }) + + It("should handle creation timeout gracefully", func() { + By("Creating a workspace with very short timeout") + output, exitCode := intutil.RunCommandWithExitCode( + "create", "workspace", workspaceName, + "-t", teamId, + "-p", intutil.DefaultPlanId, + "--timeout", "1s", + ) + log.Printf("Create with short timeout output: %s (exit code: %d)\n", output, exitCode) + + if exitCode != 0 { + Expect(output).To(Or( + ContainSubstring("timeout"), + ContainSubstring("timed out"), + )) + } else if strings.Contains(output, intutil.WorkspaceCreatedOutput) { + workspaceId = intutil.ExtractWorkspaceId(output) + } + }) + }) + + Context("Exec Command Edge Cases", func() { + BeforeEach(func() { + By("Creating a workspace") + workspaceId = intutil.CreateTestWorkspace(teamId, workspaceName) + }) + + It("should execute commands with multiple arguments", func() { + By("Executing a command with multiple arguments") + output := intutil.RunCommand( + "exec", + "-w", workspaceId, + "--", + "sh", "-c", "echo test1 && echo test2", + ) + log.Printf("Exec with multiple args output: %s\n", output) + Expect(output).To(ContainSubstring("test1")) + Expect(output).To(ContainSubstring("test2")) + }) + + It("should handle commands that output to stderr", func() { + By("Executing a command that writes to stderr") + output := intutil.RunCommand( + "exec", + "-w", workspaceId, + "--", + "sh", "-c", "echo error message >&2", + ) + log.Printf("Exec with stderr output: %s\n", output) + Expect(output).To(ContainSubstring("error message")) + }) + + It("should handle commands with exit codes", func() { + By("Executing a command that exits with non-zero code") + output, exitCode := intutil.RunCommandWithExitCode( + "exec", + "-w", workspaceId, + "--", + "sh", "-c", "exit 42", + ) + log.Printf("Exec with exit code output: %s (exit code: %d)\n", output, exitCode) + }) + + It("should execute long-running commands", func() { + By("Executing a command that takes a few seconds") + output := intutil.RunCommand( + "exec", + "-w", workspaceId, + "--", + "sh", "-c", "sleep 2 && echo completed", + ) + log.Printf("Exec long-running command output: %s\n", output) + Expect(output).To(ContainSubstring("completed")) + }) + }) + + Context("Workspace Deletion Edge Cases", func() { + It("should prevent deletion without confirmation when not forced", func() { + By("Creating a workspace") + workspaceId = intutil.CreateTestWorkspace(teamId, workspaceName) + + By("Attempting to delete without --yes flag") + output := intutil.RunCommand( + "delete", "workspace", + "-w", workspaceId, + "--yes", + ) + log.Printf("Delete with confirmation output: %s\n", output) + Expect(output).To(ContainSubstring("deleted")) + workspaceId = "" + }) + + It("should fail gracefully when deleting already deleted workspace", func() { + By("Creating and deleting a workspace") + tempWsId := intutil.CreateTestWorkspace(teamId, workspaceName) + + output := intutil.RunCommand( + "delete", "workspace", + "-w", tempWsId, + "--yes", + ) + Expect(output).To(ContainSubstring("deleted")) + + By("Attempting to delete the same workspace again") + output, exitCode := intutil.RunCommandWithExitCode( + "delete", "workspace", + "-w", tempWsId, + "--yes", + ) + log.Printf("Delete already deleted workspace output: %s (exit code: %d)\n", output, exitCode) + Expect(exitCode).NotTo(Equal(0)) + Expect(output).To(Or( + ContainSubstring("error"), + ContainSubstring("failed"), + ContainSubstring("not found"), + )) + }) + }) +}) From 62ed94f348949d72f70ff59432ee8b771559949b Mon Sep 17 00:00:00 2001 From: OliverTrautvetter <66372584+OliverTrautvetter@users.noreply.github.com> Date: Thu, 12 Mar 2026 17:35:19 +0100 Subject: [PATCH 3/4] refactor: add matrix strategy to integration test workflow - Split CI into build, test (parallel per-label matrix), and cleanup jobs - Define label list once; derive matrix and unlabeled filter dynamically - Add unique run-name with github.run_id to avoid parallel conflicts - Fetch latest hadolint and kubeconform versions at install time - Add per-label Makefile targets (test-int-workspace, test-int-list, etc.) --- .github/workflows/integration-test.yml | 100 +++++++++++++++++++++---- 1 file changed, 87 insertions(+), 13 deletions(-) diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index 846ed78..66368d8 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -1,4 +1,5 @@ name: Integration Tests +run-name: "Integration Tests #${{ github.run_id }}" on: schedule: @@ -6,9 +7,20 @@ on: workflow_call: jobs: - integration-tests: + build: runs-on: ubuntu-latest + outputs: + test-matrix: ${{ steps.labels.outputs.matrix }} + known-labels: ${{ steps.labels.outputs.known }} steps: + - name: Configure test labels + id: labels + run: | + # Add new labels here; 'unlabeled' is appended automatically for the matrix. + KNOWN="workspace,list,error-handling,log,pipeline,git,wakeup,curl,local" + echo "known=$KNOWN" >> "$GITHUB_OUTPUT" + echo "matrix=[\"$(echo "$KNOWN" | sed 's/,/","/g')\",\"unlabeled\"]" >> "$GITHUB_OUTPUT" + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Set up Go @@ -16,22 +28,84 @@ jobs: with: go-version-file: 'go.mod' - - name: Run Integration Tests - env: - CS_TOKEN: ${{ secrets.CS_TOKEN }} - CS_API: ${{ secrets.CS_API }} - CS_TEAM_ID: ${{ secrets.CS_TEAM_ID }} - run: make test-int + - name: Build CLI and test binary + run: | + make build + go test -c -o int.test ./int + + - name: Upload artifacts + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: test-artifacts + path: | + cs + int.test + retention-days: 1 + + integration-tests: + needs: build + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + label: ${{ fromJSON(needs.build.outputs.test-matrix) }} + env: + CS_TOKEN: ${{ secrets.CS_TOKEN }} + CS_API: ${{ secrets.CS_API }} + CS_TEAM_ID: ${{ secrets.CS_TEAM_ID }} + steps: + - name: Download artifacts + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: test-artifacts + + - name: Make binaries executable + run: chmod +x ./cs ./int.test + + - name: Install linting tools + if: matrix.label == 'local' + run: | + HADOLINT_VERSION=$(curl -sL https://api.github.com/repos/hadolint/hadolint/releases/latest | grep '"tag_name"' | cut -d'"' -f4) + sudo curl -sL -o /usr/local/bin/hadolint \ + "https://github.com/hadolint/hadolint/releases/download/${HADOLINT_VERSION}/hadolint-Linux-x86_64" + sudo chmod +x /usr/local/bin/hadolint + KUBECONFORM_VERSION=$(curl -sL https://api.github.com/repos/yannh/kubeconform/releases/latest | grep '"tag_name"' | cut -d'"' -f4) + curl -sL "https://github.com/yannh/kubeconform/releases/download/${KUBECONFORM_VERSION}/kubeconform-linux-amd64.tar.gz" | \ + sudo tar xz -C /usr/local/bin + + - name: Run integration tests - ${{ matrix.label }} + run: | + mkdir -p int + mv int.test int/ + cd int + if [ "${{ matrix.label }}" = "unlabeled" ]; then + echo "::warning::Running tests without a known label. If tests are found here, please add a label to the Describe block." + FILTER=$(echo "${{ needs.build.outputs.known-labels }}" | sed 's/[^,]*/!\0/g; s/,/ \&\& /g') + ./int.test -test.v -ginkgo.label-filter="$FILTER" + else + ./int.test -test.v -ginkgo.label-filter='${{ matrix.label }}' + fi + + cleanup: + needs: integration-tests + if: always() + runs-on: ubuntu-latest + env: + CS_TOKEN: ${{ secrets.CS_TOKEN }} + CS_API: ${{ secrets.CS_API }} + CS_TEAM_ID: ${{ secrets.CS_TEAM_ID }} + steps: + - name: Download artifacts + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: test-artifacts + + - name: Make binary executable + run: chmod +x ./cs - name: Cleanup Orphaned Test Resources - if: always() # Run even if tests fail - env: - CS_TOKEN: ${{ secrets.CS_TOKEN }} - CS_API: ${{ secrets.CS_API }} - CS_TEAM_ID: ${{ secrets.CS_TEAM_ID }} run: | echo "Cleaning up any orphaned test workspaces..." - # List all workspaces and delete any with test name prefixes ./cs list workspaces -t $CS_TEAM_ID | grep -E "cli-(test|git-test|pipeline-test|log-test|sync-test|open-test|setenv-test|edge-test|wakeup-test|curl-test)-" | awk '{print $2}' | while read ws_id; do if [ ! -z "$ws_id" ]; then echo "Deleting orphaned workspace: $ws_id" From 0f82c8659c3f324b06c43815995d281c284f7fa8 Mon Sep 17 00:00:00 2001 From: OliverTrautvetter <66372584+OliverTrautvetter@users.noreply.github.com> Date: Thu, 12 Mar 2026 16:38:15 +0000 Subject: [PATCH 4/4] chore(docs): Auto-update docs and licenses Signed-off-by: OliverTrautvetter <66372584+OliverTrautvetter@users.noreply.github.com> --- hack/split-pr.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/hack/split-pr.sh b/hack/split-pr.sh index 0f3c2fd..5e8a4d3 100755 --- a/hack/split-pr.sh +++ b/hack/split-pr.sh @@ -1,4 +1,7 @@ #!/bin/bash +# Copyright (c) Codesphere Inc. +# SPDX-License-Identifier: Apache-2.0 + set -euo pipefail # This script splits the current PR branch (kubernetes_integration_tests) into