diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index 846ed78..b8fd54f 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -16,12 +16,82 @@ jobs: with: go-version-file: 'go.mod' - - name: Run Integration Tests + - name: Integration Tests - workspace env: CS_TOKEN: ${{ secrets.CS_TOKEN }} CS_API: ${{ secrets.CS_API }} CS_TEAM_ID: ${{ secrets.CS_TEAM_ID }} - run: make test-int + run: make test-int-workspace + + - name: Integration Tests - list + if: always() + env: + CS_TOKEN: ${{ secrets.CS_TOKEN }} + CS_API: ${{ secrets.CS_API }} + CS_TEAM_ID: ${{ secrets.CS_TEAM_ID }} + run: make test-int-list + + - name: Integration Tests - error-handling + if: always() + env: + CS_TOKEN: ${{ secrets.CS_TOKEN }} + CS_API: ${{ secrets.CS_API }} + CS_TEAM_ID: ${{ secrets.CS_TEAM_ID }} + run: make test-int-error-handling + + - name: Integration Tests - log + if: always() + env: + CS_TOKEN: ${{ secrets.CS_TOKEN }} + CS_API: ${{ secrets.CS_API }} + CS_TEAM_ID: ${{ secrets.CS_TEAM_ID }} + run: make test-int-log + + - name: Integration Tests - pipeline + if: always() + env: + CS_TOKEN: ${{ secrets.CS_TOKEN }} + CS_API: ${{ secrets.CS_API }} + CS_TEAM_ID: ${{ secrets.CS_TEAM_ID }} + run: make test-int-pipeline + + - name: Integration Tests - git + if: always() + env: + CS_TOKEN: ${{ secrets.CS_TOKEN }} + CS_API: ${{ secrets.CS_API }} + CS_TEAM_ID: ${{ secrets.CS_TEAM_ID }} + run: make test-int-git + + - name: Integration Tests - wakeup + if: always() + env: + CS_TOKEN: ${{ secrets.CS_TOKEN }} + CS_API: ${{ secrets.CS_API }} + CS_TEAM_ID: ${{ secrets.CS_TEAM_ID }} + run: make test-int-wakeup + + - name: Integration Tests - curl + if: always() + env: + CS_TOKEN: ${{ secrets.CS_TOKEN }} + CS_API: ${{ secrets.CS_API }} + CS_TEAM_ID: ${{ secrets.CS_TEAM_ID }} + run: make test-int-curl + + - name: Integration Tests - local + if: always() + run: make test-int-local + + - name: Integration Tests - unlabeled (label missing!) + if: always() + env: + CS_TOKEN: ${{ secrets.CS_TOKEN }} + CS_API: ${{ secrets.CS_API }} + CS_TEAM_ID: ${{ secrets.CS_TEAM_ID }} + run: | + echo "::warning::Running tests without a known label. If tests are found here, please add a label to the Describe block." + make test-int-unlabeled - name: Cleanup Orphaned Test Resources if: always() # Run even if tests fail diff --git a/Makefile b/Makefile index 5948a15..8a6cfbb 100644 --- a/Makefile +++ b/Makefile @@ -16,6 +16,38 @@ test: test-int: build go test ./int/... -count=1 -v +# new integration tests should be added here with a label, e.g. 'workspace' or 'list', +# the label must be added to the unlabeled test filter in the 'test-int-unlabeled' target below +test-int-workspace: build + go test ./int/... -count=1 -v -ginkgo.label-filter='workspace' + +test-int-list: build + go test ./int/... -count=1 -v -ginkgo.label-filter='list' + +test-int-error-handling: build + go test ./int/... -count=1 -v -ginkgo.label-filter='error-handling' + +test-int-log: build + go test ./int/... -count=1 -v -ginkgo.label-filter='log' + +test-int-pipeline: build + go test ./int/... -count=1 -v -ginkgo.label-filter='pipeline' + +test-int-git: build + go test ./int/... -count=1 -v -ginkgo.label-filter='git' + +test-int-wakeup: build + go test ./int/... -count=1 -v -ginkgo.label-filter='wakeup' + +test-int-curl: build + go test ./int/... -count=1 -v -ginkgo.label-filter='curl' + +test-int-local: build + go test ./int/... -count=1 -v -ginkgo.label-filter='local' + +test-int-unlabeled: build + go test ./int/... -count=1 -v -ginkgo.label-filter='!local && !workspace && !list && !error-handling && !log && !pipeline && !git && !wakeup && !curl' + generate: install-build-deps go generate ./... 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 0 { + result = append(result, trimmed) + } + } + return result +} + +// unmarshalDeployment unmarshals YAML into a Kubernetes Deployment and validates its structure. +func unmarshalDeployment(yamlContent []byte) *apps.Deployment { + GinkgoHelper() + deployment := &apps.Deployment{} + err := sigsyaml.Unmarshal(yamlContent, deployment) + Expect(err).NotTo(HaveOccurred(), "Failed to unmarshal Deployment YAML") + Expect(deployment.Kind).To(Equal("Deployment"), "Expected kind Deployment") + Expect(deployment.APIVersion).To(Equal("apps/v1"), "Expected apiVersion apps/v1") + Expect(deployment.Name).NotTo(BeEmpty(), "Deployment name should not be empty") + return deployment +} + +// unmarshalService unmarshals YAML into a Kubernetes Service and validates its structure. +func unmarshalService(yamlContent []byte) *core.Service { + GinkgoHelper() + service := &core.Service{} + err := sigsyaml.Unmarshal(yamlContent, service) + Expect(err).NotTo(HaveOccurred(), "Failed to unmarshal Service YAML") + Expect(service.Kind).To(Equal("Service"), "Expected kind Service") + Expect(service.APIVersion).To(Equal("v1"), "Expected apiVersion v1") + Expect(service.Name).NotTo(BeEmpty(), "Service name should not be empty") + return service +} + +// unmarshalIngress unmarshals YAML into a Kubernetes Ingress and validates its structure. +func unmarshalIngress(yamlContent []byte) *networking.Ingress { + GinkgoHelper() + ingress := &networking.Ingress{} + err := sigsyaml.Unmarshal(yamlContent, ingress) + Expect(err).NotTo(HaveOccurred(), "Failed to unmarshal Ingress YAML") + Expect(ingress.Kind).To(Equal("Ingress"), "Expected kind Ingress") + Expect(ingress.APIVersion).To(Equal("networking.k8s.io/v1"), "Expected apiVersion networking.k8s.io/v1") + Expect(ingress.Name).NotTo(BeEmpty(), "Ingress name should not be empty") + return ingress +} + +// validateServiceFile validates a K8s service YAML file containing a Deployment and Service. +func validateServiceFile(content []byte) (*apps.Deployment, *core.Service) { + GinkgoHelper() + docs := splitYAMLDocuments(content) + Expect(docs).To(HaveLen(2), "Service file should contain a Deployment and a Service document") + return unmarshalDeployment(docs[0]), unmarshalService(docs[1]) +} + +// validateDockerfile performs basic structural validation of a Dockerfile. +func validateDockerfile(content string) { + GinkgoHelper() + lines := strings.Split(strings.TrimSpace(content), "\n") + Expect(len(lines)).To(BeNumerically(">", 0), "Dockerfile should not be empty") + + validInstructions := map[string]bool{ + "FROM": true, "RUN": true, "CMD": true, "LABEL": true, + "EXPOSE": true, "ENV": true, "ADD": true, "COPY": true, + "ENTRYPOINT": true, "VOLUME": true, "USER": true, + "WORKDIR": true, "ARG": true, "ONBUILD": true, + "STOPSIGNAL": true, "HEALTHCHECK": true, "SHELL": true, + } + + hasFrom := false + inContinuation := false + for _, line := range lines { + trimmed := strings.TrimSpace(line) + if trimmed == "" || strings.HasPrefix(trimmed, "#") { + inContinuation = false + continue + } + if inContinuation { + inContinuation = strings.HasSuffix(trimmed, "\\") + continue + } + + parts := strings.Fields(trimmed) + instruction := strings.ToUpper(parts[0]) + Expect(validInstructions).To(HaveKey(instruction), + fmt.Sprintf("Invalid Dockerfile instruction: '%s' in line: '%s'", parts[0], trimmed)) + if instruction == "FROM" { + hasFrom = true + } + inContinuation = strings.HasSuffix(trimmed, "\\") + } + Expect(hasFrom).To(BeTrue(), "Dockerfile must contain a FROM instruction") +} + +// validateShellScript validates that a shell script has a proper shebang and is non-empty. +func validateShellScript(content string) { + GinkgoHelper() + Expect(content).NotTo(BeEmpty(), "Shell script should not be empty") + Expect(content).To(HavePrefix("#!/bin/bash"), "Shell script should start with #!/bin/bash shebang") +} + +// validateDockerCompose validates docker-compose.yml content is valid YAML with required structure. +func validateDockerCompose(content []byte) { + GinkgoHelper() + var compose map[string]interface{} + err := sigsyaml.Unmarshal(content, &compose) + Expect(err).NotTo(HaveOccurred(), "docker-compose.yml should be valid YAML") + Expect(compose).To(HaveKey("services"), "docker-compose.yml should have a 'services' key") +} + +var _ = Describe("Kubernetes Export Integration Tests", Label("local"), func() { + var ( + tempDir string + ) + + BeforeEach(func() { + var err error + tempDir, err = os.MkdirTemp("", "cs-export-test-") + Expect(err).NotTo(HaveOccurred()) + }) + + AfterEach(func() { + if tempDir != "" { + Expect(os.RemoveAll(tempDir)).NotTo(HaveOccurred()) + } + }) + + Context("Generate Docker Command", func() { + It("should generate Dockerfiles and docker-compose from flask-demo ci.yml", func() { + By("Creating ci.yml in temp directory") + ciYmlPath := filepath.Join(tempDir, "ci.yml") + err := os.WriteFile(ciYmlPath, []byte(flaskDemoCiYml), 0644) + Expect(err).NotTo(HaveOccurred()) + + By("Running generate docker command") + output := intutil.RunCommand( + "generate", "docker", + "--reporoot", tempDir, + "-b", "ubuntu:latest", + "-i", "ci.yml", + "-o", "export", + ) + fmt.Printf("Generate docker output: %s\n", output) + + Expect(output).To(ContainSubstring("docker artifacts created")) + Expect(output).To(ContainSubstring("docker compose up")) + + By("Verifying frontend-service Dockerfile was created") + frontendDockerfile := filepath.Join(tempDir, "export", "frontend-service", "Dockerfile") + Expect(frontendDockerfile).To(BeAnExistingFile()) + content, err := os.ReadFile(frontendDockerfile) + Expect(err).NotTo(HaveOccurred()) + Expect(string(content)).To(ContainSubstring("FROM ubuntu:latest")) + Expect(string(content)).To(ContainSubstring("pip install")) + validateDockerfile(string(content)) + + By("Verifying frontend-service entrypoint was created") + frontendEntrypoint := filepath.Join(tempDir, "export", "frontend-service", "entrypoint.sh") + Expect(frontendEntrypoint).To(BeAnExistingFile()) + content, err = os.ReadFile(frontendEntrypoint) + Expect(err).NotTo(HaveOccurred()) + Expect(string(content)).To(ContainSubstring("python app.py")) + validateShellScript(string(content)) + + By("Verifying backend-service Dockerfile was created") + backendDockerfile := filepath.Join(tempDir, "export", "backend-service", "Dockerfile") + Expect(backendDockerfile).To(BeAnExistingFile()) + content, err = os.ReadFile(backendDockerfile) + Expect(err).NotTo(HaveOccurred()) + validateDockerfile(string(content)) + + By("Verifying backend-service entrypoint was created") + backendEntrypoint := filepath.Join(tempDir, "export", "backend-service", "entrypoint.sh") + Expect(backendEntrypoint).To(BeAnExistingFile()) + content, err = os.ReadFile(backendEntrypoint) + Expect(err).NotTo(HaveOccurred()) + Expect(string(content)).To(ContainSubstring("python backend.py")) + validateShellScript(string(content)) + + By("Verifying docker-compose.yml was created") + dockerComposePath := filepath.Join(tempDir, "export", "docker-compose.yml") + Expect(dockerComposePath).To(BeAnExistingFile()) + content, err = os.ReadFile(dockerComposePath) + Expect(err).NotTo(HaveOccurred()) + Expect(string(content)).To(ContainSubstring("frontend-service")) + Expect(string(content)).To(ContainSubstring("backend-service")) + validateDockerCompose(content) + + By("Verifying nginx config was created") + nginxConfigPath := filepath.Join(tempDir, "export", "nginx.conf") + Expect(nginxConfigPath).To(BeAnExistingFile()) + }) + + It("should generate Docker artifacts with different base image", func() { + By("Creating ci.yml in temp directory") + ciYmlPath := filepath.Join(tempDir, "ci.yml") + err := os.WriteFile(ciYmlPath, []byte(simpleCiYml), 0644) + Expect(err).NotTo(HaveOccurred()) + + By("Running generate docker with alpine base image") + output := intutil.RunCommand( + "generate", "docker", + "--reporoot", tempDir, + "-b", "alpine:latest", + "-i", "ci.yml", + "-o", "export", + ) + fmt.Printf("Generate docker output: %s\n", output) + + Expect(output).To(ContainSubstring("docker artifacts created")) + + By("Verifying Dockerfile uses alpine base image") + dockerfile := filepath.Join(tempDir, "export", "web", "Dockerfile") + Expect(dockerfile).To(BeAnExistingFile()) + content, err := os.ReadFile(dockerfile) + Expect(err).NotTo(HaveOccurred()) + Expect(string(content)).To(ContainSubstring("FROM alpine:latest")) + validateDockerfile(string(content)) + }) + + It("should fail when baseimage is not provided", func() { + By("Creating ci.yml in temp directory") + ciYmlPath := filepath.Join(tempDir, "ci.yml") + err := os.WriteFile(ciYmlPath, []byte(simpleCiYml), 0644) + Expect(err).NotTo(HaveOccurred()) + + By("Running generate docker without baseimage") + output, exitCode := intutil.RunCommandWithExitCode( + "generate", "docker", + "--reporoot", tempDir, + "-i", "ci.yml", + ) + fmt.Printf("Generate docker without baseimage output: %s (exit code: %d)\n", output, exitCode) + + Expect(exitCode).NotTo(Equal(0)) + Expect(output).To(ContainSubstring("baseimage is required")) + }) + + It("should fail when ci.yml does not exist", func() { + By("Running generate docker without ci.yml") + output, exitCode := intutil.RunCommandWithExitCode( + "generate", "docker", + "--reporoot", tempDir, + "-b", "ubuntu:latest", + "-i", "nonexistent.yml", + ) + fmt.Printf("Generate docker with nonexistent file output: %s (exit code: %d)\n", output, exitCode) + + Expect(exitCode).NotTo(Equal(0)) + }) + + It("should fail with invalid YAML content", func() { + By("Creating invalid ci.yml") + ciYmlPath := filepath.Join(tempDir, "ci.yml") + err := os.WriteFile(ciYmlPath, []byte(invalidYaml), 0644) + Expect(err).NotTo(HaveOccurred()) + + By("Running generate docker with invalid YAML") + output, exitCode := intutil.RunCommandWithExitCode( + "generate", "docker", + "--reporoot", tempDir, + "-b", "ubuntu:latest", + "-i", "ci.yml", + "-o", "export", + ) + fmt.Printf("Generate docker with invalid YAML output: %s (exit code: %d)\n", output, exitCode) + + Expect(exitCode).NotTo(Equal(0)) + }) + + It("should fail with ci.yml with no services", func() { + By("Creating ci.yml with empty run section") + ciYmlPath := filepath.Join(tempDir, "ci.yml") + err := os.WriteFile(ciYmlPath, []byte(emptyCiYml), 0644) + Expect(err).NotTo(HaveOccurred()) + + By("Running generate docker with empty services") + output, exitCode := intutil.RunCommandWithExitCode( + "generate", "docker", + "--reporoot", tempDir, + "-b", "ubuntu:latest", + "-i", "ci.yml", + "-o", "export", + ) + fmt.Printf("Generate docker with empty services output: %s (exit code: %d)\n", output, exitCode) + + Expect(exitCode).NotTo(Equal(0)) + Expect(output).To(ContainSubstring("at least one service is required")) + }) + }) + + Context("Generate Kubernetes Command", func() { + BeforeEach(func() { + By("Creating ci.yml and generating docker artifacts first") + ciYmlPath := filepath.Join(tempDir, "ci.yml") + err := os.WriteFile(ciYmlPath, []byte(flaskDemoCiYml), 0644) + Expect(err).NotTo(HaveOccurred()) + + output := intutil.RunCommand( + "generate", "docker", + "--reporoot", tempDir, + "-b", "ubuntu:latest", + "-i", "ci.yml", + "-o", "export", + ) + Expect(output).To(ContainSubstring("docker artifacts created")) + }) + + It("should generate Kubernetes artifacts with registry and namespace", func() { + By("Running generate kubernetes command") + output := intutil.RunCommand( + "generate", "kubernetes", + "--reporoot", tempDir, + "-r", "ghcr.io/codesphere-cloud/flask-demo", + "-p", "cs-demo", + "-i", "ci.yml", + "-o", "export", + "-n", "flask-demo", + "--hostname", "flask-demo.local", + ) + fmt.Printf("Generate kubernetes output: %s\n", output) + + Expect(output).To(ContainSubstring("Kubernetes artifacts export successful")) + Expect(output).To(ContainSubstring("kubectl apply")) + + By("Verifying kubernetes directory was created") + kubernetesDir := filepath.Join(tempDir, "export", "kubernetes") + info, err := os.Stat(kubernetesDir) + Expect(err).NotTo(HaveOccurred()) + Expect(info.IsDir()).To(BeTrue()) + + By("Verifying frontend-service deployment was created and is valid") + frontendServicePath := filepath.Join(kubernetesDir, "service-frontend-service.yml") + Expect(frontendServicePath).To(BeAnExistingFile()) + content, err := os.ReadFile(frontendServicePath) + Expect(err).NotTo(HaveOccurred()) + frontDep, frontSvc := validateServiceFile(content) + Expect(frontDep.Namespace).To(Equal("flask-demo")) + Expect(frontDep.Spec.Template.Spec.Containers).To(HaveLen(1)) + Expect(frontDep.Spec.Template.Spec.Containers[0].Image).To(Equal("ghcr.io/codesphere-cloud/flask-demo/cs-demo-frontend-service:latest")) + Expect(frontSvc.Namespace).To(Equal("flask-demo")) + + By("Verifying backend-service deployment was created and is valid") + backendServicePath := filepath.Join(kubernetesDir, "service-backend-service.yml") + Expect(backendServicePath).To(BeAnExistingFile()) + content, err = os.ReadFile(backendServicePath) + Expect(err).NotTo(HaveOccurred()) + backDep, backSvc := validateServiceFile(content) + Expect(backDep.Namespace).To(Equal("flask-demo")) + Expect(backDep.Spec.Template.Spec.Containers).To(HaveLen(1)) + Expect(backDep.Spec.Template.Spec.Containers[0].Image).To(ContainSubstring("cs-demo-backend-service:latest")) + Expect(backSvc.Namespace).To(Equal("flask-demo")) + + By("Verifying ingress was created and is valid") + ingressPath := filepath.Join(kubernetesDir, "ingress.yml") + Expect(ingressPath).To(BeAnExistingFile()) + content, err = os.ReadFile(ingressPath) + Expect(err).NotTo(HaveOccurred()) + ingress := unmarshalIngress(content) + Expect(ingress.Namespace).To(Equal("flask-demo")) + Expect(ingress.Spec.Rules).To(HaveLen(1)) + Expect(ingress.Spec.Rules[0].Host).To(Equal("flask-demo.local")) + Expect(*ingress.Spec.IngressClassName).To(Equal("nginx")) + }) + + It("should generate Kubernetes artifacts with custom ingress class", func() { + By("Running generate kubernetes with custom ingress class") + output := intutil.RunCommand( + "generate", "kubernetes", + "--reporoot", tempDir, + "-r", "docker.io/myorg", + "-i", "ci.yml", + "-o", "export", + "-n", "production", + "--hostname", "myapp.example.com", + "--ingressClass", "traefik", + ) + fmt.Printf("Generate kubernetes with traefik output: %s\n", output) + + Expect(output).To(ContainSubstring("Kubernetes artifacts export successful")) + + By("Verifying ingress uses traefik class") + ingressPath := filepath.Join(tempDir, "export", "kubernetes", "ingress.yml") + content, err := os.ReadFile(ingressPath) + Expect(err).NotTo(HaveOccurred()) + ingress := unmarshalIngress(content) + Expect(*ingress.Spec.IngressClassName).To(Equal("traefik")) + }) + + It("should generate Kubernetes artifacts with pull secret", func() { + By("Running generate kubernetes with pull secret") + output := intutil.RunCommand( + "generate", "kubernetes", + "--reporoot", tempDir, + "-r", "private-registry.io/myorg", + "-i", "ci.yml", + "-o", "export", + "-n", "staging", + "--hostname", "staging.myapp.com", + "--pullsecret", "my-registry-secret", + ) + fmt.Printf("Generate kubernetes with pull secret output: %s\n", output) + + Expect(output).To(ContainSubstring("Kubernetes artifacts export successful")) + + By("Verifying deployment includes pull secret") + frontendServicePath := filepath.Join(tempDir, "export", "kubernetes", "service-frontend-service.yml") + content, err := os.ReadFile(frontendServicePath) + Expect(err).NotTo(HaveOccurred()) + dep, _ := validateServiceFile(content) + Expect(dep.Spec.Template.Spec.ImagePullSecrets).To(HaveLen(1)) + Expect(dep.Spec.Template.Spec.ImagePullSecrets[0].Name).To(Equal("my-registry-secret")) + }) + + It("should fail when registry is not provided", func() { + By("Running generate kubernetes without registry") + output, exitCode := intutil.RunCommandWithExitCode( + "generate", "kubernetes", + "--reporoot", tempDir, + "-i", "ci.yml", + "-o", "export", + ) + fmt.Printf("Generate kubernetes without registry output: %s (exit code: %d)\n", output, exitCode) + + Expect(exitCode).NotTo(Equal(0)) + Expect(output).To(ContainSubstring("registry is required")) + }) + }) + + Context("Generate Images Command", func() { + BeforeEach(func() { + By("Creating ci.yml and generating docker artifacts first") + ciYmlPath := filepath.Join(tempDir, "ci.yml") + err := os.WriteFile(ciYmlPath, []byte(simpleCiYml), 0644) + Expect(err).NotTo(HaveOccurred()) + + output := intutil.RunCommand( + "generate", "docker", + "--reporoot", tempDir, + "-b", "ubuntu:latest", + "-i", "ci.yml", + "-o", "export", + ) + Expect(output).To(ContainSubstring("docker artifacts created")) + }) + + It("should fail when registry is not provided for generate images", func() { + By("Running generate images without registry") + output, exitCode := intutil.RunCommandWithExitCode( + "generate", "images", + "--reporoot", tempDir, + "-i", "ci.yml", + "-o", "export", + ) + fmt.Printf("Generate images without registry output: %s (exit code: %d)\n", output, exitCode) + + Expect(exitCode).NotTo(Equal(0)) + Expect(output).To(ContainSubstring("registry is required")) + }) + }) + + Context("Full Export Workflow", func() { + It("should complete the full export workflow from ci.yml to Kubernetes artifacts", func() { + By("Step 1: Creating ci.yml with multi-service application") + ciYmlPath := filepath.Join(tempDir, "ci.yml") + err := os.WriteFile(ciYmlPath, []byte(flaskDemoCiYml), 0644) + Expect(err).NotTo(HaveOccurred()) + + By("Step 2: Generate Docker artifacts") + dockerOutput := intutil.RunCommand( + "generate", "docker", + "--reporoot", tempDir, + "-b", "ubuntu:latest", + "-i", "ci.yml", + "-o", "export", + ) + fmt.Printf("Docker generation output: %s\n", dockerOutput) + Expect(dockerOutput).To(ContainSubstring("docker artifacts created")) + + // Verify Docker artifacts + Expect(filepath.Join(tempDir, "export", "frontend-service", "Dockerfile")).To(BeAnExistingFile()) + Expect(filepath.Join(tempDir, "export", "backend-service", "Dockerfile")).To(BeAnExistingFile()) + Expect(filepath.Join(tempDir, "export", "docker-compose.yml")).To(BeAnExistingFile()) + + By("Step 3: Generate Kubernetes artifacts") + k8sOutput := intutil.RunCommand( + "generate", "kubernetes", + "--reporoot", tempDir, + "-r", "ghcr.io/codesphere-cloud/flask-demo", + "-p", "cs-demo", + "-i", "ci.yml", + "-o", "export", + "-n", "flask-demo-ns", + "--hostname", "colima-cluster", + ) + fmt.Printf("Kubernetes generation output: %s\n", k8sOutput) + Expect(k8sOutput).To(ContainSubstring("Kubernetes artifacts export successful")) + + By("Step 4: Verify all expected files exist") + expectedFiles := []string{ + "export/frontend-service/Dockerfile", + "export/frontend-service/entrypoint.sh", + "export/backend-service/Dockerfile", + "export/backend-service/entrypoint.sh", + "export/docker-compose.yml", + "export/nginx.conf", + "export/Dockerfile.nginx", + "export/kubernetes/service-frontend-service.yml", + "export/kubernetes/service-backend-service.yml", + "export/kubernetes/ingress.yml", + } + + for _, file := range expectedFiles { + fullPath := filepath.Join(tempDir, file) + Expect(fullPath).To(BeAnExistingFile(), fmt.Sprintf("Expected file %s to exist", file)) + } + + By("Step 5: Validate generated Dockerfiles and shell scripts") + for _, svc := range []string{"frontend-service", "backend-service"} { + df, err := os.ReadFile(filepath.Join(tempDir, "export", svc, "Dockerfile")) + Expect(err).NotTo(HaveOccurred()) + validateDockerfile(string(df)) + + ep, err := os.ReadFile(filepath.Join(tempDir, "export", svc, "entrypoint.sh")) + Expect(err).NotTo(HaveOccurred()) + validateShellScript(string(ep)) + } + dc, err := os.ReadFile(filepath.Join(tempDir, "export", "docker-compose.yml")) + Expect(err).NotTo(HaveOccurred()) + validateDockerCompose(dc) + + By("Step 6: Verify Kubernetes manifests by unmarshalling into typed structs") + kubernetesDir := filepath.Join(tempDir, "export", "kubernetes") + + // Validate ingress + ingressContent, err := os.ReadFile(filepath.Join(kubernetesDir, "ingress.yml")) + Expect(err).NotTo(HaveOccurred()) + ingress := unmarshalIngress(ingressContent) + Expect(ingress.Spec.Rules).To(HaveLen(1)) + Expect(ingress.Spec.Rules[0].Host).To(Equal("colima-cluster")) + // Verify all paths are present in the ingress + ingressPaths := ingress.Spec.Rules[0].HTTP.Paths + pathStrings := make([]string, len(ingressPaths)) + for i, p := range ingressPaths { + pathStrings[i] = p.Path + } + Expect(pathStrings).To(ContainElements("/", "/api")) + + // Validate frontend service + frontendContent, err := os.ReadFile(filepath.Join(kubernetesDir, "service-frontend-service.yml")) + Expect(err).NotTo(HaveOccurred()) + frontDep, _ := validateServiceFile(frontendContent) + Expect(frontDep.Spec.Template.Spec.Containers[0].Image).To(Equal("ghcr.io/codesphere-cloud/flask-demo/cs-demo-frontend-service:latest")) + + // Validate backend service + backendContent, err := os.ReadFile(filepath.Join(kubernetesDir, "service-backend-service.yml")) + Expect(err).NotTo(HaveOccurred()) + backDep, _ := validateServiceFile(backendContent) + Expect(backDep.Spec.Template.Spec.Containers[0].Image).To(Equal("ghcr.io/codesphere-cloud/flask-demo/cs-demo-backend-service:latest")) + }) + + It("should handle different ci.yml profiles", func() { + By("Creating multiple ci.yml profiles") + // Dev profile + devCiYml := strings.Replace(simpleCiYml, "npm start", "npm run dev", 1) + err := os.WriteFile(filepath.Join(tempDir, "ci.dev.yml"), []byte(devCiYml), 0644) + Expect(err).NotTo(HaveOccurred()) + + // Prod profile + prodCiYml := strings.Replace(simpleCiYml, "npm start", "npm run prod", 1) + err = os.WriteFile(filepath.Join(tempDir, "ci.prod.yml"), []byte(prodCiYml), 0644) + Expect(err).NotTo(HaveOccurred()) + + By("Generating Docker artifacts for dev profile") + devDockerOutput := intutil.RunCommand( + "generate", "docker", + "--reporoot", tempDir, + "-b", "node:18", + "-i", "ci.dev.yml", + "-o", "export-dev", + ) + Expect(devDockerOutput).To(ContainSubstring("docker artifacts created")) + + By("Generating Docker artifacts for prod profile") + prodDockerOutput := intutil.RunCommand( + "generate", "docker", + "--reporoot", tempDir, + "-b", "node:18-alpine", + "-i", "ci.prod.yml", + "-o", "export-prod", + ) + Expect(prodDockerOutput).To(ContainSubstring("docker artifacts created")) + + By("Verifying dev and prod have different configurations") + devEntrypoint, err := os.ReadFile(filepath.Join(tempDir, "export-dev", "web", "entrypoint.sh")) + Expect(err).NotTo(HaveOccurred()) + Expect(string(devEntrypoint)).To(ContainSubstring("npm run dev")) + validateShellScript(string(devEntrypoint)) + + prodEntrypoint, err := os.ReadFile(filepath.Join(tempDir, "export-prod", "web", "entrypoint.sh")) + Expect(err).NotTo(HaveOccurred()) + Expect(string(prodEntrypoint)).To(ContainSubstring("npm run prod")) + validateShellScript(string(prodEntrypoint)) + + devDockerfile, err := os.ReadFile(filepath.Join(tempDir, "export-dev", "web", "Dockerfile")) + Expect(err).NotTo(HaveOccurred()) + Expect(string(devDockerfile)).To(ContainSubstring("FROM node:18")) + validateDockerfile(string(devDockerfile)) + + prodDockerfile, err := os.ReadFile(filepath.Join(tempDir, "export-prod", "web", "Dockerfile")) + Expect(err).NotTo(HaveOccurred()) + Expect(string(prodDockerfile)).To(ContainSubstring("FROM node:18-alpine")) + validateDockerfile(string(prodDockerfile)) + }) + }) + + Context("Legacy ci.yml Format Support", func() { + It("should handle legacy ci.yml with path directly in network", func() { + By("Creating legacy format ci.yml") + ciYmlPath := filepath.Join(tempDir, "ci.yml") + err := os.WriteFile(ciYmlPath, []byte(legacyCiYml), 0644) + Expect(err).NotTo(HaveOccurred()) + + By("Generating Docker artifacts") + dockerOutput := intutil.RunCommand( + "generate", "docker", + "--reporoot", tempDir, + "-b", "ubuntu:latest", + "-i", "ci.yml", + "-o", "export", + ) + fmt.Printf("Legacy Docker generation output: %s\n", dockerOutput) + Expect(dockerOutput).To(ContainSubstring("docker artifacts created")) + + By("Generating Kubernetes artifacts") + k8sOutput := intutil.RunCommand( + "generate", "kubernetes", + "--reporoot", tempDir, + "-r", "docker.io/myorg", + "-i", "ci.yml", + "-o", "export", + "-n", "legacy-app", + "--hostname", "legacy.local", + ) + fmt.Printf("Legacy Kubernetes generation output: %s\n", k8sOutput) + Expect(k8sOutput).To(ContainSubstring("Kubernetes artifacts export successful")) + + By("Verifying artifacts were created and are valid") + appDockerfile := filepath.Join(tempDir, "export", "app", "Dockerfile") + Expect(appDockerfile).To(BeAnExistingFile()) + dfContent, err := os.ReadFile(appDockerfile) + Expect(err).NotTo(HaveOccurred()) + validateDockerfile(string(dfContent)) + + serviceFilePath := filepath.Join(tempDir, "export", "kubernetes", "service-app.yml") + Expect(serviceFilePath).To(BeAnExistingFile()) + serviceContent, err := os.ReadFile(serviceFilePath) + Expect(err).NotTo(HaveOccurred()) + validateServiceFile(serviceContent) + + ingressFilePath := filepath.Join(tempDir, "export", "kubernetes", "ingress.yml") + Expect(ingressFilePath).To(BeAnExistingFile()) + ingressContent, err := os.ReadFile(ingressFilePath) + Expect(err).NotTo(HaveOccurred()) + unmarshalIngress(ingressContent) + }) + }) + + Context("Environment Variables in Docker Artifacts", func() { + It("should include environment variables in generated artifacts", func() { + By("Creating ci.yml") + ciYmlPath := filepath.Join(tempDir, "ci.yml") + err := os.WriteFile(ciYmlPath, []byte(simpleCiYml), 0644) + Expect(err).NotTo(HaveOccurred()) + + By("Generating Docker artifacts with environment variables") + output := intutil.RunCommand( + "generate", "docker", + "--reporoot", tempDir, + "-b", "node:18", + "-i", "ci.yml", + "-o", "export", + "-e", "NODE_ENV=production", + "-e", "API_URL=https://api.example.com", + ) + fmt.Printf("Docker generation with envs output: %s\n", output) + Expect(output).To(ContainSubstring("docker artifacts created")) + + By("Verifying docker-compose contains environment variables") + dockerCompose, err := os.ReadFile(filepath.Join(tempDir, "export", "docker-compose.yml")) + Expect(err).NotTo(HaveOccurred()) + content := string(dockerCompose) + Expect(content).To(ContainSubstring("NODE_ENV")) + Expect(content).To(ContainSubstring("API_URL")) + validateDockerCompose(dockerCompose) + }) + }) + + Context("Force Overwrite Behavior", func() { + It("should overwrite existing files when --force is specified", func() { + By("Creating ci.yml") + ciYmlPath := filepath.Join(tempDir, "ci.yml") + err := os.WriteFile(ciYmlPath, []byte(simpleCiYml), 0644) + Expect(err).NotTo(HaveOccurred()) + + By("First generation") + output := intutil.RunCommand( + "generate", "docker", + "--reporoot", tempDir, + "-b", "ubuntu:latest", + "-i", "ci.yml", + "-o", "export", + ) + Expect(output).To(ContainSubstring("docker artifacts created")) + + By("Second generation with --force") + output = intutil.RunCommand( + "generate", "docker", + "--reporoot", tempDir, + "-b", "alpine:latest", + "-i", "ci.yml", + "-o", "export", + "--force", + ) + Expect(output).To(ContainSubstring("docker artifacts created")) + + By("Verifying files were overwritten with new base image") + dockerfile, err := os.ReadFile(filepath.Join(tempDir, "export", "web", "Dockerfile")) + Expect(err).NotTo(HaveOccurred()) + Expect(string(dockerfile)).To(ContainSubstring("FROM alpine:latest")) + validateDockerfile(string(dockerfile)) + }) + }) + + Context("Generate Command Help", func() { + It("should display help for generate docker command", func() { + output := intutil.RunCommand("generate", "docker", "--help") + fmt.Printf("Generate docker help: %s\n", output) + + Expect(output).To(ContainSubstring("generated artifacts")) + Expect(output).To(ContainSubstring("-b, --baseimage")) + Expect(output).To(ContainSubstring("-i, --input")) + Expect(output).To(ContainSubstring("-o, --output")) + }) + + It("should display help for generate kubernetes command", func() { + output := intutil.RunCommand("generate", "kubernetes", "--help") + fmt.Printf("Generate kubernetes help: %s\n", output) + + Expect(output).To(ContainSubstring("generated artifacts")) + Expect(output).To(ContainSubstring("-r, --registry")) + Expect(output).To(ContainSubstring("-p, --imagePrefix")) + Expect(output).To(ContainSubstring("-n, --namespace")) + Expect(output).To(ContainSubstring("--hostname")) + Expect(output).To(ContainSubstring("--pullsecret")) + Expect(output).To(ContainSubstring("--ingressClass")) + }) + + It("should display help for generate images command", func() { + output := intutil.RunCommand("generate", "images", "--help") + fmt.Printf("Generate images help: %s\n", output) + + Expect(output).To(ContainSubstring("generated images will be pushed")) + Expect(output).To(ContainSubstring("-r, --registry")) + Expect(output).To(ContainSubstring("-p, --imagePrefix")) + }) + }) +}) diff --git a/int/git_test.go b/int/git_test.go new file mode 100644 index 0000000..9bbca27 --- /dev/null +++ b/int/git_test.go @@ -0,0 +1,61 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + +package int_test + +import ( + "fmt" + "log" + "time" + + 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 = 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()) + }) + }) +}) 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..9b846d8 --- /dev/null +++ b/int/log_test.go @@ -0,0 +1,62 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + +package int_test + +import ( + "fmt" + "log" + "time" + + 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 = 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))) + }) + }) +}) 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..ea7ff6f --- /dev/null +++ b/int/pipeline_test.go @@ -0,0 +1,61 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + +package int_test + +import ( + "fmt" + "log" + "time" + + 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 = 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()) + }) + }) +}) diff --git a/int/util/test_helpers.go b/int/util/test_helpers.go index 5c40c11..dd678fd 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") 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..4be418c --- /dev/null +++ b/int/wakeup_test.go @@ -0,0 +1,188 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + +package int_test + +import ( + "fmt" + "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 = 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")) + }) + }) +}) diff --git a/int/workspace_test.go b/int/workspace_test.go new file mode 100644 index 0000000..5849231 --- /dev/null +++ b/int/workspace_test.go @@ -0,0 +1,271 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + +package int_test + +import ( + "fmt" + "log" + "os" + "strings" + "time" + + 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 = 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", Label("workspace"), func() { + var ( + teamId string + workspaceName string + workspaceId string + ) + + BeforeEach(func() { + teamId, _ = intutil.FailIfMissingEnvVars() + 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"), + )) + }) + }) +}) 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()) }) }) })