diff --git a/integration-tests/environment_delete_test.go b/integration-tests/environment_delete_test.go new file mode 100644 index 00000000..a6530106 --- /dev/null +++ b/integration-tests/environment_delete_test.go @@ -0,0 +1,91 @@ +package tests + +import ( + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/upsun/cli/pkg/mockapi" +) + +func TestEnvironmentDelete(t *testing.T) { + authServer := mockapi.NewAuthServer(t) + defer authServer.Close() + + apiHandler := mockapi.NewHandler(t) + apiServer := httptest.NewServer(apiHandler) + defer apiServer.Close() + + projectID := mockapi.ProjectID() + apiHandler.SetProjects([]*mockapi.Project{{ + ID: projectID, + Links: mockapi.MakeHALLinks( + "self=/projects/"+projectID, + "environments=/projects/"+projectID+"/environments", + ), + DefaultBranch: "main", + }}) + apiHandler.SetEnvironments([]*mockapi.Environment{ + makeEnv(projectID, "main", "production", "active", nil), + makeEnv(projectID, "test-1", "development", "active", "main"), + makeEnv(projectID, "test-2", "development", "active", "main"), + makeEnv(projectID, "dev", "development", "active", "main"), + }) + + f := newCommandFactory(t, apiServer.URL, authServer.URL) + f.Run("cc") + + cases := []struct { + name string + args []string + extraEnv []string + wantErr bool + wantStdErr []string + wantMissing []string + }{ + { + name: "single wildcard argument", + args: []string{"test-*"}, + wantStdErr: []string{"2 environments found by ID.", "Selected environments: test-1, test-2"}, + wantMissing: []string{"Specified environment not found"}, + }, + { + name: "wildcard option", + args: []string{"-e", "test-*"}, + wantStdErr: []string{"2 environments found by ID.", "Selected environments: test-1, test-2"}, + wantMissing: []string{"Specified environment not found"}, + }, + { + name: "ignores an unknown branch variable", + args: []string{"--type", "development"}, + extraEnv: []string{"PLATFORM_BRANCH=missing"}, + wantStdErr: []string{"3 environments found matching type(s): development"}, + wantMissing: []string{"Specified environment not found"}, + }, + { + name: "missing environment", + args: []string{"missing"}, + wantErr: true, + wantStdErr: []string{"Environment not found: missing", "0 environments found by ID."}, + wantMissing: []string{"Specified environment not found"}, + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + f.extraEnv = c.extraEnv + args := append([]string{"environment:delete", "-p", projectID}, c.args...) + // Decline any confirmation, so nothing is deleted. + _, stdErr, err := f.RunInteractive("n\nn\n", args...) + if c.wantErr { + assert.Error(t, err) + } + for _, s := range c.wantStdErr { + assert.Contains(t, stdErr, s) + } + for _, s := range c.wantMissing { + assert.NotContains(t, stdErr, s) + } + }) + } +} diff --git a/legacy/src/Command/Environment/EnvironmentDeleteCommand.php b/legacy/src/Command/Environment/EnvironmentDeleteCommand.php index d78aabde..76e168d8 100644 --- a/legacy/src/Command/Environment/EnvironmentDeleteCommand.php +++ b/legacy/src/Command/Environment/EnvironmentDeleteCommand.php @@ -77,12 +77,8 @@ protected function configure(): void protected function execute(InputInterface $input, OutputInterface $output): int { - // Select the current project, deliberately ignoring the 'environment' - // argument and option, as those will be processed separately. - $inputCopy = clone $input; - $inputCopy->setArgument('environment', null); - $inputCopy->setOption('environment', null); - $selection = $this->selector->getSelection($input, new SelectorConfig(envRequired: false)); + // Select only the project: the 'environment' argument and option are processed separately. + $selection = $this->selector->getSelection($input, new SelectorConfig(selectEnv: false)); $environments = $this->api->getEnvironments($selection->getProject()); @@ -103,7 +99,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int if ($specifiedEnvironmentIds) { $anythingSpecified = true; $allIds = \array_map(fn(Environment $e) => $e->id, $environments); - $specifiedEnvironmentIds = Wildcard::select($allIds, $specifiedEnvironmentIds); + // Keep exact IDs even if they don't match, so they can be reported as not found. + $exactIds = array_filter($specifiedEnvironmentIds, fn(string $id): bool => !str_contains($id, '%') && !str_contains($id, '*')); + $specifiedEnvironmentIds = array_values(array_unique(array_merge(Wildcard::select($allIds, $specifiedEnvironmentIds), $exactIds))); $notFound = array_diff($specifiedEnvironmentIds, array_keys($environments)); if (!empty($notFound)) { // Refresh the environments list if any environment is not found. diff --git a/legacy/src/Selector/Selector.php b/legacy/src/Selector/Selector.php index 76179a63..9950fe27 100644 --- a/legacy/src/Selector/Selector.php +++ b/legacy/src/Selector/Selector.php @@ -144,7 +144,9 @@ public function getSelection(InputInterface $input, ?SelectorConfig $config = nu $environment = null; $envArgName = $config->envArgName; - if ($input->hasArgument($envArgName) + if (!$config->selectEnv) { + $this->debug('Skipping environment selection'); + } elseif ($input->hasArgument($envArgName) && $input->getArgument($envArgName) !== null && $input->getArgument($envArgName) !== []) { if ($input->hasOption($envOptionName) && Option::stringOrNull($input, $envOptionName)) { diff --git a/legacy/src/Selector/SelectorConfig.php b/legacy/src/Selector/SelectorConfig.php index 378aebe1..80373739 100644 --- a/legacy/src/Selector/SelectorConfig.php +++ b/legacy/src/Selector/SelectorConfig.php @@ -10,6 +10,8 @@ class SelectorConfig { public function __construct( public bool $envRequired = true, + // Set to false to select only the project, e.g. when the command handles environments itself. + public bool $selectEnv = true, public string $envArgName = 'environment', public string $chooseProjectText = 'Enter a number to choose a project:', public string $chooseEnvText = 'Enter a number to choose an environment:',