github.com/kyma-incubator/compass/components/director@v0.0.0-20230623144113-d764f56ff805/pkg/webhook_client/client_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 webhookclient_test 18 19 import ( 20 "bytes" 21 "context" 22 "errors" 23 "fmt" 24 "io" 25 "net/http" 26 "testing" 27 28 webhookclient "github.com/kyma-incubator/compass/components/director/pkg/webhook_client" 29 30 accessstrategy2 "github.com/kyma-incubator/compass/components/director/pkg/accessstrategy" 31 "github.com/kyma-incubator/compass/components/director/pkg/str" 32 33 "github.com/kyma-incubator/compass/components/director/pkg/auth" 34 "github.com/kyma-incubator/compass/components/director/pkg/correlation" 35 36 "github.com/kyma-incubator/compass/components/director/pkg/graphql" 37 "github.com/kyma-incubator/compass/components/director/pkg/webhook" 38 39 "github.com/stretchr/testify/require" 40 ) 41 42 var ( 43 invalidTemplate = "invalidTemplate" 44 emptyTemplate = "{}" 45 mockedError = "mocked error" 46 mockedLocationURL = "https://test-domain.com/operation" 47 webhookAsyncMode = graphql.WebhookModeAsync 48 ) 49 50 func TestClient_Do_WhenUrlTemplateIsInvalid_ShouldReturnError(t *testing.T) { 51 webhookReq := &webhookclient.Request{ 52 Webhook: graphql.Webhook{ 53 URLTemplate: &invalidTemplate, 54 OutputTemplate: &emptyTemplate, 55 }, 56 Object: &webhook.ApplicationLifecycleWebhookRequestObject{}, 57 } 58 59 client := webhookclient.NewClient(http.DefaultClient, nil, nil) 60 61 resp, err := client.Do(context.Background(), webhookReq) 62 63 require.Error(t, err) 64 require.Contains(t, err.Error(), "unable to parse webhook URL") 65 require.Nil(t, resp) 66 } 67 68 func TestClient_Do_WhenUrlTemplateIsNil_ShouldReturnError(t *testing.T) { 69 webhookReq := &webhookclient.Request{ 70 Webhook: graphql.Webhook{ 71 URLTemplate: nil, 72 OutputTemplate: &emptyTemplate, 73 }, 74 Object: &webhook.ApplicationLifecycleWebhookRequestObject{}, 75 } 76 77 client := webhookclient.NewClient(http.DefaultClient, nil, nil) 78 79 resp, err := client.Do(context.Background(), webhookReq) 80 81 require.Error(t, err) 82 require.Contains(t, err.Error(), "missing webhook url") 83 require.Nil(t, resp) 84 } 85 86 func TestClient_Do_WhenOutputTemplateIsNil_ShouldReturnError(t *testing.T) { 87 webhookReq := &webhookclient.Request{ 88 Webhook: graphql.Webhook{ 89 OutputTemplate: nil, 90 }, 91 Object: &webhook.ApplicationLifecycleWebhookRequestObject{}, 92 } 93 94 client := webhookclient.NewClient(http.DefaultClient, nil, nil) 95 96 resp, err := client.Do(context.Background(), webhookReq) 97 98 require.Error(t, err) 99 require.Contains(t, err.Error(), "missing output template") 100 require.Nil(t, resp) 101 } 102 103 func TestClient_Do_WhenParseInputTemplateIsInvalid_ShouldReturnError(t *testing.T) { 104 URLTemplate := "{\"method\": \"DELETE\",\"path\":\"https://test-domain.com/api/v1/applications/{{.Application.ID}}\"}" 105 invalidInputTemplate := "{\"application_id\": \"{{.Application.ID}}\",\"group\": \"{{.Application.Group}}\"}" 106 app := &graphql.Application{BaseEntity: &graphql.BaseEntity{ID: "appID"}} 107 webhookReq := &webhookclient.Request{ 108 Webhook: graphql.Webhook{ 109 URLTemplate: &URLTemplate, 110 InputTemplate: &invalidInputTemplate, 111 OutputTemplate: &emptyTemplate, 112 }, 113 Object: &webhook.ApplicationLifecycleWebhookRequestObject{Application: app}, 114 } 115 116 client := webhookclient.NewClient(http.DefaultClient, nil, nil) 117 118 resp, err := client.Do(context.Background(), webhookReq) 119 120 require.Error(t, err) 121 require.Contains(t, err.Error(), "unable to parse webhook input body") 122 require.Nil(t, resp) 123 } 124 125 func TestClient_Do_WhenHeadersTemplateIsInvalid_ShouldReturnError(t *testing.T) { 126 URLTemplate := "{\"method\": \"DELETE\",\"path\":\"https://test-domain.com/api/v1/applications/{{.Application.ID}}\"}" 127 inputTemplate := "{\"application_id\": \"{{.Application.ID}}\",\"name\": \"{{.Application.Name}}\"}" 128 app := &graphql.Application{BaseEntity: &graphql.BaseEntity{ID: "appID"}} 129 webhookReq := &webhookclient.Request{ 130 Webhook: graphql.Webhook{ 131 URLTemplate: &URLTemplate, 132 InputTemplate: &inputTemplate, 133 HeaderTemplate: &invalidTemplate, 134 OutputTemplate: &emptyTemplate, 135 }, 136 Object: &webhook.ApplicationLifecycleWebhookRequestObject{Application: app}, 137 } 138 139 client := webhookclient.NewClient(http.DefaultClient, nil, nil) 140 141 resp, err := client.Do(context.Background(), webhookReq) 142 143 require.Error(t, err) 144 require.Contains(t, err.Error(), "unable to parse webhook headers") 145 require.Nil(t, resp) 146 } 147 148 func TestClient_Do_WhenAuthFlowCannotBeDetermined_ShouldReturnError(t *testing.T) { 149 URLTemplate := "{\"method\": \"DELETE\",\"path\":\"https://test-domain.com/api/v1/applications/{{.Application.ID}}\"}" 150 inputTemplate := "{\"application_id\": \"{{.Application.ID}}\",\"name\": \"{{.Application.Name}}\"}" 151 headersTemplate := "{\"user-identity\":[\"{{.Headers.Client_user}}\"]}" 152 app := &graphql.Application{BaseEntity: &graphql.BaseEntity{ID: "appID"}} 153 webhookReq := &webhookclient.Request{ 154 Webhook: graphql.Webhook{ 155 URLTemplate: &URLTemplate, 156 InputTemplate: &inputTemplate, 157 HeaderTemplate: &headersTemplate, 158 OutputTemplate: &emptyTemplate, 159 Auth: &graphql.Auth{AccessStrategy: str.Ptr("wrong")}, 160 }, 161 Object: &webhook.ApplicationLifecycleWebhookRequestObject{Application: app}, 162 } 163 164 client := webhookclient.NewClient(http.DefaultClient, nil, nil) 165 166 resp, err := client.Do(context.Background(), webhookReq) 167 168 require.Error(t, err) 169 require.Contains(t, err.Error(), "could not determine auth flow") 170 require.Nil(t, resp) 171 } 172 173 func TestClient_Do_WhenExecutingRequestFails_ShouldReturnError(t *testing.T) { 174 URLTemplate := "{\"method\": \"DELETE\",\"path\":\"https://test-domain.com/api/v1/applications/{{.Application.ID}}\"}" 175 inputTemplate := "{\"application_id\": \"{{.Application.ID}}\",\"name\": \"{{.Application.Name}}\"}" 176 headersTemplate := "{\"user-identity\":[\"{{.Headers.Client_user}}\"]}" 177 app := &graphql.Application{BaseEntity: &graphql.BaseEntity{ID: "appID"}} 178 webhookReq := &webhookclient.Request{ 179 Webhook: graphql.Webhook{ 180 URLTemplate: &URLTemplate, 181 InputTemplate: &inputTemplate, 182 HeaderTemplate: &headersTemplate, 183 OutputTemplate: &emptyTemplate, 184 }, 185 Object: &webhook.ApplicationLifecycleWebhookRequestObject{Application: app}, 186 } 187 188 client := webhookclient.NewClient(&http.Client{ 189 Transport: mockedTransport{err: errors.New(mockedError)}, 190 }, nil, nil) 191 192 resp, err := client.Do(context.Background(), webhookReq) 193 194 require.Error(t, err) 195 require.Contains(t, err.Error(), mockedError) 196 require.Nil(t, resp) 197 } 198 199 func TestClient_Do_WhenWebhookResponseDoesNotContainLocationURL_ShouldReturnError(t *testing.T) { 200 URLTemplate := "{\"method\": \"DELETE\",\"path\":\"https://test-domain.com/api/v1/applications/{{.Application.ID}}\"}" 201 inputTemplate := "{\"application_id\": \"{{.Application.ID}}\",\"name\": \"{{.Application.Name}}\"}" 202 headersTemplate := "{\"user-identity\":[\"{{.Headers.Client_user}}\"]}" 203 outputTemplate := "{\"location\":\"{{.Headers.Location}}\",\"success_status_code\": 202,\"incomplete_status_code\": 204,\"error\": \"{{.Body.error}}\"}" 204 app := &graphql.Application{BaseEntity: &graphql.BaseEntity{ID: "appID"}} 205 webhookReq := &webhookclient.Request{ 206 Webhook: graphql.Webhook{ 207 URLTemplate: &URLTemplate, 208 InputTemplate: &inputTemplate, 209 HeaderTemplate: &headersTemplate, 210 OutputTemplate: &outputTemplate, 211 Mode: &webhookAsyncMode, 212 }, 213 Object: &webhook.ApplicationLifecycleWebhookRequestObject{Application: app}, 214 } 215 216 client := webhookclient.NewClient(&http.Client{ 217 Transport: mockedTransport{ 218 resp: &http.Response{ 219 Body: io.NopCloser(bytes.NewReader([]byte("{}"))), 220 StatusCode: http.StatusAccepted, 221 }, 222 }, 223 }, nil, nil) 224 225 resp, err := client.Do(context.Background(), webhookReq) 226 227 require.Error(t, err) 228 require.Contains(t, err.Error(), "missing location url after executing async webhook") 229 require.Nil(t, resp) 230 } 231 232 func TestClient_Do_WhenWebhookResponseBodyContainsError_ShouldReturnError(t *testing.T) { 233 URLTemplate := "{\"method\": \"DELETE\",\"path\":\"https://test-domain.com/api/v1/applications/{{.Application.ID}}\"}" 234 inputTemplate := "{\"application_id\": \"{{.Application.ID}}\",\"name\": \"{{.Application.Name}}\"}" 235 headersTemplate := "{\"user-identity\":[\"{{.Headers.Client_user}}\"]}" 236 outputTemplate := "{\"location\":\"{{.Headers.Location}}\",\"success_status_code\": 202,\"incomplete_status_code\": 204,\"error\": \"{{.Body.error}}\"}" 237 app := &graphql.Application{BaseEntity: &graphql.BaseEntity{ID: "appID"}} 238 webhookReq := &webhookclient.Request{ 239 Webhook: graphql.Webhook{ 240 URLTemplate: &URLTemplate, 241 InputTemplate: &inputTemplate, 242 HeaderTemplate: &headersTemplate, 243 OutputTemplate: &outputTemplate, 244 Mode: &webhookAsyncMode, 245 }, 246 Object: &webhook.ApplicationLifecycleWebhookRequestObject{Application: app}, 247 } 248 249 client := webhookclient.NewClient(&http.Client{ 250 Transport: mockedTransport{ 251 resp: &http.Response{ 252 Body: io.NopCloser(bytes.NewReader([]byte(fmt.Sprintf("{\"error\": \"%s\"}", mockedError)))), 253 Header: http.Header{"Location": []string{mockedLocationURL}}, 254 StatusCode: http.StatusAccepted, 255 }, 256 }, 257 }, nil, nil) 258 259 resp, err := client.Do(context.Background(), webhookReq) 260 261 require.Error(t, err) 262 require.Contains(t, err.Error(), mockedError) 263 require.Contains(t, err.Error(), "received error while calling external system") 264 require.Equal(t, http.StatusAccepted, *resp.ActualStatusCode) 265 } 266 267 func TestClient_Do_WhenWebhookResponseBodyContainsErrorWithJSONObjects_ShouldParseErrorSuccessfully(t *testing.T) { 268 URLTemplate := "{\"method\": \"DELETE\",\"path\":\"https://test-domain.com/api/v1/applications/{{.Application.ID}}\"}" 269 inputTemplate := "{\"application_id\": \"{{.Application.ID}}\",\"name\": \"{{.Application.Name}}\"}" 270 headersTemplate := "{\"user-identity\":[\"{{.Headers.Client_user}}\"]}" 271 outputTemplate := "{\"location\":\"{{.Headers.Location}}\",\"success_status_code\": 202,\"incomplete_status_code\": 204,\"error\": \"{{.Body.error}}\"}" 272 app := &graphql.Application{BaseEntity: &graphql.BaseEntity{ID: "appID"}} 273 webhookReq := &webhookclient.Request{ 274 Webhook: graphql.Webhook{ 275 URLTemplate: &URLTemplate, 276 InputTemplate: &inputTemplate, 277 HeaderTemplate: &headersTemplate, 278 OutputTemplate: &outputTemplate, 279 Mode: &webhookAsyncMode, 280 }, 281 Object: &webhook.ApplicationLifecycleWebhookRequestObject{Application: app}, 282 } 283 284 mockedJSONObjectError := "{\"code\":\"401\",\"message\":\"Unauthorized\",\"correlationId\":\"12345678-e89b-12d3-a456-556642440000\"}" 285 286 client := webhookclient.NewClient(&http.Client{ 287 Transport: mockedTransport{ 288 resp: &http.Response{ 289 Body: io.NopCloser(bytes.NewReader([]byte(fmt.Sprintf("{\"error\": %s}", mockedJSONObjectError)))), 290 Header: http.Header{"Location": []string{mockedLocationURL}}, 291 StatusCode: http.StatusAccepted, 292 }, 293 }, 294 }, nil, nil) 295 296 resp, err := client.Do(context.Background(), webhookReq) 297 298 require.Error(t, err) 299 require.Contains(t, err.Error(), "Unauthorized") 300 require.Contains(t, err.Error(), "received error while calling external system") 301 require.Equal(t, http.StatusAccepted, *resp.ActualStatusCode) 302 } 303 304 func TestClient_Do_WhenWebhookResponseStatusCodeIsGoneAndGoneStatusISDefined_ShouldReturnWebhookStatusGoneError(t *testing.T) { 305 goneCodeString := "404" 306 URLTemplate := "{\"method\": \"DELETE\",\"path\":\"https://test-domain.com/api/v1/applications/{{.Application.ID}}\"}" 307 inputTemplate := "{\"application_id\": \"{{.Application.ID}}\",\"name\": \"{{.Application.Name}}\"}" 308 headersTemplate := "{\"user-identity\":[\"{{.Headers.Client_user}}\"]}" 309 outputTemplate := fmt.Sprintf("{\"location\":\"{{.Headers.Location}}\",\"success_status_code\": 202,\"incomplete_status_code\": 204,\"gone_status_code\": %s,\"error\": \"{{.Body.error}}\"}", goneCodeString) 310 app := &graphql.Application{BaseEntity: &graphql.BaseEntity{ID: "appID"}} 311 webhookReq := &webhookclient.Request{ 312 Webhook: graphql.Webhook{ 313 URLTemplate: &URLTemplate, 314 InputTemplate: &inputTemplate, 315 HeaderTemplate: &headersTemplate, 316 OutputTemplate: &outputTemplate, 317 Mode: &webhookAsyncMode, 318 }, 319 Object: &webhook.ApplicationLifecycleWebhookRequestObject{Application: app}, 320 } 321 322 client := webhookclient.NewClient(&http.Client{ 323 Transport: mockedTransport{ 324 resp: &http.Response{ 325 Body: io.NopCloser(bytes.NewReader([]byte("{}"))), 326 Header: http.Header{"Location": []string{mockedLocationURL}}, 327 StatusCode: http.StatusNotFound, 328 }, 329 }, 330 }, nil, nil) 331 332 resp, err := client.Do(context.Background(), webhookReq) 333 334 require.Error(t, err) 335 require.IsType(t, webhookclient.WebhookStatusGoneErr{}, err) 336 require.Contains(t, err.Error(), goneCodeString) 337 require.Equal(t, http.StatusNotFound, *resp.ActualStatusCode) 338 } 339 340 func TestClient_Do_WhenWebhookResponseStatusCodeIsNotSuccess_ShouldReturnError(t *testing.T) { 341 URLTemplate := "{\"method\": \"DELETE\",\"path\":\"https://test-domain.com/api/v1/applications/{{.Application.ID}}\"}" 342 inputTemplate := "{\"application_id\": \"{{.Application.ID}}\",\"name\": \"{{.Application.Name}}\"}" 343 headersTemplate := "{\"user-identity\":[\"{{.Headers.Client_user}}\"]}" 344 outputTemplate := "{\"location\":\"{{.Headers.Location}}\",\"success_status_code\": 202,\"incomplete_status_code\": 204,\"error\": \"{{.Body.error}}\"}" 345 app := &graphql.Application{BaseEntity: &graphql.BaseEntity{ID: "appID"}} 346 webhookReq := &webhookclient.Request{ 347 Webhook: graphql.Webhook{ 348 URLTemplate: &URLTemplate, 349 InputTemplate: &inputTemplate, 350 HeaderTemplate: &headersTemplate, 351 OutputTemplate: &outputTemplate, 352 Mode: &webhookAsyncMode, 353 }, 354 Object: &webhook.ApplicationLifecycleWebhookRequestObject{Application: app}, 355 } 356 357 client := webhookclient.NewClient(&http.Client{ 358 Transport: mockedTransport{ 359 resp: &http.Response{ 360 Body: io.NopCloser(bytes.NewReader([]byte("{}"))), 361 Header: http.Header{"Location": []string{mockedLocationURL}}, 362 StatusCode: http.StatusInternalServerError, 363 }, 364 }, 365 }, nil, nil) 366 367 resp, err := client.Do(context.Background(), webhookReq) 368 369 require.Error(t, err) 370 require.Contains(t, err.Error(), fmt.Sprintf("response success status code was not met - expected success status code '202' or incomplete status code '204', got '%d'", http.StatusInternalServerError)) 371 require.Equal(t, http.StatusInternalServerError, *resp.ActualStatusCode) 372 } 373 374 func TestClient_Do_WhenWebhookResponseStatusCodeIsIncomplete_ShouldBeSuccessful(t *testing.T) { 375 URLTemplate := "{\"method\": \"DELETE\",\"path\":\"https://test-domain.com/api/v1/applications/{{.Application.ID}}\"}" 376 inputTemplate := "{\"application_id\": \"{{.Application.ID}}\",\"name\": \"{{.Application.Name}}\"}" 377 headersTemplate := "{\"user-identity\":[\"{{.Headers.Client_user}}\"]}" 378 outputTemplate := "{\"location\":\"{{.Headers.Location}}\",\"success_status_code\": 202,\"incomplete_status_code\": 204,\"error\": \"{{.Body.error}}\"}" 379 app := &graphql.Application{BaseEntity: &graphql.BaseEntity{ID: "appID"}} 380 webhookReq := &webhookclient.Request{ 381 Webhook: graphql.Webhook{ 382 URLTemplate: &URLTemplate, 383 InputTemplate: &inputTemplate, 384 HeaderTemplate: &headersTemplate, 385 OutputTemplate: &outputTemplate, 386 Mode: &webhookAsyncMode, 387 }, 388 Object: &webhook.ApplicationLifecycleWebhookRequestObject{Application: app}, 389 } 390 391 client := webhookclient.NewClient(&http.Client{ 392 Transport: mockedTransport{ 393 resp: &http.Response{ 394 Body: io.NopCloser(bytes.NewReader([]byte("{}"))), 395 Header: http.Header{"Location": []string{mockedLocationURL}}, 396 StatusCode: http.StatusNoContent, 397 }, 398 }, 399 }, nil, nil) 400 401 resp, err := client.Do(context.Background(), webhookReq) 402 403 require.NoError(t, err) 404 require.Equal(t, http.StatusNoContent, *resp.ActualStatusCode) 405 } 406 407 func TestClient_Do_WhenSuccessfulBasicAuthWebhook_ShouldBeSuccessful(t *testing.T) { 408 URLTemplate := "{\"method\": \"DELETE\",\"path\":\"https://test-domain.com/api/v1/applications/{{.Application.ID}}\"}" 409 inputTemplate := "{\"application_id\": \"{{.Application.ID}}\",\"name\": \"{{.Application.Name}}\"}" 410 headersTemplate := "{\"user-identity\":[\"{{.Headers.Client_user}}\"]}" 411 outputTemplate := "{\"location\":\"{{.Headers.Location}}\",\"success_status_code\": 202,\"incomplete_status_code\": 204,\"error\": \"{{.Body.error}}\"}" 412 username, password := "user", "pass" 413 app := &graphql.Application{BaseEntity: &graphql.BaseEntity{ID: "appID"}} 414 webhookReq := &webhookclient.Request{ 415 Webhook: graphql.Webhook{ 416 URLTemplate: &URLTemplate, 417 InputTemplate: &inputTemplate, 418 HeaderTemplate: &headersTemplate, 419 OutputTemplate: &outputTemplate, 420 Mode: &webhookAsyncMode, 421 Auth: &graphql.Auth{ 422 Credential: graphql.BasicCredentialData{ 423 Username: username, 424 Password: password, 425 }, 426 }, 427 }, 428 Object: &webhook.ApplicationLifecycleWebhookRequestObject{Application: app}, 429 } 430 431 client := webhookclient.NewClient(&http.Client{ 432 Transport: mockedTransport{ 433 resp: &http.Response{ 434 Body: io.NopCloser(bytes.NewReader([]byte("{}"))), 435 Header: http.Header{"Location": []string{mockedLocationURL}}, 436 StatusCode: http.StatusAccepted, 437 }, 438 roundTripExpectations: func(r *http.Request) { 439 credentials, err := auth.LoadFromContext(r.Context()) 440 require.NoError(t, err) 441 basicCreds, ok := credentials.(*auth.BasicCredentials) 442 require.True(t, ok) 443 require.Equal(t, username, basicCreds.Username) 444 require.Equal(t, password, basicCreds.Password) 445 }, 446 }, 447 }, nil, nil) 448 449 resp, err := client.Do(context.Background(), webhookReq) 450 451 require.NoError(t, err) 452 require.Equal(t, http.StatusAccepted, *resp.ActualStatusCode) 453 } 454 455 func TestClient_Do_WhenSuccessfulOAuthWebhook_ShouldBeSuccessful(t *testing.T) { 456 URLTemplate := "{\"method\": \"DELETE\",\"path\":\"https://test-domain.com/api/v1/applications/{{.Application.ID}}\"}" 457 inputTemplate := "{\"application_id\": \"{{.Application.ID}}\",\"name\": \"{{.Application.Name}}\"}" 458 headersTemplate := "{\"user-identity\":[\"{{.Headers.Client_user}}\"]}" 459 outputTemplate := "{\"location\":\"{{.Headers.Location}}\",\"success_status_code\": 202,\"incomplete_status_code\": 204,\"error\": \"{{.Body.error}}\"}" 460 clientID, clientSecret, tokenURL := "client-id", "client-secret", "https://test-domain.com/oauth/token" 461 app := &graphql.Application{BaseEntity: &graphql.BaseEntity{ID: "appID"}} 462 webhookReq := &webhookclient.Request{ 463 Webhook: graphql.Webhook{ 464 URLTemplate: &URLTemplate, 465 InputTemplate: &inputTemplate, 466 HeaderTemplate: &headersTemplate, 467 OutputTemplate: &outputTemplate, 468 Mode: &webhookAsyncMode, 469 Auth: &graphql.Auth{ 470 Credential: graphql.OAuthCredentialData{ 471 ClientID: clientID, 472 ClientSecret: clientSecret, 473 URL: tokenURL, 474 }, 475 }, 476 }, 477 Object: &webhook.ApplicationLifecycleWebhookRequestObject{Application: app}, 478 } 479 480 client := webhookclient.NewClient(&http.Client{ 481 Transport: mockedTransport{ 482 resp: &http.Response{ 483 Body: io.NopCloser(bytes.NewReader([]byte("{}"))), 484 Header: http.Header{"Location": []string{mockedLocationURL}}, 485 StatusCode: http.StatusAccepted, 486 }, 487 roundTripExpectations: func(r *http.Request) { 488 credentials, err := auth.LoadFromContext(r.Context()) 489 require.NoError(t, err) 490 oAuthCredentials, ok := credentials.(*auth.OAuthCredentials) 491 require.True(t, ok) 492 require.Equal(t, clientID, oAuthCredentials.ClientID) 493 require.Equal(t, clientSecret, oAuthCredentials.ClientSecret) 494 require.Equal(t, tokenURL, oAuthCredentials.TokenURL) 495 }, 496 }, 497 }, nil, nil) 498 499 resp, err := client.Do(context.Background(), webhookReq) 500 501 require.NoError(t, err) 502 require.Equal(t, http.StatusAccepted, *resp.ActualStatusCode) 503 } 504 505 func TestClient_Do_WhenSuccessfulMTLSWebhook_ShouldBeSuccessful(t *testing.T) { 506 URLTemplate := "{\"method\": \"DELETE\",\"path\":\"https://test-domain.com/api/v1/applications/{{.Application.ID}}\"}" 507 outputTemplate := "{\"location\":\"{{.Headers.Location}}\",\"success_status_code\": 202,\"incomplete_status_code\": 204,\"error\": \"{{.Body.error}}\"}" 508 mtlsCalled := false 509 app := &graphql.Application{BaseEntity: &graphql.BaseEntity{ID: "appID"}} 510 webhookReq := &webhookclient.Request{ 511 Webhook: graphql.Webhook{ 512 URLTemplate: &URLTemplate, 513 OutputTemplate: &outputTemplate, 514 Mode: &webhookAsyncMode, 515 Auth: &graphql.Auth{ 516 AccessStrategy: str.Ptr(string(accessstrategy2.CMPmTLSAccessStrategy)), 517 }, 518 }, 519 Object: &webhook.ApplicationLifecycleWebhookRequestObject{Application: app}, 520 } 521 522 mtlsClient := &http.Client{ 523 Transport: mockedTransport{ 524 resp: &http.Response{ 525 Body: io.NopCloser(bytes.NewReader([]byte("{}"))), 526 Header: http.Header{"Location": []string{mockedLocationURL}}, 527 StatusCode: http.StatusAccepted, 528 }, 529 roundTripExpectations: func(r *http.Request) { 530 mtlsCalled = true 531 }, 532 }, 533 } 534 535 client := webhookclient.NewClient(nil, mtlsClient, nil) 536 537 resp, err := client.Do(context.Background(), webhookReq) 538 539 require.NoError(t, err) 540 require.True(t, mtlsCalled) 541 require.Equal(t, http.StatusAccepted, *resp.ActualStatusCode) 542 } 543 544 func TestClient_Do_WhenSuccessfulOpenStrategyWebhook_ShouldBeSuccessful(t *testing.T) { 545 URLTemplate := "{\"method\": \"DELETE\",\"path\":\"https://test-domain.com/api/v1/applications/{{.Application.ID}}\"}" 546 outputTemplate := "{\"location\":\"{{.Headers.Location}}\",\"success_status_code\": 202,\"incomplete_status_code\": 204,\"error\": \"{{.Body.error}}\"}" 547 openCalled := false 548 app := &graphql.Application{BaseEntity: &graphql.BaseEntity{ID: "appID"}} 549 webhookReq := &webhookclient.Request{ 550 Webhook: graphql.Webhook{ 551 URLTemplate: &URLTemplate, 552 OutputTemplate: &outputTemplate, 553 Mode: &webhookAsyncMode, 554 Auth: &graphql.Auth{ 555 AccessStrategy: str.Ptr(string(accessstrategy2.OpenAccessStrategy)), 556 }, 557 }, 558 Object: &webhook.ApplicationLifecycleWebhookRequestObject{Application: app}, 559 } 560 561 openClient := &http.Client{ 562 Transport: mockedTransport{ 563 resp: &http.Response{ 564 Body: io.NopCloser(bytes.NewReader([]byte("{}"))), 565 Header: http.Header{"Location": []string{mockedLocationURL}}, 566 StatusCode: http.StatusAccepted, 567 }, 568 roundTripExpectations: func(r *http.Request) { 569 openCalled = true 570 }, 571 }, 572 } 573 574 client := webhookclient.NewClient(openClient, nil, nil) 575 576 resp, err := client.Do(context.Background(), webhookReq) 577 578 require.NoError(t, err) 579 require.True(t, openCalled) 580 require.Equal(t, http.StatusAccepted, *resp.ActualStatusCode) 581 } 582 583 func TestClient_Do_WhenMissingCorrelationID_ShouldBeSuccessful(t *testing.T) { 584 URLTemplate := "{\"method\": \"DELETE\",\"path\":\"https://test-domain.com/api/v1/applications/{{.Application.ID}}\"}" 585 inputTemplate := "{\"application_id\": \"{{.Application.ID}}\",\"name\": \"{{.Application.Name}}\"}" 586 headersTemplate := "{\"user-identity\":[\"{{.Headers.Client_user}}\"]}" 587 outputTemplate := "{\"location\":\"{{.Headers.Location}}\",\"success_status_code\": 202,\"incomplete_status_code\": 204,\"error\": \"{{.Body.error}}\"}" 588 correlationIDKey := "X-Correlation-Id" 589 correlationID := "abc" 590 app := &graphql.Application{BaseEntity: &graphql.BaseEntity{ID: "appID"}} 591 webhookReq := &webhookclient.Request{ 592 Webhook: graphql.Webhook{ 593 CorrelationIDKey: &correlationIDKey, 594 URLTemplate: &URLTemplate, 595 InputTemplate: &inputTemplate, 596 HeaderTemplate: &headersTemplate, 597 OutputTemplate: &outputTemplate, 598 Mode: &webhookAsyncMode, 599 }, 600 Object: &webhook.ApplicationLifecycleWebhookRequestObject{Application: app, Headers: map[string]string{}}, 601 CorrelationID: correlationID, 602 } 603 604 client := webhookclient.NewClient(&http.Client{ 605 Transport: mockedTransport{ 606 resp: &http.Response{ 607 Body: io.NopCloser(bytes.NewReader([]byte("{}"))), 608 Header: http.Header{"Location": []string{mockedLocationURL}}, 609 StatusCode: http.StatusAccepted, 610 }, 611 roundTripExpectations: func(r *http.Request) { 612 headers := correlation.HeadersFromContext(r.Context()) 613 correlationIDAttached := false 614 for headerKey, headerValue := range headers { 615 if headerKey == correlationIDKey && headerValue == correlationID { 616 correlationIDAttached = true 617 break 618 } 619 } 620 require.True(t, correlationIDAttached) 621 }, 622 }, 623 }, nil, nil) 624 625 resp, err := client.Do(context.Background(), webhookReq) 626 627 require.NoError(t, err) 628 require.Equal(t, http.StatusAccepted, *resp.ActualStatusCode) 629 } 630 631 func TestClient_Poll_WhenHeadersTemplateIsInvalid_ShouldReturnError(t *testing.T) { 632 app := &graphql.Application{BaseEntity: &graphql.BaseEntity{ID: "appID"}} 633 webhookReq := &webhookclient.PollRequest{ 634 Request: &webhookclient.Request{ 635 Webhook: graphql.Webhook{ 636 HeaderTemplate: &invalidTemplate, 637 StatusTemplate: &emptyTemplate, 638 }, 639 Object: &webhook.ApplicationLifecycleWebhookRequestObject{Application: app}, 640 }, 641 } 642 643 client := webhookclient.NewClient(http.DefaultClient, nil, nil) 644 645 _, err := client.Poll(context.Background(), webhookReq) 646 647 require.Error(t, err) 648 require.Contains(t, err.Error(), "unable to parse webhook headers") 649 } 650 651 func TestClient_Poll_WhenCreatingRequestFails_ShouldReturnError(t *testing.T) { 652 headersTemplate := "{\"user-identity\":[\"{{.Headers.Client_user}}\"]}" 653 app := &graphql.Application{BaseEntity: &graphql.BaseEntity{ID: "appID"}} 654 webhookReq := &webhookclient.PollRequest{ 655 Request: &webhookclient.Request{ 656 Webhook: graphql.Webhook{ 657 HeaderTemplate: &headersTemplate, 658 StatusTemplate: &emptyTemplate, 659 }, 660 Object: &webhook.ApplicationLifecycleWebhookRequestObject{Application: app}, 661 }, 662 PollURL: mockedLocationURL, 663 } 664 665 client := webhookclient.NewClient(http.DefaultClient, nil, nil) 666 var ctx context.Context 667 668 _, err := client.Poll(ctx, webhookReq) 669 670 require.Error(t, err) 671 require.Contains(t, err.Error(), "nil Context") 672 } 673 674 func TestClient_Poll_WhenAuthFlowCannotBeDetermined_ShouldReturnError(t *testing.T) { 675 headersTemplate := "{\"user-identity\":[\"{{.Headers.Client_user}}\"]}" 676 app := &graphql.Application{BaseEntity: &graphql.BaseEntity{ID: "appID"}} 677 webhookReq := &webhookclient.PollRequest{ 678 679 Request: &webhookclient.Request{ 680 Webhook: graphql.Webhook{ 681 HeaderTemplate: &headersTemplate, 682 StatusTemplate: &emptyTemplate, 683 Auth: &graphql.Auth{AccessStrategy: str.Ptr("wrong")}, 684 }, 685 Object: &webhook.ApplicationLifecycleWebhookRequestObject{Application: app}, 686 }, 687 PollURL: mockedLocationURL, 688 } 689 690 client := webhookclient.NewClient(http.DefaultClient, nil, nil) 691 692 _, err := client.Poll(context.Background(), webhookReq) 693 694 require.Error(t, err) 695 require.Contains(t, err.Error(), "could not determine auth flow") 696 } 697 698 func TestClient_Poll_WhenExecutingRequestFails_ShouldReturnError(t *testing.T) { 699 headersTemplate := "{\"user-identity\":[\"{{.Headers.Client_user}}\"]}" 700 app := &graphql.Application{BaseEntity: &graphql.BaseEntity{ID: "appID"}} 701 webhookReq := &webhookclient.PollRequest{ 702 Request: &webhookclient.Request{ 703 Webhook: graphql.Webhook{ 704 HeaderTemplate: &headersTemplate, 705 StatusTemplate: &emptyTemplate, 706 }, 707 Object: &webhook.ApplicationLifecycleWebhookRequestObject{Application: app}, 708 }, 709 PollURL: mockedLocationURL, 710 } 711 712 client := webhookclient.NewClient(&http.Client{ 713 Transport: mockedTransport{err: errors.New(mockedError)}, 714 }, nil, nil) 715 716 _, err := client.Poll(context.Background(), webhookReq) 717 718 require.Error(t, err) 719 require.Contains(t, err.Error(), mockedError) 720 } 721 722 func TestClient_Poll_WhenParseStatusTemplateFails_ShouldReturnError(t *testing.T) { 723 headersTemplate := "{\"user-identity\":[\"{{.Headers.Client_user}}\"]}" 724 statusTemplate := "{\"status\":\"{{.Body.status}}\",\"success_status_identifier\":\"SUCCEEDED\",\"in_progress_status_identifier\":\"IN_PROGRESS\",\"failed_status_identifier\":\"FAILED\",\"error\": \"{{.Body.error}}\"}" 725 app := &graphql.Application{BaseEntity: &graphql.BaseEntity{ID: "appID"}} 726 webhookReq := &webhookclient.PollRequest{ 727 Request: &webhookclient.Request{ 728 Webhook: graphql.Webhook{ 729 HeaderTemplate: &headersTemplate, 730 StatusTemplate: &statusTemplate, 731 Mode: &webhookAsyncMode, 732 }, 733 Object: &webhook.ApplicationLifecycleWebhookRequestObject{Application: app}, 734 }, 735 PollURL: mockedLocationURL, 736 } 737 738 client := webhookclient.NewClient(&http.Client{ 739 Transport: mockedTransport{ 740 resp: &http.Response{Body: io.NopCloser(bytes.NewReader([]byte("{}")))}, 741 }, 742 }, nil, nil) 743 744 _, err := client.Poll(context.Background(), webhookReq) 745 746 require.Error(t, err) 747 require.Contains(t, err.Error(), "missing Status Template success status code field") 748 } 749 750 func TestClient_Poll_WhenWebhookResponseBodyContainsError_ShouldReturnError(t *testing.T) { 751 headersTemplate := "{\"user-identity\":[\"{{.Headers.Client_user}}\"]}" 752 statusTemplate := "{\"status\":\"{{.Body.status}}\",\"success_status_code\": 200,\"success_status_identifier\":\"SUCCEEDED\",\"in_progress_status_identifier\":\"IN_PROGRESS\",\"failed_status_identifier\":\"FAILED\",\"error\": \"{{.Body.error}}\"}" 753 app := &graphql.Application{BaseEntity: &graphql.BaseEntity{ID: "appID"}} 754 webhookReq := &webhookclient.PollRequest{ 755 Request: &webhookclient.Request{ 756 Webhook: graphql.Webhook{ 757 HeaderTemplate: &headersTemplate, 758 StatusTemplate: &statusTemplate, 759 Mode: &webhookAsyncMode, 760 }, 761 Object: &webhook.ApplicationLifecycleWebhookRequestObject{Application: app}, 762 }, 763 PollURL: mockedLocationURL, 764 } 765 766 client := webhookclient.NewClient(&http.Client{ 767 Transport: mockedTransport{ 768 resp: &http.Response{ 769 Body: io.NopCloser(bytes.NewReader([]byte(fmt.Sprintf("{\"error\": \"%s\"}", mockedError)))), 770 StatusCode: http.StatusOK, 771 }, 772 }, 773 }, nil, nil) 774 775 _, err := client.Poll(context.Background(), webhookReq) 776 777 require.Error(t, err) 778 require.Contains(t, err.Error(), mockedError) 779 require.Contains(t, err.Error(), "received error while calling external system") 780 } 781 782 func TestClient_Poll_WhenWebhookResponseStatusCodeIsNotSuccess_ShouldReturnError(t *testing.T) { 783 headersTemplate := "{\"user-identity\":[\"{{.Headers.Client_user}}\"]}" 784 statusTemplate := "{\"status\":\"{{.Body.status}}\",\"success_status_code\": 200,\"success_status_identifier\":\"SUCCEEDED\",\"in_progress_status_identifier\":\"IN_PROGRESS\",\"failed_status_identifier\":\"FAILED\",\"error\": \"{{.Body.error}}\"}" 785 app := &graphql.Application{BaseEntity: &graphql.BaseEntity{ID: "appID"}} 786 webhookReq := &webhookclient.PollRequest{ 787 Request: &webhookclient.Request{ 788 Webhook: graphql.Webhook{ 789 HeaderTemplate: &headersTemplate, 790 StatusTemplate: &statusTemplate, 791 Mode: &webhookAsyncMode, 792 }, 793 Object: &webhook.ApplicationLifecycleWebhookRequestObject{Application: app}, 794 }, 795 PollURL: mockedLocationURL, 796 } 797 798 client := webhookclient.NewClient(&http.Client{ 799 Transport: mockedTransport{ 800 resp: &http.Response{ 801 Body: io.NopCloser(bytes.NewReader([]byte("{}"))), 802 StatusCode: http.StatusInternalServerError, 803 }, 804 }, 805 }, nil, nil) 806 807 _, err := client.Poll(context.Background(), webhookReq) 808 809 require.Error(t, err) 810 require.Contains(t, err.Error(), fmt.Sprintf("response success status code was not met - expected success status code '200', got '%d'", http.StatusInternalServerError)) 811 } 812 813 func TestClient_Poll_WhenSuccessfulBasicAuthWebhook_ShouldBeSuccessful(t *testing.T) { 814 headersTemplate := "{\"user-identity\":[\"{{.Headers.Client_user}}\"]}" 815 statusTemplate := "{\"status\":\"{{.Body.status}}\",\"success_status_code\": 200,\"success_status_identifier\":\"SUCCEEDED\",\"in_progress_status_identifier\":\"IN_PROGRESS\",\"failed_status_identifier\":\"FAILED\",\"error\": \"{{.Body.error}}\"}" 816 username, password := "user", "pass" 817 app := &graphql.Application{BaseEntity: &graphql.BaseEntity{ID: "appID"}} 818 webhookReq := &webhookclient.PollRequest{ 819 Request: &webhookclient.Request{ 820 Webhook: graphql.Webhook{ 821 HeaderTemplate: &headersTemplate, 822 StatusTemplate: &statusTemplate, 823 Mode: &webhookAsyncMode, 824 Auth: &graphql.Auth{ 825 Credential: graphql.BasicCredentialData{ 826 Username: username, 827 Password: password, 828 }, 829 }, 830 }, 831 Object: &webhook.ApplicationLifecycleWebhookRequestObject{Application: app}, 832 }, 833 PollURL: mockedLocationURL, 834 } 835 836 client := webhookclient.NewClient(&http.Client{ 837 Transport: mockedTransport{ 838 resp: &http.Response{ 839 Body: io.NopCloser(bytes.NewReader([]byte("{}"))), 840 StatusCode: http.StatusOK, 841 }, 842 roundTripExpectations: func(r *http.Request) { 843 credentials, err := auth.LoadFromContext(r.Context()) 844 require.NoError(t, err) 845 basicCreds, ok := credentials.(*auth.BasicCredentials) 846 require.True(t, ok) 847 require.Equal(t, username, basicCreds.Username) 848 require.Equal(t, password, basicCreds.Password) 849 }, 850 }, 851 }, nil, nil) 852 853 _, err := client.Poll(context.Background(), webhookReq) 854 855 require.NoError(t, err) 856 } 857 858 func TestClient_Poll_WhenSuccessfulOAuthWebhook_ShouldBeSuccessful(t *testing.T) { 859 headersTemplate := "{\"user-identity\":[\"{{.Headers.Client_user}}\"]}" 860 statusTemplate := "{\"status\":\"{{.Body.status}}\",\"success_status_code\": 200,\"success_status_identifier\":\"SUCCEEDED\",\"in_progress_status_identifier\":\"IN_PROGRESS\",\"failed_status_identifier\":\"FAILED\",\"error\": \"{{.Body.error}}\"}" 861 clientID, clientSecret, tokenURL := "client-id", "client-secret", "https://test-domain.com/oauth/token" 862 app := &graphql.Application{BaseEntity: &graphql.BaseEntity{ID: "appID"}} 863 webhookReq := &webhookclient.PollRequest{ 864 Request: &webhookclient.Request{ 865 Webhook: graphql.Webhook{ 866 HeaderTemplate: &headersTemplate, 867 StatusTemplate: &statusTemplate, 868 Mode: &webhookAsyncMode, 869 Auth: &graphql.Auth{ 870 Credential: graphql.OAuthCredentialData{ 871 ClientID: "client-id", 872 ClientSecret: "client-secret", 873 URL: "https://test-domain.com/oauth/token", 874 }, 875 }, 876 }, 877 Object: &webhook.ApplicationLifecycleWebhookRequestObject{Application: app}, 878 }, 879 PollURL: mockedLocationURL, 880 } 881 882 client := webhookclient.NewClient(&http.Client{ 883 Transport: mockedTransport{ 884 resp: &http.Response{ 885 Body: io.NopCloser(bytes.NewReader([]byte("{}"))), 886 StatusCode: http.StatusOK, 887 }, 888 roundTripExpectations: func(r *http.Request) { 889 credentials, err := auth.LoadFromContext(r.Context()) 890 require.NoError(t, err) 891 oAuthCredentials, ok := credentials.(*auth.OAuthCredentials) 892 require.True(t, ok) 893 require.Equal(t, clientID, oAuthCredentials.ClientID) 894 require.Equal(t, clientSecret, oAuthCredentials.ClientSecret) 895 require.Equal(t, tokenURL, oAuthCredentials.TokenURL) 896 }, 897 }, 898 }, nil, nil) 899 _, err := client.Poll(context.Background(), webhookReq) 900 901 require.NoError(t, err) 902 } 903 904 func TestClient_Poll_WhenSuccessfulMTLSWebhook_ShouldBeSuccessful(t *testing.T) { 905 statusTemplate := "{\"status\":\"{{.Body.status}}\",\"success_status_code\": 200,\"success_status_identifier\":\"SUCCEEDED\",\"in_progress_status_identifier\":\"IN_PROGRESS\",\"failed_status_identifier\":\"FAILED\",\"error\": \"{{.Body.error}}\"}" 906 outputTemplate := "{\"location\":\"{{.Headers.Location}}\",\"success_status_code\": 202,\"incomplete_status_code\": 204,\"error\": \"{{.Body.error}}\"}" 907 mtlsCalled := false 908 909 app := &graphql.Application{BaseEntity: &graphql.BaseEntity{ID: "appID"}} 910 pollRequest := &webhookclient.PollRequest{ 911 Request: &webhookclient.Request{ 912 Webhook: graphql.Webhook{ 913 OutputTemplate: &outputTemplate, 914 StatusTemplate: &statusTemplate, 915 Mode: &webhookAsyncMode, 916 Auth: &graphql.Auth{ 917 AccessStrategy: str.Ptr(string(accessstrategy2.CMPmTLSAccessStrategy)), 918 }, 919 }, 920 Object: &webhook.ApplicationLifecycleWebhookRequestObject{Application: app}, 921 }, 922 PollURL: "https://test-domain.com/poll/", 923 } 924 925 mtlsClient := &http.Client{ 926 Transport: mockedTransport{ 927 resp: &http.Response{ 928 Body: io.NopCloser(bytes.NewReader([]byte("{}"))), 929 Header: http.Header{"Location": []string{mockedLocationURL}}, 930 StatusCode: http.StatusOK, 931 }, 932 roundTripExpectations: func(r *http.Request) { 933 mtlsCalled = true 934 }, 935 }, 936 } 937 938 client := webhookclient.NewClient(nil, mtlsClient, nil) 939 940 _, err := client.Poll(context.Background(), pollRequest) 941 942 require.NoError(t, err) 943 require.True(t, mtlsCalled) 944 } 945 946 func TestClient_Poll_WhenSuccessfulWebhookPollResponseContainsNullErrorField_ShouldBeSuccessful(t *testing.T) { 947 headersTemplate := "{\"user-identity\":[\"{{.Headers.Client_user}}\"]}" 948 statusTemplate := "{\"status\":\"{{.Body.status}}\",\"success_status_code\": 200,\"success_status_identifier\":\"SUCCEEDED\",\"in_progress_status_identifier\":\"IN_PROGRESS\",\"failed_status_identifier\":\"FAILED\",\"error\": \"{{.Body.error}}\"}" 949 app := &graphql.Application{BaseEntity: &graphql.BaseEntity{ID: "appID"}} 950 webhookReq := &webhookclient.PollRequest{ 951 Request: &webhookclient.Request{ 952 Webhook: graphql.Webhook{ 953 HeaderTemplate: &headersTemplate, 954 StatusTemplate: &statusTemplate, 955 Mode: &webhookAsyncMode, 956 }, 957 Object: &webhook.ApplicationLifecycleWebhookRequestObject{Application: app}, 958 }, 959 PollURL: mockedLocationURL, 960 } 961 962 client := webhookclient.NewClient(&http.Client{ 963 Transport: mockedTransport{ 964 resp: &http.Response{ 965 Body: io.NopCloser(bytes.NewReader([]byte("{\"error\":null}"))), 966 StatusCode: http.StatusOK, 967 }, 968 }, 969 }, nil, nil) 970 _, err := client.Poll(context.Background(), webhookReq) 971 972 require.NoError(t, err) 973 } 974 975 func TestClient_Poll_WhenSuccessfulWebhookPollResponseContainsEmptyErrorField_ShouldBeSuccessful(t *testing.T) { 976 headersTemplate := "{\"user-identity\":[\"{{.Headers.Client_user}}\"]}" 977 statusTemplate := "{\"status\":\"{{.Body.status}}\",\"success_status_code\": 200,\"success_status_identifier\":\"SUCCEEDED\",\"in_progress_status_identifier\":\"IN_PROGRESS\",\"failed_status_identifier\":\"FAILED\",\"error\": \"{{.Body.error}}\"}" 978 app := &graphql.Application{BaseEntity: &graphql.BaseEntity{ID: "appID"}} 979 webhookReq := &webhookclient.PollRequest{ 980 Request: &webhookclient.Request{ 981 Webhook: graphql.Webhook{ 982 HeaderTemplate: &headersTemplate, 983 StatusTemplate: &statusTemplate, 984 Mode: &webhookAsyncMode, 985 }, 986 Object: &webhook.ApplicationLifecycleWebhookRequestObject{Application: app}, 987 }, 988 PollURL: mockedLocationURL, 989 } 990 991 client := webhookclient.NewClient(&http.Client{ 992 Transport: mockedTransport{ 993 resp: &http.Response{ 994 Body: io.NopCloser(bytes.NewReader([]byte("{\"error\":\"\"}"))), 995 StatusCode: http.StatusOK, 996 }, 997 }, 998 }, nil, nil) 999 _, err := client.Poll(context.Background(), webhookReq) 1000 1001 require.NoError(t, err) 1002 } 1003 1004 func TestClient_Poll_WhenMissingCorrelationID_ShouldBeSuccessful(t *testing.T) { 1005 headersTemplate := "{\"user-identity\":[\"{{.Headers.Client_user}}\"]}" 1006 statusTemplate := "{\"status\":\"{{.Body.status}}\",\"success_status_code\": 200,\"success_status_identifier\":\"SUCCEEDED\",\"in_progress_status_identifier\":\"IN_PROGRESS\",\"failed_status_identifier\":\"FAILED\",\"error\": \"{{.Body.error}}\"}" 1007 correlationIDKey := "X-Correlation-Id" 1008 correlationID := "abc" 1009 app := &graphql.Application{BaseEntity: &graphql.BaseEntity{ID: "appID"}} 1010 webhookReq := &webhookclient.PollRequest{ 1011 Request: &webhookclient.Request{ 1012 Webhook: graphql.Webhook{ 1013 CorrelationIDKey: &correlationIDKey, 1014 HeaderTemplate: &headersTemplate, 1015 StatusTemplate: &statusTemplate, 1016 Mode: &webhookAsyncMode, 1017 }, 1018 Object: &webhook.ApplicationLifecycleWebhookRequestObject{Application: app, Headers: map[string]string{}}, 1019 CorrelationID: correlationID, 1020 }, 1021 PollURL: mockedLocationURL, 1022 } 1023 1024 client := webhookclient.NewClient(&http.Client{ 1025 Transport: mockedTransport{ 1026 resp: &http.Response{ 1027 Body: io.NopCloser(bytes.NewReader([]byte("{}"))), 1028 StatusCode: http.StatusOK, 1029 }, 1030 roundTripExpectations: func(r *http.Request) { 1031 headers := correlation.HeadersFromContext(r.Context()) 1032 correlationIDAttached := false 1033 for headerKey, headerValue := range headers { 1034 if headerKey == correlationIDKey && headerValue == correlationID { 1035 correlationIDAttached = true 1036 break 1037 } 1038 } 1039 require.True(t, correlationIDAttached) 1040 }, 1041 }, 1042 }, nil, nil) 1043 1044 _, err := client.Poll(context.Background(), webhookReq) 1045 1046 require.NoError(t, err) 1047 } 1048 1049 type mockedTransport struct { 1050 resp *http.Response 1051 err error 1052 roundTripExpectations func(r *http.Request) 1053 } 1054 1055 func (m mockedTransport) RoundTrip(r *http.Request) (*http.Response, error) { 1056 if m.roundTripExpectations != nil { 1057 m.roundTripExpectations(r) 1058 } 1059 return m.resp, m.err 1060 }