From 96ff3f00e63f62bab8209d5df8db8b2f2daa2a14 Mon Sep 17 00:00:00 2001 From: omattsson Date: Fri, 15 May 2026 21:23:45 +0200 Subject: [PATCH 1/3] feat(template): add publish and unpublish subcommands Implements issue #61. - Add PublishTemplate and UnpublishTemplate client methods (POST /api/v1/templates/:id/publish and unpublish) - Add 'template publish ' and 'template unpublish ' commands - Both commands use printTemplate() for consistent output (table/JSON/YAML/quiet) - 403 Forbidden renders as 'Permission denied' via existing APIError mapping - Unit tests: all output modes + 403 for both commands - Integration tests: publish/unpublish state reflected on subsequent GetTemplate - E2E tests: happy path, quiet output --- cli/cmd/template.go | 62 +++++++++ cli/cmd/template_test.go | 127 ++++++++++++++++++ cli/pkg/client/client.go | 20 +++ cli/pkg/client/client_test.go | 106 +++++++++++++++ cli/test/e2e/cli_e2e_test.go | 73 ++++++++++ .../template_definition_integration_test.go | 76 +++++++++++ 6 files changed, 464 insertions(+) diff --git a/cli/cmd/template.go b/cli/cmd/template.go index 3a1dc13..3f0081d 100644 --- a/cli/cmd/template.go +++ b/cli/cmd/template.go @@ -383,6 +383,66 @@ Examples: }, } +var templatePublishCmd = &cobra.Command{ + Use: "publish ", + Short: "Publish a stack template", + Long: `Publish a stack template to make it available for use. + +Examples: + stackctl template publish 1 + stackctl template publish 1 -o json`, + Args: cobra.ExactArgs(1), + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + id, err := parseID(args[0]) + if err != nil { + return err + } + + c, err := newClient() + if err != nil { + return err + } + + tmpl, err := c.PublishTemplate(id) + if err != nil { + return err + } + + return printTemplate(tmpl) + }, +} + +var templateUnpublishCmd = &cobra.Command{ + Use: "unpublish ", + Short: "Unpublish a stack template", + Long: `Unpublish a stack template to prevent new instantiations. + +Examples: + stackctl template unpublish 1 + stackctl template unpublish 1 -o json`, + Args: cobra.ExactArgs(1), + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + id, err := parseID(args[0]) + if err != nil { + return err + } + + c, err := newClient() + if err != nil { + return err + } + + tmpl, err := c.UnpublishTemplate(id) + if err != nil { + return err + } + + return printTemplate(tmpl) + }, +} + // printTemplate outputs a StackTemplate in the active format. func printTemplate(tmpl *types.StackTemplate) error { if printer.Quiet { @@ -462,5 +522,7 @@ func init() { templateCmd.AddCommand(templateCreateCmd) templateCmd.AddCommand(templateUpdateCmd) templateCmd.AddCommand(templateCloneCmd) + templateCmd.AddCommand(templatePublishCmd) + templateCmd.AddCommand(templateUnpublishCmd) rootCmd.AddCommand(templateCmd) } diff --git a/cli/cmd/template_test.go b/cli/cmd/template_test.go index 56a2846..f511b38 100644 --- a/cli/cmd/template_test.go +++ b/cli/cmd/template_test.go @@ -1179,3 +1179,130 @@ func TestTemplateCloneCmd_EmptyName(t *testing.T) { require.Error(t, err) assert.Contains(t, err.Error(), "--name must not be empty") } + +// ---------- template publish ---------- + +func TestTemplatePublishCmd_Success(t *testing.T) { + tmpl := sampleTemplate() + tmpl.Published = true + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, "/api/v1/templates/10/publish", r.URL.Path) + require.Equal(t, http.MethodPost, r.Method) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(tmpl) + })) + defer server.Close() + + buf := setupStackTestCmd(t, server.URL) + err := templatePublishCmd.RunE(templatePublishCmd, []string{"10"}) + require.NoError(t, err) + out := buf.String() + assert.Contains(t, out, "10") + assert.Contains(t, out, "web-app-template") +} + +func TestTemplatePublishCmd_JSONOutput(t *testing.T) { + tmpl := sampleTemplate() + tmpl.Published = true + 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(tmpl) + })) + defer server.Close() + + buf := setupStackTestCmd(t, server.URL) + printer.Format = output.FormatJSON + err := templatePublishCmd.RunE(templatePublishCmd, []string{"10"}) + require.NoError(t, err) + + var result types.StackTemplate + require.NoError(t, json.Unmarshal(buf.Bytes(), &result)) + assert.Equal(t, "10", result.ID) + assert.True(t, result.Published) +} + +func TestTemplatePublishCmd_YAMLOutput(t *testing.T) { + tmpl := sampleTemplate() + tmpl.Published = true + 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(tmpl) + })) + defer server.Close() + + buf := setupStackTestCmd(t, server.URL) + printer.Format = output.FormatYAML + err := templatePublishCmd.RunE(templatePublishCmd, []string{"10"}) + require.NoError(t, err) + assert.Contains(t, buf.String(), "web-app-template") +} + +func TestTemplatePublishCmd_QuietOutput(t *testing.T) { + tmpl := sampleTemplate() + tmpl.Published = true + 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(tmpl) + })) + defer server.Close() + + buf := setupStackTestCmd(t, server.URL) + printer.Quiet = true + err := templatePublishCmd.RunE(templatePublishCmd, []string{"10"}) + require.NoError(t, err) + assert.Equal(t, "10\n", buf.String()) +} + +func TestTemplatePublishCmd_Forbidden(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.StatusForbidden) + json.NewEncoder(w).Encode(types.ErrorResponse{Error: "Permission denied"}) + })) + defer server.Close() + + _ = setupStackTestCmd(t, server.URL) + err := templatePublishCmd.RunE(templatePublishCmd, []string{"10"}) + require.Error(t, err) + assert.Contains(t, err.Error(), "Permission denied") +} + +// ---------- template unpublish ---------- + +func TestTemplateUnpublishCmd_Success(t *testing.T) { + tmpl := sampleTemplate() + tmpl.Published = false + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, "/api/v1/templates/10/unpublish", r.URL.Path) + require.Equal(t, http.MethodPost, r.Method) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(tmpl) + })) + defer server.Close() + + buf := setupStackTestCmd(t, server.URL) + err := templateUnpublishCmd.RunE(templateUnpublishCmd, []string{"10"}) + require.NoError(t, err) + out := buf.String() + assert.Contains(t, out, "10") + assert.Contains(t, out, "web-app-template") +} + +func TestTemplateUnpublishCmd_Forbidden(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.StatusForbidden) + json.NewEncoder(w).Encode(types.ErrorResponse{Error: "Permission denied"}) + })) + defer server.Close() + + _ = setupStackTestCmd(t, server.URL) + err := templateUnpublishCmd.RunE(templateUnpublishCmd, []string{"10"}) + require.Error(t, err) + assert.Contains(t, err.Error(), "Permission denied") +} diff --git a/cli/pkg/client/client.go b/cli/pkg/client/client.go index c418bd5..a88c32e 100644 --- a/cli/pkg/client/client.go +++ b/cli/pkg/client/client.go @@ -638,6 +638,26 @@ func (c *Client) CloneTemplate(id string, req *types.CloneTemplateRequest) (*typ return &tmpl, nil } +// PublishTemplate publishes a stack template by ID. +func (c *Client) PublishTemplate(id string) (*types.StackTemplate, error) { + var tmpl types.StackTemplate + err := c.Post(fmt.Sprintf(pathTemplate+"/publish", id), nil, &tmpl) + if err != nil { + return nil, err + } + return &tmpl, nil +} + +// UnpublishTemplate unpublishes a stack template by ID. +func (c *Client) UnpublishTemplate(id string) (*types.StackTemplate, error) { + var tmpl types.StackTemplate + err := c.Post(fmt.Sprintf(pathTemplate+"/unpublish", id), nil, &tmpl) + if err != nil { + return nil, err + } + return &tmpl, nil +} + // ListOrphanedNamespaces returns namespaces that have the stack-manager label but no matching DB record. func (c *Client) ListOrphanedNamespaces() ([]types.OrphanedNamespace, error) { var ns []types.OrphanedNamespace diff --git a/cli/pkg/client/client_test.go b/cli/pkg/client/client_test.go index 41632bd..9d4d50e 100644 --- a/cli/pkg/client/client_test.go +++ b/cli/pkg/client/client_test.go @@ -2816,3 +2816,109 @@ func TestCloneTemplate_NotFound(t *testing.T) { require.True(t, ok) assert.Equal(t, http.StatusNotFound, apiErr.StatusCode) } + +func TestPublishTemplate(t *testing.T) { + t.Parallel() + tests := []struct { + name string + statusCode int + body interface{} + wantErr bool + wantStatus int + }{ + { + name: "Success", + statusCode: http.StatusOK, + body: types.StackTemplate{Base: types.Base{ID: "1"}, Name: "web-app", Published: true}, + wantErr: false, + }, + { + name: "Forbidden", + statusCode: http.StatusForbidden, + body: types.ErrorResponse{Error: "Permission denied"}, + wantErr: true, + wantStatus: http.StatusForbidden, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(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/templates/1/publish", r.URL.Path) + w.WriteHeader(tt.statusCode) + json.NewEncoder(w).Encode(tt.body) + })) + defer server.Close() + + c := New(server.URL) + tmpl, err := c.PublishTemplate("1") + if tt.wantErr { + require.Error(t, err) + assert.Nil(t, tmpl) + apiErr, ok := err.(*APIError) + require.True(t, ok) + assert.Equal(t, tt.wantStatus, apiErr.StatusCode) + } else { + require.NoError(t, err) + require.NotNil(t, tmpl) + assert.Equal(t, "1", tmpl.ID) + assert.True(t, tmpl.Published) + } + }) + } +} + +func TestUnpublishTemplate(t *testing.T) { + t.Parallel() + tests := []struct { + name string + statusCode int + body interface{} + wantErr bool + wantStatus int + }{ + { + name: "Success", + statusCode: http.StatusOK, + body: types.StackTemplate{Base: types.Base{ID: "1"}, Name: "web-app", Published: false}, + wantErr: false, + }, + { + name: "Forbidden", + statusCode: http.StatusForbidden, + body: types.ErrorResponse{Error: "Permission denied"}, + wantErr: true, + wantStatus: http.StatusForbidden, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(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/templates/1/unpublish", r.URL.Path) + w.WriteHeader(tt.statusCode) + json.NewEncoder(w).Encode(tt.body) + })) + defer server.Close() + + c := New(server.URL) + tmpl, err := c.UnpublishTemplate("1") + if tt.wantErr { + require.Error(t, err) + assert.Nil(t, tmpl) + apiErr, ok := err.(*APIError) + require.True(t, ok) + assert.Equal(t, tt.wantStatus, apiErr.StatusCode) + } else { + require.NoError(t, err) + require.NotNil(t, tmpl) + assert.Equal(t, "1", tmpl.ID) + assert.False(t, tmpl.Published) + } + }) + } +} diff --git a/cli/test/e2e/cli_e2e_test.go b/cli/test/e2e/cli_e2e_test.go index 06e413d..6148372 100644 --- a/cli/test/e2e/cli_e2e_test.go +++ b/cli/test/e2e/cli_e2e_test.go @@ -815,6 +815,30 @@ func startE2ETemplateDefMockServer(t *testing.T) *httptest.Server { w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(resp) + // Publish template + case r.URL.Path == "/api/v1/templates/1/publish" && r.Method == http.MethodPost: + resp := map[string]interface{}{ + "id": "1", "name": "web-template", "description": "Web app stack", + "published": true, "owner": "admin", "charts": []map[string]interface{}{ + {"id": "1", "name": "frontend", "repo_url": "https://charts.example.com", "chart_version": "1.0.0"}, + }, + "created_at": "2025-01-01T00:00:00Z", "updated_at": "2025-01-01T00:00:00Z", "version": "1", + } + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(resp) + + // Unpublish template + case r.URL.Path == "/api/v1/templates/1/unpublish" && r.Method == http.MethodPost: + resp := map[string]interface{}{ + "id": "1", "name": "web-template", "description": "Web app stack", + "published": false, "owner": "admin", "charts": []map[string]interface{}{ + {"id": "1", "name": "frontend", "repo_url": "https://charts.example.com", "chart_version": "1.0.0"}, + }, + "created_at": "2025-01-01T00:00:00Z", "updated_at": "2025-01-01T00:00:00Z", "version": "1", + } + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(resp) + default: w.WriteHeader(http.StatusNotFound) json.NewEncoder(w).Encode(map[string]string{"error": "not found"}) @@ -1355,3 +1379,52 @@ func TestE2E_TemplateClone_QuietOutput(t *testing.T) { require.NoError(t, err) assert.Equal(t, "100\n", stdout) } + +func TestE2E_TemplatePublish(t *testing.T) { + if testing.Short() { + t.Skip("skipping e2e test in short mode") + } + + server := startE2ETemplateDefMockServer(t) + defer server.Close() + + dir := t.TempDir() + setupE2EStackContext(t, dir, server.URL) + + stdout, _, err := runStackctl(t, dir, "template", "publish", "1") + require.NoError(t, err) + assert.Contains(t, stdout, "web-template") + assert.Contains(t, stdout, "true") +} + +func TestE2E_TemplateUnpublish(t *testing.T) { + if testing.Short() { + t.Skip("skipping e2e test in short mode") + } + + server := startE2ETemplateDefMockServer(t) + defer server.Close() + + dir := t.TempDir() + setupE2EStackContext(t, dir, server.URL) + + stdout, _, err := runStackctl(t, dir, "template", "unpublish", "1") + require.NoError(t, err) + assert.Contains(t, stdout, "web-template") +} + +func TestE2E_TemplatePublish_QuietOutput(t *testing.T) { + if testing.Short() { + t.Skip("skipping e2e test in short mode") + } + + server := startE2ETemplateDefMockServer(t) + defer server.Close() + + dir := t.TempDir() + setupE2EStackContext(t, dir, server.URL) + + stdout, _, err := runStackctl(t, dir, "template", "publish", "1", "--quiet") + require.NoError(t, err) + assert.Equal(t, "1\n", stdout) +} diff --git a/cli/test/integration/template_definition_integration_test.go b/cli/test/integration/template_definition_integration_test.go index 4499410..435bcd1 100644 --- a/cli/test/integration/template_definition_integration_test.go +++ b/cli/test/integration/template_definition_integration_test.go @@ -226,6 +226,22 @@ func startTemplateDefMockServer(t *testing.T, state *templateDefMockState) *http w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(inst) return + + case tmplAction == "publish" && r.Method == http.MethodPost: + state.mu.Lock() + tmpl.Published = true + state.mu.Unlock() + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(tmpl) + return + + case tmplAction == "unpublish" && r.Method == http.MethodPost: + state.mu.Lock() + tmpl.Published = false + state.mu.Unlock() + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(tmpl) + return } } } @@ -764,3 +780,63 @@ func TestTemplateClone_NotFound(t *testing.T) { require.Error(t, err) assert.Contains(t, err.Error(), "template not found") } + +func TestTemplatePublish_Success(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test in short mode") + } + + state := newTemplateDefMockState() + server := startTemplateDefMockServer(t, state) + defer server.Close() + + c := client.New(server.URL) + + tmpl, err := c.PublishTemplate("1") + require.NoError(t, err) + assert.True(t, tmpl.Published) + + fetched, err := c.GetTemplate("1") + require.NoError(t, err) + assert.True(t, fetched.Published) +} + +func TestTemplateUnpublish_Success(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test in short mode") + } + + state := newTemplateDefMockState() + server := startTemplateDefMockServer(t, state) + defer server.Close() + + c := client.New(server.URL) + + // Ensure published first + _, err := c.PublishTemplate("1") + require.NoError(t, err) + + tmpl, err := c.UnpublishTemplate("1") + require.NoError(t, err) + assert.False(t, tmpl.Published) + + fetched, err := c.GetTemplate("1") + require.NoError(t, err) + assert.False(t, fetched.Published) +} + +func TestTemplatePublish_NotFound(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test in short mode") + } + + state := newTemplateDefMockState() + server := startTemplateDefMockServer(t, state) + defer server.Close() + + c := client.New(server.URL) + + _, err := c.PublishTemplate("999") + require.Error(t, err) + assert.Contains(t, err.Error(), "template not found") +} From 7e65621aa94de436b0409bb7da3da62f6a042237 Mon Sep 17 00:00:00 2001 From: omattsson Date: Fri, 15 May 2026 21:36:32 +0200 Subject: [PATCH 2/3] fix(test): correct JSON field names in e2e publish/unpublish mock StackTemplate uses is_published (not published), owner_id (not owner). ChartConfig uses chart_name (not name) and repository_url (not repo_url). --- cli/test/e2e/cli_e2e_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cli/test/e2e/cli_e2e_test.go b/cli/test/e2e/cli_e2e_test.go index 6148372..34498be 100644 --- a/cli/test/e2e/cli_e2e_test.go +++ b/cli/test/e2e/cli_e2e_test.go @@ -819,8 +819,8 @@ func startE2ETemplateDefMockServer(t *testing.T) *httptest.Server { case r.URL.Path == "/api/v1/templates/1/publish" && r.Method == http.MethodPost: resp := map[string]interface{}{ "id": "1", "name": "web-template", "description": "Web app stack", - "published": true, "owner": "admin", "charts": []map[string]interface{}{ - {"id": "1", "name": "frontend", "repo_url": "https://charts.example.com", "chart_version": "1.0.0"}, + "is_published": true, "owner_id": "admin", "charts": []map[string]interface{}{ + {"id": "1", "chart_name": "frontend", "repository_url": "https://charts.example.com", "chart_version": "1.0.0"}, }, "created_at": "2025-01-01T00:00:00Z", "updated_at": "2025-01-01T00:00:00Z", "version": "1", } @@ -831,8 +831,8 @@ func startE2ETemplateDefMockServer(t *testing.T) *httptest.Server { case r.URL.Path == "/api/v1/templates/1/unpublish" && r.Method == http.MethodPost: resp := map[string]interface{}{ "id": "1", "name": "web-template", "description": "Web app stack", - "published": false, "owner": "admin", "charts": []map[string]interface{}{ - {"id": "1", "name": "frontend", "repo_url": "https://charts.example.com", "chart_version": "1.0.0"}, + "is_published": false, "owner_id": "admin", "charts": []map[string]interface{}{ + {"id": "1", "chart_name": "frontend", "repository_url": "https://charts.example.com", "chart_version": "1.0.0"}, }, "created_at": "2025-01-01T00:00:00Z", "updated_at": "2025-01-01T00:00:00Z", "version": "1", } From 9f8c64e5f09c3c4103c0fd7c26b480b6cc193f90 Mon Sep 17 00:00:00 2001 From: omattsson Date: Fri, 15 May 2026 21:38:30 +0200 Subject: [PATCH 3/3] test: address Copilot review comments on unpublish test coverage - Add JSON, YAML, and quiet output tests for templateUnpublishCmd - Assert Published=false in TestE2E_TemplateUnpublish - Add TestE2E_TemplateUnpublish_QuietOutput --- cli/cmd/template_test.go | 55 ++++++++++++++++++++++++++++++++++++ cli/test/e2e/cli_e2e_test.go | 17 +++++++++++ 2 files changed, 72 insertions(+) diff --git a/cli/cmd/template_test.go b/cli/cmd/template_test.go index f511b38..bdf78ad 100644 --- a/cli/cmd/template_test.go +++ b/cli/cmd/template_test.go @@ -1293,6 +1293,61 @@ func TestTemplateUnpublishCmd_Success(t *testing.T) { assert.Contains(t, out, "web-app-template") } +func TestTemplateUnpublishCmd_JSONOutput(t *testing.T) { + tmpl := sampleTemplate() + tmpl.Published = false + 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(tmpl) + })) + defer server.Close() + + buf := setupStackTestCmd(t, server.URL) + printer.Format = output.FormatJSON + err := templateUnpublishCmd.RunE(templateUnpublishCmd, []string{"10"}) + require.NoError(t, err) + + var result types.StackTemplate + require.NoError(t, json.Unmarshal(buf.Bytes(), &result)) + assert.Equal(t, "10", result.ID) + assert.False(t, result.Published) +} + +func TestTemplateUnpublishCmd_YAMLOutput(t *testing.T) { + tmpl := sampleTemplate() + tmpl.Published = false + 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(tmpl) + })) + defer server.Close() + + buf := setupStackTestCmd(t, server.URL) + printer.Format = output.FormatYAML + err := templateUnpublishCmd.RunE(templateUnpublishCmd, []string{"10"}) + require.NoError(t, err) + assert.Contains(t, buf.String(), "web-app-template") +} + +func TestTemplateUnpublishCmd_QuietOutput(t *testing.T) { + tmpl := sampleTemplate() + tmpl.Published = false + 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(tmpl) + })) + defer server.Close() + + buf := setupStackTestCmd(t, server.URL) + printer.Quiet = true + err := templateUnpublishCmd.RunE(templateUnpublishCmd, []string{"10"}) + require.NoError(t, err) + assert.Equal(t, "10\n", buf.String()) +} + func TestTemplateUnpublishCmd_Forbidden(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/test/e2e/cli_e2e_test.go b/cli/test/e2e/cli_e2e_test.go index 34498be..d77fd20 100644 --- a/cli/test/e2e/cli_e2e_test.go +++ b/cli/test/e2e/cli_e2e_test.go @@ -1411,6 +1411,7 @@ func TestE2E_TemplateUnpublish(t *testing.T) { stdout, _, err := runStackctl(t, dir, "template", "unpublish", "1") require.NoError(t, err) assert.Contains(t, stdout, "web-template") + assert.Contains(t, stdout, "false") } func TestE2E_TemplatePublish_QuietOutput(t *testing.T) { @@ -1428,3 +1429,19 @@ func TestE2E_TemplatePublish_QuietOutput(t *testing.T) { require.NoError(t, err) assert.Equal(t, "1\n", stdout) } + +func TestE2E_TemplateUnpublish_QuietOutput(t *testing.T) { + if testing.Short() { + t.Skip("skipping e2e test in short mode") + } + + server := startE2ETemplateDefMockServer(t) + defer server.Close() + + dir := t.TempDir() + setupE2EStackContext(t, dir, server.URL) + + stdout, _, err := runStackctl(t, dir, "template", "unpublish", "1", "--quiet") + require.NoError(t, err) + assert.Equal(t, "1\n", stdout) +}