From 9f17637090627e971d244be33c72629f6dd051a3 Mon Sep 17 00:00:00 2001 From: bzp2010 Date: Tue, 19 Oct 2021 00:32:13 -0500 Subject: [PATCH] feat: support proto API (#2099) --- .github/workflows/backend-e2e-test.yml | 14 ++ api/internal/handler/proto/proto.go | 260 ++++++++++++++++++++++ api/internal/handler/proto/proto_test.go | 47 ++++ api/internal/route.go | 2 + api/test/docker/docker-compose.yaml | 10 + api/test/e2enew/base/base.go | 1 + api/test/e2enew/proto/proto_suite_test.go | 40 ++++ api/test/e2enew/proto/proto_test.go | 255 +++++++++++++++++++++ 8 files changed, 629 insertions(+) create mode 100644 api/internal/handler/proto/proto.go create mode 100644 api/internal/handler/proto/proto_test.go create mode 100644 api/test/e2enew/proto/proto_suite_test.go create mode 100644 api/test/e2enew/proto/proto_test.go diff --git a/.github/workflows/backend-e2e-test.yml b/.github/workflows/backend-e2e-test.yml index ba8d43d1ad..f870cfadeb 100644 --- a/.github/workflows/backend-e2e-test.yml +++ b/.github/workflows/backend-e2e-test.yml @@ -70,6 +70,13 @@ jobs: --set *.cache-from=type=local,src=/tmp/.buildx-cache \ --set *.cache-to=type=local,dest=/tmp/.buildx-cache + - name: build and start grpc_server_example + working-directory: ./api/test/docker + run: | + wget https://github.com/api7/grpc_server_example/archive/refs/tags/20210819.tar.gz + tar -xzvf 20210819.tar.gz && cd grpc_server_example-20210819 + docker build -t grpc_server_example:latest . + - name: run docker compose working-directory: ./api/test/docker run: | @@ -171,6 +178,13 @@ jobs: --set *.cache-from=type=local,src=/tmp/.buildx-cache \ --set *.cache-to=type=local,dest=/tmp/.buildx-cache + - name: build and start grpc_server_example + working-directory: ./api/test/docker + run: | + wget https://github.com/api7/grpc_server_example/archive/refs/tags/20210819.tar.gz + tar -xzvf 20210819.tar.gz && cd grpc_server_example-20210819 + docker build -t grpc_server_example:latest . + - name: run docker compose working-directory: ./api/test/docker run: | diff --git a/api/internal/handler/proto/proto.go b/api/internal/handler/proto/proto.go new file mode 100644 index 0000000000..6c815e733c --- /dev/null +++ b/api/internal/handler/proto/proto.go @@ -0,0 +1,260 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package proto + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "reflect" + "strings" + + "github.com/gin-gonic/gin" + "github.com/shiningrush/droplet" + "github.com/shiningrush/droplet/data" + "github.com/shiningrush/droplet/wrapper" + wgin "github.com/shiningrush/droplet/wrapper/gin" + + "github.com/apisix/manager-api/internal/core/entity" + "github.com/apisix/manager-api/internal/core/store" + "github.com/apisix/manager-api/internal/handler" + "github.com/apisix/manager-api/internal/utils" +) + +type Handler struct { + routeStore store.Interface + serviceStore store.Interface + consumerStore store.Interface + pluginConfigStore store.Interface + globalRuleStore store.Interface + protoStore store.Interface +} + +func NewHandler() (handler.RouteRegister, error) { + return &Handler{ + routeStore: store.GetStore(store.HubKeyRoute), + serviceStore: store.GetStore(store.HubKeyService), + consumerStore: store.GetStore(store.HubKeyConsumer), + pluginConfigStore: store.GetStore(store.HubKeyPluginConfig), + globalRuleStore: store.GetStore(store.HubKeyGlobalRule), + protoStore: store.GetStore(store.HubKeyProto), + }, nil +} + +func (h *Handler) ApplyRoute(r *gin.Engine) { + r.GET("/apisix/admin/proto/:id", wgin.Wraps(h.Get, + wrapper.InputType(reflect.TypeOf(GetInput{})))) + r.GET("/apisix/admin/proto", wgin.Wraps(h.List, + wrapper.InputType(reflect.TypeOf(ListInput{})))) + r.POST("/apisix/admin/proto", wgin.Wraps(h.Create, + wrapper.InputType(reflect.TypeOf(entity.Proto{})))) + r.PUT("/apisix/admin/proto", wgin.Wraps(h.Update, + wrapper.InputType(reflect.TypeOf(UpdateInput{})))) + r.PUT("/apisix/admin/proto/:id", wgin.Wraps(h.Update, + wrapper.InputType(reflect.TypeOf(UpdateInput{})))) + r.PATCH("/apisix/admin/proto/:id", wgin.Wraps(h.Patch, + wrapper.InputType(reflect.TypeOf(PatchInput{})))) + r.PATCH("/apisix/admin/proto/:id/*path", wgin.Wraps(h.Patch, + wrapper.InputType(reflect.TypeOf(PatchInput{})))) + r.DELETE("/apisix/admin/proto/:ids", wgin.Wraps(h.BatchDelete, + wrapper.InputType(reflect.TypeOf(BatchDeleteInput{})))) +} + +var plugins = []string{"grpc-transcode"} + +type GetInput struct { + ID string `auto_read:"id,path" validate:"required"` +} + +func (h *Handler) Get(c droplet.Context) (interface{}, error) { + input := c.Input().(*GetInput) + + r, err := h.protoStore.Get(c.Context(), input.ID) + if err != nil { + return handler.SpecCodeResponse(err), err + } + + return r, nil +} + +type ListInput struct { + Desc string `auto_read:"desc,query"` + store.Pagination +} + +func (h *Handler) List(c droplet.Context) (interface{}, error) { + input := c.Input().(*ListInput) + + ret, err := h.protoStore.List(c.Context(), store.ListInput{ + Predicate: func(obj interface{}) bool { + if input.Desc != "" { + return strings.Contains(obj.(*entity.Proto).Desc, input.Desc) + } + return true + }, + PageSize: input.PageSize, + PageNumber: input.PageNumber, + }) + if err != nil { + return nil, err + } + + return ret, nil +} + +func (h *Handler) Create(c droplet.Context) (interface{}, error) { + input := c.Input().(*entity.Proto) + + // check proto id exist + if input.ID != nil { + protoID := utils.InterfaceToString(input.ID) + ret, err := h.protoStore.Get(c.Context(), protoID) + if err != nil && err != data.ErrNotFound { + return handler.SpecCodeResponse(err), err + } + if ret != nil { + return &data.SpecCodeResponse{StatusCode: http.StatusBadRequest}, errors.New("proto id exists") + } + } + + // create + ret, err := h.protoStore.Create(c.Context(), input) + if err != nil { + return handler.SpecCodeResponse(err), err + } + + return ret, nil +} + +type UpdateInput struct { + ID string `auto_read:"id,path"` + entity.Proto +} + +func (h *Handler) Update(c droplet.Context) (interface{}, error) { + input := c.Input().(*UpdateInput) + + // check if ID in body is equal ID in path + if err := handler.IDCompare(input.ID, input.Proto.ID); err != nil { + return &data.SpecCodeResponse{StatusCode: http.StatusBadRequest}, err + } + + if input.ID != "" { + input.Proto.ID = input.ID + } + + res, err := h.protoStore.Update(c.Context(), &input.Proto, true) + if err != nil { + return handler.SpecCodeResponse(err), err + } + + return res, nil +} + +type PatchInput struct { + ID string `auto_read:"id,path"` + SubPath string `auto_read:"path,path"` + Body []byte `auto_read:"@body"` +} + +func (h *Handler) Patch(c droplet.Context) (interface{}, error) { + input := c.Input().(*PatchInput) + reqBody := input.Body + id := input.ID + subPath := input.SubPath + + stored, err := h.protoStore.Get(c.Context(), id) + if err != nil { + return handler.SpecCodeResponse(err), err + } + + res, err := utils.MergePatch(stored, subPath, reqBody) + + if err != nil { + return handler.SpecCodeResponse(err), err + } + + var proto entity.Proto + if err := json.Unmarshal(res, &proto); err != nil { + return handler.SpecCodeResponse(err), err + } + + ret, err := h.protoStore.Update(c.Context(), &proto, false) + if err != nil { + return handler.SpecCodeResponse(err), err + } + + return ret, nil +} + +type BatchDeleteInput struct { + IDs string `auto_read:"ids,path"` +} + +func (h *Handler) BatchDelete(c droplet.Context) (interface{}, error) { + input := c.Input().(*BatchDeleteInput) + + ids := strings.Split(input.IDs, ",") + checklist := []store.Interface{h.routeStore, h.consumerStore, h.serviceStore, h.pluginConfigStore, h.globalRuleStore} + + for _, id := range ids { + for _, store := range checklist { + if err := h.checkProtoUsed(c.Context(), store, id); err != nil { + return handler.SpecCodeResponse(err), err + } + } + } + + if err := h.protoStore.BatchDelete(c.Context(), ids); err != nil { + return handler.SpecCodeResponse(err), err + } + + return nil, nil +} + +func (h *Handler) checkProtoUsed(ctx context.Context, storeInterface store.Interface, key string) error { + ret, err := storeInterface.List(ctx, store.ListInput{ + Predicate: func(obj interface{}) bool { + record := obj.(entity.GetPlugins) + for _, plugin := range plugins { + if _, ok := record.GetPlugins()[plugin]; ok { + configs := record.GetPlugins()[plugin].(map[string]interface{}) + protoId := utils.InterfaceToString(configs["proto_id"]) + if protoId == key { + return true + } + } + } + return false + }, + Format: func(obj interface{}) interface{} { + return obj.(entity.GetPlugins) + }, + PageSize: 0, + PageNumber: 0, + }) + if err != nil { + return err + } + if ret.TotalSize > 0 { + return fmt.Errorf("proto used check invalid: %s: %s is using this proto", storeInterface.Type(), ret.Rows[0].(entity.GetBaseInfo).GetBaseInfo().ID) + } + return nil + +} diff --git a/api/internal/handler/proto/proto_test.go b/api/internal/handler/proto/proto_test.go new file mode 100644 index 0000000000..942dacdd14 --- /dev/null +++ b/api/internal/handler/proto/proto_test.go @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package proto + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/apisix/manager-api/internal/core/entity" +) + +func TestStructUnmarshal(t *testing.T) { + // define and parse data + jsonStr := `{ + "id": 1, + "create_time": 1700000000, + "update_time": 1700000000, + "desc": "desc", + "content": "content" +}` + proto := entity.Proto{} + err := json.Unmarshal([]byte(jsonStr), &proto) + + // asserts + assert.Nil(t, err) + assert.Equal(t, proto.ID, float64(1)) + assert.Equal(t, proto.CreateTime, int64(1700000000)) + assert.Equal(t, proto.UpdateTime, int64(1700000000)) + assert.Equal(t, proto.Desc, "desc") + assert.Equal(t, proto.Content, "content") +} diff --git a/api/internal/route.go b/api/internal/route.go index 06aa690d69..6a507cc258 100644 --- a/api/internal/route.go +++ b/api/internal/route.go @@ -35,6 +35,7 @@ import ( "github.com/apisix/manager-api/internal/handler/label" "github.com/apisix/manager-api/internal/handler/migrate" "github.com/apisix/manager-api/internal/handler/plugin_config" + "github.com/apisix/manager-api/internal/handler/proto" "github.com/apisix/manager-api/internal/handler/route" "github.com/apisix/manager-api/internal/handler/schema" "github.com/apisix/manager-api/internal/handler/server_info" @@ -77,6 +78,7 @@ func SetUpRouter() *gin.Engine { tool.NewHandler, plugin_config.NewHandler, migrate.NewHandler, + proto.NewHandler, } for i := range factories { diff --git a/api/test/docker/docker-compose.yaml b/api/test/docker/docker-compose.yaml index 4782d0ed17..ce4fe03bce 100644 --- a/api/test/docker/docker-compose.yaml +++ b/api/test/docker/docker-compose.yaml @@ -125,6 +125,16 @@ services: apisix_dashboard_e2e: ipv4_address: 172.16.238.20 + upstream_grpc: + image: grpc_server_example + restart: always + ports: + - '50051:50051' + - '50052:50052' + networks: + apisix_dashboard_e2e: + ipv4_address: 172.16.238.21 + apisix: hostname: apisix_server1 image: apache/apisix:2.10.0-alpine diff --git a/api/test/e2enew/base/base.go b/api/test/e2enew/base/base.go index 32253ffa83..ced3bf82e2 100644 --- a/api/test/e2enew/base/base.go +++ b/api/test/e2enew/base/base.go @@ -37,6 +37,7 @@ var ( token string UpstreamIp = "172.16.238.20" + UpstreamGrpcIp = "172.16.238.21" APISIXHost = "http://127.0.0.1:9080" APISIXInternalUrl = "http://172.16.238.30:9080" APISIXSingleWorkerHost = "http://127.0.0.1:9081" diff --git a/api/test/e2enew/proto/proto_suite_test.go b/api/test/e2enew/proto/proto_suite_test.go new file mode 100644 index 0000000000..ab0b82603b --- /dev/null +++ b/api/test/e2enew/proto/proto_suite_test.go @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package proto + +import ( + "testing" + "time" + + "github.com/onsi/ginkgo" + "github.com/onsi/gomega" + + "github.com/apisix/manager-api/test/e2enew/base" +) + +func TestRoute(t *testing.T) { + gomega.RegisterFailHandler(ginkgo.Fail) + ginkgo.RunSpecs(t, "proto suite") +} + +var _ = ginkgo.AfterSuite(func() { + base.CleanResource("routes") + base.CleanResource("upstreams") + base.CleanResource("consumers") + base.CleanResource("proto") + time.Sleep(base.SleepTime) +}) diff --git a/api/test/e2enew/proto/proto_test.go b/api/test/e2enew/proto/proto_test.go new file mode 100644 index 0000000000..0b1698090c --- /dev/null +++ b/api/test/e2enew/proto/proto_test.go @@ -0,0 +1,255 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package proto + +import ( + "encoding/json" + "net/http" + + "github.com/onsi/ginkgo" + "github.com/onsi/gomega" + + "github.com/apisix/manager-api/test/e2enew/base" +) + +var correctProtobuf = `syntax = "proto3"; + package helloworld; + service Greeter { + rpc SayHello (HelloRequest) returns (HelloReply) {} + } + message HelloRequest { + string name = 1; + } + message HelloReply { + string message = 1; + }` + +var _ = ginkgo.Describe("Proto", func() { + ginkgo.It("create proto success", func() { + createProtoBody := make(map[string]interface{}) + createProtoBody["id"] = 1 + createProtoBody["desc"] = "test_proto1" + createProtoBody["content"] = correctProtobuf + + _createProtoBody, err := json.Marshal(createProtoBody) + gomega.Expect(err).To(gomega.BeNil()) + + base.RunTestCase(base.HttpTestCase{ + Object: base.ManagerApiExpect(), + Method: http.MethodPost, + Path: "/apisix/admin/proto", + Body: string(_createProtoBody), + Headers: map[string]string{"Authorization": base.GetToken()}, + ExpectStatus: http.StatusOK, + }) + }) + ginkgo.It("create proto failed, id existed", func() { + createProtoBody := make(map[string]interface{}) + createProtoBody["id"] = 1 + createProtoBody["desc"] = "test_proto1" + createProtoBody["content"] = correctProtobuf + + _createProtoBody, err := json.Marshal(createProtoBody) + gomega.Expect(err).To(gomega.BeNil()) + + base.RunTestCase(base.HttpTestCase{ + Object: base.ManagerApiExpect(), + Method: http.MethodPost, + Path: "/apisix/admin/proto", + Body: string(_createProtoBody), + Headers: map[string]string{"Authorization": base.GetToken()}, + ExpectBody: "proto id exists", + ExpectStatus: http.StatusBadRequest, + }) + }) + ginkgo.It("update proto success", func() { + updateProtoBody := make(map[string]interface{}) + updateProtoBody["id"] = 1 + updateProtoBody["desc"] = "test_proto1_modify" + updateProtoBody["content"] = correctProtobuf + + _updateProtoBody, err := json.Marshal(updateProtoBody) + gomega.Expect(err).To(gomega.BeNil()) + + base.RunTestCase(base.HttpTestCase{ + Object: base.ManagerApiExpect(), + Method: http.MethodPut, + Path: "/apisix/admin/proto", + Body: string(_updateProtoBody), + Headers: map[string]string{"Authorization": base.GetToken()}, + ExpectBody: "test_proto1_modify", + ExpectStatus: http.StatusOK, + }) + }) + ginkgo.It("list proto", func() { + base.RunTestCase(base.HttpTestCase{ + Object: base.ManagerApiExpect(), + Method: http.MethodGet, + Path: "/apisix/admin/proto", + Headers: map[string]string{"Authorization": base.GetToken()}, + ExpectBody: "test_proto1_modify", + ExpectStatus: http.StatusOK, + }) + }) + ginkgo.It("get proto", func() { + base.RunTestCase(base.HttpTestCase{ + Object: base.ManagerApiExpect(), + Method: http.MethodGet, + Path: "/apisix/admin/proto/1", + Headers: map[string]string{"Authorization": base.GetToken()}, + ExpectBody: "test_proto1_modify", + ExpectStatus: http.StatusOK, + }) + }) + ginkgo.It("delete not existed proto", func() { + base.RunTestCase(base.HttpTestCase{ + Object: base.ManagerApiExpect(), + Method: http.MethodDelete, + Path: "/apisix/admin/proto/not-exist", + Headers: map[string]string{"Authorization": base.GetToken()}, + ExpectStatus: http.StatusNotFound, + }) + }) + ginkgo.It("delete proto", func() { + base.RunTestCase(base.HttpTestCase{ + Object: base.ManagerApiExpect(), + Method: http.MethodDelete, + Path: "/apisix/admin/proto/1", + Headers: map[string]string{"Authorization": base.GetToken()}, + ExpectStatus: http.StatusOK, + }) + }) +}) + +var _ = ginkgo.Describe("Proto with grpc-transcode plugin", func() { + ginkgo.It("create proto success", func() { + createProtoBody := make(map[string]interface{}) + createProtoBody["id"] = 1 + createProtoBody["desc"] = "test_proto1" + createProtoBody["content"] = correctProtobuf + + _createProtoBody, err := json.Marshal(createProtoBody) + gomega.Expect(err).To(gomega.BeNil()) + + base.RunTestCase(base.HttpTestCase{ + Object: base.ManagerApiExpect(), + Method: http.MethodPost, + Path: "/apisix/admin/proto", + Body: string(_createProtoBody), + Headers: map[string]string{"Authorization": base.GetToken()}, + ExpectStatus: http.StatusOK, + }) + }) + ginkgo.It("create route with grpc-transcode", func() { + createRouteBody := make(map[string]interface{}) + createRouteBody["id"] = 1 + createRouteBody["name"] = "test_route" + createRouteBody["uri"] = "/grpc_test" + createRouteBody["methods"] = []string{"GET", "POST"} + createRouteBody["upstream"] = map[string]interface{}{ + "nodes": []map[string]interface{}{ + { + "host": base.UpstreamGrpcIp, + "port": 50051, + "weight": 1, + }, + }, + "type": "roundrobin", + "scheme": "grpc", + } + createRouteBody["plugins"] = map[string]interface{}{ + "grpc-transcode": map[string]interface{}{ + "method": "SayHello", + "proto_id": "1", + "service": "helloworld.Greeter", + }, + } + + _createRouteBody, err := json.Marshal(createRouteBody) + gomega.Expect(err).To(gomega.BeNil()) + + base.RunTestCase(base.HttpTestCase{ + Object: base.ManagerApiExpect(), + Method: http.MethodPost, + Path: "/apisix/admin/routes", + Body: string(_createRouteBody), + Headers: map[string]string{"Authorization": base.GetToken()}, + ExpectStatus: http.StatusOK, + }) + }) + ginkgo.It("hit GET route for grpc-transcode test", func() { + base.RunTestCase(base.HttpTestCase{ + Object: base.APISIXExpect(), + Method: http.MethodGet, + Path: "/grpc_test", + Query: "name=world", + Headers: map[string]string{"Authorization": base.GetToken()}, + ExpectBody: "{\"message\":\"Hello world\"}", + ExpectStatus: http.StatusOK, + }) + }) + ginkgo.It("hit POST route for grpc-transcode test", func() { + base.RunTestCase(base.HttpTestCase{ + Object: base.APISIXExpect(), + Method: http.MethodPost, + Path: "/grpc_test", + Body: "name=world", + Headers: map[string]string{"Authorization": base.GetToken()}, + ExpectBody: "{\"message\":\"Hello world\"}", + ExpectStatus: http.StatusOK, + }) + }) + ginkgo.It("hit JSON POST route for grpc-transcode test", func() { + base.RunTestCase(base.HttpTestCase{ + Object: base.APISIXExpect(), + Method: http.MethodPost, + Path: "/grpc_test", + Body: "{\"name\": \"world\"}", + Headers: map[string]string{"Authorization": base.GetToken()}, + ExpectBody: "{\"message\":\"Hello world\"}", + ExpectStatus: http.StatusOK, + }) + }) + ginkgo.It("delete route used proto", func() { + base.RunTestCase(base.HttpTestCase{ + Object: base.ManagerApiExpect(), + Method: http.MethodDelete, + Path: "/apisix/admin/proto/1", + Headers: map[string]string{"Authorization": base.GetToken()}, + ExpectBody: "proto used check invalid: route", + ExpectStatus: http.StatusBadRequest, + }) + }) + ginkgo.It("delete conflict route", func() { + base.RunTestCase(base.HttpTestCase{ + Object: base.ManagerApiExpect(), + Method: http.MethodDelete, + Path: "/apisix/admin/routes/1", + Headers: map[string]string{"Authorization": base.GetToken()}, + ExpectStatus: http.StatusOK, + }) + }) + ginkgo.It("delete proto again", func() { + base.RunTestCase(base.HttpTestCase{ + Object: base.ManagerApiExpect(), + Method: http.MethodDelete, + Path: "/apisix/admin/proto/1", + Headers: map[string]string{"Authorization": base.GetToken()}, + ExpectStatus: http.StatusOK, + }) + }) +})