github.com/dbernstein1/tyk@v2.9.0-beta9-dl-apic+incompatible/coprocess/grpc/coprocess_grpc_test.go (about)

     1  // +build coprocess
     2  // +build grpc
     3  
     4  package grpc
     5  
     6  import (
     7  	"bytes"
     8  	"encoding/json"
     9  	"io/ioutil"
    10  	"mime/multipart"
    11  	"net"
    12  	"net/http"
    13  	"os"
    14  	"strings"
    15  	"testing"
    16  
    17  	"context"
    18  
    19  	"google.golang.org/grpc"
    20  
    21  	"github.com/TykTechnologies/tyk/apidef"
    22  	"github.com/TykTechnologies/tyk/config"
    23  	"github.com/TykTechnologies/tyk/coprocess"
    24  	"github.com/TykTechnologies/tyk/gateway"
    25  	"github.com/TykTechnologies/tyk/test"
    26  	"github.com/TykTechnologies/tyk/user"
    27  )
    28  
    29  const (
    30  	grpcListenAddr = ":9999"
    31  	grpcListenPath = "tcp://127.0.0.1:9999"
    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.Metadata["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.Metadata["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.Metadata)
   101  		for k, v := range object.Metadata {
   102  			sessionKeyValue, ok := object.Session.Metadata[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  	coprocess.RegisterDispatcherServer(s, &dispatcher{})
   121  	return s
   122  }
   123  
   124  func loadTestGRPCAPIs() {
   125  	gateway.BuildAndLoadAPI(func(spec *gateway.APISpec) {
   126  		spec.APIID = "1"
   127  		spec.OrgID = gateway.MockOrgID
   128  		spec.Auth = apidef.Auth{
   129  			AuthHeaderName: "authorization",
   130  		}
   131  		spec.UseKeylessAccess = false
   132  		spec.VersionData = struct {
   133  			NotVersioned   bool                          `bson:"not_versioned" json:"not_versioned"`
   134  			DefaultVersion string                        `bson:"default_version" json:"default_version"`
   135  			Versions       map[string]apidef.VersionInfo `bson:"versions" json:"versions"`
   136  		}{
   137  			NotVersioned: true,
   138  			Versions: map[string]apidef.VersionInfo{
   139  				"v1": {
   140  					Name: "v1",
   141  				},
   142  			},
   143  		}
   144  		spec.Proxy.ListenPath = "/grpc-test-api/"
   145  		spec.Proxy.StripListenPath = true
   146  		spec.CustomMiddleware = apidef.MiddlewareSection{
   147  			Pre: []apidef.MiddlewareDefinition{
   148  				{Name: "testPreHook1"},
   149  			},
   150  			Driver: apidef.GrpcDriver,
   151  		}
   152  	}, func(spec *gateway.APISpec) {
   153  		spec.APIID = "2"
   154  		spec.OrgID = gateway.MockOrgID
   155  		spec.Auth = apidef.Auth{
   156  			AuthHeaderName: "authorization",
   157  		}
   158  		spec.UseKeylessAccess = true
   159  		spec.VersionData = struct {
   160  			NotVersioned   bool                          `bson:"not_versioned" json:"not_versioned"`
   161  			DefaultVersion string                        `bson:"default_version" json:"default_version"`
   162  			Versions       map[string]apidef.VersionInfo `bson:"versions" json:"versions"`
   163  		}{
   164  			NotVersioned: true,
   165  			Versions: map[string]apidef.VersionInfo{
   166  				"v1": {
   167  					Name: "v1",
   168  				},
   169  			},
   170  		}
   171  		spec.Proxy.ListenPath = "/grpc-test-api-2/"
   172  		spec.Proxy.StripListenPath = true
   173  		spec.CustomMiddleware = apidef.MiddlewareSection{
   174  			Pre: []apidef.MiddlewareDefinition{
   175  				{Name: "testPreHook2"},
   176  			},
   177  			Driver: apidef.GrpcDriver,
   178  		}
   179  	}, func(spec *gateway.APISpec) {
   180  		spec.APIID = "3"
   181  		spec.OrgID = "default"
   182  		spec.Auth = apidef.Auth{
   183  			AuthHeaderName: "authorization",
   184  		}
   185  		spec.UseKeylessAccess = false
   186  		spec.VersionData = struct {
   187  			NotVersioned   bool                          `bson:"not_versioned" json:"not_versioned"`
   188  			DefaultVersion string                        `bson:"default_version" json:"default_version"`
   189  			Versions       map[string]apidef.VersionInfo `bson:"versions" json:"versions"`
   190  		}{
   191  			NotVersioned: true,
   192  			Versions: map[string]apidef.VersionInfo{
   193  				"v1": {
   194  					Name: "v1",
   195  				},
   196  			},
   197  		}
   198  		spec.Proxy.ListenPath = "/grpc-test-api-3/"
   199  		spec.Proxy.StripListenPath = true
   200  		spec.CustomMiddleware = apidef.MiddlewareSection{
   201  			Post: []apidef.MiddlewareDefinition{
   202  				{Name: "testPostHook1"},
   203  			},
   204  			Driver: apidef.GrpcDriver,
   205  		}
   206  	})
   207  }
   208  
   209  func startTykWithGRPC() (*gateway.Test, *grpc.Server) {
   210  	// Setup the gRPC server:
   211  	listener, _ := net.Listen("tcp", grpcListenAddr)
   212  	grpcServer := newTestGRPCServer()
   213  	go grpcServer.Serve(listener)
   214  
   215  	// Setup Tyk:
   216  	cfg := config.CoProcessConfig{
   217  		EnableCoProcess:     true,
   218  		CoProcessGRPCServer: grpcListenPath,
   219  	}
   220  	ts := gateway.StartTest(gateway.TestConfig{CoprocessConfig: cfg})
   221  
   222  	// Load test APIs:
   223  	loadTestGRPCAPIs()
   224  	return &ts, grpcServer
   225  }
   226  
   227  func TestMain(m *testing.M) {
   228  	os.Exit(gateway.InitTestMain(m))
   229  }
   230  
   231  func TestGRPCDispatch(t *testing.T) {
   232  	ts, grpcServer := startTykWithGRPC()
   233  	defer ts.Close()
   234  	defer grpcServer.Stop()
   235  
   236  	keyID := gateway.CreateSession(func(s *user.SessionState) {
   237  		s.MetaData = map[string]interface{}{
   238  			"testkey":  map[string]interface{}{"nestedkey": "nestedvalue"},
   239  			"testkey2": "testvalue",
   240  		}
   241  	})
   242  	headers := map[string]string{"authorization": keyID}
   243  
   244  	t.Run("Pre Hook with SetHeaders", func(t *testing.T) {
   245  		res, err := ts.Run(t, test.TestCase{
   246  			Path:    "/grpc-test-api/",
   247  			Method:  http.MethodGet,
   248  			Code:    http.StatusOK,
   249  			Headers: headers,
   250  		})
   251  		if err != nil {
   252  			t.Fatalf("Request failed: %s", err.Error())
   253  		}
   254  		data, err := ioutil.ReadAll(res.Body)
   255  		if err != nil {
   256  			t.Fatalf("Couldn't read response body: %s", err.Error())
   257  		}
   258  		var testResponse gateway.TestHttpResponse
   259  		err = json.Unmarshal(data, &testResponse)
   260  		if err != nil {
   261  			t.Fatalf("Couldn't unmarshal test response JSON: %s", err.Error())
   262  		}
   263  		value, ok := testResponse.Headers[testHeaderName]
   264  		if !ok {
   265  			t.Fatalf("Header not found, expecting %s", testHeaderName)
   266  		}
   267  		if value != testHeaderValue {
   268  			t.Fatalf("Header value isn't %s", testHeaderValue)
   269  		}
   270  	})
   271  
   272  	t.Run("Pre Hook with UTF-8/non-UTF-8 request data", func(t *testing.T) {
   273  		fileData := gateway.GenerateTestBinaryData()
   274  		var buf bytes.Buffer
   275  		multipartWriter := multipart.NewWriter(&buf)
   276  		file, err := multipartWriter.CreateFormFile("file", "test.bin")
   277  		if err != nil {
   278  			t.Fatalf("Couldn't use multipart writer: %s", err.Error())
   279  		}
   280  		_, err = fileData.WriteTo(file)
   281  		if err != nil {
   282  			t.Fatalf("Couldn't write to multipart file: %s", err.Error())
   283  		}
   284  		field, err := multipartWriter.CreateFormField("testfield")
   285  		if err != nil {
   286  			t.Fatalf("Couldn't use multipart writer: %s", err.Error())
   287  		}
   288  		_, err = field.Write([]byte("testvalue"))
   289  		if err != nil {
   290  			t.Fatalf("Couldn't write to form field: %s", err.Error())
   291  		}
   292  		err = multipartWriter.Close()
   293  		if err != nil {
   294  			t.Fatalf("Couldn't close multipart writer: %s", err.Error())
   295  		}
   296  
   297  		ts.Run(t, []test.TestCase{
   298  			{Path: "/grpc-test-api-2/", Code: 200, Data: &buf, Headers: map[string]string{"Content-Type": multipartWriter.FormDataContentType()}},
   299  			{Path: "/grpc-test-api-2/", Code: 200, Data: "{}", Headers: map[string]string{"Content-Type": "application/json"}},
   300  		}...)
   301  	})
   302  
   303  	t.Run("Post Hook with metadata", func(t *testing.T) {
   304  		ts.Run(t, test.TestCase{
   305  			Path:    "/grpc-test-api-3/",
   306  			Method:  http.MethodGet,
   307  			Code:    http.StatusOK,
   308  			Headers: headers,
   309  		})
   310  	})
   311  
   312  }
   313  
   314  func BenchmarkGRPCDispatch(b *testing.B) {
   315  	ts, grpcServer := startTykWithGRPC()
   316  	defer ts.Close()
   317  	defer grpcServer.Stop()
   318  
   319  	keyID := gateway.CreateSession(func(s *user.SessionState) {})
   320  	headers := map[string]string{"authorization": keyID}
   321  
   322  	b.Run("Pre Hook with SetHeaders", func(b *testing.B) {
   323  		path := "/grpc-test-api/"
   324  		b.ReportAllocs()
   325  		for i := 0; i < b.N; i++ {
   326  			ts.Run(b, test.TestCase{
   327  				Path:    path,
   328  				Method:  http.MethodGet,
   329  				Code:    http.StatusOK,
   330  				Headers: headers,
   331  			})
   332  		}
   333  	})
   334  }