From f57e7180ff114274bf37e2bb82b6dc1afc9d5415 Mon Sep 17 00:00:00 2001 From: krls2020 Date: Wed, 21 Oct 2026 16:07:47 +0200 Subject: [PATCH] fix(scope): an explicit project id beats the remembered project scope With a project scope saved, a positional project id was ignored because it was applied only when no project was selected yet, so the scope won. An explicit id, flag or positional, is now resolved before the saved scope, and the flag wins when both are given. --- src/cmd/projectDelete_integration_test.go | 122 ++++++++++++++++++++++ src/cmdBuilder/scopeProject.go | 50 +++------ 2 files changed, 138 insertions(+), 34 deletions(-) create mode 100644 src/cmd/projectDelete_integration_test.go diff --git a/src/cmd/projectDelete_integration_test.go b/src/cmd/projectDelete_integration_test.go new file mode 100644 index 00000000..c0ff880d --- /dev/null +++ b/src/cmd/projectDelete_integration_test.go @@ -0,0 +1,122 @@ +package cmd + +import ( + "encoding/json" + "net/http" + "sync" + "testing" + + "github.com/stretchr/testify/require" +) + +const deleteOtherProjectID = "0vzEq7ETUceKlQdesTBLhQ" + +// registerProjectDeleteStubs serves GET and DELETE for the given project ids and returns a func listing the ids that received a DELETE. +func registerProjectDeleteStubs(f *fixture, projectIDs ...string) func() []string { + var mu sync.Mutex + var deleted []string + + for _, id := range projectIDs { + f.Mux.HandleFunc("/api/rest/public/project/"+id, func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + if r.Method == http.MethodDelete { + mu.Lock() + deleted = append(deleted, id) + mu.Unlock() + _ = json.NewEncoder(w).Encode(deleteProcessBody("RUNNING")) + return + } + _ = json.NewEncoder(w).Encode(map[string]any{ + "id": id, + "clientId": pushClientID, + "name": "project-" + id, + "mode": "LIGHT", + "status": "ACTIVE", + "created": "2024-01-01T00:00:00.000Z", + "lastUpdate": "2024-01-01T00:00:00.000Z", + "tagList": []string{}, + }) + }) + } + f.HandleJSON("/api/rest/public/process/"+pushDeployProcID, 200, deleteProcessBody("FINISHED")) + + return func() []string { + mu.Lock() + defer mu.Unlock() + return append([]string(nil), deleted...) + } +} + +func deleteProcessBody(status string) map[string]any { + return map[string]any{ + "id": pushDeployProcID, + "clientId": pushClientID, + "projectId": pushProjectID, + "status": status, + "sequence": 1, + "actionName": "DeleteProject", + "created": "2024-01-01T00:00:00.000Z", + "lastUpdate": "2024-01-01T00:00:00.000Z", + "createdBySystem": false, + "createdByUser": map[string]any{}, + "serviceStacks": []any{}, + } +} + +// TestProjectDeleteCommand_ExplicitProjectBeatsSavedScope checks that a project id given explicitly, positional or as --project-id, +// is the project that gets deleted even when a different project is saved as the scope, and that the saved scope still applies without one. +func TestProjectDeleteCommand_ExplicitProjectBeatsSavedScope(t *testing.T) { + tests := []struct { + name string + scoped bool + args []string + wantDelete string + }{ + { + name: "positional id with a saved scope", + scoped: true, + args: []string{"project", "delete", deleteOtherProjectID, "--confirm"}, + wantDelete: deleteOtherProjectID, + }, + { + name: "positional id without a saved scope", + args: []string{"project", "delete", deleteOtherProjectID, "--confirm"}, + wantDelete: deleteOtherProjectID, + }, + { + name: "flag with a saved scope", + scoped: true, + args: []string{"project", "delete", "--project-id", deleteOtherProjectID, "--confirm"}, + wantDelete: deleteOtherProjectID, + }, + { + name: "flag beats positional when both are given", + scoped: true, + args: []string{"project", "delete", pushProjectID, "--project-id", deleteOtherProjectID, "--confirm"}, + wantDelete: deleteOtherProjectID, + }, + { + name: "saved scope without an explicit id", + scoped: true, + args: []string{"project", "delete", "--confirm"}, + wantDelete: pushProjectID, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f := newFixture(t) + if tt.scoped { + f.SeedScopedLogin("test-token", pushProjectID) + } else { + f.SeedLogin("test-token") + } + deleted := registerProjectDeleteStubs(f, pushProjectID, deleteOtherProjectID) + + res := f.Run(tt.args...) + + require.Equalf(t, 0, res.ExitCode, "stderr=%q stdout=%q", res.Stderr, res.Stdout) + require.Equal(t, []string{tt.wantDelete}, deleted()) + }) + } +} diff --git a/src/cmdBuilder/scopeProject.go b/src/cmdBuilder/scopeProject.go index 3307392d..4dcbfce7 100644 --- a/src/cmdBuilder/scopeProject.go +++ b/src/cmdBuilder/scopeProject.go @@ -72,27 +72,16 @@ func (p *projectScope) LoadSelectedScope(ctx context.Context, _ *Cmd, cmdData *L cmdData.Project = optional.New(project) } - // project scope is set - if cmdData.CliStorage.Data().ScopeProjectId.Filled() { - projectId, _ := cmdData.CliStorage.Data().ScopeProjectId.Get() - - project, err = repository.GetProjectById(ctx, cmdData.RestApiClient, projectId) - if err != nil { - if errorsx.Is(err, errorsx.ErrorCode(errorCode.ProjectNotFound)) { - err := ProjectScopeReset(cmdData) - if err != nil { - return err - } - } else { - return err - } + // an explicitly given project id always beats the saved scope, the flag beats the positional argument + explicitProjectId := cmdData.Params.GetString(ProjectArgName) + if explicitProjectId == "" { + if projectId, exists := cmdData.Args[ProjectArgName]; exists { + explicitProjectId = projectId[0] } - cmdData.Project = optional.New(project) } - // project id is passed as a flag - if projectId := cmdData.Params.GetString(ProjectArgName); projectId != "" { - project, err = repository.GetProjectById(ctx, cmdData.RestApiClient, uuid.ProjectId(projectId)) + if explicitProjectId != "" { + project, err = repository.GetProjectById(ctx, cmdData.RestApiClient, uuid.ProjectId(explicitProjectId)) if err != nil { return errorsx.Convert( err, @@ -100,29 +89,22 @@ func (p *projectScope) LoadSelectedScope(ctx context.Context, _ *Cmd, cmdData *L "id", errorsx.InvalidUserInputErrorMessage( func(_ apiError.Error, metaItemTyped map[string]interface{}) string { - return i18n.T(i18n.ErrorInvalidProjectId, projectId, metaItemTyped["message"]) + return i18n.T(i18n.ErrorInvalidProjectId, explicitProjectId, metaItemTyped["message"]) }, ), ), ) } cmdData.Project = optional.New(project) - } - - if projectId, exists := cmdData.Args[ProjectArgName]; exists && !cmdData.Project.Filled() { - project, err = repository.GetProjectById(ctx, cmdData.RestApiClient, uuid.ProjectId(projectId[0])) + } else if projectId, scoped := cmdData.CliStorage.Data().ScopeProjectId.Get(); scoped { + project, err = repository.GetProjectById(ctx, cmdData.RestApiClient, projectId) if err != nil { - return errorsx.Convert( - err, - errorsx.InvalidUserInput( - "id", - errorsx.InvalidUserInputErrorMessage( - func(_ apiError.Error, metaItemTyped map[string]interface{}) string { - return i18n.T(i18n.ErrorInvalidProjectId, projectId, metaItemTyped["message"]) - }, - ), - ), - ) + if !errorsx.Is(err, errorsx.ErrorCode(errorCode.ProjectNotFound)) { + return err + } + if err := ProjectScopeReset(cmdData); err != nil { + return err + } } cmdData.Project = optional.New(project) }