github.com/renbou/grpcbridge@v0.0.2-0.20240416012907-bcbd8b12648a/routing/service_router_test.go (about) 1 package routing 2 3 import ( 4 "context" 5 "fmt" 6 "sync" 7 "testing" 8 9 "github.com/renbou/grpcbridge/bridgedesc" 10 "github.com/renbou/grpcbridge/internal/bridgetest" 11 "google.golang.org/grpc" 12 "google.golang.org/grpc/codes" 13 "google.golang.org/protobuf/reflect/protoreflect" 14 ) 15 16 type fakeServerTransportStream struct { 17 grpc.ServerTransportStream 18 method string 19 } 20 21 func (sts fakeServerTransportStream) Method() string { 22 return sts.method 23 } 24 25 type grpcTestCase struct { 26 method string 27 routedService protoreflect.FullName 28 routedMethod string 29 statusCode codes.Code 30 } 31 32 func checkGRPCTestCase(t *testing.T, sr *ServiceRouter, tc *grpcTestCase) { 33 t.Helper() 34 35 // Act 36 _, route, err := sr.RouteGRPC(grpc.NewContextWithServerTransportStream(context.Background(), fakeServerTransportStream{method: tc.method})) 37 38 // Assert 39 if cmpErr := bridgetest.StatusCodeIs(err, tc.statusCode); cmpErr != nil { 40 t.Fatalf("RouteGRPC(%s) returned error = %q with unexpected code: %s", tc.method, err, cmpErr) 41 } 42 43 if tc.statusCode != codes.OK { 44 return 45 } 46 47 if route.Method == nil || route.Service == nil || route.Target == nil { 48 t.Fatalf("RouteGRPC(%s) returned route with missing info about method, service, or target: %#v", tc.method, route) 49 } 50 51 if route.Method.RPCName != tc.routedMethod { 52 t.Errorf("RouteGRPC(%s) returned route with method = %q, want %q", tc.method, route.Method.RPCName, tc.routedMethod) 53 } 54 55 if route.Service.Name != tc.routedService { 56 t.Errorf("RouteGRPC(%s) returned route with service = %q, want %q", tc.method, route.Service.Name, tc.routedService) 57 } 58 } 59 60 // Test_ServiceRouter_RouteGRPC_Ok tests that ServiceRouter routes GRPC requests correctly. 61 func Test_ServiceRouter_RouteGRPC_Ok(t *testing.T) { 62 t.Parallel() 63 64 // Arrange 65 sr := NewServiceRouter(nilConnPool{}, ServiceRouterOpts{}) 66 67 watcher, err := sr.Watch(testTarget.Name) 68 if err != nil { 69 t.Fatalf("Watch(%q) returned non-nil error = %q", testTarget.Name, err) 70 } 71 72 defer watcher.Close() 73 watcher.UpdateDesc(&testTarget) 74 75 tests := []grpcTestCase{ 76 {method: "/grpcbridge.routing.v1.PureGRPCSvc/Get", routedService: "grpcbridge.routing.v1.PureGRPCSvc", routedMethod: "/grpcbridge.routing.v1.PureGRPCSvc/Get", statusCode: codes.OK}, 77 {method: "grpcbridge.routing.v1.PureGRPCSvc/Get", routedService: "grpcbridge.routing.v1.PureGRPCSvc", routedMethod: "/grpcbridge.routing.v1.PureGRPCSvc/Get", statusCode: codes.OK}, 78 {method: "grpcbridge.routing.v1.PureGRPCSvc/UndefinedMethodButDefinedService", routedService: "grpcbridge.routing.v1.PureGRPCSvc", routedMethod: "/grpcbridge.routing.v1.PureGRPCSvc/UndefinedMethodButDefinedService", statusCode: codes.OK}, 79 {method: "/grpcbridge.routing.v2.RestSvc/GetEntity", routedService: "grpcbridge.routing.v2.RestSvc", routedMethod: "/grpcbridge.routing.v2.RestSvc/GetEntity", statusCode: codes.OK}, 80 {method: "/grpcbridge.unknown.Service/Get", statusCode: codes.Unimplemented}, 81 {method: "not a method", statusCode: codes.Unimplemented}, 82 {method: "/grpcbridge.missing_method.Service", statusCode: codes.Unimplemented}, 83 } 84 85 // Run in non-parallel subtest so that watcher.Close() runs AFTER all the subtests. 86 t.Run("cases", func(t *testing.T) { 87 for _, tt := range tests { 88 t.Run(tt.method, func(t *testing.T) { 89 t.Parallel() 90 91 // Act & Assert 92 checkGRPCTestCase(t, sr, &tt) 93 }) 94 } 95 }) 96 } 97 98 // Test_ServiceRouter_RouteHTTP_Ok tests that ServiceRouter routes HTTP requests correctly. 99 func Test_ServiceRouter_RouteHTTP_Ok(t *testing.T) { 100 t.Parallel() 101 102 // Arrange 103 sr := NewServiceRouter(nilConnPool{}, ServiceRouterOpts{}) 104 105 watcher, err := sr.Watch(testTarget.Name) 106 if err != nil { 107 t.Fatalf("Watch(%q) returned non-nil error = %q", testTarget.Name, err) 108 } 109 110 defer watcher.Close() 111 watcher.UpdateDesc(&testTarget) 112 113 tests := []patternTestCase{ 114 {method: "POST", path: "/grpcbridge.routing.v1.PureGRPCSvc/Get", routedMethod: "/grpcbridge.routing.v1.PureGRPCSvc/Get", routedPattern: "/grpcbridge.routing.v1.PureGRPCSvc/Get", statusCode: codes.OK}, 115 {method: "GET", path: "/grpcbridge.routing.v1.PureGRPCSvc/Get", statusCode: codes.Unimplemented}, 116 {method: "POST", path: "/grpcbridge.routing.v1.PureGRPCSvc/List", routedMethod: "/grpcbridge.routing.v1.PureGRPCSvc/List", routedPattern: "/grpcbridge.routing.v1.PureGRPCSvc/List", statusCode: codes.OK}, 117 {method: "POST", path: "/grpcbridge.routing.v1.PureGRPCSvc/Unknown", routedMethod: "/grpcbridge.routing.v1.PureGRPCSvc/Unknown", routedPattern: "/grpcbridge.routing.v1.PureGRPCSvc/Unknown", statusCode: codes.OK}, 118 {method: "POST", path: "not a method", statusCode: codes.NotFound}, 119 {method: "POST", path: "/grpcbridge.missing_method.Service", statusCode: codes.NotFound}, 120 {method: "POST", path: "/grpcbridge.unknown.Service/Get", statusCode: codes.NotFound}, 121 } 122 123 // Run in non-parallel subtest so that watcher.Close() runs AFTER all the subtests. 124 t.Run("cases", func(t *testing.T) { 125 for _, tt := range tests { 126 t.Run(fmt.Sprintf("%s %s", tt.method, tt.path), func(t *testing.T) { 127 t.Parallel() 128 129 // Act & Assert 130 checkPatternTestCase(t, sr, &tt) 131 }) 132 } 133 }) 134 } 135 136 // Test_ServiceRouter_RouteGRPC_Updates tests that ServiceRouter properly handles concurrently occurring updates to many targets. 137 // N targets are created with one of the 3 possible "behaviour" templates, which describes which updates come from the target's watcher. 138 // After these updates are applied, a routing test is performed to check that only the active routes for each target are routed. 139 func Test_ServiceRouter_RouteGRPC_Updates(t *testing.T) { 140 t.Parallel() 141 142 const N = 100 143 const seed = 1 144 145 // Arrange 146 // A few update templates outlining different modifications which can come to a service router watcher. 147 updateTemplates := [][]bridgedesc.Target{ 148 { 149 // 2 services originally 150 {Services: []bridgedesc.Service{{Name: "grpcbridge.routing.testsvc_%d.ServiceV1"}, {Name: "grpcbridge.routing.testsvc_%d.ServiceV2"}}}, 151 // v1 service gets removed 152 {Services: []bridgedesc.Service{{Name: "grpcbridge.routing.testsvc_%d.ServiceV2"}}}, 153 // v2 service gets swapped for v3 154 {Services: []bridgedesc.Service{{Name: "grpcbridge.routing.testsvc_%d.ServiceV3"}}}, 155 }, 156 { 157 // No services 158 {Services: []bridgedesc.Service{}}, 159 // 1 service gets added 160 {Services: []bridgedesc.Service{{Name: "grpcbridge.routing.testsvc_%d.ServiceOne"}}}, 161 }, 162 { 163 // 1 service which gets removed due to the watcher closing. 164 {Services: []bridgedesc.Service{{Name: "grpcbridge.routing.testsvc_%d.ServiceClosed"}}}, 165 }, 166 } 167 168 templateTests := [][]grpcTestCase{ 169 { 170 // Only v3 should be available 171 {method: "/grpcbridge.routing.testsvc_%d.ServiceV1/MethodV1", statusCode: codes.Unimplemented}, 172 {method: "/grpcbridge.routing.testsvc_%d.ServiceV2/MethodV2", statusCode: codes.Unimplemented}, 173 {method: "/grpcbridge.routing.testsvc_%d.ServiceV3/MethodV3", routedService: "grpcbridge.routing.testsvc_%d.ServiceV3", routedMethod: "/grpcbridge.routing.testsvc_%d.ServiceV3/MethodV3", statusCode: codes.OK}, 174 }, 175 { 176 // The added service should be available 177 {method: "/grpcbridge.routing.testsvc_%d.ServiceOne/Method", routedService: "grpcbridge.routing.testsvc_%d.ServiceOne", routedMethod: "/grpcbridge.routing.testsvc_%d.ServiceOne/Method", statusCode: codes.OK}, 178 }, 179 { 180 // The closed service shouldn't be available 181 {method: "/grpcbridge.routing.testsvc_%d.ServiceClosed/Method", statusCode: codes.Unimplemented}, 182 }, 183 } 184 185 targets := buildTemplateTargets(N, seed, updateTemplates) 186 sr := NewServiceRouter(nilConnPool{}, ServiceRouterOpts{}) 187 188 watchers := make([]*ServiceRouterWatcher, len(targets)) 189 for i, ti := range targets { 190 var err error 191 if watchers[i], err = sr.Watch(ti.name); err != nil { 192 t.Fatalf("Watch(%q) returned non-nil error: %q", ti.name, err) 193 } 194 } 195 196 // Act 197 // Apply all the updates for each templated target. 198 var wg sync.WaitGroup 199 wg.Add(len(targets)) 200 201 for i, ti := range targets { 202 go func() { 203 defer wg.Done() 204 205 var update bridgedesc.Target 206 207 // Templating performed in parallel for faster testing. 208 for updateIdx := range updateTemplates[ti.template] { 209 buildDescTemplate(&updateTemplates[ti.template][updateIdx], &update, ti.name, i) 210 watchers[i].UpdateDesc(&update) 211 } 212 213 // Additionally separately handle targets with template 3, which should be closed completely. 214 if ti.template == 2 { 215 watchers[i].Close() 216 } 217 }() 218 } 219 220 wg.Wait() 221 222 // Assert 223 // Concurrently validate routing for each target 224 for i, ti := range targets { 225 t.Run(fmt.Sprintf("%d-%s", ti.template, ti.name), func(t *testing.T) { 226 t.Parallel() 227 228 // Apply templating to each case before actually testing it. 229 for _, tt := range templateTests[ti.template] { 230 tt.method = fmt.Sprintf(tt.method, i) 231 tt.routedMethod = fmt.Sprintf(tt.routedMethod, i) 232 tt.routedService = protoreflect.FullName(fmt.Sprintf(string(tt.routedService), i)) 233 234 checkGRPCTestCase(t, sr, &tt) 235 } 236 }) 237 } 238 }