github.com/craicoverflow/tyk@v2.9.6-rc3+incompatible/coprocess/grpc/coprocess_grpc_test.go (about)

     1  package grpc
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"io/ioutil"
     7  	"math/rand"
     8  	"mime/multipart"
     9  	"net"
    10  	"net/http"
    11  	"os"
    12  	"strings"
    13  	"sync"
    14  	"testing"
    15  
    16  	"context"
    17  
    18  	"google.golang.org/grpc"
    19  
    20  	"github.com/TykTechnologies/tyk/apidef"
    21  	"github.com/TykTechnologies/tyk/config"
    22  	"github.com/TykTechnologies/tyk/coprocess"
    23  	"github.com/TykTechnologies/tyk/gateway"
    24  	"github.com/TykTechnologies/tyk/test"
    25  	"github.com/TykTechnologies/tyk/user"
    26  )
    27  
    28  const (
    29  	grpcListenAddr  = ":9999"
    30  	grpcListenPath  = "tcp://127.0.0.1:9999"
    31  	grpcTestMaxSize = 100000000
    32  
    33  	testHeaderName  = "Testheader"
    34  	testHeaderValue = "testvalue"
    35  )
    36  
    37  type dispatcher struct{}
    38  
    39  func (d *dispatcher) grpcError(object *coprocess.Object, errorMsg string) (*coprocess.Object, error) {
    40  	object.Request.ReturnOverrides.ResponseError = errorMsg
    41  	object.Request.ReturnOverrides.ResponseCode = 400
    42  	return object, nil
    43  }
    44  
    45  func (d *dispatcher) Dispatch(ctx context.Context, object *coprocess.Object) (*coprocess.Object, error) {
    46  	switch object.HookName {
    47  	case "testPreHook1":
    48  		object.Request.SetHeaders = map[string]string{
    49  			testHeaderName: testHeaderValue,
    50  		}
    51  	case "testPreHook2":
    52  		contentType, found := object.Request.Headers["Content-Type"]
    53  		if !found {
    54  			return d.grpcError(object, "Content Type field not found")
    55  		}
    56  		if strings.Contains(contentType, "json") {
    57  			if len(object.Request.Body) == 0 {
    58  				return d.grpcError(object, "Body field is empty")
    59  			}
    60  			if len(object.Request.RawBody) == 0 {
    61  				return d.grpcError(object, "Raw body field is empty")
    62  			}
    63  			if strings.Compare(object.Request.Body, string(object.Request.Body)) != 0 {
    64  				return d.grpcError(object, "Raw body and body fields don't match")
    65  			}
    66  		} else if strings.Contains(contentType, "multipart") {
    67  			if len(object.Request.Body) != 0 {
    68  				return d.grpcError(object, "Body field isn't empty")
    69  			}
    70  			if len(object.Request.RawBody) == 0 {
    71  				return d.grpcError(object, "Raw body field is empty")
    72  			}
    73  		} else {
    74  			return d.grpcError(object, "Request content type should be either JSON or multipart")
    75  		}
    76  	case "testPostHook1":
    77  		testKeyValue, ok := object.Session.GetMetadata()["testkey"]
    78  		if !ok {
    79  			return d.grpcError(object, "'testkey' not found in session metadata")
    80  		}
    81  		jsonObject := make(map[string]string)
    82  		if err := json.Unmarshal([]byte(testKeyValue), &jsonObject); err != nil {
    83  			return d.grpcError(object, "couldn't decode 'testkey' nested value")
    84  		}
    85  		nestedKeyValue, ok := jsonObject["nestedkey"]
    86  		if !ok {
    87  			return d.grpcError(object, "'nestedkey' not found in JSON object")
    88  		}
    89  		if nestedKeyValue != "nestedvalue" {
    90  			return d.grpcError(object, "'nestedvalue' value doesn't match")
    91  		}
    92  		testKey2Value, ok := object.Session.GetMetadata()["testkey2"]
    93  		if !ok {
    94  			return d.grpcError(object, "'testkey' not found in session metadata")
    95  		}
    96  		if testKey2Value != "testvalue" {
    97  			return d.grpcError(object, "'testkey2' value doesn't match")
    98  		}
    99  
   100  		// Check for compatibility (object.Metadata should contain the same keys as object.Session.GetMetadata())
   101  		for k, v := range object.Metadata {
   102  			sessionKeyValue, ok := object.Session.GetMetadata()[k]
   103  			if !ok {
   104  				return d.grpcError(object, k+" not found in object.Session.Metadata")
   105  			}
   106  			if strings.Compare(sessionKeyValue, v) != 0 {
   107  				return d.grpcError(object, k+" doesn't match value in object.Session.Metadata")
   108  			}
   109  		}
   110  	}
   111  	return object, nil
   112  }
   113  
   114  func (d *dispatcher) DispatchEvent(ctx context.Context, event *coprocess.Event) (*coprocess.EventReply, error) {
   115  	return &coprocess.EventReply{}, nil
   116  }
   117  
   118  func newTestGRPCServer() (s *grpc.Server) {
   119  	s = grpc.NewServer(
   120  		grpc.MaxRecvMsgSize(grpcTestMaxSize),
   121  		grpc.MaxSendMsgSize(grpcTestMaxSize),
   122  	)
   123  	coprocess.RegisterDispatcherServer(s, &dispatcher{})
   124  	return s
   125  }
   126  
   127  func loadTestGRPCAPIs() {
   128  	gateway.BuildAndLoadAPI(func(spec *gateway.APISpec) {
   129  		spec.APIID = "1"
   130  		spec.OrgID = gateway.MockOrgID
   131  		spec.Auth = apidef.AuthConfig{
   132  			AuthHeaderName: "authorization",
   133  		}
   134  		spec.UseKeylessAccess = false
   135  		spec.VersionData = struct {
   136  			NotVersioned   bool                          `bson:"not_versioned" json:"not_versioned"`
   137  			DefaultVersion string                        `bson:"default_version" json:"default_version"`
   138  			Versions       map[string]apidef.VersionInfo `bson:"versions" json:"versions"`
   139  		}{
   140  			NotVersioned: true,
   141  			Versions: map[string]apidef.VersionInfo{
   142  				"v1": {
   143  					Name: "v1",
   144  				},
   145  			},
   146  		}
   147  		spec.Proxy.ListenPath = "/grpc-test-api/"
   148  		spec.Proxy.StripListenPath = true
   149  		spec.CustomMiddleware = apidef.MiddlewareSection{
   150  			Pre: []apidef.MiddlewareDefinition{
   151  				{Name: "testPreHook1"},
   152  			},
   153  			Driver: apidef.GrpcDriver,
   154  		}
   155  	}, func(spec *gateway.APISpec) {
   156  		spec.APIID = "2"
   157  		spec.OrgID = gateway.MockOrgID
   158  		spec.Auth = apidef.AuthConfig{
   159  			AuthHeaderName: "authorization",
   160  		}
   161  		spec.UseKeylessAccess = true
   162  		spec.VersionData = struct {
   163  			NotVersioned   bool                          `bson:"not_versioned" json:"not_versioned"`
   164  			DefaultVersion string                        `bson:"default_version" json:"default_version"`
   165  			Versions       map[string]apidef.VersionInfo `bson:"versions" json:"versions"`
   166  		}{
   167  			NotVersioned: true,
   168  			Versions: map[string]apidef.VersionInfo{
   169  				"v1": {
   170  					Name: "v1",
   171  				},
   172  			},
   173  		}
   174  		spec.Proxy.ListenPath = "/grpc-test-api-2/"
   175  		spec.Proxy.StripListenPath = true
   176  		spec.CustomMiddleware = apidef.MiddlewareSection{
   177  			Pre: []apidef.MiddlewareDefinition{
   178  				{Name: "testPreHook2"},
   179  			},
   180  			Driver: apidef.GrpcDriver,
   181  		}
   182  	}, func(spec *gateway.APISpec) {
   183  		spec.APIID = "3"
   184  		spec.OrgID = "default"
   185  		spec.Auth = apidef.AuthConfig{
   186  			AuthHeaderName: "authorization",
   187  		}
   188  		spec.UseKeylessAccess = false
   189  		spec.VersionData = struct {
   190  			NotVersioned   bool                          `bson:"not_versioned" json:"not_versioned"`
   191  			DefaultVersion string                        `bson:"default_version" json:"default_version"`
   192  			Versions       map[string]apidef.VersionInfo `bson:"versions" json:"versions"`
   193  		}{
   194  			NotVersioned: true,
   195  			Versions: map[string]apidef.VersionInfo{
   196  				"v1": {
   197  					Name: "v1",
   198  				},
   199  			},
   200  		}
   201  		spec.Proxy.ListenPath = "/grpc-test-api-3/"
   202  		spec.Proxy.StripListenPath = true
   203  		spec.CustomMiddleware = apidef.MiddlewareSection{
   204  			Post: []apidef.MiddlewareDefinition{
   205  				{Name: "testPostHook1"},
   206  			},
   207  			Driver: apidef.GrpcDriver,
   208  		}
   209  	})
   210  }
   211  
   212  func startTykWithGRPC() (*gateway.Test, *grpc.Server) {
   213  	// Setup the gRPC server:
   214  	listener, _ := net.Listen("tcp", grpcListenAddr)
   215  	grpcServer := newTestGRPCServer()
   216  	go grpcServer.Serve(listener)
   217  
   218  	// Setup Tyk:
   219  	cfg := config.CoProcessConfig{
   220  		EnableCoProcess:     true,
   221  		CoProcessGRPCServer: grpcListenPath,
   222  		GRPCRecvMaxSize:     grpcTestMaxSize,
   223  		GRPCSendMaxSize:     grpcTestMaxSize,
   224  	}
   225  	ts := gateway.StartTest(gateway.TestConfig{CoprocessConfig: cfg})
   226  
   227  	// Load test APIs:
   228  	loadTestGRPCAPIs()
   229  	return ts, grpcServer
   230  }
   231  
   232  func TestMain(m *testing.M) {
   233  	os.Exit(gateway.InitTestMain(context.Background(), m))
   234  }
   235  
   236  func TestGRPCDispatch(t *testing.T) {
   237  	ts, grpcServer := startTykWithGRPC()
   238  	defer ts.Close()
   239  	defer grpcServer.Stop()
   240  
   241  	keyID := gateway.CreateSession(func(s *user.SessionState) {
   242  		s.MetaData = map[string]interface{}{
   243  			"testkey":  map[string]interface{}{"nestedkey": "nestedvalue"},
   244  			"testkey2": "testvalue",
   245  		}
   246  		s.Mutex = &sync.RWMutex{}
   247  	})
   248  	headers := map[string]string{"authorization": keyID}
   249  
   250  	t.Run("Pre Hook with SetHeaders", func(t *testing.T) {
   251  		res, err := ts.Run(t, test.TestCase{
   252  			Path:    "/grpc-test-api/",
   253  			Method:  http.MethodGet,
   254  			Code:    http.StatusOK,
   255  			Headers: headers,
   256  		})
   257  		if err != nil {
   258  			t.Fatalf("Request failed: %s", err.Error())
   259  		}
   260  		data, err := ioutil.ReadAll(res.Body)
   261  		if err != nil {
   262  			t.Fatalf("Couldn't read response body: %s", err.Error())
   263  		}
   264  		var testResponse gateway.TestHttpResponse
   265  		err = json.Unmarshal(data, &testResponse)
   266  		if err != nil {
   267  			t.Fatalf("Couldn't unmarshal test response JSON: %s", err.Error())
   268  		}
   269  		value, ok := testResponse.Headers[testHeaderName]
   270  		if !ok {
   271  			t.Fatalf("Header not found, expecting %s", testHeaderName)
   272  		}
   273  		if value != testHeaderValue {
   274  			t.Fatalf("Header value isn't %s", testHeaderValue)
   275  		}
   276  	})
   277  
   278  	t.Run("Pre Hook with UTF-8/non-UTF-8 request data", func(t *testing.T) {
   279  		fileData := gateway.GenerateTestBinaryData()
   280  		var buf bytes.Buffer
   281  		multipartWriter := multipart.NewWriter(&buf)
   282  		file, err := multipartWriter.CreateFormFile("file", "test.bin")
   283  		if err != nil {
   284  			t.Fatalf("Couldn't use multipart writer: %s", err.Error())
   285  		}
   286  		_, err = fileData.WriteTo(file)
   287  		if err != nil {
   288  			t.Fatalf("Couldn't write to multipart file: %s", err.Error())
   289  		}
   290  		field, err := multipartWriter.CreateFormField("testfield")
   291  		if err != nil {
   292  			t.Fatalf("Couldn't use multipart writer: %s", err.Error())
   293  		}
   294  		_, err = field.Write([]byte("testvalue"))
   295  		if err != nil {
   296  			t.Fatalf("Couldn't write to form field: %s", err.Error())
   297  		}
   298  		err = multipartWriter.Close()
   299  		if err != nil {
   300  			t.Fatalf("Couldn't close multipart writer: %s", err.Error())
   301  		}
   302  
   303  		ts.Run(t, []test.TestCase{
   304  			{Path: "/grpc-test-api-2/", Code: 200, Data: &buf, Headers: map[string]string{"Content-Type": multipartWriter.FormDataContentType()}},
   305  			{Path: "/grpc-test-api-2/", Code: 200, Data: "{}", Headers: map[string]string{"Content-Type": "application/json"}},
   306  		}...)
   307  	})
   308  
   309  	t.Run("Post Hook with metadata", func(t *testing.T) {
   310  		ts.Run(t, test.TestCase{
   311  			Path:    "/grpc-test-api-3/",
   312  			Method:  http.MethodGet,
   313  			Code:    http.StatusOK,
   314  			Headers: headers,
   315  		})
   316  	})
   317  
   318  	t.Run("Post Hook with allowed message length", func(t *testing.T) {
   319  		s := randStringBytes(20000000)
   320  		ts.Run(t, test.TestCase{
   321  			Path:    "/grpc-test-api-3/",
   322  			Method:  http.MethodGet,
   323  			Code:    http.StatusOK,
   324  			Headers: headers,
   325  			Data:    s,
   326  		})
   327  	})
   328  
   329  	t.Run("Post Hook with with unallowed message length", func(t *testing.T) {
   330  		s := randStringBytes(150000000)
   331  		ts.Run(t, test.TestCase{
   332  			Path:    "/grpc-test-api-3/",
   333  			Method:  http.MethodGet,
   334  			Code:    http.StatusInternalServerError,
   335  			Headers: headers,
   336  			Data:    s,
   337  		})
   338  	})
   339  }
   340  
   341  func BenchmarkGRPCDispatch(b *testing.B) {
   342  	ts, grpcServer := startTykWithGRPC()
   343  	defer ts.Close()
   344  	defer grpcServer.Stop()
   345  
   346  	keyID := gateway.CreateSession(func(s *user.SessionState) {
   347  		s.Mutex = &sync.RWMutex{}
   348  	})
   349  	headers := map[string]string{"authorization": keyID}
   350  
   351  	b.Run("Pre Hook with SetHeaders", func(b *testing.B) {
   352  		path := "/grpc-test-api/"
   353  		b.ReportAllocs()
   354  		for i := 0; i < b.N; i++ {
   355  			ts.Run(b, test.TestCase{
   356  				Path:    path,
   357  				Method:  http.MethodGet,
   358  				Code:    http.StatusOK,
   359  				Headers: headers,
   360  			})
   361  		}
   362  	})
   363  }
   364  
   365  const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
   366  
   367  func randStringBytes(n int) string {
   368  	b := make([]byte, n)
   369  
   370  	for i := range b {
   371  		b[i] = letters[rand.Intn(len(letters))]
   372  	}
   373  
   374  	return string(b)
   375  }