diff --git a/README.md b/README.md index 08c224ad..cfe7e29c 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,11 @@ docker-compose exec opi-evpn-bridge grpcurl -plaintext -d '{"name": "//network.o docker-compose exec opi-evpn-bridge grpcurl -plaintext -d '{"name": "//network.opiproject.org/bridges/testbridge"}' localhost:50151 opi_api.network.evpn-gw.v1alpha1.LogicalBridgeService.GetLogicalBridge docker-compose exec opi-evpn-bridge grpcurl -plaintext -d '{"name": "//network.opiproject.org/svis/testsvi"}' localhost:50151 opi_api.network.evpn-gw.v1alpha1.SviService.GetSvi docker-compose exec opi-evpn-bridge grpcurl -plaintext -d '{"name": "//network.opiproject.org/vrfs/testvrf"}' localhost:50151 opi_api.network.evpn-gw.v1alpha1.VrfService.GetVrf +#list +docker-compose exec opi-evpn-bridge grpcurl -plaintext localhost:50151 opi_api.network.evpn-gw.v1alpha1.BridgePortService.ListBridgePorts +docker-compose exec opi-evpn-bridge grpcurl -plaintext localhost:50151 opi_api.network.evpn-gw.v1alpha1.BridgePortService.ListLogicalBridges +docker-compose exec opi-evpn-bridge grpcurl -plaintext localhost:50151 opi_api.network.evpn-gw.v1alpha1.BridgePortService.ListSvis +docker-compose exec opi-evpn-bridge grpcurl -plaintext localhost:50151 opi_api.network.evpn-gw.v1alpha1.BridgePortService.ListVrfs # delete docker-compose exec opi-evpn-bridge grpcurl -plaintext -d '{"name": "//network.opiproject.org/ports/testinterface"}' localhost:50151 opi_api.network.evpn-gw.v1alpha1.BridgePortService.DeleteBridgePort docker-compose exec opi-evpn-bridge grpcurl -plaintext -d '{"name": "//network.opiproject.org/bridges/testbridge"}' localhost:50151 opi_api.network.evpn-gw.v1alpha1.LogicalBridgeService.DeleteLogicalBridge diff --git a/docker-compose.yml b/docker-compose.yml index f68b9859..68a09fb8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -276,6 +276,11 @@ services: /entrypoint.sh call --json_input --json_output localhost:50151 GetSvi "{\"name\" : \"//network.opiproject.org/svis/yellow-vlan50\" }" && \ /entrypoint.sh call --json_input --json_output localhost:50151 GetBridgePort "{\"name\" : \"//network.opiproject.org/ports/eth1\" }" && \ /entrypoint.sh call --json_input --json_output localhost:50151 GetBridgePort "{\"name\" : \"//network.opiproject.org/ports/eth2\" }" && \ + echo list && \ + /entrypoint.sh call --json_input --json_output localhost:50151 ListVrfs "{}" && \ + /entrypoint.sh call --json_input --json_output localhost:50151 ListLogicalBridges "{}" && \ + /entrypoint.sh call --json_input --json_output localhost:50151 ListSvis "{}" && \ + /entrypoint.sh call --json_input --json_output localhost:50151 ListBridgePorts "{}" && \ echo done' host2-leaf2: diff --git a/pkg/evpn/bridge.go b/pkg/evpn/bridge.go index 17d76a81..f2334b58 100644 --- a/pkg/evpn/bridge.go +++ b/pkg/evpn/bridge.go @@ -11,6 +11,7 @@ import ( "fmt" "log" "net" + "sort" "github.com/vishvananda/netlink" @@ -25,6 +26,12 @@ import ( "google.golang.org/protobuf/types/known/emptypb" ) +func sortLogicalBridges(bridges []*pb.LogicalBridge) { + sort.Slice(bridges, func(i int, j int) bool { + return bridges[i].Name < bridges[j].Name + }) +} + // CreateLogicalBridge executes the creation of the LogicalBridge func (s *Server) CreateLogicalBridge(_ context.Context, in *pb.CreateLogicalBridgeRequest) (*pb.LogicalBridge, error) { log.Printf("CreateLogicalBridge: Received from client: %v", in) @@ -237,3 +244,23 @@ func (s *Server) GetLogicalBridge(_ context.Context, in *pb.GetLogicalBridgeRequ // TODO return &pb.LogicalBridge{Name: in.Name, Spec: &pb.LogicalBridgeSpec{Vni: bridge.Spec.Vni, VlanId: bridge.Spec.VlanId}, Status: &pb.LogicalBridgeStatus{OperStatus: pb.LBOperStatus_LB_OPER_STATUS_UP}}, nil } + +// ListLogicalBridges lists logical bridges +func (s *Server) ListLogicalBridges(_ context.Context, in *pb.ListLogicalBridgesRequest) (*pb.ListLogicalBridgesResponse, error) { + log.Printf("ListLogicalBridges: Received from client: %v", in) + // check required fields + if err := fieldbehavior.ValidateRequiredFields(in); err != nil { + log.Printf("error: %v", err) + return nil, err + } + token := "" + Blobarray := []*pb.LogicalBridge{} + for _, bridge := range s.Bridges { + r := protoClone(bridge) + r.Status = &pb.LogicalBridgeStatus{OperStatus: pb.LBOperStatus_LB_OPER_STATUS_UP} + Blobarray = append(Blobarray, r) + } + // TODO: Limit results to offset and size and rememeber pagination + sortLogicalBridges(Blobarray) + return &pb.ListLogicalBridgesResponse{LogicalBridges: Blobarray, NextPageToken: token}, nil +} diff --git a/pkg/evpn/bridge_test.go b/pkg/evpn/bridge_test.go index c4c525f1..05dd9638 100644 --- a/pkg/evpn/bridge_test.go +++ b/pkg/evpn/bridge_test.go @@ -49,6 +49,13 @@ var ( }, }, } + testLogicalBridgeWithStatus = pb.LogicalBridge{ + Name: testLogicalBridgeName, + Spec: testLogicalBridge.Spec, + Status: &pb.LogicalBridgeStatus{ + OperStatus: pb.LBOperStatus_LB_OPER_STATUS_UP, + }, + } ) func Test_CreateLogicalBridge(t *testing.T) { @@ -220,14 +227,9 @@ func Test_CreateLogicalBridge(t *testing.T) { }, }, "successful call": { - id: testLogicalBridgeID, - in: &testLogicalBridge, - out: &pb.LogicalBridge{ - Spec: testLogicalBridge.Spec, - Status: &pb.LogicalBridgeStatus{ - OperStatus: pb.LBOperStatus_LB_OPER_STATUS_UP, - }, - }, + id: testLogicalBridgeID, + in: &testLogicalBridge, + out: &testLogicalBridgeWithStatus, errCode: codes.OK, errMsg: "", exist: false, @@ -631,3 +633,72 @@ func Test_GetLogicalBridge(t *testing.T) { }) } } + +func Test_ListLogicalBridges(t *testing.T) { + tests := map[string]struct { + in string + out []*pb.LogicalBridge + errCode codes.Code + errMsg string + size int32 + token string + }{ + "example test": { + in: "", + out: []*pb.LogicalBridge{&testLogicalBridgeWithStatus}, + errCode: codes.OK, + errMsg: "", + size: 0, + token: "", + }, + } + + // run tests + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + // start GRPC mockup server + ctx := context.Background() + mockNetlink := mocks.NewNetlink(t) + opi := NewServerWithArgs(mockNetlink) + conn, err := grpc.DialContext(ctx, + "", + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithContextDialer(dialer(opi))) + if err != nil { + log.Fatal(err) + } + defer func(conn *grpc.ClientConn) { + err := conn.Close() + if err != nil { + log.Fatal(err) + } + }(conn) + client := pb.NewLogicalBridgeServiceClient(conn) + + opi.Bridges[testLogicalBridgeName] = protoClone(&testLogicalBridge) + opi.Bridges[testLogicalBridgeName].Name = testLogicalBridgeName + + request := &pb.ListLogicalBridgesRequest{PageSize: tt.size, PageToken: tt.token} + response, err := client.ListLogicalBridges(ctx, request) + if !equalProtoSlices(response.GetLogicalBridges(), tt.out) { + t.Error("response: expected", tt.out, "received", response.GetLogicalBridges()) + } + + // Empty NextPageToken indicates end of results list + if tt.size != 1 && response.GetNextPageToken() != "" { + t.Error("Expected end of results, received non-empty next page token", response.GetNextPageToken()) + } + + if er, ok := status.FromError(err); ok { + if er.Code() != tt.errCode { + t.Error("error code: expected", tt.errCode, "received", er.Code()) + } + if er.Message() != tt.errMsg { + t.Error("error message: expected", tt.errMsg, "received", er.Message()) + } + } else { + t.Error("expected grpc error status") + } + }) + } +} diff --git a/pkg/evpn/evpn_test.go b/pkg/evpn/evpn_test.go index 9e51c364..602c1aa4 100644 --- a/pkg/evpn/evpn_test.go +++ b/pkg/evpn/evpn_test.go @@ -13,6 +13,7 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/test/bufconn" + "google.golang.org/protobuf/proto" pe "github.com/opiproject/opi-api/network/evpn-gw/v1alpha1/gen/go" @@ -39,6 +40,20 @@ func dialer(opi *Server) func(context.Context, string) (net.Conn, error) { } } +func equalProtoSlices[T proto.Message](x, y []T) bool { + if len(x) != len(y) { + return false + } + + for i := 0; i < len(x); i++ { + if !proto.Equal(x[i], y[i]) { + return false + } + } + + return true +} + func TestFrontEnd_NewServerWithArgs(t *testing.T) { tests := map[string]struct { nLink utils.Netlink diff --git a/pkg/evpn/port.go b/pkg/evpn/port.go index d0b264b6..cb264a6f 100644 --- a/pkg/evpn/port.go +++ b/pkg/evpn/port.go @@ -10,6 +10,7 @@ import ( "fmt" "log" "path" + "sort" // "github.com/vishvananda/netlink" @@ -24,6 +25,12 @@ import ( "google.golang.org/protobuf/types/known/emptypb" ) +func sortBridgePorts(ports []*pb.BridgePort) { + sort.Slice(ports, func(i int, j int) bool { + return ports[i].Name < ports[j].Name + }) +} + // CreateBridgePort executes the creation of the port func (s *Server) CreateBridgePort(_ context.Context, in *pb.CreateBridgePortRequest) (*pb.BridgePort, error) { log.Printf("CreateBridgePort: Received from client: %v", in) @@ -262,3 +269,23 @@ func (s *Server) GetBridgePort(_ context.Context, in *pb.GetBridgePortRequest) ( // TODO return &pb.BridgePort{Name: in.Name, Spec: &pb.BridgePortSpec{MacAddress: port.Spec.MacAddress}, Status: &pb.BridgePortStatus{OperStatus: pb.BPOperStatus_BP_OPER_STATUS_UP}}, nil } + +// ListBridgePorts lists logical bridges +func (s *Server) ListBridgePorts(_ context.Context, in *pb.ListBridgePortsRequest) (*pb.ListBridgePortsResponse, error) { + log.Printf("ListBridgePorts: Received from client: %v", in) + // check required fields + if err := fieldbehavior.ValidateRequiredFields(in); err != nil { + log.Printf("error: %v", err) + return nil, err + } + token := "" + Blobarray := []*pb.BridgePort{} + for _, port := range s.Ports { + r := protoClone(port) + r.Status = &pb.BridgePortStatus{OperStatus: pb.BPOperStatus_BP_OPER_STATUS_UP} + Blobarray = append(Blobarray, r) + } + // TODO: Limit results to offset and size and rememeber pagination + sortBridgePorts(Blobarray) + return &pb.ListBridgePortsResponse{BridgePorts: Blobarray, NextPageToken: token}, nil +} diff --git a/pkg/evpn/port_test.go b/pkg/evpn/port_test.go index bbb467a7..33face01 100644 --- a/pkg/evpn/port_test.go +++ b/pkg/evpn/port_test.go @@ -40,6 +40,13 @@ var ( LogicalBridges: []string{testLogicalBridgeName}, }, } + testBridgePortWithStatus = pb.BridgePort{ + Name: testBridgePortName, + Spec: testBridgePort.Spec, + Status: &pb.BridgePortStatus{ + OperStatus: pb.BPOperStatus_BP_OPER_STATUS_UP, + }, + } ) func Test_CreateBridgePort(t *testing.T) { @@ -263,14 +270,9 @@ func Test_CreateBridgePort(t *testing.T) { }, }, "successful call": { - id: testBridgePortID, - in: &testBridgePort, - out: &pb.BridgePort{ - Spec: testBridgePort.Spec, - Status: &pb.BridgePortStatus{ - OperStatus: pb.BPOperStatus_BP_OPER_STATUS_UP, - }, - }, + id: testBridgePortID, + in: &testBridgePort, + out: &testBridgePortWithStatus, errCode: codes.OK, errMsg: "", exist: false, @@ -664,3 +666,72 @@ func Test_GetBridgePort(t *testing.T) { }) } } + +func Test_ListBridgePorts(t *testing.T) { + tests := map[string]struct { + in string + out []*pb.BridgePort + errCode codes.Code + errMsg string + size int32 + token string + }{ + "example test": { + in: "", + out: []*pb.BridgePort{&testBridgePortWithStatus}, + errCode: codes.OK, + errMsg: "", + size: 0, + token: "", + }, + } + + // run tests + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + // start GRPC mockup server + ctx := context.Background() + mockNetlink := mocks.NewNetlink(t) + opi := NewServerWithArgs(mockNetlink) + conn, err := grpc.DialContext(ctx, + "", + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithContextDialer(dialer(opi))) + if err != nil { + log.Fatal(err) + } + defer func(conn *grpc.ClientConn) { + err := conn.Close() + if err != nil { + log.Fatal(err) + } + }(conn) + client := pb.NewBridgePortServiceClient(conn) + + opi.Ports[testBridgePortName] = protoClone(&testBridgePort) + opi.Ports[testBridgePortName].Name = testBridgePortName + + request := &pb.ListBridgePortsRequest{PageSize: tt.size, PageToken: tt.token} + response, err := client.ListBridgePorts(ctx, request) + if !equalProtoSlices(response.GetBridgePorts(), tt.out) { + t.Error("response: expected", tt.out, "received", response.GetBridgePorts()) + } + + // Empty NextPageToken indicates end of results list + if tt.size != 1 && response.GetNextPageToken() != "" { + t.Error("Expected end of results, received non-empty next page token", response.GetNextPageToken()) + } + + if er, ok := status.FromError(err); ok { + if er.Code() != tt.errCode { + t.Error("error code: expected", tt.errCode, "received", er.Code()) + } + if er.Message() != tt.errMsg { + t.Error("error message: expected", tt.errMsg, "received", er.Message()) + } + } else { + t.Error("expected grpc error status") + } + }) + } +} diff --git a/pkg/evpn/svi.go b/pkg/evpn/svi.go index e27ea610..ed7382ee 100644 --- a/pkg/evpn/svi.go +++ b/pkg/evpn/svi.go @@ -12,6 +12,7 @@ import ( "log" "net" "path" + "sort" "github.com/vishvananda/netlink" @@ -26,6 +27,12 @@ import ( "google.golang.org/protobuf/types/known/emptypb" ) +func sortSvis(svis []*pb.Svi) { + sort.Slice(svis, func(i int, j int) bool { + return svis[i].Name < svis[j].Name + }) +} + // CreateSvi executes the creation of the VLAN func (s *Server) CreateSvi(_ context.Context, in *pb.CreateSviRequest) (*pb.Svi, error) { log.Printf("CreateSvi: Received from client: %v", in) @@ -295,3 +302,23 @@ func (s *Server) GetSvi(_ context.Context, in *pb.GetSviRequest) (*pb.Svi, error // TODO return &pb.Svi{Name: in.Name, Spec: &pb.SviSpec{MacAddress: obj.Spec.MacAddress, EnableBgp: obj.Spec.EnableBgp, RemoteAs: obj.Spec.RemoteAs}, Status: &pb.SviStatus{OperStatus: pb.SVIOperStatus_SVI_OPER_STATUS_UP}}, nil } + +// ListSvis lists logical bridges +func (s *Server) ListSvis(_ context.Context, in *pb.ListSvisRequest) (*pb.ListSvisResponse, error) { + log.Printf("ListSvis: Received from client: %v", in) + // check required fields + if err := fieldbehavior.ValidateRequiredFields(in); err != nil { + log.Printf("error: %v", err) + return nil, err + } + token := "" + Blobarray := []*pb.Svi{} + for _, svi := range s.Svis { + r := protoClone(svi) + r.Status = &pb.SviStatus{OperStatus: pb.SVIOperStatus_SVI_OPER_STATUS_UP} + Blobarray = append(Blobarray, r) + } + // TODO: Limit results to offset and size and rememeber pagination + sortSvis(Blobarray) + return &pb.ListSvisResponse{Svis: Blobarray, NextPageToken: token}, nil +} diff --git a/pkg/evpn/svi_test.go b/pkg/evpn/svi_test.go index 41893df9..7b9208c7 100644 --- a/pkg/evpn/svi_test.go +++ b/pkg/evpn/svi_test.go @@ -41,6 +41,13 @@ var ( GwIpPrefix: []*pc.IPPrefix{{Len: 24}}, }, } + testSviWithStatus = pb.Svi{ + Name: testSviName, + Spec: testSvi.Spec, + Status: &pb.SviStatus{ + OperStatus: pb.SVIOperStatus_SVI_OPER_STATUS_UP, + }, + } ) func Test_CreateSvi(t *testing.T) { @@ -355,14 +362,9 @@ func Test_CreateSvi(t *testing.T) { }, }, "successful call": { - id: testSviID, - in: &testSvi, - out: &pb.Svi{ - Spec: testSvi.Spec, - Status: &pb.SviStatus{ - OperStatus: pb.SVIOperStatus_SVI_OPER_STATUS_UP, - }, - }, + id: testSviID, + in: &testSvi, + out: &testSviWithStatus, errCode: codes.OK, errMsg: "", exist: false, @@ -790,3 +792,72 @@ func Test_GetSvi(t *testing.T) { }) } } + +func Test_ListSvis(t *testing.T) { + tests := map[string]struct { + in string + out []*pb.Svi + errCode codes.Code + errMsg string + size int32 + token string + }{ + "example test": { + in: "", + out: []*pb.Svi{&testSviWithStatus}, + errCode: codes.OK, + errMsg: "", + size: 0, + token: "", + }, + } + + // run tests + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + // start GRPC mockup server + ctx := context.Background() + mockNetlink := mocks.NewNetlink(t) + opi := NewServerWithArgs(mockNetlink) + conn, err := grpc.DialContext(ctx, + "", + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithContextDialer(dialer(opi))) + if err != nil { + log.Fatal(err) + } + defer func(conn *grpc.ClientConn) { + err := conn.Close() + if err != nil { + log.Fatal(err) + } + }(conn) + client := pb.NewSviServiceClient(conn) + + opi.Svis[testSviName] = protoClone(&testSvi) + opi.Svis[testSviName].Name = testSviName + + request := &pb.ListSvisRequest{PageSize: tt.size, PageToken: tt.token} + response, err := client.ListSvis(ctx, request) + if !equalProtoSlices(response.GetSvis(), tt.out) { + t.Error("response: expected", tt.out, "received", response.GetSvis()) + } + + // Empty NextPageToken indicates end of results list + if tt.size != 1 && response.GetNextPageToken() != "" { + t.Error("Expected end of results, received non-empty next page token", response.GetNextPageToken()) + } + + if er, ok := status.FromError(err); ok { + if er.Code() != tt.errCode { + t.Error("error code: expected", tt.errCode, "received", er.Code()) + } + if er.Message() != tt.errMsg { + t.Error("error message: expected", tt.errMsg, "received", er.Message()) + } + } else { + t.Error("expected grpc error status") + } + }) + } +} diff --git a/pkg/evpn/vrf.go b/pkg/evpn/vrf.go index b2fb388d..c3c73525 100644 --- a/pkg/evpn/vrf.go +++ b/pkg/evpn/vrf.go @@ -13,6 +13,7 @@ import ( "math" "net" "path" + "sort" "github.com/vishvananda/netlink" @@ -27,6 +28,12 @@ import ( "google.golang.org/protobuf/types/known/emptypb" ) +func sortVrfs(vrfs []*pb.Vrf) { + sort.Slice(vrfs, func(i int, j int) bool { + return vrfs[i].Name < vrfs[j].Name + }) +} + // CreateVrf executes the creation of the VRF func (s *Server) CreateVrf(_ context.Context, in *pb.CreateVrfRequest) (*pb.Vrf, error) { log.Printf("CreateVrf: Received from client: %v", in) @@ -310,3 +317,23 @@ func (s *Server) GetVrf(_ context.Context, in *pb.GetVrfRequest) (*pb.Vrf, error // TODO return &pb.Vrf{Name: in.Name, Spec: &pb.VrfSpec{Vni: obj.Spec.Vni}, Status: &pb.VrfStatus{LocalAs: 77}}, nil } + +// ListVrfs lists logical bridges +func (s *Server) ListVrfs(_ context.Context, in *pb.ListVrfsRequest) (*pb.ListVrfsResponse, error) { + log.Printf("ListVrfs: Received from client: %v", in) + // check required fields + if err := fieldbehavior.ValidateRequiredFields(in); err != nil { + log.Printf("error: %v", err) + return nil, err + } + token := "" + Blobarray := []*pb.Vrf{} + for _, vrf := range s.Vrfs { + r := protoClone(vrf) + r.Status = &pb.VrfStatus{LocalAs: 4} + Blobarray = append(Blobarray, r) + } + // TODO: fetch object from the database + sortVrfs(Blobarray) + return &pb.ListVrfsResponse{Vrfs: Blobarray, NextPageToken: token}, nil +} diff --git a/pkg/evpn/vrf_test.go b/pkg/evpn/vrf_test.go index e4d39359..6f669bac 100644 --- a/pkg/evpn/vrf_test.go +++ b/pkg/evpn/vrf_test.go @@ -58,6 +58,13 @@ var ( }, }, } + testVrfWithStatus = pb.Vrf{ + Name: testVrfName, + Spec: testVrf.Spec, + Status: &pb.VrfStatus{ + LocalAs: 4, + }, + } ) func Test_CreateVrf(t *testing.T) { @@ -817,3 +824,72 @@ func Test_GetVrf(t *testing.T) { }) } } + +func Test_ListVrfs(t *testing.T) { + tests := map[string]struct { + in string + out []*pb.Vrf + errCode codes.Code + errMsg string + size int32 + token string + }{ + "example test": { + in: "", + out: []*pb.Vrf{&testVrfWithStatus}, + errCode: codes.OK, + errMsg: "", + size: 0, + token: "", + }, + } + + // run tests + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + // start GRPC mockup server + ctx := context.Background() + mockNetlink := mocks.NewNetlink(t) + opi := NewServerWithArgs(mockNetlink) + conn, err := grpc.DialContext(ctx, + "", + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithContextDialer(dialer(opi))) + if err != nil { + log.Fatal(err) + } + defer func(conn *grpc.ClientConn) { + err := conn.Close() + if err != nil { + log.Fatal(err) + } + }(conn) + client := pb.NewVrfServiceClient(conn) + + opi.Vrfs[testVrfName] = protoClone(&testVrf) + opi.Vrfs[testVrfName].Name = testVrfName + + request := &pb.ListVrfsRequest{PageSize: tt.size, PageToken: tt.token} + response, err := client.ListVrfs(ctx, request) + if !equalProtoSlices(response.GetVrfs(), tt.out) { + t.Error("response: expected", tt.out, "received", response.GetVrfs()) + } + + // Empty NextPageToken indicates end of results list + if tt.size != 1 && response.GetNextPageToken() != "" { + t.Error("Expected end of results, received non-empty next page token", response.GetNextPageToken()) + } + + if er, ok := status.FromError(err); ok { + if er.Code() != tt.errCode { + t.Error("error code: expected", tt.errCode, "received", er.Code()) + } + if er.Message() != tt.errMsg { + t.Error("error message: expected", tt.errMsg, "received", er.Message()) + } + } else { + t.Error("expected grpc error status") + } + }) + } +}