github.com/kyma-incubator/compass/components/director@v0.0.0-20230623144113-d764f56ff805/internal/domain/fetchrequest/service_test.go (about)

     1  package fetchrequest_test
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"net/http"
    10  	"testing"
    11  	"time"
    12  
    13  	"github.com/kyma-incubator/compass/components/director/pkg/retry"
    14  
    15  	"github.com/kyma-incubator/compass/components/director/internal/domain/tenant"
    16  	"github.com/kyma-incubator/compass/components/director/pkg/certloader"
    17  
    18  	"github.com/kyma-incubator/compass/components/director/pkg/accessstrategy"
    19  	accessstrategyautomock "github.com/kyma-incubator/compass/components/director/pkg/accessstrategy/automock"
    20  
    21  	"github.com/kyma-incubator/compass/components/director/pkg/str"
    22  	"github.com/stretchr/testify/mock"
    23  
    24  	"github.com/kyma-incubator/compass/components/director/internal/domain/fetchrequest/automock"
    25  
    26  	"github.com/kyma-incubator/compass/components/director/internal/domain/fetchrequest"
    27  
    28  	"github.com/kyma-incubator/compass/components/director/internal/model"
    29  	"github.com/kyma-incubator/compass/components/director/pkg/apperrors"
    30  	"github.com/stretchr/testify/assert"
    31  )
    32  
    33  const (
    34  	externalClientCertSecretName = "resource-name1"
    35  	extSvcClientCertSecretName   = "resource-name2"
    36  )
    37  
    38  var testErr = errors.New("test")
    39  
    40  type RoundTripFunc func(req *http.Request) *http.Response
    41  
    42  func (f RoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
    43  	response := f(req)
    44  	if response.StatusCode == http.StatusBadRequest {
    45  		return nil, errors.New("error")
    46  	}
    47  	return response, nil
    48  }
    49  
    50  func NewTestClient(fn RoundTripFunc) *http.Client {
    51  	return &http.Client{
    52  		Transport: fn,
    53  	}
    54  }
    55  
    56  func TestService_Update(t *testing.T) {
    57  	fetchReq := &model.FetchRequest{
    58  		ID:   "test",
    59  		Mode: model.FetchModeSingle,
    60  	}
    61  
    62  	testCases := []struct {
    63  		Name                 string
    64  		Context              context.Context
    65  		FetchRequest         *model.FetchRequest
    66  		FetchRequestRepoMock *automock.FetchRequestRepository
    67  		ExpectedError        error
    68  	}{
    69  
    70  		{
    71  			Name:         "Success",
    72  			Context:      tenant.SaveToContext(context.TODO(), tenantID, tenantID),
    73  			FetchRequest: fetchReq,
    74  			FetchRequestRepoMock: func() *automock.FetchRequestRepository {
    75  				fetchReqRepoMock := automock.FetchRequestRepository{}
    76  				fetchReqRepoMock.On("Update", mock.Anything, tenantID, fetchReq).Return(nil).Once()
    77  				return &fetchReqRepoMock
    78  			}(),
    79  			ExpectedError: nil,
    80  		},
    81  		{
    82  			Name:                 "Fails when tenant is missing in context",
    83  			Context:              context.TODO(),
    84  			FetchRequest:         fetchReq,
    85  			FetchRequestRepoMock: &automock.FetchRequestRepository{},
    86  			ExpectedError:        apperrors.NewCannotReadTenantError(),
    87  		},
    88  		{
    89  			Name:         "Fails when repo update fails",
    90  			Context:      tenant.SaveToContext(context.TODO(), tenantID, tenantID),
    91  			FetchRequest: fetchReq,
    92  			FetchRequestRepoMock: func() *automock.FetchRequestRepository {
    93  				fetchReqRepoMock := automock.FetchRequestRepository{}
    94  				fetchReqRepoMock.On("Update", mock.Anything, tenantID, fetchReq).Return(testErr).Once()
    95  				return &fetchReqRepoMock
    96  			}(),
    97  			ExpectedError: testErr,
    98  		},
    99  	}
   100  
   101  	for _, testCase := range testCases {
   102  		t.Run(testCase.Name, func(t *testing.T) {
   103  			repo := testCase.FetchRequestRepoMock
   104  			svc := fetchrequest.NewService(repo, nil, nil)
   105  			resultErr := svc.Update(testCase.Context, testCase.FetchRequest)
   106  			assert.Equal(t, testCase.ExpectedError, resultErr)
   107  
   108  			repo.AssertExpectations(t)
   109  		})
   110  	}
   111  }
   112  
   113  func TestService_UpdateGlobal(t *testing.T) {
   114  	fetchReq := &model.FetchRequest{
   115  		ID:   "test",
   116  		Mode: model.FetchModeSingle,
   117  	}
   118  
   119  	testCases := []struct {
   120  		Name                 string
   121  		FetchRequest         *model.FetchRequest
   122  		FetchRequestRepoMock *automock.FetchRequestRepository
   123  		ExpectedError        error
   124  	}{
   125  
   126  		{
   127  			Name:         "Success",
   128  			FetchRequest: fetchReq,
   129  			FetchRequestRepoMock: func() *automock.FetchRequestRepository {
   130  				fetchReqRepoMock := automock.FetchRequestRepository{}
   131  				fetchReqRepoMock.On("UpdateGlobal", mock.Anything, fetchReq).Return(nil).Once()
   132  				return &fetchReqRepoMock
   133  			}(),
   134  			ExpectedError: nil,
   135  		},
   136  		{
   137  			Name:         "Fails when repo update fails",
   138  			FetchRequest: fetchReq,
   139  			FetchRequestRepoMock: func() *automock.FetchRequestRepository {
   140  				fetchReqRepoMock := automock.FetchRequestRepository{}
   141  				fetchReqRepoMock.On("UpdateGlobal", mock.Anything, fetchReq).Return(testErr).Once()
   142  				return &fetchReqRepoMock
   143  			}(),
   144  			ExpectedError: testErr,
   145  		},
   146  	}
   147  
   148  	for _, testCase := range testCases {
   149  		t.Run(testCase.Name, func(t *testing.T) {
   150  			repo := testCase.FetchRequestRepoMock
   151  			svc := fetchrequest.NewService(repo, nil, nil)
   152  			resultErr := svc.UpdateGlobal(context.TODO(), testCase.FetchRequest)
   153  			assert.Equal(t, testCase.ExpectedError, resultErr)
   154  
   155  			repo.AssertExpectations(t)
   156  		})
   157  	}
   158  }
   159  
   160  func TestService_HandleSpec(t *testing.T) {
   161  	const username = "username"
   162  	const password = "password"
   163  	const clientID = "clId"
   164  	const secret = "clSecret"
   165  	const url = "mocked-url/oauth/token"
   166  
   167  	var testAccessStrategy = "testAccessStrategy"
   168  
   169  	mockSpec := "spec"
   170  	timestamp := time.Now()
   171  
   172  	modelInput := model.FetchRequest{
   173  		ID:   "test",
   174  		Mode: model.FetchModeSingle,
   175  	}
   176  
   177  	modelInputBundle := model.FetchRequest{
   178  		ID:   "test",
   179  		Mode: model.FetchModeBundle,
   180  	}
   181  
   182  	modelInputFilter := model.FetchRequest{
   183  		ID:     "test",
   184  		Mode:   model.FetchModeSingle,
   185  		Filter: str.Ptr("filter"),
   186  	}
   187  
   188  	modelInputAccessStrategy := model.FetchRequest{
   189  		ID:   "test",
   190  		Mode: model.FetchModeSingle,
   191  		URL:  "http://test.com",
   192  		Auth: &model.Auth{AccessStrategy: &testAccessStrategy},
   193  	}
   194  
   195  	modelInputBasicCredentials := model.FetchRequest{
   196  		ID: "test",
   197  		Auth: &model.Auth{
   198  			Credential: model.CredentialData{
   199  				Basic: &model.BasicCredentialData{
   200  					Username: username,
   201  					Password: password,
   202  				},
   203  			},
   204  		},
   205  		Mode: model.FetchModeSingle,
   206  	}
   207  
   208  	modelInputMissingCredentials := model.FetchRequest{
   209  		ID: "test",
   210  		Auth: &model.Auth{
   211  			Credential: model.CredentialData{
   212  				Basic: nil,
   213  				Oauth: nil,
   214  			},
   215  		},
   216  		Mode: model.FetchModeSingle,
   217  	}
   218  
   219  	modelInputOauth := model.FetchRequest{
   220  		ID:  "test",
   221  		URL: "http://dummy.url.sth",
   222  		Auth: &model.Auth{
   223  			Credential: model.CredentialData{
   224  				Basic: nil,
   225  				Oauth: &model.OAuthCredentialData{
   226  					ClientID:     clientID,
   227  					ClientSecret: secret,
   228  					URL:          url,
   229  				},
   230  			},
   231  		},
   232  		Mode: model.FetchModeSingle,
   233  	}
   234  
   235  	testCases := []struct {
   236  		Name                 string
   237  		Client               func(t *testing.T) *http.Client
   238  		localTenantID        string
   239  		InputFr              model.FetchRequest
   240  		ExecutorProviderFunc func() accessstrategy.ExecutorProvider
   241  		ExpectedResult       *string
   242  		ExpectedStatus       *model.FetchRequestStatus
   243  	}{
   244  
   245  		{
   246  			Name: "Success without authentication",
   247  			Client: func(t *testing.T) *http.Client {
   248  				return NewTestClient(func(req *http.Request) *http.Response {
   249  					return &http.Response{
   250  						StatusCode: http.StatusOK,
   251  						Body:       io.NopCloser(bytes.NewBufferString(mockSpec)),
   252  					}
   253  				})
   254  			},
   255  			InputFr:        modelInput,
   256  			localTenantID:  localTenantID,
   257  			ExpectedResult: &mockSpec,
   258  			ExpectedStatus: fetchrequest.FixStatus(model.FetchRequestStatusConditionSucceeded, nil, timestamp),
   259  		},
   260  		{
   261  			Name: "Success when local tenant id is missing",
   262  			Client: func(t *testing.T) *http.Client {
   263  				return NewTestClient(func(req *http.Request) *http.Response {
   264  					return &http.Response{
   265  						StatusCode: http.StatusOK,
   266  						Body:       io.NopCloser(bytes.NewBufferString(mockSpec)),
   267  					}
   268  				})
   269  			},
   270  			InputFr:        modelInput,
   271  			ExpectedResult: &mockSpec,
   272  			ExpectedStatus: fetchrequest.FixStatus(model.FetchRequestStatusConditionSucceeded, nil, timestamp),
   273  		},
   274  		{
   275  			Name: "Nil when fetch request validation fails due to mode Bundle",
   276  			Client: func(t *testing.T) *http.Client {
   277  				return NewTestClient(func(req *http.Request) *http.Response {
   278  					return &http.Response{}
   279  				})
   280  			},
   281  
   282  			InputFr:        modelInputBundle,
   283  			localTenantID:  localTenantID,
   284  			ExpectedResult: nil,
   285  			ExpectedStatus: fetchrequest.FixStatus(model.FetchRequestStatusConditionInitial, str.Ptr("Invalid data [reason=Unsupported fetch mode: BUNDLE]"), timestamp),
   286  		},
   287  		{
   288  			Name: "Nil when fetch request validation fails due to provided filter",
   289  			Client: func(t *testing.T) *http.Client {
   290  				return NewTestClient(func(req *http.Request) *http.Response {
   291  					return &http.Response{}
   292  				})
   293  			},
   294  
   295  			InputFr:        modelInputFilter,
   296  			localTenantID:  localTenantID,
   297  			ExpectedResult: nil,
   298  			ExpectedStatus: fetchrequest.FixStatus(model.FetchRequestStatusConditionInitial, str.Ptr("Invalid data [reason=Filter for Fetch Request was provided, currently it's unsupported]"), timestamp),
   299  		},
   300  		{
   301  			Name: "Success with access strategy",
   302  			ExecutorProviderFunc: func() accessstrategy.ExecutorProvider {
   303  				executor := &accessstrategyautomock.Executor{}
   304  				executor.On("Execute", mock.Anything, mock.Anything, modelInputAccessStrategy.URL, localTenantID).Return(&http.Response{
   305  					StatusCode: http.StatusOK,
   306  					Body:       io.NopCloser(bytes.NewBufferString(mockSpec)),
   307  				}, nil).Once()
   308  
   309  				executorProvider := &accessstrategyautomock.ExecutorProvider{}
   310  				executorProvider.On("Provide", accessstrategy.Type(testAccessStrategy)).Return(executor, nil).Once()
   311  				return executorProvider
   312  			},
   313  			Client: func(t *testing.T) *http.Client {
   314  				return nil
   315  			},
   316  			InputFr:        modelInputAccessStrategy,
   317  			localTenantID:  localTenantID,
   318  			ExpectedResult: &mockSpec,
   319  			ExpectedStatus: fetchrequest.FixStatus(model.FetchRequestStatusConditionSucceeded, nil, timestamp),
   320  		},
   321  		{
   322  			Name: "Fails when access strategy is unknown",
   323  			ExecutorProviderFunc: func() accessstrategy.ExecutorProvider {
   324  				executorProvider := &accessstrategyautomock.ExecutorProvider{}
   325  				executorProvider.On("Provide", accessstrategy.Type(testAccessStrategy)).Return(nil, testErr).Once()
   326  				return executorProvider
   327  			},
   328  			Client: func(t *testing.T) *http.Client {
   329  				return nil
   330  			},
   331  			InputFr:        modelInputAccessStrategy,
   332  			localTenantID:  localTenantID,
   333  			ExpectedStatus: fetchrequest.FixStatus(model.FetchRequestStatusConditionFailed, str.Ptr("While fetching Spec: test"), timestamp),
   334  		},
   335  		{
   336  			Name: "Fails when access strategy execution fail",
   337  			ExecutorProviderFunc: func() accessstrategy.ExecutorProvider {
   338  				executor := &accessstrategyautomock.Executor{}
   339  				executor.On("Execute", mock.Anything, mock.Anything, modelInputAccessStrategy.URL, localTenantID).Return(nil, testErr).Once()
   340  
   341  				executorProvider := &accessstrategyautomock.ExecutorProvider{}
   342  				executorProvider.On("Provide", accessstrategy.Type(testAccessStrategy)).Return(executor, nil).Once()
   343  				return executorProvider
   344  			},
   345  			Client: func(t *testing.T) *http.Client {
   346  				return nil
   347  			},
   348  			InputFr:        modelInputAccessStrategy,
   349  			localTenantID:  localTenantID,
   350  			ExpectedStatus: fetchrequest.FixStatus(model.FetchRequestStatusConditionFailed, str.Ptr("While fetching Spec: test"), timestamp),
   351  		},
   352  		{
   353  			Name: "Success with basic authentication",
   354  			Client: func(t *testing.T) *http.Client {
   355  				return NewTestClient(func(req *http.Request) *http.Response {
   356  					actualUsername, actualPassword, ok := req.BasicAuth()
   357  					assert.True(t, ok)
   358  					assert.Equal(t, username, actualUsername)
   359  					assert.Equal(t, password, actualPassword)
   360  					return &http.Response{
   361  						StatusCode: http.StatusOK,
   362  						Body:       io.NopCloser(bytes.NewBufferString(mockSpec)),
   363  					}
   364  				})
   365  			},
   366  			InputFr:        modelInputBasicCredentials,
   367  			localTenantID:  localTenantID,
   368  			ExpectedResult: &mockSpec,
   369  			ExpectedStatus: fetchrequest.FixStatus(model.FetchRequestStatusConditionSucceeded, nil, timestamp),
   370  		},
   371  		{
   372  			Name: "Fails to execute the request with basic authentication",
   373  			Client: func(t *testing.T) *http.Client {
   374  				return NewTestClient(func(req *http.Request) *http.Response {
   375  					actualUsername, actualPassword, ok := req.BasicAuth()
   376  					assert.True(t, ok)
   377  					assert.Equal(t, username, actualUsername)
   378  					assert.Equal(t, password, actualPassword)
   379  					return &http.Response{
   380  						StatusCode: http.StatusInternalServerError,
   381  					}
   382  				})
   383  			},
   384  			InputFr:        modelInputBasicCredentials,
   385  			localTenantID:  localTenantID,
   386  			ExpectedStatus: fetchrequest.FixStatus(model.FetchRequestStatusConditionFailed, str.Ptr("While fetching Spec status code: 500"), timestamp),
   387  		},
   388  		{
   389  			Name: "Nil when auth without credentials is provided",
   390  			Client: func(t *testing.T) *http.Client {
   391  				return NewTestClient(func(req *http.Request) *http.Response {
   392  					return &http.Response{}
   393  				})
   394  			},
   395  
   396  			InputFr:        modelInputMissingCredentials,
   397  			localTenantID:  localTenantID,
   398  			ExpectedResult: nil,
   399  			ExpectedStatus: fetchrequest.FixStatus(model.FetchRequestStatusConditionFailed, str.Ptr("While fetching Spec: Invalid data [reason=Credentials not provided]"), timestamp),
   400  		},
   401  		{
   402  			Name: "Success with oauth authentication",
   403  			Client: func(t *testing.T) *http.Client {
   404  				return NewTestClient(func(req *http.Request) *http.Response {
   405  					if req.URL.String() == url {
   406  						actualClientID, actualSecret, ok := req.BasicAuth()
   407  						assert.True(t, ok)
   408  						assert.Equal(t, clientID, actualClientID)
   409  						assert.Equal(t, secret, actualSecret)
   410  						return &http.Response{
   411  							StatusCode: http.StatusOK,
   412  							Body:       io.NopCloser(bytes.NewBufferString(`{"access_token":"token"}`)),
   413  						}
   414  					}
   415  
   416  					return &http.Response{
   417  						StatusCode: http.StatusOK,
   418  						Body:       io.NopCloser(bytes.NewBufferString(mockSpec)),
   419  					}
   420  				})
   421  			},
   422  			InputFr:        modelInputOauth,
   423  			localTenantID:  localTenantID,
   424  			ExpectedResult: &mockSpec,
   425  			ExpectedStatus: fetchrequest.FixStatus(model.FetchRequestStatusConditionSucceeded, nil, timestamp),
   426  		},
   427  		{
   428  			Name: "Fails to fetch oauth token with oauth authentication",
   429  			Client: func(t *testing.T) *http.Client {
   430  				return NewTestClient(func(req *http.Request) *http.Response {
   431  					actualClientID, actualSecret, ok := req.BasicAuth()
   432  					if ok {
   433  						assert.Equal(t, clientID, actualClientID)
   434  						assert.Equal(t, secret, actualSecret)
   435  					} else {
   436  						credentials, err := io.ReadAll(req.Body)
   437  						assert.NoError(t, err)
   438  						assert.Contains(t, string(credentials), fmt.Sprintf("client_id=%s&client_secret=%s&grant_type=client_credentials", clientID, secret))
   439  					}
   440  					return &http.Response{
   441  						StatusCode: http.StatusInternalServerError,
   442  					}
   443  				})
   444  			},
   445  			InputFr:        modelInputOauth,
   446  			localTenantID:  localTenantID,
   447  			ExpectedStatus: fetchrequest.FixStatus(model.FetchRequestStatusConditionFailed, str.Ptr("While fetching Spec: Get \"http://dummy.url.sth\": oauth2: cannot fetch token: \nResponse: "), timestamp),
   448  		},
   449  		{
   450  			Name: "Fails to execute the request with oauth authentication",
   451  			Client: func(t *testing.T) *http.Client {
   452  				return NewTestClient(func(req *http.Request) *http.Response {
   453  					if req.URL.String() == url {
   454  						actualClientID, actualSecret, ok := req.BasicAuth()
   455  						assert.True(t, ok)
   456  						assert.Equal(t, clientID, actualClientID)
   457  						assert.Equal(t, secret, actualSecret)
   458  						return &http.Response{
   459  							StatusCode: http.StatusOK,
   460  							Body:       io.NopCloser(bytes.NewBufferString(`{"access_token":"token"}`)),
   461  						}
   462  					}
   463  
   464  					return &http.Response{
   465  						StatusCode: http.StatusInternalServerError,
   466  						Body:       io.NopCloser(bytes.NewBufferString(mockSpec)),
   467  					}
   468  				})
   469  			},
   470  			InputFr:        modelInputOauth,
   471  			localTenantID:  localTenantID,
   472  			ExpectedStatus: fetchrequest.FixStatus(model.FetchRequestStatusConditionFailed, str.Ptr("While fetching Spec status code: 500"), timestamp),
   473  		},
   474  	}
   475  
   476  	for _, testCase := range testCases {
   477  		t.Run(testCase.Name, func(t *testing.T) {
   478  			certCache := certloader.NewCertificateCache()
   479  			var executorProviderMock accessstrategy.ExecutorProvider = accessstrategy.NewDefaultExecutorProvider(certCache, externalClientCertSecretName, extSvcClientCertSecretName)
   480  			if testCase.ExecutorProviderFunc != nil {
   481  				executorProviderMock = testCase.ExecutorProviderFunc()
   482  			}
   483  
   484  			ctx := context.TODO()
   485  			ctx = tenant.SaveToContext(ctx, tenantID, tenantID)
   486  			ctx = tenant.SaveLocalTenantIDToContext(ctx, testCase.localTenantID)
   487  
   488  			frRepo := &automock.FetchRequestRepository{}
   489  			frRepo.On("Update", ctx, tenantID, mock.Anything).Return(nil).Once()
   490  
   491  			svc := fetchrequest.NewService(frRepo, testCase.Client(t), executorProviderMock)
   492  			svc.SetTimestampGen(func() time.Time { return timestamp })
   493  
   494  			result := svc.HandleSpec(ctx, &testCase.InputFr)
   495  
   496  			assert.Equal(t, testCase.ExpectedStatus, testCase.InputFr.Status)
   497  			assert.Equal(t, testCase.ExpectedResult, result)
   498  
   499  			if testCase.ExecutorProviderFunc != nil {
   500  				mock.AssertExpectationsForObjects(t, executorProviderMock)
   501  			}
   502  		})
   503  	}
   504  }
   505  
   506  func TestService_HandleSpec_FailedToUpdateStatusAfterFetching(t *testing.T) {
   507  	ctx := context.TODO()
   508  	ctx = tenant.SaveToContext(ctx, tenantID, tenantID)
   509  	ctx = tenant.SaveLocalTenantIDToContext(ctx, localTenantID)
   510  
   511  	timestamp := time.Now()
   512  	frRepo := &automock.FetchRequestRepository{}
   513  	frRepo.On("Update", ctx, tenantID, mock.Anything).Return(errors.New("error")).Once()
   514  
   515  	certCache := certloader.NewCertificateCache()
   516  	svc := fetchrequest.NewService(frRepo, NewTestClient(func(req *http.Request) *http.Response {
   517  		return &http.Response{
   518  			StatusCode: http.StatusOK,
   519  			Body:       io.NopCloser(bytes.NewBufferString("spec")),
   520  		}
   521  	}), accessstrategy.NewDefaultExecutorProvider(certCache, externalClientCertSecretName, extSvcClientCertSecretName))
   522  	svc.SetTimestampGen(func() time.Time { return timestamp })
   523  
   524  	modelInput := &model.FetchRequest{
   525  		ID:   "test",
   526  		Mode: model.FetchModeSingle,
   527  	}
   528  
   529  	result := svc.HandleSpec(ctx, modelInput)
   530  	expectedStatus := fetchrequest.FixStatus(model.FetchRequestStatusConditionSucceeded, nil, timestamp)
   531  
   532  	assert.Equal(t, expectedStatus, modelInput.Status)
   533  	assert.Nil(t, result)
   534  }
   535  
   536  func TestService_HandleSpec_SucceedsAfterRetryMechanismIsLeveraged(t *testing.T) {
   537  	ctx := context.TODO()
   538  	ctx = tenant.SaveToContext(ctx, tenantID, tenantID)
   539  	ctx = tenant.SaveLocalTenantIDToContext(ctx, localTenantID)
   540  
   541  	timestamp := time.Now()
   542  	frRepo := &automock.FetchRequestRepository{}
   543  	frRepo.On("Update", ctx, tenantID, mock.Anything).Return(nil).Once()
   544  
   545  	certCache := certloader.NewCertificateCache()
   546  	retryConfig := &retry.Config{
   547  		Attempts: 3,
   548  		Delay:    100 * time.Millisecond,
   549  	}
   550  
   551  	mockSpec := "spec"
   552  
   553  	invocations := 0
   554  	svc := fetchrequest.NewServiceWithRetry(frRepo, NewTestClient(func(req *http.Request) *http.Response {
   555  		defer func() {
   556  			invocations++
   557  		}()
   558  
   559  		if invocations != int(retryConfig.Attempts)-1 {
   560  			return &http.Response{StatusCode: http.StatusInternalServerError}
   561  		}
   562  
   563  		return &http.Response{
   564  			StatusCode: http.StatusOK,
   565  			Body:       io.NopCloser(bytes.NewBufferString(mockSpec)),
   566  		}
   567  	}), accessstrategy.NewDefaultExecutorProvider(certCache, externalClientCertSecretName, extSvcClientCertSecretName), retry.NewHTTPExecutor(retryConfig))
   568  	svc.SetTimestampGen(func() time.Time { return timestamp })
   569  
   570  	modelInput := &model.FetchRequest{
   571  		ID:   "test",
   572  		Mode: model.FetchModeSingle,
   573  	}
   574  
   575  	result := svc.HandleSpec(ctx, modelInput)
   576  	expectedStatus := fetchrequest.FixStatus(model.FetchRequestStatusConditionSucceeded, nil, timestamp)
   577  
   578  	assert.Equal(t, expectedStatus, modelInput.Status)
   579  	assert.Equal(t, mockSpec, *result)
   580  	assert.Equal(t, int(retryConfig.Attempts), invocations)
   581  }
   582  
   583  func TestService_HandleSpec_FailsAfterRetryMechanismIsExhausted(t *testing.T) {
   584  	ctx := context.TODO()
   585  	ctx = tenant.SaveToContext(ctx, tenantID, tenantID)
   586  	ctx = tenant.SaveLocalTenantIDToContext(ctx, localTenantID)
   587  
   588  	timestamp := time.Now()
   589  	frRepo := &automock.FetchRequestRepository{}
   590  	frRepo.On("Update", ctx, tenantID, mock.Anything).Return(nil).Once()
   591  
   592  	certCache := certloader.NewCertificateCache()
   593  	retryConfig := &retry.Config{
   594  		Attempts: 3,
   595  		Delay:    100 * time.Millisecond,
   596  	}
   597  
   598  	invocations := 0
   599  	svc := fetchrequest.NewServiceWithRetry(frRepo, NewTestClient(func(req *http.Request) *http.Response {
   600  		defer func() {
   601  			invocations++
   602  		}()
   603  
   604  		return &http.Response{StatusCode: http.StatusInternalServerError}
   605  	}), accessstrategy.NewDefaultExecutorProvider(certCache, externalClientCertSecretName, extSvcClientCertSecretName), retry.NewHTTPExecutor(retryConfig))
   606  	svc.SetTimestampGen(func() time.Time { return timestamp })
   607  
   608  	modelInput := &model.FetchRequest{
   609  		ID:   "test",
   610  		Mode: model.FetchModeSingle,
   611  	}
   612  
   613  	result := svc.HandleSpec(ctx, modelInput)
   614  	respStatusCodeErr := fmt.Sprintf("unexpected status code: %d", http.StatusInternalServerError)
   615  	expectedErr := fmt.Sprintf("All attempts fail:\n#1: %s\n#2: %s\n#3: %s", respStatusCodeErr, respStatusCodeErr, respStatusCodeErr)
   616  	expectedStatus := fetchrequest.FixStatus(model.FetchRequestStatusConditionFailed, str.Ptr(fmt.Sprintf("While fetching Spec: %s", expectedErr)), timestamp)
   617  
   618  	assert.Equal(t, expectedStatus, modelInput.Status)
   619  	assert.Nil(t, result)
   620  	assert.Equal(t, int(retryConfig.Attempts), invocations)
   621  }