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  }