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

     1  package systemfetcher_test
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"net/http"
     7  	"net/http/httptest"
     8  	"sync"
     9  	"testing"
    10  	"time"
    11  
    12  	"github.com/kyma-incubator/compass/components/director/internal/model"
    13  
    14  	"github.com/kyma-incubator/compass/components/director/pkg/oauth"
    15  
    16  	"github.com/kyma-incubator/compass/components/director/internal/systemfetcher"
    17  	"github.com/stretchr/testify/require"
    18  )
    19  
    20  var fourSystemsResp = `[{
    21  			"displayName": "name1",
    22  			"productDescription": "description",
    23  			"productId": "FSM",
    24  			"ppmsProductVersionId": "123456",
    25  			"baseUrl": "url",
    26  			"infrastructureProvider": "provider1",
    27  			"templateProp": "type1"
    28  		}, 
    29  		{
    30  			"displayName": "name2",
    31  			"productDescription": "description",
    32  			"productId": "FSM",
    33  			"ppmsProductVersionId": "123456",
    34  			"baseUrl": "url",
    35  			"infrastructureProvider": "provider1",
    36  			"templateProp": "type1"
    37  		},
    38  		{
    39  			"displayName": "name3",
    40  			"productDescription": "description",
    41  			"productId": "FSM",
    42  			"ppmsProductVersionId": "123456",
    43  			"baseUrl": "url",
    44  			"infrastructureProvider": "provider1",
    45  			"templateProp": "type1"
    46  		}, {
    47  			"displayName": "name4",
    48  			"productDescription": "description",
    49  			"productId": "FSM",
    50  			"ppmsProductVersionId": "123456",
    51  			"baseUrl": "url",
    52  			"infrastructureProvider": "provider1",
    53  			"templateProp": "type1"
    54  		}]`
    55  
    56  func TestFetchSystemsForTenant(t *testing.T) {
    57  	systemsJSON, err := json.Marshal(fixSystems())
    58  	require.NoError(t, err)
    59  
    60  	mock, url := fixHTTPClient(t)
    61  	mock.bodiesToReturn = [][]byte{systemsJSON}
    62  	mock.expectedFilterCriteria = ""
    63  
    64  	sourceKey := "key"
    65  	labelFilter := "templateProp"
    66  
    67  	tenantID := "tenantId1"
    68  	syncTimestampID := "timestampId1"
    69  
    70  	systemfetcher.SystemSourceKey = sourceKey
    71  	systemfetcher.ApplicationTemplateLabelFilter = labelFilter
    72  
    73  	var mutex sync.Mutex
    74  	client := systemfetcher.NewClient(systemfetcher.APIConfig{
    75  		Endpoint:        url + "/fetch",
    76  		FilterCriteria:  "%s",
    77  		PageSize:        4,
    78  		PagingSkipParam: "$skip",
    79  		PagingSizeParam: "$top",
    80  		SystemSourceKey: sourceKey,
    81  		SystemRPSLimit:  15,
    82  	}, mock.httpClient)
    83  
    84  	t.Run("Success", func(t *testing.T) {
    85  		mock.callNumber = 0
    86  		mock.pageCount = 1
    87  		systems, err := client.FetchSystemsForTenant(context.Background(), "tenant1", &mutex)
    88  		require.NoError(t, err)
    89  		require.Len(t, systems, 1)
    90  		require.Equal(t, systems[0].TemplateID, "")
    91  	})
    92  
    93  	t.Run("Success with template mappings", func(t *testing.T) {
    94  		mock.expectedFilterCriteria = "(key eq 'type1')"
    95  
    96  		systemfetcher.ApplicationTemplates = []systemfetcher.TemplateMapping{
    97  			{
    98  				AppTemplate: &model.ApplicationTemplate{
    99  					ID: "type1",
   100  				},
   101  				Labels: map[string]*model.Label{
   102  					labelFilter: {
   103  						Key:   labelFilter,
   104  						Value: "type1",
   105  					},
   106  				},
   107  			},
   108  		}
   109  		mock.bodiesToReturn = [][]byte{[]byte(`[{
   110  			"displayName": "name1",
   111  			"productDescription": "description",
   112  			"baseUrl": "url",
   113  			"infrastructureProvider": "provider1",
   114  			"key": "type1"
   115  		}, {
   116  			"displayName": "name2",
   117  			"productDescription": "description",
   118  			"baseUrl": "url",
   119  			"infrastructureProvider": "provider1",
   120  			"key": "type2"
   121  		}]`)}
   122  		mock.callNumber = 0
   123  		mock.pageCount = 1
   124  		systems, err := client.FetchSystemsForTenant(context.Background(), "tenant1", &mutex)
   125  		require.NoError(t, err)
   126  		require.Len(t, systems, 2)
   127  		require.Equal(t, systems[0].TemplateID, "type1")
   128  		require.Equal(t, systems[1].TemplateID, "")
   129  	})
   130  
   131  	t.Run("Success with template mappings and SystemSynchronizationTimestamps exist", func(t *testing.T) {
   132  		mock.expectedFilterCriteria = "(key eq 'type1' and lastChangeDateTime gt '2023-05-02 20:30:00 +0000 UTC')"
   133  
   134  		systemfetcher.ApplicationTemplates = []systemfetcher.TemplateMapping{
   135  			{
   136  				AppTemplate: &model.ApplicationTemplate{
   137  					ID: "type1",
   138  				},
   139  				Labels: map[string]*model.Label{
   140  					labelFilter: {
   141  						Key:   labelFilter,
   142  						Value: "type1",
   143  					},
   144  				},
   145  			},
   146  		}
   147  
   148  		systemfetcher.SystemSynchronizationTimestamps = map[string]map[string]systemfetcher.SystemSynchronizationTimestamp{
   149  			tenantID: {
   150  				"type1": {
   151  					ID:                syncTimestampID,
   152  					LastSyncTimestamp: time.Date(2023, 5, 2, 20, 30, 0, 0, time.UTC).UTC(),
   153  				},
   154  			},
   155  		}
   156  
   157  		mock.bodiesToReturn = [][]byte{[]byte(`[{
   158  			"displayName": "name1",
   159  			"productDescription": "description",
   160  			"baseUrl": "url",
   161  			"infrastructureProvider": "provider1",
   162  			"key": "type1"
   163  		}, {
   164  			"displayName": "name2",
   165  			"productDescription": "description",
   166  			"baseUrl": "url",
   167  			"infrastructureProvider": "provider1",
   168  			"key": "type2"
   169  		}]`)}
   170  		mock.callNumber = 0
   171  		mock.pageCount = 1
   172  		systems, err := client.FetchSystemsForTenant(context.Background(), "tenant1", &mutex)
   173  		require.NoError(t, err)
   174  		require.Len(t, systems, 2)
   175  		require.Equal(t, systems[0].TemplateID, "type1")
   176  		require.Equal(t, systems[1].TemplateID, "")
   177  
   178  		systemfetcher.SystemSynchronizationTimestamps = nil
   179  	})
   180  
   181  	t.Run("Success for more than one page", func(t *testing.T) {
   182  		mock.expectedFilterCriteria = "(key eq 'type1')"
   183  
   184  		systemfetcher.ApplicationTemplates = []systemfetcher.TemplateMapping{
   185  			{
   186  				AppTemplate: &model.ApplicationTemplate{
   187  					ID: "type1",
   188  				},
   189  				Labels: map[string]*model.Label{
   190  					labelFilter: {
   191  						Key:   labelFilter,
   192  						Value: "type1",
   193  					},
   194  				},
   195  			},
   196  		}
   197  
   198  		mock.bodiesToReturn = [][]byte{
   199  			[]byte(fourSystemsResp),
   200  			[]byte(`[{
   201  			"displayName": "name5",
   202  			"productDescription": "description",
   203  			"baseUrl": "url",
   204  			"infrastructureProvider": "provider1",
   205  			"key": "type1"
   206  		}]`)}
   207  		mock.callNumber = 0
   208  		mock.pageCount = 2
   209  		systems, err := client.FetchSystemsForTenant(context.Background(), "tenant1", &mutex)
   210  		require.NoError(t, err)
   211  		require.Len(t, systems, 5)
   212  	})
   213  
   214  	t.Run("Does not map to the last template mapping if haven't matched before", func(t *testing.T) {
   215  		mock.expectedFilterCriteria = "(key eq 'type1') or (key eq 'type2') or (key eq 'type3')"
   216  		systemfetcher.ApplicationTemplates = []systemfetcher.TemplateMapping{
   217  			{
   218  				AppTemplate: &model.ApplicationTemplate{
   219  					ID: "type1",
   220  				},
   221  				Labels: map[string]*model.Label{
   222  					labelFilter: {
   223  						Key:   labelFilter,
   224  						Value: "type1",
   225  					},
   226  				},
   227  			},
   228  			{
   229  				AppTemplate: &model.ApplicationTemplate{
   230  					ID: "type2",
   231  				},
   232  				Labels: map[string]*model.Label{
   233  					labelFilter: {
   234  						Key:   labelFilter,
   235  						Value: "type2",
   236  					},
   237  				},
   238  			},
   239  			{
   240  				AppTemplate: &model.ApplicationTemplate{
   241  					ID: "type3",
   242  				},
   243  				Labels: map[string]*model.Label{
   244  					labelFilter: {
   245  						Key:   labelFilter,
   246  						Value: "type3",
   247  					},
   248  				},
   249  			},
   250  		}
   251  
   252  		mock.bodiesToReturn = [][]byte{[]byte(`[{
   253  			"displayName": "name1",
   254  			"productDescription": "description",
   255  			"baseUrl": "url",
   256  			"infrastructureProvider": "provider1",
   257  			"key": "type1"
   258  		}, {
   259  			"displayName": "name2",
   260  			"productDescription": "description",
   261  			"baseUrl": "url",
   262  			"infrastructureProvider": "provider1",
   263  			"key": "type2"
   264  		}, {
   265  			"displayName": "name3",
   266  			"productDescription": "description",
   267  			"baseUrl": "url",
   268  			"infrastructureProvider": "provider1",
   269  			"key": "type4"
   270  		}]`)}
   271  		mock.callNumber = 0
   272  		mock.pageCount = 1
   273  		systems, err := client.FetchSystemsForTenant(context.Background(), "tenant1", &mutex)
   274  		require.NoError(t, err)
   275  		require.Len(t, systems, 3)
   276  		require.Equal(t, systems[0].TemplateID, "type1")
   277  		require.Equal(t, systems[1].TemplateID, "type2")
   278  		require.Equal(t, systems[2].TemplateID, "")
   279  	})
   280  
   281  	t.Run("Fail with unexpected status code", func(t *testing.T) {
   282  		mock.callNumber = 0
   283  		mock.pageCount = 1
   284  		mock.statusCodeToReturn = http.StatusBadRequest
   285  		_, err := client.FetchSystemsForTenant(context.Background(), "tenant1", &mutex)
   286  		require.Contains(t, err.Error(), "unexpected status code")
   287  	})
   288  
   289  	t.Run("Fail because response body is not JSON", func(t *testing.T) {
   290  		mock.callNumber = 0
   291  		mock.pageCount = 1
   292  		mock.bodiesToReturn = [][]byte{[]byte("not a JSON")}
   293  		mock.statusCodeToReturn = http.StatusOK
   294  		_, err := client.FetchSystemsForTenant(context.Background(), "tenant1", &mutex)
   295  		require.Contains(t, err.Error(), "failed to unmarshal systems response")
   296  	})
   297  }
   298  
   299  type mockData struct {
   300  	expectedFilterCriteria string
   301  	statusCodeToReturn     int
   302  	bodiesToReturn         [][]byte
   303  	httpClient             systemfetcher.APIClient
   304  	callNumber             int
   305  	pageCount              int
   306  }
   307  
   308  func fixHTTPClient(t *testing.T) (*mockData, string) {
   309  	mux := http.NewServeMux()
   310  	requests := []string{}
   311  
   312  	mock := mockData{
   313  		callNumber: 1,
   314  	}
   315  	mux.HandleFunc("/fetch", func(w http.ResponseWriter, r *http.Request) {
   316  		filter := r.URL.Query().Get("$filter")
   317  		require.Equal(t, mock.expectedFilterCriteria, filter)
   318  
   319  		requests = append(requests, filter)
   320  		w.Header().Set("Content-Type", "application/json")
   321  		if mock.statusCodeToReturn == 0 {
   322  			mock.statusCodeToReturn = http.StatusOK
   323  		}
   324  		w.WriteHeader(mock.statusCodeToReturn)
   325  
   326  		if mock.statusCodeToReturn == http.StatusOK {
   327  			index := mock.callNumber % mock.pageCount //this way each of the body to return mocks will be returned once for both filter criteria
   328  			_, err := w.Write(mock.bodiesToReturn[index])
   329  			require.NoError(t, err)
   330  		} else {
   331  			_, err := w.Write([]byte{})
   332  			require.NoError(t, err)
   333  		}
   334  		mock.callNumber++
   335  	})
   336  
   337  	ts := httptest.NewServer(mux)
   338  	mock.httpClient = systemfetcher.NewOauthClient(oauth.Config{}, ts.Client())
   339  
   340  	return &mock, ts.URL
   341  }
   342  
   343  func fixSystems() []systemfetcher.System {
   344  	return []systemfetcher.System{
   345  		{
   346  			SystemPayload: map[string]interface{}{
   347  				"displayName":            "System1",
   348  				"productDescription":     "System1 description",
   349  				"baseUrl":                "http://example1.com",
   350  				"infrastructureProvider": "test",
   351  				"additionalUrls":         map[string]string{"mainUrl": "http://mainurl.com"},
   352  			},
   353  			StatusCondition: model.ApplicationStatusConditionInitial,
   354  		},
   355  	}
   356  }
   357  
   358  func fixSystemsWithTbt() []systemfetcher.System {
   359  	return []systemfetcher.System{
   360  		{
   361  			SystemPayload: map[string]interface{}{
   362  				"displayName":             "System2",
   363  				"productDescription":      "System2 description",
   364  				"baseUrl":                 "http://example2.com",
   365  				"infrastructureProvider":  "test",
   366  				"additionalUrls":          map[string]string{"mainUrl": "http://mainurl.com"},
   367  				"businessTypeId":          "Test business type id",
   368  				"businessTypeDescription": "Test business description",
   369  			},
   370  			StatusCondition: model.ApplicationStatusConditionInitial,
   371  		},
   372  	}
   373  }