func (r staticRegistry) GetHandlerSpec(service string, procedure string) (transport.HandlerSpec, error) { if procedure == testProcedure { return transport.NewUnaryHandlerSpec(r.Handler), nil } else { return transport.NewOnewayHandlerSpec(r.OnewayHandler), nil } }
// Procedure builds a Registrant from the given raw handler. func Procedure(name string, handler UnaryHandler) []transport.Registrant { return []transport.Registrant{ { Procedure: name, HandlerSpec: transport.NewUnaryHandlerSpec(rawUnaryHandler{handler}), }, } }
func TestMapRegistry_ServiceProcedures(t *testing.T) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() m := transport.NewMapRegistry("myservice") bar := transporttest.NewMockUnaryHandler(mockCtrl) foo := transporttest.NewMockUnaryHandler(mockCtrl) aww := transporttest.NewMockUnaryHandler(mockCtrl) m.Register([]transport.Registrant{ { Service: "anotherservice", Procedure: "bar", HandlerSpec: transport.NewUnaryHandlerSpec(bar), }, { Procedure: "foo", HandlerSpec: transport.NewUnaryHandlerSpec(foo), }, { Service: "anotherservice", Procedure: "aww", HandlerSpec: transport.NewUnaryHandlerSpec(aww), }, }) expectedOrderedServiceProcedures := []transport.ServiceProcedure{ { Service: "anotherservice", Procedure: "aww", }, { Service: "anotherservice", Procedure: "bar", }, { Service: "myservice", Procedure: "foo", }, } serviceProcedures := m.ServiceProcedures() assert.Equal(t, expectedOrderedServiceProcedures, serviceProcedures) }
func TestMapRegistry(t *testing.T) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() m := transport.NewMapRegistry("myservice") foo := transporttest.NewMockUnaryHandler(mockCtrl) bar := transporttest.NewMockUnaryHandler(mockCtrl) m.Register([]transport.Registrant{ { Procedure: "foo", HandlerSpec: transport.NewUnaryHandlerSpec(foo), }, { Service: "anotherservice", Procedure: "bar", HandlerSpec: transport.NewUnaryHandlerSpec(bar), }, }) tests := []struct { service, procedure string want transport.UnaryHandler }{ {"myservice", "foo", foo}, {"", "foo", foo}, {"anotherservice", "foo", nil}, {"", "bar", nil}, {"myservice", "bar", nil}, {"anotherservice", "bar", bar}, } for _, tt := range tests { got, err := m.GetHandlerSpec(tt.service, tt.procedure) if tt.want != nil { assert.NoError(t, err, "GetHandlerSpec(%q, %q) failed", tt.service, tt.procedure) assert.True(t, tt.want == got.Unary(), // want == match, not deep equals "GetHandlerSpec(%q, %q) did not match", tt.service, tt.procedure) } else { assert.Error(t, err) } } }
// Procedure builds a Registrant from the given JSON handler. handler must be // a function with a signature similar to, // // f(ctx context.Context, reqMeta yarpc.ReqMeta, body $reqBody) ($resBody, yarpc.ResMeta, error) // // Where $reqBody and $resBody are a map[string]interface{} or pointers to // structs. func Procedure(name string, handler interface{}) []transport.Registrant { return []transport.Registrant{ { Procedure: name, HandlerSpec: transport.NewUnaryHandlerSpec( wrapUnaryHandler(name, handler), ), }, } }
func TestHandlerInternalFailure(t *testing.T) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() headers := make(http.Header) headers.Set(CallerHeader, "somecaller") headers.Set(EncodingHeader, "raw") headers.Set(TTLMSHeader, "1000") headers.Set(ProcedureHeader, "hello") headers.Set(ServiceHeader, "fake") request := http.Request{ Method: "POST", Header: headers, Body: ioutil.NopCloser(bytes.NewReader([]byte{})), } rpcHandler := transporttest.NewMockUnaryHandler(mockCtrl) rpcHandler.EXPECT().Handle( transporttest.NewContextMatcher(t, transporttest.ContextTTL(time.Second)), transporttest.NewRequestMatcher( t, &transport.Request{ Caller: "somecaller", Service: "fake", Encoding: raw.Encoding, Procedure: "hello", Body: bytes.NewReader([]byte{}), }, ), gomock.Any(), ).Return(fmt.Errorf("great sadness")) registry := transporttest.NewMockRegistry(mockCtrl) spec := transport.NewUnaryHandlerSpec(rpcHandler) registry.EXPECT().GetHandlerSpec("fake", "hello").Return(spec, nil) httpHandler := handler{Registry: registry} httpResponse := httptest.NewRecorder() httpHandler.ServeHTTP(httpResponse, &request) code := httpResponse.Code assert.True(t, code >= 500 && code < 600, "expected 500 level response") assert.Equal(t, `UnexpectedError: error for procedure "hello" of service "fake": great sadness`+"\n", httpResponse.Body.String()) }
func TestHandlerSucces(t *testing.T) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() headers := make(http.Header) headers.Set(CallerHeader, "moe") headers.Set(EncodingHeader, "raw") headers.Set(TTLMSHeader, "1000") headers.Set(ProcedureHeader, "nyuck") headers.Set(ServiceHeader, "curly") registry := transporttest.NewMockRegistry(mockCtrl) rpcHandler := transporttest.NewMockUnaryHandler(mockCtrl) spec := transport.NewUnaryHandlerSpec(rpcHandler) registry.EXPECT().GetHandlerSpec("curly", "nyuck").Return(spec, nil) rpcHandler.EXPECT().Handle( transporttest.NewContextMatcher(t, transporttest.ContextTTL(time.Second), ), transporttest.NewRequestMatcher( t, &transport.Request{ Caller: "moe", Service: "curly", Encoding: raw.Encoding, Procedure: "nyuck", Body: bytes.NewReader([]byte("Nyuck Nyuck")), }, ), gomock.Any(), ).Return(nil) httpHandler := handler{Registry: registry} req := &http.Request{ Method: "POST", Header: headers, Body: ioutil.NopCloser(bytes.NewReader([]byte("Nyuck Nyuck"))), } rw := httptest.NewRecorder() httpHandler.ServeHTTP(rw, req) code := rw.Code assert.Equal(t, code, 200, "expected 200 code") assert.Equal(t, rw.Body.String(), "") }
// BuildRegistrants builds a list of Registrants from a Thrift service // specification. func BuildRegistrants(s Service, opts ...RegisterOption) []transport.Registrant { var rc registerConfig for _, opt := range opts { opt.applyRegisterOption(&rc) } proto := protocol.Binary if rc.Protocol != nil { proto = rc.Protocol } rs := make([]transport.Registrant, 0, len(s.Methods)) // unary procedures for methodName, handler := range s.Methods { spec := transport.NewUnaryHandlerSpec(thriftUnaryHandler{ UnaryHandler: handler, Protocol: proto, Enveloping: rc.Enveloping, }) rs = append(rs, transport.Registrant{ Procedure: procedureName(s.Name, methodName), HandlerSpec: spec, }) } // oneway procedures for methodName, handler := range s.OnewayMethods { spec := transport.NewOnewayHandlerSpec(thriftOnewayHandler{ OnewayHandler: handler, Protocol: proto, Enveloping: rc.Enveloping, }) rs = append(rs, transport.Registrant{ Procedure: procedureName(s.Name, methodName), HandlerSpec: spec, }) } return rs }
func (d dispatcher) Register(rs []transport.Registrant) { registrants := make([]transport.Registrant, 0, len(rs)) for _, r := range rs { switch r.HandlerSpec.Type() { case transport.Unary: h := transport.ApplyInterceptor(r.HandlerSpec.Unary(), d.Interceptor) r.HandlerSpec = transport.NewUnaryHandlerSpec(h) case transport.Oneway: //TODO(apb): add oneway interceptors https://github.com/yarpc/yarpc-go/issues/413 default: panic(fmt.Sprintf("unknown handler type %q for service %q, procedure %q", r.HandlerSpec.Type(), r.Service, r.Procedure)) } registrants = append(registrants, r) } d.Registrar.Register(registrants) }
func TestHandlerPanic(t *testing.T) { inbound := NewInbound("localhost:0") serverDispatcher := yarpc.NewDispatcher(yarpc.Config{ Name: "yarpc-test", Inbounds: []transport.Inbound{inbound}, }) serverDispatcher.Register([]transport.Registrant{ { Procedure: "panic", HandlerSpec: transport.NewUnaryHandlerSpec(panickedHandler{}), }, }) require.NoError(t, serverDispatcher.Start()) defer serverDispatcher.Stop() clientDispatcher := yarpc.NewDispatcher(yarpc.Config{ Name: "yarpc-test-client", Outbounds: yarpc.Outbounds{ "yarpc-test": { Unary: NewOutbound(fmt.Sprintf("http://%s", inbound.Addr().String())), }, }, }) require.NoError(t, clientDispatcher.Start()) defer clientDispatcher.Stop() client := raw.New(clientDispatcher.Channel("yarpc-test")) ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() _, _, err := client.Call(ctx, yarpc.NewReqMeta().Procedure("panic"), []byte{}) assert.True(t, transport.IsUnexpectedError(err), "Must be an UnexpectedError") assert.Equal(t, `UnexpectedError: error for procedure "panic" of service "yarpc-test": panic: oops I panicked!`, err.Error()) }
func TestHandlerHeaders(t *testing.T) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() tests := []struct { giveHeaders http.Header wantTTL time.Duration wantHeaders map[string]string }{ { giveHeaders: http.Header{ TTLMSHeader: {"1000"}, "Rpc-Header-Foo": {"bar"}, }, wantTTL: time.Second, wantHeaders: map[string]string{ "foo": "bar", }, }, { giveHeaders: http.Header{ TTLMSHeader: {"100"}, "Rpc-Foo": {"ignored"}, }, wantTTL: 100 * time.Millisecond, wantHeaders: map[string]string{}, }, } for _, tt := range tests { registry := transporttest.NewMockRegistry(mockCtrl) rpcHandler := transporttest.NewMockUnaryHandler(mockCtrl) spec := transport.NewUnaryHandlerSpec(rpcHandler) registry.EXPECT().GetHandlerSpec("service", "hello").Return(spec, nil) httpHandler := handler{Registry: registry} rpcHandler.EXPECT().Handle( transporttest.NewContextMatcher(t, transporttest.ContextTTL(tt.wantTTL), ), transporttest.NewRequestMatcher(t, &transport.Request{ Caller: "caller", Service: "service", Encoding: raw.Encoding, Procedure: "hello", Headers: transport.HeadersFromMap(tt.wantHeaders), Body: bytes.NewReader([]byte("world")), }), gomock.Any(), ).Return(nil) headers := http.Header{} for k, vs := range tt.giveHeaders { for _, v := range vs { headers.Add(k, v) } } headers.Set(CallerHeader, "caller") headers.Set(ServiceHeader, "service") headers.Set(EncodingHeader, "raw") headers.Set(ProcedureHeader, "hello") req := &http.Request{ Method: "POST", Header: headers, Body: ioutil.NopCloser(bytes.NewReader([]byte("world"))), } rw := httptest.NewRecorder() httpHandler.ServeHTTP(rw, req) assert.Equal(t, 200, rw.Code, "expected 200 status code") } }
func TestHandlerFailures(t *testing.T) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() service, procedure := "fake", "hello" baseHeaders := make(http.Header) baseHeaders.Set(CallerHeader, "somecaller") baseHeaders.Set(EncodingHeader, "raw") baseHeaders.Set(TTLMSHeader, "1000") baseHeaders.Set(ProcedureHeader, procedure) baseHeaders.Set(ServiceHeader, service) headersWithBadTTL := headerCopyWithout(baseHeaders, TTLMSHeader) headersWithBadTTL.Set(TTLMSHeader, "not a number") tests := []struct { req *http.Request msg string // if we expect an error as a result of the TTL errTTL bool }{ {req: &http.Request{Method: "GET"}, msg: "404 page not found\n"}, { req: &http.Request{ Method: "POST", Header: headerCopyWithout(baseHeaders, CallerHeader), }, msg: "BadRequest: missing caller name\n", }, { req: &http.Request{ Method: "POST", Header: headerCopyWithout(baseHeaders, ServiceHeader), }, msg: "BadRequest: missing service name\n", }, { req: &http.Request{ Method: "POST", Header: headerCopyWithout(baseHeaders, ProcedureHeader), }, msg: "BadRequest: missing procedure\n", }, { req: &http.Request{ Method: "POST", Header: headerCopyWithout(baseHeaders, TTLMSHeader), }, msg: "BadRequest: missing TTL\n", errTTL: true, }, { req: &http.Request{ Method: "POST", }, msg: "BadRequest: missing service name, procedure, caller name, and encoding\n", }, { req: &http.Request{ Method: "POST", Header: headersWithBadTTL, }, msg: `BadRequest: invalid TTL "not a number" for procedure "hello" of service "fake": must be positive integer` + "\n", errTTL: true, }, } for _, tt := range tests { req := tt.req if req.Body == nil { req.Body = ioutil.NopCloser(bytes.NewReader([]byte{})) } reg := transporttest.NewMockRegistry(mockCtrl) if tt.errTTL { // since TTL is checked after we've determined the transport type, if we have an // error with TTL it will be discovered after we read from the registry spec := transport.NewUnaryHandlerSpec(panickedHandler{}) reg.EXPECT().GetHandlerSpec(service, procedure).Return(spec, nil) } h := handler{Registry: reg} rw := httptest.NewRecorder() h.ServeHTTP(rw, tt.req) code := rw.Code assert.True(t, code >= 400 && code < 500, "expected 400 level code") assert.Equal(t, rw.Body.String(), tt.msg) } }
func TestInboundMux(t *testing.T) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() mux := http.NewServeMux() mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("healthy")) }) i := NewInbound(":0", Mux("/rpc/v1", mux)) h := transporttest.NewMockUnaryHandler(mockCtrl) reg := transporttest.NewMockRegistry(mockCtrl) require.NoError(t, i.Start(transport.ServiceDetail{Name: "foo", Registry: reg}, transport.NoDeps)) defer i.Stop() addr := fmt.Sprintf("http://%v/", i.Addr().String()) resp, err := http.Get(addr + "health") if assert.NoError(t, err, "/health failed") { defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if assert.NoError(t, err, "/health body read error") { assert.Equal(t, "healthy", string(body), "/health body mismatch") } } // this should fail o := NewOutbound(addr) require.NoError(t, o.Start(transport.NoDeps), "failed to start outbound") defer o.Stop() ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() _, err = o.Call(ctx, &transport.Request{ Caller: "foo", Service: "bar", Procedure: "hello", Encoding: raw.Encoding, Body: bytes.NewReader([]byte("derp")), }) if assert.Error(t, err, "RPC call to / should have failed") { assert.Equal(t, err.Error(), "404 page not found") } o = NewOutbound(addr + "rpc/v1") require.NoError(t, o.Start(transport.NoDeps), "failed to start outbound") defer o.Stop() spec := transport.NewUnaryHandlerSpec(h) reg.EXPECT().GetHandlerSpec("bar", "hello").Return(spec, nil) h.EXPECT().Handle(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) res, err := o.Call(ctx, &transport.Request{ Caller: "foo", Service: "bar", Procedure: "hello", Encoding: raw.Encoding, Body: bytes.NewReader([]byte("derp")), }) if assert.NoError(t, err, "expected rpc request to succeed") { defer res.Body.Close() s, err := ioutil.ReadAll(res.Body) if assert.NoError(t, err) { assert.Empty(t, s) } } }
func TestHandlerErrors(t *testing.T) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() tests := []struct { format tchannel.Format headers []byte wantHeaders map[string]string }{ { format: tchannel.JSON, headers: []byte(`{"Rpc-Header-Foo": "bar"}`), wantHeaders: map[string]string{"rpc-header-foo": "bar"}, }, { format: tchannel.Thrift, headers: []byte{ 0x00, 0x01, // 1 header 0x00, 0x03, 'F', 'o', 'o', // Foo 0x00, 0x03, 'B', 'a', 'r', // Bar }, wantHeaders: map[string]string{"foo": "Bar"}, }, } for _, tt := range tests { rpcHandler := transporttest.NewMockUnaryHandler(mockCtrl) registry := transporttest.NewMockRegistry(mockCtrl) spec := transport.NewUnaryHandlerSpec(rpcHandler) tchHandler := handler{Registry: registry} registry.EXPECT().GetHandlerSpec("service", "hello").Return(spec, nil) rpcHandler.EXPECT().Handle( transporttest.NewContextMatcher(t), transporttest.NewRequestMatcher(t, &transport.Request{ Caller: "caller", Service: "service", Headers: transport.HeadersFromMap(tt.wantHeaders), Encoding: transport.Encoding(tt.format), Procedure: "hello", Body: bytes.NewReader([]byte("world")), }), gomock.Any(), ).Return(nil) respRecorder := newResponseRecorder() ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() tchHandler.handle(ctx, &fakeInboundCall{ service: "service", caller: "caller", format: tt.format, method: "hello", arg2: tt.headers, arg3: []byte("world"), resp: respRecorder, }) assert.NoError(t, respRecorder.systemErr, "did not expect an error") } }
func TestHandlerFailures(t *testing.T) { tests := []struct { desc string // context to use in the callm a default one is used otherwise. ctx context.Context ctxFunc func() (context.Context, context.CancelFunc) sendCall *fakeInboundCall expectCall func(*transporttest.MockUnaryHandler) wantErrors []string // error message contents wantStatus tchannel.SystemErrCode // expected status }{ { desc: "no timeout on context", ctx: context.Background(), sendCall: &fakeInboundCall{ service: "foo", caller: "bar", method: "hello", format: tchannel.Raw, arg2: []byte{0x00, 0x00}, arg3: []byte{0x00}, }, wantErrors: []string{"timeout required"}, wantStatus: tchannel.ErrCodeBadRequest, }, { desc: "arg2 reader error", sendCall: &fakeInboundCall{ service: "foo", caller: "bar", method: "hello", format: tchannel.Raw, arg2: nil, arg3: []byte{0x00}, }, wantErrors: []string{ `BadRequest: failed to decode "raw" request headers for`, `procedure "hello" of service "foo" from caller "bar"`, }, wantStatus: tchannel.ErrCodeBadRequest, }, { desc: "arg2 parse error", sendCall: &fakeInboundCall{ service: "foo", caller: "bar", method: "hello", format: tchannel.JSON, arg2: []byte("{not valid JSON}"), arg3: []byte{0x00}, }, wantErrors: []string{ `BadRequest: failed to decode "json" request headers for`, `procedure "hello" of service "foo" from caller "bar"`, }, wantStatus: tchannel.ErrCodeBadRequest, }, { desc: "arg3 reader error", sendCall: &fakeInboundCall{ service: "foo", caller: "bar", method: "hello", format: tchannel.Raw, arg2: []byte{0x00, 0x00}, arg3: nil, }, wantErrors: []string{ `UnexpectedError: error for procedure "hello" of service "foo"`, }, wantStatus: tchannel.ErrCodeUnexpected, }, { desc: "internal error", sendCall: &fakeInboundCall{ service: "foo", caller: "bar", method: "hello", format: tchannel.Raw, arg2: []byte{0x00, 0x00}, arg3: []byte{0x00}, }, expectCall: func(h *transporttest.MockUnaryHandler) { h.EXPECT().Handle( transporttest.NewContextMatcher(t, transporttest.ContextTTL(time.Second)), transporttest.NewRequestMatcher( t, &transport.Request{ Caller: "bar", Service: "foo", Encoding: raw.Encoding, Procedure: "hello", Body: bytes.NewReader([]byte{0x00}), }, ), gomock.Any(), ).Return(fmt.Errorf("great sadness")) }, wantErrors: []string{ `UnexpectedError: error for procedure "hello" of service "foo":`, "great sadness", }, wantStatus: tchannel.ErrCodeUnexpected, }, { desc: "arg3 encode error", sendCall: &fakeInboundCall{ service: "foo", caller: "bar", method: "hello", format: tchannel.JSON, arg2: []byte("{}"), arg3: []byte("{}"), }, expectCall: func(h *transporttest.MockUnaryHandler) { req := &transport.Request{ Caller: "bar", Service: "foo", Encoding: json.Encoding, Procedure: "hello", Body: bytes.NewReader([]byte("{}")), } h.EXPECT().Handle( transporttest.NewContextMatcher(t, transporttest.ContextTTL(time.Second)), transporttest.NewRequestMatcher(t, req), gomock.Any(), ).Return( encoding.ResponseBodyEncodeError(req, errors.New( "serialization derp", ))) }, wantErrors: []string{ `UnexpectedError: failed to encode "json" response body for`, `procedure "hello" of service "foo" from caller "bar":`, `serialization derp`, }, wantStatus: tchannel.ErrCodeUnexpected, }, { desc: "handler timeout", ctxFunc: func() (context.Context, context.CancelFunc) { return context.WithTimeout(context.Background(), time.Millisecond) }, sendCall: &fakeInboundCall{ service: "foo", caller: "bar", method: "waituntiltimeout", format: tchannel.Raw, arg2: []byte{0x00, 0x00}, arg3: []byte{0x00}, }, expectCall: func(h *transporttest.MockUnaryHandler) { req := &transport.Request{ Service: "foo", Caller: "bar", Procedure: "waituntiltimeout", Encoding: raw.Encoding, Body: bytes.NewReader([]byte{0x00}), } h.EXPECT().Handle( transporttest.NewContextMatcher( t, transporttest.ContextTTL(time.Millisecond)), transporttest.NewRequestMatcher(t, req), gomock.Any(), ).Do(func(ctx context.Context, _ *transport.Request, _ transport.ResponseWriter) { <-ctx.Done() }).Return(context.DeadlineExceeded) }, wantErrors: []string{ `tchannel error ErrCodeTimeout: Timeout: call to procedure "waituntiltimeout" of service "foo" from caller "bar" timed out after `}, wantStatus: tchannel.ErrCodeTimeout, }, { desc: "handler panic", sendCall: &fakeInboundCall{ service: "foo", caller: "bar", method: "panic", format: tchannel.Raw, arg2: []byte{0x00, 0x00}, arg3: []byte{0x00}, }, expectCall: func(h *transporttest.MockUnaryHandler) { req := &transport.Request{ Service: "foo", Caller: "bar", Procedure: "panic", Encoding: raw.Encoding, Body: bytes.NewReader([]byte{0x00}), } h.EXPECT().Handle( transporttest.NewContextMatcher( t, transporttest.ContextTTL(time.Second)), transporttest.NewRequestMatcher(t, req), gomock.Any(), ).Do(func(context.Context, *transport.Request, transport.ResponseWriter) { panic("oops I panicked!") }) }, wantErrors: []string{ `UnexpectedError: error for procedure "panic" of service "foo": panic: oops I panicked!`, }, wantStatus: tchannel.ErrCodeUnexpected, }, } for _, tt := range tests { ctx, cancel := context.WithTimeout(context.Background(), time.Second) if tt.ctx != nil { ctx = tt.ctx } else if tt.ctxFunc != nil { ctx, cancel = tt.ctxFunc() } defer cancel() mockCtrl := gomock.NewController(t) thandler := transporttest.NewMockUnaryHandler(mockCtrl) spec := transport.NewUnaryHandlerSpec(thandler) if tt.expectCall != nil { tt.expectCall(thandler) } resp := newResponseRecorder() tt.sendCall.resp = resp registry := transporttest.NewMockRegistry(mockCtrl) registry.EXPECT().GetHandlerSpec(tt.sendCall.service, tt.sendCall.method). Return(spec, nil).AnyTimes() handler{Registry: registry}.handle(ctx, tt.sendCall) err := resp.systemErr require.Error(t, err, "expected error for %q", tt.desc) systemErr, isSystemErr := err.(tchannel.SystemError) require.True(t, isSystemErr, "expected %v for %q to be a system error", err, tt.desc) assert.Equal(t, tt.wantStatus, systemErr.Code(), tt.desc) for _, msg := range tt.wantErrors { assert.Contains( t, err.Error(), msg, "error should contain message for %q", tt.desc) } mockCtrl.Finish() } }