package genopenapi_test import ( "os" "reflect" "sort" "strings" "testing" "github.com/google/go-cmp/cmp" "github.com/grpc-ecosystem/grpc-gateway/v2/internal/descriptor" "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2/internal/genopenapi" "gopkg.in/yaml.v3" "google.golang.org/protobuf/encoding/prototext" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/descriptorpb" "google.golang.org/protobuf/types/pluginpb" ) func TestGenerate_YAML(t *testing.T) { t.Parallel() req := &pluginpb.CodeGeneratorRequest{ ProtoFile: []*descriptorpb.FileDescriptorProto{{ Name: proto.String("file.proto"), Package: proto.String("example"), Options: &descriptorpb.FileOptions{ GoPackage: proto.String("goexample/v1;goexample"), }, }}, FileToGenerate: []string{ "file.proto", }, } resp := requireGenerate(t, req, genopenapi.FormatYAML, false, false) if len(resp) != 1 { t.Fatalf("invalid count, expected: 1, actual: %d", len(resp)) } var p map[string]interface{} err := yaml.Unmarshal([]byte(resp[0].GetContent()), &p) if err != nil { t.Fatalf("failed to unmarshall yaml: %s", err) } } func TestGenerateExtension(t *testing.T) { t.Parallel() const in = ` file_to_generate: "exampleproto/v1/example.proto" parameter: "output_format=yaml,allow_delete_body=true" proto_file: { name: "exampleproto/v1/example.proto" package: "example.v1" message_type: { name: "Foo" field: { name: "bar" number: 1 label: LABEL_OPTIONAL type: TYPE_STRING json_name: "bar" options: { [grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field]: { description: "This is bar" extensions: { key: "x-go-default" value: { string_value: "0.5s" } } } } } } service: { name: "TestService" method: { name: "Test" input_type: ".example.v1.Foo" output_type: ".example.v1.Foo" options: {} } } options: { go_package: "exampleproto/v1;exampleproto" } }` var req pluginpb.CodeGeneratorRequest if err := prototext.Unmarshal([]byte(in), &req); err != nil { t.Fatalf("failed to marshall yaml: %s", err) } formats := [...]genopenapi.Format{ genopenapi.FormatJSON, genopenapi.FormatYAML, } for _, format := range formats { format := format t.Run(string(format), func(t *testing.T) { t.Parallel() resp := requireGenerate(t, &req, format, false, false) if len(resp) != 1 { t.Fatalf("invalid count, expected: 1, actual: %d", len(resp)) } content := resp[0].GetContent() t.Log(content) if !strings.Contains(content, "x-go-default") { t.Fatal("x-go-default not found in content message") } }) } } func TestGenerateYAML(t *testing.T) { t.Parallel() tests := []struct { name string inputProtoText string wantYAML string }{ { // It tests https://github.com/grpc-ecosystem/grpc-gateway/issues/3557. name: "path item object", inputProtoText: "testdata/generator/path_item_object.prototext", wantYAML: "testdata/generator/path_item_object.swagger.yaml", }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() b, err := os.ReadFile(tt.inputProtoText) if err != nil { t.Fatal(err) } var req pluginpb.CodeGeneratorRequest if err := prototext.Unmarshal(b, &req); err != nil { t.Fatal(err) } resp := requireGenerate(t, &req, genopenapi.FormatYAML, false, true) if len(resp) != 1 { t.Fatalf("invalid count, expected: 1, actual: %d", len(resp)) } got := resp[0].GetContent() want, err := os.ReadFile(tt.wantYAML) if err != nil { t.Fatal(err) } diff := cmp.Diff(string(want), got) if diff != "" { t.Fatalf("content not match\n%s", diff) } }) } } func requireGenerate( tb testing.TB, req *pluginpb.CodeGeneratorRequest, format genopenapi.Format, preserveRPCOrder bool, allowMerge bool, ) []*descriptor.ResponseFile { tb.Helper() reg := descriptor.NewRegistry() reg.SetPreserveRPCOrder(preserveRPCOrder) reg.SetAllowMerge(allowMerge) if err := reg.Load(req); err != nil { tb.Fatalf("failed to load request: %s", err) } var targets []*descriptor.File for _, target := range req.FileToGenerate { f, err := reg.LookupFile(target) if err != nil { tb.Fatalf("failed to lookup file: %s", err) } targets = append(targets, f) } g := genopenapi.New(reg, format) resp, err := g.Generate(targets) switch { case err != nil: tb.Fatalf("failed to generate targets: %s", err) case len(resp) != len(targets) && !allowMerge: tb.Fatalf("invalid count, expected: %d, actual: %d", len(targets), len(resp)) } return resp } func TestGeneratedYAMLIndent(t *testing.T) { // It tests https://github.com/grpc-ecosystem/grpc-gateway/issues/2745. const in = ` file_to_generate: "exampleproto/v1/exampleproto.proto" parameter: "output_format=yaml,allow_delete_body=true" proto_file: { name: "exampleproto/v1/exampleproto.proto" package: "repro" message_type: { name: "RollupRequest" field: { name: "type" number: 1 label: LABEL_OPTIONAL type: TYPE_ENUM type_name: ".repro.RollupType" json_name: "type" } } message_type: { name: "RollupResponse" } enum_type: { name: "RollupType" value: { name: "UNKNOWN_ROLLUP" number: 0 } value: { name: "APPLE" number: 1 } value: { name: "BANANA" number: 2 } value: { name: "CARROT" number: 3 } } service: { name: "Repro" method: { name: "GetRollup" input_type: ".repro.RollupRequest" output_type: ".repro.RollupResponse" options: { [google.api.http]: { get: "/rollup" } } } } options: { go_package: "repro/foobar" } source_code_info: { location: { path: 5 path: 0 path: 2 path: 1 span: 24 span: 4 span: 14 leading_comments: " Apples are good\n" } location: { path: 5 path: 0 path: 2 path: 3 span: 28 span: 4 span: 15 leading_comments: " Carrots are mediocre\n" } } syntax: "proto3" } ` var req pluginpb.CodeGeneratorRequest if err := prototext.Unmarshal([]byte(in), &req); err != nil { t.Fatalf("failed to marshall yaml: %s", err) } resp := requireGenerate(t, &req, genopenapi.FormatYAML, false, false) if len(resp) != 1 { t.Fatalf("invalid count, expected: 1, actual: %d", len(resp)) } content := resp[0].GetContent() err := yaml.Unmarshal([]byte(content), map[string]interface{}{}) if err != nil { t.Log(content) t.Fatalf("got invalid yaml: %s", err) } } func TestGenerateRPCOrderPreserved(t *testing.T) { t.Parallel() const in = ` file_to_generate: "exampleproto/v1/example.proto" parameter: "output_format=yaml,allow_delete_body=true" proto_file: { name: "exampleproto/v1/example.proto" package: "example.v1" message_type: { name: "Foo" field: { name: "bar" number: 1 label: LABEL_OPTIONAL type: TYPE_STRING json_name: "bar" } } service: { name: "TestService" method: { name: "Test1" input_type: ".example.v1.Foo" output_type: ".example.v1.Foo" options: { [google.api.http]: { get: "/b/first" } } } method: { name: "Test2" input_type: ".example.v1.Foo" output_type: ".example.v1.Foo" options: { [google.api.http]: { get: "/a/second" } } } method: { name: "Test3" input_type: ".example.v1.Foo" output_type: ".example.v1.Foo" options: { [google.api.http]: { get: "/c/third" } } } } options: { go_package: "exampleproto/v1;exampleproto" } }` var req pluginpb.CodeGeneratorRequest if err := prototext.Unmarshal([]byte(in), &req); err != nil { t.Fatalf("failed to marshall yaml: %s", err) } formats := [...]genopenapi.Format{ genopenapi.FormatJSON, genopenapi.FormatYAML, } for _, format := range formats { format := format t.Run(string(format), func(t *testing.T) { t.Parallel() resp := requireGenerate(t, &req, format, true, false) if len(resp) != 1 { t.Fatalf("invalid count, expected: 1, actual: %d", len(resp)) } content := resp[0].GetContent() t.Log(content) contentsSlice := strings.Fields(content) expectedPaths := []string{"/b/first", "/a/second", "/c/third"} foundPaths := []string{} for _, contentValue := range contentsSlice { findExpectedPaths(&foundPaths, expectedPaths, contentValue) } if allPresent := reflect.DeepEqual(foundPaths, expectedPaths); !allPresent { t.Fatalf("Found paths differed from expected paths. Got: %#v, want %#v", foundPaths, expectedPaths) } }) } } func TestGenerateRPCOrderNotPreserved(t *testing.T) { t.Parallel() const in = ` file_to_generate: "exampleproto/v1/example.proto" parameter: "output_format=yaml,allow_delete_body=true" proto_file: { name: "exampleproto/v1/example.proto" package: "example.v1" message_type: { name: "Foo" field: { name: "bar" number: 1 label: LABEL_OPTIONAL type: TYPE_STRING json_name: "bar" } } service: { name: "TestService" method: { name: "Test1" input_type: ".example.v1.Foo" output_type: ".example.v1.Foo" options: { [google.api.http]: { get: "/b/first" } } } method: { name: "Test2" input_type: ".example.v1.Foo" output_type: ".example.v1.Foo" options: { [google.api.http]: { get: "/a/second" } } } method: { name: "Test3" input_type: ".example.v1.Foo" output_type: ".example.v1.Foo" options: { [google.api.http]: { get: "/c/third" } } } } options: { go_package: "exampleproto/v1;exampleproto" } }` var req pluginpb.CodeGeneratorRequest if err := prototext.Unmarshal([]byte(in), &req); err != nil { t.Fatalf("failed to marshall yaml: %s", err) } formats := [...]genopenapi.Format{ genopenapi.FormatJSON, genopenapi.FormatYAML, } for _, format := range formats { format := format t.Run(string(format), func(t *testing.T) { t.Parallel() resp := requireGenerate(t, &req, format, false, false) if len(resp) != 1 { t.Fatalf("invalid count, expected: 1, actual: %d", len(resp)) } content := resp[0].GetContent() t.Log(content) contentsSlice := strings.Fields(content) expectedPaths := []string{"/a/second", "/b/first", "/c/third"} foundPaths := []string{} for _, contentValue := range contentsSlice { findExpectedPaths(&foundPaths, expectedPaths, contentValue) } if allPresent := reflect.DeepEqual(foundPaths, expectedPaths); !allPresent { t.Fatalf("Found paths differed from expected paths. Got: %#v, want %#v", foundPaths, expectedPaths) } }) } } func TestGenerateRPCOrderPreservedMultipleServices(t *testing.T) { t.Parallel() const in = ` file_to_generate: "exampleproto/v1/example.proto" parameter: "output_format=yaml,allow_delete_body=true" proto_file: { name: "exampleproto/v1/example.proto" package: "example.v1" message_type: { name: "Foo" field: { name: "bar" number: 1 label: LABEL_OPTIONAL type: TYPE_STRING json_name: "bar" } } service: { name: "TestServiceOne" method: { name: "Test1" input_type: ".example.v1.Foo" output_type: ".example.v1.Foo" options: { [google.api.http]: { get: "/d/first" } } } method: { name: "Test2" input_type: ".example.v1.Foo" output_type: ".example.v1.Foo" options: { [google.api.http]: { get: "/e/second" } } } method: { name: "Test3" input_type: ".example.v1.Foo" output_type: ".example.v1.Foo" options: { [google.api.http]: { get: "/c/third" } } } } service: { name: "TestServiceTwo" method: { name: "Test1" input_type: ".example.v1.Foo" output_type: ".example.v1.Foo" options: { [google.api.http]: { get: "/b/first" } } } method: { name: "Test2" input_type: ".example.v1.Foo" output_type: ".example.v1.Foo" options: { [google.api.http]: { get: "/a/second" } } } method: { name: "Test3" input_type: ".example.v1.Foo" output_type: ".example.v1.Foo" options: { [google.api.http]: { get: "/g/third" } } } } options: { go_package: "exampleproto/v1;exampleproto" } }` var req pluginpb.CodeGeneratorRequest if err := prototext.Unmarshal([]byte(in), &req); err != nil { t.Fatalf("failed to marshall yaml: %s", err) } formats := [...]genopenapi.Format{ genopenapi.FormatJSON, genopenapi.FormatYAML, } for _, format := range formats { format := format t.Run(string(format), func(t *testing.T) { t.Parallel() resp := requireGenerate(t, &req, format, true, false) if len(resp) != 1 { t.Fatalf("invalid count, expected: 1, actual: %d", len(resp)) } content := resp[0].GetContent() t.Log(content) contentsSlice := strings.Fields(content) expectedPaths := []string{"/d/first", "/e/second", "/c/third", "/b/first", "/a/second", "/g/third"} foundPaths := []string{} for _, contentValue := range contentsSlice { findExpectedPaths(&foundPaths, expectedPaths, contentValue) } if allPresent := reflect.DeepEqual(foundPaths, expectedPaths); !allPresent { t.Fatalf("Found paths differed from expected paths. Got: %#v, want %#v", foundPaths, expectedPaths) } }) } } func TestGenerateRPCOrderNotPreservedMultipleServices(t *testing.T) { t.Parallel() const in = ` file_to_generate: "exampleproto/v1/example.proto" parameter: "output_format=yaml,allow_delete_body=true" proto_file: { name: "exampleproto/v1/example.proto" package: "example.v1" message_type: { name: "Foo" field: { name: "bar" number: 1 label: LABEL_OPTIONAL type: TYPE_STRING json_name: "bar" } } service: { name: "TestServiceOne" method: { name: "Test1" input_type: ".example.v1.Foo" output_type: ".example.v1.Foo" options: { [google.api.http]: { get: "/d/first" } } } method: { name: "Test2" input_type: ".example.v1.Foo" output_type: ".example.v1.Foo" options: { [google.api.http]: { get: "/e/second" } } } method: { name: "Test3" input_type: ".example.v1.Foo" output_type: ".example.v1.Foo" options: { [google.api.http]: { get: "/c/third" } } } } service: { name: "TestServiceTwo" method: { name: "Test1" input_type: ".example.v1.Foo" output_type: ".example.v1.Foo" options: { [google.api.http]: { get: "/b/first" } } } method: { name: "Test2" input_type: ".example.v1.Foo" output_type: ".example.v1.Foo" options: { [google.api.http]: { get: "/a/second" } } } method: { name: "Test3" input_type: ".example.v1.Foo" output_type: ".example.v1.Foo" options: { [google.api.http]: { get: "/g/third" } } } } options: { go_package: "exampleproto/v1;exampleproto" } }` var req pluginpb.CodeGeneratorRequest if err := prototext.Unmarshal([]byte(in), &req); err != nil { t.Fatalf("failed to marshall yaml: %s", err) } formats := [...]genopenapi.Format{ genopenapi.FormatJSON, genopenapi.FormatYAML, } for _, format := range formats { format := format t.Run(string(format), func(t *testing.T) { t.Parallel() resp := requireGenerate(t, &req, format, false, false) if len(resp) != 1 { t.Fatalf("invalid count, expected: 1, actual: %d", len(resp)) } content := resp[0].GetContent() t.Log(content) contentsSlice := strings.Fields(content) expectedPaths := []string{"/d/first", "/e/second", "/c/third", "/b/first", "/a/second", "/g/third"} sort.Strings(expectedPaths) foundPaths := []string{} for _, contentValue := range contentsSlice { findExpectedPaths(&foundPaths, expectedPaths, contentValue) } if allPresent := reflect.DeepEqual(foundPaths, expectedPaths); !allPresent { t.Fatalf("Found paths differed from expected paths. Got: %#v, want %#v", foundPaths, expectedPaths) } }) } } func TestGenerateRPCOrderPreservedMergeFiles(t *testing.T) { t.Parallel() const in1 = ` file_to_generate: "exampleproto/v1/example.proto" parameter: "output_format=yaml,allow_delete_body=true" proto_file: { name: "exampleproto/v1/example.proto" package: "example.v1" message_type: { name: "Foo" field: { name: "bar" number: 1 label: LABEL_OPTIONAL type: TYPE_STRING json_name: "bar" } } service: { name: "TestService" method: { name: "Test1" input_type: ".example.v1.Foo" output_type: ".example.v1.Foo" options: { [google.api.http]: { get: "/c/cpath" } } } method: { name: "Test2" input_type: ".example.v1.Foo" output_type: ".example.v1.Foo" options: { [google.api.http]: { get: "/b/bpath" } } } method: { name: "Test3" input_type: ".example.v1.Foo" output_type: ".example.v1.Foo" options: { [google.api.http]: { get: "/a/apath" } } } } options: { go_package: "exampleproto/v1;exampleproto" } }` const in2 = ` file_to_generate: "exampleproto/v2/example.proto" parameter: "output_format=yaml,allow_delete_body=true" proto_file: { name: "exampleproto/v2/example.proto" package: "example.v2" message_type: { name: "Foo" field: { name: "bar" number: 1 label: LABEL_OPTIONAL type: TYPE_STRING json_name: "bar" } } service: { name: "TestService" method: { name: "Test1" input_type: ".example.v2.Foo" output_type: ".example.v2.Foo" options: { [google.api.http]: { get: "/f/fpath" } } } method: { name: "Test2" input_type: ".example.v2.Foo" output_type: ".example.v2.Foo" options: { [google.api.http]: { get: "/e/epath" } } } method: { name: "Test3" input_type: ".example.v2.Foo" output_type: ".example.v2.Foo" options: { [google.api.http]: { get: "/d/dpath" } } } } options: { go_package: "exampleproto/v2;exampleproto" } }` var req1, req2 pluginpb.CodeGeneratorRequest if err := prototext.Unmarshal([]byte(in1), &req1); err != nil { t.Fatalf("failed to marshall yaml: %s", err) } if err := prototext.Unmarshal([]byte(in2), &req2); err != nil { t.Fatalf("failed to marshall yaml: %s", err) } req1.ProtoFile = append(req1.ProtoFile, req2.ProtoFile...) req1.FileToGenerate = append(req1.FileToGenerate, req2.FileToGenerate...) formats := [...]genopenapi.Format{ genopenapi.FormatJSON, genopenapi.FormatYAML, } for _, format := range formats { format := format t.Run(string(format), func(t *testing.T) { t.Parallel() resp := requireGenerate(t, &req1, format, true, true) if len(resp) != 1 { t.Fatalf("invalid count, expected: 1, actual: %d", len(resp)) } content := resp[0].GetContent() t.Log(content) contentsSlice := strings.Fields(content) expectedPaths := []string{"/c/cpath", "/b/bpath", "/a/apath", "/f/fpath", "/e/epath", "/d/dpath"} foundPaths := []string{} for _, contentValue := range contentsSlice { findExpectedPaths(&foundPaths, expectedPaths, contentValue) } if allPresent := reflect.DeepEqual(foundPaths, expectedPaths); !allPresent { t.Fatalf("Found paths differed from expected paths. Got: %#v, want %#v", foundPaths, expectedPaths) } }) } } func TestGenerateRPCOrderNotPreservedMergeFiles(t *testing.T) { t.Parallel() const in1 = ` file_to_generate: "exampleproto/v1/example.proto" parameter: "output_format=yaml,allow_delete_body=true" proto_file: { name: "exampleproto/v1/example.proto" package: "example.v1" message_type: { name: "Foo" field: { name: "bar" number: 1 label: LABEL_OPTIONAL type: TYPE_STRING json_name: "bar" } } service: { name: "TestService" method: { name: "Test1" input_type: ".example.v1.Foo" output_type: ".example.v1.Foo" options: { [google.api.http]: { get: "/c/cpath" } } } method: { name: "Test2" input_type: ".example.v1.Foo" output_type: ".example.v1.Foo" options: { [google.api.http]: { get: "/b/bpath" } } } method: { name: "Test3" input_type: ".example.v1.Foo" output_type: ".example.v1.Foo" options: { [google.api.http]: { get: "/a/apath" } } } } options: { go_package: "exampleproto/v1;exampleproto" } }` const in2 = ` file_to_generate: "exampleproto/v2/example.proto" parameter: "output_format=yaml,allow_delete_body=true" proto_file: { name: "exampleproto/v2/example.proto" package: "example.v2" message_type: { name: "Foo" field: { name: "bar" number: 1 label: LABEL_OPTIONAL type: TYPE_STRING json_name: "bar" } } service: { name: "TestService" method: { name: "Test1" input_type: ".example.v2.Foo" output_type: ".example.v2.Foo" options: { [google.api.http]: { get: "/f/fpath" } } } method: { name: "Test2" input_type: ".example.v2.Foo" output_type: ".example.v2.Foo" options: { [google.api.http]: { get: "/e/epath" } } } method: { name: "Test3" input_type: ".example.v2.Foo" output_type: ".example.v2.Foo" options: { [google.api.http]: { get: "/d/dpath" } } } } options: { go_package: "exampleproto/v2;exampleproto" } }` var req1, req2 pluginpb.CodeGeneratorRequest if err := prototext.Unmarshal([]byte(in1), &req1); err != nil { t.Fatalf("failed to marshall yaml: %s", err) } if err := prototext.Unmarshal([]byte(in2), &req2); err != nil { t.Fatalf("failed to marshall yaml: %s", err) } req1.ProtoFile = append(req1.ProtoFile, req2.ProtoFile...) req1.FileToGenerate = append(req1.FileToGenerate, req2.FileToGenerate...) formats := [...]genopenapi.Format{ genopenapi.FormatJSON, genopenapi.FormatYAML, } for _, format := range formats { format := format t.Run(string(format), func(t *testing.T) { t.Parallel() resp := requireGenerate(t, &req1, format, false, true) if len(resp) != 1 { t.Fatalf("invalid count, expected: 1, actual: %d", len(resp)) } content := resp[0].GetContent() t.Log(content) contentsSlice := strings.Fields(content) expectedPaths := []string{"/c/cpath", "/b/bpath", "/a/apath", "/f/fpath", "/e/epath", "/d/dpath"} sort.Strings(expectedPaths) foundPaths := []string{} for _, contentValue := range contentsSlice { findExpectedPaths(&foundPaths, expectedPaths, contentValue) } if allPresent := reflect.DeepEqual(foundPaths, expectedPaths); !allPresent { t.Fatalf("Found paths differed from expected paths. Got: %#v, want %#v", foundPaths, expectedPaths) } }) } } func TestGenerateRPCOrderPreservedAdditionalBindings(t *testing.T) { t.Parallel() const in = ` file_to_generate: "exampleproto/v1/example.proto" parameter: "output_format=yaml,allow_delete_body=true" proto_file: { name: "exampleproto/v1/example.proto" package: "example.v1" message_type: { name: "Foo" field: { name: "bar" number: 1 label: LABEL_OPTIONAL type: TYPE_STRING json_name: "bar" } } service: { name: "TestService" method: { name: "Test1" input_type: ".example.v1.Foo" output_type: ".example.v1.Foo" options: { [google.api.http]: { get: "/b/first" additional_bindings { get: "/a/additional" } } } } method: { name: "Test2" input_type: ".example.v1.Foo" output_type: ".example.v1.Foo" options: { [google.api.http]: { get: "/a/second" additional_bindings { get: "/z/zAdditional" } } } } method: { name: "Test3" input_type: ".example.v1.Foo" output_type: ".example.v1.Foo" options: { [google.api.http]: { get: "/c/third" additional_bindings { get: "/b/bAdditional" } } } } } options: { go_package: "exampleproto/v1;exampleproto" } }` var req pluginpb.CodeGeneratorRequest if err := prototext.Unmarshal([]byte(in), &req); err != nil { t.Fatalf("failed to marshall yaml: %s", err) } formats := [...]genopenapi.Format{ genopenapi.FormatJSON, genopenapi.FormatYAML, } for _, format := range formats { format := format t.Run(string(format), func(t *testing.T) { t.Parallel() resp := requireGenerate(t, &req, format, true, false) if len(resp) != 1 { t.Fatalf("invalid count, expected: 1, actual: %d", len(resp)) } content := resp[0].GetContent() t.Log(content) contentsSlice := strings.Fields(content) expectedPaths := []string{"/b/first", "/a/additional", "/a/second", "/z/zAdditional", "/c/third", "/b/bAdditional"} foundPaths := []string{} for _, contentValue := range contentsSlice { findExpectedPaths(&foundPaths, expectedPaths, contentValue) } if allPresent := reflect.DeepEqual(foundPaths, expectedPaths); !allPresent { t.Fatalf("Found paths differed from expected paths. Got: %#v, want %#v", foundPaths, expectedPaths) } }) } } func TestGenerateRPCOneOfFieldBodyAdditionalBindings(t *testing.T) { t.Parallel() const in = ` file_to_generate: "exampleproto/v1/example.proto" parameter: "output_format=yaml,allow_delete_body=true" proto_file: { name: "exampleproto/v1/example.proto" package: "example.v1" message_type: { name: "Foo" oneof_decl: { name: "foo" } field: { name: "bar" number: 1 label: LABEL_OPTIONAL type: TYPE_STRING json_name: "bar" oneof_index: 0 } field: { name: "baz" number: 2 label: LABEL_OPTIONAL type: TYPE_STRING json_name: "bar" oneof_index: 0 } } service: { name: "TestService" method: { name: "Test1" input_type: ".example.v1.Foo" output_type: ".example.v1.Foo" options: { [google.api.http]: { post: "/b/foo" body: "*" additional_bindings { post: "/b/foo/bar" body: "bar" } additional_bindings { post: "/b/foo/baz" body: "baz" } } } } } options: { go_package: "exampleproto/v1;exampleproto" } }` var req pluginpb.CodeGeneratorRequest if err := prototext.Unmarshal([]byte(in), &req); err != nil { t.Fatalf("failed to marshall yaml: %s", err) } formats := [...]genopenapi.Format{ genopenapi.FormatJSON, genopenapi.FormatYAML, } for _, format := range formats { format := format t.Run(string(format), func(t *testing.T) { t.Parallel() resp := requireGenerate(t, &req, format, true, false) if len(resp) != 1 { t.Fatalf("invalid count, expected: 1, actual: %d", len(resp)) } content := resp[0].GetContent() t.Log(content) contentsSlice := strings.Fields(content) expectedPaths := []string{"/b/foo", "/b/foo/bar", "/b/foo/baz"} foundPaths := []string{} for _, contentValue := range contentsSlice { findExpectedPaths(&foundPaths, expectedPaths, contentValue) } if allPresent := reflect.DeepEqual(foundPaths, expectedPaths); !allPresent { t.Fatalf("Found paths differed from expected paths. Got: %#v, want %#v", foundPaths, expectedPaths) } // The input message only contains oneof fields, so no other fields should be mapped to the query. if strings.Contains(content, "query") { t.Fatalf("Found query in content, expected not to find any") } }) } } func TestGenerateRPCOrderNotPreservedAdditionalBindings(t *testing.T) { t.Parallel() const in = ` file_to_generate: "exampleproto/v1/example.proto" parameter: "output_format=yaml,allow_delete_body=true" proto_file: { name: "exampleproto/v1/example.proto" package: "example.v1" message_type: { name: "Foo" field: { name: "bar" number: 1 label: LABEL_OPTIONAL type: TYPE_STRING json_name: "bar" } } service: { name: "TestService" method: { name: "Test1" input_type: ".example.v1.Foo" output_type: ".example.v1.Foo" options: { [google.api.http]: { get: "/b/first" additional_bindings { get: "/a/additional" } } } } method: { name: "Test2" input_type: ".example.v1.Foo" output_type: ".example.v1.Foo" options: { [google.api.http]: { get: "/a/second" additional_bindings { get: "/z/zAdditional" } } } } method: { name: "Test3" input_type: ".example.v1.Foo" output_type: ".example.v1.Foo" options: { [google.api.http]: { get: "/c/third" additional_bindings { get: "/b/bAdditional" } } } } } options: { go_package: "exampleproto/v1;exampleproto" } }` var req pluginpb.CodeGeneratorRequest if err := prototext.Unmarshal([]byte(in), &req); err != nil { t.Fatalf("failed to marshall yaml: %s", err) } formats := [...]genopenapi.Format{ genopenapi.FormatJSON, genopenapi.FormatYAML, } for _, format := range formats { format := format t.Run(string(format), func(t *testing.T) { t.Parallel() resp := requireGenerate(t, &req, format, false, false) if len(resp) != 1 { t.Fatalf("invalid count, expected: 1, actual: %d", len(resp)) } content := resp[0].GetContent() t.Log(content) contentsSlice := strings.Fields(content) expectedPaths := []string{"/b/first", "/a/additional", "/a/second", "/z/zAdditional", "/c/third", "/b/bAdditional"} sort.Strings(expectedPaths) foundPaths := []string{} for _, contentValue := range contentsSlice { findExpectedPaths(&foundPaths, expectedPaths, contentValue) } if allPresent := reflect.DeepEqual(foundPaths, expectedPaths); !allPresent { t.Fatalf("Found paths differed from expected paths. Got: %#v, want %#v", foundPaths, expectedPaths) } }) } } func TestGenerateRPCOrderPreservedMergeFilesAdditionalBindingsMultipleServices(t *testing.T) { t.Parallel() const in1 = ` file_to_generate: "exampleproto/v1/example.proto" parameter: "output_format=yaml,allow_delete_body=true" proto_file: { name: "exampleproto/v1/example.proto" package: "example.v1" message_type: { name: "Foo" field: { name: "bar" number: 1 label: LABEL_OPTIONAL type: TYPE_STRING json_name: "bar" } } service: { name: "TestServiceOne" method: { name: "Test1" input_type: ".example.v1.Foo" output_type: ".example.v1.Foo" options: { [google.api.http]: { get: "/d/first" } } } method: { name: "Test2" input_type: ".example.v1.Foo" output_type: ".example.v1.Foo" options: { [google.api.http]: { get: "/e/second" } } } method: { name: "Test3" input_type: ".example.v1.Foo" output_type: ".example.v1.Foo" options: { [google.api.http]: { get: "/c/third" } } } } service: { name: "TestServiceTwo" method: { name: "Test1" input_type: ".example.v1.Foo" output_type: ".example.v1.Foo" options: { [google.api.http]: { get: "/b/first" } } } method: { name: "Test2" input_type: ".example.v1.Foo" output_type: ".example.v1.Foo" options: { [google.api.http]: { get: "/a/second" } } } method: { name: "Test3" input_type: ".example.v1.Foo" output_type: ".example.v1.Foo" options: { [google.api.http]: { get: "/g/third" } } } } options: { go_package: "exampleproto/v1;exampleproto" } }` const in2 = ` file_to_generate: "exampleproto/v2/example.proto" parameter: "output_format=yaml,allow_delete_body=true" proto_file: { name: "exampleproto/v2/example.proto" package: "example.v2" message_type: { name: "Foo" field: { name: "bar" number: 1 label: LABEL_OPTIONAL type: TYPE_STRING json_name: "bar" } } service: { name: "TestService" method: { name: "Test1" input_type: ".example.v2.Foo" output_type: ".example.v2.Foo" options: { [google.api.http]: { get: "/b/bpath" additional_bindings { get: "/a/additional" } } } } method: { name: "Test2" input_type: ".example.v2.Foo" output_type: ".example.v2.Foo" options: { [google.api.http]: { get: "/a/apath" additional_bindings { get: "/z/zAdditional" } } } } method: { name: "Test3" input_type: ".example.v2.Foo" output_type: ".example.v2.Foo" options: { [google.api.http]: { get: "/c/cpath" additional_bindings { get: "/b/bAdditional" } } } } } options: { go_package: "exampleproto/v2;exampleproto" } }` var req1, req2 pluginpb.CodeGeneratorRequest if err := prototext.Unmarshal([]byte(in1), &req1); err != nil { t.Fatalf("failed to marshall yaml: %s", err) } if err := prototext.Unmarshal([]byte(in2), &req2); err != nil { t.Fatalf("failed to marshall yaml: %s", err) } req1.ProtoFile = append(req1.ProtoFile, req2.ProtoFile...) req1.FileToGenerate = append(req1.FileToGenerate, req2.FileToGenerate...) formats := [...]genopenapi.Format{ genopenapi.FormatJSON, genopenapi.FormatYAML, } for _, format := range formats { format := format t.Run(string(format), func(t *testing.T) { t.Parallel() resp := requireGenerate(t, &req1, format, true, true) if len(resp) != 1 { t.Fatalf("invalid count, expected: 1, actual: %d", len(resp)) } content := resp[0].GetContent() t.Log(content) contentsSlice := strings.Fields(content) expectedPaths := []string{"/d/first", "/e/second", "/c/third", "/b/first", "/a/second", "/g/third", "/b/bpath", "/a/additional", "/a/apath", "/z/zAdditional", "/c/cpath", "/b/bAdditional"} foundPaths := []string{} for _, contentValue := range contentsSlice { findExpectedPaths(&foundPaths, expectedPaths, contentValue) } if allPresent := reflect.DeepEqual(foundPaths, expectedPaths); !allPresent { t.Fatalf("Found paths differed from expected paths. Got: %#v, want %#v", foundPaths, expectedPaths) } }) } } func TestGenerateRPCOrderNotPreservedMergeFilesAdditionalBindingsMultipleServices(t *testing.T) { t.Parallel() const in1 = ` file_to_generate: "exampleproto/v1/example.proto" parameter: "output_format=yaml,allow_delete_body=true" proto_file: { name: "exampleproto/v1/example.proto" package: "example.v1" message_type: { name: "Foo" field: { name: "bar" number: 1 label: LABEL_OPTIONAL type: TYPE_STRING json_name: "bar" } } service: { name: "TestServiceOne" method: { name: "Test1" input_type: ".example.v1.Foo" output_type: ".example.v1.Foo" options: { [google.api.http]: { get: "/d/first" } } } method: { name: "Test2" input_type: ".example.v1.Foo" output_type: ".example.v1.Foo" options: { [google.api.http]: { get: "/e/second" } } } method: { name: "Test3" input_type: ".example.v1.Foo" output_type: ".example.v1.Foo" options: { [google.api.http]: { get: "/c/third" } } } } service: { name: "TestServiceTwo" method: { name: "Test1" input_type: ".example.v1.Foo" output_type: ".example.v1.Foo" options: { [google.api.http]: { get: "/b/first" } } } method: { name: "Test2" input_type: ".example.v1.Foo" output_type: ".example.v1.Foo" options: { [google.api.http]: { get: "/a/second" } } } method: { name: "Test3" input_type: ".example.v1.Foo" output_type: ".example.v1.Foo" options: { [google.api.http]: { get: "/g/third" } } } } options: { go_package: "exampleproto/v1;exampleproto" } }` const in2 = ` file_to_generate: "exampleproto/v2/example.proto" parameter: "output_format=yaml,allow_delete_body=true" proto_file: { name: "exampleproto/v2/example.proto" package: "example.v2" message_type: { name: "Foo" field: { name: "bar" number: 1 label: LABEL_OPTIONAL type: TYPE_STRING json_name: "bar" } } service: { name: "TestService" method: { name: "Test1" input_type: ".example.v2.Foo" output_type: ".example.v2.Foo" options: { [google.api.http]: { get: "/b/bpath" additional_bindings { get: "/a/additional" } } } } method: { name: "Test2" input_type: ".example.v2.Foo" output_type: ".example.v2.Foo" options: { [google.api.http]: { get: "/a/apath" additional_bindings { get: "/z/zAdditional" } } } } method: { name: "Test3" input_type: ".example.v2.Foo" output_type: ".example.v2.Foo" options: { [google.api.http]: { get: "/c/cpath" additional_bindings { get: "/b/bAdditional" } } } } } options: { go_package: "exampleproto/v2;exampleproto" } }` var req1, req2 pluginpb.CodeGeneratorRequest if err := prototext.Unmarshal([]byte(in1), &req1); err != nil { t.Fatalf("failed to marshall yaml: %s", err) } if err := prototext.Unmarshal([]byte(in2), &req2); err != nil { t.Fatalf("failed to marshall yaml: %s", err) } req1.ProtoFile = append(req1.ProtoFile, req2.ProtoFile...) req1.FileToGenerate = append(req1.FileToGenerate, req2.FileToGenerate...) formats := [...]genopenapi.Format{ genopenapi.FormatJSON, genopenapi.FormatYAML, } for _, format := range formats { format := format t.Run(string(format), func(t *testing.T) { t.Parallel() resp := requireGenerate(t, &req1, format, false, true) if len(resp) != 1 { t.Fatalf("invalid count, expected: 1, actual: %d", len(resp)) } content := resp[0].GetContent() t.Log(content) contentsSlice := strings.Fields(content) expectedPaths := []string{"/d/first", "/e/second", "/c/third", "/b/first", "/a/second", "/g/third", "/b/bpath", "/a/additional", "/a/apath", "/z/zAdditional", "/c/cpath", "/b/bAdditional"} sort.Strings(expectedPaths) foundPaths := []string{} for _, contentValue := range contentsSlice { findExpectedPaths(&foundPaths, expectedPaths, contentValue) } if allPresent := reflect.DeepEqual(foundPaths, expectedPaths); !allPresent { t.Fatalf("Found paths differed from expected paths. Got: %#v, want %#v", foundPaths, expectedPaths) } }) } } // Tries to find expected paths from a provided substring and store them in the foundPaths // slice. func findExpectedPaths(foundPaths *[]string, expectedPaths []string, potentialPath string) { seenPaths := map[string]struct{}{} // foundPaths may not be empty when this function is called multiple times, // so we add them to seenPaths map to avoid duplicates. for _, path := range *foundPaths { seenPaths[path] = struct{}{} } for _, path := range expectedPaths { _, pathAlreadySeen := seenPaths[path] if strings.Contains(potentialPath, path) && !pathAlreadySeen { *foundPaths = append(*foundPaths, path) seenPaths[path] = struct{}{} } } } func TestFindExpectedPaths(t *testing.T) { t.Parallel() testCases := [...]struct { testName string requiredPaths []string potentialPath string expectedPathsFound []string }{ { testName: "One potential path present", requiredPaths: []string{"/d/first", "/e/second", "/c/third", "/b/first"}, potentialPath: "[{\"path: \"/d/first\"", expectedPathsFound: []string{"/d/first"}, }, { testName: "No potential Paths present", requiredPaths: []string{"/d/first", "/e/second", "/c/third", "/b/first"}, potentialPath: "[{\"path: \"/z/zpath\"", expectedPathsFound: []string{}, }, { testName: "Multiple potential paths present", requiredPaths: []string{"/d/first", "/e/second", "/c/third", "/b/first", "/d/first"}, potentialPath: "[{\"path: \"/d/first\"someData\"/c/third\"someData\"/b/third\"", expectedPathsFound: []string{"/d/first", "/c/third"}, }, } for _, tc := range testCases { tc := tc t.Run(tc.testName, func(t *testing.T) { t.Parallel() foundPaths := []string{} findExpectedPaths(&foundPaths, tc.requiredPaths, tc.potentialPath) if correctPathsFound := reflect.DeepEqual(foundPaths, tc.expectedPathsFound); !correctPathsFound { t.Fatalf("Found paths differed from expected paths. Got: %#v, want %#v", foundPaths, tc.expectedPathsFound) } }) } }