...

Source file src/github.com/grpc-ecosystem/grpc-gateway/v2/internal/descriptor/services_test.go

Documentation: github.com/grpc-ecosystem/grpc-gateway/v2/internal/descriptor

     1  package descriptor
     2  
     3  import (
     4  	"reflect"
     5  	"strings"
     6  	"testing"
     7  
     8  	"github.com/grpc-ecosystem/grpc-gateway/v2/internal/httprule"
     9  	"google.golang.org/protobuf/compiler/protogen"
    10  	"google.golang.org/protobuf/encoding/prototext"
    11  	"google.golang.org/protobuf/proto"
    12  	"google.golang.org/protobuf/types/descriptorpb"
    13  )
    14  
    15  func compilePath(t *testing.T, path string) httprule.Template {
    16  	parsed, err := httprule.Parse(path)
    17  	if err != nil {
    18  		t.Fatalf("httprule.Parse(%q) failed with %v; want success", path, err)
    19  	}
    20  	return parsed.Compile()
    21  }
    22  
    23  func testExtractServices(t *testing.T, input []*descriptorpb.FileDescriptorProto, target string, wantSvcs []*Service) {
    24  	testExtractServicesWithRegistry(t, NewRegistry(), input, target, wantSvcs)
    25  }
    26  
    27  func testExtractServicesWithRegistry(t *testing.T, reg *Registry, input []*descriptorpb.FileDescriptorProto, target string, wantSvcs []*Service) {
    28  	for _, file := range input {
    29  		reg.loadFile(file.GetName(), &protogen.File{
    30  			Proto: file,
    31  		})
    32  	}
    33  	err := reg.loadServices(reg.files[target])
    34  	if err != nil {
    35  		t.Errorf("loadServices(%q) failed with %v; want success; files=%v", target, err, input)
    36  	}
    37  
    38  	file := reg.files[target]
    39  	svcs := file.Services
    40  	var i int
    41  	for i = 0; i < len(svcs) && i < len(wantSvcs); i++ {
    42  		svc, wantSvc := svcs[i], wantSvcs[i]
    43  		if got, want := svc.ServiceDescriptorProto, wantSvc.ServiceDescriptorProto; !proto.Equal(got, want) {
    44  			t.Errorf("svcs[%d].ServiceDescriptorProto = %v; want %v; input = %v", i, got, want, input)
    45  			continue
    46  		}
    47  		var j int
    48  		for j = 0; j < len(svc.Methods) && j < len(wantSvc.Methods); j++ {
    49  			meth, wantMeth := svc.Methods[j], wantSvc.Methods[j]
    50  			if got, want := meth.MethodDescriptorProto, wantMeth.MethodDescriptorProto; !proto.Equal(got, want) {
    51  				t.Errorf("svcs[%d].Methods[%d].MethodDescriptorProto = %v; want %v; input = %v", i, j, got, want, input)
    52  				continue
    53  			}
    54  			if got, want := meth.RequestType, wantMeth.RequestType; got.FQMN() != want.FQMN() {
    55  				t.Errorf("svcs[%d].Methods[%d].RequestType = %s; want %s; input = %v", i, j, got.FQMN(), want.FQMN(), input)
    56  			}
    57  			if got, want := meth.ResponseType, wantMeth.ResponseType; got.FQMN() != want.FQMN() {
    58  				t.Errorf("svcs[%d].Methods[%d].ResponseType = %s; want %s; input = %v", i, j, got.FQMN(), want.FQMN(), input)
    59  			}
    60  			var k int
    61  			for k = 0; k < len(meth.Bindings) && k < len(wantMeth.Bindings); k++ {
    62  				binding, wantBinding := meth.Bindings[k], wantMeth.Bindings[k]
    63  				if got, want := binding.Index, wantBinding.Index; got != want {
    64  					t.Errorf("svcs[%d].Methods[%d].Bindings[%d].Index = %d; want %d; input = %v", i, j, k, got, want, input)
    65  				}
    66  				if got, want := binding.PathTmpl, wantBinding.PathTmpl; !reflect.DeepEqual(got, want) {
    67  					t.Errorf("svcs[%d].Methods[%d].Bindings[%d].PathTmpl = %#v; want %#v; input = %v", i, j, k, got, want, input)
    68  				}
    69  				if got, want := binding.HTTPMethod, wantBinding.HTTPMethod; got != want {
    70  					t.Errorf("svcs[%d].Methods[%d].Bindings[%d].HTTPMethod = %q; want %q; input = %v", i, j, k, got, want, input)
    71  				}
    72  
    73  				var l int
    74  				for l = 0; l < len(binding.PathParams) && l < len(wantBinding.PathParams); l++ {
    75  					param, wantParam := binding.PathParams[l], wantBinding.PathParams[l]
    76  					if got, want := param.FieldPath.String(), wantParam.FieldPath.String(); got != want {
    77  						t.Errorf("svcs[%d].Methods[%d].Bindings[%d].PathParams[%d].FieldPath.String() = %q; want %q; input = %v", i, j, k, l, got, want, input)
    78  						continue
    79  					}
    80  					for m := 0; m < len(param.FieldPath) && m < len(wantParam.FieldPath); m++ {
    81  						field, wantField := param.FieldPath[m].Target, wantParam.FieldPath[m].Target
    82  						if got, want := field.FieldDescriptorProto, wantField.FieldDescriptorProto; !proto.Equal(got, want) {
    83  							t.Errorf("svcs[%d].Methods[%d].Bindings[%d].PathParams[%d].FieldPath[%d].Target.FieldDescriptorProto = %v; want %v; input = %v", i, j, k, l, m, got, want, input)
    84  						}
    85  					}
    86  				}
    87  				for ; l < len(binding.PathParams); l++ {
    88  					got := binding.PathParams[l].FieldPath.String()
    89  					t.Errorf("svcs[%d].Methods[%d].Bindings[%d].PathParams[%d] = %q; want it to be missing; input = %v", i, j, k, l, got, input)
    90  				}
    91  				for ; l < len(wantBinding.PathParams); l++ {
    92  					want := wantBinding.PathParams[l].FieldPath.String()
    93  					t.Errorf("svcs[%d].Methods[%d].Bindings[%d].PathParams[%d] missing; want %q; input = %v", i, j, k, l, want, input)
    94  				}
    95  
    96  				if got, want := (binding.Body != nil), (wantBinding.Body != nil); got != want {
    97  					if got {
    98  						t.Errorf("svcs[%d].Methods[%d].Bindings[%d].Body = %q; want it to be missing; input = %v", i, j, k, binding.Body.FieldPath.String(), input)
    99  					} else {
   100  						t.Errorf("svcs[%d].Methods[%d].Bindings[%d].Body missing; want %q; input = %v", i, j, k, wantBinding.Body.FieldPath.String(), input)
   101  					}
   102  				} else if binding.Body != nil {
   103  					if got, want := binding.Body.FieldPath.String(), wantBinding.Body.FieldPath.String(); got != want {
   104  						t.Errorf("svcs[%d].Methods[%d].Bindings[%d].Body = %q; want %q; input = %v", i, j, k, got, want, input)
   105  					}
   106  				}
   107  			}
   108  			for ; k < len(meth.Bindings); k++ {
   109  				got := meth.Bindings[k]
   110  				t.Errorf("svcs[%d].Methods[%d].Bindings[%d] = %v; want it to be missing; input = %v", i, j, k, got, input)
   111  			}
   112  			for ; k < len(wantMeth.Bindings); k++ {
   113  				want := wantMeth.Bindings[k]
   114  				t.Errorf("svcs[%d].Methods[%d].Bindings[%d] missing; want %v; input = %v", i, j, k, want, input)
   115  			}
   116  		}
   117  		for ; j < len(svc.Methods); j++ {
   118  			got := svc.Methods[j].MethodDescriptorProto
   119  			t.Errorf("svcs[%d].Methods[%d] = %v; want it to be missing; input = %v", i, j, got, input)
   120  		}
   121  		for ; j < len(wantSvc.Methods); j++ {
   122  			want := wantSvc.Methods[j].MethodDescriptorProto
   123  			t.Errorf("svcs[%d].Methods[%d] missing; want %v; input = %v", i, j, want, input)
   124  		}
   125  	}
   126  	for ; i < len(svcs); i++ {
   127  		got := svcs[i].ServiceDescriptorProto
   128  		t.Errorf("svcs[%d] = %v; want it to be missing; input = %v", i, got, input)
   129  	}
   130  	for ; i < len(wantSvcs); i++ {
   131  		want := wantSvcs[i].ServiceDescriptorProto
   132  		t.Errorf("svcs[%d] missing; want %v; input = %v", i, want, input)
   133  	}
   134  }
   135  
   136  func crossLinkFixture(f *File) *File {
   137  	for _, m := range f.Messages {
   138  		m.File = f
   139  		for _, f := range m.Fields {
   140  			f.Message = m
   141  		}
   142  	}
   143  	for _, svc := range f.Services {
   144  		svc.File = f
   145  		for _, m := range svc.Methods {
   146  			m.Service = svc
   147  			for _, b := range m.Bindings {
   148  				b.Method = m
   149  				for _, param := range b.PathParams {
   150  					param.Method = m
   151  				}
   152  			}
   153  		}
   154  	}
   155  	for _, e := range f.Enums {
   156  		e.File = f
   157  	}
   158  	return f
   159  }
   160  
   161  func TestExtractServicesSimple(t *testing.T) {
   162  	src := `
   163  		name: "path/to/example.proto",
   164  		package: "example"
   165  		message_type <
   166  			name: "StringMessage"
   167  			field <
   168  				name: "string"
   169  				number: 1
   170  				label: LABEL_OPTIONAL
   171  				type: TYPE_STRING
   172  			>
   173  		>
   174  		service <
   175  			name: "ExampleService"
   176  			method <
   177  				name: "Echo"
   178  				input_type: "StringMessage"
   179  				output_type: "StringMessage"
   180  				options <
   181  					[google.api.http] <
   182  						post: "/v1/example/echo"
   183  						body: "*"
   184  					>
   185  				>
   186  			>
   187  		>
   188  	`
   189  	var fd descriptorpb.FileDescriptorProto
   190  	if err := prototext.Unmarshal([]byte(src), &fd); err != nil {
   191  		t.Fatalf("proto.UnmarshalText(%s, &fd) failed with %v; want success", src, err)
   192  	}
   193  	msg := &Message{
   194  		DescriptorProto: fd.MessageType[0],
   195  		Fields: []*Field{
   196  			{
   197  				FieldDescriptorProto: fd.MessageType[0].Field[0],
   198  			},
   199  		},
   200  	}
   201  	file := &File{
   202  		FileDescriptorProto: &fd,
   203  		GoPkg: GoPackage{
   204  			Path: "path/to/example.pb",
   205  			Name: "example_pb",
   206  		},
   207  		Messages: []*Message{msg},
   208  		Services: []*Service{
   209  			{
   210  				ServiceDescriptorProto: fd.Service[0],
   211  				Methods: []*Method{
   212  					{
   213  						MethodDescriptorProto: fd.Service[0].Method[0],
   214  						RequestType:           msg,
   215  						ResponseType:          msg,
   216  						Bindings: []*Binding{
   217  							{
   218  								PathTmpl:   compilePath(t, "/v1/example/echo"),
   219  								HTTPMethod: "POST",
   220  								Body:       &Body{FieldPath: nil},
   221  							},
   222  						},
   223  					},
   224  				},
   225  			},
   226  		},
   227  	}
   228  
   229  	crossLinkFixture(file)
   230  	testExtractServices(t, []*descriptorpb.FileDescriptorProto{&fd}, "path/to/example.proto", file.Services)
   231  }
   232  
   233  func TestExtractServicesWithoutAnnotation(t *testing.T) {
   234  	src := `
   235  		name: "path/to/example.proto",
   236  		package: "example"
   237  		message_type <
   238  			name: "StringMessage"
   239  			field <
   240  				name: "string"
   241  				number: 1
   242  				label: LABEL_OPTIONAL
   243  				type: TYPE_STRING
   244  			>
   245  		>
   246  		service <
   247  			name: "ExampleService"
   248  			method <
   249  				name: "Echo"
   250  				input_type: "StringMessage"
   251  				output_type: "StringMessage"
   252  			>
   253  		>
   254  	`
   255  	var fd descriptorpb.FileDescriptorProto
   256  	if err := prototext.Unmarshal([]byte(src), &fd); err != nil {
   257  		t.Fatalf("proto.UnmarshalText(%s, &fd) failed with %v; want success", src, err)
   258  	}
   259  	msg := &Message{
   260  		DescriptorProto: fd.MessageType[0],
   261  		Fields: []*Field{
   262  			{
   263  				FieldDescriptorProto: fd.MessageType[0].Field[0],
   264  			},
   265  		},
   266  	}
   267  	file := &File{
   268  		FileDescriptorProto: &fd,
   269  		GoPkg: GoPackage{
   270  			Path: "path/to/example.pb",
   271  			Name: "example_pb",
   272  		},
   273  		Messages: []*Message{msg},
   274  		Services: []*Service{
   275  			{
   276  				ServiceDescriptorProto: fd.Service[0],
   277  				Methods: []*Method{
   278  					{
   279  						MethodDescriptorProto: fd.Service[0].Method[0],
   280  						RequestType:           msg,
   281  						ResponseType:          msg,
   282  					},
   283  				},
   284  			},
   285  		},
   286  	}
   287  
   288  	crossLinkFixture(file)
   289  	testExtractServices(t, []*descriptorpb.FileDescriptorProto{&fd}, "path/to/example.proto", file.Services)
   290  }
   291  
   292  func TestExtractServicesGenerateUnboundMethods(t *testing.T) {
   293  	src := `
   294  		name: "path/to/example.proto",
   295  		package: "example"
   296  		message_type <
   297  			name: "StringMessage"
   298  			field <
   299  				name: "string"
   300  				number: 1
   301  				label: LABEL_OPTIONAL
   302  				type: TYPE_STRING
   303  			>
   304  		>
   305  		service <
   306  			name: "ExampleService"
   307  			method <
   308  				name: "Echo"
   309  				input_type: "StringMessage"
   310  				output_type: "StringMessage"
   311  			>
   312  		>
   313  	`
   314  	var fd descriptorpb.FileDescriptorProto
   315  	if err := prototext.Unmarshal([]byte(src), &fd); err != nil {
   316  		t.Fatalf("prototext.Unmarshal (%s, &fd) failed with %v; want success", src, err)
   317  	}
   318  	msg := &Message{
   319  		DescriptorProto: fd.MessageType[0],
   320  		Fields: []*Field{
   321  			{
   322  				FieldDescriptorProto: fd.MessageType[0].Field[0],
   323  			},
   324  		},
   325  	}
   326  	file := &File{
   327  		FileDescriptorProto: &fd,
   328  		GoPkg: GoPackage{
   329  			Path: "path/to/example.pb",
   330  			Name: "example_pb",
   331  		},
   332  		Messages: []*Message{msg},
   333  		Services: []*Service{
   334  			{
   335  				ServiceDescriptorProto: fd.Service[0],
   336  				Methods: []*Method{
   337  					{
   338  						MethodDescriptorProto: fd.Service[0].Method[0],
   339  						RequestType:           msg,
   340  						ResponseType:          msg,
   341  						Bindings: []*Binding{
   342  							{
   343  								PathTmpl:   compilePath(t, "/example.ExampleService/Echo"),
   344  								HTTPMethod: "POST",
   345  								Body:       &Body{FieldPath: nil},
   346  							},
   347  						},
   348  					},
   349  				},
   350  			},
   351  		},
   352  	}
   353  
   354  	crossLinkFixture(file)
   355  	reg := NewRegistry()
   356  	reg.SetGenerateUnboundMethods(true)
   357  	testExtractServicesWithRegistry(t, reg, []*descriptorpb.FileDescriptorProto{&fd}, "path/to/example.proto", file.Services)
   358  }
   359  
   360  func TestExtractServicesCrossPackage(t *testing.T) {
   361  	srcs := []string{
   362  		`
   363  			name: "path/to/example.proto",
   364  			package: "example"
   365  			message_type <
   366  				name: "StringMessage"
   367  				field <
   368  					name: "string"
   369  					number: 1
   370  					label: LABEL_OPTIONAL
   371  					type: TYPE_STRING
   372  				>
   373  			>
   374  			service <
   375  				name: "ExampleService"
   376  				method <
   377  					name: "ToString"
   378  					input_type: ".another.example.BoolMessage"
   379  					output_type: "StringMessage"
   380  					options <
   381  						[google.api.http] <
   382  							post: "/v1/example/to_s"
   383  							body: "*"
   384  						>
   385  					>
   386  				>
   387  			>
   388  		`, `
   389  			name: "path/to/another/example.proto",
   390  			package: "another.example"
   391  			message_type <
   392  				name: "BoolMessage"
   393  				field <
   394  					name: "bool"
   395  					number: 1
   396  					label: LABEL_OPTIONAL
   397  					type: TYPE_BOOL
   398  				>
   399  			>
   400  		`,
   401  	}
   402  	var fds []*descriptorpb.FileDescriptorProto
   403  	for _, src := range srcs {
   404  		var fd descriptorpb.FileDescriptorProto
   405  		if err := prototext.Unmarshal([]byte(src), &fd); err != nil {
   406  			t.Fatalf("prototext.Unmarshal(%s, &fd) failed with %v; want success", src, err)
   407  		}
   408  		fds = append(fds, &fd)
   409  	}
   410  	stringMsg := &Message{
   411  		DescriptorProto: fds[0].MessageType[0],
   412  		Fields: []*Field{
   413  			{
   414  				FieldDescriptorProto: fds[0].MessageType[0].Field[0],
   415  			},
   416  		},
   417  	}
   418  	boolMsg := &Message{
   419  		DescriptorProto: fds[1].MessageType[0],
   420  		Fields: []*Field{
   421  			{
   422  				FieldDescriptorProto: fds[1].MessageType[0].Field[0],
   423  			},
   424  		},
   425  	}
   426  	files := []*File{
   427  		{
   428  			FileDescriptorProto: fds[0],
   429  			GoPkg: GoPackage{
   430  				Path: "path/to/example.pb",
   431  				Name: "example_pb",
   432  			},
   433  			Messages: []*Message{stringMsg},
   434  			Services: []*Service{
   435  				{
   436  					ServiceDescriptorProto: fds[0].Service[0],
   437  					Methods: []*Method{
   438  						{
   439  							MethodDescriptorProto: fds[0].Service[0].Method[0],
   440  							RequestType:           boolMsg,
   441  							ResponseType:          stringMsg,
   442  							Bindings: []*Binding{
   443  								{
   444  									PathTmpl:   compilePath(t, "/v1/example/to_s"),
   445  									HTTPMethod: "POST",
   446  									Body:       &Body{FieldPath: nil},
   447  								},
   448  							},
   449  						},
   450  					},
   451  				},
   452  			},
   453  		},
   454  		{
   455  			FileDescriptorProto: fds[1],
   456  			GoPkg: GoPackage{
   457  				Path: "path/to/another/example.pb",
   458  				Name: "example_pb",
   459  			},
   460  			Messages: []*Message{boolMsg},
   461  		},
   462  	}
   463  
   464  	for _, file := range files {
   465  		crossLinkFixture(file)
   466  	}
   467  	testExtractServices(t, fds, "path/to/example.proto", files[0].Services)
   468  }
   469  
   470  func TestExtractServicesWithBodyPath(t *testing.T) {
   471  	src := `
   472  		name: "path/to/example.proto",
   473  		package: "example"
   474  		message_type <
   475  			name: "OuterMessage"
   476  			nested_type <
   477  				name: "StringMessage"
   478  				field <
   479  					name: "string"
   480  					number: 1
   481  					label: LABEL_OPTIONAL
   482  					type: TYPE_STRING
   483  				>
   484  			>
   485  			field <
   486  				name: "nested"
   487  				number: 1
   488  				label: LABEL_OPTIONAL
   489  				type: TYPE_MESSAGE
   490  				type_name: "StringMessage"
   491  			>
   492  		>
   493  		service <
   494  			name: "ExampleService"
   495  			method <
   496  				name: "Echo"
   497  				input_type: "OuterMessage"
   498  				output_type: "OuterMessage"
   499  				options <
   500  					[google.api.http] <
   501  						post: "/v1/example/echo"
   502  						body: "nested"
   503  					>
   504  				>
   505  			>
   506  		>
   507  	`
   508  	var fd descriptorpb.FileDescriptorProto
   509  	if err := prototext.Unmarshal([]byte(src), &fd); err != nil {
   510  		t.Fatalf("proto.UnmarshalText(%s, &fd) failed with %v; want success", src, err)
   511  	}
   512  	msg := &Message{
   513  		DescriptorProto: fd.MessageType[0],
   514  		Fields: []*Field{
   515  			{
   516  				FieldDescriptorProto: fd.MessageType[0].Field[0],
   517  			},
   518  		},
   519  	}
   520  	file := &File{
   521  		FileDescriptorProto: &fd,
   522  		GoPkg: GoPackage{
   523  			Path: "path/to/example.pb",
   524  			Name: "example_pb",
   525  		},
   526  		Messages: []*Message{msg},
   527  		Services: []*Service{
   528  			{
   529  				ServiceDescriptorProto: fd.Service[0],
   530  				Methods: []*Method{
   531  					{
   532  						MethodDescriptorProto: fd.Service[0].Method[0],
   533  						RequestType:           msg,
   534  						ResponseType:          msg,
   535  						Bindings: []*Binding{
   536  							{
   537  								PathTmpl:   compilePath(t, "/v1/example/echo"),
   538  								HTTPMethod: "POST",
   539  								Body: &Body{
   540  									FieldPath: FieldPath{
   541  										{
   542  											Name:   "nested",
   543  											Target: msg.Fields[0],
   544  										},
   545  									},
   546  								},
   547  							},
   548  						},
   549  					},
   550  				},
   551  			},
   552  		},
   553  	}
   554  
   555  	crossLinkFixture(file)
   556  	testExtractServices(t, []*descriptorpb.FileDescriptorProto{&fd}, "path/to/example.proto", file.Services)
   557  }
   558  
   559  func TestExtractServicesWithPathParam(t *testing.T) {
   560  	src := `
   561  		name: "path/to/example.proto",
   562  		package: "example"
   563  		message_type <
   564  			name: "StringMessage"
   565  			field <
   566  				name: "string"
   567  				number: 1
   568  				label: LABEL_OPTIONAL
   569  				type: TYPE_STRING
   570  			>
   571  		>
   572  		service <
   573  			name: "ExampleService"
   574  			method <
   575  				name: "Echo"
   576  				input_type: "StringMessage"
   577  				output_type: "StringMessage"
   578  				options <
   579  					[google.api.http] <
   580  						get: "/v1/example/echo/{string=*}"
   581  					>
   582  				>
   583  			>
   584  		>
   585  	`
   586  	var fd descriptorpb.FileDescriptorProto
   587  	if err := prototext.Unmarshal([]byte(src), &fd); err != nil {
   588  		t.Fatalf("proto.UnmarshalText(%s, &fd) failed with %v; want success", src, err)
   589  	}
   590  	msg := &Message{
   591  		DescriptorProto: fd.MessageType[0],
   592  		Fields: []*Field{
   593  			{
   594  				FieldDescriptorProto: fd.MessageType[0].Field[0],
   595  			},
   596  		},
   597  	}
   598  	file := &File{
   599  		FileDescriptorProto: &fd,
   600  		GoPkg: GoPackage{
   601  			Path: "path/to/example.pb",
   602  			Name: "example_pb",
   603  		},
   604  		Messages: []*Message{msg},
   605  		Services: []*Service{
   606  			{
   607  				ServiceDescriptorProto: fd.Service[0],
   608  				Methods: []*Method{
   609  					{
   610  						MethodDescriptorProto: fd.Service[0].Method[0],
   611  						RequestType:           msg,
   612  						ResponseType:          msg,
   613  						Bindings: []*Binding{
   614  							{
   615  								PathTmpl:   compilePath(t, "/v1/example/echo/{string=*}"),
   616  								HTTPMethod: "GET",
   617  								PathParams: []Parameter{
   618  									{
   619  										FieldPath: FieldPath{
   620  											{
   621  												Name:   "string",
   622  												Target: msg.Fields[0],
   623  											},
   624  										},
   625  										Target: msg.Fields[0],
   626  									},
   627  								},
   628  							},
   629  						},
   630  					},
   631  				},
   632  			},
   633  		},
   634  	}
   635  
   636  	crossLinkFixture(file)
   637  	testExtractServices(t, []*descriptorpb.FileDescriptorProto{&fd}, "path/to/example.proto", file.Services)
   638  }
   639  
   640  func TestExtractServicesWithAdditionalBinding(t *testing.T) {
   641  	src := `
   642  		name: "path/to/example.proto",
   643  		package: "example"
   644  		message_type <
   645  			name: "StringMessage"
   646  			field <
   647  				name: "string"
   648  				number: 1
   649  				label: LABEL_OPTIONAL
   650  				type: TYPE_STRING
   651  			>
   652  		>
   653  		service <
   654  			name: "ExampleService"
   655  			method <
   656  				name: "Echo"
   657  				input_type: "StringMessage"
   658  				output_type: "StringMessage"
   659  				options <
   660  					[google.api.http] <
   661  						post: "/v1/example/echo"
   662  						body: "*"
   663  						additional_bindings <
   664  							get: "/v1/example/echo/{string}"
   665  						>
   666  						additional_bindings <
   667  							post: "/v2/example/echo"
   668  							body: "string"
   669  						>
   670  					>
   671  				>
   672  			>
   673  		>
   674  	`
   675  	var fd descriptorpb.FileDescriptorProto
   676  	if err := prototext.Unmarshal([]byte(src), &fd); err != nil {
   677  		t.Fatalf("proto.UnmarshalText(%s, &fd) failed with %v; want success", src, err)
   678  	}
   679  	msg := &Message{
   680  		DescriptorProto: fd.MessageType[0],
   681  		Fields: []*Field{
   682  			{
   683  				FieldDescriptorProto: fd.MessageType[0].Field[0],
   684  			},
   685  		},
   686  	}
   687  	file := &File{
   688  		FileDescriptorProto: &fd,
   689  		GoPkg: GoPackage{
   690  			Path: "path/to/example.pb",
   691  			Name: "example_pb",
   692  		},
   693  		Messages: []*Message{msg},
   694  		Services: []*Service{
   695  			{
   696  				ServiceDescriptorProto: fd.Service[0],
   697  				Methods: []*Method{
   698  					{
   699  						MethodDescriptorProto: fd.Service[0].Method[0],
   700  						RequestType:           msg,
   701  						ResponseType:          msg,
   702  						Bindings: []*Binding{
   703  							{
   704  								Index:      0,
   705  								PathTmpl:   compilePath(t, "/v1/example/echo"),
   706  								HTTPMethod: "POST",
   707  								Body:       &Body{FieldPath: nil},
   708  							},
   709  							{
   710  								Index:      1,
   711  								PathTmpl:   compilePath(t, "/v1/example/echo/{string}"),
   712  								HTTPMethod: "GET",
   713  								PathParams: []Parameter{
   714  									{
   715  										FieldPath: FieldPath{
   716  											{
   717  												Name:   "string",
   718  												Target: msg.Fields[0],
   719  											},
   720  										},
   721  										Target: msg.Fields[0],
   722  									},
   723  								},
   724  								Body: nil,
   725  							},
   726  							{
   727  								Index:      2,
   728  								PathTmpl:   compilePath(t, "/v2/example/echo"),
   729  								HTTPMethod: "POST",
   730  								Body: &Body{
   731  									FieldPath: FieldPath{
   732  										FieldPathComponent{
   733  											Name:   "string",
   734  											Target: msg.Fields[0],
   735  										},
   736  									},
   737  								},
   738  							},
   739  						},
   740  					},
   741  				},
   742  			},
   743  		},
   744  	}
   745  
   746  	crossLinkFixture(file)
   747  	testExtractServices(t, []*descriptorpb.FileDescriptorProto{&fd}, "path/to/example.proto", file.Services)
   748  }
   749  
   750  func TestExtractServicesWithError(t *testing.T) {
   751  	for _, spec := range []struct {
   752  		target string
   753  		srcs   []string
   754  	}{
   755  		{
   756  			target: "path/to/example.proto",
   757  			srcs: []string{
   758  				// message not found
   759  				`
   760  					name: "path/to/example.proto",
   761  					package: "example"
   762  					service <
   763  						name: "ExampleService"
   764  						method <
   765  							name: "Echo"
   766  							input_type: "StringMessage"
   767  							output_type: "StringMessage"
   768  							options <
   769  								[google.api.http] <
   770  									post: "/v1/example/echo"
   771  									body: "*"
   772  								>
   773  							>
   774  						>
   775  					>
   776  				`,
   777  			},
   778  		},
   779  		// body field path not resolved
   780  		{
   781  			target: "path/to/example.proto",
   782  			srcs: []string{`
   783  						name: "path/to/example.proto",
   784  						package: "example"
   785  						message_type <
   786  							name: "StringMessage"
   787  							field <
   788  								name: "string"
   789  								number: 1
   790  								label: LABEL_OPTIONAL
   791  								type: TYPE_STRING
   792  							>
   793  						>
   794  						service <
   795  							name: "ExampleService"
   796  							method <
   797  								name: "Echo"
   798  								input_type: "StringMessage"
   799  								output_type: "StringMessage"
   800  								options <
   801  									[google.api.http] <
   802  										post: "/v1/example/echo"
   803  										body: "bool"
   804  									>
   805  								>
   806  							>
   807  						>`,
   808  			},
   809  		},
   810  		// param field path not resolved
   811  		{
   812  			target: "path/to/example.proto",
   813  			srcs: []string{
   814  				`
   815  					name: "path/to/example.proto",
   816  					package: "example"
   817  					message_type <
   818  						name: "StringMessage"
   819  						field <
   820  							name: "string"
   821  							number: 1
   822  							label: LABEL_OPTIONAL
   823  							type: TYPE_STRING
   824  						>
   825  					>
   826  					service <
   827  						name: "ExampleService"
   828  						method <
   829  							name: "Echo"
   830  							input_type: "StringMessage"
   831  							output_type: "StringMessage"
   832  							options <
   833  								[google.api.http] <
   834  									post: "/v1/example/echo/{bool=*}"
   835  								>
   836  							>
   837  						>
   838  					>
   839  				`,
   840  			},
   841  		},
   842  		// non aggregate type on field path
   843  		{
   844  			target: "path/to/example.proto",
   845  			srcs: []string{
   846  				`
   847  					name: "path/to/example.proto",
   848  					package: "example"
   849  					message_type <
   850  						name: "OuterMessage"
   851  						field <
   852  							name: "mid"
   853  							number: 1
   854  							label: LABEL_OPTIONAL
   855  							type: TYPE_STRING
   856  						>
   857  						field <
   858  							name: "bool"
   859  							number: 2
   860  							label: LABEL_OPTIONAL
   861  							type: TYPE_BOOL
   862  						>
   863  					>
   864  					service <
   865  						name: "ExampleService"
   866  						method <
   867  							name: "Echo"
   868  							input_type: "OuterMessage"
   869  							output_type: "OuterMessage"
   870  							options <
   871  								[google.api.http] <
   872  									post: "/v1/example/echo/{mid.bool=*}"
   873  								>
   874  							>
   875  						>
   876  					>
   877  				`,
   878  			},
   879  		},
   880  		// path param in client streaming
   881  		{
   882  			target: "path/to/example.proto",
   883  			srcs: []string{
   884  				`
   885  					name: "path/to/example.proto",
   886  					package: "example"
   887  					message_type <
   888  						name: "StringMessage"
   889  						field <
   890  							name: "string"
   891  							number: 1
   892  							label: LABEL_OPTIONAL
   893  							type: TYPE_STRING
   894  						>
   895  					>
   896  					service <
   897  						name: "ExampleService"
   898  						method <
   899  							name: "Echo"
   900  							input_type: "StringMessage"
   901  							output_type: "StringMessage"
   902  							options <
   903  								[google.api.http] <
   904  									post: "/v1/example/echo/{bool=*}"
   905  								>
   906  							>
   907  							client_streaming: true
   908  						>
   909  					>
   910  				`,
   911  			},
   912  		},
   913  		// body for GET
   914  		{
   915  			target: "path/to/example.proto",
   916  			srcs: []string{
   917  				`
   918  					name: "path/to/example.proto",
   919  					package: "example"
   920  					message_type <
   921  						name: "StringMessage"
   922  						field <
   923  							name: "string"
   924  							number: 1
   925  							label: LABEL_OPTIONAL
   926  							type: TYPE_STRING
   927  						>
   928  					>
   929  					service <
   930  						name: "ExampleService"
   931  						method <
   932  							name: "Echo"
   933  							input_type: "StringMessage"
   934  							output_type: "StringMessage"
   935  							options <
   936  								[google.api.http] <
   937  									get: "/v1/example/echo"
   938  									body: "string"
   939  								>
   940  							>
   941  						>
   942  					>
   943  				`,
   944  			},
   945  		},
   946  		// body for DELETE
   947  		{
   948  			target: "path/to/example.proto",
   949  			srcs: []string{
   950  				`
   951  					name: "path/to/example.proto",
   952  					package: "example"
   953  					message_type <
   954  						name: "StringMessage"
   955  						field <
   956  							name: "string"
   957  							number: 1
   958  							label: LABEL_OPTIONAL
   959  							type: TYPE_STRING
   960  						>
   961  					>
   962  					service <
   963  						name: "ExampleService"
   964  						method <
   965  							name: "RemoveResource"
   966  							input_type: "StringMessage"
   967  							output_type: "StringMessage"
   968  							options <
   969  								[google.api.http] <
   970  									delete: "/v1/example/resource"
   971  									body: "string"
   972  								>
   973  							>
   974  						>
   975  					>
   976  				`,
   977  			},
   978  		},
   979  		// no pattern specified
   980  		{
   981  			target: "path/to/example.proto",
   982  			srcs: []string{
   983  				`
   984  					name: "path/to/example.proto",
   985  					package: "example"
   986  					service <
   987  						name: "ExampleService"
   988  						method <
   989  							name: "RemoveResource"
   990  							input_type: "StringMessage"
   991  							output_type: "StringMessage"
   992  							options <
   993  								[google.api.http] <
   994  									body: "string"
   995  								>
   996  							>
   997  						>
   998  					>
   999  				`,
  1000  			},
  1001  		},
  1002  		// unsupported path parameter type
  1003  		{
  1004  			target: "path/to/example.proto",
  1005  			srcs: []string{`
  1006  					name: "path/to/example.proto",
  1007  					package: "example"
  1008  					message_type <
  1009  						name: "OuterMessage"
  1010  						nested_type <
  1011  							name: "StringMessage"
  1012  							field <
  1013  								name: "value"
  1014  								number: 1
  1015  								label: LABEL_OPTIONAL
  1016  								type: TYPE_STRING
  1017  							>
  1018  						>
  1019  						field <
  1020  							name: "string"
  1021  							number: 1
  1022  							label: LABEL_OPTIONAL
  1023  							type: TYPE_MESSAGE
  1024  							type_name: "StringMessage"
  1025  						>
  1026  					>
  1027  					service <
  1028  						name: "ExampleService"
  1029  						method <
  1030  							name: "Echo"
  1031  							input_type: "OuterMessage"
  1032  							output_type: "OuterMessage"
  1033  							options <
  1034  								[google.api.http] <
  1035  									get: "/v1/example/echo/{string=*}"
  1036  								>
  1037  							>
  1038  						>
  1039  					>
  1040  				`,
  1041  			},
  1042  		},
  1043  	} {
  1044  		reg := NewRegistry()
  1045  
  1046  		for _, src := range spec.srcs {
  1047  			var fd descriptorpb.FileDescriptorProto
  1048  			if err := prototext.Unmarshal([]byte(src), &fd); err != nil {
  1049  				t.Fatalf("proto.UnmarshalText(%s, &fd) failed with %v; want success", src, err)
  1050  			}
  1051  			reg.loadFile(spec.target, &protogen.File{
  1052  				Proto: &fd,
  1053  			})
  1054  		}
  1055  		err := reg.loadServices(reg.files[spec.target])
  1056  		if err == nil {
  1057  			t.Errorf("loadServices(%q) succeeded; want an error; files=%v", spec.target, spec.srcs)
  1058  		}
  1059  		t.Log(err)
  1060  	}
  1061  }
  1062  
  1063  func TestResolveFieldPath(t *testing.T) {
  1064  	for _, spec := range []struct {
  1065  		src     string
  1066  		path    string
  1067  		wantErr bool
  1068  	}{
  1069  		{
  1070  			src: `
  1071  				name: 'example.proto'
  1072  				package: 'example'
  1073  				message_type <
  1074  					name: 'ExampleMessage'
  1075  					field <
  1076  						name: 'string'
  1077  						type: TYPE_STRING
  1078  						label: LABEL_OPTIONAL
  1079  						number: 1
  1080  					>
  1081  				>
  1082  			`,
  1083  			path:    "string",
  1084  			wantErr: false,
  1085  		},
  1086  		// no such field
  1087  		{
  1088  			src: `
  1089  				name: 'example.proto'
  1090  				package: 'example'
  1091  				message_type <
  1092  					name: 'ExampleMessage'
  1093  					field <
  1094  						name: 'string'
  1095  						type: TYPE_STRING
  1096  						label: LABEL_OPTIONAL
  1097  						number: 1
  1098  					>
  1099  				>
  1100  			`,
  1101  			path:    "something_else",
  1102  			wantErr: true,
  1103  		},
  1104  		// repeated field
  1105  		{
  1106  			src: `
  1107  				name: 'example.proto'
  1108  				package: 'example'
  1109  				message_type <
  1110  					name: 'ExampleMessage'
  1111  					field <
  1112  						name: 'string'
  1113  						type: TYPE_STRING
  1114  						label: LABEL_REPEATED
  1115  						number: 1
  1116  					>
  1117  				>
  1118  			`,
  1119  			path:    "string",
  1120  			wantErr: false,
  1121  		},
  1122  		// nested field
  1123  		{
  1124  			src: `
  1125  				name: 'example.proto'
  1126  				package: 'example'
  1127  				message_type <
  1128  					name: 'ExampleMessage'
  1129  					field <
  1130  						name: 'nested'
  1131  						type: TYPE_MESSAGE
  1132  						type_name: 'AnotherMessage'
  1133  						label: LABEL_OPTIONAL
  1134  						number: 1
  1135  					>
  1136  					field <
  1137  						name: 'terminal'
  1138  						type: TYPE_BOOL
  1139  						label: LABEL_OPTIONAL
  1140  						number: 2
  1141  					>
  1142  				>
  1143  				message_type <
  1144  					name: 'AnotherMessage'
  1145  					field <
  1146  						name: 'nested2'
  1147  						type: TYPE_MESSAGE
  1148  						type_name: 'ExampleMessage'
  1149  						label: LABEL_OPTIONAL
  1150  						number: 1
  1151  					>
  1152  				>
  1153  			`,
  1154  			path:    "nested.nested2.nested.nested2.nested.nested2.terminal",
  1155  			wantErr: false,
  1156  		},
  1157  		// non aggregate field on the path
  1158  		{
  1159  			src: `
  1160  				name: 'example.proto'
  1161  				package: 'example'
  1162  				message_type <
  1163  					name: 'ExampleMessage'
  1164  					field <
  1165  						name: 'nested'
  1166  						type: TYPE_MESSAGE
  1167  						type_name: 'AnotherMessage'
  1168  						label: LABEL_OPTIONAL
  1169  						number: 1
  1170  					>
  1171  					field <
  1172  						name: 'terminal'
  1173  						type: TYPE_BOOL
  1174  						label: LABEL_OPTIONAL
  1175  						number: 2
  1176  					>
  1177  				>
  1178  				message_type <
  1179  					name: 'AnotherMessage'
  1180  					field <
  1181  						name: 'nested2'
  1182  						type: TYPE_MESSAGE
  1183  						type_name: 'ExampleMessage'
  1184  						label: LABEL_OPTIONAL
  1185  						number: 1
  1186  					>
  1187  				>
  1188  			`,
  1189  			path:    "nested.terminal.nested2",
  1190  			wantErr: true,
  1191  		},
  1192  		// repeated field
  1193  		{
  1194  			src: `
  1195  				name: 'example.proto'
  1196  				package: 'example'
  1197  				message_type <
  1198  					name: 'ExampleMessage'
  1199  					field <
  1200  						name: 'nested'
  1201  						type: TYPE_MESSAGE
  1202  						type_name: 'AnotherMessage'
  1203  						label: LABEL_OPTIONAL
  1204  						number: 1
  1205  					>
  1206  					field <
  1207  						name: 'terminal'
  1208  						type: TYPE_BOOL
  1209  						label: LABEL_OPTIONAL
  1210  						number: 2
  1211  					>
  1212  				>
  1213  				message_type <
  1214  					name: 'AnotherMessage'
  1215  					field <
  1216  						name: 'nested2'
  1217  						type: TYPE_MESSAGE
  1218  						type_name: 'ExampleMessage'
  1219  						label: LABEL_REPEATED
  1220  						number: 1
  1221  					>
  1222  				>
  1223  			`,
  1224  			path:    "nested.nested2.terminal",
  1225  			wantErr: false,
  1226  		},
  1227  	} {
  1228  		var file descriptorpb.FileDescriptorProto
  1229  		if err := prototext.Unmarshal([]byte(spec.src), &file); err != nil {
  1230  			t.Fatalf("proto.Unmarshal(%s) failed with %v; want success", spec.src, err)
  1231  		}
  1232  		reg := NewRegistry()
  1233  		reg.loadFile(file.GetName(), &protogen.File{
  1234  			Proto: &file,
  1235  		})
  1236  		f, err := reg.LookupFile(file.GetName())
  1237  		if err != nil {
  1238  			t.Fatalf("reg.LookupFile(%q) failed with %v; want success; on file=%s", file.GetName(), err, spec.src)
  1239  		}
  1240  		_, err = reg.resolveFieldPath(f.Messages[0], spec.path, false)
  1241  		if got, want := err != nil, spec.wantErr; got != want {
  1242  			if want {
  1243  				t.Errorf("reg.resolveFiledPath(%q, %q) succeeded; want an error", f.Messages[0].GetName(), spec.path)
  1244  				continue
  1245  			}
  1246  			t.Errorf("reg.resolveFiledPath(%q, %q) failed with %v; want success", f.Messages[0].GetName(), spec.path, err)
  1247  		}
  1248  	}
  1249  }
  1250  
  1251  func TestExtractServicesWithDeleteBody(t *testing.T) {
  1252  	for _, spec := range []struct {
  1253  		allowDeleteBody bool
  1254  		expectErr       bool
  1255  		target          string
  1256  		srcs            []string
  1257  	}{
  1258  		// body for DELETE, but registry configured to allow it
  1259  		{
  1260  			allowDeleteBody: true,
  1261  			expectErr:       false,
  1262  			target:          "path/to/example.proto",
  1263  			srcs: []string{
  1264  				`
  1265  					name: "path/to/example.proto",
  1266  					package: "example"
  1267  					message_type <
  1268  						name: "StringMessage"
  1269  						field <
  1270  							name: "string"
  1271  							number: 1
  1272  							label: LABEL_OPTIONAL
  1273  							type: TYPE_STRING
  1274  						>
  1275  					>
  1276  					service <
  1277  						name: "ExampleService"
  1278  						method <
  1279  							name: "RemoveResource"
  1280  							input_type: "StringMessage"
  1281  							output_type: "StringMessage"
  1282  							options <
  1283  								[google.api.http] <
  1284  									delete: "/v1/example/resource"
  1285  									body: "string"
  1286  								>
  1287  							>
  1288  						>
  1289  					>
  1290  				`,
  1291  			},
  1292  		},
  1293  		// body for DELETE, registry configured not to allow it
  1294  		{
  1295  			allowDeleteBody: false,
  1296  			expectErr:       true,
  1297  			target:          "path/to/example.proto",
  1298  			srcs: []string{
  1299  				`
  1300  					name: "path/to/example.proto",
  1301  					package: "example"
  1302  					message_type <
  1303  						name: "StringMessage"
  1304  						field <
  1305  							name: "string"
  1306  							number: 1
  1307  							label: LABEL_OPTIONAL
  1308  							type: TYPE_STRING
  1309  						>
  1310  					>
  1311  					service <
  1312  						name: "ExampleService"
  1313  						method <
  1314  							name: "RemoveResource"
  1315  							input_type: "StringMessage"
  1316  							output_type: "StringMessage"
  1317  							options <
  1318  								[google.api.http] <
  1319  									delete: "/v1/example/resource"
  1320  									body: "string"
  1321  								>
  1322  							>
  1323  						>
  1324  					>
  1325  				`,
  1326  			},
  1327  		},
  1328  	} {
  1329  		reg := NewRegistry()
  1330  		reg.SetAllowDeleteBody(spec.allowDeleteBody)
  1331  
  1332  		for _, src := range spec.srcs {
  1333  			var fd descriptorpb.FileDescriptorProto
  1334  			if err := prototext.Unmarshal([]byte(src), &fd); err != nil {
  1335  				t.Fatalf("proto.UnmarshalText(%s, &fd) failed with %v; want success", src, err)
  1336  			}
  1337  			reg.loadFile(fd.GetName(), &protogen.File{
  1338  				Proto: &fd,
  1339  			})
  1340  		}
  1341  		err := reg.loadServices(reg.files[spec.target])
  1342  		if spec.expectErr && err == nil {
  1343  			t.Errorf("loadServices(%q) succeeded; want an error; allowDeleteBody=%v, files=%v", spec.target, spec.allowDeleteBody, spec.srcs)
  1344  		}
  1345  		if !spec.expectErr && err != nil {
  1346  			t.Errorf("loadServices(%q) failed; do not want an error; allowDeleteBody=%v, files=%v", spec.target, spec.allowDeleteBody, spec.srcs)
  1347  		}
  1348  		t.Log(err)
  1349  	}
  1350  }
  1351  
  1352  func TestCauseErrorWithPathParam(t *testing.T) {
  1353  	src := `
  1354  		name: "path/to/example.proto",
  1355  		package: "example"
  1356  		message_type <
  1357  			name: "TypeMessage"
  1358  			field <
  1359  					name: "message"
  1360  					type: TYPE_MESSAGE
  1361  					type_name: 'ExampleMessage'
  1362  					number: 1,
  1363  					label: LABEL_OPTIONAL
  1364  				>
  1365  		>
  1366  		service <
  1367  			name: "ExampleService"
  1368  			method <
  1369  				name: "Echo"
  1370  				input_type: "TypeMessage"
  1371  				output_type: "TypeMessage"
  1372  				options <
  1373  					[google.api.http] <
  1374  						get: "/v1/example/echo/{message=*}"
  1375  					>
  1376  				>
  1377  			>
  1378  		>
  1379  	`
  1380  	var fd descriptorpb.FileDescriptorProto
  1381  	if err := prototext.Unmarshal([]byte(src), &fd); err != nil {
  1382  		t.Fatalf("proto.UnmarshalText(%s, &fd) failed with %v; want success", src, err)
  1383  	}
  1384  	target := "path/to/example.proto"
  1385  	reg := NewRegistry()
  1386  	input := []*descriptorpb.FileDescriptorProto{&fd}
  1387  	reg.loadFile(fd.GetName(), &protogen.File{
  1388  		Proto: &fd,
  1389  	})
  1390  	// switch this field to see the error
  1391  	wantErr := true
  1392  	err := reg.loadServices(reg.files[target])
  1393  	if got, want := err != nil, wantErr; got != want {
  1394  		if want {
  1395  			t.Errorf("loadServices(%q, %q) succeeded; want an error", target, input)
  1396  		}
  1397  		t.Errorf("loadServices(%q, %q) failed with %v; want success", target, input, err)
  1398  	}
  1399  }
  1400  
  1401  func TestOptionalProto3URLPathMappingError(t *testing.T) {
  1402  	src := `
  1403  		name: "path/to/example.proto"
  1404  		package: "example"
  1405  		message_type <
  1406  			name: "StringMessage"
  1407  			field <
  1408  				name: "field1"
  1409  				number: 1
  1410  				type: TYPE_STRING
  1411  				proto3_optional: true
  1412  			>
  1413  		>
  1414  		service <
  1415  			name: "ExampleService"
  1416  			method <
  1417  				name: "Echo"
  1418  				input_type: "StringMessage"
  1419  				output_type: "StringMessage"
  1420  				options <
  1421  					[google.api.http] <
  1422  						get: "/v1/example/echo/{field1=*}"
  1423  					>
  1424  				>
  1425  			>
  1426  		>
  1427  	`
  1428  	var fd descriptorpb.FileDescriptorProto
  1429  	if err := prototext.Unmarshal([]byte(src), &fd); err != nil {
  1430  		t.Fatalf("proto.UnmarshalText(%s, &fd) failed with %v; want success", src, err)
  1431  	}
  1432  	target := "path/to/example.proto"
  1433  	reg := NewRegistry()
  1434  	input := []*descriptorpb.FileDescriptorProto{&fd}
  1435  	reg.loadFile(fd.GetName(), &protogen.File{
  1436  		Proto: &fd,
  1437  	})
  1438  	wantErrMsg := "field not allowed in field path: field1 in field1"
  1439  	err := reg.loadServices(reg.files[target])
  1440  	if err != nil {
  1441  		if !strings.Contains(err.Error(), wantErrMsg) {
  1442  			t.Errorf("loadServices(%q, %q) failed with %v; want %s", target, input, err, wantErrMsg)
  1443  		}
  1444  	} else {
  1445  		t.Errorf("loadServices(%q, %q) expcted an error %s, got nil", target, input, wantErrMsg)
  1446  	}
  1447  }
  1448  

View as plain text