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  }