From 79c165450bb68856f565fd874e21e191e9a873b6 Mon Sep 17 00:00:00 2001 From: Austin Ely Date: Fri, 15 Jan 2021 16:47:40 -0800 Subject: [PATCH] chore(cli): add task delete validate/ask (#1831) Adds the skeleton of a new `task delete` command with Ask() and Validate() filled in. Tests for Ask() forthcoming, then I'll work on Execute in a separate PR. By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. --- internal/pkg/cli/flag.go | 2 + internal/pkg/cli/interfaces.go | 4 + internal/pkg/cli/mocks/mock_interfaces.go | 43 +++ internal/pkg/cli/task_delete.go | 337 ++++++++++++++++++ internal/pkg/cli/task_delete_test.go | 270 ++++++++++++++ .../pkg/deploy/cloudformation/task_test.go | 1 - 6 files changed, 656 insertions(+), 1 deletion(-) create mode 100644 internal/pkg/cli/task_delete.go create mode 100644 internal/pkg/cli/task_delete_test.go diff --git a/internal/pkg/cli/flag.go b/internal/pkg/cli/flag.go index aeb56b44fb1..ade62af6fa0 100644 --- a/internal/pkg/cli/flag.go +++ b/internal/pkg/cli/flag.go @@ -130,6 +130,8 @@ Cannot be specified with '%s', '%s' or '%s'.`, appFlag, envFlag, taskDefaultFlag Cannot be specified with '%s' or '%s'.`, appFlag, envFlag) taskDefaultFlagDescription = fmt.Sprintf(`Optional. Run tasks in default cluster and default subnets. Cannot be specified with '%s', '%s' or '%s'.`, appFlag, envFlag, subnetsFlag) + taskDeleteDefaultFlagDescription = fmt.Sprintf(`Optional. Delete a task which was launched in the default cluster and subnets. +Cannot be specified with '%s' or '%s'`, appFlag, envFlag) taskEnvFlagDescription = fmt.Sprintf(`Optional. Name of the environment. Cannot be specified with '%s', '%s' or '%s'`, taskDefaultFlag, subnetsFlag, securityGroupsFlag) taskAppFlagDescription = fmt.Sprintf(`Optional. Name of the application. diff --git a/internal/pkg/cli/interfaces.go b/internal/pkg/cli/interfaces.go index 62ce1b09743..d4b340c05cd 100644 --- a/internal/pkg/cli/interfaces.go +++ b/internal/pkg/cli/interfaces.go @@ -457,6 +457,10 @@ type initJobSelector interface { Schedule(scheduleTypePrompt, scheduleTypeHelp string, scheduleValidator, rateValidator prompt.ValidatorFunc) (string, error) } +type cfTaskSelector interface { + Task(prompt, help string, opts ...selector.GetDeployedTaskOpts) (*selector.DeployedTask, error) +} + type dockerfileSelector interface { Dockerfile(selPrompt, notFoundPrompt, selHelp, notFoundHelp string, pv prompt.ValidatorFunc) (string, error) } diff --git a/internal/pkg/cli/mocks/mock_interfaces.go b/internal/pkg/cli/mocks/mock_interfaces.go index fc7b5446faa..46d0fa3289a 100644 --- a/internal/pkg/cli/mocks/mock_interfaces.go +++ b/internal/pkg/cli/mocks/mock_interfaces.go @@ -4906,6 +4906,49 @@ func (mr *MockinitJobSelectorMockRecorder) Schedule(scheduleTypePrompt, schedule return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Schedule", reflect.TypeOf((*MockinitJobSelector)(nil).Schedule), scheduleTypePrompt, scheduleTypeHelp, scheduleValidator, rateValidator) } +// MockcfTaskSelector is a mock of cfTaskSelector interface +type MockcfTaskSelector struct { + ctrl *gomock.Controller + recorder *MockcfTaskSelectorMockRecorder +} + +// MockcfTaskSelectorMockRecorder is the mock recorder for MockcfTaskSelector +type MockcfTaskSelectorMockRecorder struct { + mock *MockcfTaskSelector +} + +// NewMockcfTaskSelector creates a new mock instance +func NewMockcfTaskSelector(ctrl *gomock.Controller) *MockcfTaskSelector { + mock := &MockcfTaskSelector{ctrl: ctrl} + mock.recorder = &MockcfTaskSelectorMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockcfTaskSelector) EXPECT() *MockcfTaskSelectorMockRecorder { + return m.recorder +} + +// Task mocks base method +func (m *MockcfTaskSelector) Task(prompt, help string, opts ...selector.GetDeployedTaskOpts) (*selector.DeployedTask, error) { + m.ctrl.T.Helper() + varargs := []interface{}{prompt, help} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Task", varargs...) + ret0, _ := ret[0].(*selector.DeployedTask) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Task indicates an expected call of Task +func (mr *MockcfTaskSelectorMockRecorder) Task(prompt, help interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{prompt, help}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Task", reflect.TypeOf((*MockcfTaskSelector)(nil).Task), varargs...) +} + // MockdockerfileSelector is a mock of dockerfileSelector interface type MockdockerfileSelector struct { ctrl *gomock.Controller diff --git a/internal/pkg/cli/task_delete.go b/internal/pkg/cli/task_delete.go new file mode 100644 index 00000000000..e6c4637642b --- /dev/null +++ b/internal/pkg/cli/task_delete.go @@ -0,0 +1,337 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package cli + +import ( + "errors" + "fmt" + + awssession "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/copilot-cli/internal/pkg/aws/sessions" + "github.com/aws/copilot-cli/internal/pkg/config" + "github.com/aws/copilot-cli/internal/pkg/deploy/cloudformation" + "github.com/aws/copilot-cli/internal/pkg/term/color" + "github.com/aws/copilot-cli/internal/pkg/term/log" + "github.com/aws/copilot-cli/internal/pkg/term/prompt" + "github.com/aws/copilot-cli/internal/pkg/term/selector" + "github.com/aws/copilot-cli/internal/pkg/workspace" + + "github.com/spf13/cobra" +) + +const ( + taskDeleteNamePrompt = "Which task would you like to delete?" + taskDeleteAppPrompt = "Which application would you like to delete a task from?" + taskDeleteEnvPrompt = "Which environment would you like to delete a task from?" + fmtTaskDeleteDefaultConfirmPrompt = "Are you sure you want to delete %s from the default cluster?" + fmtTaskDeleteFromEnvConfirmPrompt = "Are you sure you want to delete %s from application %s and environment %s?" + taskDeleteConfirmHelp = "This will delete the task's stack and stop all current executions." +) + +var errTaskDeleteCancelled = errors.New("task delete cancelled - no changes made") + +type deleteTaskVars struct { + name string + app string + env string + skipConfirmation bool + defaultCluster bool +} + +type deleteTaskOpts struct { + deleteTaskVars + + // Dependencies to interact with other modules + store store + prompt prompter + sess sessionProvider + sel wsSelector + + // Generators for env-specific clients + newTaskSel func(session *awssession.Session) cfTaskSelector +} + +func newDeleteTaskOpts(vars deleteTaskVars) (*deleteTaskOpts, error) { + store, err := config.NewStore() + if err != nil { + return nil, fmt.Errorf("new config store: %w", err) + } + + provider := sessions.NewProvider() + + prompter := prompt.New() + + ws, err := workspace.New() + if err != nil { + return nil, fmt.Errorf("new workspace: %w", err) + } + + return &deleteTaskOpts{ + deleteTaskVars: vars, + + store: store, + prompt: prompter, + sess: provider, + sel: selector.NewWorkspaceSelect(prompter, store, ws), + newTaskSel: func(session *awssession.Session) cfTaskSelector { + cfn := cloudformation.New(session) + return selector.NewCFTaskSelect(prompter, store, cfn) + }, + }, nil +} + +// Validate checks that flag inputs are valid. +func (o *deleteTaskOpts) Validate() error { + + if o.name != "" { + if err := basicNameValidation(o.name); err != nil { + return err + } + } + + // If default flag specified, + if err := o.validateFlagsWithDefaultCluster(); err != nil { + return err + } + + if err := o.validateFlagsWithEnv(); err != nil { + return err + } + + return nil +} + +func (o *deleteTaskOpts) validateFlagsWithEnv() error { + if o.app != "" { + if _, err := o.store.GetApplication(o.app); err != nil { + return fmt.Errorf("get application: %w", err) + } + } + + if o.env != "" && o.app != "" { + if _, err := o.store.GetEnvironment(o.app, o.env); err != nil { + return err + } + } + + return nil +} + +func (o *deleteTaskOpts) validateFlagsWithDefaultCluster() error { + if !o.defaultCluster { + return nil + } + + // If app is specified and the same as the workspace app, don't throw an error. + // If app is not the same as the workspace app, either + // a) there is no WS app and it's specified erroneously, in which case we should error + // b) there is a WS app and the flag has been set to a different app, in which case we should error. + // The app flag defaults to the WS app so there's an edge case where it's possible to specify + // `copilot task delete --app ws-app --default` and not error out, but this should be taken as + // specifying "default". + if o.app != tryReadingAppName() { + return fmt.Errorf("cannot specify both `--app` and `--default`") + } + + if o.env != "" { + return fmt.Errorf("cannot specify both `--env` and `--default`") + } + + return nil +} + +func (o *deleteTaskOpts) askAppName() error { + if o.defaultCluster { + return nil + } + + if o.app != "" { + return nil + } + + app, err := o.sel.Application(taskDeleteAppPrompt, "", appEnvOptionNone) + if err != nil { + return fmt.Errorf("select application name: %w", err) + } + if app == appEnvOptionNone { + o.env = "" + o.defaultCluster = true + return nil + } + o.app = app + return nil +} + +func (o *deleteTaskOpts) askEnvName() error { + if o.defaultCluster { + return nil + } + + if o.env != "" { + return nil + } + env, err := o.sel.Environment(taskDeleteEnvPrompt, "", o.app, appEnvOptionNone) + if err != nil { + return fmt.Errorf("select environment: %w", err) + } + if env == appEnvOptionNone { + o.env = "" + o.app = "" + o.defaultCluster = true + return nil + } + o.env = env + return nil +} + +// Ask prompts for missing information and fills in gaps. +func (o *deleteTaskOpts) Ask() error { + if err := o.askAppName(); err != nil { + return err + } + + if err := o.askEnvName(); err != nil { + return err + } + + if err := o.askTaskName(); err != nil { + return err + } + + if o.skipConfirmation { + return nil + } + + // Confirm deletion + deletePrompt := fmt.Sprintf(fmtTaskDeleteDefaultConfirmPrompt, color.HighlightUserInput(o.name)) + if o.env != "" && o.app != "" { + deletePrompt = fmt.Sprintf( + fmtTaskDeleteFromEnvConfirmPrompt, + color.HighlightUserInput(o.name), + color.HighlightUserInput(o.app), + color.HighlightUserInput(o.env), + ) + } + + deleteConfirmed, err := o.prompt.Confirm( + deletePrompt, + taskDeleteConfirmHelp) + + if err != nil { + return fmt.Errorf("task delete confirmation prompt: %w", err) + } + if !deleteConfirmed { + return errTaskDeleteCancelled + } + return nil +} + +func (o *deleteTaskOpts) getSession() (*awssession.Session, error) { + if o.defaultCluster { + sess, err := o.sess.Default() + if err != nil { + return nil, err + } + return sess, nil + } + // Get environment manager role for deleting stack. + env, err := o.store.GetEnvironment(o.app, o.env) + if err != nil { + return nil, err + } + sess, err := o.sess.FromRole(env.ManagerRoleARN, env.Region) + if err != nil { + return nil, err + } + return sess, nil +} + +func (o *deleteTaskOpts) askTaskName() error { + if o.name != "" { + return nil + } + + sess, err := o.getSession() + if err != nil { + return fmt.Errorf("get task select session: %w", err) + } + sel := o.newTaskSel(sess) + if o.defaultCluster { + task, err := sel.Task(taskDeleteNamePrompt, "", selector.TaskWithDefaultCluster()) + if err != nil { + return fmt.Errorf("select task from default cluster: %w", err) + } + o.name = task.Name + return nil + } + task, err := sel.Task(taskDeleteNamePrompt, "", selector.TaskWithAppEnv(o.app, o.env)) + if err != nil { + return fmt.Errorf("select task from environment: %w", err) + } + o.name = task.Name + return nil +} + +func (o *deleteTaskOpts) Execute() error { + // Get clients. + + // Stop tasks. + + // Clear repository. + + // Delete stack. + + return nil +} + +func (o *deleteTaskOpts) RecommendedActions() []string { + return nil +} + +// BuildTaskDeleteCmd builds the command to delete application(s). +func BuildTaskDeleteCmd() *cobra.Command { + vars := deleteTaskVars{} + cmd := &cobra.Command{ + Hidden: true, + Use: "delete", + Short: "Deletes a one-off task from an application or default cluster.", + Example: ` + Delete the "test" task from the default cluster. + /code $ copilot task delete --name test --default + + Delete the "test" task from the prod environment. + /code $ copilot task delete --name test --env prod + + Delete the "test" task without confirmation prompt. + /code $ copilot task delete --name test --yes`, + RunE: runCmdE(func(cmd *cobra.Command, args []string) error { + opts, err := newDeleteTaskOpts(vars) + if err != nil { + return err + } + if err := opts.Validate(); err != nil { + return err + } + if err := opts.Ask(); err != nil { + return err + } + if err := opts.Execute(); err != nil { + return err + } + + log.Infoln("Recommended follow-up actions:") + for _, followup := range opts.RecommendedActions() { + log.Infof("- %s\n", followup) + } + return nil + }), + } + + cmd.Flags().StringVarP(&vars.app, appFlag, appFlagShort, tryReadingAppName(), appFlagDescription) + cmd.Flags().StringVarP(&vars.name, nameFlag, nameFlagShort, "", svcFlagDescription) + cmd.Flags().StringVarP(&vars.env, envFlag, envFlagShort, "", envFlagDescription) + cmd.Flags().BoolVar(&vars.skipConfirmation, yesFlag, false, yesFlagDescription) + cmd.Flags().BoolVar(&vars.defaultCluster, taskDefaultFlag, false, taskDeleteDefaultFlagDescription) + return cmd +} diff --git a/internal/pkg/cli/task_delete_test.go b/internal/pkg/cli/task_delete_test.go new file mode 100644 index 00000000000..2b664e796b1 --- /dev/null +++ b/internal/pkg/cli/task_delete_test.go @@ -0,0 +1,270 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package cli + +import ( + "errors" + "testing" + + awssession "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/copilot-cli/internal/pkg/cli/mocks" + "github.com/aws/copilot-cli/internal/pkg/config" + "github.com/aws/copilot-cli/internal/pkg/term/selector" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/require" +) + +func TestDeleteTaskOpts_Validate(t *testing.T) { + + testCases := map[string]struct { + inAppName string + inEnvName string + inName string + inDefaultCluster bool + setupMocks func(m *mocks.Mockstore) + + want error + }{ + "with only app flag": { + inAppName: "phonetool", + setupMocks: func(m *mocks.Mockstore) { + m.EXPECT().GetApplication("phonetool").Return(&config.Application{Name: "phonetool"}, nil) + }, + want: nil, + }, + "with no flags": { + setupMocks: func(m *mocks.Mockstore) {}, + want: nil, + }, + "with app/env flags set": { + inAppName: "phonetool", + inEnvName: "test", + setupMocks: func(m *mocks.Mockstore) { + m.EXPECT().GetApplication("phonetool").Return(&config.Application{Name: "phonetool"}, nil) + m.EXPECT().GetEnvironment("phonetool", "test").Return(&config.Environment{Name: "test", App: "phonetool"}, nil) + }, + want: nil, + }, + "with default cluster flag set": { + inDefaultCluster: true, + inName: "oneoff", + setupMocks: func(m *mocks.Mockstore) {}, + want: nil, + }, + "with default cluster and env flag": { + inDefaultCluster: true, + inEnvName: "test", + inAppName: "phonetool", + setupMocks: func(m *mocks.Mockstore) {}, + want: errors.New("cannot specify both `--app` and `--default`"), + }, + "with error getting app": { + inAppName: "phonetool", + inEnvName: "test", + setupMocks: func(m *mocks.Mockstore) { + m.EXPECT().GetApplication("phonetool").Return(nil, errors.New("some error")) + }, + want: errors.New("get application: some error"), + }, + } + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + // GIVEN + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockstore := mocks.NewMockstore(ctrl) + + tc.setupMocks(mockstore) + + opts := deleteTaskOpts{ + deleteTaskVars: deleteTaskVars{ + skipConfirmation: false, + app: tc.inAppName, + env: tc.inEnvName, + name: tc.inName, + defaultCluster: tc.inDefaultCluster, + }, + store: mockstore, + } + + // WHEN + err := opts.Validate() + + // THEN + if tc.want != nil { + require.EqualError(t, err, tc.want.Error()) + } else { + require.NoError(t, err) + } + }) + } + +} + +func TestDeleteTaskOpts_Ask(t *testing.T) { + testCases := map[string]struct { + inAppName string + inEnvName string + inName string + inDefaultCluster bool + inSkipConfirmation bool + + mockStore func(m *mocks.Mockstore) + mockSel func(m *mocks.MockwsSelector) + mockTaskSelect func(m *mocks.MockcfTaskSelector) + mockSess func(m *mocks.MocksessionProvider) + mockPrompter func(m *mocks.Mockprompter) + + wantErr string + }{ + "all flags specified": { + inAppName: "phonetool", + inEnvName: "test", + inName: "abcd", + inSkipConfirmation: true, + + mockStore: func(m *mocks.Mockstore) {}, + mockSel: func(m *mocks.MockwsSelector) {}, + mockTaskSelect: func(m *mocks.MockcfTaskSelector) {}, + mockSess: func(m *mocks.MocksessionProvider) {}, + mockPrompter: func(m *mocks.Mockprompter) {}, + }, + "name flag not specified": { + inAppName: "phonetool", + inEnvName: "test", + + mockStore: func(m *mocks.Mockstore) { + // This call is in GetSession when an environment is specified and we need to get the Manager Role's session. + m.EXPECT().GetEnvironment("phonetool", "test").Return(&config.Environment{Name: "test", App: "phonetool"}, nil) + }, + mockSel: func(m *mocks.MockwsSelector) {}, + mockTaskSelect: func(m *mocks.MockcfTaskSelector) { + m.EXPECT().Task(taskDeleteNamePrompt, "", gomock.Any()).Return(&selector.DeployedTask{Name: "abc"}, nil) + }, + mockSess: func(m *mocks.MocksessionProvider) { + m.EXPECT().FromRole(gomock.Any(), gomock.Any()).Return(&awssession.Session{}, nil) + }, + mockPrompter: func(m *mocks.Mockprompter) { + m.EXPECT().Confirm("Are you sure you want to delete abc from application phonetool and environment test?", gomock.Any()).Return(true, nil) + }, + }, + "name flag not specified and confirm cancelled": { + inAppName: "phonetool", + inEnvName: "test", + + mockStore: func(m *mocks.Mockstore) { + // This call is in GetSession when an environment is specified and we need to get the Manager Role's session. + m.EXPECT().GetEnvironment("phonetool", "test").Return(&config.Environment{Name: "test", App: "phonetool"}, nil) + }, + mockSel: func(m *mocks.MockwsSelector) {}, + mockTaskSelect: func(m *mocks.MockcfTaskSelector) { + m.EXPECT().Task(taskDeleteNamePrompt, "", gomock.Any()).Return(&selector.DeployedTask{Name: "abc"}, nil) + }, + mockSess: func(m *mocks.MocksessionProvider) { + m.EXPECT().FromRole(gomock.Any(), gomock.Any()).Return(&awssession.Session{}, nil) + }, + mockPrompter: func(m *mocks.Mockprompter) { + m.EXPECT().Confirm("Are you sure you want to delete abc from application phonetool and environment test?", gomock.Any()).Return(false, nil) + }, + wantErr: "task delete cancelled - no changes made", + }, + "default flag specified": { + inDefaultCluster: true, + + mockStore: func(m *mocks.Mockstore) { + }, + mockSel: func(m *mocks.MockwsSelector) {}, + mockTaskSelect: func(m *mocks.MockcfTaskSelector) { + m.EXPECT().Task(taskDeleteNamePrompt, "", gomock.Any()).Return(&selector.DeployedTask{Name: "abc"}, nil) + }, + mockSess: func(m *mocks.MocksessionProvider) { + m.EXPECT().Default().Return(&awssession.Session{}, nil) + }, + mockPrompter: func(m *mocks.Mockprompter) { + m.EXPECT().Confirm("Are you sure you want to delete abc from the default cluster?", gomock.Any()).Return(true, nil) + }, + }, + "no flags specified": { + mockStore: func(m *mocks.Mockstore) { + // This call is in GetSession when an environment is specified and we need to get the Manager Role's session. + m.EXPECT().GetEnvironment("phonetool", "test").Return(&config.Environment{Name: "test", App: "phonetool"}, nil) + }, + mockSel: func(m *mocks.MockwsSelector) { + m.EXPECT().Application(taskDeleteAppPrompt, "", appEnvOptionNone).Return("phonetool", nil) + m.EXPECT().Environment(taskDeleteEnvPrompt, "", "phonetool", appEnvOptionNone).Return("test", nil) + }, + mockTaskSelect: func(m *mocks.MockcfTaskSelector) { + m.EXPECT().Task(taskDeleteNamePrompt, "", gomock.Any()).Return(&selector.DeployedTask{Name: "abc"}, nil) + }, + mockSess: func(m *mocks.MocksessionProvider) { + m.EXPECT().FromRole(gomock.Any(), gomock.Any()).Return(&awssession.Session{}, nil) + }, + mockPrompter: func(m *mocks.Mockprompter) { + m.EXPECT().Confirm("Are you sure you want to delete abc from application phonetool and environment test?", gomock.Any()).Return(true, nil) + }, + }, + "no flags specified (default path)": { + mockStore: func(m *mocks.Mockstore) {}, + mockSel: func(m *mocks.MockwsSelector) { + m.EXPECT().Application(taskDeleteAppPrompt, "", appEnvOptionNone).Return(appEnvOptionNone, nil) + }, + mockTaskSelect: func(m *mocks.MockcfTaskSelector) { + m.EXPECT().Task(taskDeleteNamePrompt, "", gomock.Any()).Return(&selector.DeployedTask{Name: "abc"}, nil) + }, + mockSess: func(m *mocks.MocksessionProvider) { + m.EXPECT().Default().Return(&awssession.Session{}, nil) + }, + mockPrompter: func(m *mocks.Mockprompter) { + m.EXPECT().Confirm("Are you sure you want to delete abc from the default cluster?", gomock.Any()).Return(true, nil) + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + // GIVEN + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockStore := mocks.NewMockstore(ctrl) + mockSel := mocks.NewMockwsSelector(ctrl) + mockSess := mocks.NewMocksessionProvider(ctrl) + mockTaskSel := mocks.NewMockcfTaskSelector(ctrl) + mockPrompt := mocks.NewMockprompter(ctrl) + + tc.mockStore(mockStore) + tc.mockSel(mockSel) + tc.mockSess(mockSess) + tc.mockTaskSelect(mockTaskSel) + tc.mockPrompter(mockPrompt) + + opts := deleteTaskOpts{ + deleteTaskVars: deleteTaskVars{ + skipConfirmation: tc.inSkipConfirmation, + defaultCluster: tc.inDefaultCluster, + app: tc.inAppName, + env: tc.inEnvName, + name: tc.inName, + }, + + store: mockStore, + sel: mockSel, + sess: mockSess, + prompt: mockPrompt, + + newTaskSel: func(sess *awssession.Session) cfTaskSelector { return mockTaskSel }, + } + + // WHEN + err := opts.Ask() + + // THEN + if tc.wantErr != "" { + require.EqualError(t, err, tc.wantErr) + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/internal/pkg/deploy/cloudformation/task_test.go b/internal/pkg/deploy/cloudformation/task_test.go index 386e92b97fe..ca3829ac333 100644 --- a/internal/pkg/deploy/cloudformation/task_test.go +++ b/internal/pkg/deploy/cloudformation/task_test.go @@ -155,7 +155,6 @@ func TestCloudFormation_ListTaskStacks(t *testing.T) { }, }, }, - "error listing stacks": { inAppName: "appname", mockClient: func(m *mocks.MockcfnClient) {