github.com/kyma-incubator/compass/components/director@v0.0.0-20230623144113-d764f56ff805/internal/destinationfetchersvc/client_test.go (about) 1 package destinationfetchersvc_test 2 3 import ( 4 "context" 5 "crypto/rand" 6 "crypto/rsa" 7 "crypto/tls" 8 "crypto/x509" 9 "crypto/x509/pkix" 10 "encoding/json" 11 "encoding/pem" 12 "fmt" 13 "io" 14 "math/big" 15 "net" 16 "net/http" 17 "net/http/httptest" 18 "strings" 19 "testing" 20 "time" 21 22 "github.com/kyma-incubator/compass/components/director/internal/destinationfetchersvc" 23 "github.com/kyma-incubator/compass/components/director/pkg/apperrors" 24 "github.com/kyma-incubator/compass/components/director/pkg/config" 25 "github.com/kyma-incubator/compass/components/director/pkg/oauth" 26 "github.com/kyma-incubator/compass/components/director/pkg/resource" 27 "github.com/stretchr/testify/assert" 28 "github.com/stretchr/testify/require" 29 ) 30 31 const ( 32 sensitiveEndpoint = "/destination-configuration/v1/destinations" 33 syncEndpoint = "/destination-configuration/v1/syncDestinations" 34 subdomain = "test" 35 tokenPath = "/test" 36 noPageCountHeader = "noPageCount" 37 ) 38 39 func TestClient_TenantEndpoint(t *testing.T) { 40 // GIVEN 41 ctx := context.Background() 42 mockServer := mockServerWithSyncEndpoint(t) 43 defer mockServer.Close() 44 45 apiConfig := destinationfetchersvc.DestinationServiceAPIConfig{ 46 GoroutineLimit: 10, 47 RetryInterval: 100 * time.Millisecond, 48 RetryAttempts: 3, 49 EndpointGetTenantDestinations: mockServer.URL + syncEndpoint, 50 EndpointFindDestination: "", 51 Timeout: 100 * time.Millisecond, 52 PageSize: 100, 53 PagingPageParam: "$page", 54 PagingSizeParam: "$pageSize", 55 PagingCountParam: "$pageCount", 56 PagingCountHeader: "Page-Count", 57 OAuthTokenPath: tokenPath, 58 } 59 60 cert, key := generateTestCertAndKey(t, "test") 61 instanceCfg := config.InstanceConfig{ 62 TokenURL: "https://subdomain.tokenurl", 63 } 64 instanceCfg.Cert = string(cert) 65 instanceCfg.Key = string(key) 66 client, err := destinationfetchersvc.NewClient(instanceCfg, apiConfig, subdomain) 67 require.NoError(t, err) 68 69 defer client.HTTPClient.CloseIdleConnections() 70 setHTTPClientMockHost(client.HTTPClient, mockServer.URL) 71 72 t.Run("Success fetching data page 3", func(t *testing.T) { 73 // WHEN 74 res, err := client.FetchTenantDestinationsPage(ctx, tenantID, "3") 75 // THEN 76 require.NoError(t, err) 77 assert.NotEmpty(t, res) 78 }) 79 80 t.Run("Success fetching data page but no Page-Count header is in response", func(t *testing.T) { 81 // WHEN 82 _, err := client.FetchTenantDestinationsPage(ctx, tenantID, noPageCountHeader) 83 // THEN 84 require.Error(t, err) 85 require.Contains(t, err.Error(), fmt.Sprintf("missing '%s' header from destinations response", 86 apiConfig.PagingCountHeader)) 87 }) 88 89 t.Run("Fetch should fail with status code 500, but do three attempts", func(t *testing.T) { 90 // WHEN 91 _, err := client.FetchTenantDestinationsPage(ctx, tenantID, "internalServerError") 92 // THEN 93 require.Error(t, err) 94 require.Contains(t, err.Error(), "#3") 95 require.Contains(t, err.Error(), "status code 500") 96 }) 97 98 t.Run("Fetch should fail with status code 4xx", func(t *testing.T) { 99 // WHEN 100 _, err := client.FetchTenantDestinationsPage(ctx, tenantID, "forbidden") 101 // THEN 102 require.Error(t, err) 103 require.Contains(t, err.Error(), "status code 403") 104 }) 105 } 106 107 func TestClient_SensitiveDataEndpoint(t *testing.T) { 108 // GIVEN 109 ctx := context.Background() 110 mockServer := mockServerWithDestinationEndpoint(t) 111 defer mockServer.Close() 112 113 apiConfig := destinationfetchersvc.DestinationServiceAPIConfig{} 114 apiConfig.EndpointFindDestination = mockServer.URL + sensitiveEndpoint 115 apiConfig.EndpointGetTenantDestinations = mockServer.URL + syncEndpoint 116 apiConfig.RetryAttempts = 3 117 apiConfig.RetryInterval = 100 * time.Millisecond 118 apiConfig.OAuthTokenPath = tokenPath 119 120 instanceCfg := config.InstanceConfig{ 121 TokenURL: "https://domain.tokenurl", 122 } 123 cert, key := generateTestCertAndKey(t, "test") 124 instanceCfg.Cert = string(cert) 125 instanceCfg.Key = string(key) 126 client, err := destinationfetchersvc.NewClient(instanceCfg, apiConfig, subdomain) 127 require.NoError(t, err) 128 defer client.HTTPClient.CloseIdleConnections() 129 setHTTPClientMockHost(client.HTTPClient, mockServer.URL) 130 131 t.Run("Success fetching sensitive data", func(t *testing.T) { 132 // WHEN 133 res, err := client.FetchDestinationSensitiveData(ctx, "s4ext") 134 // THEN 135 require.NoError(t, err) 136 assert.NotEmpty(t, res) 137 }) 138 139 t.Run("Fetch should fail with status code 500, but do three attempts", func(t *testing.T) { 140 // WHEN 141 _, err := client.FetchDestinationSensitiveData(ctx, "internalServerError") 142 // THEN 143 require.Error(t, err) 144 require.Contains(t, err.Error(), "#3") 145 require.Contains(t, err.Error(), "status code 500") 146 }) 147 148 t.Run("NewNotFoundError should be returned for status 404", func(t *testing.T) { 149 // WHEN 150 _, err := client.FetchDestinationSensitiveData(ctx, "notFound") 151 // THEN 152 require.ErrorIs(t, err, apperrors.NewNotFoundError(resource.Destination, "notFound")) 153 }) 154 155 t.Run("Error should be returned for status 400", func(t *testing.T) { 156 // WHEN 157 _, err := client.FetchDestinationSensitiveData(ctx, "badRequest") 158 // THEN 159 require.Error(t, err) 160 require.Contains(t, err.Error(), "400") 161 }) 162 } 163 164 func setHTTPClientMockHost(client *http.Client, testServerURL string) { 165 client.Transport = &http.Transport{ 166 DialContext: func(_ context.Context, network, address string) (net.Conn, error) { 167 return net.Dial(network, strings.TrimPrefix(testServerURL, "https://")) 168 }, 169 TLSClientConfig: &tls.Config{ 170 InsecureSkipVerify: true, 171 }, 172 } 173 } 174 175 func mockServerWithSyncEndpoint(t *testing.T) *httptest.Server { 176 mux := http.NewServeMux() 177 178 mux.HandleFunc(syncEndpoint, func(w http.ResponseWriter, r *http.Request) { 179 pageCount := r.URL.Query().Get("$pageCount") 180 page := r.URL.Query().Get("$page") 181 pageSize := r.URL.Query().Get("$pageSize") 182 183 if page == "forbidden" { 184 http.Error(w, "forbidden", http.StatusForbidden) 185 return 186 } 187 188 if page != "3" && page != noPageCountHeader { 189 http.Error(w, "page number invalid", http.StatusInternalServerError) 190 return 191 } 192 193 if pageSize != "100" { 194 http.Error(w, "pageSize invalid", http.StatusInternalServerError) 195 return 196 } 197 198 if pageCount != "true" { 199 http.Error(w, "pageCount invalid", http.StatusInternalServerError) 200 return 201 } 202 203 w.Header().Set("Content-Type", "application/json") 204 if page != noPageCountHeader { 205 w.Header().Set("Page-Count", "3") 206 } 207 208 w.WriteHeader(http.StatusOK) 209 _, err := io.WriteString(w, fixTenantDestinationsEndpoint()) 210 require.NoError(t, err) 211 }) 212 213 mux.HandleFunc(tokenPath, tokenEndpointHandler) 214 return httptest.NewTLSServer(mux) 215 } 216 217 func tokenEndpointHandler(w http.ResponseWriter, r *http.Request) { 218 w.Header().Set("Content-Type", "application/json") 219 if _, err := w.Write(newToken()); err != nil { 220 panic(err) 221 } 222 } 223 224 func newToken() []byte { 225 data := map[string]interface{}{} 226 data["access_token"] = "MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3" 227 data["token_type"] = "Bearer" 228 data["expires_in"] = 3600 229 token, err := json.Marshal(data) 230 if err != nil { 231 panic(err) 232 } 233 return token 234 } 235 236 func mockServerWithDestinationEndpoint(t *testing.T) *httptest.Server { 237 mux := http.NewServeMux() 238 239 mux.HandleFunc(sensitiveEndpoint+"/s4ext", func(w http.ResponseWriter, r *http.Request) { 240 w.Header().Set("Content-Type", "application/json") 241 w.WriteHeader(http.StatusOK) 242 _, err := io.WriteString(w, fixSensitiveDataJSON()) 243 require.NoError(t, err) 244 }) 245 mux.HandleFunc(sensitiveEndpoint+"/internalServerError", func(w http.ResponseWriter, r *http.Request) { 246 w.WriteHeader(http.StatusInternalServerError) 247 }) 248 249 mux.HandleFunc(sensitiveEndpoint+"/badRequest", func(w http.ResponseWriter, r *http.Request) { 250 w.WriteHeader(http.StatusBadRequest) 251 }) 252 253 mux.HandleFunc(tokenPath, tokenEndpointHandler) 254 255 return httptest.NewTLSServer(mux) 256 } 257 258 func TestNewClient(t *testing.T) { 259 const clientID = "client" 260 const clientSecret = "secret" 261 262 cert, key := generateTestCertAndKey(t, "test") 263 264 t.Run("mtls+client-secret mode", func(t *testing.T) { 265 instanceCfg := config.InstanceConfig{ 266 ClientID: clientID, 267 ClientSecret: clientSecret, 268 URL: "url", 269 TokenURL: "https://subdomain.tokenurl", 270 Cert: string(cert), 271 Key: string(key), 272 } 273 274 client, err := destinationfetchersvc.NewClient(instanceCfg, 275 destinationfetchersvc.DestinationServiceAPIConfig{OAuthTokenPath: "/oauth/token"}, "subdomain") 276 require.NoError(t, err) 277 278 certCfg := oauth.X509Config{ 279 Cert: string(cert), 280 Key: string(key), 281 } 282 283 tlsCert, err := certCfg.ParseCertificate() 284 require.NoError(t, err) 285 286 expectedTransport := &http.Transport{ 287 TLSClientConfig: &tls.Config{ 288 Certificates: []tls.Certificate{*tlsCert}, 289 }, 290 } 291 require.Equal(t, client.HTTPClient.Transport, expectedTransport) 292 }) 293 294 t.Run("token url with no subdomain", func(t *testing.T) { 295 instanceCfg := config.InstanceConfig{ 296 TokenURL: "https://nosubdomaintokenurl", 297 } 298 _, err := destinationfetchersvc.NewClient(instanceCfg, 299 destinationfetchersvc.DestinationServiceAPIConfig{OAuthTokenPath: "/oauth/token"}, "subdomain") 300 require.Error(t, err, fmt.Sprintf("auth url '%s' should have a subdomain", instanceCfg.TokenURL)) 301 }) 302 303 t.Run("invalid token url", func(t *testing.T) { 304 instanceCfg := config.InstanceConfig{ 305 TokenURL: ":invalid", 306 } 307 _, err := destinationfetchersvc.NewClient(instanceCfg, 308 destinationfetchersvc.DestinationServiceAPIConfig{OAuthTokenPath: "/oauth/token"}, "subdomain") 309 require.Error(t, err) 310 require.Contains(t, err.Error(), "failed to parse auth url") 311 }) 312 } 313 314 func fixSensitiveDataJSON() string { 315 return `{ 316 "s4ext": { 317 "owner": { 318 "SubaccountId": "8fb6ac72-124e-11ed-861d-0242ac120002", 319 "InstanceId": null 320 }, 321 "destinationConfiguration": { 322 "Name": "s4ext", 323 "Type": "HTTP", 324 "URL": "https://kaladin.bg", 325 "Authentication": "BasicAuthentication", 326 "ProxyType": "Internet", 327 "XFSystemName": "Rock", 328 "HTML5.DynamicDestination": "true", 329 "User": "Kaladin", 330 "product.name": "SAP S/4HANA Cloud", 331 "Password": "securePass", 332 }, 333 "authTokens": [ 334 { 335 "type": "Basic", 336 "value": "blJhbHQ1==", 337 "http_header": { 338 "key": "Authorization", 339 "value": "Basic blJhbHQ1==" 340 } 341 } 342 ] 343 } 344 }` 345 } 346 347 func fixTenantDestinationsEndpoint() string { 348 return ` 349 [ 350 { 351 "Name": "string", 352 "Type": "HTTP", 353 "PropertyName": "string" 354 }, 355 { 356 "Name": "string", 357 "Type": "HTTP", 358 "PropertyName": "string" 359 } 360 ]` 361 } 362 363 func generateTestCertAndKey(t *testing.T, commonName string) (crtPem, keyPem []byte) { 364 clientKey, err := rsa.GenerateKey(rand.Reader, 2048) 365 require.NoError(t, err) 366 367 template := &x509.Certificate{ 368 IsCA: true, 369 SerialNumber: big.NewInt(1234), 370 Subject: pkix.Name{ 371 CommonName: commonName, 372 }, 373 NotBefore: time.Now(), 374 NotAfter: time.Now().Add(time.Hour), 375 KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, 376 ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, 377 } 378 379 parent := template 380 certRaw, err := x509.CreateCertificate(rand.Reader, template, parent, &clientKey.PublicKey, clientKey) 381 require.NoError(t, err) 382 383 crtPem = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certRaw}) 384 keyPem = pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(clientKey)}) 385 386 return 387 }