diff --git a/extensions/collector/go.mod b/extensions/collector/go.mod index 48d5ea82..d585f788 100644 --- a/extensions/collector/go.mod +++ b/extensions/collector/go.mod @@ -4,10 +4,11 @@ go 1.19 require ( github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819 + github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2 github.com/linuxsuren/api-testing v0.0.11 github.com/spf13/cobra v1.7.0 github.com/stretchr/testify v1.8.4 - gopkg.in/yaml.v2 v2.4.0 + gopkg.in/yaml.v3 v3.0.1 ) require ( @@ -20,19 +21,18 @@ require ( github.com/huandu/xstrings v1.3.3 // indirect github.com/imdario/mergo v0.3.11 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/kr/pretty v0.1.0 // indirect github.com/mitchellh/copystructure v1.0.0 // indirect github.com/mitchellh/reflectwalk v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/shopspring/decimal v1.2.0 // indirect - github.com/spf13/cast v1.3.1 // indirect + github.com/spf13/cast v1.5.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect - golang.org/x/crypto v0.3.0 // indirect + golang.org/x/crypto v0.14.0 // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect ) replace github.com/linuxsuren/api-testing => ../../. diff --git a/extensions/collector/go.sum b/extensions/collector/go.sum index 946ed0cd..0e4bb206 100644 --- a/extensions/collector/go.sum +++ b/extensions/collector/go.sum @@ -12,22 +12,23 @@ github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819 h1:RIB4cRk+lBqKK3O github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2 h1:dWB6v3RcOy03t/bUadywsbyrQwCqZeNIEX6M1OtSZOM= github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8= +github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE= +github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4= github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= @@ -35,11 +36,13 @@ github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= +github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= @@ -59,8 +62,9 @@ github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= diff --git a/extensions/collector/pkg/exporter.go b/extensions/collector/pkg/exporter.go index 7a5be2ea..ac7fbfdc 100644 --- a/extensions/collector/pkg/exporter.go +++ b/extensions/collector/pkg/exporter.go @@ -7,7 +7,7 @@ import ( "github.com/linuxsuren/api-testing/pkg/testing" atestpkg "github.com/linuxsuren/api-testing/pkg/testing" - "gopkg.in/yaml.v2" + "gopkg.in/yaml.v3" ) // SampleExporter is a sample exporter diff --git a/extensions/collector/pkg/exporter_test.go b/extensions/collector/pkg/exporter_test.go index 20293a0b..a338729f 100644 --- a/extensions/collector/pkg/exporter_test.go +++ b/extensions/collector/pkg/exporter_test.go @@ -31,7 +31,7 @@ func TestSampleExporter(t *testing.T) { var result string result, err = exporter.Export() assert.NoError(t, err) - assert.Equal(t, sampleSuite, result) + assert.Equal(t, sampleSuite, result, result) } func newRequest() (request *http.Request, err error) { diff --git a/extensions/collector/pkg/testdata/sample_suite.yaml b/extensions/collector/pkg/testdata/sample_suite.yaml index c3a69b01..601f9d72 100644 --- a/extensions/collector/pkg/testdata/sample_suite.yaml +++ b/extensions/collector/pkg/testdata/sample_suite.yaml @@ -2,22 +2,22 @@ # yaml-language-server: $schema=https://linuxsuren.github.io/api-testing/api-testing-schema.json name: sample items: -- name: v1 - request: - api: http://foo/api/v1 - method: GET - header: - Authorization: Bearer token - Content-Type: application/json - body: hello -- name: v1-1 - request: - api: http://foo/api/v1 - method: GET - header: - Authorization: Bearer token - Content-Type: application/json - body: hello - expect: - statusCode: 200 - body: hello + - name: v1 + request: + api: http://foo/api/v1 + method: GET + header: + Authorization: Bearer token + Content-Type: application/json + body: hello + - name: v1-1 + request: + api: http://foo/api/v1 + method: GET + header: + Authorization: Bearer token + Content-Type: application/json + body: hello + expect: + statusCode: 200 + body: hello diff --git a/pkg/generator/code_generator.go b/pkg/generator/code_generator.go index 91972c0e..64764e36 100644 --- a/pkg/generator/code_generator.go +++ b/pkg/generator/code_generator.go @@ -28,7 +28,7 @@ import "github.com/linuxsuren/api-testing/pkg/testing" // CodeGenerator is the interface of code generator type CodeGenerator interface { - Generate(testcase *testing.TestCase) (result string, err error) + Generate(testSuite *testing.TestSuite, testcase *testing.TestCase) (result string, err error) } var codeGenerators = map[string]CodeGenerator{} diff --git a/pkg/generator/code_generator_test.go b/pkg/generator/code_generator_test.go index ebc6c640..adcdf020 100644 --- a/pkg/generator/code_generator_test.go +++ b/pkg/generator/code_generator_test.go @@ -61,7 +61,7 @@ func TestGenerators(t *testing.T) { }, } t.Run("golang", func(t *testing.T) { - result, err := generator.GetCodeGenerator("golang").Generate(testcase) + result, err := generator.GetCodeGenerator("golang").Generate(nil, testcase) assert.NoError(t, err) assert.Equal(t, expectedGoCode, result) }) @@ -71,7 +71,7 @@ func TestGenerators(t *testing.T) { "key": "value", } t.Run("golang form HTTP request", func(t *testing.T) { - result, err := generator.GetCodeGenerator("golang").Generate(formRequest) + result, err := generator.GetCodeGenerator("golang").Generate(nil, formRequest) assert.NoError(t, err) assert.Equal(t, expectedFormRequestGoCode, result, result) }) diff --git a/pkg/generator/curl_generator.go b/pkg/generator/curl_generator.go index 2a926d5f..719a200e 100644 --- a/pkg/generator/curl_generator.go +++ b/pkg/generator/curl_generator.go @@ -41,7 +41,7 @@ func NewCurlGenerator() CodeGenerator { return &curlGenerator{} } -func (g *curlGenerator) Generate(testcase *testing.TestCase) (result string, err error) { +func (g *curlGenerator) Generate(testSuite *testing.TestSuite, testcase *testing.TestCase) (result string, err error) { if testcase.Request.Method == "" { testcase.Request.Method = http.MethodGet } diff --git a/pkg/generator/curl_generator_test.go b/pkg/generator/curl_generator_test.go index 7a0a1447..d46051df 100644 --- a/pkg/generator/curl_generator_test.go +++ b/pkg/generator/curl_generator_test.go @@ -94,7 +94,7 @@ func TestCurlGenerator(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { g := &curlGenerator{} - if got, err := g.Generate(&tt.testCase); err != nil || got != tt.expect { + if got, err := g.Generate(nil, &tt.testCase); err != nil || got != tt.expect { t.Errorf("Generate() = %v, want %v", got, tt.expect) } }) diff --git a/pkg/generator/golang_generator.go b/pkg/generator/golang_generator.go index d3b59f40..c7fc0b91 100644 --- a/pkg/generator/golang_generator.go +++ b/pkg/generator/golang_generator.go @@ -41,7 +41,7 @@ func NewGolangGenerator() CodeGenerator { return &golangGenerator{} } -func (g *golangGenerator) Generate(testcase *testing.TestCase) (result string, err error) { +func (g *golangGenerator) Generate(testSuite *testing.TestSuite, testcase *testing.TestCase) (result string, err error) { if testcase.Request.Method == "" { testcase.Request.Method = http.MethodGet } diff --git a/pkg/generator/payload_generator.go b/pkg/generator/payload_generator.go new file mode 100644 index 00000000..8cff738e --- /dev/null +++ b/pkg/generator/payload_generator.go @@ -0,0 +1,209 @@ +/** +MIT License + +Copyright (c) 2023 API Testing Authors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +package generator + +import ( + "context" + "encoding/json" + "io" + "math/rand" + "path/filepath" + "strings" + + "github.com/bufbuild/protocompile" + "github.com/linuxsuren/api-testing/pkg/testing" + "github.com/linuxsuren/api-testing/pkg/util" + "google.golang.org/protobuf/reflect/protoreflect" +) + +type grpcPayloadGenerator struct { +} + +func NewGrpcPayloadGenerator() CodeGenerator { + return &grpcPayloadGenerator{} +} + +func (g *grpcPayloadGenerator) Generate(testSuite *testing.TestSuite, testcase *testing.TestCase) (result string, err error) { + result, err = generateGRPCPayloadAsJSON(testSuite.Spec.RPC, + parseGRPCService(strings.TrimPrefix(testcase.Request.API, testSuite.API))) + return +} + +func parseGRPCService(service string) string { + service = strings.TrimPrefix(service, "/") + return strings.ReplaceAll(service, "/", ".") +} + +func generateGRPCPayloadAsJSON(rpc *testing.RPCDesc, service string) (resultJSON string, err error) { + protoFile := rpc.ProtoFile + protoContent := rpc.Raw + + if protoFile == "" { + // don't really need a regular file, just give it a name + protoFile = "placeholder.proto" + } + + var ( + importPath []string + parentProtoDir string + ) + protoFile, importPath, parentProtoDir, err = util.LoadProtoFiles(protoFile) + if err != nil { + return + } + + if len(importPath) == 0 { + importPath = rpc.ImportPath + } + + if parentProtoDir != "" { + for i, p := range importPath { + importPath[i] = filepath.Join(parentProtoDir, p) + } + if len(importPath) == 0 { + importPath = append(importPath, parentProtoDir) + } + } + + var accessor func(path string) (io.ReadCloser, error) + if protoContent != "" { + accessor = protocompile.SourceAccessorFromMap(map[string]string{ + protoFile: protoContent, + }) + } + + compiler := protocompile.Compiler{ + Resolver: protocompile.WithStandardImports( + &protocompile.SourceResolver{ + Accessor: accessor, + ImportPaths: importPath, + }, + ), + SourceInfoMode: protocompile.SourceInfoStandard, + } + + files, err := compiler.Compile(context.TODO(), protoFile) + if err != nil { + return "", err + } + + dp, err := files.AsResolver().FindDescriptorByName(protoreflect.FullName(service)) + if err != nil { + return "", err + } + + randFuncMap := map[protoreflect.Kind]func(md protoreflect.FieldDescriptor) any{} + randFuncMap[protoreflect.Int32Kind] = func(md protoreflect.FieldDescriptor) any { + if md.IsList() { + return []int{rand.Intn(100), rand.Intn(100), rand.Intn(100)} + } + return rand.Intn(100) + } + randFuncMap[protoreflect.Uint32Kind] = func(md protoreflect.FieldDescriptor) any { + if md.IsList() { + return []int{rand.Intn(100), rand.Intn(100), rand.Intn(100)} + } + return rand.Intn(100) + } + randFuncMap[protoreflect.Int64Kind] = func(md protoreflect.FieldDescriptor) any { + if md.IsList() { + return []int{rand.Intn(100), rand.Intn(100), rand.Intn(100)} + } + return rand.Intn(100) + } + randFuncMap[protoreflect.Uint64Kind] = func(md protoreflect.FieldDescriptor) any { + if md.IsList() { + return []int{rand.Intn(100), rand.Intn(100), rand.Intn(100)} + } + return rand.Intn(100) + } + randFuncMap[protoreflect.FloatKind] = func(md protoreflect.FieldDescriptor) any { + if md.IsList() { + return []float32{rand.Float32(), rand.Float32(), rand.Float32()} + } + return rand.Float32() + } + randFuncMap[protoreflect.DoubleKind] = func(md protoreflect.FieldDescriptor) any { + if md.IsList() { + return []float64{rand.Float64(), rand.Float64(), rand.Float64()} + } + return rand.Float64() + } + randFuncMap[protoreflect.BoolKind] = func(md protoreflect.FieldDescriptor) any { + if md.IsList() { + return []bool{true, false, true} + } + return true + } + randFuncMap[protoreflect.StringKind] = func(md protoreflect.FieldDescriptor) any { + if md.IsList() { + return []string{"xxx", "yyy", "zzz"} + } + return "xxx" + } + randFuncMap[protoreflect.EnumKind] = func(md protoreflect.FieldDescriptor) any { + enums := md.Enum().Values() + return enums.Get(rand.Intn(enums.Len())).Name() + } + randFuncMap[protoreflect.MessageKind] = func(md protoreflect.FieldDescriptor) any { + result := map[string]any{} + if md.IsMap() { + key := randFuncMap[md.MapKey().Kind()](md.MapKey()) + if strKey, ok := key.(string); ok { + result[strKey] = randFuncMap[md.MapValue().Kind()](md.MapValue()) + } + } else { + for i := 0; i < md.Message().Fields().Len(); i++ { + field := md.Message().Fields().Get(i) + randFunc := randFuncMap[field.Kind()] + if randFunc != nil { + result[field.JSONName()] = randFunc(field) + } + } + } + return result + } + + data := map[string]any{} + abc := dp.(protoreflect.MethodDescriptor) + for i := 0; i < abc.Input().Fields().Len(); i++ { + field := abc.Input().Fields().Get(i) + randFunc := randFuncMap[field.Kind()] + if randFunc != nil { + data[string(field.Name())] = randFunc(field) + } + } + + var result []byte + result, err = json.Marshal(data) + if err == nil { + resultJSON = string(result) + } + return +} + +func init() { + RegisterCodeGenerator("gRPCPayload", NewGrpcPayloadGenerator()) +} diff --git a/pkg/generator/payload_generator_test.go b/pkg/generator/payload_generator_test.go new file mode 100644 index 00000000..08133a23 --- /dev/null +++ b/pkg/generator/payload_generator_test.go @@ -0,0 +1,139 @@ +/** +MIT License + +Copyright (c) 2023 API Testing Authors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +package generator + +import ( + "encoding/json" + "testing" + + _ "embed" + + atest "github.com/linuxsuren/api-testing/pkg/testing" + "github.com/linuxsuren/api-testing/pkg/util" + "github.com/stretchr/testify/assert" +) + +func TestGenerateJSON(t *testing.T) { + t.Run("TestAdvancedType", func(t *testing.T) { + result, err := generateGRPCPayloadAsJSON(&atest.RPCDesc{ + ProtoFile: "testdata/test.proto", + }, "grpctest.Main.TestAdvancedType") + if err != nil { + t.Fatal(err) + } + + obj := map[string]interface{}{} + err = json.Unmarshal([]byte(result), &obj) + if assert.NoError(t, err) { + keys := util.Keys(obj) + assert.ElementsMatch(t, keys, + []string{"Int32Array", "Int64Array", "Uint32Array", "Uint64Array", + "Float32Array", "Float64Array", "StringArray", "BoolArray", "HelloReplyMap", "Protocol"}) + } + }) + + t.Run("TestBasicType", func(t *testing.T) { + result, err := generateGRPCPayloadAsJSON(&atest.RPCDesc{ + ProtoFile: "testdata/test.proto", + }, "grpctest.Main.TestBasicType") + if err != nil { + t.Fatal(err) + } + + obj := map[string]interface{}{} + err = json.Unmarshal([]byte(result), &obj) + if assert.NoError(t, err) { + keys := util.Keys(obj) + assert.ElementsMatch(t, keys, + []string{"Int32", "Int64", "Uint32", "Uint64", + "Float32", "Float64", "String", "Bool"}) + } + }) + + t.Run("ClientStream", func(t *testing.T) { + result, err := generateGRPCPayloadAsJSON(&atest.RPCDesc{ + ProtoFile: "testdata/test.proto", + Raw: protoContent, + }, "grpctest.Main.ClientStream") + if err != nil { + t.Fatal(err) + } + + obj := map[string]interface{}{} + err = json.Unmarshal([]byte(result), &obj) + if assert.NoError(t, err) { + keys := util.Keys(obj) + assert.ElementsMatch(t, keys, + []string{"MsgID", "ExpectLen"}) + } + }) + + t.Run("BidStream, no proto file give, only file content", func(t *testing.T) { + result, err := generateGRPCPayloadAsJSON(&atest.RPCDesc{ + Raw: protoContent, + }, "grpctest.Main.BidStream") + if err != nil { + t.Fatal(err) + } + + obj := map[string]interface{}{} + err = json.Unmarshal([]byte(result), &obj) + if assert.NoError(t, err) { + keys := util.Keys(obj) + assert.ElementsMatch(t, keys, + []string{"MsgID", "ExpectLen"}) + } + }) + + t.Run("call the generate method", func(t *testing.T) { + generator := NewGrpcPayloadGenerator() + result, err := generator.Generate(&atest.TestSuite{ + API: "localhost:7070", + Spec: atest.APISpec{ + RPC: &atest.RPCDesc{ + ProtoFile: "testdata/test.proto", + }, + }, + }, &atest.TestCase{ + Request: atest.Request{ + API: "/grpctest.Main/ServerStream", + }, + }) + if err != nil { + t.Fatal(err) + } + + obj := map[string]interface{}{} + err = json.Unmarshal([]byte(result), &obj) + if assert.NoError(t, err) { + keys := util.Keys(obj) + assert.ElementsMatch(t, keys, + []string{"data"}) + } + }) +} + +//go:embed testdata/test.proto +var protoContent string diff --git a/pkg/generator/testdata/test.proto b/pkg/generator/testdata/test.proto new file mode 100644 index 00000000..c00a46fc --- /dev/null +++ b/pkg/generator/testdata/test.proto @@ -0,0 +1,61 @@ +syntax = "proto3"; + +option go_package = "github.com/linuxsuren/api-testing/pkg/runner/grpc_test"; + +package grpctest; + +service Main { + rpc Unary(Empty) returns (HelloReply); + rpc ClientStream(stream StreamMessage) returns (StreamMessageRepeated); + rpc ServerStream(StreamMessageRepeated) returns (stream StreamMessage); + rpc BidStream(stream StreamMessage) returns (stream StreamMessage); + + rpc TestBasicType(BasicType) returns (BasicType); + rpc TestAdvancedType(AdvancedType) returns (AdvancedType); +}; + +message Empty { +} + +message HelloReply { + string message = 1; +} + +message StreamMessage{ + int32 MsgID = 1; + int32 ExpectLen =2; +} +message StreamMessageRepeated{ + repeated StreamMessage data = 1; +} + +message BasicType { + int32 Int32 = 1; + int64 Int64 = 2; + uint32 Uint32 = 3; + uint64 Uint64 = 4; + float Float32 = 5; + double Float64 = 6; + string String = 7; + bool Bool = 8; +} + +message AdvancedType { + repeated int32 Int32Array = 1; + repeated int64 Int64Array = 2; + repeated uint32 Uint32Array = 3; + repeated uint64 Uint64Array = 4; + repeated float Float32Array = 5; + repeated double Float64Array = 6; + + repeated string StringArray = 7; + repeated bool BoolArray = 8; + + map HelloReplyMap = 9; + Protocol Protocol = 10; +} + +enum Protocol { + HTTP = 0; + gRPC = 1; +} diff --git a/pkg/runner/grpc.go b/pkg/runner/grpc.go index 20c4715e..ae337e75 100644 --- a/pkg/runner/grpc.go +++ b/pkg/runner/grpc.go @@ -260,7 +260,7 @@ func getByProto(ctx context.Context, r *gRPCTestCaseRunner, fullName protoreflec return getByProtoSet(ctx, r, fullName) } - protoFile, importPath, parentProtoDir, err := loadProtoFiles(r.proto.ProtoFile) + protoFile, importPath, parentProtoDir, err := util.LoadProtoFiles(r.proto.ProtoFile) if err != nil { return nil, err } diff --git a/pkg/runner/grpc_test.go b/pkg/runner/grpc_test.go index d6428f2f..fd46d6bd 100644 --- a/pkg/runner/grpc_test.go +++ b/pkg/runner/grpc_test.go @@ -26,9 +26,7 @@ import ( "log" "math/rand" "net" - "net/http" "os" - "strings" "sync" "testing" "time" @@ -38,7 +36,6 @@ import ( "github.com/h2non/gock" testsrv "github.com/linuxsuren/api-testing/pkg/runner/grpc_test" atest "github.com/linuxsuren/api-testing/pkg/testing" - "github.com/linuxsuren/api-testing/pkg/util" fakeruntime "github.com/linuxsuren/go-fake-runtime" "github.com/stretchr/testify/assert" "google.golang.org/grpc" @@ -610,67 +607,6 @@ func TestAPINameMatch(t *testing.T) { ) } -func TestLoadProtoFiles(t *testing.T) { - t.Run("plain string proto file", func(t *testing.T) { - targetProtoFile, importPath, _, _ := loadProtoFiles("test.proto") - assert.Equal(t, "test.proto", targetProtoFile) - assert.Empty(t, importPath) - }) - - t.Run("URL with invalid status code", func(t *testing.T) { - defer gock.Clean() - gock.New("http://localhost").Get("/test.proto").Reply(http.StatusNotFound) - - _, _, _, err := loadProtoFiles("http://localhost/test.proto") - assert.Error(t, err) - }) - - t.Run("single file URL", func(t *testing.T) { - defer gock.Clean() - gock.New("http://localhost").Get("/test.proto"). - MatchParam("rand", "123"). - Reply(http.StatusOK). - File("grpc_test/test.proto") - - targetProtoFile, importPath, _, err := loadProtoFiles("http://localhost/test.proto?rand=123") - defer os.Remove(targetProtoFile) - - assert.True(t, strings.HasPrefix(targetProtoFile, os.TempDir()), targetProtoFile) - assert.Empty(t, importPath) - assert.NoError(t, err) - }) - - t.Run("URL with zip file, the query is missing", func(t *testing.T) { - defer gock.Clean() - gock.New("http://localhost").Get("/test.proto"). - MatchParam("rand", "234"). - Reply(http.StatusOK). - AddHeader(util.ContentType, util.ZIP) - - _, _, _, err := loadProtoFiles("http://localhost/test.proto?rand=234") - assert.Error(t, err) - }) - - t.Run("URL with zip file", func(t *testing.T) { - defer gock.Clean() - gock.New("http://localhost").Get("/test.proto"). - MatchParam("file", "testdata/report.html"). - Reply(http.StatusOK). - AddHeader(util.ContentType, util.ZIP). - AddHeader(util.ContentDisposition, "attachment; filename=test.zip"). - File("testdata/test.zip") - - _, _, targetProtoFileDir, err := loadProtoFiles("http://localhost/test.proto?file=testdata/report.html") - defer os.RemoveAll(targetProtoFileDir) - - assert.True(t, strings.HasPrefix(targetProtoFileDir, os.TempDir())) - assert.NoError(t, err) - }) - - assert.Error(t, extractFiles("a", "", "")) - assert.Error(t, extractFiles("", "b", "")) -} - // getJSONOrCache can store the JSON string of value. // // Let key be nil represent not using cache. diff --git a/pkg/runner/grpc_test/test.proto b/pkg/runner/grpc_test/test.proto index ca343c6d..c00a46fc 100644 --- a/pkg/runner/grpc_test/test.proto +++ b/pkg/runner/grpc_test/test.proto @@ -52,6 +52,10 @@ message AdvancedType { repeated bool BoolArray = 8; map HelloReplyMap = 9; + Protocol Protocol = 10; } - +enum Protocol { + HTTP = 0; + gRPC = 1; +} diff --git a/pkg/server/remote_server.go b/pkg/server/remote_server.go index 27c47aee..b4b99a0e 100644 --- a/pkg/server/remote_server.go +++ b/pkg/server/remote_server.go @@ -571,7 +571,7 @@ func (s *server) GenerateCode(ctx context.Context, in *CodeGenerateRequest) (rep if result, err = loader.GetTestCase(in.TestSuite, in.TestCase); err == nil { result.Request.RenderAPI(suite.API) - output, genErr := instance.Generate(&result) + output, genErr := instance.Generate(&suite, &result) reply.Success = genErr == nil reply.Message = util.OrErrorMessage(genErr, output) } diff --git a/pkg/server/remote_server_test.go b/pkg/server/remote_server_test.go index 7ac66201..dd8cd983 100644 --- a/pkg/server/remote_server_test.go +++ b/pkg/server/remote_server_test.go @@ -595,7 +595,7 @@ func TestCodeGenerator(t *testing.T) { t.Run("ListCodeGenerator", func(t *testing.T) { generators, err := server.ListCodeGenerator(ctx, &Empty{}) assert.NoError(t, err) - assert.Equal(t, 2, len(generators.Data)) + assert.Equal(t, 3, len(generators.Data)) }) t.Run("GenerateCode, no generator found", func(t *testing.T) { diff --git a/pkg/util/expand.go b/pkg/util/expand.go index 88357b7e..eaaa057a 100644 --- a/pkg/util/expand.go +++ b/pkg/util/expand.go @@ -1,3 +1,27 @@ +/** +MIT License + +Copyright (c) 2023 API Testing Authors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + package util import ( diff --git a/pkg/util/expand_test.go b/pkg/util/expand_test.go index ca1d6065..1f9cb746 100644 --- a/pkg/util/expand_test.go +++ b/pkg/util/expand_test.go @@ -1,3 +1,27 @@ +/** +MIT License + +Copyright (c) 2023 API Testing Authors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + package util_test import ( diff --git a/pkg/util/grpc.go b/pkg/util/grpc.go new file mode 100644 index 00000000..aff46c67 --- /dev/null +++ b/pkg/util/grpc.go @@ -0,0 +1,139 @@ +/** +MIT License + +Copyright (c) 2023 API Testing Authors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +package util + +import ( + "archive/zip" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "os" + "path/filepath" + "strings" + + "log" +) + +func LoadProtoFiles(protoFile string) (targetProtoFile string, importPath []string, protoParentDir string, err error) { + if !strings.HasPrefix(protoFile, "http://") && !strings.HasPrefix(protoFile, "https://") { + targetProtoFile = protoFile + return + } + + var protoURL *url.URL + if protoURL, err = url.Parse(protoFile); err != nil { + return + } + + log.Printf("start to download proto file %q\n", protoFile) + resp, err := GetDefaultCachedHTTPClient().Get(protoFile) + if err != nil { + return + } + + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + err = fmt.Errorf("unexpected status code %d with %q", resp.StatusCode, protoFile) + return + } + + var f *os.File + contentType := resp.Header.Get(ContentType) + if contentType != ZIP { + var data []byte + if data, err = io.ReadAll(resp.Body); err == nil { + if f, err = os.CreateTemp(os.TempDir(), "proto"); err == nil { + _, err = f.Write(data) + targetProtoFile = f.Name() + } + } + } else { + targetProtoFile = protoURL.Query().Get("file") + if targetProtoFile == "" { + err = errors.New("query parameter file is empty") + return + } + + attachment := resp.Header.Get(ContentDisposition) + filename := strings.TrimPrefix(attachment, "attachment; filename=") + name := strings.TrimSuffix(filename, filepath.Ext(filename)) + + parentDir := os.TempDir() + if f, err = os.CreateTemp(parentDir, filename); err == nil { + _, err = io.Copy(f, resp.Body) + + protoParentDir = filepath.Join(parentDir, name) + err = extractFiles(f.Name(), protoParentDir, targetProtoFile) + if err != nil { + return + } + } + } + return +} + +func extractFiles(sourceFile, targetDir, filter string) (err error) { + if sourceFile == "" || targetDir == "" { + err = errors.New("source or target filename is empty") + return + } + + var archive *zip.ReadCloser + if archive, err = zip.OpenReader(sourceFile); err != nil { + return + } + defer func() { + _ = archive.Close() + }() + + for _, f := range archive.File { + if f.FileInfo().IsDir() { + continue + } + + targetFilePath := filepath.Join(targetDir, f.Name) + if err = os.MkdirAll(filepath.Dir(targetFilePath), os.ModePerm); err != nil { + return + } + + var targetFile *os.File + if targetFile, err = os.OpenFile(targetFilePath, + os.O_CREATE|os.O_RDWR, f.Mode()); err != nil { + continue + } + + var fileInArchive io.ReadCloser + fileInArchive, err = f.Open() + if err != nil { + continue + } + + _, err = io.Copy(targetFile, fileInArchive) + _ = targetFile.Close() + } + return +} diff --git a/pkg/util/grpc_test.go b/pkg/util/grpc_test.go new file mode 100644 index 00000000..67718783 --- /dev/null +++ b/pkg/util/grpc_test.go @@ -0,0 +1,96 @@ +/** +MIT License + +Copyright (c) 2023 API Testing Authors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +package util + +import ( + "net/http" + "os" + "strings" + "testing" + + "github.com/h2non/gock" + "github.com/stretchr/testify/assert" +) + +func TestLoadProtoFiles(t *testing.T) { + t.Run("plain string proto file", func(t *testing.T) { + targetProtoFile, importPath, _, _ := LoadProtoFiles("test.proto") + assert.Equal(t, "test.proto", targetProtoFile) + assert.Empty(t, importPath) + }) + + t.Run("URL with invalid status code", func(t *testing.T) { + defer gock.Off() + gock.New("http://localhost").Get("/test.proto").Reply(http.StatusNotFound) + + _, _, _, err := LoadProtoFiles("http://localhost/test.proto") + assert.Error(t, err) + }) + + t.Run("single file URL", func(t *testing.T) { + defer gock.Off() + gock.New("http://localhost").Get("/test.proto"). + MatchParam("rand", "123"). + Reply(http.StatusOK). + File("testdata/test.proto") + + targetProtoFile, importPath, _, err := LoadProtoFiles("http://localhost/test.proto?rand=123") + defer os.Remove(targetProtoFile) + + assert.True(t, strings.HasPrefix(targetProtoFile, os.TempDir()), targetProtoFile) + assert.Empty(t, importPath) + assert.NoError(t, err) + }) + + t.Run("URL with zip file, the query is missing", func(t *testing.T) { + defer gock.Off() + gock.New("http://localhost").Get("/test.proto"). + MatchParam("rand", "234"). + Reply(http.StatusOK). + AddHeader(ContentType, ZIP) + + _, _, _, err := LoadProtoFiles("http://localhost/test.proto?rand=234") + assert.Error(t, err) + }) + + t.Run("URL with zip file", func(t *testing.T) { + defer gock.Off() + gock.New("http://localhost").Get("/test.proto"). + MatchParam("file", "testdata/report.html"). + Reply(http.StatusOK). + AddHeader(ContentType, ZIP). + AddHeader(ContentDisposition, "attachment; filename=test.zip"). + File("testdata/test.zip") + + _, _, targetProtoFileDir, err := LoadProtoFiles("http://localhost/test.proto?file=testdata/report.html") + defer os.RemoveAll(targetProtoFileDir) + + assert.True(t, strings.HasPrefix(targetProtoFileDir, os.TempDir())) + assert.NoError(t, err) + }) + + assert.Error(t, extractFiles("a", "", "")) + assert.Error(t, extractFiles("", "b", "")) +} diff --git a/pkg/util/testdata/report.html b/pkg/util/testdata/report.html new file mode 100644 index 00000000..7c4b4cc7 --- /dev/null +++ b/pkg/util/testdata/report.html @@ -0,0 +1,60 @@ + + + + API Testing Report + + + + + + + + +
API Testing Report
APIAverageMaxMinCountError
/foo3ns3ns3ns10
+ + + \ No newline at end of file diff --git a/pkg/util/testdata/test.proto b/pkg/util/testdata/test.proto new file mode 100644 index 00000000..c00a46fc --- /dev/null +++ b/pkg/util/testdata/test.proto @@ -0,0 +1,61 @@ +syntax = "proto3"; + +option go_package = "github.com/linuxsuren/api-testing/pkg/runner/grpc_test"; + +package grpctest; + +service Main { + rpc Unary(Empty) returns (HelloReply); + rpc ClientStream(stream StreamMessage) returns (StreamMessageRepeated); + rpc ServerStream(StreamMessageRepeated) returns (stream StreamMessage); + rpc BidStream(stream StreamMessage) returns (stream StreamMessage); + + rpc TestBasicType(BasicType) returns (BasicType); + rpc TestAdvancedType(AdvancedType) returns (AdvancedType); +}; + +message Empty { +} + +message HelloReply { + string message = 1; +} + +message StreamMessage{ + int32 MsgID = 1; + int32 ExpectLen =2; +} +message StreamMessageRepeated{ + repeated StreamMessage data = 1; +} + +message BasicType { + int32 Int32 = 1; + int64 Int64 = 2; + uint32 Uint32 = 3; + uint64 Uint64 = 4; + float Float32 = 5; + double Float64 = 6; + string String = 7; + bool Bool = 8; +} + +message AdvancedType { + repeated int32 Int32Array = 1; + repeated int64 Int64Array = 2; + repeated uint32 Uint32Array = 3; + repeated uint64 Uint64Array = 4; + repeated float Float32Array = 5; + repeated double Float64Array = 6; + + repeated string StringArray = 7; + repeated bool BoolArray = 8; + + map HelloReplyMap = 9; + Protocol Protocol = 10; +} + +enum Protocol { + HTTP = 0; + gRPC = 1; +} diff --git a/pkg/runner/testdata/test.zip b/pkg/util/testdata/test.zip similarity index 100% rename from pkg/runner/testdata/test.zip rename to pkg/util/testdata/test.zip