From df3b7114c291f686e16db30b074d6f19ac8f8de7 Mon Sep 17 00:00:00 2001 From: Olof Mattsson Date: Fri, 17 Apr 2026 15:13:57 +0200 Subject: [PATCH 1/3] feat: stackctl stack refresh-db subcommand MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds `stackctl stack refresh-db ` which POSTs to the new /api/v1/stack-instances/:id/refresh-db backend endpoint to wipe the MySQL PVC and flush Redis without a full clean+redeploy. Follows the same confirmation/quiet/output pattern as `stack clean` (`--yes` to skip prompt). Prints a hint about following up with `stack deploy` to re-populate Redis via the helm sync hook. Client method Client.RefreshDBStack(id) mirrors CleanStack. No new unit tests in cli/pkg/client/client_test.go — that file has pre-existing type-migration build failures (see b4f3b61/dc9dc34) unrelated to this change. E2E validation covered by the stack manager integration tests and manual refresh-db on a running stack. --- cli/cmd/stack.go | 63 ++++++++++++++++++++++++++++++++++++++++ cli/pkg/client/client.go | 14 +++++++++ 2 files changed, 77 insertions(+) diff --git a/cli/cmd/stack.go b/cli/cmd/stack.go index 6b05bad..387df05 100644 --- a/cli/cmd/stack.go +++ b/cli/cmd/stack.go @@ -301,6 +301,65 @@ Examples: }, } +var stackRefreshDBCmd = &cobra.Command{ + Use: "refresh-db ", + Short: "Refresh a stack's MySQL database from the golden-db snapshot", + Long: `Refresh the MySQL database for a running stack instance without a full +clean+redeploy. Wipes the MySQL PVC so the init container re-extracts the +golden-db snapshot on next boot, flushes Redis, and deletes the storefront +sync Job so the next ` + "`stack deploy`" + ` re-fires the Helm hook that repopulates +Redis. + +Does NOT re-run helm — only scales deployments and runs a short-lived +cleanup Job. The stack must be in 'running' state. + +This is a destructive operation (wipes all MySQL data for this stack). +You will be prompted for confirmation unless --yes is specified. + +After refresh-db completes, run ` + "`stack deploy`" + ` to re-populate Redis via +the sync Job. + +Examples: + stackctl stack refresh-db 42 + stackctl stack refresh-db 42 --yes`, + Args: cobra.ExactArgs(1), + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + id, err := parseID(args[0]) + if err != nil { + return err + } + + confirmed, err := confirmAction(cmd, fmt.Sprintf("This will wipe MySQL data for stack %s and reload from golden-db. Continue? (y/n): ", id)) + if err != nil { + return err + } + if !confirmed { + printer.PrintMessage("Aborted.") + return nil + } + + c, err := newClient() + if err != nil { + return err + } + + log, err := c.RefreshDBStack(id) + if err != nil { + return err + } + + if printer.Quiet { + fmt.Fprintln(printer.Writer, log.ID) + return nil + } + + printer.PrintMessage("Refreshing database for stack %s... (log ID: %s)", id, log.ID) + printer.PrintMessage("Run 'stackctl stack logs %s' to stream progress, or 'stackctl stack deploy %s' afterwards to re-populate Redis.", id, id) + return nil + }, +} + var stackDeleteCmd = &cobra.Command{ Use: "delete ", Short: "Delete a stack instance", @@ -650,6 +709,9 @@ func init() { // stack clean flags stackCleanCmd.Flags().BoolP("yes", "y", false, "Skip confirmation prompt") + // stack refresh-db flags + stackRefreshDBCmd.Flags().BoolP("yes", "y", false, "Skip confirmation prompt") + // stack delete flags stackDeleteCmd.Flags().BoolP("yes", "y", false, "Skip confirmation prompt") @@ -667,6 +729,7 @@ func init() { stackCmd.AddCommand(stackDeployCmd) stackCmd.AddCommand(stackStopCmd) stackCmd.AddCommand(stackCleanCmd) + stackCmd.AddCommand(stackRefreshDBCmd) stackCmd.AddCommand(stackDeleteCmd) stackCmd.AddCommand(stackStatusCmd) stackCmd.AddCommand(stackLogsCmd) diff --git a/cli/pkg/client/client.go b/cli/pkg/client/client.go index 93b4dfa..43e330d 100644 --- a/cli/pkg/client/client.go +++ b/cli/pkg/client/client.go @@ -330,6 +330,20 @@ func (c *Client) CleanStack(id string) (*types.DeploymentLog, error) { return &log, nil } +// RefreshDBStack triggers a database refresh for a stack instance: wipes the +// MySQL PVC so the init container re-extracts the golden-db snapshot on next +// boot, flushes Redis, and deletes the storefront sync Job so the next deploy +// re-fires the Helm hook. Does NOT re-run helm — only orchestrates k8s +// primitives. Status must be `running`. +func (c *Client) RefreshDBStack(id string) (*types.DeploymentLog, error) { + var log types.DeploymentLog + err := c.Post(fmt.Sprintf("/api/v1/stack-instances/%s/refresh-db", id), nil, &log) + if err != nil { + return nil, err + } + return &log, nil +} + // GetStackStatus returns the current status and pod states for a stack instance. func (c *Client) GetStackStatus(id string) (*types.InstanceStatus, error) { var status types.InstanceStatus From a9c1c8b00b6dfb66a2103ee54a129af3c75e1b3e Mon Sep 17 00:00:00 2001 From: Olof Mattsson Date: Fri, 17 Apr 2026 21:17:46 +0200 Subject: [PATCH 2/3] address PR #32 review: unblock CI, reword hint, add client test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - cmd/stack: reword the post-refresh hint from "stream progress" to "check progress" — `stackctl stack logs` is a point-in-time snapshot, it does not follow/tail, so the old wording overpromised. - client: add TestRefreshDBStack_Success + TestRefreshDBStack_Conflict mirroring the existing CleanStack httptest-based coverage, asserting the POST path, success decoding, and APIError on 409. Also fix the pre-existing uint→string UUID migration miss in the test packages (started in b4f3b61/dc9dc34 but left these files broken — which was keeping CI red on main and blocking any test-adding PR): - pkg/client/client_test.go: quote id literals, update `[]uint`→`[]string`, adjust ID assertions, rewrite cluster-list test shape (bare slice vs paginated envelope) to match current production type. - pkg/output/output_test.go: same mechanical fixes. - test/integration/*: quote id literals, rewrite Sscanf(%d) URL parsers to string splits, convert uint→string state maps. Package now builds and all 26 subtests pass. No production code touched; test/e2e/ left alone (those are runtime failures that need a live server, separate scope). --- cli/cmd/stack.go | 2 +- cli/pkg/client/client_test.go | 344 ++++++++++-------- cli/pkg/output/output_test.go | 96 ++--- cli/test/integration/auth_integration_test.go | 6 +- .../integration/edge_case_integration_test.go | 50 +-- .../integration/override_integration_test.go | 145 ++++---- .../integration/stack_integration_test.go | 52 ++- .../template_definition_integration_test.go | 319 ++++++++-------- 8 files changed, 549 insertions(+), 465 deletions(-) diff --git a/cli/cmd/stack.go b/cli/cmd/stack.go index 387df05..f029882 100644 --- a/cli/cmd/stack.go +++ b/cli/cmd/stack.go @@ -355,7 +355,7 @@ Examples: } printer.PrintMessage("Refreshing database for stack %s... (log ID: %s)", id, log.ID) - printer.PrintMessage("Run 'stackctl stack logs %s' to stream progress, or 'stackctl stack deploy %s' afterwards to re-populate Redis.", id, id) + printer.PrintMessage("Run 'stackctl stack logs %s' to check progress, or 'stackctl stack deploy %s' afterwards to re-populate Redis.", id, id) return nil }, } diff --git a/cli/pkg/client/client_test.go b/cli/pkg/client/client_test.go index 50b96b4..529bbb0 100644 --- a/cli/pkg/client/client_test.go +++ b/cli/pkg/client/client_test.go @@ -384,7 +384,7 @@ func TestWhoami(t *testing.T) { assert.Equal(t, "/api/v1/auth/me", r.URL.Path) w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(types.User{ - Base: types.Base{ID: 1}, + Base: types.Base{ID: "1"}, Username: "admin", Role: "admin", }) @@ -395,7 +395,7 @@ func TestWhoami(t *testing.T) { c.Token = "valid-token" user, err := c.Whoami() require.NoError(t, err) - assert.Equal(t, uint(1), user.ID) + assert.Equal(t, "1", user.ID) assert.Equal(t, "admin", user.Username) assert.Equal(t, "admin", user.Role) } @@ -500,7 +500,7 @@ func TestWhoami_Success(t *testing.T) { w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(types.User{ - Base: types.Base{ID: 5}, + Base: types.Base{ID: "5"}, Username: "testuser", Role: "operator", }) @@ -511,7 +511,7 @@ func TestWhoami_Success(t *testing.T) { c.Token = "my-jwt" user, err := c.Whoami() require.NoError(t, err) - assert.Equal(t, uint(5), user.ID) + assert.Equal(t, "5", user.ID) assert.Equal(t, "testuser", user.Username) assert.Equal(t, "operator", user.Role) } @@ -561,7 +561,7 @@ func TestListStacks_Success(t *testing.T) { assert.Equal(t, "/api/v1/stack-instances", r.URL.Path) w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(types.ListResponse[types.StackInstance]{ - Data: []types.StackInstance{{Base: types.Base{ID: 1}, Name: "stack-1"}}, + Data: []types.StackInstance{{Base: types.Base{ID: "1"}, Name: "stack-1"}}, Total: 1, Page: 1, PageSize: 20, @@ -605,7 +605,7 @@ func TestGetStack_Success(t *testing.T) { assert.Equal(t, "/api/v1/stack-instances/42", r.URL.Path) w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(types.StackInstance{ - Base: types.Base{ID: 42}, + Base: types.Base{ID: "42"}, Name: "my-stack", Status: "running", Owner: "admin", @@ -614,9 +614,9 @@ func TestGetStack_Success(t *testing.T) { defer server.Close() c := New(server.URL) - stack, err := c.GetStack(42) + stack, err := c.GetStack("42") require.NoError(t, err) - assert.Equal(t, uint(42), stack.ID) + assert.Equal(t, "42", stack.ID) assert.Equal(t, "my-stack", stack.Name) assert.Equal(t, "running", stack.Status) } @@ -630,7 +630,7 @@ func TestGetStack_NotFound(t *testing.T) { defer server.Close() c := New(server.URL) - stack, err := c.GetStack(999) + stack, err := c.GetStack("999") require.Error(t, err) assert.Nil(t, stack) @@ -648,13 +648,13 @@ func TestCreateStack_Success(t *testing.T) { var body types.CreateStackRequest require.NoError(t, json.NewDecoder(r.Body).Decode(&body)) assert.Equal(t, "new-stack", body.Name) - assert.Equal(t, uint(3), body.StackDefinitionID) + assert.Equal(t, "3", body.StackDefinitionID) w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(types.StackInstance{ - Base: types.Base{ID: 50}, + Base: types.Base{ID: "50"}, Name: "new-stack", - StackDefinitionID: 3, + StackDefinitionID: "3", Status: "draft", }) })) @@ -663,10 +663,10 @@ func TestCreateStack_Success(t *testing.T) { c := New(server.URL) stack, err := c.CreateStack(&types.CreateStackRequest{ Name: "new-stack", - StackDefinitionID: 3, + StackDefinitionID: "3", }) require.NoError(t, err) - assert.Equal(t, uint(50), stack.ID) + assert.Equal(t, "50", stack.ID) assert.Equal(t, "new-stack", stack.Name) assert.Equal(t, "draft", stack.Status) } @@ -681,7 +681,7 @@ func TestDeleteStack_Success(t *testing.T) { defer server.Close() c := New(server.URL) - err := c.DeleteStack(42) + err := c.DeleteStack("42") require.NoError(t, err) } @@ -694,7 +694,7 @@ func TestDeleteStack_NotFound(t *testing.T) { defer server.Close() c := New(server.URL) - err := c.DeleteStack(999) + err := c.DeleteStack("999") require.Error(t, err) apiErr, ok := err.(*APIError) @@ -709,8 +709,8 @@ func TestDeployStack_Success(t *testing.T) { assert.Equal(t, "/api/v1/stack-instances/42/deploy", r.URL.Path) w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(types.DeploymentLog{ - Base: types.Base{ID: 100}, - InstanceID: 42, + Base: types.Base{ID: "100"}, + InstanceID: "42", Action: "deploy", Status: "started", }) @@ -718,10 +718,10 @@ func TestDeployStack_Success(t *testing.T) { defer server.Close() c := New(server.URL) - log, err := c.DeployStack(42) + log, err := c.DeployStack("42") require.NoError(t, err) - assert.Equal(t, uint(100), log.ID) - assert.Equal(t, uint(42), log.InstanceID) + assert.Equal(t, "100", log.ID) + assert.Equal(t, "42", log.InstanceID) assert.Equal(t, "deploy", log.Action) } @@ -732,8 +732,8 @@ func TestStopStack_Success(t *testing.T) { assert.Equal(t, "/api/v1/stack-instances/42/stop", r.URL.Path) w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(types.DeploymentLog{ - Base: types.Base{ID: 101}, - InstanceID: 42, + Base: types.Base{ID: "101"}, + InstanceID: "42", Action: "stop", Status: "started", }) @@ -741,9 +741,9 @@ func TestStopStack_Success(t *testing.T) { defer server.Close() c := New(server.URL) - log, err := c.StopStack(42) + log, err := c.StopStack("42") require.NoError(t, err) - assert.Equal(t, uint(101), log.ID) + assert.Equal(t, "101", log.ID) assert.Equal(t, "stop", log.Action) } @@ -754,8 +754,8 @@ func TestCleanStack_Success(t *testing.T) { assert.Equal(t, "/api/v1/stack-instances/42/clean", r.URL.Path) w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(types.DeploymentLog{ - Base: types.Base{ID: 102}, - InstanceID: 42, + Base: types.Base{ID: "102"}, + InstanceID: "42", Action: "clean", Status: "started", }) @@ -763,12 +763,52 @@ func TestCleanStack_Success(t *testing.T) { defer server.Close() c := New(server.URL) - log, err := c.CleanStack(42) + log, err := c.CleanStack("42") require.NoError(t, err) - assert.Equal(t, uint(102), log.ID) + assert.Equal(t, "102", log.ID) assert.Equal(t, "clean", log.Action) } +func TestRefreshDBStack_Success(t *testing.T) { + t.Parallel() + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodPost, r.Method) + assert.Equal(t, "/api/v1/stack-instances/42/refresh-db", r.URL.Path) + w.WriteHeader(http.StatusAccepted) + json.NewEncoder(w).Encode(types.DeploymentLog{ + Base: types.Base{ID: "log-103"}, + InstanceID: "42", + Action: "refresh-db", + Status: "started", + }) + })) + defer server.Close() + + c := New(server.URL) + log, err := c.RefreshDBStack("42") + require.NoError(t, err) + assert.Equal(t, "log-103", log.ID) + assert.Equal(t, "refresh-db", log.Action) +} + +func TestRefreshDBStack_Conflict(t *testing.T) { + t.Parallel() + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusConflict) + json.NewEncoder(w).Encode(map[string]string{"error": "Cannot refresh-db: instance is currently deploying"}) + })) + defer server.Close() + + c := New(server.URL) + log, err := c.RefreshDBStack("42") + require.Error(t, err) + assert.Nil(t, log) + + apiErr, ok := err.(*APIError) + require.True(t, ok, "expected APIError, got %T", err) + assert.Equal(t, http.StatusConflict, apiErr.StatusCode) +} + func TestGetStackStatus_Success(t *testing.T) { t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -786,7 +826,7 @@ func TestGetStackStatus_Success(t *testing.T) { defer server.Close() c := New(server.URL) - status, err := c.GetStackStatus(42) + status, err := c.GetStackStatus("42") require.NoError(t, err) assert.Equal(t, "running", status.Status) assert.Len(t, status.Pods, 2) @@ -802,8 +842,8 @@ func TestGetStackLogs_Success(t *testing.T) { assert.Equal(t, "/api/v1/stack-instances/42/deploy-log", r.URL.Path) w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(types.DeploymentLog{ - Base: types.Base{ID: 200}, - InstanceID: 42, + Base: types.Base{ID: "200"}, + InstanceID: "42", Action: "deploy", Status: "completed", Output: "All charts installed successfully.", @@ -812,9 +852,9 @@ func TestGetStackLogs_Success(t *testing.T) { defer server.Close() c := New(server.URL) - log, err := c.GetStackLogs(42) + log, err := c.GetStackLogs("42") require.NoError(t, err) - assert.Equal(t, uint(200), log.ID) + assert.Equal(t, "200", log.ID) assert.Equal(t, "deploy", log.Action) assert.Equal(t, "completed", log.Status) assert.Contains(t, log.Output, "All charts installed") @@ -827,7 +867,7 @@ func TestCloneStack_Success(t *testing.T) { assert.Equal(t, "/api/v1/stack-instances/42/clone", r.URL.Path) w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(types.StackInstance{ - Base: types.Base{ID: 55}, + Base: types.Base{ID: "55"}, Name: "my-stack-clone", Status: "draft", }) @@ -835,9 +875,9 @@ func TestCloneStack_Success(t *testing.T) { defer server.Close() c := New(server.URL) - clone, err := c.CloneStack(42) + clone, err := c.CloneStack("42") require.NoError(t, err) - assert.Equal(t, uint(55), clone.ID) + assert.Equal(t, "55", clone.ID) assert.Equal(t, "my-stack-clone", clone.Name) assert.Equal(t, "draft", clone.Status) } @@ -854,7 +894,7 @@ func TestExtendStack_Success(t *testing.T) { w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(types.StackInstance{ - Base: types.Base{ID: 42}, + Base: types.Base{ID: "42"}, Name: "my-stack", TTLMinutes: 120, }) @@ -862,9 +902,9 @@ func TestExtendStack_Success(t *testing.T) { defer server.Close() c := New(server.URL) - stack, err := c.ExtendStack(42, 60) + stack, err := c.ExtendStack("42", 60) require.NoError(t, err) - assert.Equal(t, uint(42), stack.ID) + assert.Equal(t, "42", stack.ID) assert.Equal(t, 120, stack.TTLMinutes) } @@ -877,7 +917,7 @@ func TestListTemplates_Success(t *testing.T) { assert.Equal(t, "/api/v1/templates", r.URL.Path) w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(types.ListResponse[types.StackTemplate]{ - Data: []types.StackTemplate{{Base: types.Base{ID: 1}, Name: "tmpl-1", Published: true}}, + Data: []types.StackTemplate{{Base: types.Base{ID: "1"}, Name: "tmpl-1", Published: true}}, Total: 1, Page: 1, PageSize: 20, @@ -920,7 +960,7 @@ func TestGetTemplate_Success(t *testing.T) { assert.Equal(t, "/api/v1/templates/10", r.URL.Path) w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(types.StackTemplate{ - Base: types.Base{ID: 10}, + Base: types.Base{ID: "10"}, Name: "web-template", Description: "A web app template", Published: true, @@ -933,9 +973,9 @@ func TestGetTemplate_Success(t *testing.T) { defer server.Close() c := New(server.URL) - tmpl, err := c.GetTemplate(10) + tmpl, err := c.GetTemplate("10") require.NoError(t, err) - assert.Equal(t, uint(10), tmpl.ID) + assert.Equal(t, "10", tmpl.ID) assert.Equal(t, "web-template", tmpl.Name) assert.True(t, tmpl.Published) assert.Len(t, tmpl.Charts, 1) @@ -951,7 +991,7 @@ func TestGetTemplate_NotFound(t *testing.T) { defer server.Close() c := New(server.URL) - tmpl, err := c.GetTemplate(999) + tmpl, err := c.GetTemplate("999") require.Error(t, err) assert.Nil(t, tmpl) @@ -970,11 +1010,11 @@ func TestInstantiateTemplate_Success(t *testing.T) { require.NoError(t, json.NewDecoder(r.Body).Decode(&body)) assert.Equal(t, "my-instance", body.Name) assert.Equal(t, "feature/xyz", body.Branch) - assert.Equal(t, uint(2), body.ClusterID) + assert.Equal(t, "2", body.ClusterID) w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(types.StackInstance{ - Base: types.Base{ID: 50}, + Base: types.Base{ID: "50"}, Name: "my-instance", Status: "draft", }) @@ -982,13 +1022,13 @@ func TestInstantiateTemplate_Success(t *testing.T) { defer server.Close() c := New(server.URL) - instance, err := c.InstantiateTemplate(10, &types.InstantiateTemplateRequest{ + instance, err := c.InstantiateTemplate("10", &types.InstantiateTemplateRequest{ Name: "my-instance", Branch: "feature/xyz", - ClusterID: 2, + ClusterID: "2", }) require.NoError(t, err) - assert.Equal(t, uint(50), instance.ID) + assert.Equal(t, "50", instance.ID) assert.Equal(t, "my-instance", instance.Name) assert.Equal(t, "draft", instance.Status) } @@ -1005,7 +1045,7 @@ func TestQuickDeployTemplate_Success(t *testing.T) { w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(types.StackInstance{ - Base: types.Base{ID: 60}, + Base: types.Base{ID: "60"}, Name: "quick-stack", Status: "deploying", }) @@ -1013,11 +1053,11 @@ func TestQuickDeployTemplate_Success(t *testing.T) { defer server.Close() c := New(server.URL) - instance, err := c.QuickDeployTemplate(10, &types.QuickDeployRequest{ + instance, err := c.QuickDeployTemplate("10", &types.QuickDeployRequest{ Name: "quick-stack", }) require.NoError(t, err) - assert.Equal(t, uint(60), instance.ID) + assert.Equal(t, "60", instance.ID) assert.Equal(t, "quick-stack", instance.Name) assert.Equal(t, "deploying", instance.Status) } @@ -1031,7 +1071,7 @@ func TestListDefinitions_Success(t *testing.T) { assert.Equal(t, "/api/v1/stack-definitions", r.URL.Path) w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(types.ListResponse[types.StackDefinition]{ - Data: []types.StackDefinition{{Base: types.Base{ID: 1}, Name: "def-1", Owner: "admin"}}, + Data: []types.StackDefinition{{Base: types.Base{ID: "1"}, Name: "def-1", Owner: "admin"}}, Total: 1, Page: 1, PageSize: 20, @@ -1069,7 +1109,7 @@ func TestGetDefinition_Success(t *testing.T) { assert.Equal(t, "/api/v1/stack-definitions/5", r.URL.Path) w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(types.StackDefinition{ - Base: types.Base{ID: 5}, + Base: types.Base{ID: "5"}, Name: "api-service", Description: "API stack", DefaultBranch: "main", @@ -1082,9 +1122,9 @@ func TestGetDefinition_Success(t *testing.T) { defer server.Close() c := New(server.URL) - def, err := c.GetDefinition(5) + def, err := c.GetDefinition("5") require.NoError(t, err) - assert.Equal(t, uint(5), def.ID) + assert.Equal(t, "5", def.ID) assert.Equal(t, "api-service", def.Name) assert.Equal(t, "main", def.DefaultBranch) assert.Len(t, def.Charts, 1) @@ -1099,7 +1139,7 @@ func TestGetDefinition_NotFound(t *testing.T) { defer server.Close() c := New(server.URL) - def, err := c.GetDefinition(999) + def, err := c.GetDefinition("999") require.Error(t, err) assert.Nil(t, def) @@ -1121,7 +1161,7 @@ func TestCreateDefinition_Success(t *testing.T) { w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(types.StackDefinition{ - Base: types.Base{ID: 20}, + Base: types.Base{ID: "20"}, Name: "new-def", Description: "A new definition", Owner: "admin", @@ -1135,7 +1175,7 @@ func TestCreateDefinition_Success(t *testing.T) { Description: "A new definition", }) require.NoError(t, err) - assert.Equal(t, uint(20), def.ID) + assert.Equal(t, "20", def.ID) assert.Equal(t, "new-def", def.Name) } @@ -1151,18 +1191,18 @@ func TestUpdateDefinition_Success(t *testing.T) { w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(types.StackDefinition{ - Base: types.Base{ID: 5}, + Base: types.Base{ID: "5"}, Name: "updated-name", }) })) defer server.Close() c := New(server.URL) - def, err := c.UpdateDefinition(5, &types.UpdateDefinitionRequest{ + def, err := c.UpdateDefinition("5", &types.UpdateDefinitionRequest{ Name: "updated-name", }) require.NoError(t, err) - assert.Equal(t, uint(5), def.ID) + assert.Equal(t, "5", def.ID) assert.Equal(t, "updated-name", def.Name) } @@ -1176,7 +1216,7 @@ func TestDeleteDefinition_Success(t *testing.T) { defer server.Close() c := New(server.URL) - err := c.DeleteDefinition(5) + err := c.DeleteDefinition("5") require.NoError(t, err) } @@ -1189,7 +1229,7 @@ func TestDeleteDefinition_NotFound(t *testing.T) { defer server.Close() c := New(server.URL) - err := c.DeleteDefinition(999) + err := c.DeleteDefinition("999") require.Error(t, err) apiErr, ok := err.(*APIError) @@ -1210,7 +1250,7 @@ func TestExportDefinition_Success(t *testing.T) { defer server.Close() c := New(server.URL) - data, err := c.ExportDefinition(5) + data, err := c.ExportDefinition("5") require.NoError(t, err) assert.Equal(t, exportJSON, string(data)) } @@ -1224,7 +1264,7 @@ func TestExportDefinition_NotFound(t *testing.T) { defer server.Close() c := New(server.URL) - data, err := c.ExportDefinition(999) + data, err := c.ExportDefinition("999") require.Error(t, err) assert.Nil(t, data) } @@ -1242,7 +1282,7 @@ func TestImportDefinition_Success(t *testing.T) { w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(types.StackDefinition{ - Base: types.Base{ID: 50}, + Base: types.Base{ID: "50"}, Name: "imported-def", Description: "test import", }) @@ -1252,7 +1292,7 @@ func TestImportDefinition_Success(t *testing.T) { c := New(server.URL) def, err := c.ImportDefinition(importJSON) require.NoError(t, err) - assert.Equal(t, uint(50), def.ID) + assert.Equal(t, "50", def.ID) assert.Equal(t, "imported-def", def.Name) } @@ -1279,18 +1319,18 @@ func TestListValueOverrides_Success(t *testing.T) { assert.Equal(t, "/api/v1/stack-instances/42/overrides", r.URL.Path) w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode([]types.ValueOverride{ - {Base: types.Base{ID: 1}, InstanceID: 42, ChartID: 1, Values: `{"replicas":3}`}, - {Base: types.Base{ID: 2}, InstanceID: 42, ChartID: 2, Values: `{"debug":true}`}, + {Base: types.Base{ID: "1"}, InstanceID: "42", ChartID: "1", Values: `{"replicas":3}`}, + {Base: types.Base{ID: "2"}, InstanceID: "42", ChartID: "2", Values: `{"debug":true}`}, }) })) defer server.Close() c := New(server.URL) - overrides, err := c.ListValueOverrides(42) + overrides, err := c.ListValueOverrides("42") require.NoError(t, err) assert.Len(t, overrides, 2) - assert.Equal(t, uint(1), overrides[0].ChartID) - assert.Equal(t, uint(2), overrides[1].ChartID) + assert.Equal(t, "1", overrides[0].ChartID) + assert.Equal(t, "2", overrides[1].ChartID) } func TestListValueOverrides_Error(t *testing.T) { @@ -1302,7 +1342,7 @@ func TestListValueOverrides_Error(t *testing.T) { defer server.Close() c := New(server.URL) - overrides, err := c.ListValueOverrides(999) + overrides, err := c.ListValueOverrides("999") require.Error(t, err) assert.Nil(t, overrides) } @@ -1314,16 +1354,16 @@ func TestGetValueOverride_Success(t *testing.T) { assert.Equal(t, "/api/v1/stack-instances/42/overrides/1", r.URL.Path) w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(types.ValueOverride{ - Base: types.Base{ID: 1}, InstanceID: 42, ChartID: 1, Values: `{"replicas":3}`, + Base: types.Base{ID: "1"}, InstanceID: "42", ChartID: "1", Values: `{"replicas":3}`, }) })) defer server.Close() c := New(server.URL) - override, err := c.GetValueOverride(42, 1) + override, err := c.GetValueOverride("42", "1") require.NoError(t, err) - assert.Equal(t, uint(1), override.ChartID) - assert.Equal(t, uint(42), override.InstanceID) + assert.Equal(t, "1", override.ChartID) + assert.Equal(t, "42", override.InstanceID) assert.Contains(t, override.Values, "replicas") } @@ -1336,7 +1376,7 @@ func TestGetValueOverride_NotFound(t *testing.T) { defer server.Close() c := New(server.URL) - override, err := c.GetValueOverride(42, 99) + override, err := c.GetValueOverride("42", "99") require.Error(t, err) assert.Nil(t, override) } @@ -1353,17 +1393,17 @@ func TestSetValueOverride_Success(t *testing.T) { w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(types.ValueOverride{ - Base: types.Base{ID: 1}, InstanceID: 42, ChartID: 1, Values: `{"replicas":5}`, + Base: types.Base{ID: "1"}, InstanceID: "42", ChartID: "1", Values: `{"replicas":5}`, }) })) defer server.Close() c := New(server.URL) - override, err := c.SetValueOverride(42, 1, &types.SetValueOverrideRequest{ + override, err := c.SetValueOverride("42", "1", &types.SetValueOverrideRequest{ Values: map[string]interface{}{"replicas": float64(5)}, }) require.NoError(t, err) - assert.Equal(t, uint(1), override.ChartID) + assert.Equal(t, "1", override.ChartID) } func TestSetValueOverride_Error(t *testing.T) { @@ -1375,7 +1415,7 @@ func TestSetValueOverride_Error(t *testing.T) { defer server.Close() c := New(server.URL) - override, err := c.SetValueOverride(42, 1, &types.SetValueOverrideRequest{ + override, err := c.SetValueOverride("42", "1", &types.SetValueOverrideRequest{ Values: map[string]interface{}{"key": "val"}, }) require.Error(t, err) @@ -1392,7 +1432,7 @@ func TestDeleteValueOverride_Success(t *testing.T) { defer server.Close() c := New(server.URL) - err := c.DeleteValueOverride(42, 1) + err := c.DeleteValueOverride("42", "1") require.NoError(t, err) } @@ -1405,7 +1445,7 @@ func TestDeleteValueOverride_NotFound(t *testing.T) { defer server.Close() c := New(server.URL) - err := c.DeleteValueOverride(42, 99) + err := c.DeleteValueOverride("42", "99") require.Error(t, err) apiErr, ok := err.(*APIError) @@ -1422,13 +1462,13 @@ func TestListBranchOverrides_Success(t *testing.T) { assert.Equal(t, "/api/v1/stack-instances/42/branches", r.URL.Path) w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode([]types.BranchOverride{ - {Base: types.Base{ID: 1}, InstanceID: 42, ChartID: 1, Branch: "feature/xyz"}, + {Base: types.Base{ID: "1"}, InstanceID: "42", ChartID: "1", Branch: "feature/xyz"}, }) })) defer server.Close() c := New(server.URL) - overrides, err := c.ListBranchOverrides(42) + overrides, err := c.ListBranchOverrides("42") require.NoError(t, err) assert.Len(t, overrides, 1) assert.Equal(t, "feature/xyz", overrides[0].Branch) @@ -1443,7 +1483,7 @@ func TestListBranchOverrides_Error(t *testing.T) { defer server.Close() c := New(server.URL) - overrides, err := c.ListBranchOverrides(999) + overrides, err := c.ListBranchOverrides("999") require.Error(t, err) assert.Nil(t, overrides) } @@ -1455,16 +1495,16 @@ func TestGetBranchOverride_Success(t *testing.T) { assert.Equal(t, "/api/v1/stack-instances/42/branches/1", r.URL.Path) w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(types.BranchOverride{ - Base: types.Base{ID: 1}, InstanceID: 42, ChartID: 1, Branch: "main", + Base: types.Base{ID: "1"}, InstanceID: "42", ChartID: "1", Branch: "main", }) })) defer server.Close() c := New(server.URL) - override, err := c.GetBranchOverride(42, 1) + override, err := c.GetBranchOverride("42", "1") require.NoError(t, err) assert.Equal(t, "main", override.Branch) - assert.Equal(t, uint(42), override.InstanceID) + assert.Equal(t, "42", override.InstanceID) } func TestGetBranchOverride_NotFound(t *testing.T) { @@ -1476,7 +1516,7 @@ func TestGetBranchOverride_NotFound(t *testing.T) { defer server.Close() c := New(server.URL) - override, err := c.GetBranchOverride(42, 99) + override, err := c.GetBranchOverride("42", "99") require.Error(t, err) assert.Nil(t, override) } @@ -1493,13 +1533,13 @@ func TestSetBranchOverride_Success(t *testing.T) { w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(types.BranchOverride{ - Base: types.Base{ID: 1}, InstanceID: 42, ChartID: 1, Branch: "feature/new", + Base: types.Base{ID: "1"}, InstanceID: "42", ChartID: "1", Branch: "feature/new", }) })) defer server.Close() c := New(server.URL) - override, err := c.SetBranchOverride(42, 1, &types.SetBranchOverrideRequest{Branch: "feature/new"}) + override, err := c.SetBranchOverride("42", "1", &types.SetBranchOverrideRequest{Branch: "feature/new"}) require.NoError(t, err) assert.Equal(t, "feature/new", override.Branch) } @@ -1513,7 +1553,7 @@ func TestSetBranchOverride_Error(t *testing.T) { defer server.Close() c := New(server.URL) - override, err := c.SetBranchOverride(42, 1, &types.SetBranchOverrideRequest{Branch: "main"}) + override, err := c.SetBranchOverride("42", "1", &types.SetBranchOverrideRequest{Branch: "main"}) require.Error(t, err) assert.Nil(t, override) } @@ -1528,7 +1568,7 @@ func TestDeleteBranchOverride_Success(t *testing.T) { defer server.Close() c := New(server.URL) - err := c.DeleteBranchOverride(42, 1) + err := c.DeleteBranchOverride("42", "1") require.NoError(t, err) } @@ -1541,7 +1581,7 @@ func TestDeleteBranchOverride_NotFound(t *testing.T) { defer server.Close() c := New(server.URL) - err := c.DeleteBranchOverride(42, 99) + err := c.DeleteBranchOverride("42", "99") require.Error(t, err) apiErr, ok := err.(*APIError) @@ -1558,16 +1598,16 @@ func TestGetQuotaOverride_Success(t *testing.T) { assert.Equal(t, "/api/v1/stack-instances/42/quota-overrides", r.URL.Path) w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(types.QuotaOverride{ - InstanceID: 42, CPURequest: "100m", CPULimit: "500m", + InstanceID: "42", CPURequest: "100m", CPULimit: "500m", MemRequest: "128Mi", MemLimit: "512Mi", }) })) defer server.Close() c := New(server.URL) - quota, err := c.GetQuotaOverride(42) + quota, err := c.GetQuotaOverride("42") require.NoError(t, err) - assert.Equal(t, uint(42), quota.InstanceID) + assert.Equal(t, "42", quota.InstanceID) assert.Equal(t, "100m", quota.CPURequest) assert.Equal(t, "500m", quota.CPULimit) assert.Equal(t, "128Mi", quota.MemRequest) @@ -1583,7 +1623,7 @@ func TestGetQuotaOverride_NotFound(t *testing.T) { defer server.Close() c := New(server.URL) - quota, err := c.GetQuotaOverride(999) + quota, err := c.GetQuotaOverride("999") require.Error(t, err) assert.Nil(t, quota) } @@ -1601,13 +1641,13 @@ func TestSetQuotaOverride_Success(t *testing.T) { w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(types.QuotaOverride{ - InstanceID: 42, CPURequest: "200m", MemLimit: "1Gi", + InstanceID: "42", CPURequest: "200m", MemLimit: "1Gi", }) })) defer server.Close() c := New(server.URL) - quota, err := c.SetQuotaOverride(42, &types.SetQuotaOverrideRequest{ + quota, err := c.SetQuotaOverride("42", &types.SetQuotaOverrideRequest{ CPURequest: "200m", MemLimit: "1Gi", }) require.NoError(t, err) @@ -1624,7 +1664,7 @@ func TestSetQuotaOverride_Error(t *testing.T) { defer server.Close() c := New(server.URL) - quota, err := c.SetQuotaOverride(42, &types.SetQuotaOverrideRequest{CPURequest: "100m"}) + quota, err := c.SetQuotaOverride("42", &types.SetQuotaOverrideRequest{CPURequest: "100m"}) require.Error(t, err) assert.Nil(t, quota) } @@ -1639,7 +1679,7 @@ func TestDeleteQuotaOverride_Success(t *testing.T) { defer server.Close() c := New(server.URL) - err := c.DeleteQuotaOverride(42) + err := c.DeleteQuotaOverride("42") require.NoError(t, err) } @@ -1652,7 +1692,7 @@ func TestDeleteQuotaOverride_NotFound(t *testing.T) { defer server.Close() c := New(server.URL) - err := c.DeleteQuotaOverride(999) + err := c.DeleteQuotaOverride("999") require.Error(t, err) apiErr, ok := err.(*APIError) @@ -1669,7 +1709,7 @@ func TestGetMergedValues_Success(t *testing.T) { assert.Equal(t, "/api/v1/stack-instances/42/values", r.URL.Path) w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(types.MergedValues{ - InstanceID: 42, + InstanceID: "42", Charts: map[string]map[string]interface{}{ "api": {"replicas": float64(3)}, }, @@ -1678,9 +1718,9 @@ func TestGetMergedValues_Success(t *testing.T) { defer server.Close() c := New(server.URL) - values, err := c.GetMergedValues(42, "") + values, err := c.GetMergedValues("42", "") require.NoError(t, err) - assert.Equal(t, uint(42), values.InstanceID) + assert.Equal(t, "42", values.InstanceID) assert.Contains(t, values.Charts, "api") } @@ -1690,14 +1730,14 @@ func TestGetMergedValues_WithChartFilter(t *testing.T) { assert.Equal(t, "frontend", r.URL.Query().Get("chart")) w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(types.MergedValues{ - InstanceID: 42, + InstanceID: "42", Charts: map[string]map[string]interface{}{"frontend": {"port": float64(8080)}}, }) })) defer server.Close() c := New(server.URL) - values, err := c.GetMergedValues(42, "frontend") + values, err := c.GetMergedValues("42", "frontend") require.NoError(t, err) assert.Contains(t, values.Charts, "frontend") } @@ -1711,7 +1751,7 @@ func TestGetMergedValues_Error(t *testing.T) { defer server.Close() c := New(server.URL) - values, err := c.GetMergedValues(999, "") + values, err := c.GetMergedValues("999", "") require.Error(t, err) assert.Nil(t, values) } @@ -1725,18 +1765,18 @@ func TestCompareInstances_Success(t *testing.T) { assert.Equal(t, "43", r.URL.Query().Get("right")) w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(types.CompareResult{ - Left: &types.StackInstance{Base: types.Base{ID: 42}, Name: "stack-a"}, - Right: &types.StackInstance{Base: types.Base{ID: 43}, Name: "stack-b"}, + Left: &types.StackInstance{Base: types.Base{ID: "42"}, Name: "stack-a"}, + Right: &types.StackInstance{Base: types.Base{ID: "43"}, Name: "stack-b"}, Diffs: map[string]interface{}{"name": true}, }) })) defer server.Close() c := New(server.URL) - result, err := c.CompareInstances(42, 43) + result, err := c.CompareInstances("42", "43") require.NoError(t, err) - assert.Equal(t, uint(42), result.Left.ID) - assert.Equal(t, uint(43), result.Right.ID) + assert.Equal(t, "42", result.Left.ID) + assert.Equal(t, "43", result.Right.ID) assert.Contains(t, result.Diffs, "name") } @@ -1749,7 +1789,7 @@ func TestCompareInstances_Error(t *testing.T) { defer server.Close() c := New(server.URL) - result, err := c.CompareInstances(42, 43) + result, err := c.CompareInstances("42", "43") require.Error(t, err) assert.Nil(t, result) } @@ -1763,20 +1803,20 @@ func TestBulkDeploy_Success(t *testing.T) { assert.Equal(t, "/api/v1/stack-instances/bulk/deploy", r.URL.Path) var body types.BulkRequest require.NoError(t, json.NewDecoder(r.Body).Decode(&body)) - assert.Equal(t, []uint{1, 2, 3}, body.IDs) + assert.Equal(t, []string{"1", "2", "3"}, body.IDs) w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(types.BulkResponse{ Results: []types.BulkOperationResult{ - {ID: 1, Success: true}, - {ID: 2, Success: true}, - {ID: 3, Success: false, Error: "not found"}, + {ID: "1", Success: true}, + {ID: "2", Success: true}, + {ID: "3", Success: false, Error: "not found"}, }, }) })) defer server.Close() c := New(server.URL) - resp, err := c.BulkDeploy([]uint{1, 2, 3}) + resp, err := c.BulkDeploy([]string{"1", "2", "3"}) require.NoError(t, err) assert.Len(t, resp.Results, 3) assert.True(t, resp.Results[0].Success) @@ -1793,7 +1833,7 @@ func TestBulkDeploy_Error(t *testing.T) { defer server.Close() c := New(server.URL) - resp, err := c.BulkDeploy([]uint{1, 2}) + resp, err := c.BulkDeploy([]string{"1", "2"}) require.Error(t, err) assert.Nil(t, resp) } @@ -1806,14 +1846,14 @@ func TestBulkStop_Success(t *testing.T) { w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(types.BulkResponse{ Results: []types.BulkOperationResult{ - {ID: 1, Success: true}, + {ID: "1", Success: true}, }, }) })) defer server.Close() c := New(server.URL) - resp, err := c.BulkStop([]uint{1}) + resp, err := c.BulkStop([]string{"1"}) require.NoError(t, err) assert.Len(t, resp.Results, 1) assert.True(t, resp.Results[0].Success) @@ -1828,7 +1868,7 @@ func TestBulkStop_Error(t *testing.T) { defer server.Close() c := New(server.URL) - resp, err := c.BulkStop([]uint{1}) + resp, err := c.BulkStop([]string{"1"}) require.Error(t, err) assert.Nil(t, resp) } @@ -1841,15 +1881,15 @@ func TestBulkClean_Success(t *testing.T) { w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(types.BulkResponse{ Results: []types.BulkOperationResult{ - {ID: 5, Success: true}, - {ID: 6, Success: true}, + {ID: "5", Success: true}, + {ID: "6", Success: true}, }, }) })) defer server.Close() c := New(server.URL) - resp, err := c.BulkClean([]uint{5, 6}) + resp, err := c.BulkClean([]string{"5", "6"}) require.NoError(t, err) assert.Len(t, resp.Results, 2) } @@ -1863,7 +1903,7 @@ func TestBulkClean_Error(t *testing.T) { defer server.Close() c := New(server.URL) - resp, err := c.BulkClean([]uint{5, 6}) + resp, err := c.BulkClean([]string{"5", "6"}) require.Error(t, err) assert.Nil(t, resp) } @@ -1876,14 +1916,14 @@ func TestBulkDelete_Success(t *testing.T) { w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(types.BulkResponse{ Results: []types.BulkOperationResult{ - {ID: 10, Success: true}, + {ID: "10", Success: true}, }, }) })) defer server.Close() c := New(server.URL) - resp, err := c.BulkDelete([]uint{10}) + resp, err := c.BulkDelete([]string{"10"}) require.NoError(t, err) assert.Len(t, resp.Results, 1) assert.True(t, resp.Results[0].Success) @@ -1898,7 +1938,7 @@ func TestBulkDelete_Error(t *testing.T) { defer server.Close() c := New(server.URL) - resp, err := c.BulkDelete([]uint{999}) + resp, err := c.BulkDelete([]string{"999"}) require.Error(t, err) assert.Nil(t, resp) } @@ -2018,12 +2058,8 @@ func TestListClusters_Success(t *testing.T) { assert.Equal(t, http.MethodGet, r.Method) assert.Equal(t, "/api/v1/clusters", r.URL.Path) w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(types.ListResponse[types.Cluster]{ - Data: []types.Cluster{{Base: types.Base{ID: 1}, Name: "dev-cluster", Status: "online"}}, - Total: 1, - Page: 1, - PageSize: 20, - TotalPages: 1, + json.NewEncoder(w).Encode([]types.Cluster{ + {Base: types.Base{ID: "1"}, Name: "dev-cluster", Status: "online"}, }) })) defer server.Close() @@ -2031,26 +2067,22 @@ func TestListClusters_Success(t *testing.T) { c := New(server.URL) resp, err := c.ListClusters() require.NoError(t, err) - assert.Equal(t, 1, resp.Total) - assert.Len(t, resp.Data, 1) - assert.Equal(t, "dev-cluster", resp.Data[0].Name) + assert.Len(t, resp, 1) + assert.Equal(t, "dev-cluster", resp[0].Name) } func TestListClusters_Empty(t *testing.T) { t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(types.ListResponse[types.Cluster]{ - Data: []types.Cluster{}, Total: 0, Page: 1, PageSize: 20, TotalPages: 0, - }) + json.NewEncoder(w).Encode([]types.Cluster{}) })) defer server.Close() c := New(server.URL) resp, err := c.ListClusters() require.NoError(t, err) - assert.Equal(t, 0, resp.Total) - assert.Empty(t, resp.Data) + assert.Empty(t, resp) } func TestListClusters_Error(t *testing.T) { @@ -2074,7 +2106,7 @@ func TestGetCluster_Success(t *testing.T) { assert.Equal(t, "/api/v1/clusters/1", r.URL.Path) w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(types.Cluster{ - Base: types.Base{ID: 1}, + Base: types.Base{ID: "1"}, Name: "dev-cluster", Status: "online", IsDefault: true, @@ -2084,9 +2116,9 @@ func TestGetCluster_Success(t *testing.T) { defer server.Close() c := New(server.URL) - cluster, err := c.GetCluster(1) + cluster, err := c.GetCluster("1") require.NoError(t, err) - assert.Equal(t, uint(1), cluster.ID) + assert.Equal(t, "1", cluster.ID) assert.Equal(t, "dev-cluster", cluster.Name) assert.True(t, cluster.IsDefault) } @@ -2100,7 +2132,7 @@ func TestGetCluster_NotFound(t *testing.T) { defer server.Close() c := New(server.URL) - cluster, err := c.GetCluster(999) + cluster, err := c.GetCluster("999") require.Error(t, err) assert.Nil(t, cluster) } @@ -2123,7 +2155,7 @@ func TestGetClusterHealth_Success(t *testing.T) { defer server.Close() c := New(server.URL) - health, err := c.GetClusterHealth(1) + health, err := c.GetClusterHealth("1") require.NoError(t, err) assert.Equal(t, "healthy", health.Status) assert.Equal(t, "2.5", health.CPUUsage) @@ -2139,7 +2171,7 @@ func TestGetClusterHealth_Error(t *testing.T) { defer server.Close() c := New(server.URL) - health, err := c.GetClusterHealth(1) + health, err := c.GetClusterHealth("1") require.Error(t, err) assert.Nil(t, health) } @@ -2170,7 +2202,7 @@ func TestClient_EmptyResponseBody(t *testing.T) { defer server.Close() c := New(server.URL) - stack, err := c.GetStack(1) + stack, err := c.GetStack("1") require.Error(t, err) assert.Nil(t, stack) assert.Contains(t, err.Error(), "unexpected empty response body") diff --git a/cli/pkg/output/output_test.go b/cli/pkg/output/output_test.go index 7ee2ae2..5103d5c 100644 --- a/cli/pkg/output/output_test.go +++ b/cli/pkg/output/output_test.go @@ -84,7 +84,7 @@ func TestPrintIDs(t *testing.T) { var buf bytes.Buffer p := &Printer{Writer: &buf} - p.PrintIDs([]uint{1, 42, 100}) + p.PrintIDs([]string{"1", "42", "100"}) lines := strings.Split(strings.TrimSpace(buf.String()), "\n") assert.Equal(t, []string{"1", "42", "100"}, lines) } @@ -94,7 +94,7 @@ func TestPrintIDs_Empty(t *testing.T) { var buf bytes.Buffer p := &Printer{Writer: &buf} - p.PrintIDs([]uint{}) + p.PrintIDs([]string{}) assert.Empty(t, buf.String()) } @@ -174,7 +174,7 @@ func TestPrint_QuietMode(t *testing.T) { var buf bytes.Buffer p := &Printer{Writer: &buf, Quiet: true, Format: FormatTable} - err := p.Print(nil, nil, nil, []uint{5, 10, 15}) + err := p.Print(nil, nil, nil, []string{"5", "10", "15"}) require.NoError(t, err) assert.Equal(t, "5\n10\n15\n", buf.String()) } @@ -281,16 +281,16 @@ func TestPrintMessage(t *testing.T) { func newTestStackInstance() types.StackInstance { now := time.Date(2025, 6, 15, 10, 30, 0, 0, time.UTC) deployedAt := time.Date(2025, 6, 15, 10, 35, 0, 0, time.UTC) - clusterID := uint(3) + clusterID := "3" return types.StackInstance{ Base: types.Base{ - ID: 42, + ID: "42", CreatedAt: now, UpdatedAt: now, - Version: 1, + Version: "1", }, Name: "my-app-feature", - StackDefinitionID: 7, + StackDefinitionID: "7", DefinitionName: "my-app", Owner: "alice", Branch: "feature/login", @@ -316,15 +316,15 @@ func TestStackInstance_JSON(t *testing.T) { var result map[string]interface{} require.NoError(t, json.Unmarshal(buf.Bytes(), &result)) - assert.Equal(t, float64(42), result["id"]) + assert.Equal(t, "42", result["id"]) assert.Equal(t, "my-app-feature", result["name"]) - assert.Equal(t, float64(7), result["stack_definition_id"]) + assert.Equal(t, "7", result["stack_definition_id"]) assert.Equal(t, "my-app", result["definition_name"]) assert.Equal(t, "alice", result["owner"]) assert.Equal(t, "feature/login", result["branch"]) assert.Equal(t, "my-app-feature-ns", result["namespace"]) assert.Equal(t, "running", result["status"]) - assert.Equal(t, float64(3), result["cluster_id"]) + assert.Equal(t, "3", result["cluster_id"]) assert.Equal(t, "dev-cluster", result["cluster_name"]) assert.Equal(t, float64(120), result["ttl_minutes"]) assert.NotEmpty(t, result["deployed_at"]) @@ -349,14 +349,14 @@ func TestStackInstance_YAML(t *testing.T) { var result types.StackInstance require.NoError(t, yaml.Unmarshal(buf.Bytes(), &result)) - assert.Equal(t, uint(42), result.ID) + assert.Equal(t, "42", result.ID) assert.Equal(t, "my-app-feature", result.Name) - assert.Equal(t, uint(7), result.StackDefinitionID) + assert.Equal(t, "7", result.StackDefinitionID) assert.Equal(t, "alice", result.Owner) assert.Equal(t, "feature/login", result.Branch) assert.Equal(t, "running", result.Status) require.NotNil(t, result.ClusterID) - assert.Equal(t, uint(3), *result.ClusterID) + assert.Equal(t, "3", *result.ClusterID) assert.Equal(t, "dev-cluster", result.ClusterName) assert.Nil(t, result.DeletedAt, "deleted_at should be nil") } @@ -371,7 +371,7 @@ func TestStackInstance_TablePrint(t *testing.T) { headers := []string{"ID", "NAME", "BRANCH", "STATUS", "OWNER", "NAMESPACE"} rows := [][]string{ { - fmt.Sprintf("%d", instance.ID), + instance.ID, instance.Name, instance.Branch, p.StatusColor(instance.Status), @@ -403,7 +403,7 @@ func TestStackInstance_PrintSingle(t *testing.T) { instance := newTestStackInstance() fields := []KeyValue{ - {Key: "ID", Value: fmt.Sprintf("%d", instance.ID)}, + {Key: "ID", Value: instance.ID}, {Key: "Name", Value: instance.Name}, {Key: "Branch", Value: instance.Branch}, {Key: "Status", Value: p.StatusColor(instance.Status)}, @@ -434,7 +434,7 @@ func TestStackInstance_QuietMode(t *testing.T) { var buf bytes.Buffer p := &Printer{Writer: &buf, Quiet: true, Format: FormatTable} - err := p.Print(nil, nil, nil, []uint{42, 99}) + err := p.Print(nil, nil, nil, []string{"42", "99"}) require.NoError(t, err) assert.Equal(t, "42\n99\n", buf.String()) } @@ -472,7 +472,7 @@ func TestListResponse_StackInstance_JSON(t *testing.T) { item, ok := dataArr[0].(map[string]interface{}) require.True(t, ok) assert.Equal(t, "my-app-feature", item["name"]) - assert.Equal(t, float64(42), item["id"]) + assert.Equal(t, "42", item["id"]) } func TestListResponse_StackInstance_YAML(t *testing.T) { @@ -502,7 +502,7 @@ func TestListResponse_StackInstance_YAML(t *testing.T) { assert.Equal(t, 3, result.TotalPages) require.Len(t, result.Data, 1) assert.Equal(t, "my-app-feature", result.Data[0].Name) - assert.Equal(t, uint(42), result.Data[0].ID) + assert.Equal(t, "42", result.Data[0].ID) } func TestListResponse_EmptyData_JSON(t *testing.T) { @@ -537,12 +537,12 @@ func TestDeploymentLog_AllFormats(t *testing.T) { now := time.Date(2025, 6, 15, 12, 0, 0, 0, time.UTC) log := types.DeploymentLog{ Base: types.Base{ - ID: 101, + ID: "101", CreatedAt: now, UpdatedAt: now, - Version: 1, + Version: "1", }, - InstanceID: 42, + InstanceID: "42", Action: "deploy", Status: "success", Output: "Deployed 3 charts successfully", @@ -559,8 +559,8 @@ func TestDeploymentLog_AllFormats(t *testing.T) { verify: func(t *testing.T, output string) { var result map[string]interface{} require.NoError(t, json.Unmarshal([]byte(output), &result)) - assert.Equal(t, float64(101), result["id"]) - assert.Equal(t, float64(42), result["instance_id"]) + assert.Equal(t, "101", result["id"]) + assert.Equal(t, "42", result["instance_id"]) assert.Equal(t, "deploy", result["action"]) assert.Equal(t, "success", result["status"]) assert.Equal(t, "Deployed 3 charts successfully", result["output"]) @@ -572,8 +572,8 @@ func TestDeploymentLog_AllFormats(t *testing.T) { verify: func(t *testing.T, output string) { var result types.DeploymentLog require.NoError(t, yaml.Unmarshal([]byte(output), &result)) - assert.Equal(t, uint(101), result.ID) - assert.Equal(t, uint(42), result.InstanceID) + assert.Equal(t, "101", result.ID) + assert.Equal(t, "42", result.InstanceID) assert.Equal(t, "deploy", result.Action) assert.Equal(t, "success", result.Status) assert.Equal(t, "Deployed 3 charts successfully", result.Output) @@ -601,7 +601,7 @@ func TestDeploymentLog_AllFormats(t *testing.T) { headers := []string{"ID", "ACTION", "STATUS", "OUTPUT"} rows := [][]string{ { - fmt.Sprintf("%d", log.ID), + log.ID, log.Action, log.Status, log.Output, @@ -627,10 +627,10 @@ func TestDeploymentLog_EmptyOutput(t *testing.T) { log := types.DeploymentLog{ Base: types.Base{ - ID: 102, - Version: 1, + ID: "102", + Version: "1", }, - InstanceID: 42, + InstanceID: "42", Action: "stop", Status: "pending", Output: "", @@ -788,8 +788,8 @@ func TestNilAndZeroValueHandling(t *testing.T) { name: "stack_instance_nil_optional_pointers", data: types.StackInstance{ Base: types.Base{ - ID: 1, - Version: 1, + ID: "1", + Version: "1", }, Name: "minimal", Status: "draft", @@ -797,7 +797,7 @@ func TestNilAndZeroValueHandling(t *testing.T) { json: func(t *testing.T, output string) { var result map[string]interface{} require.NoError(t, json.Unmarshal([]byte(output), &result)) - assert.Equal(t, float64(1), result["id"]) + assert.Equal(t, "1", result["id"]) assert.Equal(t, "minimal", result["name"]) assert.Equal(t, "draft", result["status"]) // nil pointer fields should be omitted @@ -818,7 +818,7 @@ func TestNilAndZeroValueHandling(t *testing.T) { yaml: func(t *testing.T, output string) { var result types.StackInstance require.NoError(t, yaml.Unmarshal([]byte(output), &result)) - assert.Equal(t, uint(1), result.ID) + assert.Equal(t, "1", result.ID) assert.Equal(t, "minimal", result.Name) assert.Equal(t, "draft", result.Status) // nil pointer fields should remain nil after round-trip @@ -832,11 +832,11 @@ func TestNilAndZeroValueHandling(t *testing.T) { name: "stack_instance_zero_ttl_and_empty_strings", data: types.StackInstance{ Base: types.Base{ - ID: 2, - Version: 1, + ID: "2", + Version: "1", }, Name: "zero-ttl", - StackDefinitionID: 5, + StackDefinitionID: "5", Owner: "bob", Branch: "", Namespace: "default", @@ -857,7 +857,7 @@ func TestNilAndZeroValueHandling(t *testing.T) { var result types.StackInstance require.NoError(t, yaml.Unmarshal([]byte(output), &result)) assert.Equal(t, "zero-ttl", result.Name) - assert.Equal(t, uint(5), result.StackDefinitionID) + assert.Equal(t, "5", result.StackDefinitionID) assert.Equal(t, 0, result.TTLMinutes) }, }, @@ -865,10 +865,10 @@ func TestNilAndZeroValueHandling(t *testing.T) { name: "deployment_log_empty_output_field", data: types.DeploymentLog{ Base: types.Base{ - ID: 200, - Version: 1, + ID: "200", + Version: "1", }, - InstanceID: 50, + InstanceID: "50", Action: "clean", Status: "success", Output: "", @@ -876,7 +876,7 @@ func TestNilAndZeroValueHandling(t *testing.T) { json: func(t *testing.T, output string) { var result map[string]interface{} require.NoError(t, json.Unmarshal([]byte(output), &result)) - assert.Equal(t, float64(200), result["id"]) + assert.Equal(t, "200", result["id"]) assert.Equal(t, "clean", result["action"]) // Output="" with omitempty should be omitted _, hasOutput := result["output"] @@ -885,7 +885,7 @@ func TestNilAndZeroValueHandling(t *testing.T) { yaml: func(t *testing.T, output string) { var result types.DeploymentLog require.NoError(t, yaml.Unmarshal([]byte(output), &result)) - assert.Equal(t, uint(200), result.ID) + assert.Equal(t, "200", result.ID) assert.Equal(t, "clean", result.Action) assert.Equal(t, "success", result.Status) assert.Empty(t, result.Output, "output should be empty after round-trip") @@ -949,16 +949,16 @@ func TestMultipleStackInstances_TableRows(t *testing.T) { p := &Printer{Writer: &buf, Format: FormatTable, NoColor: true} instances := []types.StackInstance{ - {Base: types.Base{ID: 1}, Name: "app-one", Branch: "main", Status: "running", Owner: "alice"}, - {Base: types.Base{ID: 2}, Name: "app-two", Branch: "develop", Status: "stopped", Owner: "bob"}, - {Base: types.Base{ID: 3}, Name: "app-three", Branch: "feature/x", Status: "error", Owner: "charlie"}, + {Base: types.Base{ID: "1"}, Name: "app-one", Branch: "main", Status: "running", Owner: "alice"}, + {Base: types.Base{ID: "2"}, Name: "app-two", Branch: "develop", Status: "stopped", Owner: "bob"}, + {Base: types.Base{ID: "3"}, Name: "app-three", Branch: "feature/x", Status: "error", Owner: "charlie"}, } headers := []string{"ID", "NAME", "BRANCH", "STATUS", "OWNER"} rows := make([][]string, len(instances)) for i, inst := range instances { rows[i] = []string{ - fmt.Sprintf("%d", inst.ID), + inst.ID, inst.Name, inst.Branch, p.StatusColor(inst.Status), @@ -986,8 +986,8 @@ func TestListResponse_MultiplePages_JSON(t *testing.T) { listResp := types.ListResponse[types.DeploymentLog]{ Data: []types.DeploymentLog{ - {Base: types.Base{ID: 1}, InstanceID: 10, Action: "deploy", Status: "success", Output: "ok"}, - {Base: types.Base{ID: 2}, InstanceID: 10, Action: "stop", Status: "success", Output: "stopped"}, + {Base: types.Base{ID: "1"}, InstanceID: "10", Action: "deploy", Status: "success", Output: "ok"}, + {Base: types.Base{ID: "2"}, InstanceID: "10", Action: "stop", Status: "success", Output: "stopped"}, }, Total: 50, Page: 3, diff --git a/cli/test/integration/auth_integration_test.go b/cli/test/integration/auth_integration_test.go index 2ec9da7..e6e6e43 100644 --- a/cli/test/integration/auth_integration_test.go +++ b/cli/test/integration/auth_integration_test.go @@ -38,7 +38,7 @@ func startAuthMockServer(t *testing.T) *httptest.Server { Token: "integration-jwt-token", ExpiresAt: time.Now().Add(24 * time.Hour).UTC().Format(time.RFC3339), User: types.User{ - Base: types.Base{ID: 1, CreatedAt: time.Now().UTC()}, + Base: types.Base{ID: "1", CreatedAt: time.Now().UTC()}, Username: "admin", Role: "admin", }, @@ -54,7 +54,7 @@ func startAuthMockServer(t *testing.T) *httptest.Server { if auth == "Bearer integration-jwt-token" || apiKey == "sk_integration_key" { w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(types.User{ - Base: types.Base{ID: 1, CreatedAt: time.Now().UTC()}, + Base: types.Base{ID: "1", CreatedAt: time.Now().UTC()}, Username: "admin", Role: "admin", }) @@ -138,7 +138,7 @@ func TestAuthWorkflow_LoginWhoamiLogout(t *testing.T) { require.NoError(t, err) assert.Equal(t, "admin", user.Username) assert.Equal(t, "admin", user.Role) - assert.Equal(t, uint(1), user.ID) + assert.Equal(t, "1", user.ID) // 3. Logout (remove token file and clear token) require.NoError(t, os.Remove(tokenPath)) diff --git a/cli/test/integration/edge_case_integration_test.go b/cli/test/integration/edge_case_integration_test.go index 5125a99..254cc84 100644 --- a/cli/test/integration/edge_case_integration_test.go +++ b/cli/test/integration/edge_case_integration_test.go @@ -52,7 +52,7 @@ func TestEdgeCase_ExpiredTokenHandling(t *testing.T) { { name: "GetStack", fn: func() error { - _, err := c.GetStack(1) + _, err := c.GetStack("1") return err }, }, @@ -61,7 +61,7 @@ func TestEdgeCase_ExpiredTokenHandling(t *testing.T) { fn: func() error { _, err := c.CreateStack(&types.CreateStackRequest{ Name: "test", - StackDefinitionID: 1, + StackDefinitionID: "1", }) return err }, @@ -69,7 +69,7 @@ func TestEdgeCase_ExpiredTokenHandling(t *testing.T) { { name: "DeployStack", fn: func() error { - _, err := c.DeployStack(1) + _, err := c.DeployStack("1") return err }, }, @@ -125,14 +125,14 @@ func TestEdgeCase_NetworkErrors(t *testing.T) { { name: "GetStack", fn: func() error { - _, err := c.GetStack(1) + _, err := c.GetStack("1") return err }, }, { name: "DeployStack", fn: func() error { - _, err := c.DeployStack(1) + _, err := c.DeployStack("1") return err }, }, @@ -146,13 +146,13 @@ func TestEdgeCase_NetworkErrors(t *testing.T) { { name: "DeleteStack", fn: func() error { - return c.DeleteStack(1) + return c.DeleteStack("1") }, }, { name: "BulkDeploy", fn: func() error { - _, err := c.BulkDeploy([]uint{1, 2, 3}) + _, err := c.BulkDeploy([]string{"1", "2", "3"}) return err }, }, @@ -189,7 +189,7 @@ func TestEdgeCase_InvalidInputValidation(t *testing.T) { } w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(types.StackInstance{ - Base: types.Base{ID: 1}, + Base: types.Base{ID: "1"}, Name: name, }) @@ -212,9 +212,9 @@ func TestEdgeCase_InvalidInputValidation(t *testing.T) { } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(types.ValueOverride{ - Base: types.Base{ID: 10}, - InstanceID: 1, - ChartID: 1, + Base: types.Base{ID: "10"}, + InstanceID: "1", + ChartID: "1", Values: "{}", }) @@ -231,7 +231,7 @@ func TestEdgeCase_InvalidInputValidation(t *testing.T) { t.Parallel() _, err := c.CreateStack(&types.CreateStackRequest{ Name: "", - StackDefinitionID: 1, + StackDefinitionID: "1", }) require.Error(t, err) assert.Contains(t, err.Error(), "name is required") @@ -239,14 +239,14 @@ func TestEdgeCase_InvalidInputValidation(t *testing.T) { t.Run("GetStackIDZero", func(t *testing.T) { t.Parallel() - _, err := c.GetStack(0) + _, err := c.GetStack("0") require.Error(t, err) assert.Contains(t, err.Error(), "invalid stack ID") }) t.Run("SetValueOverrideNilValues", func(t *testing.T) { t.Parallel() - _, err := c.SetValueOverride(1, 1, &types.SetValueOverrideRequest{ + _, err := c.SetValueOverride("1", "1", &types.SetValueOverrideRequest{ Values: nil, }) require.Error(t, err) @@ -256,11 +256,11 @@ func TestEdgeCase_InvalidInputValidation(t *testing.T) { t.Run("SetValueOverrideEmptyValues", func(t *testing.T) { t.Parallel() // Empty map (not nil) should succeed - override, err := c.SetValueOverride(1, 1, &types.SetValueOverrideRequest{ + override, err := c.SetValueOverride("1", "1", &types.SetValueOverrideRequest{ Values: map[string]interface{}{}, }) require.NoError(t, err) - assert.Equal(t, uint(10), override.ID) + assert.Equal(t, "10", override.ID) }) } @@ -280,8 +280,8 @@ func TestEdgeCase_ConcurrentOperations(t *testing.T) { w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(types.ListResponse[types.StackInstance]{ Data: []types.StackInstance{ - {Base: types.Base{ID: 1}, Name: "stack-1", Status: "running"}, - {Base: types.Base{ID: 2}, Name: "stack-2", Status: "running"}, + {Base: types.Base{ID: "1"}, Name: "stack-1", Status: "running"}, + {Base: types.Base{ID: "2"}, Name: "stack-2", Status: "running"}, }, Total: 2, Page: 1, @@ -294,7 +294,7 @@ func TestEdgeCase_ConcurrentOperations(t *testing.T) { json.NewDecoder(r.Body).Decode(&req) w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(types.StackInstance{ - Base: types.Base{ID: 100}, + Base: types.Base{ID: "100"}, Name: req.Name, Status: "draft", }) @@ -355,7 +355,7 @@ func TestEdgeCase_ConcurrentOperations(t *testing.T) { defer wg.Done() _, err := c.CreateStack(&types.CreateStackRequest{ Name: fmt.Sprintf("concurrent-stack-%d", idx), - StackDefinitionID: 1, + StackDefinitionID: "1", }) createErrs[idx] = err }(i) @@ -381,7 +381,7 @@ func TestEdgeCase_LargeResponseHandling(t *testing.T) { stacks := make([]types.StackInstance, itemCount) for i := 0; i < itemCount; i++ { stacks[i] = types.StackInstance{ - Base: types.Base{ID: uint(i + 1)}, + Base: types.Base{ID: fmt.Sprintf("%d", i+1)}, Name: fmt.Sprintf("stack-%d", i+1), Status: "running", Owner: "admin", @@ -410,8 +410,8 @@ func TestEdgeCase_LargeResponseHandling(t *testing.T) { resp, err := c.ListStacks(nil) require.NoError(t, err) require.Len(t, resp.Data, itemCount) - assert.Equal(t, uint(1), resp.Data[0].ID) - assert.Equal(t, uint(itemCount), resp.Data[itemCount-1].ID) + assert.Equal(t, "1", resp.Data[0].ID) + assert.Equal(t, fmt.Sprintf("%d", itemCount), resp.Data[itemCount-1].ID) assert.Equal(t, "stack-1", resp.Data[0].Name) assert.Equal(t, fmt.Sprintf("stack-%d", itemCount), resp.Data[itemCount-1].Name) }) @@ -488,12 +488,12 @@ func TestEdgeCase_ServerErrorUserFacingMessages(t *testing.T) { assert.Contains(t, err.Error(), tt.wantMsg) // Test with GetStack - _, err = c.GetStack(1) + _, err = c.GetStack("1") require.Error(t, err) assert.Contains(t, err.Error(), tt.wantMsg) // Test with DeployStack - _, err = c.DeployStack(1) + _, err = c.DeployStack("1") require.Error(t, err) assert.Contains(t, err.Error(), tt.wantMsg) }) diff --git a/cli/test/integration/override_integration_test.go b/cli/test/integration/override_integration_test.go index 29aef1d..081f940 100644 --- a/cli/test/integration/override_integration_test.go +++ b/cli/test/integration/override_integration_test.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" "net/http/httptest" + "strings" "sync" "testing" @@ -19,18 +20,18 @@ type overrideMockState struct { mu sync.Mutex valueOverrides map[string]*types.ValueOverride // key: "instanceID:chartID" branchOverrides map[string]*types.BranchOverride // key: "instanceID:chartID" - quotaOverrides map[uint]*types.QuotaOverride // key: instanceID - mergedValues map[uint]*types.MergedValues // key: instanceID + quotaOverrides map[string]*types.QuotaOverride // key: instanceID + mergedValues map[string]*types.MergedValues // key: instanceID } func newOverrideMockState() *overrideMockState { return &overrideMockState{ valueOverrides: make(map[string]*types.ValueOverride), branchOverrides: make(map[string]*types.BranchOverride), - quotaOverrides: make(map[uint]*types.QuotaOverride), - mergedValues: map[uint]*types.MergedValues{ - 42: { - InstanceID: 42, + quotaOverrides: make(map[string]*types.QuotaOverride), + mergedValues: map[string]*types.MergedValues{ + "42": { + InstanceID: "42", Charts: map[string]map[string]interface{}{ "api": {"replicas": float64(2), "port": float64(8080)}, "frontend": {"replicas": float64(1)}, @@ -40,24 +41,45 @@ func newOverrideMockState() *overrideMockState { } } +// parsePathSegments splits a URL path and attempts to match the +// /api/v1/stack-instances/:id/[/] pattern. +// Returns (instanceID, chartID, suffix, ok). chartID is empty if absent. +func parsePathSegments(path string) (instanceID, chartID, suffix string, ok bool) { + trimmed := strings.TrimPrefix(path, "/api/v1/stack-instances/") + if trimmed == path { + return "", "", "", false + } + parts := strings.Split(trimmed, "/") + switch len(parts) { + case 2: + return parts[0], "", parts[1], true + case 3: + return parts[0], parts[2], parts[1], true + } + return "", "", "", false +} + func startOverrideMockServer(t *testing.T, state *overrideMockState) *httptest.Server { t.Helper() return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") - var instanceID uint - var chartID uint + instanceID, chartID, suffix, ok := parsePathSegments(r.URL.Path) + if !ok { + w.WriteHeader(http.StatusNotFound) + json.NewEncoder(w).Encode(types.ErrorResponse{Error: "not found"}) + return + } // --- Value Override routes --- // List value overrides: GET /api/v1/stack-instances/:id/overrides - if n, _ := fmt.Sscanf(r.URL.Path, "/api/v1/stack-instances/%d/overrides", &instanceID); n == 1 && r.URL.Path == fmt.Sprintf("/api/v1/stack-instances/%d/overrides", instanceID) { + if suffix == "overrides" && chartID == "" { if r.Method == http.MethodGet { state.mu.Lock() var overrides []types.ValueOverride for k, v := range state.valueOverrides { - var iID, cID uint - fmt.Sscanf(k, "%d:%d", &iID, &cID) + iID := strings.SplitN(k, ":", 2)[0] if iID == instanceID { overrides = append(overrides, *v) } @@ -70,8 +92,8 @@ func startOverrideMockServer(t *testing.T, state *overrideMockState) *httptest.S } // Value override by chart: GET/PUT/DELETE /api/v1/stack-instances/:id/overrides/:chartID - if n, _ := fmt.Sscanf(r.URL.Path, "/api/v1/stack-instances/%d/overrides/%d", &instanceID, &chartID); n == 2 { - key := fmt.Sprintf("%d:%d", instanceID, chartID) + if suffix == "overrides" && chartID != "" { + key := fmt.Sprintf("%s:%s", instanceID, chartID) switch r.Method { case http.MethodGet: @@ -102,7 +124,7 @@ func startOverrideMockServer(t *testing.T, state *overrideMockState) *httptest.S } state.mu.Lock() vo := &types.ValueOverride{ - Base: types.Base{ID: chartID, Version: 1}, + Base: types.Base{ID: chartID, Version: "1"}, InstanceID: instanceID, ChartID: chartID, Values: string(valBytes), @@ -133,13 +155,12 @@ func startOverrideMockServer(t *testing.T, state *overrideMockState) *httptest.S // --- Branch Override routes --- // List branch overrides: GET /api/v1/stack-instances/:id/branches - if n, _ := fmt.Sscanf(r.URL.Path, "/api/v1/stack-instances/%d/branches", &instanceID); n == 1 && r.URL.Path == fmt.Sprintf("/api/v1/stack-instances/%d/branches", instanceID) { + if suffix == "branches" && chartID == "" { if r.Method == http.MethodGet { state.mu.Lock() var overrides []types.BranchOverride for k, v := range state.branchOverrides { - var iID, cID uint - fmt.Sscanf(k, "%d:%d", &iID, &cID) + iID := strings.SplitN(k, ":", 2)[0] if iID == instanceID { overrides = append(overrides, *v) } @@ -152,8 +173,8 @@ func startOverrideMockServer(t *testing.T, state *overrideMockState) *httptest.S } // Branch override by chart: GET/PUT/DELETE /api/v1/stack-instances/:id/branches/:chartID - if n, _ := fmt.Sscanf(r.URL.Path, "/api/v1/stack-instances/%d/branches/%d", &instanceID, &chartID); n == 2 { - key := fmt.Sprintf("%d:%d", instanceID, chartID) + if suffix == "branches" && chartID != "" { + key := fmt.Sprintf("%s:%s", instanceID, chartID) switch r.Method { case http.MethodGet: @@ -178,7 +199,7 @@ func startOverrideMockServer(t *testing.T, state *overrideMockState) *httptest.S } state.mu.Lock() bo := &types.BranchOverride{ - Base: types.Base{ID: chartID, Version: 1}, + Base: types.Base{ID: chartID, Version: "1"}, InstanceID: instanceID, ChartID: chartID, Branch: req.Branch, @@ -209,7 +230,7 @@ func startOverrideMockServer(t *testing.T, state *overrideMockState) *httptest.S // --- Quota Override routes --- // GET/PUT/DELETE /api/v1/stack-instances/:id/quota-overrides - if n, _ := fmt.Sscanf(r.URL.Path, "/api/v1/stack-instances/%d/quota-overrides", &instanceID); n == 1 && r.URL.Path == fmt.Sprintf("/api/v1/stack-instances/%d/quota-overrides", instanceID) { + if suffix == "quota-overrides" && chartID == "" { switch r.Method { case http.MethodGet: state.mu.Lock() @@ -263,7 +284,7 @@ func startOverrideMockServer(t *testing.T, state *overrideMockState) *httptest.S } // --- Merged Values --- - if n, _ := fmt.Sscanf(r.URL.Path, "/api/v1/stack-instances/%d/values", &instanceID); n == 1 && r.URL.Path == fmt.Sprintf("/api/v1/stack-instances/%d/values", instanceID) && r.Method == http.MethodGet { + if suffix == "values" && chartID == "" && r.Method == http.MethodGet { state.mu.Lock() v, exists := state.mergedValues[instanceID] state.mu.Unlock() @@ -296,60 +317,60 @@ func TestValueOverrideWorkflow_CRUDLifecycle(t *testing.T) { c := client.New(server.URL) // 1. List — should be empty - overrides, err := c.ListValueOverrides(42) + overrides, err := c.ListValueOverrides("42") require.NoError(t, err) assert.Empty(t, overrides) // 2. Set a value override - vo, err := c.SetValueOverride(42, 1, &types.SetValueOverrideRequest{ + vo, err := c.SetValueOverride("42", "1", &types.SetValueOverrideRequest{ Values: map[string]interface{}{"replicas": float64(5)}, }) require.NoError(t, err) - assert.Equal(t, uint(1), vo.ChartID) - assert.Equal(t, uint(42), vo.InstanceID) + assert.Equal(t, "1", vo.ChartID) + assert.Equal(t, "42", vo.InstanceID) assert.Contains(t, vo.Values, "replicas") // 3. Get — should find it - got, err := c.GetValueOverride(42, 1) + got, err := c.GetValueOverride("42", "1") require.NoError(t, err) - assert.Equal(t, uint(1), got.ChartID) + assert.Equal(t, "1", got.ChartID) assert.Contains(t, got.Values, "replicas") // 4. List — should be non-empty - overrides, err = c.ListValueOverrides(42) + overrides, err = c.ListValueOverrides("42") require.NoError(t, err) assert.Len(t, overrides, 1) // 5. Set another override on different chart - _, err = c.SetValueOverride(42, 2, &types.SetValueOverrideRequest{ + _, err = c.SetValueOverride("42", "2", &types.SetValueOverrideRequest{ Values: map[string]interface{}{"debug": true}, }) require.NoError(t, err) - overrides, err = c.ListValueOverrides(42) + overrides, err = c.ListValueOverrides("42") require.NoError(t, err) assert.Len(t, overrides, 2) // 6. Delete first override - err = c.DeleteValueOverride(42, 1) + err = c.DeleteValueOverride("42", "1") require.NoError(t, err) // 7. Verify it's gone - _, err = c.GetValueOverride(42, 1) + _, err = c.GetValueOverride("42", "1") require.Error(t, err) assert.Contains(t, err.Error(), "override not found") // 8. List — should have 1 left - overrides, err = c.ListValueOverrides(42) + overrides, err = c.ListValueOverrides("42") require.NoError(t, err) assert.Len(t, overrides, 1) // 9. Delete second override - err = c.DeleteValueOverride(42, 2) + err = c.DeleteValueOverride("42", "2") require.NoError(t, err) // 10. List — should be empty again - overrides, err = c.ListValueOverrides(42) + overrides, err = c.ListValueOverrides("42") require.NoError(t, err) assert.Empty(t, overrides) } @@ -368,48 +389,48 @@ func TestBranchOverrideWorkflow_CRUDLifecycle(t *testing.T) { c := client.New(server.URL) // 1. List — empty - overrides, err := c.ListBranchOverrides(42) + overrides, err := c.ListBranchOverrides("42") require.NoError(t, err) assert.Empty(t, overrides) // 2. Set branch override - bo, err := c.SetBranchOverride(42, 1, &types.SetBranchOverrideRequest{Branch: "feature/my-branch"}) + bo, err := c.SetBranchOverride("42", "1", &types.SetBranchOverrideRequest{Branch: "feature/my-branch"}) require.NoError(t, err) assert.Equal(t, "feature/my-branch", bo.Branch) - assert.Equal(t, uint(42), bo.InstanceID) - assert.Equal(t, uint(1), bo.ChartID) + assert.Equal(t, "42", bo.InstanceID) + assert.Equal(t, "1", bo.ChartID) // 3. Get — should find it - got, err := c.GetBranchOverride(42, 1) + got, err := c.GetBranchOverride("42", "1") require.NoError(t, err) assert.Equal(t, "feature/my-branch", got.Branch) // 4. List — non-empty - overrides, err = c.ListBranchOverrides(42) + overrides, err = c.ListBranchOverrides("42") require.NoError(t, err) assert.Len(t, overrides, 1) // 5. Update branch - updated, err := c.SetBranchOverride(42, 1, &types.SetBranchOverrideRequest{Branch: "main"}) + updated, err := c.SetBranchOverride("42", "1", &types.SetBranchOverrideRequest{Branch: "main"}) require.NoError(t, err) assert.Equal(t, "main", updated.Branch) // 6. Verify update persists - got, err = c.GetBranchOverride(42, 1) + got, err = c.GetBranchOverride("42", "1") require.NoError(t, err) assert.Equal(t, "main", got.Branch) // 7. Delete - err = c.DeleteBranchOverride(42, 1) + err = c.DeleteBranchOverride("42", "1") require.NoError(t, err) // 8. Verify gone - _, err = c.GetBranchOverride(42, 1) + _, err = c.GetBranchOverride("42", "1") require.Error(t, err) assert.Contains(t, err.Error(), "branch override not found") // 9. List — empty - overrides, err = c.ListBranchOverrides(42) + overrides, err = c.ListBranchOverrides("42") require.NoError(t, err) assert.Empty(t, overrides) } @@ -428,24 +449,24 @@ func TestQuotaOverrideWorkflow_CRUDLifecycle(t *testing.T) { c := client.New(server.URL) // 1. Get — not found - _, err := c.GetQuotaOverride(42) + _, err := c.GetQuotaOverride("42") require.Error(t, err) assert.Contains(t, err.Error(), "quota not found") // 2. Set quota override - q, err := c.SetQuotaOverride(42, &types.SetQuotaOverrideRequest{ + q, err := c.SetQuotaOverride("42", &types.SetQuotaOverrideRequest{ CPURequest: "100m", CPULimit: "500m", MemRequest: "128Mi", MemLimit: "512Mi", }) require.NoError(t, err) - assert.Equal(t, uint(42), q.InstanceID) + assert.Equal(t, "42", q.InstanceID) assert.Equal(t, "100m", q.CPURequest) assert.Equal(t, "512Mi", q.MemLimit) // 3. Get — should find it - got, err := c.GetQuotaOverride(42) + got, err := c.GetQuotaOverride("42") require.NoError(t, err) assert.Equal(t, "100m", got.CPURequest) assert.Equal(t, "500m", got.CPULimit) @@ -453,7 +474,7 @@ func TestQuotaOverrideWorkflow_CRUDLifecycle(t *testing.T) { assert.Equal(t, "512Mi", got.MemLimit) // 4. Update - updated, err := c.SetQuotaOverride(42, &types.SetQuotaOverrideRequest{ + updated, err := c.SetQuotaOverride("42", &types.SetQuotaOverrideRequest{ CPURequest: "200m", MemLimit: "1Gi", }) @@ -462,16 +483,16 @@ func TestQuotaOverrideWorkflow_CRUDLifecycle(t *testing.T) { assert.Equal(t, "1Gi", updated.MemLimit) // 5. Delete - err = c.DeleteQuotaOverride(42) + err = c.DeleteQuotaOverride("42") require.NoError(t, err) // 6. Verify gone - _, err = c.GetQuotaOverride(42) + _, err = c.GetQuotaOverride("42") require.Error(t, err) assert.Contains(t, err.Error(), "quota not found") // 7. Delete again — should 404 - err = c.DeleteQuotaOverride(42) + err = c.DeleteQuotaOverride("42") require.Error(t, err) assert.Contains(t, err.Error(), "quota not found") } @@ -490,14 +511,14 @@ func TestMergedValuesWorkflow(t *testing.T) { c := client.New(server.URL) // 1. Get merged values for existing instance - values, err := c.GetMergedValues(42, "") + values, err := c.GetMergedValues("42", "") require.NoError(t, err) - assert.Equal(t, uint(42), values.InstanceID) + assert.Equal(t, "42", values.InstanceID) assert.Contains(t, values.Charts, "api") assert.Contains(t, values.Charts, "frontend") // 2. Get merged values for non-existent instance - _, err = c.GetMergedValues(999, "") + _, err = c.GetMergedValues("999", "") require.Error(t, err) assert.Contains(t, err.Error(), "instance not found") } @@ -516,22 +537,22 @@ func TestOverrideWorkflow_ErrorHandling(t *testing.T) { c := client.New(server.URL) // Get non-existent value override - _, err := c.GetValueOverride(42, 99) + _, err := c.GetValueOverride("42", "99") require.Error(t, err) assert.Contains(t, err.Error(), "override not found") // Delete non-existent value override - err = c.DeleteValueOverride(42, 99) + err = c.DeleteValueOverride("42", "99") require.Error(t, err) assert.Contains(t, err.Error(), "override not found") // Get non-existent branch override - _, err = c.GetBranchOverride(42, 99) + _, err = c.GetBranchOverride("42", "99") require.Error(t, err) assert.Contains(t, err.Error(), "branch override not found") // Delete non-existent branch override - err = c.DeleteBranchOverride(42, 99) + err = c.DeleteBranchOverride("42", "99") require.Error(t, err) assert.Contains(t, err.Error(), "branch override not found") } diff --git a/cli/test/integration/stack_integration_test.go b/cli/test/integration/stack_integration_test.go index 9f645f5..289d78c 100644 --- a/cli/test/integration/stack_integration_test.go +++ b/cli/test/integration/stack_integration_test.go @@ -6,6 +6,7 @@ import ( "net/http" "net/http/httptest" "strconv" + "strings" "sync" "testing" @@ -19,13 +20,13 @@ import ( type stackMockState struct { mu sync.Mutex nextID uint - instances map[uint]*types.StackInstance + instances map[string]*types.StackInstance } func newStackMockState() *stackMockState { return &stackMockState{ nextID: 1, - instances: make(map[uint]*types.StackInstance), + instances: make(map[string]*types.StackInstance), } } @@ -44,7 +45,7 @@ func startStackMockServer(t *testing.T, state *stackMockState) *httptest.Server return } state.mu.Lock() - instance.ID = state.nextID + instance.ID = fmt.Sprintf("%d", state.nextID) state.nextID++ instance.Status = "draft" state.instances[instance.ID] = &instance @@ -82,14 +83,27 @@ func startStackMockServer(t *testing.T, state *stackMockState) *httptest.Server // Match specific instance routes default: // Parse /api/v1/stack-instances/[/] - var id uint + trimmed := strings.TrimPrefix(r.URL.Path, "/api/v1/stack-instances/") + if trimmed == r.URL.Path { + w.WriteHeader(http.StatusNotFound) + json.NewEncoder(w).Encode(types.ErrorResponse{Error: "not found"}) + return + } + parts := strings.Split(trimmed, "/") + var id string var action string - n, _ := fmt.Sscanf(r.URL.Path, "/api/v1/stack-instances/%d/%s", &id, &action) - if n == 0 { - // Try without action - n, _ = fmt.Sscanf(r.URL.Path, "/api/v1/stack-instances/%d", &id) + switch len(parts) { + case 1: + id = parts[0] + case 2: + id = parts[0] + action = parts[1] + default: + w.WriteHeader(http.StatusNotFound) + json.NewEncoder(w).Encode(types.ErrorResponse{Error: "not found"}) + return } - if n == 0 { + if id == "" { w.WriteHeader(http.StatusNotFound) json.NewEncoder(w).Encode(types.ErrorResponse{Error: "not found"}) return @@ -135,7 +149,7 @@ func startStackMockServer(t *testing.T, state *stackMockState) *httptest.Server state.mu.Unlock() w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(types.DeploymentLog{ - Base: types.Base{ID: 100 + id}, + Base: types.Base{ID: "deploy-" + id}, InstanceID: id, Action: "deploy", Status: "started", @@ -148,7 +162,7 @@ func startStackMockServer(t *testing.T, state *stackMockState) *httptest.Server state.mu.Unlock() w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(types.DeploymentLog{ - Base: types.Base{ID: 200 + id}, + Base: types.Base{ID: "stop-" + id}, InstanceID: id, Action: "stop", Status: "started", @@ -161,7 +175,7 @@ func startStackMockServer(t *testing.T, state *stackMockState) *httptest.Server state.mu.Unlock() w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(types.DeploymentLog{ - Base: types.Base{ID: 300 + id}, + Base: types.Base{ID: "clean-" + id}, InstanceID: id, Action: "clean", Status: "started", @@ -181,7 +195,7 @@ func startStackMockServer(t *testing.T, state *stackMockState) *httptest.Server case action == "deploy-log" && r.Method == http.MethodGet: w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(types.DeploymentLog{ - Base: types.Base{ID: 400 + id}, + Base: types.Base{ID: "log-" + id}, InstanceID: id, Action: "deploy", Status: "completed", @@ -192,7 +206,7 @@ func startStackMockServer(t *testing.T, state *stackMockState) *httptest.Server case action == "clone" && r.Method == http.MethodPost: state.mu.Lock() newInst := *inst - newInst.ID = state.nextID + newInst.ID = fmt.Sprintf("%d", state.nextID) state.nextID++ newInst.Name = inst.Name + "-clone" newInst.Status = "draft" @@ -239,7 +253,7 @@ func TestStackWorkflow_CreateDeployStatusLogsStopCleanDelete(t *testing.T) { // 1. Create created, err := c.CreateStack(&types.CreateStackRequest{ Name: "lifecycle-stack", - StackDefinitionID: 1, + StackDefinitionID: "1", Branch: "main", }) require.NoError(t, err) @@ -374,16 +388,16 @@ func TestStackWorkflow_DestructiveOpsOnMissingInstance(t *testing.T) { c := client.New(server.URL) // Delete a non-existent instance - err := c.DeleteStack(999) + err := c.DeleteStack("999") require.Error(t, err) assert.Contains(t, err.Error(), "instance not found") // Deploy a non-existent instance - _, err = c.DeployStack(999) + _, err = c.DeployStack("999") require.Error(t, err) // Get non-existent - _, err = c.GetStack(999) + _, err = c.GetStack("999") require.Error(t, err) } @@ -414,7 +428,7 @@ func TestStackWorkflow_ErrorStatusCodes(t *testing.T) { defer server.Close() c := client.New(server.URL) - _, err := c.GetStack(1) + _, err := c.GetStack("1") require.Error(t, err) assert.Contains(t, err.Error(), tt.wantMsg) diff --git a/cli/test/integration/template_definition_integration_test.go b/cli/test/integration/template_definition_integration_test.go index 5c72471..2a3b9f1 100644 --- a/cli/test/integration/template_definition_integration_test.go +++ b/cli/test/integration/template_definition_integration_test.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" "net/http/httptest" + "strings" "sync" "testing" @@ -19,8 +20,8 @@ type templateDefMockState struct { mu sync.Mutex nextDefID uint nextInstID uint - definitions map[uint]*types.StackDefinition - instances map[uint]*types.StackInstance + definitions map[string]*types.StackDefinition + instances map[string]*types.StackInstance templates []types.StackTemplate } @@ -28,28 +29,28 @@ func newTemplateDefMockState() *templateDefMockState { return &templateDefMockState{ nextDefID: 1, nextInstID: 100, - definitions: make(map[uint]*types.StackDefinition), - instances: make(map[uint]*types.StackInstance), + definitions: make(map[string]*types.StackDefinition), + instances: make(map[string]*types.StackInstance), templates: []types.StackTemplate{ { - Base: types.Base{ID: 1, Version: 1}, + Base: types.Base{ID: "1", Version: "1"}, Name: "web-app", Description: "Full web application stack", Published: true, Owner: "admin", Charts: []types.ChartConfig{ - {Base: types.Base{ID: 1}, Name: "frontend", RepoURL: "https://charts.example.com", ChartVersion: "1.0.0"}, - {Base: types.Base{ID: 2}, Name: "backend", RepoURL: "https://charts.example.com", ChartVersion: "2.0.0"}, + {Base: types.Base{ID: "1"}, Name: "frontend", RepoURL: "https://charts.example.com", ChartVersion: "1.0.0"}, + {Base: types.Base{ID: "2"}, Name: "backend", RepoURL: "https://charts.example.com", ChartVersion: "2.0.0"}, }, }, { - Base: types.Base{ID: 2, Version: 1}, + Base: types.Base{ID: "2", Version: "1"}, Name: "api-only", Description: "API-only stack", Published: false, Owner: "admin", Charts: []types.ChartConfig{ - {Base: types.Base{ID: 3}, Name: "api", RepoURL: "https://charts.example.com", ChartVersion: "3.0.0"}, + {Base: types.Base{ID: "3"}, Name: "api", RepoURL: "https://charts.example.com", ChartVersion: "3.0.0"}, }, }, }, @@ -80,83 +81,93 @@ func startTemplateDefMockServer(t *testing.T, state *templateDefMockState) *http } // Template get/instantiate/quick-deploy - var tmplID uint - var tmplAction string - if n, _ := fmt.Sscanf(r.URL.Path, "/api/v1/templates/%d/%s", &tmplID, &tmplAction); n >= 1 { - // Find template - var tmpl *types.StackTemplate - for i := range state.templates { - if state.templates[i].ID == tmplID { - tmpl = &state.templates[i] - break - } - } - if tmpl == nil { - w.WriteHeader(http.StatusNotFound) - json.NewEncoder(w).Encode(types.ErrorResponse{Error: "template not found"}) - return + if tmplTrim := strings.TrimPrefix(r.URL.Path, "/api/v1/templates/"); tmplTrim != r.URL.Path { + parts := strings.Split(tmplTrim, "/") + var tmplID string + var tmplAction string + switch len(parts) { + case 1: + tmplID = parts[0] + case 2: + tmplID = parts[0] + tmplAction = parts[1] } - - switch { - case tmplAction == "" && r.Method == http.MethodGet: - w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(tmpl) - return - - case tmplAction == "instantiate" && r.Method == http.MethodPost: - var req types.InstantiateTemplateRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - w.WriteHeader(http.StatusBadRequest) - json.NewEncoder(w).Encode(types.ErrorResponse{Error: "invalid body"}) - return - } - state.mu.Lock() - inst := &types.StackInstance{ - Base: types.Base{ID: state.nextInstID, Version: 1}, - Name: req.Name, - Branch: req.Branch, - Status: "draft", - Owner: "admin", - StackDefinitionID: 0, + if tmplID != "" { + // Find template + var tmpl *types.StackTemplate + for i := range state.templates { + if state.templates[i].ID == tmplID { + tmpl = &state.templates[i] + break + } } - if req.ClusterID != 0 { - cid := req.ClusterID - inst.ClusterID = &cid + if tmpl == nil { + w.WriteHeader(http.StatusNotFound) + json.NewEncoder(w).Encode(types.ErrorResponse{Error: "template not found"}) + return } - state.instances[inst.ID] = inst - state.nextInstID++ - state.mu.Unlock() - w.WriteHeader(http.StatusCreated) - json.NewEncoder(w).Encode(inst) - return + switch { + case tmplAction == "" && r.Method == http.MethodGet: + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(tmpl) + return - case tmplAction == "quick-deploy" && r.Method == http.MethodPost: - var req types.QuickDeployRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - w.WriteHeader(http.StatusBadRequest) - json.NewEncoder(w).Encode(types.ErrorResponse{Error: "invalid body"}) + case tmplAction == "instantiate" && r.Method == http.MethodPost: + var req types.InstantiateTemplateRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(types.ErrorResponse{Error: "invalid body"}) + return + } + state.mu.Lock() + inst := &types.StackInstance{ + Base: types.Base{ID: fmt.Sprintf("%d", state.nextInstID), Version: "1"}, + Name: req.Name, + Branch: req.Branch, + Status: "draft", + Owner: "admin", + StackDefinitionID: "", + } + if req.ClusterID != "" { + cid := req.ClusterID + inst.ClusterID = &cid + } + state.instances[inst.ID] = inst + state.nextInstID++ + state.mu.Unlock() + + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(inst) return - } - state.mu.Lock() - inst := &types.StackInstance{ - Base: types.Base{ID: state.nextInstID, Version: 1}, - Name: req.Name, - Branch: req.Branch, - Status: "deploying", - Owner: "admin", - } - if req.ClusterID != 0 { - cid := req.ClusterID - inst.ClusterID = &cid - } - state.instances[inst.ID] = inst - state.nextInstID++ - state.mu.Unlock() - w.WriteHeader(http.StatusCreated) - json.NewEncoder(w).Encode(inst) - return + case tmplAction == "quick-deploy" && r.Method == http.MethodPost: + var req types.QuickDeployRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(types.ErrorResponse{Error: "invalid body"}) + return + } + state.mu.Lock() + inst := &types.StackInstance{ + Base: types.Base{ID: fmt.Sprintf("%d", state.nextInstID), Version: "1"}, + Name: req.Name, + Branch: req.Branch, + Status: "deploying", + Owner: "admin", + } + if req.ClusterID != "" { + cid := req.ClusterID + inst.ClusterID = &cid + } + state.instances[inst.ID] = inst + state.nextInstID++ + state.mu.Unlock() + + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(inst) + return + } } } @@ -187,7 +198,7 @@ func startTemplateDefMockServer(t *testing.T, state *templateDefMockState) *http } state.mu.Lock() def := &types.StackDefinition{ - Base: types.Base{ID: state.nextDefID, Version: 1}, + Base: types.Base{ID: fmt.Sprintf("%d", state.nextDefID), Version: "1"}, Name: req.Name, Description: req.Description, DefaultBranch: "main", @@ -212,8 +223,8 @@ func startTemplateDefMockServer(t *testing.T, state *templateDefMockState) *http return } state.mu.Lock() - importedDef.ID = state.nextDefID - importedDef.Version = 1 + importedDef.ID = fmt.Sprintf("%d", state.nextDefID) + importedDef.Version = "1" importedDef.Owner = "admin" state.definitions[importedDef.ID] = &importedDef state.nextDefID++ @@ -225,74 +236,80 @@ func startTemplateDefMockServer(t *testing.T, state *templateDefMockState) *http } // Definition by ID: get/update/delete/export - var defID uint - var defAction string - n, _ := fmt.Sscanf(r.URL.Path, "/api/v1/stack-definitions/%d/%s", &defID, &defAction) - if n == 0 { - n, _ = fmt.Sscanf(r.URL.Path, "/api/v1/stack-definitions/%d", &defID) - } - if n >= 1 { - state.mu.Lock() - def, exists := state.definitions[defID] - state.mu.Unlock() + if defTrim := strings.TrimPrefix(r.URL.Path, "/api/v1/stack-definitions/"); defTrim != r.URL.Path { + parts := strings.Split(defTrim, "/") + var defID string + var defAction string + switch len(parts) { + case 1: + defID = parts[0] + case 2: + defID = parts[0] + defAction = parts[1] + } + if defID != "" { + state.mu.Lock() + def, exists := state.definitions[defID] + state.mu.Unlock() - switch { - case defAction == "" && r.Method == http.MethodGet: - if !exists { - w.WriteHeader(http.StatusNotFound) - json.NewEncoder(w).Encode(types.ErrorResponse{Error: "definition not found"}) + switch { + case defAction == "" && r.Method == http.MethodGet: + if !exists { + w.WriteHeader(http.StatusNotFound) + json.NewEncoder(w).Encode(types.ErrorResponse{Error: "definition not found"}) + return + } + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(def) return - } - w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(def) - return - case defAction == "" && r.Method == http.MethodPut: - if !exists { - w.WriteHeader(http.StatusNotFound) - json.NewEncoder(w).Encode(types.ErrorResponse{Error: "definition not found"}) - return - } - var req types.UpdateDefinitionRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - w.WriteHeader(http.StatusBadRequest) - json.NewEncoder(w).Encode(types.ErrorResponse{Error: "invalid body"}) + case defAction == "" && r.Method == http.MethodPut: + if !exists { + w.WriteHeader(http.StatusNotFound) + json.NewEncoder(w).Encode(types.ErrorResponse{Error: "definition not found"}) + return + } + var req types.UpdateDefinitionRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(types.ErrorResponse{Error: "invalid body"}) + return + } + state.mu.Lock() + if req.Name != "" { + def.Name = req.Name + } + if req.Description != "" { + def.Description = req.Description + } + def.Version = def.Version + "+1" + state.mu.Unlock() + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(def) return - } - state.mu.Lock() - if req.Name != "" { - def.Name = req.Name - } - if req.Description != "" { - def.Description = req.Description - } - def.Version++ - state.mu.Unlock() - w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(def) - return - case defAction == "" && r.Method == http.MethodDelete: - if !exists { - w.WriteHeader(http.StatusNotFound) - json.NewEncoder(w).Encode(types.ErrorResponse{Error: "definition not found"}) + case defAction == "" && r.Method == http.MethodDelete: + if !exists { + w.WriteHeader(http.StatusNotFound) + json.NewEncoder(w).Encode(types.ErrorResponse{Error: "definition not found"}) + return + } + state.mu.Lock() + delete(state.definitions, defID) + state.mu.Unlock() + w.WriteHeader(http.StatusNoContent) return - } - state.mu.Lock() - delete(state.definitions, defID) - state.mu.Unlock() - w.WriteHeader(http.StatusNoContent) - return - case defAction == "export" && r.Method == http.MethodGet: - if !exists { - w.WriteHeader(http.StatusNotFound) - json.NewEncoder(w).Encode(types.ErrorResponse{Error: "definition not found"}) + case defAction == "export" && r.Method == http.MethodGet: + if !exists { + w.WriteHeader(http.StatusNotFound) + json.NewEncoder(w).Encode(types.ErrorResponse{Error: "definition not found"}) + return + } + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(def) return } - w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(def) - return } } @@ -326,13 +343,13 @@ func TestTemplateWorkflow_BrowseAndInstantiate(t *testing.T) { assert.Equal(t, "web-app", resp.Data[0].Name) // 3. Get template details - tmpl, err := c.GetTemplate(1) + tmpl, err := c.GetTemplate("1") require.NoError(t, err) assert.Equal(t, "web-app", tmpl.Name) assert.Len(t, tmpl.Charts, 2) // 4. Instantiate from template - instance, err := c.InstantiateTemplate(1, &types.InstantiateTemplateRequest{ + instance, err := c.InstantiateTemplate("1", &types.InstantiateTemplateRequest{ Name: "my-web-app", Branch: "main", }) @@ -360,7 +377,7 @@ func TestTemplateWorkflow_QuickDeploy(t *testing.T) { c := client.New(server.URL) // Quick deploy creates and deploys in one step - instance, err := c.QuickDeployTemplate(1, &types.QuickDeployRequest{ + instance, err := c.QuickDeployTemplate("1", &types.QuickDeployRequest{ Name: "quick-web-app", Branch: "feature/xyz", }) @@ -382,12 +399,12 @@ func TestTemplateWorkflow_NotFound(t *testing.T) { c := client.New(server.URL) // Get non-existent template - _, err := c.GetTemplate(999) + _, err := c.GetTemplate("999") require.Error(t, err) assert.Contains(t, err.Error(), "template not found") // Instantiate non-existent template - _, err = c.InstantiateTemplate(999, &types.InstantiateTemplateRequest{Name: "test"}) + _, err = c.InstantiateTemplate("999", &types.InstantiateTemplateRequest{Name: "test"}) require.Error(t, err) assert.Contains(t, err.Error(), "template not found") } @@ -505,22 +522,22 @@ func TestDefinitionWorkflow_ErrorHandling(t *testing.T) { c := client.New(server.URL) // Get non-existent definition - _, err := c.GetDefinition(999) + _, err := c.GetDefinition("999") require.Error(t, err) assert.Contains(t, err.Error(), "definition not found") // Delete non-existent definition - err = c.DeleteDefinition(999) + err = c.DeleteDefinition("999") require.Error(t, err) assert.Contains(t, err.Error(), "definition not found") // Export non-existent definition - _, err = c.ExportDefinition(999) + _, err = c.ExportDefinition("999") require.Error(t, err) assert.Contains(t, err.Error(), "definition not found") // Update non-existent definition - _, err = c.UpdateDefinition(999, &types.UpdateDefinitionRequest{Name: "test"}) + _, err = c.UpdateDefinition("999", &types.UpdateDefinitionRequest{Name: "test"}) require.Error(t, err) assert.Contains(t, err.Error(), "definition not found") } @@ -550,7 +567,7 @@ func TestDefinitionWorkflow_MultipleDefinitions(t *testing.T) { assert.Equal(t, 3, resp.Total) // Delete one - err = c.DeleteDefinition(2) + err = c.DeleteDefinition("2") require.NoError(t, err) // List — should return 2 From 929274fcadfb9b096ca83f16125c009b31fb3e68 Mon Sep 17 00:00:00 2001 From: Olof Mattsson Date: Fri, 17 Apr 2026 21:41:44 +0200 Subject: [PATCH 3/3] address PR #32 review round 2: refresh-db cmd tests, finish UUID migration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - cmd/stack_test.go: add four command-level tests for `stack refresh-db` mirroring the `stack clean` coverage: - WithConfirmation (prompt accept with y/n) - Declined (n → API never called, "Aborted" printed) - WithYesFlag (--yes bypasses prompt) - QuietPrintsOnlyLogID (--quiet prints only the log ID, no human prefix) Also finish the pre-existing uint→string UUID migration that was still blocking CI and caused `go vet` to fail on main. These files were missed by earlier commits b4f3b61 / dc9dc34: - cmd/bulk_test.go, cluster_test.go, definition_test.go, login_test.go, override_test.go, stack_test.go, template_test.go: quote id literals, [uint]→[]string for bulk args, drop 30 obsolete TestParseID_Zero / *_InvalidID tests (parseID now only rejects empty strings). - cmd/override.go, cmd/stack.go: two %d → %s format strings that the earlier dc9dc34 fix-up missed; these caused `go vet` to fail. - test/integration/template_definition_integration_test.go: the mock server was doing `def.Version = def.Version + "+1"`, producing versions like "1+1". Switch to a strconv.Atoi/Itoa round-trip so updates increment realistically (1 → 2 → 3). Result: `go vet ./...` clean, `go build ./...` clean, `go test ./cmd/ ./pkg/... ./test/integration/` all pass. test/e2e/ still has pre-existing runtime failures (needs a live server) — unrelated. --- cli/cmd/bulk_test.go | 30 +- cli/cmd/cluster_test.go | 43 +-- cli/cmd/definition_test.go | 84 +---- cli/cmd/login_test.go | 18 +- cli/cmd/override.go | 10 +- cli/cmd/override_test.go | 211 +---------- cli/cmd/stack.go | 4 +- cli/cmd/stack_test.go | 346 ++++++++---------- cli/cmd/template_test.go | 82 +---- .../template_definition_integration_test.go | 5 +- 10 files changed, 234 insertions(+), 599 deletions(-) diff --git a/cli/cmd/bulk_test.go b/cli/cmd/bulk_test.go index 1116079..8db87ba 100644 --- a/cli/cmd/bulk_test.go +++ b/cli/cmd/bulk_test.go @@ -24,9 +24,9 @@ func setupBulkTestCmd(t *testing.T, apiURL string) *bytes.Buffer { func sampleBulkResponse() types.BulkResponse { return types.BulkResponse{ Results: []types.BulkOperationResult{ - {ID: 1, Success: true}, - {ID: 2, Success: true}, - {ID: 3, Success: false, Error: "not found"}, + {ID: "1", Success: true}, + {ID: "2", Success: true}, + {ID: "3", Success: false, Error: "not found"}, }, } } @@ -41,7 +41,7 @@ func TestBulkDeployCmd_TableOutput(t *testing.T) { var body types.BulkRequest require.NoError(t, json.NewDecoder(r.Body).Decode(&body)) - assert.Equal(t, []uint{1, 2, 3}, body.IDs) + assert.Equal(t, []string{"1", "2", "3"}, body.IDs) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) @@ -318,7 +318,7 @@ func TestParseBulkIDs_Valid(t *testing.T) { ids, err := parseBulkIDs(cmd, nil) require.NoError(t, err) - assert.Equal(t, []uint{1, 2, 3}, ids) + assert.Equal(t, []string{"1", "2", "3"}, ids) } func TestParseBulkIDs_InvalidID(t *testing.T) { @@ -402,7 +402,7 @@ func TestBulkDeployCmd_YAMLOutput(t *testing.T) { require.NoError(t, err) out := buf.String() - assert.Contains(t, out, "id: 1") + assert.Contains(t, out, "id: \"1\"") assert.Contains(t, out, "success: true") assert.Contains(t, out, "error: not found") } @@ -452,7 +452,7 @@ func TestBulkStopCmd_YAMLOutput(t *testing.T) { require.NoError(t, err) out := buf.String() - assert.Contains(t, out, "id: 1") + assert.Contains(t, out, "id: \"1\"") assert.Contains(t, out, "success: true") } @@ -521,7 +521,7 @@ func TestBulkCleanCmd_YAMLOutput(t *testing.T) { require.NoError(t, err) out := buf.String() - assert.Contains(t, out, "id: 1") + assert.Contains(t, out, "id: \"1\"") assert.Contains(t, out, "success: true") } @@ -550,7 +550,7 @@ func TestBulkDeleteCmd_YAMLOutput(t *testing.T) { require.NoError(t, err) out := buf.String() - assert.Contains(t, out, "id: 1") + assert.Contains(t, out, "id: \"1\"") assert.Contains(t, out, "success: true") } @@ -573,7 +573,7 @@ func TestParseBulkIDs_WhitespaceHandling(t *testing.T) { ids, err := parseBulkIDs(cmd, nil) require.NoError(t, err) - assert.Equal(t, []uint{1, 2, 3}, ids) + assert.Equal(t, []string{"1", "2", "3"}, ids) } func TestParseBulkIDs_NegativeID(t *testing.T) { @@ -594,7 +594,7 @@ func TestParseBulkIDs_PositionalArgs(t *testing.T) { ids, err := parseBulkIDs(cmd, []string{"1", "2", "3"}) require.NoError(t, err) - assert.Equal(t, []uint{1, 2, 3}, ids) + assert.Equal(t, []string{"1", "2", "3"}, ids) } func TestParseBulkIDs_MixedFlagAndPositional(t *testing.T) { @@ -604,7 +604,7 @@ func TestParseBulkIDs_MixedFlagAndPositional(t *testing.T) { ids, err := parseBulkIDs(cmd, []string{"3"}) require.NoError(t, err) - assert.Equal(t, []uint{1, 2, 3}, ids) + assert.Equal(t, []string{"1", "2", "3"}, ids) } func TestParseBulkIDs_MixedDedup(t *testing.T) { @@ -614,7 +614,7 @@ func TestParseBulkIDs_MixedDedup(t *testing.T) { ids, err := parseBulkIDs(cmd, []string{"2", "3"}) require.NoError(t, err) - assert.Equal(t, []uint{1, 2, 3}, ids) + assert.Equal(t, []string{"1", "2", "3"}, ids) } func TestBulkDeployCmd_PositionalArgs(t *testing.T) { @@ -624,7 +624,7 @@ func TestBulkDeployCmd_PositionalArgs(t *testing.T) { var body types.BulkRequest require.NoError(t, json.NewDecoder(r.Body).Decode(&body)) - assert.Equal(t, []uint{1, 2, 3}, body.IDs) + assert.Equal(t, []string{"1", "2", "3"}, body.IDs) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) @@ -647,7 +647,7 @@ func TestBulkDeployCmd_MixedArgs(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var body types.BulkRequest require.NoError(t, json.NewDecoder(r.Body).Decode(&body)) - assert.Equal(t, []uint{1, 2, 3}, body.IDs) + assert.Equal(t, []string{"1", "2", "3"}, body.IDs) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) diff --git a/cli/cmd/cluster_test.go b/cli/cmd/cluster_test.go index 131e3e1..1e323f5 100644 --- a/cli/cmd/cluster_test.go +++ b/cli/cmd/cluster_test.go @@ -22,7 +22,7 @@ func setupClusterTestCmd(t *testing.T, apiURL string) *bytes.Buffer { func sampleCluster() types.Cluster { now := time.Date(2025, 6, 15, 10, 0, 0, 0, time.UTC) return types.Cluster{ - Base: types.Base{ID: 1, CreatedAt: now, UpdatedAt: now, Version: 1}, + Base: types.Base{ID: "1", CreatedAt: now, UpdatedAt: now, Version: "1"}, Name: "dev-cluster", Description: "Development cluster", Status: "online", @@ -51,9 +51,7 @@ func TestClusterListCmd_TableOutput(t *testing.T) { require.Equal(t, http.MethodGet, r.Method) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(types.ListResponse[types.Cluster]{ - Data: []types.Cluster{cl}, Total: 1, Page: 1, PageSize: 20, TotalPages: 1, - }) + json.NewEncoder(w).Encode([]types.Cluster{cl}) })) defer server.Close() @@ -80,9 +78,7 @@ func TestClusterListCmd_JSONOutput(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(types.ListResponse[types.Cluster]{ - Data: []types.Cluster{cl}, Total: 1, Page: 1, PageSize: 20, TotalPages: 1, - }) + json.NewEncoder(w).Encode([]types.Cluster{cl}) })) defer server.Close() @@ -92,23 +88,21 @@ func TestClusterListCmd_JSONOutput(t *testing.T) { err := clusterListCmd.RunE(clusterListCmd, []string{}) require.NoError(t, err) - var result types.ListResponse[types.Cluster] + var result []types.Cluster require.NoError(t, json.Unmarshal(buf.Bytes(), &result)) - assert.Equal(t, 1, result.Total) - assert.Equal(t, "dev-cluster", result.Data[0].Name) + assert.Len(t, result, 1) + assert.Equal(t, "dev-cluster", result[0].Name) } func TestClusterListCmd_QuietOutput(t *testing.T) { c1 := sampleCluster() c2 := sampleCluster() - c2.ID = 2 + c2.ID = "2" c2.Name = "prod-cluster" server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(types.ListResponse[types.Cluster]{ - Data: []types.Cluster{c1, c2}, Total: 2, Page: 1, PageSize: 20, TotalPages: 1, - }) + json.NewEncoder(w).Encode([]types.Cluster{c1, c2}) })) defer server.Close() @@ -126,9 +120,7 @@ func TestClusterListCmd_YAMLOutput(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(types.ListResponse[types.Cluster]{ - Data: []types.Cluster{cl}, Total: 1, Page: 1, PageSize: 20, TotalPages: 1, - }) + json.NewEncoder(w).Encode([]types.Cluster{cl}) })) defer server.Close() @@ -147,9 +139,7 @@ func TestClusterListCmd_EmptyResult(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(types.ListResponse[types.Cluster]{ - Data: []types.Cluster{}, Total: 0, Page: 1, PageSize: 20, TotalPages: 0, - }) + json.NewEncoder(w).Encode([]types.Cluster{}) })) defer server.Close() @@ -336,19 +326,6 @@ func TestClusterGetCmd_JSONOutput_HealthUnavailable(t *testing.T) { assert.False(t, hasHealth, "health key should not be present when unavailable") } -func TestClusterGetCmd_InvalidID(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - t.Fatal("API should not be called for invalid ID") - })) - defer server.Close() - - _ = setupClusterTestCmd(t, server.URL) - - err := clusterGetCmd.RunE(clusterGetCmd, []string{"abc"}) - require.Error(t, err) - assert.Contains(t, err.Error(), "invalid ID") -} - func TestClusterGetCmd_NotFound(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") diff --git a/cli/cmd/definition_test.go b/cli/cmd/definition_test.go index a6c2501..1f9afe5 100644 --- a/cli/cmd/definition_test.go +++ b/cli/cmd/definition_test.go @@ -25,14 +25,14 @@ import ( func sampleDefinition() types.StackDefinition { now := time.Date(2025, 6, 15, 10, 0, 0, 0, time.UTC) return types.StackDefinition{ - Base: types.Base{ID: 5, CreatedAt: now, UpdatedAt: now, Version: 1}, + Base: types.Base{ID: "5", CreatedAt: now, UpdatedAt: now, Version: "1"}, Name: "api-service", Description: "API microservice stack", DefaultBranch: "main", Owner: "admin", Charts: []types.ChartConfig{ { - Base: types.Base{ID: 1}, + Base: types.Base{ID: "1"}, Name: "api", RepoURL: "https://charts.example.com", ChartName: "api-chart", @@ -119,7 +119,7 @@ func TestDefinitionListCmd_YAMLOutput(t *testing.T) { func TestDefinitionListCmd_QuietOutput(t *testing.T) { d1 := sampleDefinition() d2 := sampleDefinition() - d2.ID = 15 + d2.ID = "15" d2.Name = "second-def" server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") @@ -215,7 +215,7 @@ func TestDefinitionGetCmd_JSONOutput(t *testing.T) { var result types.StackDefinition require.NoError(t, json.Unmarshal(buf.Bytes(), &result)) - assert.Equal(t, uint(5), result.ID) + assert.Equal(t, "5", result.ID) assert.Equal(t, "api-service", result.Name) } @@ -235,30 +235,6 @@ func TestDefinitionGetCmd_QuietOutput(t *testing.T) { assert.Equal(t, "5\n", buf.String()) } -func TestDefinitionGetCmd_InvalidID(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - t.Fatal("API should not be called for invalid ID") - })) - defer server.Close() - - _ = setupStackTestCmd(t, server.URL) - err := definitionGetCmd.RunE(definitionGetCmd, []string{"abc"}) - require.Error(t, err) - assert.Contains(t, err.Error(), "invalid ID") -} - -func TestDefinitionGetCmd_ZeroID(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - t.Fatal("API should not be called for zero ID") - })) - defer server.Close() - - _ = setupStackTestCmd(t, server.URL) - err := definitionGetCmd.RunE(definitionGetCmd, []string{"0"}) - require.Error(t, err) - assert.Contains(t, err.Error(), "invalid ID") -} - func TestDefinitionGetCmd_NotFound(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") @@ -317,7 +293,7 @@ func TestDefinitionCreateCmd_WithDescription(t *testing.T) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) - json.NewEncoder(w).Encode(types.StackDefinition{Base: types.Base{ID: 20}, Name: "test-def"}) + json.NewEncoder(w).Encode(types.StackDefinition{Base: types.Base{ID: "20"}, Name: "test-def"}) })) defer server.Close() @@ -351,7 +327,7 @@ func TestDefinitionCreateCmd_WithFromFile(t *testing.T) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) - json.NewEncoder(w).Encode(types.StackDefinition{Base: types.Base{ID: 25}, Name: "file-def"}) + json.NewEncoder(w).Encode(types.StackDefinition{Base: types.Base{ID: "25"}, Name: "file-def"}) })) defer server.Close() @@ -412,7 +388,7 @@ func TestDefinitionCreateCmd_QuietOutput(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) - json.NewEncoder(w).Encode(types.StackDefinition{Base: types.Base{ID: 30}}) + json.NewEncoder(w).Encode(types.StackDefinition{Base: types.Base{ID: "30"}}) })) defer server.Close() @@ -479,7 +455,7 @@ func TestDefinitionUpdateCmd_WithFromFile(t *testing.T) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(types.StackDefinition{Base: types.Base{ID: 5}, Name: "file-update"}) + json.NewEncoder(w).Encode(types.StackDefinition{Base: types.Base{ID: "5"}, Name: "file-update"}) })) defer server.Close() @@ -497,18 +473,6 @@ func TestDefinitionUpdateCmd_WithFromFile(t *testing.T) { assert.Contains(t, buf.String(), "file-update") } -func TestDefinitionUpdateCmd_InvalidID(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - t.Fatal("API should not be called for invalid ID") - })) - defer server.Close() - - _ = setupStackTestCmd(t, server.URL) - err := definitionUpdateCmd.RunE(definitionUpdateCmd, []string{"abc"}) - require.Error(t, err) - assert.Contains(t, err.Error(), "invalid ID") -} - func TestDefinitionUpdateCmd_ServerError(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") @@ -602,22 +566,6 @@ func TestDefinitionDeleteCmd_Declined(t *testing.T) { assert.Contains(t, buf.String(), "Aborted") } -func TestDefinitionDeleteCmd_InvalidID(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - t.Fatal("API should not be called for invalid ID") - })) - defer server.Close() - - _ = setupStackTestCmd(t, server.URL) - - definitionDeleteCmd.Flags().Set("yes", "true") - t.Cleanup(func() { definitionDeleteCmd.Flags().Set("yes", "false") }) - - err := definitionDeleteCmd.RunE(definitionDeleteCmd, []string{"abc"}) - require.Error(t, err) - assert.Contains(t, err.Error(), "invalid ID") -} - func TestDefinitionDeleteCmd_NotFound(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") @@ -709,18 +657,6 @@ func TestDefinitionExportCmd_ToFile(t *testing.T) { assert.Contains(t, buf.String(), "Exported definition 5") } -func TestDefinitionExportCmd_InvalidID(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - t.Fatal("API should not be called for invalid ID") - })) - defer server.Close() - - _ = setupStackTestCmd(t, server.URL) - err := definitionExportCmd.RunE(definitionExportCmd, []string{"abc"}) - require.Error(t, err) - assert.Contains(t, err.Error(), "invalid ID") -} - func TestDefinitionExportCmd_NotFound(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") @@ -750,7 +686,7 @@ func TestDefinitionImportCmd_Success(t *testing.T) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) - json.NewEncoder(w).Encode(types.StackDefinition{Base: types.Base{ID: 50}, Name: "imported-def"}) + json.NewEncoder(w).Encode(types.StackDefinition{Base: types.Base{ID: "50"}, Name: "imported-def"}) })) defer server.Close() @@ -826,7 +762,7 @@ func TestDefinitionImportCmd_QuietOutput(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) - json.NewEncoder(w).Encode(types.StackDefinition{Base: types.Base{ID: 55}}) + json.NewEncoder(w).Encode(types.StackDefinition{Base: types.Base{ID: "55"}}) })) defer server.Close() diff --git a/cli/cmd/login_test.go b/cli/cmd/login_test.go index 2f45e9d..ce77fb2 100644 --- a/cli/cmd/login_test.go +++ b/cli/cmd/login_test.go @@ -67,7 +67,7 @@ func TestLoginCmd_WithFlags(t *testing.T) { json.NewEncoder(w).Encode(types.LoginResponse{ Token: "jwt-test-token", ExpiresAt: expiresAt.Format(time.RFC3339), - User: types.User{Base: types.Base{ID: 1}, Username: "admin", Role: "admin"}, + User: types.User{Base: types.Base{ID: "1"}, Username: "admin", Role: "admin"}, }) })) defer server.Close() @@ -348,7 +348,7 @@ func TestWhoamiCmd_TableOutput(t *testing.T) { require.Equal(t, "/api/v1/auth/me", r.URL.Path) w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(types.User{ - Base: types.Base{ID: 1, CreatedAt: createdAt}, + Base: types.Base{ID: "1", CreatedAt: createdAt}, Username: "admin", Role: "admin", }) @@ -373,7 +373,7 @@ func TestWhoamiCmd_JSONOutput(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(types.User{ - Base: types.Base{ID: 42}, + Base: types.Base{ID: "42"}, Username: "jsonuser", Role: "viewer", }) @@ -392,14 +392,14 @@ func TestWhoamiCmd_JSONOutput(t *testing.T) { require.NoError(t, json.Unmarshal(buf.Bytes(), &user)) assert.Equal(t, "jsonuser", user.Username) assert.Equal(t, "viewer", user.Role) - assert.Equal(t, uint(42), user.ID) + assert.Equal(t, "42", user.ID) } func TestWhoamiCmd_YAMLOutput(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(types.User{ - Base: types.Base{ID: 7}, + Base: types.Base{ID: "7"}, Username: "yamluser", Role: "operator", }) @@ -423,7 +423,7 @@ func TestWhoamiCmd_QuietOutput(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(types.User{ - Base: types.Base{ID: 1}, + Base: types.Base{ID: "1"}, Username: "quietuser", Role: "admin", }) @@ -477,7 +477,7 @@ func TestWhoamiCmd_TokenNotInOutput(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(types.User{ - Base: types.Base{ID: 1}, + Base: types.Base{ID: "1"}, Username: "admin", Role: "admin", }) @@ -537,7 +537,7 @@ func TestWhoamiCmd_OutputModes(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(types.User{ - Base: types.Base{ID: 1, CreatedAt: createdAt}, + Base: types.Base{ID: "1", CreatedAt: createdAt}, Username: "admin", Role: "admin", }) @@ -613,7 +613,7 @@ func TestLoginCmd_EmptyTokenFromServer(t *testing.T) { json.NewEncoder(w).Encode(types.LoginResponse{ Token: "", ExpiresAt: "2030-01-01T00:00:00Z", - User: types.User{Base: types.Base{ID: 1}, Username: "test", Role: "admin"}, + User: types.User{Base: types.Base{ID: "1"}, Username: "test", Role: "admin"}, }) })) defer server.Close() diff --git a/cli/cmd/override.go b/cli/cmd/override.go index d02aab3..dc5bee5 100644 --- a/cli/cmd/override.go +++ b/cli/cmd/override.go @@ -172,7 +172,7 @@ Examples: case output.FormatYAML: return printer.PrintYAML(override) default: - printer.PrintMessage("Set value override for chart %d on instance %d", chartID, instanceID) + printer.PrintMessage("Set value override for chart %s on instance %s", chartID, instanceID) return nil } }, @@ -304,7 +304,7 @@ Examples: case output.FormatYAML: return printer.PrintYAML(override) default: - printer.PrintMessage("Set branch override %q for chart %d on instance %d", branch, chartID, instanceID) + printer.PrintMessage("Set branch override %q for chart %s on instance %s", branch, chartID, instanceID) return nil } }, @@ -441,7 +441,7 @@ Examples: case output.FormatYAML: return printer.PrintYAML(quota) default: - printer.PrintMessage("Set quota override for instance %d", instanceID) + printer.PrintMessage("Set quota override for instance %s", instanceID) return nil } }, @@ -489,7 +489,7 @@ Examples: return nil } - printer.PrintMessage("Deleted quota override for instance %d", instanceID) + printer.PrintMessage("Deleted quota override for instance %s", instanceID) return nil }, } @@ -527,7 +527,7 @@ func deleteChartOverride(cmd *cobra.Command, args []string, kind string, deleteF return nil } - printer.PrintMessage("Deleted %s override for chart %d on instance %d", kind, chartID, instanceID) + printer.PrintMessage("Deleted %s override for chart %s on instance %s", kind, chartID, instanceID) return nil } diff --git a/cli/cmd/override_test.go b/cli/cmd/override_test.go index 09c698b..42c605a 100644 --- a/cli/cmd/override_test.go +++ b/cli/cmd/override_test.go @@ -25,9 +25,9 @@ import ( func sampleValueOverride() types.ValueOverride { now := time.Date(2025, 6, 15, 10, 0, 0, 0, time.UTC) return types.ValueOverride{ - Base: types.Base{ID: 1, CreatedAt: now, UpdatedAt: now, Version: 1}, - InstanceID: 42, - ChartID: 1, + Base: types.Base{ID: "1", CreatedAt: now, UpdatedAt: now, Version: "1"}, + InstanceID: "42", + ChartID: "1", Values: `{"replicas":3}`, } } @@ -36,9 +36,9 @@ func sampleValueOverride() types.ValueOverride { func sampleBranchOverride() types.BranchOverride { now := time.Date(2025, 6, 15, 10, 0, 0, 0, time.UTC) return types.BranchOverride{ - Base: types.Base{ID: 2, CreatedAt: now, UpdatedAt: now, Version: 1}, - InstanceID: 42, - ChartID: 1, + Base: types.Base{ID: "2", CreatedAt: now, UpdatedAt: now, Version: "1"}, + InstanceID: "42", + ChartID: "1", Branch: "feature/my-branch", } } @@ -46,7 +46,7 @@ func sampleBranchOverride() types.BranchOverride { // sampleQuotaOverride returns a QuotaOverride used across override tests. func sampleQuotaOverride() types.QuotaOverride { return types.QuotaOverride{ - InstanceID: 42, + InstanceID: "42", CPURequest: "100m", CPULimit: "500m", MemRequest: "128Mi", @@ -111,8 +111,8 @@ func TestOverrideListCmd_JSONOutput(t *testing.T) { var result []types.ValueOverride require.NoError(t, json.Unmarshal(buf.Bytes(), &result)) require.Len(t, result, 1) - assert.Equal(t, uint(1), result[0].ChartID) - assert.Equal(t, uint(42), result[0].InstanceID) + assert.Equal(t, "1", result[0].ChartID) + assert.Equal(t, "42", result[0].InstanceID) } func TestOverrideListCmd_YAMLOutput(t *testing.T) { @@ -130,14 +130,14 @@ func TestOverrideListCmd_YAMLOutput(t *testing.T) { require.NoError(t, err) out := buf.String() - assert.Contains(t, out, "instance_id: 42") - assert.Contains(t, out, "chart_id: 1") + assert.Contains(t, out, "instance_id: \"42\"") + assert.Contains(t, out, "chart_id: \"1\"") } func TestOverrideListCmd_QuietOutput(t *testing.T) { o1 := sampleValueOverride() o2 := sampleValueOverride() - o2.ChartID = 3 + o2.ChartID = "3" server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) @@ -170,30 +170,6 @@ func TestOverrideListCmd_EmptyList(t *testing.T) { assert.Contains(t, out, "CHART ID") } -func TestOverrideListCmd_InvalidID(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - t.Fatal("API should not be called for invalid ID") - })) - defer server.Close() - - _ = setupStackTestCmd(t, server.URL) - err := overrideListCmd.RunE(overrideListCmd, []string{"abc"}) - require.Error(t, err) - assert.Contains(t, err.Error(), "invalid ID") -} - -func TestOverrideListCmd_ZeroID(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - t.Fatal("API should not be called for zero ID") - })) - defer server.Close() - - _ = setupStackTestCmd(t, server.URL) - err := overrideListCmd.RunE(overrideListCmd, []string{"0"}) - require.Error(t, err) - assert.Contains(t, err.Error(), "invalid ID") -} - func TestOverrideListCmd_ServerError(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") @@ -411,38 +387,6 @@ func TestOverrideSetCmd_InvalidSetFormat(t *testing.T) { assert.Contains(t, err.Error(), "invalid --set format") } -func TestOverrideSetCmd_InvalidInstanceID(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - t.Fatal("API should not be called for invalid ID") - })) - defer server.Close() - - _ = setupStackTestCmd(t, server.URL) - - overrideSetCmd.Flags().Set("set", "key=val") - t.Cleanup(func() { resetOverrideSetFlags(t) }) - - err := overrideSetCmd.RunE(overrideSetCmd, []string{"abc", "1"}) - require.Error(t, err) - assert.Contains(t, err.Error(), "invalid ID") -} - -func TestOverrideSetCmd_InvalidChartID(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - t.Fatal("API should not be called for invalid ID") - })) - defer server.Close() - - _ = setupStackTestCmd(t, server.URL) - - overrideSetCmd.Flags().Set("set", "key=val") - t.Cleanup(func() { resetOverrideSetFlags(t) }) - - err := overrideSetCmd.RunE(overrideSetCmd, []string{"42", "bad"}) - require.Error(t, err) - assert.Contains(t, err.Error(), "invalid ID") -} - func TestOverrideSetCmd_JSONOutput(t *testing.T) { override := sampleValueOverride() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -463,7 +407,7 @@ func TestOverrideSetCmd_JSONOutput(t *testing.T) { var result types.ValueOverride require.NoError(t, json.Unmarshal(buf.Bytes(), &result)) - assert.Equal(t, uint(1), result.ChartID) + assert.Equal(t, "1", result.ChartID) } func TestOverrideSetCmd_YAMLOutput(t *testing.T) { @@ -485,7 +429,7 @@ func TestOverrideSetCmd_YAMLOutput(t *testing.T) { require.NoError(t, err) out := buf.String() - assert.Contains(t, out, "chart_id: 1") + assert.Contains(t, out, "chart_id: \"1\"") } func TestOverrideSetCmd_QuietOutput(t *testing.T) { @@ -615,30 +559,6 @@ func TestOverrideDeleteCmd_QuietOutput(t *testing.T) { assert.Equal(t, "1\n", buf.String()) } -func TestOverrideDeleteCmd_InvalidID(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - t.Fatal("API should not be called for invalid ID") - })) - defer server.Close() - - _ = setupStackTestCmd(t, server.URL) - err := overrideDeleteCmd.RunE(overrideDeleteCmd, []string{"abc", "1"}) - require.Error(t, err) - assert.Contains(t, err.Error(), "invalid ID") -} - -func TestOverrideDeleteCmd_InvalidChartID(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - t.Fatal("API should not be called for invalid ID") - })) - defer server.Close() - - _ = setupStackTestCmd(t, server.URL) - err := overrideDeleteCmd.RunE(overrideDeleteCmd, []string{"42", "bad"}) - require.Error(t, err) - assert.Contains(t, err.Error(), "invalid ID") -} - func TestOverrideDeleteCmd_ServerError(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") @@ -740,7 +660,7 @@ func TestOverrideBranchListCmd_YAMLOutput(t *testing.T) { func TestOverrideBranchListCmd_QuietOutput(t *testing.T) { o1 := sampleBranchOverride() o2 := sampleBranchOverride() - o2.ChartID = 5 + o2.ChartID = "5" server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) @@ -757,18 +677,6 @@ func TestOverrideBranchListCmd_QuietOutput(t *testing.T) { assert.Equal(t, "1\n5", lines) } -func TestOverrideBranchListCmd_InvalidID(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - t.Fatal("API should not be called for invalid ID") - })) - defer server.Close() - - _ = setupStackTestCmd(t, server.URL) - err := overrideBranchListCmd.RunE(overrideBranchListCmd, []string{"xyz"}) - require.Error(t, err) - assert.Contains(t, err.Error(), "invalid ID") -} - func TestOverrideBranchListCmd_ServerError(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") @@ -863,30 +771,6 @@ func TestOverrideBranchSetCmd_QuietOutput(t *testing.T) { assert.Equal(t, "1\n", buf.String()) } -func TestOverrideBranchSetCmd_InvalidInstanceID(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - t.Fatal("API should not be called for invalid ID") - })) - defer server.Close() - - _ = setupStackTestCmd(t, server.URL) - err := overrideBranchSetCmd.RunE(overrideBranchSetCmd, []string{"abc", "1", "main"}) - require.Error(t, err) - assert.Contains(t, err.Error(), "invalid ID") -} - -func TestOverrideBranchSetCmd_InvalidChartID(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - t.Fatal("API should not be called for invalid ID") - })) - defer server.Close() - - _ = setupStackTestCmd(t, server.URL) - err := overrideBranchSetCmd.RunE(overrideBranchSetCmd, []string{"42", "bad", "main"}) - require.Error(t, err) - assert.Contains(t, err.Error(), "invalid ID") -} - func TestOverrideBranchSetCmd_ServerError(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") @@ -990,18 +874,6 @@ func TestOverrideBranchDeleteCmd_QuietOutput(t *testing.T) { assert.Equal(t, "1\n", buf.String()) } -func TestOverrideBranchDeleteCmd_InvalidID(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - t.Fatal("API should not be called for invalid ID") - })) - defer server.Close() - - _ = setupStackTestCmd(t, server.URL) - err := overrideBranchDeleteCmd.RunE(overrideBranchDeleteCmd, []string{"abc", "1"}) - require.Error(t, err) - assert.Contains(t, err.Error(), "invalid ID") -} - func TestOverrideBranchDeleteCmd_ServerError(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") @@ -1061,7 +933,7 @@ func TestOverrideQuotaGetCmd_JSONOutput(t *testing.T) { var result types.QuotaOverride require.NoError(t, json.Unmarshal(buf.Bytes(), &result)) - assert.Equal(t, uint(42), result.InstanceID) + assert.Equal(t, "42", result.InstanceID) assert.Equal(t, "100m", result.CPURequest) } @@ -1080,7 +952,7 @@ func TestOverrideQuotaGetCmd_YAMLOutput(t *testing.T) { require.NoError(t, err) out := buf.String() - assert.Contains(t, out, "instance_id: 42") + assert.Contains(t, out, "instance_id: \"42\"") assert.Contains(t, out, "cpu_request: 100m") } @@ -1100,18 +972,6 @@ func TestOverrideQuotaGetCmd_QuietOutput(t *testing.T) { assert.Equal(t, "42\n", buf.String()) } -func TestOverrideQuotaGetCmd_InvalidID(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - t.Fatal("API should not be called for invalid ID") - })) - defer server.Close() - - _ = setupStackTestCmd(t, server.URL) - err := overrideQuotaGetCmd.RunE(overrideQuotaGetCmd, []string{"abc"}) - require.Error(t, err) - assert.Contains(t, err.Error(), "invalid ID") -} - func TestOverrideQuotaGetCmd_ServerError(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") @@ -1284,7 +1144,7 @@ func TestOverrideQuotaSetCmd_JSONOutput(t *testing.T) { var result types.QuotaOverride require.NoError(t, json.Unmarshal(buf.Bytes(), &result)) - assert.Equal(t, uint(42), result.InstanceID) + assert.Equal(t, "42", result.InstanceID) } func TestOverrideQuotaSetCmd_YAMLOutput(t *testing.T) { @@ -1309,7 +1169,7 @@ func TestOverrideQuotaSetCmd_YAMLOutput(t *testing.T) { err := overrideQuotaSetCmd.RunE(overrideQuotaSetCmd, []string{"42"}) require.NoError(t, err) - assert.Contains(t, buf.String(), "instance_id: 42") + assert.Contains(t, buf.String(), "instance_id: \"42\"") } func TestOverrideQuotaSetCmd_QuietOutput(t *testing.T) { @@ -1337,27 +1197,6 @@ func TestOverrideQuotaSetCmd_QuietOutput(t *testing.T) { assert.Equal(t, "42\n", buf.String()) } -func TestOverrideQuotaSetCmd_InvalidID(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - t.Fatal("API should not be called for invalid ID") - })) - defer server.Close() - - _ = setupStackTestCmd(t, server.URL) - - overrideQuotaSetCmd.Flags().Set("cpu-request", "100m") - t.Cleanup(func() { - overrideQuotaSetCmd.Flags().Set("cpu-request", "") - overrideQuotaSetCmd.Flags().Set("cpu-limit", "") - overrideQuotaSetCmd.Flags().Set("memory-request", "") - overrideQuotaSetCmd.Flags().Set("memory-limit", "") - }) - - err := overrideQuotaSetCmd.RunE(overrideQuotaSetCmd, []string{"abc"}) - require.Error(t, err) - assert.Contains(t, err.Error(), "invalid ID") -} - func TestOverrideQuotaSetCmd_ServerError(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") @@ -1470,18 +1309,6 @@ func TestOverrideQuotaDeleteCmd_QuietOutput(t *testing.T) { assert.Equal(t, "42\n", buf.String()) } -func TestOverrideQuotaDeleteCmd_InvalidID(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - t.Fatal("API should not be called for invalid ID") - })) - defer server.Close() - - _ = setupStackTestCmd(t, server.URL) - err := overrideQuotaDeleteCmd.RunE(overrideQuotaDeleteCmd, []string{"abc"}) - require.Error(t, err) - assert.Contains(t, err.Error(), "invalid ID") -} - func TestOverrideQuotaDeleteCmd_ServerError(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") diff --git a/cli/cmd/stack.go b/cli/cmd/stack.go index f029882..300e0cd 100644 --- a/cli/cmd/stack.go +++ b/cli/cmd/stack.go @@ -637,7 +637,7 @@ Examples: } if leftID == rightID { - return fmt.Errorf("cannot compare an instance with itself (both IDs are %d)", leftID) + return fmt.Errorf("cannot compare an instance with itself (both IDs are %s)", leftID) } c, err := newClient() @@ -678,7 +678,7 @@ Examples: } } if len(rows) == 0 { - printer.PrintMessage("No differences found between stack %d and %d", leftID, rightID) + printer.PrintMessage("No differences found between stack %s and %s", leftID, rightID) return nil } return printer.PrintTable(headers, rows) diff --git a/cli/cmd/stack_test.go b/cli/cmd/stack_test.go index d3bb792..1be5376 100644 --- a/cli/cmd/stack_test.go +++ b/cli/cmd/stack_test.go @@ -48,12 +48,12 @@ func setupStackTestCmd(t *testing.T, apiURL string) *bytes.Buffer { // sampleStackJSON returns a StackInstance object used across many tests. func sampleStack() types.StackInstance { - clusterID := uint(1) + clusterID := "1" now := time.Date(2025, 6, 15, 10, 0, 0, 0, time.UTC) return types.StackInstance{ - Base: types.Base{ID: 42, CreatedAt: now, UpdatedAt: now, Version: 1}, + Base: types.Base{ID: "42", CreatedAt: now, UpdatedAt: now, Version: "1"}, Name: "my-stack", - StackDefinitionID: 5, + StackDefinitionID: "5", DefinitionName: "api-service", Owner: "admin", Branch: "main", @@ -122,7 +122,7 @@ func TestStackListCmd_JSONOutput(t *testing.T) { func TestStackListCmd_QuietOutput(t *testing.T) { s1 := sampleStack() s2 := sampleStack() - s2.ID = 99 + s2.ID = "99" server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) @@ -248,22 +248,10 @@ func TestStackGetCmd_JSONOutput(t *testing.T) { var result types.StackInstance require.NoError(t, json.Unmarshal(buf.Bytes(), &result)) - assert.Equal(t, uint(42), result.ID) + assert.Equal(t, "42", result.ID) assert.Equal(t, "my-stack", result.Name) } -func TestStackGetCmd_InvalidID(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - t.Fatal("API should not be called for invalid ID") - })) - defer server.Close() - - _ = setupStackTestCmd(t, server.URL) - err := stackGetCmd.RunE(stackGetCmd, []string{"abc"}) - require.Error(t, err) - assert.Contains(t, err.Error(), "invalid ID") -} - func TestStackGetCmd_NotFound(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") @@ -289,7 +277,7 @@ func TestStackCreateCmd_Success(t *testing.T) { var body types.CreateStackRequest require.NoError(t, json.NewDecoder(r.Body).Decode(&body)) assert.Equal(t, "my-stack", body.Name) - assert.Equal(t, uint(5), body.StackDefinitionID) + assert.Equal(t, "5", body.StackDefinitionID) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) @@ -335,14 +323,14 @@ func TestStackCreateCmd_AllFlags(t *testing.T) { var body types.CreateStackRequest require.NoError(t, json.NewDecoder(r.Body).Decode(&body)) assert.Equal(t, "feat-stack", body.Name) - assert.Equal(t, uint(3), body.StackDefinitionID) + assert.Equal(t, "3", body.StackDefinitionID) assert.Equal(t, "feature/xyz", body.Branch) - assert.Equal(t, uint(2), body.ClusterID) + assert.Equal(t, "2", body.ClusterID) assert.Equal(t, 120, body.TTLMinutes) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) - json.NewEncoder(w).Encode(types.StackInstance{Base: types.Base{ID: 50}, Name: "feat-stack"}) + json.NewEncoder(w).Encode(types.StackInstance{Base: types.Base{ID: "50"}, Name: "feat-stack"}) })) defer server.Close() @@ -377,7 +365,7 @@ func TestStackDeployCmd_Success(t *testing.T) { require.Equal(t, http.MethodPost, r.Method) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(types.DeploymentLog{Base: types.Base{ID: 100}, InstanceID: 42, Action: "deploy", Status: "started"}) + json.NewEncoder(w).Encode(types.DeploymentLog{Base: types.Base{ID: "100"}, InstanceID: "42", Action: "deploy", Status: "started"}) })) defer server.Close() @@ -394,7 +382,7 @@ func TestStackDeployCmd_QuietOutput(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(types.DeploymentLog{Base: types.Base{ID: 100}}) + json.NewEncoder(w).Encode(types.DeploymentLog{Base: types.Base{ID: "100"}}) })) defer server.Close() @@ -413,7 +401,7 @@ func TestStackStopCmd_Success(t *testing.T) { require.Equal(t, http.MethodPost, r.Method) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(types.DeploymentLog{Base: types.Base{ID: 101}, InstanceID: 42, Action: "stop", Status: "started"}) + json.NewEncoder(w).Encode(types.DeploymentLog{Base: types.Base{ID: "101"}, InstanceID: "42", Action: "stop", Status: "started"}) })) defer server.Close() @@ -435,7 +423,7 @@ func TestStackCleanCmd_WithConfirmation(t *testing.T) { require.Equal(t, "/api/v1/stack-instances/42/clean", r.URL.Path) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(types.DeploymentLog{Base: types.Base{ID: 102}}) + json.NewEncoder(w).Encode(types.DeploymentLog{Base: types.Base{ID: "102"}}) })) defer server.Close() @@ -486,7 +474,7 @@ func TestStackCleanCmd_WithYesFlag(t *testing.T) { called = true w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(types.DeploymentLog{Base: types.Base{ID: 103}}) + json.NewEncoder(w).Encode(types.DeploymentLog{Base: types.Base{ID: "103"}}) })) defer server.Close() @@ -501,6 +489,112 @@ func TestStackCleanCmd_WithYesFlag(t *testing.T) { assert.Contains(t, buf.String(), "Cleaning stack 42") } +// ---------- stack refresh-db (destructive) ---------- + +func TestStackRefreshDBCmd_WithConfirmation(t *testing.T) { + called := false + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + called = true + require.Equal(t, http.MethodPost, r.Method) + require.Equal(t, "/api/v1/stack-instances/42/refresh-db", r.URL.Path) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + json.NewEncoder(w).Encode(types.DeploymentLog{Base: types.Base{ID: "log-104"}}) + })) + defer server.Close() + + buf := setupStackTestCmd(t, server.URL) + + stackRefreshDBCmd.Flags().Set("yes", "false") + t.Cleanup(func() { + stackRefreshDBCmd.Flags().Set("yes", "false") + stackRefreshDBCmd.SetIn(nil) + stackRefreshDBCmd.SetErr(nil) + }) + + stackRefreshDBCmd.SetIn(strings.NewReader("y\n")) + stackRefreshDBCmd.SetErr(&bytes.Buffer{}) + + err := stackRefreshDBCmd.RunE(stackRefreshDBCmd, []string{"42"}) + require.NoError(t, err) + assert.True(t, called, "API should be called after confirming with y") + assert.Contains(t, buf.String(), "Refreshing database for stack 42") + assert.Contains(t, buf.String(), "log-104") +} + +func TestStackRefreshDBCmd_Declined(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Fatal("API should NOT be called when user declines") + })) + defer server.Close() + + buf := setupStackTestCmd(t, server.URL) + + stackRefreshDBCmd.Flags().Set("yes", "false") + t.Cleanup(func() { + stackRefreshDBCmd.Flags().Set("yes", "false") + stackRefreshDBCmd.SetIn(nil) + stackRefreshDBCmd.SetErr(nil) + }) + + stackRefreshDBCmd.SetIn(strings.NewReader("n\n")) + stackRefreshDBCmd.SetErr(&bytes.Buffer{}) + + err := stackRefreshDBCmd.RunE(stackRefreshDBCmd, []string{"42"}) + require.NoError(t, err) + assert.Contains(t, buf.String(), "Aborted") +} + +func TestStackRefreshDBCmd_WithYesFlag(t *testing.T) { + called := false + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + called = true + require.Equal(t, http.MethodPost, r.Method) + require.Equal(t, "/api/v1/stack-instances/42/refresh-db", r.URL.Path) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + json.NewEncoder(w).Encode(types.DeploymentLog{Base: types.Base{ID: "log-105"}}) + })) + defer server.Close() + + buf := setupStackTestCmd(t, server.URL) + + stackRefreshDBCmd.Flags().Set("yes", "true") + t.Cleanup(func() { stackRefreshDBCmd.Flags().Set("yes", "false") }) + + err := stackRefreshDBCmd.RunE(stackRefreshDBCmd, []string{"42"}) + require.NoError(t, err) + assert.True(t, called, "API should be called with --yes flag") + assert.Contains(t, buf.String(), "Refreshing database for stack 42") +} + +func TestStackRefreshDBCmd_QuietPrintsOnlyLogID(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + json.NewEncoder(w).Encode(types.DeploymentLog{Base: types.Base{ID: "log-quiet-106"}}) + })) + defer server.Close() + + buf := setupStackTestCmd(t, server.URL) + + stackRefreshDBCmd.Flags().Set("yes", "true") + prevQuiet := printer.Quiet + printer.Quiet = true + t.Cleanup(func() { + stackRefreshDBCmd.Flags().Set("yes", "false") + printer.Quiet = prevQuiet + }) + + err := stackRefreshDBCmd.RunE(stackRefreshDBCmd, []string{"42"}) + require.NoError(t, err) + out := buf.String() + assert.Contains(t, out, "log-quiet-106") + // Quiet mode must not print the human-readable prefix. + assert.NotContains(t, out, "Refreshing database for stack") + assert.NotContains(t, out, "Run 'stackctl stack logs") +} + // ---------- stack delete (destructive) ---------- func TestStackDeleteCmd_WithConfirmation(t *testing.T) { @@ -655,8 +749,8 @@ func TestStackLogsCmd_Success(t *testing.T) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(types.DeploymentLog{ - Base: types.Base{ID: 200}, - InstanceID: 42, + Base: types.Base{ID: "200"}, + InstanceID: "42", Action: "deploy", Status: "completed", Output: "Deployment succeeded.\nAll charts installed.", @@ -677,8 +771,8 @@ func TestStackLogsCmd_Success(t *testing.T) { func TestStackLogsCmd_JSONOutput(t *testing.T) { logEntry := types.DeploymentLog{ - Base: types.Base{ID: 200}, - InstanceID: 42, + Base: types.Base{ID: "200"}, + InstanceID: "42", Action: "deploy", Status: "completed", Output: "OK", @@ -697,7 +791,7 @@ func TestStackLogsCmd_JSONOutput(t *testing.T) { var result types.DeploymentLog require.NoError(t, json.Unmarshal(buf.Bytes(), &result)) - assert.Equal(t, uint(200), result.ID) + assert.Equal(t, "200", result.ID) assert.Equal(t, "deploy", result.Action) } @@ -709,7 +803,7 @@ func TestStackCloneCmd_Success(t *testing.T) { require.Equal(t, http.MethodPost, r.Method) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) - json.NewEncoder(w).Encode(types.StackInstance{Base: types.Base{ID: 55}, Name: "my-stack-clone"}) + json.NewEncoder(w).Encode(types.StackInstance{Base: types.Base{ID: "55"}, Name: "my-stack-clone"}) })) defer server.Close() @@ -726,7 +820,7 @@ func TestStackCloneCmd_QuietOutput(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) - json.NewEncoder(w).Encode(types.StackInstance{Base: types.Base{ID: 55}}) + json.NewEncoder(w).Encode(types.StackInstance{Base: types.Base{ID: "55"}}) })) defer server.Close() @@ -751,7 +845,7 @@ func TestStackExtendCmd_Success(t *testing.T) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(types.StackInstance{Base: types.Base{ID: 42}, TTLMinutes: 120}) + json.NewEncoder(w).Encode(types.StackInstance{Base: types.Base{ID: "42"}, TTLMinutes: 120}) })) defer server.Close() @@ -813,18 +907,6 @@ func TestStackStopCmd_NotFound(t *testing.T) { assert.Contains(t, err.Error(), "stack not found") } -func TestStackCloneCmd_InvalidID(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - t.Fatal("API should not be called for invalid ID") - })) - defer server.Close() - - _ = setupStackTestCmd(t, server.URL) - err := stackCloneCmd.RunE(stackCloneCmd, []string{"not-a-number"}) - require.Error(t, err) - assert.Contains(t, err.Error(), "invalid ID") -} - func TestStackStatusCmd_NoPods(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") @@ -846,7 +928,7 @@ func TestStackLogsCmd_QuietOutput(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(types.DeploymentLog{Base: types.Base{ID: 200}}) + json.NewEncoder(w).Encode(types.DeploymentLog{Base: types.Base{ID: "200"}}) })) defer server.Close() @@ -909,7 +991,7 @@ func TestStackStopCmd_QuietOutput(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(types.DeploymentLog{Base: types.Base{ID: 101}}) + json.NewEncoder(w).Encode(types.DeploymentLog{Base: types.Base{ID: "101"}}) })) defer server.Close() @@ -924,7 +1006,7 @@ func TestStackExtendCmd_QuietOutput(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(types.StackInstance{Base: types.Base{ID: 42}}) + json.NewEncoder(w).Encode(types.StackInstance{Base: types.Base{ID: "42"}}) })) defer server.Close() @@ -943,7 +1025,7 @@ func TestStackCleanCmd_QuietOutput(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(types.DeploymentLog{Base: types.Base{ID: 102}}) + json.NewEncoder(w).Encode(types.DeploymentLog{Base: types.Base{ID: "102"}}) })) defer server.Close() @@ -1026,7 +1108,7 @@ func TestStackLogsCmd_YAMLOutput(t *testing.T) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(types.DeploymentLog{ - Base: types.Base{ID: 200}, Action: "deploy", Status: "completed", Output: "OK", + Base: types.Base{ID: "200"}, Action: "deploy", Status: "completed", Output: "OK", }) })) defer server.Close() @@ -1041,12 +1123,6 @@ func TestStackLogsCmd_YAMLOutput(t *testing.T) { assert.Contains(t, out, "status: completed") } -func TestParseID_Zero(t *testing.T) { - _, err := parseID("0") - require.Error(t, err) - assert.Contains(t, err.Error(), "invalid ID") -} - // ========== Additional coverage tests ========== // ---------- stack clean: error cases ---------- @@ -1069,18 +1145,6 @@ func TestStackCleanCmd_ServerError(t *testing.T) { assert.Contains(t, err.Error(), "backend failure") } -func TestStackCleanCmd_InvalidID(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - t.Fatal("API should not be called for invalid ID") - })) - defer server.Close() - - _ = setupStackTestCmd(t, server.URL) - err := stackCleanCmd.RunE(stackCleanCmd, []string{"bad"}) - require.Error(t, err) - assert.Contains(t, err.Error(), "invalid ID") -} - func TestStackCleanCmd_NotFound(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") @@ -1101,18 +1165,6 @@ func TestStackCleanCmd_NotFound(t *testing.T) { // ---------- stack delete: additional error cases ---------- -func TestStackDeleteCmd_InvalidID(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - t.Fatal("API should not be called for invalid ID") - })) - defer server.Close() - - _ = setupStackTestCmd(t, server.URL) - err := stackDeleteCmd.RunE(stackDeleteCmd, []string{"xyz"}) - require.Error(t, err) - assert.Contains(t, err.Error(), "invalid ID") -} - func TestStackDeleteCmd_ServerError(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") @@ -1179,22 +1231,6 @@ func TestStackExtendCmd_NegativeMinutes(t *testing.T) { assert.Contains(t, err.Error(), "--minutes must be a positive integer") } -func TestStackExtendCmd_InvalidID(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - t.Fatal("API should not be called for invalid ID") - })) - defer server.Close() - - _ = setupStackTestCmd(t, server.URL) - - stackExtendCmd.Flags().Set("minutes", "60") - t.Cleanup(func() { stackExtendCmd.Flags().Set("minutes", "0") }) - - err := stackExtendCmd.RunE(stackExtendCmd, []string{"not-a-number"}) - require.Error(t, err) - assert.Contains(t, err.Error(), "invalid ID") -} - func TestStackExtendCmd_ServerError(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") @@ -1229,18 +1265,6 @@ func TestStackStatusCmd_NotFound(t *testing.T) { assert.Contains(t, err.Error(), "stack not found") } -func TestStackStatusCmd_InvalidID(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - t.Fatal("API should not be called for invalid ID") - })) - defer server.Close() - - _ = setupStackTestCmd(t, server.URL) - err := stackStatusCmd.RunE(stackStatusCmd, []string{"abc"}) - require.Error(t, err) - assert.Contains(t, err.Error(), "invalid ID") -} - // ---------- stack logs: additional tests ---------- func TestStackLogsCmd_NotFound(t *testing.T) { @@ -1257,18 +1281,6 @@ func TestStackLogsCmd_NotFound(t *testing.T) { assert.Contains(t, err.Error(), "no logs found") } -func TestStackLogsCmd_InvalidID(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - t.Fatal("API should not be called for invalid ID") - })) - defer server.Close() - - _ = setupStackTestCmd(t, server.URL) - err := stackLogsCmd.RunE(stackLogsCmd, []string{"abc"}) - require.Error(t, err) - assert.Contains(t, err.Error(), "invalid ID") -} - // ---------- stack list: additional filter tests ---------- func TestStackListCmd_PageAndPageSize(t *testing.T) { @@ -1331,9 +1343,9 @@ func TestStackListCmd_ServerError(t *testing.T) { // ---------- stack list: table with no cluster name (uses cluster ID) ---------- func TestStackListCmd_ClusterIDFallback(t *testing.T) { - clusterID := uint(5) + clusterID := "5" stack := types.StackInstance{ - Base: types.Base{ID: 10}, + Base: types.Base{ID: "10"}, Name: "no-cluster-name", Status: "running", ClusterID: &clusterID, @@ -1383,32 +1395,8 @@ func TestStackCreateCmd_NegativeTTL(t *testing.T) { // ---------- stack deploy: invalid ID ---------- -func TestStackDeployCmd_InvalidID(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - t.Fatal("API should not be called for invalid ID") - })) - defer server.Close() - - _ = setupStackTestCmd(t, server.URL) - err := stackDeployCmd.RunE(stackDeployCmd, []string{"not-valid"}) - require.Error(t, err) - assert.Contains(t, err.Error(), "invalid ID") -} - // ---------- stack stop: additional tests ---------- -func TestStackStopCmd_InvalidID(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - t.Fatal("API should not be called for invalid ID") - })) - defer server.Close() - - _ = setupStackTestCmd(t, server.URL) - err := stackStopCmd.RunE(stackStopCmd, []string{"abc"}) - require.Error(t, err) - assert.Contains(t, err.Error(), "invalid ID") -} - func TestStackStopCmd_ServerError(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") @@ -1427,7 +1415,7 @@ func TestStackStopCmd_ServerError(t *testing.T) { func TestStackValuesCmd_JSONOutput(t *testing.T) { values := types.MergedValues{ - InstanceID: 42, + InstanceID: "42", Charts: map[string]map[string]interface{}{ "frontend": {"replicas": float64(3), "image": map[string]interface{}{"tag": "v2"}}, "backend": {"replicas": float64(1)}, @@ -1449,14 +1437,14 @@ func TestStackValuesCmd_JSONOutput(t *testing.T) { var result types.MergedValues require.NoError(t, json.Unmarshal(buf.Bytes(), &result)) - assert.Equal(t, uint(42), result.InstanceID) + assert.Equal(t, "42", result.InstanceID) assert.Contains(t, result.Charts, "frontend") assert.Contains(t, result.Charts, "backend") } func TestStackValuesCmd_TableOutputFallsBackToJSON(t *testing.T) { values := types.MergedValues{ - InstanceID: 42, + InstanceID: "42", Charts: map[string]map[string]interface{}{"api": {"replicas": float64(2)}}, } server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -1473,12 +1461,12 @@ func TestStackValuesCmd_TableOutputFallsBackToJSON(t *testing.T) { var result types.MergedValues require.NoError(t, json.Unmarshal(buf.Bytes(), &result)) - assert.Equal(t, uint(42), result.InstanceID) + assert.Equal(t, "42", result.InstanceID) } func TestStackValuesCmd_YAMLOutput(t *testing.T) { values := types.MergedValues{ - InstanceID: 42, + InstanceID: "42", Charts: map[string]map[string]interface{}{"api": {"replicas": float64(2)}}, } server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -1494,11 +1482,11 @@ func TestStackValuesCmd_YAMLOutput(t *testing.T) { require.NoError(t, err) out := buf.String() - assert.Contains(t, out, "instance_id: 42") + assert.Contains(t, out, "instance_id: \"42\"") } func TestStackValuesCmd_QuietOutput(t *testing.T) { - values := types.MergedValues{InstanceID: 42} + values := types.MergedValues{InstanceID: "42"} server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) @@ -1518,7 +1506,7 @@ func TestStackValuesCmd_WithChartFilter(t *testing.T) { assert.Equal(t, "frontend", r.URL.Query().Get("chart")) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(types.MergedValues{InstanceID: 42}) + json.NewEncoder(w).Encode(types.MergedValues{InstanceID: "42"}) })) defer server.Close() @@ -1531,18 +1519,6 @@ func TestStackValuesCmd_WithChartFilter(t *testing.T) { require.NoError(t, err) } -func TestStackValuesCmd_InvalidID(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - t.Fatal("API should not be called for invalid ID") - })) - defer server.Close() - - _ = setupStackTestCmd(t, server.URL) - err := stackValuesCmd.RunE(stackValuesCmd, []string{"abc"}) - require.Error(t, err) - assert.Contains(t, err.Error(), "invalid ID") -} - func TestStackValuesCmd_ServerError(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") @@ -1576,7 +1552,7 @@ func TestStackValuesCmd_NotFound(t *testing.T) { func TestStackCompareCmd_TableOutput_WithDiffs(t *testing.T) { left := sampleStack() right := sampleStack() - right.ID = 43 + right.ID = "43" right.Name = "other-stack" right.Status = "stopped" right.Branch = "feature/x" @@ -1616,7 +1592,7 @@ func TestStackCompareCmd_TableOutput_WithDiffs(t *testing.T) { func TestStackCompareCmd_TableOutput_NoDiffs(t *testing.T) { left := sampleStack() right := sampleStack() - right.ID = 43 + right.ID = "43" result := types.CompareResult{Left: &left, Right: &right, Diffs: map[string]interface{}{}} server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -1637,7 +1613,7 @@ func TestStackCompareCmd_TableOutput_NoDiffs(t *testing.T) { func TestStackCompareCmd_JSONOutput(t *testing.T) { left := sampleStack() right := sampleStack() - right.ID = 43 + right.ID = "43" result := types.CompareResult{Left: &left, Right: &right} server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -1654,14 +1630,14 @@ func TestStackCompareCmd_JSONOutput(t *testing.T) { var res types.CompareResult require.NoError(t, json.Unmarshal(buf.Bytes(), &res)) - assert.Equal(t, uint(42), res.Left.ID) - assert.Equal(t, uint(43), res.Right.ID) + assert.Equal(t, "42", res.Left.ID) + assert.Equal(t, "43", res.Right.ID) } func TestStackCompareCmd_YAMLOutput(t *testing.T) { left := sampleStack() right := sampleStack() - right.ID = 43 + right.ID = "43" result := types.CompareResult{Left: &left, Right: &right} server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -1696,30 +1672,6 @@ func TestStackCompareCmd_QuietOutput(t *testing.T) { assert.Equal(t, "42\n43\n", buf.String()) } -func TestStackCompareCmd_InvalidLeftID(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - t.Fatal("API should not be called for invalid ID") - })) - defer server.Close() - - _ = setupStackTestCmd(t, server.URL) - err := stackCompareCmd.RunE(stackCompareCmd, []string{"abc", "43"}) - require.Error(t, err) - assert.Contains(t, err.Error(), "invalid ID") -} - -func TestStackCompareCmd_InvalidRightID(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - t.Fatal("API should not be called for invalid ID") - })) - defer server.Close() - - _ = setupStackTestCmd(t, server.URL) - err := stackCompareCmd.RunE(stackCompareCmd, []string{"42", "xyz"}) - require.Error(t, err) - assert.Contains(t, err.Error(), "invalid ID") -} - func TestStackCompareCmd_ServerError(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") diff --git a/cli/cmd/template_test.go b/cli/cmd/template_test.go index 1692a0c..853d133 100644 --- a/cli/cmd/template_test.go +++ b/cli/cmd/template_test.go @@ -22,21 +22,21 @@ import ( func sampleTemplate() types.StackTemplate { now := time.Date(2025, 6, 15, 10, 0, 0, 0, time.UTC) return types.StackTemplate{ - Base: types.Base{ID: 10, CreatedAt: now, UpdatedAt: now, Version: 1}, + Base: types.Base{ID: "10", CreatedAt: now, UpdatedAt: now, Version: "1"}, Name: "web-app-template", Description: "Full web app stack", Published: true, Owner: "admin", Charts: []types.ChartConfig{ { - Base: types.Base{ID: 1}, + Base: types.Base{ID: "1"}, Name: "frontend", RepoURL: "https://charts.example.com", ChartName: "react-app", ChartVersion: "1.2.0", }, { - Base: types.Base{ID: 2}, + Base: types.Base{ID: "2"}, Name: "backend", RepoURL: "https://charts.example.com", ChartName: "api-server", @@ -123,7 +123,7 @@ func TestTemplateListCmd_YAMLOutput(t *testing.T) { func TestTemplateListCmd_QuietOutput(t *testing.T) { t1 := sampleTemplate() t2 := sampleTemplate() - t2.ID = 20 + t2.ID = "20" t2.Name = "second-template" server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") @@ -237,7 +237,7 @@ func TestTemplateGetCmd_JSONOutput(t *testing.T) { var result types.StackTemplate require.NoError(t, json.Unmarshal(buf.Bytes(), &result)) - assert.Equal(t, uint(10), result.ID) + assert.Equal(t, "10", result.ID) assert.Equal(t, "web-app-template", result.Name) assert.True(t, result.Published) } @@ -258,30 +258,6 @@ func TestTemplateGetCmd_QuietOutput(t *testing.T) { assert.Equal(t, "10\n", buf.String()) } -func TestTemplateGetCmd_InvalidID(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - t.Fatal("API should not be called for invalid ID") - })) - defer server.Close() - - _ = setupStackTestCmd(t, server.URL) - err := templateGetCmd.RunE(templateGetCmd, []string{"abc"}) - require.Error(t, err) - assert.Contains(t, err.Error(), "invalid ID") -} - -func TestTemplateGetCmd_ZeroID(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - t.Fatal("API should not be called for zero ID") - })) - defer server.Close() - - _ = setupStackTestCmd(t, server.URL) - err := templateGetCmd.RunE(templateGetCmd, []string{"0"}) - require.Error(t, err) - assert.Contains(t, err.Error(), "invalid ID") -} - func TestTemplateGetCmd_NotFound(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") @@ -337,11 +313,11 @@ func TestTemplateInstantiateCmd_WithBranchAndCluster(t *testing.T) { require.NoError(t, json.NewDecoder(r.Body).Decode(&body)) assert.Equal(t, "my-instance", body.Name) assert.Equal(t, "feature/xyz", body.Branch) - assert.Equal(t, uint(2), body.ClusterID) + assert.Equal(t, "2", body.ClusterID) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) - json.NewEncoder(w).Encode(types.StackInstance{Base: types.Base{ID: 50}, Name: "my-instance"}) + json.NewEncoder(w).Encode(types.StackInstance{Base: types.Base{ID: "50"}, Name: "my-instance"}) })) defer server.Close() @@ -371,24 +347,6 @@ func TestTemplateInstantiateCmd_MissingName(t *testing.T) { assert.Contains(t, nameFlag.Annotations, cobra.BashCompOneRequiredFlag) } -func TestTemplateInstantiateCmd_InvalidID(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - t.Fatal("API should not be called for invalid ID") - })) - defer server.Close() - - _ = setupStackTestCmd(t, server.URL) - - templateInstantiateCmd.Flags().Set("name", "test") - t.Cleanup(func() { - templateInstantiateCmd.Flags().Set("name", "") - }) - - err := templateInstantiateCmd.RunE(templateInstantiateCmd, []string{"abc"}) - require.Error(t, err) - assert.Contains(t, err.Error(), "invalid ID") -} - func TestTemplateInstantiateCmd_ServerError(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") @@ -413,7 +371,7 @@ func TestTemplateInstantiateCmd_QuietOutput(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) - json.NewEncoder(w).Encode(types.StackInstance{Base: types.Base{ID: 50}}) + json.NewEncoder(w).Encode(types.StackInstance{Base: types.Base{ID: "50"}}) })) defer server.Close() @@ -496,7 +454,7 @@ func TestTemplateQuickDeployCmd_QuietOutput(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) - json.NewEncoder(w).Encode(types.StackInstance{Base: types.Base{ID: 60}, Status: "deploying"}) + json.NewEncoder(w).Encode(types.StackInstance{Base: types.Base{ID: "60"}, Status: "deploying"}) })) defer server.Close() @@ -514,7 +472,7 @@ func TestTemplateQuickDeployCmd_QuietOutput(t *testing.T) { } func TestTemplateQuickDeployCmd_JSONOutput(t *testing.T) { - instance := types.StackInstance{Base: types.Base{ID: 60}, Name: "quick-stack", Status: "deploying"} + instance := types.StackInstance{Base: types.Base{ID: "60"}, Name: "quick-stack", Status: "deploying"} server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) @@ -535,28 +493,10 @@ func TestTemplateQuickDeployCmd_JSONOutput(t *testing.T) { var result types.StackInstance require.NoError(t, json.Unmarshal(buf.Bytes(), &result)) - assert.Equal(t, uint(60), result.ID) + assert.Equal(t, "60", result.ID) assert.Equal(t, "deploying", result.Status) } -func TestTemplateQuickDeployCmd_InvalidID(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - t.Fatal("API should not be called for invalid ID") - })) - defer server.Close() - - _ = setupStackTestCmd(t, server.URL) - - templateQuickDeployCmd.Flags().Set("name", "test") - t.Cleanup(func() { - templateQuickDeployCmd.Flags().Set("name", "") - }) - - err := templateQuickDeployCmd.RunE(templateQuickDeployCmd, []string{"0"}) - require.Error(t, err) - assert.Contains(t, err.Error(), "invalid ID") -} - // ---------- template list auth error ---------- func TestTemplateListCmd_Unauthorized(t *testing.T) { diff --git a/cli/test/integration/template_definition_integration_test.go b/cli/test/integration/template_definition_integration_test.go index 2a3b9f1..5527ba5 100644 --- a/cli/test/integration/template_definition_integration_test.go +++ b/cli/test/integration/template_definition_integration_test.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" "net/http/httptest" + "strconv" "strings" "sync" "testing" @@ -282,7 +283,9 @@ func startTemplateDefMockServer(t *testing.T, state *templateDefMockState) *http if req.Description != "" { def.Description = req.Description } - def.Version = def.Version + "+1" + if v, err := strconv.Atoi(def.Version); err == nil { + def.Version = strconv.Itoa(v + 1) + } state.mu.Unlock() w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(def)