diff --git a/.github/workflows/update-docs-and-licenses.yml b/.github/workflows/update-docs-and-licenses.yml index 30578b1..8cccdf3 100644 --- a/.github/workflows/update-docs-and-licenses.yml +++ b/.github/workflows/update-docs-and-licenses.yml @@ -25,7 +25,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: '1.24.2' + go-version-file: 'go.mod' - name: Update docs & licenses run: | diff --git a/cli/cmd/api_key_integration_test.go b/cli/cmd/api_key_integration_test.go index b90d100..a275092 100644 --- a/cli/cmd/api_key_integration_test.go +++ b/cli/cmd/api_key_integration_test.go @@ -9,6 +9,8 @@ package cmd_test import ( "fmt" "os" + "os/exec" + "strings" "time" . "github.com/onsi/ginkgo/v2" @@ -217,4 +219,116 @@ var _ = Describe("API Key Integration Tests", func() { Expect(err.Error()).To(ContainSubstring("invalid date format")) }) }) + + Describe("Old API Key Detection and Warning", func() { + var ( + cliPath string + ) + + BeforeEach(func() { + cliPath = "./oms-cli" + + _, err := os.Stat(cliPath) + if err != nil { + Skip("OMS CLI not found at " + cliPath + ", please build it first with 'make build-cli'") + } + }) + + Context("when using a 22-character old API key format", func() { + It("should detect the old format and attempt to upgrade", func() { + cmd := exec.Command(cliPath, "version") + cmd.Env = append(os.Environ(), + "OMS_PORTAL_API_KEY=fakeapikeywith22charsa", // 22 characters + "OMS_PORTAL_API=http://localhost:3000/api", + ) + + output, _ := cmd.CombinedOutput() + outputStr := string(output) + + Expect(outputStr).To(ContainSubstring("OMS CLI version")) + }) + }) + + Context("when using a new long-format API key", func() { + It("should not show any warning", func() { + cmd := exec.Command(cliPath, "version") + cmd.Env = append(os.Environ(), + "OMS_PORTAL_API_KEY=4hBieJRj2pWeB9qKJ9wQGE3CrcldLnLwP8fz6qutMjkf1n1", + "OMS_PORTAL_API=http://localhost:3000/api", + ) + + output, _ := cmd.CombinedOutput() + outputStr := string(output) + + Expect(outputStr).To(ContainSubstring("OMS CLI version")) + Expect(outputStr).NotTo(ContainSubstring("old API key")) + Expect(outputStr).NotTo(ContainSubstring("Failed to upgrade")) + }) + }) + + Context("when using a 22-character key with list api-keys command", func() { + It("should attempt the upgrade and handle the error gracefully", func() { + cmd := exec.Command(cliPath, "list", "api-keys") + cmd.Env = append(os.Environ(), + "OMS_PORTAL_API_KEY=fakeapikeywith22charsa", // 22 characters (old format) + "OMS_PORTAL_API=http://localhost:3000/api", + ) + + output, err := cmd.CombinedOutput() + outputStr := string(output) + + Expect(err).To(HaveOccurred()) + + hasWarning := strings.Contains(outputStr, "old API key") || + strings.Contains(outputStr, "Failed to upgrade") || + strings.Contains(outputStr, "Unauthorized") + + Expect(hasWarning).To(BeTrue(), + "Should contain warning about old key or auth failure. Got: "+outputStr) + }) + }) + + Context("when checking key length detection", func() { + It("should correctly identify 22-character old format", func() { + oldKey := "fakeapikeywith22charsa" + Expect(len(oldKey)).To(Equal(22)) + }) + + It("should correctly identify new long format", func() { + newKey := "4hBieJRj2pWeB9qKJ9wQGE3CrcldLnLwP8fz6qutMjkf1n1" + Expect(len(newKey)).NotTo(Equal(22)) + Expect(len(newKey)).To(BeNumerically(">", 22)) + }) + }) + }) + + Describe("PreRun Hook Execution", func() { + var ( + cliPath string + ) + + BeforeEach(func() { + cliPath = "./oms-cli" + + _, err := os.Stat(cliPath) + if err != nil { + Skip("OMS CLI not found at " + cliPath + ", please build it first with 'make build-cli'") + } + }) + + Context("when running any OMS command", func() { + It("should execute the PreRun hook", func() { + cmd := exec.Command(cliPath, "version") + cmd.Env = append(os.Environ(), + "OMS_PORTAL_API_KEY=valid-key-format-short", + "OMS_PORTAL_API=http://localhost:3000/api", + ) + + output, _ := cmd.CombinedOutput() + outputStr := string(output) + + Expect(outputStr).To(ContainSubstring("OMS CLI version")) + }) + }) + }) }) diff --git a/cli/cmd/cmd_suite_test.go b/cli/cmd/cmd_suite_test.go index bff0329..39c0fba 100644 --- a/cli/cmd/cmd_suite_test.go +++ b/cli/cmd/cmd_suite_test.go @@ -4,8 +4,18 @@ package cmd_test import ( + "bytes" + "encoding/json" + "io" + "net/http" + "os" "testing" + "github.com/codesphere-cloud/oms/cli/cmd" + "github.com/codesphere-cloud/oms/internal/env" + "github.com/codesphere-cloud/oms/internal/portal" + "github.com/stretchr/testify/mock" + . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) @@ -14,3 +24,97 @@ func TestCmd(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "Cmd Suite") } + +var _ = Describe("RootCmd", func() { + var ( + mockEnv *env.MockEnv + mockHttpClient *portal.MockHttpClient + ) + + BeforeEach(func() { + mockEnv = env.NewMockEnv(GinkgoT()) + mockHttpClient = portal.NewMockHttpClient(GinkgoT()) + }) + + AfterEach(func() { + mockEnv.AssertExpectations(GinkgoT()) + mockHttpClient.AssertExpectations(GinkgoT()) + }) + + Describe("PreRun hook with old API key", func() { + Context("when API key is 22 characters (old format)", func() { + It("attempts to upgrade the key via GetApiKeyByHeader", func() { + oldKey := "fakeapikeywith22charsa" // 22 characters + keyId := "test-key-id-12345" + expectedNewKey := keyId + oldKey + + Expect(os.Setenv("OMS_PORTAL_API_KEY", oldKey)).NotTo(HaveOccurred()) + Expect(os.Setenv("OMS_PORTAL_API", "http://test-portal.com/api")).NotTo(HaveOccurred()) + + mockEnv.EXPECT().GetOmsPortalApi().Return("http://test-portal.com/api") + + mockHttpClient.EXPECT().Do(mock.Anything).RunAndReturn( + func(req *http.Request) (*http.Response, error) { + Expect(req.Header.Get("X-API-Key")).To(Equal(oldKey)) + Expect(req.URL.Path).To(ContainSubstring("/key")) + + response := map[string]string{ + "keyId": keyId, + } + body, _ := json.Marshal(response) + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewReader(body)), + }, nil + }) + + portalClient := &portal.PortalClient{ + Env: mockEnv, + HttpClient: mockHttpClient, + } + + result, err := portalClient.GetApiKeyByHeader(oldKey) + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(Equal(expectedNewKey)) + + Expect(os.Unsetenv("OMS_PORTAL_API_KEY")).NotTo(HaveOccurred()) + Expect(os.Unsetenv("OMS_PORTAL_API")).NotTo(HaveOccurred()) + }) + }) + + Context("when API key is not 22 characters (new format)", func() { + It("does not attempt to upgrade the key", func() { + newKey := "new-long-api-key-format-very-long-string" + + Expect(os.Setenv("OMS_PORTAL_API_KEY", newKey)).NotTo(HaveOccurred()) + Expect(os.Setenv("OMS_PORTAL_API", "http://test-portal.com/api")).NotTo(HaveOccurred()) + + Expect(len(newKey)).NotTo(Equal(22)) + + Expect(os.Unsetenv("OMS_PORTAL_API_KEY")).NotTo(HaveOccurred()) + Expect(os.Unsetenv("OMS_PORTAL_API")).NotTo(HaveOccurred()) + }) + }) + + Context("when API key is empty", func() { + It("does not attempt to upgrade", func() { + Expect(os.Setenv("OMS_PORTAL_API_KEY", "")).NotTo(HaveOccurred()) + Expect(os.Setenv("OMS_PORTAL_API", "http://test-portal.com/api")).NotTo(HaveOccurred()) + + Expect(len(os.Getenv("OMS_PORTAL_API_KEY"))).To(Equal(0)) + + Expect(os.Unsetenv("OMS_PORTAL_API_KEY")).NotTo(HaveOccurred()) + Expect(os.Unsetenv("OMS_PORTAL_API")).NotTo(HaveOccurred()) + }) + }) + }) + + Describe("GetRootCmd", func() { + It("returns a valid root command", func() { + rootCmd := cmd.GetRootCmd() + Expect(rootCmd).NotTo(BeNil()) + Expect(rootCmd.Use).To(Equal("oms")) + Expect(rootCmd.Short).To(Equal("Codesphere Operations Management System (OMS)")) + }) + }) +}) diff --git a/cli/cmd/root.go b/cli/cmd/root.go index 2264f7d..f364ca2 100644 --- a/cli/cmd/root.go +++ b/cli/cmd/root.go @@ -4,10 +4,12 @@ package cmd import ( + "fmt" "log" "os" "github.com/codesphere-cloud/cs-go/pkg/io" + "github.com/codesphere-cloud/oms/internal/portal" "github.com/spf13/cobra" ) @@ -25,6 +27,31 @@ func GetRootCmd() *cobra.Command { This command can be used to run common tasks related to managing codesphere installations, like downloading new versions.`), + PersistentPreRun: func(cmd *cobra.Command, args []string) { + apiKey := os.Getenv("OMS_PORTAL_API_KEY") + + if len(apiKey) == 22 { + fmt.Fprintf(os.Stderr, "Warning: You used an old API key format.\n") + fmt.Fprintf(os.Stderr, "Attempting to upgrade to the new format...\n\n") + + portalClient := portal.NewPortalClient() + newApiKey, err := portalClient.GetApiKeyByHeader(apiKey) + + if err != nil { + fmt.Fprintf(os.Stderr, "Error: Failed to upgrade old API key: %v\n", err) + return + } + + if err := os.Setenv("OMS_PORTAL_API_KEY", newApiKey); err != nil { + fmt.Fprintf(os.Stderr, "Error: Failed to set environment variable: %v\n", err) + return + } + opts.OmsPortalApiKey = newApiKey + + fmt.Fprintf(os.Stderr, "Please update your environment variable:\n\n") + fmt.Fprintf(os.Stderr, " export OMS_PORTAL_API_KEY='%s'\n\n", newApiKey) + } + }, } // General commands AddVersionCmd(rootCmd) diff --git a/docs/README.md b/docs/README.md index 66c5ff6..30f9f12 100644 --- a/docs/README.md +++ b/docs/README.md @@ -27,4 +27,4 @@ like downloading new versions. * [oms-cli update](oms-cli_update.md) - Update OMS related resources * [oms-cli version](oms-cli_version.md) - Print version -###### Auto generated by spf13/cobra on 23-Oct-2025 +###### Auto generated by spf13/cobra on 27-Oct-2025 diff --git a/docs/oms-cli.md b/docs/oms-cli.md index 66c5ff6..30f9f12 100644 --- a/docs/oms-cli.md +++ b/docs/oms-cli.md @@ -27,4 +27,4 @@ like downloading new versions. * [oms-cli update](oms-cli_update.md) - Update OMS related resources * [oms-cli version](oms-cli_version.md) - Print version -###### Auto generated by spf13/cobra on 23-Oct-2025 +###### Auto generated by spf13/cobra on 27-Oct-2025 diff --git a/docs/oms-cli_beta.md b/docs/oms-cli_beta.md index 7bb12f5..47b99da 100644 --- a/docs/oms-cli_beta.md +++ b/docs/oms-cli_beta.md @@ -18,4 +18,4 @@ Be aware that that usage and behavior may change as the features are developed. * [oms-cli](oms-cli.md) - Codesphere Operations Management System (OMS) * [oms-cli beta extend](oms-cli_beta_extend.md) - Extend Codesphere ressources such as base images. -###### Auto generated by spf13/cobra on 23-Oct-2025 +###### Auto generated by spf13/cobra on 27-Oct-2025 diff --git a/docs/oms-cli_beta_extend.md b/docs/oms-cli_beta_extend.md index 386d605..6324a7a 100644 --- a/docs/oms-cli_beta_extend.md +++ b/docs/oms-cli_beta_extend.md @@ -17,4 +17,4 @@ Extend Codesphere ressources such as base images to customize them for your need * [oms-cli beta](oms-cli_beta.md) - Commands for early testing * [oms-cli beta extend baseimage](oms-cli_beta_extend_baseimage.md) - Extend Codesphere's workspace base image for customization -###### Auto generated by spf13/cobra on 23-Oct-2025 +###### Auto generated by spf13/cobra on 27-Oct-2025 diff --git a/docs/oms-cli_beta_extend_baseimage.md b/docs/oms-cli_beta_extend_baseimage.md index 20df537..32d1556 100644 --- a/docs/oms-cli_beta_extend_baseimage.md +++ b/docs/oms-cli_beta_extend_baseimage.md @@ -27,4 +27,4 @@ oms-cli beta extend baseimage [flags] * [oms-cli beta extend](oms-cli_beta_extend.md) - Extend Codesphere ressources such as base images. -###### Auto generated by spf13/cobra on 23-Oct-2025 +###### Auto generated by spf13/cobra on 27-Oct-2025 diff --git a/docs/oms-cli_download.md b/docs/oms-cli_download.md index e8e52e8..cadb099 100644 --- a/docs/oms-cli_download.md +++ b/docs/oms-cli_download.md @@ -18,4 +18,4 @@ e.g. available Codesphere packages * [oms-cli](oms-cli.md) - Codesphere Operations Management System (OMS) * [oms-cli download package](oms-cli_download_package.md) - Download a codesphere package -###### Auto generated by spf13/cobra on 23-Oct-2025 +###### Auto generated by spf13/cobra on 27-Oct-2025 diff --git a/docs/oms-cli_download_package.md b/docs/oms-cli_download_package.md index 1792f41..af57779 100644 --- a/docs/oms-cli_download_package.md +++ b/docs/oms-cli_download_package.md @@ -36,4 +36,4 @@ $ oms-cli download package --version codesphere-v1.55.0 --file installer-lite.ta * [oms-cli download](oms-cli_download.md) - Download resources available through OMS -###### Auto generated by spf13/cobra on 23-Oct-2025 +###### Auto generated by spf13/cobra on 27-Oct-2025 diff --git a/docs/oms-cli_install.md b/docs/oms-cli_install.md index f40e5ad..384b744 100644 --- a/docs/oms-cli_install.md +++ b/docs/oms-cli_install.md @@ -17,4 +17,4 @@ Coming soon: Install Codesphere and other components like Ceph and PostgreSQL. * [oms-cli](oms-cli.md) - Codesphere Operations Management System (OMS) * [oms-cli install codesphere](oms-cli_install_codesphere.md) - Install a Codesphere instance -###### Auto generated by spf13/cobra on 23-Oct-2025 +###### Auto generated by spf13/cobra on 27-Oct-2025 diff --git a/docs/oms-cli_install_codesphere.md b/docs/oms-cli_install_codesphere.md index 030d3dc..0b6aa06 100644 --- a/docs/oms-cli_install_codesphere.md +++ b/docs/oms-cli_install_codesphere.md @@ -26,4 +26,4 @@ oms-cli install codesphere [flags] * [oms-cli install](oms-cli_install.md) - Coming soon: Install Codesphere and other components -###### Auto generated by spf13/cobra on 23-Oct-2025 +###### Auto generated by spf13/cobra on 27-Oct-2025 diff --git a/docs/oms-cli_licenses.md b/docs/oms-cli_licenses.md index 4ee0753..434ad9e 100644 --- a/docs/oms-cli_licenses.md +++ b/docs/oms-cli_licenses.md @@ -20,4 +20,4 @@ oms-cli licenses [flags] * [oms-cli](oms-cli.md) - Codesphere Operations Management System (OMS) -###### Auto generated by spf13/cobra on 23-Oct-2025 +###### Auto generated by spf13/cobra on 27-Oct-2025 diff --git a/docs/oms-cli_list.md b/docs/oms-cli_list.md index fdcda20..b2644c5 100644 --- a/docs/oms-cli_list.md +++ b/docs/oms-cli_list.md @@ -19,4 +19,4 @@ eg. available Codesphere packages * [oms-cli list api-keys](oms-cli_list_api-keys.md) - List API keys * [oms-cli list packages](oms-cli_list_packages.md) - List available packages -###### Auto generated by spf13/cobra on 23-Oct-2025 +###### Auto generated by spf13/cobra on 27-Oct-2025 diff --git a/docs/oms-cli_list_api-keys.md b/docs/oms-cli_list_api-keys.md index df732f3..1236ab9 100644 --- a/docs/oms-cli_list_api-keys.md +++ b/docs/oms-cli_list_api-keys.md @@ -20,4 +20,4 @@ oms-cli list api-keys [flags] * [oms-cli list](oms-cli_list.md) - List resources available through OMS -###### Auto generated by spf13/cobra on 23-Oct-2025 +###### Auto generated by spf13/cobra on 27-Oct-2025 diff --git a/docs/oms-cli_list_packages.md b/docs/oms-cli_list_packages.md index 2084552..69a753a 100644 --- a/docs/oms-cli_list_packages.md +++ b/docs/oms-cli_list_packages.md @@ -20,4 +20,4 @@ oms-cli list packages [flags] * [oms-cli list](oms-cli_list.md) - List resources available through OMS -###### Auto generated by spf13/cobra on 23-Oct-2025 +###### Auto generated by spf13/cobra on 27-Oct-2025 diff --git a/docs/oms-cli_register.md b/docs/oms-cli_register.md index 787764e..678dd44 100644 --- a/docs/oms-cli_register.md +++ b/docs/oms-cli_register.md @@ -24,4 +24,4 @@ oms-cli register [flags] * [oms-cli](oms-cli.md) - Codesphere Operations Management System (OMS) -###### Auto generated by spf13/cobra on 23-Oct-2025 +###### Auto generated by spf13/cobra on 27-Oct-2025 diff --git a/docs/oms-cli_revoke.md b/docs/oms-cli_revoke.md index 25ef5d1..3e4c68b 100644 --- a/docs/oms-cli_revoke.md +++ b/docs/oms-cli_revoke.md @@ -18,4 +18,4 @@ eg. api keys. * [oms-cli](oms-cli.md) - Codesphere Operations Management System (OMS) * [oms-cli revoke api-key](oms-cli_revoke_api-key.md) - Revoke an API key -###### Auto generated by spf13/cobra on 23-Oct-2025 +###### Auto generated by spf13/cobra on 27-Oct-2025 diff --git a/docs/oms-cli_revoke_api-key.md b/docs/oms-cli_revoke_api-key.md index 90a3183..9933d38 100644 --- a/docs/oms-cli_revoke_api-key.md +++ b/docs/oms-cli_revoke_api-key.md @@ -21,4 +21,4 @@ oms-cli revoke api-key [flags] * [oms-cli revoke](oms-cli_revoke.md) - Revoke resources available through OMS -###### Auto generated by spf13/cobra on 23-Oct-2025 +###### Auto generated by spf13/cobra on 27-Oct-2025 diff --git a/docs/oms-cli_update.md b/docs/oms-cli_update.md index 9fa9de7..0304654 100644 --- a/docs/oms-cli_update.md +++ b/docs/oms-cli_update.md @@ -23,4 +23,4 @@ oms-cli update [flags] * [oms-cli update oms](oms-cli_update_oms.md) - Update the OMS CLI * [oms-cli update package](oms-cli_update_package.md) - Download a codesphere package -###### Auto generated by spf13/cobra on 23-Oct-2025 +###### Auto generated by spf13/cobra on 27-Oct-2025 diff --git a/docs/oms-cli_update_api-key.md b/docs/oms-cli_update_api-key.md index d7f3510..5798dd2 100644 --- a/docs/oms-cli_update_api-key.md +++ b/docs/oms-cli_update_api-key.md @@ -22,4 +22,4 @@ oms-cli update api-key [flags] * [oms-cli update](oms-cli_update.md) - Update OMS related resources -###### Auto generated by spf13/cobra on 23-Oct-2025 +###### Auto generated by spf13/cobra on 27-Oct-2025 diff --git a/docs/oms-cli_update_oms.md b/docs/oms-cli_update_oms.md index 875562a..fd0a9c8 100644 --- a/docs/oms-cli_update_oms.md +++ b/docs/oms-cli_update_oms.md @@ -20,4 +20,4 @@ oms-cli update oms [flags] * [oms-cli update](oms-cli_update.md) - Update OMS related resources -###### Auto generated by spf13/cobra on 23-Oct-2025 +###### Auto generated by spf13/cobra on 27-Oct-2025 diff --git a/docs/oms-cli_update_package.md b/docs/oms-cli_update_package.md index d608c2d..87258d4 100644 --- a/docs/oms-cli_update_package.md +++ b/docs/oms-cli_update_package.md @@ -36,4 +36,4 @@ $ oms-cli download package --version codesphere-v1.55.0 --file installer-lite.ta * [oms-cli update](oms-cli_update.md) - Update OMS related resources -###### Auto generated by spf13/cobra on 23-Oct-2025 +###### Auto generated by spf13/cobra on 27-Oct-2025 diff --git a/docs/oms-cli_version.md b/docs/oms-cli_version.md index 96cd471..85238b9 100644 --- a/docs/oms-cli_version.md +++ b/docs/oms-cli_version.md @@ -20,4 +20,4 @@ oms-cli version [flags] * [oms-cli](oms-cli.md) - Codesphere Operations Management System (OMS) -###### Auto generated by spf13/cobra on 23-Oct-2025 +###### Auto generated by spf13/cobra on 27-Oct-2025 diff --git a/internal/portal/http.go b/internal/portal/http.go index ddd7497..799078a 100644 --- a/internal/portal/http.go +++ b/internal/portal/http.go @@ -27,6 +27,7 @@ type Portal interface { RevokeAPIKey(key string) error UpdateAPIKey(key string, expiresAt time.Time) error ListAPIKeys() ([]ApiKey, error) + GetApiKeyByHeader(oldKey string) (string, error) } type PortalClient struct { @@ -297,3 +298,39 @@ func (c *PortalClient) ListAPIKeys() ([]ApiKey, error) { return keys, nil } + +func (c *PortalClient) GetApiKeyByHeader(oldKey string) (string, error) { + requestBody := bytes.NewBuffer([]byte{}) + url, err := url.JoinPath(c.Env.GetOmsPortalApi(), "/key") + if err != nil { + return "", fmt.Errorf("failed to generate URL: %w", err) + } + + req, err := http.NewRequest(http.MethodGet, url, requestBody) + if err != nil { + return "", fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("X-API-Key", oldKey) + + resp, err := c.HttpClient.Do(req) + if err != nil { + return "", fmt.Errorf("failed to send request: %w", err) + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode >= 300 { + respBody, _ := io.ReadAll(resp.Body) + return "", fmt.Errorf("unexpected response status: %d - %s, %s", resp.StatusCode, http.StatusText(resp.StatusCode), string(respBody)) + } + + var result struct { + KeyID string `json:"keyId"` + } + + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return "", fmt.Errorf("failed to decode response: %w", err) + } + + return result.KeyID + oldKey, nil +} diff --git a/internal/portal/http_test.go b/internal/portal/http_test.go index 9aff801..a14d547 100644 --- a/internal/portal/http_test.go +++ b/internal/portal/http_test.go @@ -360,4 +360,179 @@ var _ = Describe("PortalClient", func() { }) }) }) + + Describe("GetApiKeyByHeader", func() { + var ( + oldApiKey string + keyId string + expectedNewKey string + responseBody []byte + requestHeader string + ) + + BeforeEach(func() { + oldApiKey = "old-key-format-1234" + keyId = "test-key-id-12345" + expectedNewKey = keyId + oldApiKey + requestHeader = "" + + mockEnv.EXPECT().GetOmsPortalApi().Return(apiUrl) + }) + + Context("when the request succeeds", func() { + BeforeEach(func() { + response := map[string]string{ + "keyId": keyId, + } + responseBody, _ = json.Marshal(response) + + mockHttpClient.EXPECT().Do(mock.Anything).RunAndReturn( + func(req *http.Request) (*http.Response, error) { + getUrl = *req.URL + requestHeader = req.Header.Get("X-API-Key") + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewReader(responseBody)), + }, nil + }) + }) + + It("returns the new API key", func() { + result, err := client.GetApiKeyByHeader(oldApiKey) + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(Equal(expectedNewKey)) + Expect(getUrl.String()).To(Equal("fake-portal.com/key")) + Expect(requestHeader).To(Equal(oldApiKey)) + }) + + It("sends the old API key in the X-API-Key header", func() { + _, err := client.GetApiKeyByHeader(oldApiKey) + Expect(err).NotTo(HaveOccurred()) + Expect(requestHeader).To(Equal(oldApiKey)) + }) + }) + + Context("when the HTTP request fails", func() { + BeforeEach(func() { + mockHttpClient.EXPECT().Do(mock.Anything).RunAndReturn( + func(req *http.Request) (*http.Response, error) { + return nil, errors.New("network error") + }) + }) + + It("returns an error", func() { + _, err := client.GetApiKeyByHeader(oldApiKey) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to send request")) + Expect(err.Error()).To(ContainSubstring("network error")) + }) + }) + + Context("when the server returns an error status code", func() { + BeforeEach(func() { + errorResponse := "Unauthorized" + responseBody = []byte(errorResponse) + + mockHttpClient.EXPECT().Do(mock.Anything).RunAndReturn( + func(req *http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: http.StatusUnauthorized, + Body: io.NopCloser(bytes.NewReader(responseBody)), + }, nil + }) + }) + + It("returns an error with the status code", func() { + _, err := client.GetApiKeyByHeader(oldApiKey) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("unexpected response status: 401")) + Expect(err.Error()).To(ContainSubstring("Unauthorized")) + }) + }) + + Context("when the response body is not valid JSON", func() { + BeforeEach(func() { + responseBody = []byte("invalid json {") + + mockHttpClient.EXPECT().Do(mock.Anything).RunAndReturn( + func(req *http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewReader(responseBody)), + }, nil + }) + }) + + It("returns an error", func() { + _, err := client.GetApiKeyByHeader(oldApiKey) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to decode response")) + }) + }) + + Context("when the response is missing the keyId field", func() { + BeforeEach(func() { + response := map[string]string{ + "someOtherField": "value", + } + responseBody, _ = json.Marshal(response) + + mockHttpClient.EXPECT().Do(mock.Anything).RunAndReturn( + func(req *http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewReader(responseBody)), + }, nil + }) + }) + + It("returns only the old key", func() { + result, err := client.GetApiKeyByHeader(oldApiKey) + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(Equal(oldApiKey)) + }) + }) + + Context("when the server returns 404", func() { + BeforeEach(func() { + errorResponse := "Not Found" + responseBody = []byte(errorResponse) + + mockHttpClient.EXPECT().Do(mock.Anything).RunAndReturn( + func(req *http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: http.StatusNotFound, + Body: io.NopCloser(bytes.NewReader(responseBody)), + }, nil + }) + }) + + It("returns an error with the status code", func() { + _, err := client.GetApiKeyByHeader(oldApiKey) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("unexpected response status: 404")) + }) + }) + + Context("when the server returns 500", func() { + BeforeEach(func() { + errorResponse := "Internal Server Error" + responseBody = []byte(errorResponse) + + mockHttpClient.EXPECT().Do(mock.Anything).RunAndReturn( + func(req *http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: http.StatusInternalServerError, + Body: io.NopCloser(bytes.NewReader(responseBody)), + }, nil + }) + }) + + It("returns an error with the status code", func() { + _, err := client.GetApiKeyByHeader(oldApiKey) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("unexpected response status: 500")) + }) + }) + }) }) diff --git a/internal/portal/mocks.go b/internal/portal/mocks.go index 857676d..ce12d3f 100644 --- a/internal/portal/mocks.go +++ b/internal/portal/mocks.go @@ -86,6 +86,60 @@ func (_c *MockPortal_DownloadBuildArtifact_Call) RunAndReturn(run func(product P return _c } +// GetApiKeyByHeader provides a mock function for the type MockPortal +func (_mock *MockPortal) GetApiKeyByHeader(oldKey string) (string, error) { + ret := _mock.Called(oldKey) + + if len(ret) == 0 { + panic("no return value specified for GetApiKeyByHeader") + } + + var r0 string + var r1 error + if returnFunc, ok := ret.Get(0).(func(string) (string, error)); ok { + return returnFunc(oldKey) + } + if returnFunc, ok := ret.Get(0).(func(string) string); ok { + r0 = returnFunc(oldKey) + } else { + r0 = ret.Get(0).(string) + } + if returnFunc, ok := ret.Get(1).(func(string) error); ok { + r1 = returnFunc(oldKey) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MockPortal_GetApiKeyByHeader_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetApiKeyByHeader' +type MockPortal_GetApiKeyByHeader_Call struct { + *mock.Call +} + +// GetApiKeyByHeader is a helper method to define mock.On call +// - oldKey +func (_e *MockPortal_Expecter) GetApiKeyByHeader(oldKey interface{}) *MockPortal_GetApiKeyByHeader_Call { + return &MockPortal_GetApiKeyByHeader_Call{Call: _e.mock.On("GetApiKeyByHeader", oldKey)} +} + +func (_c *MockPortal_GetApiKeyByHeader_Call) Run(run func(oldKey string)) *MockPortal_GetApiKeyByHeader_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *MockPortal_GetApiKeyByHeader_Call) Return(s string, err error) *MockPortal_GetApiKeyByHeader_Call { + _c.Call.Return(s, err) + return _c +} + +func (_c *MockPortal_GetApiKeyByHeader_Call) RunAndReturn(run func(oldKey string) (string, error)) *MockPortal_GetApiKeyByHeader_Call { + _c.Call.Return(run) + return _c +} + // GetBuild provides a mock function for the type MockPortal func (_mock *MockPortal) GetBuild(product Product, version string, hash string) (Build, error) { ret := _mock.Called(product, version, hash)