github.com/kyma-incubator/compass/components/director@v0.0.0-20230623144113-d764f56ff805/pkg/operation/api_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 "context" 21 "encoding/json" 22 "net/http" 23 "net/http/httptest" 24 "path" 25 "strings" 26 "testing" 27 "time" 28 29 "github.com/kyma-incubator/compass/components/director/internal/model" 30 "github.com/kyma-incubator/compass/components/director/pkg/apperrors" 31 32 "github.com/gorilla/mux" 33 34 "github.com/kyma-incubator/compass/components/director/internal/domain/tenant" 35 "github.com/kyma-incubator/compass/components/director/pkg/operation" 36 "github.com/kyma-incubator/compass/components/director/pkg/persistence/txtest" 37 "github.com/kyma-incubator/compass/components/director/pkg/resource" 38 "github.com/stretchr/testify/require" 39 ) 40 41 const ( 42 tenantID = "d6f1d2bb-62f5-4971-9efe-8af93d6528a7" 43 resourceID = "46eb9542-8b18-4e4d-96d1-67f7e9675bb2" 44 ) 45 46 func TestServeHTTP(t *testing.T) { 47 t.Run("when tenant is missing it should return internal server error", func(t *testing.T) { 48 writer := httptest.NewRecorder() 49 req := fixEmptyRequest(t, context.Background(), string(resource.Application), resourceID) 50 51 handler := operation.NewHandler(nil, nil, func(ctx context.Context) (string, error) { 52 return "", mockedError() 53 }) 54 handler.ServeHTTP(writer, req) 55 56 require.Contains(t, writer.Body.String(), "Unable to determine tenant for request") 57 require.Equal(t, http.StatusInternalServerError, writer.Code) 58 }) 59 60 t.Run("when resourceID and resourceType are missing it should return bad request", func(t *testing.T) { 61 ctx := tenant.SaveToContext(context.Background(), tenantID, tenantID) 62 63 writer := httptest.NewRecorder() 64 req := fixEmptyRequest(t, ctx, "", "") 65 66 handler := operation.NewHandler(nil, nil, loadTenantFunc) 67 handler.ServeHTTP(writer, req) 68 69 require.Contains(t, writer.Body.String(), "Unexpected resource type and/or GUID") 70 require.Equal(t, http.StatusBadRequest, writer.Code) 71 }) 72 73 t.Run("when resource ID is not GUID it should return bad request", func(t *testing.T) { 74 ctx := tenant.SaveToContext(context.Background(), tenantID, tenantID) 75 76 writer := httptest.NewRecorder() 77 req := fixEmptyRequest(t, ctx, string(resource.Application), "123") 78 79 handler := operation.NewHandler(nil, nil, loadTenantFunc) 80 handler.ServeHTTP(writer, req) 81 82 require.Contains(t, writer.Body.String(), "Unexpected resource type and/or GUID") 83 require.Equal(t, http.StatusBadRequest, writer.Code) 84 }) 85 86 t.Run("when resource type is not application it should return bad request", func(t *testing.T) { 87 ctx := tenant.SaveToContext(context.Background(), tenantID, tenantID) 88 89 writer := httptest.NewRecorder() 90 req := fixEmptyRequest(t, ctx, string(resource.Runtime), resourceID) 91 92 queryValues := req.URL.Query() 93 queryValues.Add(operation.ResourceTypeParam, "runtime") 94 95 req.URL.RawQuery = queryValues.Encode() 96 97 handler := operation.NewHandler(nil, nil, loadTenantFunc) 98 handler.ServeHTTP(writer, req) 99 100 require.Contains(t, writer.Body.String(), "Unexpected resource type and/or GUID") 101 require.Equal(t, http.StatusBadRequest, writer.Code) 102 }) 103 104 t.Run("when transaction fails to begin it should return internal server error", func(t *testing.T) { 105 ctx := tenant.SaveToContext(context.Background(), tenantID, tenantID) 106 107 writer := httptest.NewRecorder() 108 req := fixEmptyRequest(t, ctx, string(resource.Application), resourceID) 109 110 mockedTx, mockedTransactioner := txtest.NewTransactionContextGenerator(mockedError()).ThatFailsOnBegin() 111 defer mockedTx.AssertExpectations(t) 112 defer mockedTransactioner.AssertExpectations(t) 113 114 handler := operation.NewHandler(mockedTransactioner, nil, loadTenantFunc) 115 handler.ServeHTTP(writer, req) 116 117 require.Equal(t, http.StatusInternalServerError, writer.Code) 118 require.Contains(t, writer.Body.String(), "Unable to establish connection with database") 119 }) 120 121 t.Run("when resource fetcher func fails to fetch missing application resource it should return not found", func(t *testing.T) { 122 ctx := tenant.SaveToContext(context.Background(), tenantID, tenantID) 123 124 writer := httptest.NewRecorder() 125 req := fixEmptyRequest(t, ctx, string(resource.Application), resourceID) 126 127 queryValues := req.URL.Query() 128 queryValues.Add(operation.ResourceIDParam, resourceID) 129 queryValues.Add(operation.ResourceTypeParam, string(resource.Application)) 130 131 req.URL.RawQuery = queryValues.Encode() 132 133 mockedTx, mockedTransactioner := txtest.NewTransactionContextGenerator(mockedError()).ThatDoesntExpectCommit() 134 defer mockedTx.AssertExpectations(t) 135 defer mockedTransactioner.AssertExpectations(t) 136 137 handler := operation.NewHandler(mockedTransactioner, func(_ context.Context, _, _ string) (model.Entity, error) { 138 return nil, apperrors.NewNotFoundError(resource.Application, resourceID) 139 }, loadTenantFunc) 140 handler.ServeHTTP(writer, req) 141 142 require.Equal(t, http.StatusNotFound, writer.Code) 143 require.Contains(t, writer.Body.String(), "Object not found") 144 }) 145 146 t.Run("when resource fetcher func fails to fetch application it should return internal server error", func(t *testing.T) { 147 ctx := tenant.SaveToContext(context.Background(), tenantID, tenantID) 148 149 writer := httptest.NewRecorder() 150 req := fixEmptyRequest(t, ctx, string(resource.Application), resourceID) 151 152 queryValues := req.URL.Query() 153 queryValues.Add(operation.ResourceIDParam, resourceID) 154 queryValues.Add(operation.ResourceTypeParam, string(resource.Application)) 155 156 req.URL.RawQuery = queryValues.Encode() 157 158 mockedTx, mockedTransactioner := txtest.NewTransactionContextGenerator(mockedError()).ThatDoesntExpectCommit() 159 defer mockedTx.AssertExpectations(t) 160 defer mockedTransactioner.AssertExpectations(t) 161 162 handler := operation.NewHandler(mockedTransactioner, func(_ context.Context, _, _ string) (model.Entity, error) { 163 return nil, mockedError() 164 }, loadTenantFunc) 165 handler.ServeHTTP(writer, req) 166 167 require.Equal(t, http.StatusInternalServerError, writer.Code) 168 require.Contains(t, writer.Body.String(), "Unable to execute database operation") 169 }) 170 171 t.Run("when transaction fails to commit it should return internal server error ", func(t *testing.T) { 172 ctx := tenant.SaveToContext(context.Background(), tenantID, tenantID) 173 174 writer := httptest.NewRecorder() 175 req := fixEmptyRequest(t, ctx, string(resource.Application), resourceID) 176 177 queryValues := req.URL.Query() 178 queryValues.Add(operation.ResourceIDParam, resourceID) 179 queryValues.Add(operation.ResourceTypeParam, string(resource.Application)) 180 181 req.URL.RawQuery = queryValues.Encode() 182 183 mockedTx, mockedTransactioner := txtest.NewTransactionContextGenerator(mockedError()).ThatFailsOnCommit() 184 defer mockedTx.AssertExpectations(t) 185 defer mockedTransactioner.AssertExpectations(t) 186 187 handler := operation.NewHandler(mockedTransactioner, func(_ context.Context, _, _ string) (model.Entity, error) { 188 return nil, nil 189 }, loadTenantFunc) 190 handler.ServeHTTP(writer, req) 191 192 require.Equal(t, http.StatusInternalServerError, writer.Code) 193 require.Contains(t, writer.Body.String(), "Unable to finalize database operation") 194 }) 195 196 t.Run("when application is successfully fetched it should return a respective operation", func(t *testing.T) { 197 ctx := tenant.SaveToContext(context.Background(), tenantID, tenantID) 198 199 req := fixEmptyRequest(t, ctx, string(resource.Application), resourceID) 200 201 mockedErr := mockedError().Error() 202 now := time.Now() 203 type testCase struct { 204 Name string 205 Application *model.Application 206 ExpectedResponse operation.OperationResponse 207 } 208 209 cases := []testCase{ 210 { 211 Name: "Successful CREATE Operation", 212 Application: &model.Application{BaseEntity: &model.BaseEntity{ID: resourceID, CreatedAt: &now, UpdatedAt: &time.Time{}, DeletedAt: &time.Time{}, Ready: true}}, 213 ExpectedResponse: operation.OperationResponse{ 214 Operation: &operation.Operation{ 215 ResourceID: resourceID, 216 ResourceType: resource.Application, 217 OperationType: operation.OperationTypeCreate, 218 CreationTime: now, 219 }, 220 Status: operation.OperationStatusSucceeded, 221 }, 222 }, 223 { 224 Name: "Successful UPDATE Operation", 225 Application: &model.Application{BaseEntity: &model.BaseEntity{ID: resourceID, CreatedAt: &now, UpdatedAt: timeToTimePtr(now.Add(1 * time.Minute)), DeletedAt: &time.Time{}, Ready: true}}, 226 ExpectedResponse: operation.OperationResponse{ 227 Operation: &operation.Operation{ 228 ResourceID: resourceID, 229 ResourceType: resource.Application, 230 OperationType: operation.OperationTypeUpdate, 231 CreationTime: now.Add(1 * time.Minute), 232 }, 233 Status: operation.OperationStatusSucceeded, 234 }, 235 }, 236 { 237 Name: "Successful DELETE Operation", 238 Application: &model.Application{BaseEntity: &model.BaseEntity{ID: resourceID, CreatedAt: &now, UpdatedAt: &now, DeletedAt: timeToTimePtr(now.Add(1 * time.Minute)), Ready: true}}, 239 ExpectedResponse: operation.OperationResponse{ 240 Operation: &operation.Operation{ 241 ResourceID: resourceID, 242 ResourceType: resource.Application, 243 OperationType: operation.OperationTypeDelete, 244 CreationTime: now.Add(1 * time.Minute), 245 }, 246 Status: operation.OperationStatusSucceeded, 247 }, 248 }, 249 { 250 Name: "In Progress CREATE Operation", 251 Application: &model.Application{BaseEntity: &model.BaseEntity{ID: resourceID, CreatedAt: &now, UpdatedAt: &time.Time{}, DeletedAt: &time.Time{}, Ready: false}}, 252 ExpectedResponse: operation.OperationResponse{ 253 Operation: &operation.Operation{ 254 ResourceID: resourceID, 255 ResourceType: resource.Application, 256 OperationType: operation.OperationTypeCreate, 257 CreationTime: now, 258 }, 259 Status: operation.OperationStatusInProgress, 260 }, 261 }, 262 { 263 Name: "In Progress UPDATE Operation", 264 Application: &model.Application{BaseEntity: &model.BaseEntity{ID: resourceID, CreatedAt: &now, UpdatedAt: timeToTimePtr(now.Add(1 * time.Minute)), DeletedAt: &time.Time{}, Ready: false}}, 265 ExpectedResponse: operation.OperationResponse{ 266 Operation: &operation.Operation{ 267 ResourceID: resourceID, 268 ResourceType: resource.Application, 269 OperationType: operation.OperationTypeUpdate, 270 CreationTime: now.Add(1 * time.Minute), 271 }, 272 Status: operation.OperationStatusInProgress, 273 }, 274 }, 275 { 276 Name: "In Progress DELETE Operation", 277 Application: &model.Application{BaseEntity: &model.BaseEntity{ID: resourceID, CreatedAt: &now, UpdatedAt: &now, DeletedAt: timeToTimePtr(now.Add(1 * time.Minute)), Ready: false}}, 278 ExpectedResponse: operation.OperationResponse{ 279 Operation: &operation.Operation{ 280 ResourceID: resourceID, 281 ResourceType: resource.Application, 282 OperationType: operation.OperationTypeDelete, 283 CreationTime: now.Add(1 * time.Minute), 284 }, 285 Status: operation.OperationStatusInProgress, 286 }, 287 }, 288 { 289 Name: "Failed CREATE Operation", 290 Application: &model.Application{BaseEntity: &model.BaseEntity{ID: resourceID, CreatedAt: &now, UpdatedAt: &time.Time{}, DeletedAt: &time.Time{}, Ready: true, Error: &mockedErr}}, 291 ExpectedResponse: operation.OperationResponse{ 292 Operation: &operation.Operation{ 293 ResourceID: resourceID, 294 ResourceType: resource.Application, 295 OperationType: operation.OperationTypeCreate, 296 CreationTime: now, 297 }, 298 Status: operation.OperationStatusFailed, 299 Error: &mockedErr, 300 }, 301 }, 302 { 303 Name: "Failed UPDATE Operation", 304 Application: &model.Application{BaseEntity: &model.BaseEntity{ID: resourceID, CreatedAt: &now, UpdatedAt: timeToTimePtr(now.Add(1 * time.Minute)), DeletedAt: &time.Time{}, Ready: true, Error: &mockedErr}}, 305 ExpectedResponse: operation.OperationResponse{ 306 Operation: &operation.Operation{ 307 ResourceID: resourceID, 308 ResourceType: resource.Application, 309 OperationType: operation.OperationTypeUpdate, 310 CreationTime: now.Add(1 * time.Minute), 311 }, 312 Status: operation.OperationStatusFailed, 313 Error: &mockedErr, 314 }, 315 }, 316 { 317 Name: "Failed DELETE Operation", 318 Application: &model.Application{BaseEntity: &model.BaseEntity{ID: resourceID, CreatedAt: &now, UpdatedAt: &now, DeletedAt: timeToTimePtr(now.Add(1 * time.Minute)), Ready: true, Error: &mockedErr}}, 319 ExpectedResponse: operation.OperationResponse{ 320 Operation: &operation.Operation{ 321 ResourceID: resourceID, 322 ResourceType: resource.Application, 323 OperationType: operation.OperationTypeDelete, 324 CreationTime: now.Add(1 * time.Minute), 325 }, 326 Status: operation.OperationStatusFailed, 327 Error: &mockedErr, 328 }, 329 }, 330 } 331 332 mockedTx, mockedTransactioner := txtest.NewTransactionContextGenerator(nil).ThatSucceedsMultipleTimes(len(cases)) 333 defer mockedTx.AssertExpectations(t) 334 defer mockedTransactioner.AssertExpectations(t) 335 336 for _, testCase := range cases { 337 t.Run(testCase.Name, func(t *testing.T) { 338 handler := operation.NewHandler(mockedTransactioner, func(_ context.Context, _, _ string) (model.Entity, error) { 339 return testCase.Application, nil 340 }, loadTenantFunc) 341 342 writer := httptest.NewRecorder() 343 handler.ServeHTTP(writer, req) 344 345 expectedBody, err := json.Marshal(testCase.ExpectedResponse) 346 require.NoError(t, err) 347 348 require.Equal(t, http.StatusOK, writer.Code) 349 require.Equal(t, string(expectedBody), strings.TrimSpace(writer.Body.String())) 350 }) 351 } 352 }) 353 } 354 355 func fixEmptyRequest(t *testing.T, ctx context.Context, resourceType string, resourceID string) *http.Request { 356 endpointPath := path.Join("/", resourceType, resourceID) 357 req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpointPath, nil) 358 vars := map[string]string{"resource_type": resourceType, "resource_id": resourceID} 359 req = mux.SetURLVars(req, vars) 360 require.NoError(t, err) 361 362 return req 363 } 364 365 func loadTenantFunc(_ context.Context) (string, error) { 366 return tenantID, nil 367 } 368 369 func timeToTimePtr(time time.Time) *time.Time { 370 return &time 371 }