github.com/kyma-incubator/compass/components/director@v0.0.0-20230623144113-d764f56ff805/pkg/operation/directive_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 "errors" 23 "fmt" 24 "net/http" 25 "testing" 26 "time" 27 28 "github.com/kyma-incubator/compass/components/director/pkg/apperrors" 29 "github.com/kyma-incubator/compass/components/director/pkg/resource" 30 31 gqlgen "github.com/99designs/gqlgen/graphql" 32 "github.com/kyma-incubator/compass/components/director/pkg/header" 33 tx_automock "github.com/kyma-incubator/compass/components/director/pkg/persistence/automock" 34 "github.com/kyma-incubator/compass/components/director/pkg/persistence/txtest" 35 "github.com/kyma-incubator/compass/components/director/pkg/webhook" 36 "github.com/vektah/gqlparser/v2/ast" 37 38 "github.com/kyma-incubator/compass/components/director/internal/model" 39 "github.com/kyma-incubator/compass/components/director/pkg/graphql" 40 "github.com/kyma-incubator/compass/components/director/pkg/operation" 41 "github.com/kyma-incubator/compass/components/director/pkg/operation/automock" 42 "github.com/stretchr/testify/mock" 43 "github.com/stretchr/testify/require" 44 ) 45 46 const ( 47 webhookID1 = "fe8ce7c6-919f-40f0-b78b-b1662dfbac64" 48 webhookID2 = "4f40d0cf-5a33-4895-aa03-528ab0982fb2" 49 webhookID3 = "dbd54239-5188-4bea-8826-bc04587a118e" 50 ) 51 52 var ( 53 resourceIDField = "id" 54 whTypeApplicationRegister = graphql.WebhookTypeRegisterApplication 55 whTypeApplicationUnregister = graphql.WebhookTypeUnregisterApplication 56 57 mockedHeaders = http.Header{ 58 "key": []string{"value"}, 59 } 60 ) 61 62 func TestHandleOperation(t *testing.T) { 63 t.Run("missing operation mode param causes internal server error", func(t *testing.T) { 64 // GIVEN 65 ctx := context.Background() 66 rCtx := &gqlgen.FieldContext{ 67 Object: "RegisterApplication", 68 Field: gqlgen.CollectedField{}, 69 Args: map[string]interface{}{ 70 operation.ModeParam: "notModeParam", 71 }, 72 IsMethod: false, 73 } 74 ctx = gqlgen.WithFieldContext(ctx, rCtx) 75 76 directive := operation.NewDirective(nil, nil, nil, nil, nil, nil) 77 78 // WHEN 79 _, err := directive.HandleOperation(ctx, nil, nil, graphql.OperationTypeCreate, &whTypeApplicationRegister, &resourceIDField) 80 // THEN 81 require.Error(t, err, fmt.Sprintf("could not get %s parameter", operation.ModeParam)) 82 }) 83 84 t.Run("when mutation is in SYNC mode there is no operation in context but transaction fails to begin", func(t *testing.T) { 85 // GIVEN 86 ctx := context.Background() 87 operationMode := graphql.OperationModeSync 88 rCtx := &gqlgen.FieldContext{ 89 Object: "RegisterApplication", 90 Field: gqlgen.CollectedField{}, 91 Args: map[string]interface{}{operation.ModeParam: &operationMode}, 92 IsMethod: false, 93 } 94 ctx = gqlgen.WithFieldContext(ctx, rCtx) 95 96 mockedTx, mockedTransactioner := txtest.NewTransactionContextGenerator(mockedError()).ThatFailsOnBegin() 97 defer mockedTx.AssertExpectations(t) 98 defer mockedTransactioner.AssertExpectations(t) 99 100 directive := operation.NewDirective(mockedTransactioner, nil, nil, nil, nil, nil) 101 102 // WHEN 103 res, err := directive.HandleOperation(ctx, nil, nil, graphql.OperationTypeCreate, &whTypeApplicationRegister, &resourceIDField) 104 // THEN 105 require.Error(t, err, mockedError().Error(), "Unable to initialize database operation") 106 require.Empty(t, res) 107 }) 108 109 t.Run("when mutation is in SYNC mode there is no operation in context but request fails should roll-back", func(t *testing.T) { 110 // GIVEN 111 ctx := context.Background() 112 operationMode := graphql.OperationModeSync 113 rCtx := &gqlgen.FieldContext{ 114 Object: "RegisterApplication", 115 Field: gqlgen.CollectedField{}, 116 Args: map[string]interface{}{operation.ModeParam: &operationMode}, 117 IsMethod: false, 118 } 119 ctx = gqlgen.WithFieldContext(ctx, rCtx) 120 121 mockedTx, mockedTransactioner := txtest.NewTransactionContextGenerator(mockedError()).ThatDoesntExpectCommit() 122 defer mockedTx.AssertExpectations(t) 123 defer mockedTransactioner.AssertExpectations(t) 124 125 directive := operation.NewDirective(mockedTransactioner, nil, nil, nil, nil, nil) 126 127 dummyResolver := &dummyResolver{} 128 129 // WHEN 130 res, err := directive.HandleOperation(ctx, nil, dummyResolver.ErrorResolve, graphql.OperationTypeCreate, &whTypeApplicationRegister, nil) 131 // THEN 132 require.Error(t, err, mockedError().Error(), "Unable to process operation") 133 require.Empty(t, res) 134 require.Equal(t, graphql.OperationModeSync, dummyResolver.finalCtx.Value(operation.OpModeKey)) 135 }) 136 137 t.Run("when mutation is in SYNC mode there is no operation in context but transaction fails to commit should roll-back", func(t *testing.T) { 138 // GIVEN 139 ctx := context.Background() 140 operationMode := graphql.OperationModeSync 141 rCtx := &gqlgen.FieldContext{ 142 Object: "RegisterApplication", 143 Field: gqlgen.CollectedField{}, 144 Args: map[string]interface{}{operation.ModeParam: &operationMode}, 145 IsMethod: false, 146 } 147 ctx = gqlgen.WithFieldContext(ctx, rCtx) 148 149 mockedTx, mockedTransactioner := txtest.NewTransactionContextGenerator(mockedError()).ThatFailsOnCommit() 150 defer mockedTx.AssertExpectations(t) 151 defer mockedTransactioner.AssertExpectations(t) 152 153 directive := operation.NewDirective(mockedTransactioner, nil, nil, nil, nil, nil) 154 155 dummyResolver := &dummyResolver{} 156 157 // WHEN 158 res, err := directive.HandleOperation(ctx, nil, dummyResolver.SuccessResolve, graphql.OperationTypeCreate, &whTypeApplicationRegister, nil) 159 // THEN 160 require.Error(t, err, mockedError().Error(), "Unable to finalize database operation") 161 require.Empty(t, res) 162 require.Equal(t, graphql.OperationModeSync, dummyResolver.finalCtx.Value(operation.OpModeKey)) 163 }) 164 165 t.Run("when mutation is in SYNC mode there is no operation in context and finishes successfully", func(t *testing.T) { 166 // GIVEN 167 ctx := context.Background() 168 operationMode := graphql.OperationModeSync 169 rCtx := &gqlgen.FieldContext{ 170 Object: "RegisterApplication", 171 Field: gqlgen.CollectedField{}, 172 Args: map[string]interface{}{operation.ModeParam: &operationMode}, 173 IsMethod: false, 174 } 175 ctx = gqlgen.WithFieldContext(ctx, rCtx) 176 177 mockedTx, mockedTransactioner := txtest.NewTransactionContextGenerator(nil).ThatSucceeds() 178 defer mockedTx.AssertExpectations(t) 179 defer mockedTransactioner.AssertExpectations(t) 180 181 directive := operation.NewDirective(mockedTransactioner, nil, nil, nil, nil, nil) 182 183 dummyResolver := &dummyResolver{} 184 185 // WHEN 186 res, err := directive.HandleOperation(ctx, nil, dummyResolver.SuccessResolve, graphql.OperationTypeCreate, &whTypeApplicationRegister, nil) 187 // THEN 188 require.NoError(t, err) 189 require.Equal(t, mockedNextResponse(), res) 190 require.Equal(t, graphql.OperationModeSync, dummyResolver.finalCtx.Value(operation.OpModeKey)) 191 }) 192 193 t.Run("when mutation is in ASYNC mode, there is operation in context but request fails should roll-back", func(t *testing.T) { 194 // GIVEN 195 ctx := context.Background() 196 operationMode := graphql.OperationModeAsync 197 rCtx := &gqlgen.FieldContext{ 198 Object: "RegisterApplication", 199 Field: gqlgen.CollectedField{ 200 Field: &ast.Field{ 201 Name: "registerApplication", 202 }, 203 }, 204 Args: map[string]interface{}{operation.ModeParam: &operationMode}, 205 IsMethod: false, 206 } 207 ctx = gqlgen.WithFieldContext(ctx, rCtx) 208 209 mockedTx, mockedTransactioner := txtest.NewTransactionContextGenerator(nil).ThatDoesntExpectCommit() 210 defer mockedTx.AssertExpectations(t) 211 defer mockedTransactioner.AssertExpectations(t) 212 213 directive := operation.NewDirective(mockedTransactioner, nil, nil, nil, nil, nil) 214 215 dummyResolver := &dummyResolver{} 216 217 // WHEN 218 res, err := directive.HandleOperation(ctx, nil, dummyResolver.ErrorResolve, graphql.OperationTypeCreate, &whTypeApplicationRegister, nil) 219 // THEN 220 require.Error(t, err, mockedError().Error(), "Unable to process operation") 221 require.Empty(t, res) 222 require.Equal(t, graphql.OperationModeAsync, dummyResolver.finalCtx.Value(operation.OpModeKey)) 223 224 opsFromCtx := dummyResolver.finalCtx.Value(operation.OpCtxKey) 225 assertNoOperationsInCtx(t, opsFromCtx) 226 }) 227 228 t.Run("when mutation is in ASYNC mode, there is operation in context but response is not an Entity type should roll-back", func(t *testing.T) { 229 // GIVEN 230 ctx := context.Background() 231 operationMode := graphql.OperationModeAsync 232 rCtx := &gqlgen.FieldContext{ 233 Object: "RegisterApplication", 234 Field: gqlgen.CollectedField{ 235 Field: &ast.Field{ 236 Name: "registerApplication", 237 }, 238 }, 239 Args: map[string]interface{}{operation.ModeParam: &operationMode}, 240 IsMethod: false, 241 } 242 ctx = gqlgen.WithFieldContext(ctx, rCtx) 243 244 mockedTx, mockedTransactioner := txtest.NewTransactionContextGenerator(nil).ThatDoesntExpectCommit() 245 defer mockedTx.AssertExpectations(t) 246 defer mockedTransactioner.AssertExpectations(t) 247 248 directive := operation.NewDirective(mockedTransactioner, nil, nil, nil, nil, nil) 249 250 dummyResolver := &dummyResolver{} 251 252 // WHEN 253 res, err := directive.HandleOperation(ctx, nil, dummyResolver.NonEntityResolve, graphql.OperationTypeCreate, &whTypeApplicationRegister, nil) 254 // THEN 255 require.Error(t, err, mockedError().Error(), "Failed to process operation") 256 require.Empty(t, res) 257 require.Equal(t, graphql.OperationModeAsync, dummyResolver.finalCtx.Value(operation.OpModeKey)) 258 259 opsFromCtx := dummyResolver.finalCtx.Value(operation.OpCtxKey) 260 assertNoOperationsInCtx(t, opsFromCtx) 261 }) 262 263 t.Run("when mutation is in ASYNC mode, there is operation in context but server Director fails to fetch webhooks should roll-back", func(t *testing.T) { 264 // GIVEN 265 ctx := context.Background() 266 operationMode := graphql.OperationModeAsync 267 rCtx := &gqlgen.FieldContext{ 268 Object: "RegisterApplication", 269 Field: gqlgen.CollectedField{ 270 Field: &ast.Field{ 271 Name: "registerApplication", 272 }, 273 }, 274 Args: map[string]interface{}{operation.ModeParam: &operationMode}, 275 IsMethod: false, 276 } 277 ctx = gqlgen.WithFieldContext(ctx, rCtx) 278 279 mockedTx, mockedTransactioner := txtest.NewTransactionContextGenerator(nil).ThatDoesntExpectCommit() 280 defer mockedTx.AssertExpectations(t) 281 defer mockedTransactioner.AssertExpectations(t) 282 283 directive := operation.NewDirective(mockedTransactioner, func(_ context.Context, _ string) ([]*model.Webhook, error) { 284 return nil, mockedError() 285 }, nil, nil, nil, nil) 286 287 dummyResolver := &dummyResolver{} 288 289 // WHEN 290 res, err := directive.HandleOperation(ctx, nil, dummyResolver.NonEntityResolve, graphql.OperationTypeCreate, &whTypeApplicationRegister, nil) 291 // THEN 292 require.Error(t, err, mockedError().Error(), "Unable to retrieve webhooks") 293 require.Empty(t, res) 294 require.Equal(t, graphql.OperationModeAsync, dummyResolver.finalCtx.Value(operation.OpModeKey)) 295 296 opsFromCtx := dummyResolver.finalCtx.Value(operation.OpCtxKey) 297 assertNoOperationsInCtx(t, opsFromCtx) 298 }) 299 300 t.Run("when mutation is in ASYNC mode, there is operation in context but Director fails to prepare operation request due to missing tenant data should roll-back", func(t *testing.T) { 301 // GIVEN 302 ctx := context.Background() 303 operationMode := graphql.OperationModeAsync 304 rCtx := &gqlgen.FieldContext{ 305 Object: "RegisterApplication", 306 Field: gqlgen.CollectedField{ 307 Field: &ast.Field{ 308 Name: "registerApplication", 309 }, 310 }, 311 Args: map[string]interface{}{operation.ModeParam: &operationMode}, 312 IsMethod: false, 313 } 314 ctx = gqlgen.WithFieldContext(ctx, rCtx) 315 316 mockedTx, mockedTransactioner := txtest.NewTransactionContextGenerator(nil).ThatDoesntExpectCommit() 317 defer mockedTx.AssertExpectations(t) 318 defer mockedTransactioner.AssertExpectations(t) 319 320 directive := operation.NewDirective(mockedTransactioner, mockedWebhooksResponse, nil, mockedEmptyResourceUpdaterFunc, func(_ context.Context) (string, error) { 321 return "", mockedError() 322 }, nil) 323 324 dummyResolver := &dummyResolver{} 325 326 // WHEN 327 res, err := directive.HandleOperation(ctx, nil, dummyResolver.SuccessResolve, graphql.OperationTypeCreate, &whTypeApplicationRegister, nil) 328 // THEN 329 require.Error(t, err, mockedError().Error(), "Unable to prepare webhook request data") 330 require.Empty(t, res) 331 require.Equal(t, graphql.OperationModeAsync, dummyResolver.finalCtx.Value(operation.OpModeKey)) 332 333 opsFromCtx := dummyResolver.finalCtx.Value(operation.OpCtxKey) 334 assertNoOperationsInCtx(t, opsFromCtx) 335 }) 336 337 t.Run("when mutation is in ASYNC mode, there is operation in context but Director fails to prepare operation request due unsupported webhook provider type should roll-back", func(t *testing.T) { 338 // GIVEN 339 ctx := context.Background() 340 operationMode := graphql.OperationModeAsync 341 rCtx := &gqlgen.FieldContext{ 342 Object: "RegisterApplication", 343 Field: gqlgen.CollectedField{ 344 Field: &ast.Field{ 345 Name: "registerApplication", 346 }, 347 }, 348 Args: map[string]interface{}{operation.ModeParam: &operationMode}, 349 IsMethod: false, 350 } 351 ctx = gqlgen.WithFieldContext(ctx, rCtx) 352 353 mockedTx, mockedTransactioner := txtest.NewTransactionContextGenerator(nil).ThatDoesntExpectCommit() 354 defer mockedTx.AssertExpectations(t) 355 defer mockedTransactioner.AssertExpectations(t) 356 357 directive := operation.NewDirective(mockedTransactioner, mockedWebhooksResponse, nil, mockedEmptyResourceUpdaterFunc, mockedTenantLoaderFunc, nil) 358 359 dummyResolver := &dummyResolver{} 360 361 // WHEN 362 res, err := directive.HandleOperation(ctx, nil, dummyResolver.NonWebhookProviderResolve, graphql.OperationTypeCreate, &whTypeApplicationRegister, nil) 363 // THEN 364 require.Error(t, err, mockedError().Error(), "Unable to prepare webhook request data") 365 require.Empty(t, res) 366 require.Equal(t, graphql.OperationModeAsync, dummyResolver.finalCtx.Value(operation.OpModeKey)) 367 368 opsFromCtx := dummyResolver.finalCtx.Value(operation.OpCtxKey) 369 assertNoOperationsInCtx(t, opsFromCtx) 370 }) 371 372 t.Run("when mutation is in ASYNC mode, there is operation in context but Director fails to prepare operation request due failure to missing request headers should roll-back", func(t *testing.T) { 373 // GIVEN 374 ctx := context.Background() 375 operationMode := graphql.OperationModeAsync 376 rCtx := &gqlgen.FieldContext{ 377 Object: "RegisterApplication", 378 Field: gqlgen.CollectedField{ 379 Field: &ast.Field{ 380 Name: "registerApplication", 381 }, 382 }, 383 Args: map[string]interface{}{operation.ModeParam: &operationMode}, 384 IsMethod: false, 385 } 386 ctx = gqlgen.WithFieldContext(ctx, rCtx) 387 388 mockedTx, mockedTransactioner := txtest.NewTransactionContextGenerator(nil).ThatDoesntExpectCommit() 389 defer mockedTx.AssertExpectations(t) 390 defer mockedTransactioner.AssertExpectations(t) 391 392 directive := operation.NewDirective(mockedTransactioner, mockedWebhooksResponse, nil, mockedEmptyResourceUpdaterFunc, mockedTenantLoaderFunc, nil) 393 394 dummyResolver := &dummyResolver{} 395 396 // WHEN 397 res, err := directive.HandleOperation(ctx, nil, dummyResolver.SuccessResolve, graphql.OperationTypeCreate, &whTypeApplicationRegister, nil) 398 // THEN 399 require.Error(t, err, mockedError().Error(), "Unable to prepare webhook request data") 400 require.Empty(t, res) 401 require.Equal(t, graphql.OperationModeAsync, dummyResolver.finalCtx.Value(operation.OpModeKey)) 402 403 opsFromCtx := dummyResolver.finalCtx.Value(operation.OpCtxKey) 404 assertNoOperationsInCtx(t, opsFromCtx) 405 }) 406 407 t.Run("when mutation is in ASYNC mode, there is operation in context but Director fails to prepare operation request due missing webhooks", func(t *testing.T) { 408 // GIVEN 409 ctx := context.Background() 410 operationMode := graphql.OperationModeAsync 411 rCtx := &gqlgen.FieldContext{ 412 Object: "RegisterApplication", 413 Field: gqlgen.CollectedField{ 414 Field: &ast.Field{ 415 Name: "registerApplication", 416 }, 417 }, 418 Args: map[string]interface{}{operation.ModeParam: &operationMode}, 419 IsMethod: false, 420 } 421 ctx = gqlgen.WithFieldContext(ctx, rCtx) 422 423 mockedTx, mockedTransactioner := txtest.NewTransactionContextGenerator(nil).ThatDoesntExpectCommit() 424 defer mockedTx.AssertExpectations(t) 425 defer mockedTransactioner.AssertExpectations(t) 426 427 directive := operation.NewDirective(mockedTransactioner, func(_ context.Context, _ string) ([]*model.Webhook, error) { 428 return nil, mockedError() 429 }, nil, mockedEmptyResourceUpdaterFunc, mockedTenantLoaderFunc, nil) 430 431 dummyResolver := &dummyResolver{} 432 433 // WHEN 434 res, err := directive.HandleOperation(ctx, nil, dummyResolver.SuccessResolve, graphql.OperationTypeCreate, &whTypeApplicationRegister, &resourceIDField) 435 // THEN 436 require.Error(t, err, "Unable to prepare webhooks") 437 require.Empty(t, res) 438 require.Equal(t, graphql.OperationModeAsync, dummyResolver.finalCtx.Value(operation.OpModeKey)) 439 }) 440 441 t.Run("when mutation is in ASYNC mode, there is operation in context but Director fails to prepare operation request due multiple webhooks found", func(t *testing.T) { 442 // GIVEN 443 ctx := context.Background() 444 operationMode := graphql.OperationModeAsync 445 rCtx := &gqlgen.FieldContext{ 446 Object: "RegisterApplication", 447 Field: gqlgen.CollectedField{ 448 Field: &ast.Field{ 449 Name: "registerApplication", 450 }, 451 }, 452 Args: map[string]interface{}{operation.ModeParam: &operationMode}, 453 IsMethod: false, 454 } 455 ctx = gqlgen.WithFieldContext(ctx, rCtx) 456 457 mockedTx, mockedTransactioner := txtest.NewTransactionContextGenerator(nil).ThatDoesntExpectCommit() 458 defer mockedTx.AssertExpectations(t) 459 defer mockedTransactioner.AssertExpectations(t) 460 461 directive := operation.NewDirective(mockedTransactioner, func(_ context.Context, _ string) ([]*model.Webhook, error) { 462 return []*model.Webhook{ 463 {ID: webhookID1, Type: model.WebhookTypeRegisterApplication}, 464 {ID: webhookID2, Type: model.WebhookTypeRegisterApplication}, 465 {ID: webhookID3, Type: model.WebhookTypeRegisterApplication}, 466 }, nil 467 }, nil, mockedEmptyResourceUpdaterFunc, mockedTenantLoaderFunc, nil) 468 469 dummyResolver := &dummyResolver{} 470 471 // WHEN 472 res, err := directive.HandleOperation(ctx, nil, dummyResolver.SuccessResolve, graphql.OperationTypeCreate, &whTypeApplicationRegister, &resourceIDField) 473 // THEN 474 require.Error(t, err, "Unable to prepare webhooks") 475 require.Empty(t, res) 476 require.Equal(t, graphql.OperationModeAsync, dummyResolver.finalCtx.Value(operation.OpModeKey)) 477 }) 478 479 t.Run("when mutation is in ASYNC mode, there is operation in context but Scheduler fails to schedule should roll-back", func(t *testing.T) { 480 // GIVEN 481 ctx := context.Background() 482 operationMode := graphql.OperationModeAsync 483 rCtx := &gqlgen.FieldContext{ 484 Object: "RegisterApplication", 485 Field: gqlgen.CollectedField{ 486 Field: &ast.Field{ 487 Name: "registerApplication", 488 }, 489 }, 490 Args: map[string]interface{}{operation.ModeParam: &operationMode}, 491 IsMethod: false, 492 } 493 ctx = gqlgen.WithFieldContext(ctx, rCtx) 494 ctx = context.WithValue(ctx, header.ContextKey, mockedHeaders) 495 mockedTx, mockedTransactioner := txtest.NewTransactionContextGenerator(nil).ThatDoesntExpectCommit() 496 defer mockedTx.AssertExpectations(t) 497 defer mockedTransactioner.AssertExpectations(t) 498 499 mockedScheduler := &automock.Scheduler{} 500 mockedScheduler.On("Schedule", mock.Anything, mock.Anything).Return("", mockedError()) 501 defer mockedScheduler.AssertExpectations(t) 502 503 directive := operation.NewDirective(mockedTransactioner, mockedWebhooksResponse, nil, mockedEmptyResourceUpdaterFunc, mockedTenantLoaderFunc, mockedScheduler) 504 505 dummyResolver := &dummyResolver{} 506 507 // WHEN 508 res, err := directive.HandleOperation(ctx, nil, dummyResolver.SuccessResolve, graphql.OperationTypeCreate, &whTypeApplicationRegister, nil) 509 // THEN 510 require.Error(t, err, mockedError().Error(), "Unable to schedule operation") 511 require.Empty(t, res) 512 require.Equal(t, graphql.OperationModeAsync, dummyResolver.finalCtx.Value(operation.OpModeKey)) 513 514 opsFromCtx := dummyResolver.finalCtx.Value(operation.OpCtxKey) 515 assertNoOperationsInCtx(t, opsFromCtx) 516 }) 517 518 t.Run("when mutation is in ASYNC mode, there is operation in context but transaction commit fails should roll-back", func(t *testing.T) { 519 // GIVEN 520 ctx := context.Background() 521 operationMode := graphql.OperationModeAsync 522 rCtx := &gqlgen.FieldContext{ 523 Object: "RegisterApplication", 524 Field: gqlgen.CollectedField{ 525 Field: &ast.Field{ 526 Name: "registerApplication", 527 }, 528 }, 529 Args: map[string]interface{}{operation.ModeParam: &operationMode}, 530 IsMethod: false, 531 } 532 ctx = gqlgen.WithFieldContext(ctx, rCtx) 533 ctx = context.WithValue(ctx, header.ContextKey, mockedHeaders) 534 535 mockedTx, mockedTransactioner := txtest.NewTransactionContextGenerator(mockedError()).ThatFailsOnCommit() 536 defer mockedTx.AssertExpectations(t) 537 defer mockedTransactioner.AssertExpectations(t) 538 539 testID := "test-id" 540 mockedScheduler := &automock.Scheduler{} 541 mockedScheduler.On("Schedule", mock.Anything, mock.Anything).Return(testID, nil) 542 defer mockedScheduler.AssertExpectations(t) 543 544 directive := operation.NewDirective(mockedTransactioner, mockedWebhooksResponse, nil, mockedEmptyResourceUpdaterFunc, mockedTenantLoaderFunc, mockedScheduler) 545 546 dummyResolver := &dummyResolver{} 547 548 // WHEN 549 res, err := directive.HandleOperation(ctx, nil, dummyResolver.SuccessResolve, graphql.OperationTypeCreate, &whTypeApplicationRegister, nil) 550 // THEN 551 require.Error(t, err, mockedError().Error(), "Unable to finalize database operation") 552 require.Empty(t, res) 553 require.Equal(t, graphql.OperationModeAsync, dummyResolver.finalCtx.Value(operation.OpModeKey)) 554 555 opsFromCtx := dummyResolver.finalCtx.Value(operation.OpCtxKey) 556 assertNoOperationsInCtx(t, opsFromCtx) 557 }) 558 559 t.Run("when async mode is disabled it should roll-back", func(t *testing.T) { 560 operationType := graphql.OperationTypeCreate 561 operationCategory := "registerApplication" 562 563 // GIVEN 564 ctx := context.Background() 565 operationMode := graphql.OperationModeAsync 566 rCtx := &gqlgen.FieldContext{ 567 Object: "RegisterApplication", 568 Field: gqlgen.CollectedField{ 569 Field: &ast.Field{ 570 Name: operationCategory, 571 }, 572 }, 573 Args: map[string]interface{}{operation.ModeParam: &operationMode}, 574 IsMethod: false, 575 } 576 ctx = gqlgen.WithFieldContext(ctx, rCtx) 577 ctx = context.WithValue(ctx, header.ContextKey, mockedHeaders) 578 579 mockedTx, mockedTransactioner := txtest.NewTransactionContextGenerator(nil).ThatDoesntExpectCommit() 580 defer mockedTx.AssertExpectations(t) 581 defer mockedTransactioner.AssertExpectations(t) 582 dummyResolver := &dummyResolver{} 583 scheduler := &operation.DisabledScheduler{} 584 directive := operation.NewDirective(mockedTransactioner, mockedWebhooksResponse, nil, mockedEmptyResourceUpdaterFunc, mockedTenantLoaderFunc, scheduler) 585 586 // WHEN 587 _, err := directive.HandleOperation(ctx, nil, dummyResolver.SuccessResolve, operationType, nil, nil) 588 589 // THEN 590 require.Error(t, err) 591 require.Contains(t, err.Error(), "Unable to schedule operation") 592 }) 593 594 t.Run("when mutation is in ASYNC mode, there is operation in context but webhook fetcher fails should roll-back", func(t *testing.T) { 595 operationType := graphql.OperationTypeCreate 596 operationCategory := "registerApplication" 597 598 // GIVEN 599 ctx := context.Background() 600 operationMode := graphql.OperationModeAsync 601 rCtx := &gqlgen.FieldContext{ 602 Object: "RegisterApplication", 603 Field: gqlgen.CollectedField{ 604 Field: &ast.Field{ 605 Name: operationCategory, 606 }, 607 }, 608 Args: map[string]interface{}{operation.ModeParam: &operationMode}, 609 IsMethod: false, 610 } 611 ctx = gqlgen.WithFieldContext(ctx, rCtx) 612 ctx = context.WithValue(ctx, header.ContextKey, mockedHeaders) 613 614 mockedTx, mockedTransactioner := txtest.NewTransactionContextGenerator(nil).ThatDoesntExpectCommit() 615 defer mockedTx.AssertExpectations(t) 616 defer mockedTransactioner.AssertExpectations(t) 617 618 dummyResolver := &dummyResolver{} 619 620 errorWebhooksResponse := func(_ context.Context, _ string) ([]*model.Webhook, error) { 621 return nil, errors.New("fail") 622 } 623 624 mockedScheduler := &automock.Scheduler{} 625 defer mockedScheduler.AssertExpectations(t) 626 627 directive := operation.NewDirective(mockedTransactioner, errorWebhooksResponse, nil, mockedEmptyResourceUpdaterFunc, mockedTenantLoaderFunc, mockedScheduler) 628 629 // WHEN 630 _, err := directive.HandleOperation(ctx, nil, dummyResolver.SuccessResolve, operationType, &whTypeApplicationRegister, nil) 631 // THEN 632 require.Error(t, err) 633 require.Contains(t, err.Error(), "Unable to retrieve webhooks") 634 }) 635 636 t.Run("when mutation is in ASYNC mode, there is operation in context and finishes successfully", func(t *testing.T) { 637 operationType := operation.OperationTypeCreate 638 operationCategory := "registerApplication" 639 640 // GIVEN 641 ctx := context.Background() 642 operationMode := graphql.OperationModeAsync 643 rCtx := &gqlgen.FieldContext{ 644 Object: "RegisterApplication", 645 Field: gqlgen.CollectedField{ 646 Field: &ast.Field{ 647 Name: operationCategory, 648 }, 649 }, 650 Args: map[string]interface{}{operation.ModeParam: &operationMode}, 651 IsMethod: false, 652 } 653 ctx = gqlgen.WithFieldContext(ctx, rCtx) 654 ctx = context.WithValue(ctx, header.ContextKey, mockedHeaders) 655 656 mockedScheduler := &automock.Scheduler{} 657 mockedScheduler.On("Schedule", mock.Anything, mock.Anything).Return(operationID, nil) 658 defer mockedScheduler.AssertExpectations(t) 659 660 webhookType := whTypeApplicationRegister 661 662 testCases := []struct { 663 Name string 664 Webhooks []*model.Webhook 665 ExpectedWebhookIDs []string 666 }{ 667 { 668 Name: "when all webhooks match their IDs should be present in the operation", 669 Webhooks: []*model.Webhook{ 670 {ID: webhookID1, Type: model.WebhookType(webhookType)}, 671 }, 672 ExpectedWebhookIDs: []string{webhookID1}, 673 }, 674 { 675 Name: "when a single webhook matches its ID should be present in the operation", 676 Webhooks: []*model.Webhook{ 677 {ID: webhookID1, Type: model.WebhookType(webhookType)}, 678 {ID: webhookID2, Type: model.WebhookType(graphql.WebhookTypeUnregisterApplication)}, 679 {ID: webhookID3, Type: model.WebhookType(graphql.WebhookTypeUnregisterApplication)}, 680 }, 681 ExpectedWebhookIDs: []string{webhookID1}, 682 }, 683 } 684 685 mockedTx, mockedTransactioner := txtest.NewTransactionContextGenerator(nil).ThatSucceedsMultipleTimes(len(testCases)) 686 defer mockedTx.AssertExpectations(t) 687 defer mockedTransactioner.AssertExpectations(t) 688 689 for _, testCase := range testCases { 690 t.Run(testCase.Name, func(t *testing.T) { 691 directive := operation.NewDirective(mockedTransactioner, func(_ context.Context, _ string) ([]*model.Webhook, error) { 692 return testCase.Webhooks, nil 693 }, nil, mockedEmptyResourceUpdaterFunc, mockedTenantLoaderFunc, mockedScheduler) 694 695 dummyResolver := &dummyResolver{} 696 697 // WHEN 698 res, err := directive.HandleOperation(ctx, nil, dummyResolver.SuccessResolve, graphql.OperationTypeCreate, &webhookType, nil) 699 700 // THEN 701 require.NoError(t, err) 702 require.Equal(t, mockedNextResponse(), res) 703 require.Equal(t, graphql.OperationModeAsync, dummyResolver.finalCtx.Value(operation.OpModeKey)) 704 705 opsFromCtx := dummyResolver.finalCtx.Value(operation.OpCtxKey) 706 operations, ok := opsFromCtx.(*[]*operation.Operation) 707 require.True(t, ok) 708 require.Len(t, *operations, 1) 709 710 op := (*operations)[0] 711 require.Equal(t, operationID, op.OperationID) 712 require.Equal(t, operationType, op.OperationType) 713 require.Equal(t, operationCategory, op.OperationCategory) 714 715 headers := make(map[string]string) 716 for key, value := range mockedHeaders { 717 headers[key] = value[0] 718 } 719 720 expectedRequestObject := &webhook.ApplicationLifecycleWebhookRequestObject{ 721 Application: mockedNextResponse().(webhook.Resource), 722 TenantID: tenantID, 723 Headers: headers, 724 } 725 726 expectedObj, err := json.Marshal(expectedRequestObject) 727 require.NoError(t, err) 728 729 require.Equal(t, string(expectedObj), op.RequestObject) 730 731 require.Len(t, op.WebhookIDs, len(testCase.ExpectedWebhookIDs)) 732 require.Equal(t, testCase.ExpectedWebhookIDs, op.WebhookIDs) 733 }) 734 } 735 }) 736 737 t.Run("when mutation is in ASYNC mode, there is operation in context and resource updater func fails should return error", func(t *testing.T) { 738 // GIVEN 739 ctx := context.Background() 740 operationMode := graphql.OperationModeAsync 741 rCtx := &gqlgen.FieldContext{ 742 Object: "RegisterApplication", 743 Field: gqlgen.CollectedField{ 744 Field: &ast.Field{ 745 Name: "registerApplication", 746 }, 747 }, 748 Args: map[string]interface{}{operation.ModeParam: &operationMode}, 749 IsMethod: false, 750 } 751 ctx = gqlgen.WithFieldContext(ctx, rCtx) 752 753 mockedTx, mockedTransactioner := txtest.NewTransactionContextGenerator(nil).ThatDoesntExpectCommit() 754 defer mockedTx.AssertExpectations(t) 755 defer mockedTransactioner.AssertExpectations(t) 756 757 directive := operation.NewDirective(mockedTransactioner, mockedWebhooksResponse, nil, mockedResourceUpdaterFuncWithError, mockedTenantLoaderFunc, nil) 758 759 dummyResolver := &dummyResolver{} 760 761 // WHEN 762 res, err := directive.HandleOperation(ctx, nil, dummyResolver.SuccessResolve, graphql.OperationTypeCreate, &whTypeApplicationRegister, nil) 763 // THEN 764 require.Error(t, err) 765 require.Contains(t, err.Error(), "Unable to update resource application with id") 766 require.Empty(t, res) 767 require.Equal(t, graphql.OperationModeAsync, dummyResolver.finalCtx.Value(operation.OpModeKey)) 768 769 opsFromCtx := dummyResolver.finalCtx.Value(operation.OpCtxKey) 770 assertNoOperationsInCtx(t, opsFromCtx) 771 }) 772 773 t.Run("when mutation is in ASYNC mode, there is operation in context and resource updater func is executed with CREATE operation type should finish successfully and update application status to CREATING", func(t *testing.T) { 774 // GIVEN 775 ctx := context.Background() 776 operationMode := graphql.OperationModeAsync 777 operationCategory := "registerApplication" 778 rCtx := &gqlgen.FieldContext{ 779 Object: "RegisterApplication", 780 Field: gqlgen.CollectedField{ 781 Field: &ast.Field{ 782 Name: operationCategory, 783 }, 784 }, 785 Args: map[string]interface{}{operation.ModeParam: &operationMode, resourceIDField: resourceID}, 786 IsMethod: false, 787 } 788 789 ctx = gqlgen.WithFieldContext(ctx, rCtx) 790 ctx = context.WithValue(ctx, header.ContextKey, mockedHeaders) 791 792 mockedScheduler := &automock.Scheduler{} 793 mockedScheduler.On("Schedule", mock.Anything, mock.Anything).Return(operationID, nil) 794 defer mockedScheduler.AssertExpectations(t) 795 796 mockedTx, mockedTransactioner := txtest.NewTransactionContextGenerator(nil).ThatSucceeds() 797 defer mockedTx.AssertExpectations(t) 798 defer mockedTransactioner.AssertExpectations(t) 799 800 directive := operation.NewDirective(mockedTransactioner, mockedWebhooksResponse, mockedResourceFetcherFunc, func(ctx context.Context, id string, ready bool, errorMsg *string, appStatusCondition model.ApplicationStatusCondition) error { 801 require.NotNil(t, ctx) 802 require.Equal(t, resourceID, id) 803 require.Equal(t, false, ready) 804 require.Nil(t, errorMsg) 805 require.Equal(t, model.ApplicationStatusConditionCreating, appStatusCondition) 806 return nil 807 }, mockedTenantLoaderFunc, mockedScheduler) 808 809 dummyResolver := &dummyResolver{} 810 811 // WHEN 812 res, err := directive.HandleOperation(ctx, nil, dummyResolver.SuccessResolve, graphql.OperationTypeCreate, &whTypeApplicationRegister, &resourceIDField) 813 814 // THEN 815 require.NoError(t, err) 816 require.Equal(t, mockedNextResponse(), res) 817 require.Equal(t, graphql.OperationModeAsync, dummyResolver.finalCtx.Value(operation.OpModeKey)) 818 819 opsFromCtx := dummyResolver.finalCtx.Value(operation.OpCtxKey) 820 operations, ok := opsFromCtx.(*[]*operation.Operation) 821 require.True(t, ok) 822 require.Len(t, *operations, 1) 823 824 op := (*operations)[0] 825 require.Equal(t, operationID, op.OperationID) 826 require.Equal(t, operation.OperationTypeCreate, op.OperationType) 827 require.Equal(t, operationCategory, op.OperationCategory) 828 829 headers := make(map[string]string) 830 for key, value := range mockedHeaders { 831 headers[key] = value[0] 832 } 833 834 expectedRequestObject := &webhook.ApplicationLifecycleWebhookRequestObject{ 835 Application: mockedNextResponse().(webhook.Resource), 836 TenantID: tenantID, 837 Headers: headers, 838 } 839 840 expectedObj, err := json.Marshal(expectedRequestObject) 841 require.NoError(t, err) 842 843 require.Equal(t, string(expectedObj), op.RequestObject) 844 845 require.Len(t, op.WebhookIDs, 1) 846 require.Equal(t, webhookID1, op.WebhookIDs[0]) 847 }) 848 849 t.Run("when mutation is in ASYNC mode, and no webhooks are provided operation should finish successfully and update application status to CREATING", func(t *testing.T) { 850 // GIVEN 851 ctx := context.Background() 852 operationMode := graphql.OperationModeAsync 853 operationCategory := "registerApplication" 854 rCtx := &gqlgen.FieldContext{ 855 Object: "RegisterApplication", 856 Field: gqlgen.CollectedField{ 857 Field: &ast.Field{ 858 Name: operationCategory, 859 }, 860 }, 861 Args: map[string]interface{}{operation.ModeParam: &operationMode, resourceIDField: resourceID}, 862 IsMethod: false, 863 } 864 865 ctx = gqlgen.WithFieldContext(ctx, rCtx) 866 ctx = context.WithValue(ctx, header.ContextKey, mockedHeaders) 867 868 mockedScheduler := &automock.Scheduler{} 869 mockedScheduler.On("Schedule", mock.Anything, mock.Anything).Return(operationID, nil) 870 defer mockedScheduler.AssertExpectations(t) 871 872 mockedTx, mockedTransactioner := txtest.NewTransactionContextGenerator(nil).ThatSucceeds() 873 defer mockedTx.AssertExpectations(t) 874 defer mockedTransactioner.AssertExpectations(t) 875 876 directive := operation.NewDirective(mockedTransactioner, mockedEmptyWebhooksResponse, mockedResourceFetcherFunc, func(ctx context.Context, id string, ready bool, errorMsg *string, appStatusCondition model.ApplicationStatusCondition) error { 877 require.NotNil(t, ctx) 878 require.Equal(t, resourceID, id) 879 require.Equal(t, false, ready) 880 require.Nil(t, errorMsg) 881 require.Equal(t, model.ApplicationStatusConditionCreating, appStatusCondition) 882 return nil 883 }, mockedTenantLoaderFunc, mockedScheduler) 884 885 dummyResolver := &dummyResolver{} 886 887 // WHEN 888 res, err := directive.HandleOperation(ctx, nil, dummyResolver.SuccessResolve, graphql.OperationTypeCreate, &whTypeApplicationRegister, &resourceIDField) 889 890 // THEN 891 require.NoError(t, err) 892 require.Equal(t, mockedNextResponse(), res) 893 require.Equal(t, graphql.OperationModeAsync, dummyResolver.finalCtx.Value(operation.OpModeKey)) 894 895 opsFromCtx := dummyResolver.finalCtx.Value(operation.OpCtxKey) 896 operations, ok := opsFromCtx.(*[]*operation.Operation) 897 require.True(t, ok) 898 require.Len(t, *operations, 1) 899 900 op := (*operations)[0] 901 require.Equal(t, operationID, op.OperationID) 902 require.Equal(t, operation.OperationTypeCreate, op.OperationType) 903 require.Equal(t, operationCategory, op.OperationCategory) 904 905 headers := make(map[string]string) 906 for key, value := range mockedHeaders { 907 headers[key] = value[0] 908 } 909 910 expectedRequestObject := &webhook.ApplicationLifecycleWebhookRequestObject{ 911 Application: mockedNextResponse().(webhook.Resource), 912 TenantID: tenantID, 913 Headers: headers, 914 } 915 916 expectedObj, err := json.Marshal(expectedRequestObject) 917 require.NoError(t, err) 918 require.Equal(t, string(expectedObj), op.RequestObject) 919 require.Len(t, op.WebhookIDs, 0) 920 }) 921 922 t.Run("when mutation is in ASYNC mode, there is operation in context and resource updater func is executed with UPDATE operation type should finish successfully and update application status to UPDATING", func(t *testing.T) { 923 // GIVEN 924 ctx := context.Background() 925 operationMode := graphql.OperationModeAsync 926 operationCategory := "registerApplication" 927 rCtx := &gqlgen.FieldContext{ 928 Object: "RegisterApplication", 929 Field: gqlgen.CollectedField{ 930 Field: &ast.Field{ 931 Name: operationCategory, 932 }, 933 }, 934 Args: map[string]interface{}{operation.ModeParam: &operationMode, resourceIDField: resourceID}, 935 IsMethod: false, 936 } 937 938 ctx = gqlgen.WithFieldContext(ctx, rCtx) 939 ctx = context.WithValue(ctx, header.ContextKey, mockedHeaders) 940 941 mockedScheduler := &automock.Scheduler{} 942 mockedScheduler.On("Schedule", mock.Anything, mock.Anything).Return(operationID, nil) 943 defer mockedScheduler.AssertExpectations(t) 944 945 mockedTx, mockedTransactioner := txtest.NewTransactionContextGenerator(nil).ThatSucceeds() 946 defer mockedTx.AssertExpectations(t) 947 defer mockedTransactioner.AssertExpectations(t) 948 949 directive := operation.NewDirective(mockedTransactioner, mockedWebhooksResponse, mockedResourceFetcherFunc, func(ctx context.Context, id string, ready bool, errorMsg *string, appStatusCondition model.ApplicationStatusCondition) error { 950 require.NotNil(t, ctx) 951 require.Equal(t, resourceID, id) 952 require.Equal(t, false, ready) 953 require.Nil(t, errorMsg) 954 require.Equal(t, model.ApplicationStatusConditionUpdating, appStatusCondition) 955 return nil 956 }, mockedTenantLoaderFunc, mockedScheduler) 957 958 dummyResolver := &dummyResolver{} 959 960 // WHEN 961 res, err := directive.HandleOperation(ctx, nil, dummyResolver.SuccessResolve, graphql.OperationTypeUpdate, &whTypeApplicationRegister, &resourceIDField) 962 963 // THEN 964 require.NoError(t, err) 965 require.Equal(t, mockedNextResponse(), res) 966 require.Equal(t, graphql.OperationModeAsync, dummyResolver.finalCtx.Value(operation.OpModeKey)) 967 968 opsFromCtx := dummyResolver.finalCtx.Value(operation.OpCtxKey) 969 operations, ok := opsFromCtx.(*[]*operation.Operation) 970 require.True(t, ok) 971 require.Len(t, *operations, 1) 972 973 op := (*operations)[0] 974 require.Equal(t, operationID, op.OperationID) 975 require.Equal(t, operation.OperationTypeUpdate, op.OperationType) 976 require.Equal(t, operationCategory, op.OperationCategory) 977 978 headers := make(map[string]string) 979 for key, value := range mockedHeaders { 980 headers[key] = value[0] 981 } 982 983 expectedRequestObject := &webhook.ApplicationLifecycleWebhookRequestObject{ 984 Application: mockedNextResponse().(webhook.Resource), 985 TenantID: tenantID, 986 Headers: headers, 987 } 988 989 expectedObj, err := json.Marshal(expectedRequestObject) 990 require.NoError(t, err) 991 992 require.Equal(t, string(expectedObj), op.RequestObject) 993 994 require.Len(t, op.WebhookIDs, 1) 995 require.Equal(t, webhookID1, op.WebhookIDs[0]) 996 }) 997 998 t.Run("when mutation is in ASYNC mode, there is operation in context and resource updater func is executed with DELETE operation type should finish successfully and update application status to DELETING", func(t *testing.T) { 999 // GIVEN 1000 ctx := context.Background() 1001 operationMode := graphql.OperationModeAsync 1002 operationCategory := "registerApplication" 1003 rCtx := &gqlgen.FieldContext{ 1004 Object: "RegisterApplication", 1005 Field: gqlgen.CollectedField{ 1006 Field: &ast.Field{ 1007 Name: operationCategory, 1008 }, 1009 }, 1010 Args: map[string]interface{}{operation.ModeParam: &operationMode, resourceIDField: resourceID}, 1011 IsMethod: false, 1012 } 1013 1014 ctx = gqlgen.WithFieldContext(ctx, rCtx) 1015 ctx = context.WithValue(ctx, header.ContextKey, mockedHeaders) 1016 1017 mockedScheduler := &automock.Scheduler{} 1018 mockedScheduler.On("Schedule", mock.Anything, mock.Anything).Return(operationID, nil) 1019 defer mockedScheduler.AssertExpectations(t) 1020 1021 mockedTx, mockedTransactioner := txtest.NewTransactionContextGenerator(nil).ThatSucceeds() 1022 defer mockedTx.AssertExpectations(t) 1023 defer mockedTransactioner.AssertExpectations(t) 1024 1025 directive := operation.NewDirective(mockedTransactioner, mockedWebhooksResponse, mockedResourceFetcherFunc, func(ctx context.Context, id string, ready bool, errorMsg *string, appStatusCondition model.ApplicationStatusCondition) error { 1026 require.NotNil(t, ctx) 1027 require.Equal(t, resourceID, id) 1028 require.Equal(t, false, ready) 1029 require.Nil(t, errorMsg) 1030 require.Equal(t, model.ApplicationStatusConditionDeleting, appStatusCondition) 1031 return nil 1032 }, mockedTenantLoaderFunc, mockedScheduler) 1033 1034 dummyResolver := &dummyResolver{} 1035 1036 // WHEN 1037 res, err := directive.HandleOperation(ctx, nil, dummyResolver.SuccessResolve, graphql.OperationTypeDelete, &whTypeApplicationRegister, &resourceIDField) 1038 1039 // THEN 1040 require.NoError(t, err) 1041 require.Equal(t, mockedNextResponse(), res) 1042 require.Equal(t, graphql.OperationModeAsync, dummyResolver.finalCtx.Value(operation.OpModeKey)) 1043 1044 opsFromCtx := dummyResolver.finalCtx.Value(operation.OpCtxKey) 1045 operations, ok := opsFromCtx.(*[]*operation.Operation) 1046 require.True(t, ok) 1047 require.Len(t, *operations, 1) 1048 1049 op := (*operations)[0] 1050 require.Equal(t, operationID, op.OperationID) 1051 require.Equal(t, operation.OperationTypeDelete, op.OperationType) 1052 require.Equal(t, operationCategory, op.OperationCategory) 1053 1054 headers := make(map[string]string) 1055 for key, value := range mockedHeaders { 1056 headers[key] = value[0] 1057 } 1058 1059 expectedRequestObject := &webhook.ApplicationLifecycleWebhookRequestObject{ 1060 Application: mockedNextResponse().(webhook.Resource), 1061 TenantID: tenantID, 1062 Headers: headers, 1063 } 1064 1065 expectedObj, err := json.Marshal(expectedRequestObject) 1066 require.NoError(t, err) 1067 1068 require.Equal(t, string(expectedObj), op.RequestObject) 1069 1070 require.Len(t, op.WebhookIDs, 1) 1071 require.Equal(t, webhookID1, op.WebhookIDs[0]) 1072 }) 1073 1074 t.Run("when mutation is in ASYNC mode, there is operation without webhooks in context and resource updater func is executed with DELETE operation type should finish successfully and update application status to DELETING", func(t *testing.T) { 1075 // GIVEN 1076 ctx := context.Background() 1077 operationMode := graphql.OperationModeAsync 1078 operationCategory := "registerApplication" 1079 rCtx := &gqlgen.FieldContext{ 1080 Object: "RegisterApplication", 1081 Field: gqlgen.CollectedField{ 1082 Field: &ast.Field{ 1083 Name: operationCategory, 1084 }, 1085 }, 1086 Args: map[string]interface{}{operation.ModeParam: &operationMode, resourceIDField: resourceID}, 1087 IsMethod: false, 1088 } 1089 1090 ctx = gqlgen.WithFieldContext(ctx, rCtx) 1091 ctx = context.WithValue(ctx, header.ContextKey, mockedHeaders) 1092 1093 mockedScheduler := &automock.Scheduler{} 1094 mockedScheduler.On("Schedule", mock.Anything, mock.Anything).Return(operationID, nil) 1095 defer mockedScheduler.AssertExpectations(t) 1096 1097 mockedTx, mockedTransactioner := txtest.NewTransactionContextGenerator(nil).ThatSucceeds() 1098 defer mockedTx.AssertExpectations(t) 1099 defer mockedTransactioner.AssertExpectations(t) 1100 1101 directive := operation.NewDirective(mockedTransactioner, mockedEmptyWebhooksResponse, mockedResourceFetcherFunc, func(ctx context.Context, id string, ready bool, errorMsg *string, appStatusCondition model.ApplicationStatusCondition) error { 1102 require.NotNil(t, ctx) 1103 require.Equal(t, resourceID, id) 1104 require.Equal(t, false, ready) 1105 require.Nil(t, errorMsg) 1106 require.Equal(t, model.ApplicationStatusConditionDeleting, appStatusCondition) 1107 return nil 1108 }, mockedTenantLoaderFunc, mockedScheduler) 1109 1110 dummyResolver := &dummyResolver{} 1111 1112 // WHEN 1113 res, err := directive.HandleOperation(ctx, nil, dummyResolver.SuccessResolve, graphql.OperationTypeDelete, &whTypeApplicationRegister, &resourceIDField) 1114 1115 // THEN 1116 require.NoError(t, err) 1117 require.Equal(t, mockedNextResponse(), res) 1118 require.Equal(t, graphql.OperationModeAsync, dummyResolver.finalCtx.Value(operation.OpModeKey)) 1119 1120 opsFromCtx := dummyResolver.finalCtx.Value(operation.OpCtxKey) 1121 operations, ok := opsFromCtx.(*[]*operation.Operation) 1122 require.True(t, ok) 1123 require.Len(t, *operations, 1) 1124 1125 op := (*operations)[0] 1126 require.Equal(t, operationID, op.OperationID) 1127 require.Equal(t, operation.OperationTypeDelete, op.OperationType) 1128 require.Equal(t, operationCategory, op.OperationCategory) 1129 1130 headers := make(map[string]string) 1131 for key, value := range mockedHeaders { 1132 headers[key] = value[0] 1133 } 1134 1135 expectedRequestObject := &webhook.ApplicationLifecycleWebhookRequestObject{ 1136 Application: mockedNextResponse().(webhook.Resource), 1137 TenantID: tenantID, 1138 Headers: headers, 1139 } 1140 1141 expectedObj, err := json.Marshal(expectedRequestObject) 1142 require.NoError(t, err) 1143 require.Equal(t, string(expectedObj), op.RequestObject) 1144 require.Len(t, op.WebhookIDs, 0) 1145 }) 1146 1147 t.Run("when mutation is in ASYNC mode, there is operation in context and resource updater func is executed with invalid operation type should return error", func(t *testing.T) { 1148 // GIVEN 1149 ctx := context.Background() 1150 operationMode := graphql.OperationModeAsync 1151 operationCategory := "registerApplication" 1152 rCtx := &gqlgen.FieldContext{ 1153 Object: "RegisterApplication", 1154 Field: gqlgen.CollectedField{ 1155 Field: &ast.Field{ 1156 Name: operationCategory, 1157 }, 1158 }, 1159 Args: map[string]interface{}{operation.ModeParam: &operationMode, resourceIDField: resourceID}, 1160 IsMethod: false, 1161 } 1162 1163 ctx = gqlgen.WithFieldContext(ctx, rCtx) 1164 1165 mockedTx, mockedTransactioner := txtest.NewTransactionContextGenerator(nil).ThatDoesntExpectCommit() 1166 defer mockedTx.AssertExpectations(t) 1167 defer mockedTransactioner.AssertExpectations(t) 1168 1169 directive := operation.NewDirective(mockedTransactioner, mockedWebhooksResponse, mockedResourceFetcherFunc, mockedEmptyResourceUpdaterFunc, mockedTenantLoaderFunc, nil) 1170 1171 dummyResolver := &dummyResolver{} 1172 1173 // WHEN 1174 res, err := directive.HandleOperation(ctx, nil, dummyResolver.SuccessResolve, "invalid", &whTypeApplicationRegister, &resourceIDField) 1175 1176 // THEN 1177 require.Error(t, err) 1178 require.Contains(t, err.Error(), "Invalid status condition") 1179 require.Nil(t, res) 1180 require.Equal(t, graphql.OperationModeAsync, dummyResolver.finalCtx.Value(operation.OpModeKey)) 1181 1182 opsFromCtx := dummyResolver.finalCtx.Value(operation.OpCtxKey) 1183 assertNoOperationsInCtx(t, opsFromCtx) 1184 }) 1185 } 1186 1187 func TestHandleOperation_ConcurrencyCheck(t *testing.T) { 1188 type testCase struct { 1189 description string 1190 mutation string 1191 scheduler *automock.Scheduler 1192 tenantLoaderFunc func(ctx context.Context) (string, error) 1193 resourceFetcherFunc func(ctx context.Context, tenant, id string) (model.Entity, error) 1194 validationFunc func(t *testing.T, res interface{}, err error) 1195 resolverFunc func(ctx context.Context) (res interface{}, err error) 1196 resolverCtxArgs map[string]interface{} 1197 transactionFunc func() (*tx_automock.PersistenceTx, *tx_automock.Transactioner) 1198 } 1199 1200 testCases := []testCase{ 1201 { 1202 description: "when resource ID is not present in the resolver context it should roll-back", 1203 mutation: "UnregisterApplication", 1204 resolverCtxArgs: resolverContextArgs(graphql.OperationModeAsync, ""), 1205 transactionFunc: func() (*tx_automock.PersistenceTx, *tx_automock.Transactioner) { 1206 return txtest.NewTransactionContextGenerator(nil).ThatDoesntExpectCommit() 1207 }, 1208 validationFunc: func(t *testing.T, res interface{}, err error) { 1209 require.Error(t, err) 1210 require.Contains(t, err.Error(), fmt.Sprintf("could not get idField: %q from request context", resourceIDField)) 1211 require.Empty(t, res) 1212 }, 1213 }, 1214 { 1215 description: "when tenant fetching fails it should roll-back", 1216 mutation: "UnregisterApplication", 1217 resolverCtxArgs: resolverContextArgs(graphql.OperationModeAsync, resourceID), 1218 tenantLoaderFunc: tenantLoaderWithOptionalErr(mockedError()), 1219 transactionFunc: func() (*tx_automock.PersistenceTx, *tx_automock.Transactioner) { 1220 return txtest.NewTransactionContextGenerator(nil).ThatDoesntExpectCommit() 1221 }, 1222 validationFunc: func(t *testing.T, res interface{}, err error) { 1223 require.Error(t, err) 1224 require.True(t, apperrors.IsTenantRequired(err)) 1225 require.Empty(t, res) 1226 }, 1227 }, 1228 { 1229 description: "when resource is not found it should roll-back", 1230 mutation: "UnregisterApplication", 1231 resolverCtxArgs: resolverContextArgs(graphql.OperationModeAsync, resourceID), 1232 transactionFunc: func() (*tx_automock.PersistenceTx, *tx_automock.Transactioner) { 1233 return txtest.NewTransactionContextGenerator(nil).ThatDoesntExpectCommit() 1234 }, 1235 tenantLoaderFunc: tenantLoaderWithOptionalErr(nil), 1236 resourceFetcherFunc: func(ctx context.Context, tenant, id string) (model.Entity, error) { 1237 return nil, apperrors.NewNotFoundError(resource.Application, resourceID) 1238 }, 1239 validationFunc: func(t *testing.T, res interface{}, err error) { 1240 require.Error(t, err) 1241 require.True(t, apperrors.IsNotFoundError(err)) 1242 require.Empty(t, res) 1243 }, 1244 }, 1245 { 1246 description: "when resource fetching fails it should roll-back", 1247 mutation: "UnregisterApplication", 1248 resolverCtxArgs: resolverContextArgs(graphql.OperationModeAsync, resourceID), 1249 transactionFunc: func() (*tx_automock.PersistenceTx, *tx_automock.Transactioner) { 1250 return txtest.NewTransactionContextGenerator(nil).ThatDoesntExpectCommit() 1251 }, 1252 tenantLoaderFunc: tenantLoaderWithOptionalErr(nil), 1253 resourceFetcherFunc: func(ctx context.Context, tenant, id string) (model.Entity, error) { 1254 return nil, mockedError() 1255 }, 1256 validationFunc: func(t *testing.T, res interface{}, err error) { 1257 require.Error(t, err) 1258 require.Contains(t, err.Error(), fmt.Sprintf("failed to fetch resource with id %s", resourceID)) 1259 require.Empty(t, res) 1260 }, 1261 }, 1262 { 1263 description: "when concurrent create operation is running it should roll-back", 1264 mutation: "UnregisterApplication", 1265 resolverCtxArgs: resolverContextArgs(graphql.OperationModeAsync, resourceID), 1266 transactionFunc: func() (*tx_automock.PersistenceTx, *tx_automock.Transactioner) { 1267 return txtest.NewTransactionContextGenerator(nil).ThatDoesntExpectCommit() 1268 }, 1269 tenantLoaderFunc: tenantLoaderWithOptionalErr(nil), 1270 resourceFetcherFunc: func(ctx context.Context, tenant, id string) (model.Entity, error) { 1271 return &model.Application{ 1272 BaseEntity: &model.BaseEntity{ 1273 ID: resourceID, 1274 Ready: false, 1275 CreatedAt: timeToTimePtr(time.Now()), 1276 }, 1277 }, nil 1278 }, 1279 validationFunc: func(t *testing.T, res interface{}, err error) { 1280 require.Error(t, err) 1281 require.Contains(t, err.Error(), "create operation is in progress") 1282 require.Empty(t, res) 1283 }, 1284 }, 1285 { 1286 description: "when concurrent delete operation is running it should roll-back", 1287 mutation: "UnregisterApplication", 1288 resolverCtxArgs: resolverContextArgs(graphql.OperationModeAsync, resourceID), 1289 transactionFunc: func() (*tx_automock.PersistenceTx, *tx_automock.Transactioner) { 1290 return txtest.NewTransactionContextGenerator(nil).ThatDoesntExpectCommit() 1291 }, 1292 tenantLoaderFunc: tenantLoaderWithOptionalErr(nil), 1293 resourceFetcherFunc: func(ctx context.Context, tenant, id string) (model.Entity, error) { 1294 return &model.Application{ 1295 BaseEntity: &model.BaseEntity{ 1296 ID: resourceID, 1297 Ready: false, 1298 CreatedAt: timeToTimePtr(time.Now()), 1299 DeletedAt: timeToTimePtr(time.Now()), 1300 }, 1301 }, nil 1302 }, 1303 validationFunc: func(t *testing.T, res interface{}, err error) { 1304 require.Error(t, err) 1305 require.Contains(t, err.Error(), "delete operation is in progress") 1306 require.Empty(t, res) 1307 }, 1308 }, 1309 { 1310 description: "when there are no concurrent operations it should finish successfully", 1311 mutation: "UnregisterApplication", 1312 scheduler: scheduler(operationID, nil), 1313 resolverCtxArgs: resolverContextArgs(graphql.OperationModeAsync, resourceID), 1314 transactionFunc: func() (*tx_automock.PersistenceTx, *tx_automock.Transactioner) { 1315 return txtest.NewTransactionContextGenerator(nil).ThatSucceeds() 1316 }, 1317 tenantLoaderFunc: tenantLoaderWithOptionalErr(nil), 1318 resourceFetcherFunc: mockedResourceFetcherFunc, 1319 resolverFunc: (&dummyResolver{}).SuccessResolve, 1320 validationFunc: func(t *testing.T, res interface{}, err error) { 1321 require.NoError(t, err) 1322 require.Equal(t, mockedNextResponse(), res) 1323 }, 1324 }, 1325 } 1326 1327 for _, test := range testCases { 1328 t.Run(test.description, func(t *testing.T) { 1329 // GIVEN 1330 ctx := context.Background() 1331 rCtx := &gqlgen.FieldContext{ 1332 Object: test.mutation, 1333 Field: gqlgen.CollectedField{ 1334 Field: &ast.Field{ 1335 Name: test.mutation, 1336 }, 1337 }, 1338 Args: test.resolverCtxArgs, 1339 IsMethod: false, 1340 } 1341 1342 ctx = gqlgen.WithFieldContext(ctx, rCtx) 1343 ctx = context.WithValue(ctx, header.ContextKey, mockedHeaders) 1344 mockedTx, mockedTransactioner := test.transactionFunc() 1345 defer mockedTx.AssertExpectations(t) 1346 defer mockedTransactioner.AssertExpectations(t) 1347 1348 if test.scheduler != nil { 1349 defer test.scheduler.AssertExpectations(t) 1350 } 1351 1352 directive := operation.NewDirective(mockedTransactioner, func(ctx context.Context, resourceID string) ([]*model.Webhook, error) { 1353 return nil, nil 1354 }, test.resourceFetcherFunc, mockedEmptyResourceUpdaterFunc, test.tenantLoaderFunc, test.scheduler) 1355 1356 // WHEN 1357 res, err := directive.HandleOperation(ctx, nil, test.resolverFunc, graphql.OperationTypeDelete, nil, &resourceIDField) 1358 // THEN 1359 test.validationFunc(t, res, err) 1360 }) 1361 } 1362 1363 t.Run("when idField is not present in the directive it should roll-back", func(t *testing.T) { 1364 // GIVEN 1365 operationCategory := "registerApplication" 1366 ctx := context.Background() 1367 rCtx := &gqlgen.FieldContext{ 1368 Object: "UnregisterApplication", 1369 Field: gqlgen.CollectedField{ 1370 Field: &ast.Field{ 1371 Name: operationCategory, 1372 }, 1373 }, 1374 Args: resolverContextArgs(graphql.OperationModeAsync, resourceID), 1375 IsMethod: false, 1376 } 1377 1378 ctx = gqlgen.WithFieldContext(ctx, rCtx) 1379 mockedTx, mockedTransactioner := txtest.NewTransactionContextGenerator(nil).ThatDoesntExpectCommit() 1380 defer mockedTx.AssertExpectations(t) 1381 defer mockedTransactioner.AssertExpectations(t) 1382 1383 directive := operation.NewDirective(mockedTransactioner, nil, nil, nil, nil, nil) 1384 1385 // WHEN 1386 _, err := directive.HandleOperation(ctx, nil, nil, graphql.OperationTypeDelete, &whTypeApplicationUnregister, nil) 1387 // THEN 1388 require.Error(t, err) 1389 require.Contains(t, err.Error(), "idField from context should not be empty") 1390 }) 1391 } 1392 1393 type dummyResolver struct { 1394 finalCtx context.Context 1395 } 1396 1397 func (d *dummyResolver) SuccessResolve(ctx context.Context) (res interface{}, err error) { 1398 d.finalCtx = ctx 1399 return mockedNextResponse(), nil 1400 } 1401 1402 func (d *dummyResolver) ErrorResolve(ctx context.Context) (res interface{}, err error) { 1403 d.finalCtx = ctx 1404 return nil, mockedError() 1405 } 1406 1407 func (d *dummyResolver) NonEntityResolve(ctx context.Context) (res interface{}, err error) { 1408 d.finalCtx = ctx 1409 return &graphql.Runtime{}, nil 1410 } 1411 1412 func (d *dummyResolver) NonWebhookProviderResolve(ctx context.Context) (res interface{}, err error) { 1413 d.finalCtx = ctx 1414 return &graphql.Bundle{BaseEntity: &graphql.BaseEntity{ID: resourceID}}, nil 1415 } 1416 1417 func mockedNextResponse() interface{} { 1418 return &graphql.Application{BaseEntity: &graphql.BaseEntity{ID: resourceID}} 1419 } 1420 1421 func mockedWebhooksResponse(_ context.Context, _ string) ([]*model.Webhook, error) { 1422 return []*model.Webhook{ 1423 {ID: webhookID1, Type: model.WebhookTypeRegisterApplication}, 1424 }, nil 1425 } 1426 1427 func mockedEmptyWebhooksResponse(_ context.Context, _ string) ([]*model.Webhook, error) { 1428 return nil, nil 1429 } 1430 1431 func mockedResourceFetcherFunc(context.Context, string, string) (model.Entity, error) { 1432 return &model.Application{ 1433 BaseEntity: &model.BaseEntity{ 1434 ID: resourceID, 1435 Ready: true, 1436 CreatedAt: timeToTimePtr(time.Now()), 1437 }, 1438 }, nil 1439 } 1440 1441 func mockedTenantLoaderFunc(_ context.Context) (string, error) { 1442 return tenantID, nil 1443 } 1444 1445 func mockedResourceUpdaterFuncWithError(context.Context, string, bool, *string, model.ApplicationStatusCondition) error { 1446 return mockedError() 1447 } 1448 1449 func mockedEmptyResourceUpdaterFunc(context.Context, string, bool, *string, model.ApplicationStatusCondition) error { 1450 return nil 1451 } 1452 1453 func mockedError() error { 1454 return errors.New("mocked error") 1455 } 1456 1457 func resolverContextArgs(mode graphql.OperationMode, optionalResourceID string) map[string]interface{} { 1458 ctxArgs := map[string]interface{}{operation.ModeParam: &mode} 1459 if optionalResourceID != "" { 1460 ctxArgs[resourceIDField] = resourceID 1461 } 1462 1463 return ctxArgs 1464 } 1465 1466 func tenantLoaderWithOptionalErr(optionalErr error) func(ctx context.Context) (string, error) { 1467 if optionalErr != nil { 1468 return func(ctx context.Context) (string, error) { return "", optionalErr } 1469 } 1470 1471 return func(ctx context.Context) (string, error) { return tenantID, nil } 1472 } 1473 1474 func scheduler(operationID string, err error) *automock.Scheduler { 1475 mockedScheduler := &automock.Scheduler{} 1476 mockedScheduler.On("Schedule", mock.Anything, mock.Anything).Return(operationID, err) 1477 return mockedScheduler 1478 } 1479 1480 func assertNoOperationsInCtx(t *testing.T, ops interface{}) { 1481 operations, ok := ops.(*[]*operation.Operation) 1482 require.True(t, ok) 1483 require.Len(t, *operations, 0) 1484 }