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 }