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 }