github.com/kyma-incubator/compass/components/director@v0.0.0-20230623144113-d764f56ff805/pkg/operation/update_operation_handler_test.go (about)

     1  /*
     2   * Copyright 2020 The Compass Authors
     3   *
     4   * Licensed under the Apache License, Version 2.0 (the "License");
     5   * you may not use this file except in compliance with the License.
     6   * You may obtain a copy of the License at
     7   *
     8   *     http://www.apache.org/licenses/LICENSE-2.0
     9   *
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   */
    16  
    17  package operation_test
    18  
    19  import (
    20  	"bytes"
    21  	"context"
    22  	"errors"
    23  	"fmt"
    24  	"net/http"
    25  	"net/http/httptest"
    26  	"testing"
    27  
    28  	"github.com/kyma-incubator/compass/components/director/internal/model"
    29  
    30  	"github.com/stretchr/testify/require"
    31  
    32  	"github.com/kyma-incubator/compass/components/director/pkg/operation"
    33  	"github.com/kyma-incubator/compass/components/director/pkg/persistence/txtest"
    34  	"github.com/kyma-incubator/compass/components/director/pkg/resource"
    35  )
    36  
    37  func TestUpdateOperationHandler(t *testing.T) {
    38  	t.Run("when request method is not PUT it should return method not allowed", func(t *testing.T) {
    39  		writer := httptest.NewRecorder()
    40  		req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "/", nil)
    41  		require.NoError(t, err)
    42  
    43  		handler := operation.NewUpdateOperationHandler(nil, nil, nil)
    44  		handler.ServeHTTP(writer, req)
    45  
    46  		require.Contains(t, writer.Body.String(), "Method not allowed")
    47  		require.Equal(t, http.StatusMethodNotAllowed, writer.Code)
    48  	})
    49  
    50  	t.Run("when request body is not valid it should return bad request", func(t *testing.T) {
    51  		writer := httptest.NewRecorder()
    52  		reader := bytes.NewReader([]byte(`{"resource_id": 1}`))
    53  		req, err := http.NewRequestWithContext(context.Background(), http.MethodPut, "/", reader)
    54  		require.NoError(t, err)
    55  
    56  		handler := operation.NewUpdateOperationHandler(nil, nil, nil)
    57  		handler.ServeHTTP(writer, req)
    58  
    59  		require.Contains(t, writer.Body.String(), "Unable to decode body to JSON")
    60  		require.Equal(t, http.StatusBadRequest, writer.Code)
    61  	})
    62  
    63  	t.Run("when required input body properties are missing it should return bad request", func(t *testing.T) {
    64  		writer := httptest.NewRecorder()
    65  		req := fixPostRequestWithBody(t, context.Background(), `{}`)
    66  
    67  		handler := operation.NewUpdateOperationHandler(nil, nil, nil)
    68  		handler.ServeHTTP(writer, req)
    69  
    70  		require.Contains(t, writer.Body.String(), "Invalid operation properties")
    71  		require.Equal(t, http.StatusBadRequest, writer.Code)
    72  	})
    73  
    74  	t.Run("when transaction fails to begin it should return internal server error", func(t *testing.T) {
    75  		writer := httptest.NewRecorder()
    76  		req := fixPostRequestWithBody(t, context.Background(), fmt.Sprintf(`{"resource_id": "%s", "resource_type": "%s", "operation_type": "%s"}`, resourceID, resource.Application, operation.OperationTypeCreate))
    77  
    78  		mockedTx, mockedTransactioner := txtest.NewTransactionContextGenerator(mockedError()).ThatFailsOnBegin()
    79  		defer mockedTx.AssertExpectations(t)
    80  		defer mockedTransactioner.AssertExpectations(t)
    81  
    82  		handler := operation.NewUpdateOperationHandler(mockedTransactioner, map[resource.Type]operation.ResourceUpdaterFunc{
    83  			resource.Application: func(ctx context.Context, id string, ready bool, errorMsg *string, appConditionStatus model.ApplicationStatusCondition) error {
    84  				return nil
    85  			},
    86  		}, nil)
    87  		handler.ServeHTTP(writer, req)
    88  
    89  		require.Equal(t, http.StatusInternalServerError, writer.Code)
    90  		require.Contains(t, writer.Body.String(), "Unable to establish connection with database")
    91  	})
    92  
    93  	t.Run("when transaction fails to commit it should return internal server error", func(t *testing.T) {
    94  		writer := httptest.NewRecorder()
    95  		req := fixPostRequestWithBody(t, context.Background(), fmt.Sprintf(`{"resource_id": "%s", "resource_type": "%s", "operation_type": "%s"}`, resourceID, resource.Application, operation.OperationTypeCreate))
    96  
    97  		mockedTx, mockedTransactioner := txtest.NewTransactionContextGenerator(mockedError()).ThatFailsOnCommit()
    98  		defer mockedTx.AssertExpectations(t)
    99  		defer mockedTransactioner.AssertExpectations(t)
   100  
   101  		handler := operation.NewUpdateOperationHandler(mockedTransactioner, map[resource.Type]operation.ResourceUpdaterFunc{
   102  			resource.Application: func(ctx context.Context, id string, ready bool, errorMsg *string, appConditionStatus model.ApplicationStatusCondition) error {
   103  				return nil
   104  			},
   105  		}, nil)
   106  		handler.ServeHTTP(writer, req)
   107  
   108  		require.Equal(t, http.StatusInternalServerError, writer.Code)
   109  		require.Contains(t, writer.Body.String(), "Unable to finalize database operation")
   110  	})
   111  
   112  	t.Run("when update handler fails on CREATE/UPDATE operation", func(t *testing.T) {
   113  		writer := httptest.NewRecorder()
   114  		req := fixPostRequestWithBody(t, context.Background(), fmt.Sprintf(`{"resource_id": "%s", "resource_type": "%s", "operation_type": "%s"}`, resourceID, resource.Application, operation.OperationTypeCreate))
   115  
   116  		mockedTx, mockedTransactioner := txtest.NewTransactionContextGenerator(nil).ThatDoesntExpectCommit()
   117  		defer mockedTx.AssertExpectations(t)
   118  		defer mockedTransactioner.AssertExpectations(t)
   119  
   120  		handler := operation.NewUpdateOperationHandler(mockedTransactioner, map[resource.Type]operation.ResourceUpdaterFunc{
   121  			resource.Application: func(ctx context.Context, id string, ready bool, errorMsg *string, appConditionStatus model.ApplicationStatusCondition) error {
   122  				return errors.New("failed to update")
   123  			},
   124  		}, nil)
   125  		handler.ServeHTTP(writer, req)
   126  
   127  		mockedTx.AssertNotCalled(t, "Commit")
   128  		require.Equal(t, http.StatusInternalServerError, writer.Code)
   129  		require.Contains(t, writer.Body.String(), "Unable to update resource application with id")
   130  	})
   131  
   132  	t.Run("when delete handler fails on DELETE operation", func(t *testing.T) {
   133  		writer := httptest.NewRecorder()
   134  		req := fixPostRequestWithBody(t, context.Background(), fmt.Sprintf(`{"resource_id": "%s", "resource_type": "%s", "operation_type": "%s"}`, resourceID, resource.Application, operation.OperationTypeDelete))
   135  
   136  		mockedTx, mockedTransactioner := txtest.NewTransactionContextGenerator(nil).ThatDoesntExpectCommit()
   137  		defer mockedTx.AssertExpectations(t)
   138  		defer mockedTransactioner.AssertExpectations(t)
   139  
   140  		handler := operation.NewUpdateOperationHandler(mockedTransactioner, nil, map[resource.Type]operation.ResourceDeleterFunc{
   141  			resource.Application: func(ctx context.Context, id string) error {
   142  				return errors.New("failed to delete")
   143  			},
   144  		})
   145  		handler.ServeHTTP(writer, req)
   146  
   147  		mockedTx.AssertNotCalled(t, "Commit")
   148  		require.Equal(t, http.StatusInternalServerError, writer.Code)
   149  		require.Contains(t, writer.Body.String(), "Unable to delete resource application with id")
   150  	})
   151  
   152  	t.Run("when operation has finished", func(t *testing.T) {
   153  		type testCase struct {
   154  			Name               string
   155  			OperationType      operation.OperationType
   156  			ExpectedError      string
   157  			Ready              bool
   158  			AppConditionStatus model.ApplicationStatusCondition
   159  			UpdateCalled       int
   160  			DeleteCalled       int
   161  		}
   162  		cases := []testCase{
   163  			{
   164  				Name:               "CREATE with error",
   165  				OperationType:      operation.OperationTypeCreate,
   166  				ExpectedError:      "operation failed",
   167  				Ready:              true,
   168  				AppConditionStatus: model.ApplicationStatusConditionCreateFailed,
   169  				UpdateCalled:       1,
   170  			},
   171  			{
   172  				Name:               "CREATE with NO error",
   173  				OperationType:      operation.OperationTypeCreate,
   174  				Ready:              true,
   175  				AppConditionStatus: model.ApplicationStatusConditionCreateSucceeded,
   176  				UpdateCalled:       1,
   177  			},
   178  			{
   179  				Name:               "UPDATE with error",
   180  				OperationType:      operation.OperationTypeUpdate,
   181  				ExpectedError:      "operation UPDATE failed",
   182  				Ready:              true,
   183  				AppConditionStatus: model.ApplicationStatusConditionUpdateFailed,
   184  				UpdateCalled:       1,
   185  			},
   186  			{
   187  				Name:               "UPDATE with NO error",
   188  				OperationType:      operation.OperationTypeUpdate,
   189  				Ready:              true,
   190  				AppConditionStatus: model.ApplicationStatusConditionUpdateSucceeded,
   191  				UpdateCalled:       1,
   192  			},
   193  			{
   194  				Name:               "DELETE with error",
   195  				OperationType:      operation.OperationTypeDelete,
   196  				ExpectedError:      "operation DELETE failed",
   197  				Ready:              true,
   198  				AppConditionStatus: model.ApplicationStatusConditionDeleteFailed,
   199  				UpdateCalled:       1,
   200  			},
   201  			{
   202  				Name:          "DELETE with NO error",
   203  				OperationType: operation.OperationTypeDelete,
   204  				DeleteCalled:  1,
   205  			},
   206  		}
   207  
   208  		mockedTx, mockedTransactioner := txtest.NewTransactionContextGenerator(nil).ThatSucceedsMultipleTimes(len(cases))
   209  		defer mockedTx.AssertExpectations(t)
   210  		defer mockedTransactioner.AssertExpectations(t)
   211  
   212  		for _, testCase := range cases {
   213  			t.Run(testCase.Name, func(t *testing.T) {
   214  				writer := httptest.NewRecorder()
   215  				expectedErrorMsg := testCase.ExpectedError
   216  				req := fixPostRequestWithBody(t, context.Background(), fmt.Sprintf(`{"resource_id": "%s", "resource_type": "%s", "operation_type": "%s", "error": "%s"}`, resourceID, resource.Application, testCase.OperationType, expectedErrorMsg))
   217  
   218  				updateCalled := 0
   219  				deleteCalled := 0
   220  				handler := operation.NewUpdateOperationHandler(mockedTransactioner, map[resource.Type]operation.ResourceUpdaterFunc{
   221  					resource.Application: func(ctx context.Context, id string, ready bool, errorMsg *string, appConditionStatus model.ApplicationStatusCondition) error {
   222  						require.Equal(t, resourceID, id)
   223  						require.Equal(t, testCase.Ready, ready)
   224  						require.Equal(t, testCase.AppConditionStatus, appConditionStatus)
   225  						if expectedErrorMsg == "" {
   226  							require.Nil(t, errorMsg)
   227  						} else {
   228  							require.Equal(t, fmt.Sprintf("{\"error\":%q}", expectedErrorMsg), *errorMsg)
   229  						}
   230  						updateCalled++
   231  						return nil
   232  					},
   233  				}, map[resource.Type]operation.ResourceDeleterFunc{
   234  					resource.Application: func(ctx context.Context, id string) error {
   235  						require.Equal(t, resourceID, id)
   236  						deleteCalled++
   237  						return nil
   238  					},
   239  				})
   240  
   241  				handler.ServeHTTP(writer, req)
   242  				require.Equal(t, testCase.UpdateCalled, updateCalled)
   243  				require.Equal(t, testCase.DeleteCalled, deleteCalled)
   244  				require.Equal(t, http.StatusOK, writer.Code)
   245  			})
   246  		}
   247  	})
   248  }
   249  
   250  func fixPostRequestWithBody(t *testing.T, ctx context.Context, body string) *http.Request {
   251  	reader := bytes.NewReader([]byte(body))
   252  	req, err := http.NewRequestWithContext(ctx, http.MethodPut, "/", reader)
   253  	require.NoError(t, err)
   254  
   255  	return req
   256  }