github.com/renbou/grpcbridge@v0.0.2-0.20240416012907-bcbd8b12648a/internal/bridgetest/grpcbridge_test.go (about)

     1  package bridgetest
     2  
     3  import (
     4  	"context"
     5  	"io"
     6  	"math"
     7  	"net"
     8  	"net/http"
     9  	"net/http/httptest"
    10  	"strings"
    11  	"testing"
    12  	"time"
    13  
    14  	"github.com/google/go-cmp/cmp"
    15  	"github.com/renbou/grpcbridge"
    16  	"github.com/renbou/grpcbridge/internal/bridgetest/testpb"
    17  	"github.com/renbou/grpcbridge/transcoding"
    18  	"google.golang.org/grpc"
    19  	"google.golang.org/grpc/credentials/insecure"
    20  	"google.golang.org/grpc/reflection"
    21  	"google.golang.org/protobuf/testing/protocmp"
    22  	"google.golang.org/protobuf/types/known/durationpb"
    23  )
    24  
    25  var testEchoMessage = &testpb.Combined{
    26  	Scalars: &testpb.Scalars{
    27  		BoolValue:     true,
    28  		Int32Value:    -12379,
    29  		Uint32Value:   378,
    30  		Uint64Value:   math.MaxUint64,
    31  		Int64Value:    math.MinInt64,
    32  		Sfixed64Value: 1,
    33  		DoubleValue:   math.Inf(-1),
    34  		StringValue:   "sttrrr",
    35  	},
    36  	NonScalars: &testpb.NonScalars{
    37  		Str2StrMap: map[string]string{
    38  			"key1": "value1",
    39  			"key2": "value2",
    40  		},
    41  		Str2Int32Map: map[string]int32{
    42  			"one": 1,
    43  			"two": 2,
    44  		},
    45  		Duration: durationpb.New(time.Millisecond * 123),
    46  		Child: &testpb.NonScalars_Child{
    47  			Nested: &testpb.NonScalars_Child_Digits{Digits: testpb.Digits_TWO},
    48  		},
    49  	},
    50  }
    51  
    52  // Test_GRPCBridge performs a basic integration test of all the [grpcbridge] functionality.
    53  func Test_GRPCBridge(t *testing.T) {
    54  	t.Parallel()
    55  
    56  	// Arrange
    57  	// Set up a single complete grpcbridge instance for all tests to test parallelism.
    58  	targetListener, targetServer := mustRawServer(t, nil, []func(*grpc.Server){func(s *grpc.Server) {
    59  		testpb.RegisterTestServiceServer(s, new(testpb.TestService))
    60  		reflection.Register(s)
    61  	}})
    62  
    63  	defer targetServer.Stop()
    64  
    65  	router := grpcbridge.NewReflectionRouter(
    66  		grpcbridge.WithLogger(Logger(t)),
    67  		grpcbridge.WithDialOpts(
    68  			grpc.WithTransportCredentials(insecure.NewCredentials()),
    69  			grpc.WithContextDialer(func(ctx context.Context, s string) (net.Conn, error) {
    70  				return targetListener.Dial()
    71  			}),
    72  		),
    73  		grpcbridge.WithConnFunc(grpc.NewClient),
    74  		grpcbridge.WithDisabledReflectionPolling(),
    75  		grpcbridge.WithReflectionPollInterval(time.Second),
    76  	)
    77  
    78  	if ok, err := router.Add(TestTargetName, TestDialTarget); err != nil {
    79  		t.Fatalf("ReflectionRouter.Add(%q) returned non-nil error = %q", TestTargetName, err)
    80  	} else if !ok {
    81  		t.Fatalf("ReflectionRouter.Add(%q) returned false, expected it to successfully add test target", TestTargetName)
    82  	}
    83  
    84  	t.Cleanup(func() {
    85  		if ok := router.Remove(TestTargetName); !ok {
    86  			t.Fatalf("ReflectionRouter.Remove(%q) returned false, expected it to successfully remove test target", TestTargetName)
    87  		}
    88  	})
    89  
    90  	// Wait for the router to update.
    91  	// TODO(renbou): perhaps introduce a blocking mode to the reflection resolver,
    92  	// which would allow waiting for the first resolution to succeed, or return an error?
    93  	time.Sleep(time.Millisecond * 50)
    94  
    95  	proxy := grpcbridge.NewGRPCProxy(router, grpcbridge.WithLogger(Logger(t)))
    96  	bridge := grpcbridge.NewWebBridge(router,
    97  		grpcbridge.WithLogger(Logger(t)),
    98  		grpcbridge.WithDefaultMarshaler(transcoding.DefaultJSONMarshaler),
    99  		grpcbridge.WithMarshalers([]transcoding.Marshaler{transcoding.DefaultJSONMarshaler}),
   100  	)
   101  
   102  	// Set up servers which simulate grpcbridge itself.
   103  	bridgeListener, grpcServer := mustRawServer(t, []grpc.ServerOption{proxy.AsServerOption()}, nil)
   104  	defer grpcServer.Stop()
   105  
   106  	httpServer := httptest.NewServer(bridge)
   107  	defer httpServer.Close()
   108  
   109  	grpcClient, err := grpc.NewClient(TestDialTarget,
   110  		grpc.WithTransportCredentials(insecure.NewCredentials()),
   111  		grpc.WithContextDialer(func(ctx context.Context, s string) (net.Conn, error) {
   112  			return bridgeListener.Dial()
   113  		}),
   114  	)
   115  	if err != nil {
   116  		t.Fatalf("grpc.NewClient(grpcbridge) returned non-nil error = %q", err)
   117  	}
   118  
   119  	httpClient := httpServer.Client()
   120  
   121  	// Run in non-parallel subtest so that server.Stop() runs AFTER all the subtests.
   122  	t.Run("cases", func(t *testing.T) {
   123  		t.Run("proxy", func(t *testing.T) {
   124  			test_GRPCBridge_Proxy(t, grpcClient)
   125  		})
   126  
   127  		t.Run("bridge", func(t *testing.T) {
   128  			test_GRPCBridge_Bridge(t, httpClient, httpServer)
   129  		})
   130  	})
   131  }
   132  
   133  func test_GRPCBridge_Proxy(t *testing.T, client *grpc.ClientConn) {
   134  	t.Parallel()
   135  
   136  	// Arrange
   137  	ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
   138  	defer cancel()
   139  
   140  	testClient := testpb.NewTestServiceClient(client)
   141  
   142  	// Act
   143  	resp, gotErr := testClient.Echo(ctx, testEchoMessage)
   144  
   145  	// Assert
   146  	if gotErr != nil {
   147  		t.Errorf("TestService.Echo() returned non-nil error = %q", gotErr)
   148  	}
   149  
   150  	if diff := cmp.Diff(testEchoMessage, resp, protocmp.Transform()); diff != "" {
   151  		t.Fatalf("TestService.Echo() returned response differing from request (-want+got):\n%s", diff)
   152  	}
   153  }
   154  
   155  func test_GRPCBridge_Bridge(t *testing.T, client *http.Client, server *httptest.Server) {
   156  	t.Parallel()
   157  
   158  	// Arrange
   159  	testBody := `{
   160  		"scalars": {
   161  			"boolValue": true,
   162  			"int32Value": -12379,
   163  			"uint32Value": 378,
   164  			"uint64Value": "18446744073709551615",
   165  			"int64Value": "-9223372036854775808",
   166  			"sfixed64Value": 1,
   167  			"doubleValue": "-Infinity",
   168  			"stringValue": "sttrrr"
   169  		},
   170  		"nonScalars": {
   171  			"str2strMap": {
   172  				"key1": "value1",
   173  				"key2": "value2"
   174  			},
   175  			"str2int32Map": {
   176  				"one": 1,
   177  				"two": 2
   178  			},
   179  			"duration": "0.123s",
   180  			"child": {
   181  				"digits": "TWO"
   182  			}
   183  		}
   184  	}`
   185  
   186  	// Act
   187  	resp, gotErr := client.Post(server.URL+"/service/echo", "application/json", strings.NewReader(testBody))
   188  
   189  	// Assert
   190  	if gotErr != nil {
   191  		t.Fatalf("POST(/service/echo) returned non-nil error = %q", gotErr)
   192  	}
   193  
   194  	if resp.StatusCode != http.StatusOK {
   195  		body, _ := io.ReadAll(resp.Body)
   196  		t.Fatalf("POST(/service/echo) returned non-OK response = %s (%s)", resp.Status, string(body))
   197  	}
   198  
   199  	respMsg := new(testpb.Combined)
   200  	if err := transcoding.DefaultJSONMarshaler.Unmarshal(testpb.TestServiceTypesResolver, []byte(testBody), respMsg.ProtoReflect(), nil); err != nil {
   201  		t.Fatalf("POST(/service/echo) returned invalid body, failed to unmarshal with error = %q", err)
   202  	}
   203  
   204  	if diff := cmp.Diff(testEchoMessage, respMsg, protocmp.Transform()); diff != "" {
   205  		t.Fatalf("POST(/service/echo) returned response differing from request (-want+got):\n%s", diff)
   206  	}
   207  }