package stacks import ( "context" "net/http" "net/http/httptest" "strconv" "testing" "github.com/pkg/errors" portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/datastore" "github.com/portainer/portainer/api/internal/testhelpers" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // Stubs type stubComposeStackManager struct { portainer.ComposeStackManager deployErr error } func (s *stubComposeStackManager) NormalizeStackName(name string) string { return name } func (s *stubComposeStackManager) Up(_ context.Context, _ *portainer.Stack, _ *portainer.Endpoint, _ portainer.ComposeUpOptions) error { return s.deployErr } func stackStartRequest(stackID portainer.StackID, endpointID portainer.EndpointID) *http.Request { url := "/stacks/" + strconv.Itoa(int(stackID)) + "/start?endpointId=" + strconv.Itoa(int(endpointID)) return mockCreateStackRequestWithSecurityContext(http.MethodPost, url, nil) } func newStackStartHandler(t *testing.T) (*Handler, *datastore.Store) { t.Helper() _, store := datastore.MustNewTestStore(t, true, false) h := NewHandler(testhelpers.NewTestRequestBouncer()) h.DataStore = store return h, store } func TestStackStart_ActiveStack_ReturnsConflict(t *testing.T) { h, store := newStackStartHandler(t) stack := &portainer.Stack{ID: 1, Status: portainer.StackStatusActive} require.NoError(t, store.Stack().Create(stack)) w := httptest.NewRecorder() h.ServeHTTP(w, stackStartRequest(stack.ID, 1)) assert.Equal(t, http.StatusConflict, w.Code) } func TestStackStart_DeployingStack_ReturnsConflict(t *testing.T) { h, store := newStackStartHandler(t) stack := &portainer.Stack{ID: 1, Status: portainer.StackStatusDeploying} require.NoError(t, store.Stack().Create(stack)) w := httptest.NewRecorder() h.ServeHTTP(w, stackStartRequest(stack.ID, 1)) assert.Equal(t, http.StatusConflict, w.Code) } func TestStackStart_ErrorStack_ReturnsConflict(t *testing.T) { h, store := newStackStartHandler(t) stack := &portainer.Stack{ID: 1, Status: portainer.StackStatusError} require.NoError(t, store.Stack().Create(stack)) w := httptest.NewRecorder() h.ServeHTTP(w, stackStartRequest(stack.ID, 1)) assert.Equal(t, http.StatusConflict, w.Code) } func newStartableStack(endpointID portainer.EndpointID) *portainer.Stack { return &portainer.Stack{ ID: 1, EndpointID: endpointID, Type: portainer.DockerComposeStack, Name: "test-stack", } } func TestStackStart_StartSuccess_StackStatusSetToActive(t *testing.T) { h, store := newStackStartHandler(t) _, err := mockCreateUser(store) require.NoError(t, err) endpoint, err := mockCreateEndpoint(store) require.NoError(t, err) stack := newStartableStack(endpoint.ID) require.NoError(t, store.Stack().Create(stack)) h.ComposeStackManager = &stubComposeStackManager{} w := httptest.NewRecorder() h.ServeHTTP(w, stackStartRequest(stack.ID, endpoint.ID)) require.Equal(t, http.StatusOK, w.Code) updated, err := store.Stack().Read(stack.ID) require.NoError(t, err) assert.Equal(t, portainer.StackStatusActive, updated.Status) require.Len(t, updated.DeploymentStatus, 1) assert.Equal(t, portainer.StackStatusActive, updated.DeploymentStatus[0].Status) } func TestStackStart_StartFailure_StackStatusSetToError(t *testing.T) { deployErr := errors.New("failed to pull image nginx:999") h, store := newStackStartHandler(t) _, err := mockCreateUser(store) require.NoError(t, err) endpoint, err := mockCreateEndpoint(store) require.NoError(t, err) stack := newStartableStack(endpoint.ID) require.NoError(t, store.Stack().Create(stack)) h.ComposeStackManager = &stubComposeStackManager{deployErr: deployErr} w := httptest.NewRecorder() h.ServeHTTP(w, stackStartRequest(stack.ID, endpoint.ID)) require.Equal(t, http.StatusInternalServerError, w.Code) updated, err := store.Stack().Read(stack.ID) require.NoError(t, err) assert.Equal(t, portainer.StackStatusError, updated.Status) require.Len(t, updated.DeploymentStatus, 1) lastEntry := updated.DeploymentStatus[0] assert.Equal(t, portainer.StackStatusError, lastEntry.Status) assert.Equal(t, deployErr.Error(), lastEntry.Message) }