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  }