package grpcurl import ( "bytes" "fmt" "io" "strings" "testing" "github.com/golang/protobuf/jsonpb" "github.com/golang/protobuf/proto" "github.com/golang/protobuf/ptypes/struct" "github.com/jhump/protoreflect/desc" "google.golang.org/grpc/metadata" ) func TestRequestFactory(t *testing.T) { source, err := DescriptorSourceFromProtoSets("testing/example.protoset") if err != nil { t.Fatalf("failed to create descriptor source: %v", err) } msg, err := makeProto() if err != nil { t.Fatalf("failed to create message: %v", err) } testCases := []struct { format Format input string expectedOutput []proto.Message }{ { format: FormatJSON, input: "", }, { format: FormatJSON, input: messageAsJSON, expectedOutput: []proto.Message{msg}, }, { format: FormatJSON, input: messageAsJSON + messageAsJSON + messageAsJSON, expectedOutput: []proto.Message{msg, msg, msg}, }, { // unlike JSON, empty input yields one empty message (vs. zero messages) format: FormatText, input: "", expectedOutput: []proto.Message{&structpb.Value{}}, }, { format: FormatText, input: messageAsText, expectedOutput: []proto.Message{msg}, }, { format: FormatText, input: messageAsText + string(textSeparatorChar), expectedOutput: []proto.Message{msg, &structpb.Value{}}, }, { format: FormatText, input: messageAsText + string(textSeparatorChar) + messageAsText + string(textSeparatorChar) + messageAsText, expectedOutput: []proto.Message{msg, msg, msg}, }, } for i, tc := range testCases { name := fmt.Sprintf("#%d, %s, %d message(s)", i+1, tc.format, len(tc.expectedOutput)) rf, _, err := RequestParserAndFormatterFor(tc.format, source, false, false, strings.NewReader(tc.input)) if err != nil { t.Errorf("Failed to create parser and formatter: %v", err) continue } numReqs := 0 for { var req structpb.Value err := rf.Next(&req) if err == io.EOF { break } else if err != nil { t.Errorf("%s, msg %d: unexpected error: %v", name, numReqs, err) } if !proto.Equal(&req, tc.expectedOutput[numReqs]) { t.Errorf("%s, msg %d: incorrect message;\nexpecting:\n%v\ngot:\n%v", name, numReqs, tc.expectedOutput[numReqs], &req) } numReqs++ } if rf.NumRequests() != numReqs { t.Errorf("%s: factory reported wrong number of requests: expecting %d, got %d", name, numReqs, rf.NumRequests()) } } } // Handler prints response data (and headers/trailers in verbose mode). // This verifies that we get the right output in both JSON and proto text modes. func TestHandler(t *testing.T) { source, err := DescriptorSourceFromProtoSets("testing/example.protoset") if err != nil { t.Fatalf("failed to create descriptor source: %v", err) } d, err := source.FindSymbol("TestService.GetFiles") if err != nil { t.Fatalf("failed to find method 'TestService.GetFiles': %v", err) } md, ok := d.(*desc.MethodDescriptor) if !ok { t.Fatalf("wrong kind of descriptor found: %T", d) } reqHeaders := metadata.Pairs("foo", "123", "bar", "456") respHeaders := metadata.Pairs("foo", "abc", "bar", "def", "baz", "xyz") respTrailers := metadata.Pairs("a", "1", "b", "2", "c", "3") rsp, err := makeProto() if err != nil { t.Fatalf("failed to create response message: %v", err) } for _, format := range []Format{FormatJSON, FormatText} { for _, numMessages := range []int{1, 3} { for _, verbose := range []bool{true, false} { name := fmt.Sprintf("%s, %d message(s)", format, numMessages) if verbose { name += ", verbose" } _, formatter, err := RequestParserAndFormatterFor(format, source, false, !verbose, nil) if err != nil { t.Errorf("Failed to create parser and formatter: %v", err) continue } var buf bytes.Buffer h := NewDefaultEventHandler(&buf, source, formatter, verbose) h.OnResolveMethod(md) h.OnSendHeaders(reqHeaders) h.OnReceiveHeaders(respHeaders) for i := 0; i < numMessages; i++ { h.OnReceiveResponse(rsp) } h.OnReceiveTrailers(nil, respTrailers) expectedOutput := "" if verbose { expectedOutput += verbosePrefix } for i := 0; i < numMessages; i++ { if verbose { expectedOutput += verboseResponseHeader } if format == "json" { expectedOutput += messageAsJSON } else { if i > 0 && !verbose { expectedOutput += string(textSeparatorChar) } expectedOutput += messageAsText } } if verbose { expectedOutput += verboseSuffix } out := buf.String() if !compare(out, expectedOutput) { t.Errorf("%s: Incorrect output. Expected:\n%s\nGot:\n%s", name, expectedOutput, out) } } } } } // compare checks that actual and expected are equal, returning true if so. // A simple equality check (==) does not suffice because jsonpb formats // structpb.Value strangely. So if that formatting gets fixed, we don't // want this test in grpcurl to suddenly start failing. So we check each // line and compare the lines after stripping whitespace (which removes // the jsonpb format anomalies). func compare(actual, expected string) bool { actualLines := strings.Split(actual, "\n") expectedLines := strings.Split(expected, "\n") if len(actualLines) != len(expectedLines) { return false } for i := 0; i < len(actualLines); i++ { if strings.TrimSpace(actualLines[i]) != strings.TrimSpace(expectedLines[i]) { return false } } return true } func makeProto() (proto.Message, error) { var rsp structpb.Value err := jsonpb.UnmarshalString(`{ "foo": ["abc", "def", "ghi"], "bar": { "a": 1, "b": 2 }, "baz": true, "null": null }`, &rsp) if err != nil { return nil, err } return &rsp, nil } var ( verbosePrefix = ` Resolved method descriptor: rpc GetFiles ( .TestRequest ) returns ( .TestResponse ); Request metadata to send: bar: 456 foo: 123 Response headers received: bar: def baz: xyz foo: abc ` verboseSuffix = ` Response trailers received: a: 1 b: 2 c: 3 ` verboseResponseHeader = ` Response contents: ` messageAsJSON = `{ "bar": { "a": 1, "b": 2 }, "baz": true, "foo": [ "abc", "def", "ghi" ], "null": null } ` messageAsText = `struct_value: < fields: < key: "bar" value: < struct_value: < fields: < key: "a" value: < number_value: 1 > > fields: < key: "b" value: < number_value: 2 > > > > > fields: < key: "baz" value: < bool_value: true > > fields: < key: "foo" value: < list_value: < values: < string_value: "abc" > values: < string_value: "def" > values: < string_value: "ghi" > > > > fields: < key: "null" value: < null_value: NULL_VALUE > > > ` )