go.uber.org/yarpc@v1.72.1/yarpcconfig/configurator_test.go (about)

     1  // Copyright (c) 2022 Uber Technologies, Inc.
     2  //
     3  // Permission is hereby granted, free of charge, to any person obtaining a copy
     4  // of this software and associated documentation files (the "Software"), to deal
     5  // in the Software without restriction, including without limitation the rights
     6  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     7  // copies of the Software, and to permit persons to whom the Software is
     8  // furnished to do so, subject to the following conditions:
     9  //
    10  // The above copyright notice and this permission notice shall be included in
    11  // all copies or substantial portions of the Software.
    12  //
    13  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    14  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    15  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    16  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    17  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    18  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    19  // THE SOFTWARE.
    20  
    21  package yarpcconfig
    22  
    23  import (
    24  	"reflect"
    25  	"strings"
    26  	"testing"
    27  	"time"
    28  
    29  	"github.com/golang/mock/gomock"
    30  	"github.com/stretchr/testify/assert"
    31  	"github.com/stretchr/testify/require"
    32  	"go.uber.org/yarpc"
    33  	"go.uber.org/yarpc/api/transport/transporttest"
    34  	"go.uber.org/yarpc/internal/interpolate"
    35  	"go.uber.org/yarpc/internal/whitespace"
    36  	"go.uber.org/zap/zapcore"
    37  	"gopkg.in/yaml.v2"
    38  )
    39  
    40  func TestConfiguratorRegisterErrors(t *testing.T) {
    41  	require.Panics(t, func() { New().MustRegisterTransport(TransportSpec{}) })
    42  	err := New().RegisterTransport(TransportSpec{})
    43  	require.Error(t, err, "expected failure")
    44  	assert.Contains(t, err.Error(), "name is required")
    45  	err = New().RegisterTransport(TransportSpec{Name: "test"})
    46  	require.Error(t, err, "expected failure")
    47  	assert.Contains(t, err.Error(), "invalid TransportSpec for \"test\":")
    48  
    49  	require.Panics(t, func() { New().MustRegisterPeerChooser(PeerChooserSpec{}) })
    50  	err = New().RegisterPeerChooser(PeerChooserSpec{})
    51  	require.Error(t, err, "expected failure")
    52  	assert.Contains(t, err.Error(), "name is required")
    53  	err = New().RegisterPeerChooser(PeerChooserSpec{Name: "test"})
    54  	require.Error(t, err, "expected failure")
    55  	assert.Contains(t, err.Error(), "invalid PeerChooserSpec for \"test\":")
    56  
    57  	require.Panics(t, func() { New().MustRegisterPeerList(PeerListSpec{}) })
    58  	err = New().RegisterPeerList(PeerListSpec{})
    59  	require.Error(t, err, "expected failure")
    60  	assert.Contains(t, err.Error(), "name is required")
    61  	err = New().RegisterPeerList(PeerListSpec{Name: "test"})
    62  	require.Error(t, err, "expected failure")
    63  	assert.Contains(t, err.Error(), "invalid PeerListSpec for \"test\":")
    64  
    65  	require.Panics(t, func() { New().MustRegisterPeerListUpdater(PeerListUpdaterSpec{}) })
    66  	err = New().RegisterPeerListUpdater(PeerListUpdaterSpec{})
    67  	require.Error(t, err, "expected failure")
    68  	assert.Contains(t, err.Error(), "name is required")
    69  	err = New().RegisterPeerListUpdater(PeerListUpdaterSpec{Name: "test"})
    70  	require.Error(t, err, "expected failure")
    71  	assert.Contains(t, err.Error(), "invalid PeerListUpdaterSpec for \"test\":")
    72  }
    73  
    74  func TestConfigurator(t *testing.T) {
    75  	// For better test output, we have split the test case into a testCase
    76  	// struct that defines the test parameters and a different anonymous
    77  	// struct used in the table test to give a name to the test.
    78  
    79  	type testCase struct {
    80  		// List of TransportSpecs to register with the Configurator
    81  		specs []TransportSpec
    82  
    83  		// Name of the service or empty string to use the default
    84  		serviceName string
    85  
    86  		// YAML to parse using the configurator
    87  		give string
    88  
    89  		// Environment variables
    90  		env map[string]string
    91  
    92  		// If non-empty, an error is expected where the message matches all
    93  		// strings in this slice
    94  		wantErr []string
    95  
    96  		// For success cases, the output Config must match this
    97  		wantConfig yarpc.Config
    98  	}
    99  
   100  	tests := []struct {
   101  		desc string
   102  		test func(*testing.T, *gomock.Controller) testCase
   103  	}{
   104  		{
   105  			desc: "no inbounds or outbounds",
   106  			test: func(*testing.T, *gomock.Controller) (tt testCase) {
   107  				tt.serviceName = "foo"
   108  				tt.give = whitespace.Expand(``)
   109  				tt.wantConfig = yarpc.Config{Name: "foo"}
   110  				return
   111  			},
   112  		},
   113  		{
   114  			desc: "application error debug logging",
   115  			test: func(*testing.T, *gomock.Controller) (tt testCase) {
   116  				debugLevel := zapcore.DebugLevel
   117  
   118  				tt.serviceName = "foo"
   119  				tt.give = whitespace.Expand(`
   120  					logging:
   121  						levels:
   122  							applicationError: debug
   123  				`)
   124  				tt.wantConfig = yarpc.Config{
   125  					Name: "foo",
   126  					Logging: yarpc.LoggingConfig{
   127  						Levels: yarpc.LogLevelConfig{
   128  							ApplicationError: &debugLevel,
   129  						},
   130  					},
   131  				}
   132  				return
   133  			},
   134  		},
   135  		{
   136  			desc: "server error debug logging",
   137  			test: func(*testing.T, *gomock.Controller) (tt testCase) {
   138  				debugLevel := zapcore.DebugLevel
   139  
   140  				tt.serviceName = "foo"
   141  				tt.give = whitespace.Expand(`
   142  					logging:
   143  						levels:
   144  							serverError: debug
   145  				`)
   146  				tt.wantConfig = yarpc.Config{
   147  					Name: "foo",
   148  					Logging: yarpc.LoggingConfig{
   149  						Levels: yarpc.LogLevelConfig{
   150  							ServerError: &debugLevel,
   151  						},
   152  					},
   153  				}
   154  				return
   155  			},
   156  		},
   157  		{
   158  			desc: "client error debug logging",
   159  			test: func(*testing.T, *gomock.Controller) (tt testCase) {
   160  				debugLevel := zapcore.DebugLevel
   161  
   162  				tt.serviceName = "foo"
   163  				tt.give = whitespace.Expand(`
   164  					logging:
   165  						levels:
   166  							clientError: debug
   167  				`)
   168  				tt.wantConfig = yarpc.Config{
   169  					Name: "foo",
   170  					Logging: yarpc.LoggingConfig{
   171  						Levels: yarpc.LogLevelConfig{
   172  							ClientError: &debugLevel,
   173  						},
   174  					},
   175  				}
   176  				return
   177  			},
   178  		},
   179  		{
   180  			desc: "client and server error debug logging",
   181  			test: func(*testing.T, *gomock.Controller) (tt testCase) {
   182  				debugLevel := zapcore.DebugLevel
   183  				infoLevel := zapcore.InfoLevel
   184  
   185  				tt.serviceName = "foo"
   186  				tt.give = whitespace.Expand(`
   187  					logging:
   188  						levels:
   189  							serverError: debug
   190  							clientError: info
   191  				`)
   192  				tt.wantConfig = yarpc.Config{
   193  					Name: "foo",
   194  					Logging: yarpc.LoggingConfig{
   195  						Levels: yarpc.LogLevelConfig{
   196  							ClientError: &infoLevel,
   197  							ServerError: &debugLevel,
   198  						},
   199  					},
   200  				}
   201  				return
   202  			},
   203  		},
   204  		{
   205  			desc: "outbound success info logging",
   206  			test: func(*testing.T, *gomock.Controller) (tt testCase) {
   207  				debugLevel := zapcore.DebugLevel
   208  				infoLevel := zapcore.InfoLevel
   209  
   210  				tt.serviceName = "foo"
   211  				tt.give = whitespace.Expand(`
   212  					logging:
   213  						levels:
   214  							success: debug
   215  							outbound:
   216  								success: info
   217  				`)
   218  				tt.wantConfig = yarpc.Config{
   219  					Name: "foo",
   220  					Logging: yarpc.LoggingConfig{
   221  						Levels: yarpc.LogLevelConfig{
   222  							Success: &debugLevel,
   223  							Outbound: yarpc.DirectionalLogLevelConfig{
   224  								Success: &infoLevel,
   225  							},
   226  						},
   227  					},
   228  				}
   229  				return
   230  			},
   231  		},
   232  		{
   233  			desc: "outbound success server error info logging",
   234  			test: func(*testing.T, *gomock.Controller) (tt testCase) {
   235  				debugLevel := zapcore.DebugLevel
   236  				infoLevel := zapcore.InfoLevel
   237  
   238  				tt.serviceName = "foo"
   239  				tt.give = whitespace.Expand(`
   240  					logging:
   241  						levels:
   242  							serverError: debug
   243  							outbound:
   244  								serverError: info
   245  				`)
   246  				tt.wantConfig = yarpc.Config{
   247  					Name: "foo",
   248  					Logging: yarpc.LoggingConfig{
   249  						Levels: yarpc.LogLevelConfig{
   250  							ServerError: &debugLevel,
   251  							Outbound: yarpc.DirectionalLogLevelConfig{
   252  								ServerError: &infoLevel,
   253  							},
   254  						},
   255  					},
   256  				}
   257  				return
   258  			},
   259  		},
   260  		{
   261  			desc: "outbound success client error info logging",
   262  			test: func(*testing.T, *gomock.Controller) (tt testCase) {
   263  				debugLevel := zapcore.DebugLevel
   264  				infoLevel := zapcore.InfoLevel
   265  
   266  				tt.serviceName = "foo"
   267  				tt.give = whitespace.Expand(`
   268  					logging:
   269  						levels:
   270  							clientError: debug
   271  							outbound:
   272  								clientError: info
   273  				`)
   274  				tt.wantConfig = yarpc.Config{
   275  					Name: "foo",
   276  					Logging: yarpc.LoggingConfig{
   277  						Levels: yarpc.LogLevelConfig{
   278  							ClientError: &debugLevel,
   279  							Outbound: yarpc.DirectionalLogLevelConfig{
   280  								ClientError: &infoLevel,
   281  							},
   282  						},
   283  					},
   284  				}
   285  				return
   286  			},
   287  		},
   288  		{
   289  			desc: "inbound success server error info logging",
   290  			test: func(*testing.T, *gomock.Controller) (tt testCase) {
   291  				debugLevel := zapcore.DebugLevel
   292  				infoLevel := zapcore.InfoLevel
   293  
   294  				tt.serviceName = "foo"
   295  				tt.give = whitespace.Expand(`
   296  					logging:
   297  						levels:
   298  							serverError: debug
   299  							inbound:
   300  								serverError: info
   301  				`)
   302  				tt.wantConfig = yarpc.Config{
   303  					Name: "foo",
   304  					Logging: yarpc.LoggingConfig{
   305  						Levels: yarpc.LogLevelConfig{
   306  							ServerError: &debugLevel,
   307  							Inbound: yarpc.DirectionalLogLevelConfig{
   308  								ServerError: &infoLevel,
   309  							},
   310  						},
   311  					},
   312  				}
   313  				return
   314  			},
   315  		},
   316  		{
   317  			desc: "inbound success client error info logging",
   318  			test: func(*testing.T, *gomock.Controller) (tt testCase) {
   319  				debugLevel := zapcore.DebugLevel
   320  				infoLevel := zapcore.InfoLevel
   321  
   322  				tt.serviceName = "foo"
   323  				tt.give = whitespace.Expand(`
   324  					logging:
   325  						levels:
   326  							clientError: debug
   327  							inbound:
   328  								clientError: info
   329  				`)
   330  				tt.wantConfig = yarpc.Config{
   331  					Name: "foo",
   332  					Logging: yarpc.LoggingConfig{
   333  						Levels: yarpc.LogLevelConfig{
   334  							ClientError: &debugLevel,
   335  							Inbound: yarpc.DirectionalLogLevelConfig{
   336  								ClientError: &infoLevel,
   337  							},
   338  						},
   339  					},
   340  				}
   341  				return
   342  			},
   343  		},
   344  		{
   345  			desc: "metric tags blocklist",
   346  			test: func(*testing.T, *gomock.Controller) (tt testCase) {
   347  				tt.serviceName = "foo"
   348  				tt.give = whitespace.Expand(`
   349  					metrics:
   350  						tagsBlocklist:
   351  							- "routing_delegate"
   352  				`)
   353  				tt.wantConfig = yarpc.Config{
   354  					Name: "foo",
   355  					Metrics: yarpc.MetricsConfig{
   356  						TagsBlocklist: []string{
   357  							"routing_delegate",
   358  						},
   359  					},
   360  				}
   361  				return
   362  			},
   363  		},
   364  		{
   365  			desc: "application error, invalid type",
   366  			test: func(*testing.T, *gomock.Controller) (tt testCase) {
   367  				tt.give = whitespace.Expand(`
   368  					logging:
   369  						levels:
   370  							applicationError: 42
   371  				`)
   372  				tt.wantErr = []string{
   373  					"error decoding 'logging.levels.applicationError':",
   374  					"could not decode Zap log level:",
   375  					"expected type 'string', got unconvertible type 'int'",
   376  				}
   377  				return
   378  			},
   379  		},
   380  		{
   381  			desc: "application error, invalid level",
   382  			test: func(*testing.T, *gomock.Controller) (tt testCase) {
   383  				tt.give = whitespace.Expand(`
   384  					logging:
   385  						levels:
   386  							applicationError: not a level
   387  				`)
   388  				tt.wantErr = []string{
   389  					"error decoding 'logging.levels.applicationError':",
   390  					"could not decode Zap log level:",
   391  					`unrecognized level: "not a level"`,
   392  				}
   393  				return
   394  			},
   395  		},
   396  		{
   397  			desc: "server error, invalid type",
   398  			test: func(*testing.T, *gomock.Controller) (tt testCase) {
   399  				tt.give = whitespace.Expand(`
   400  					logging:
   401  						levels:
   402  							serverError: 42
   403  				`)
   404  				tt.wantErr = []string{
   405  					"error decoding 'logging.levels.serverError':",
   406  					"could not decode Zap log level:",
   407  					"expected type 'string', got unconvertible type 'int'",
   408  				}
   409  				return
   410  			},
   411  		},
   412  		{
   413  			desc: "server error, invalid level",
   414  			test: func(*testing.T, *gomock.Controller) (tt testCase) {
   415  				tt.give = whitespace.Expand(`
   416  					logging:
   417  						levels:
   418  							serverError: not a level
   419  				`)
   420  				tt.wantErr = []string{
   421  					"error decoding 'logging.levels.serverError':",
   422  					"could not decode Zap log level:",
   423  					`unrecognized level: "not a level"`,
   424  				}
   425  				return
   426  			},
   427  		},
   428  		{
   429  			desc: "client error, invalid type",
   430  			test: func(*testing.T, *gomock.Controller) (tt testCase) {
   431  				tt.give = whitespace.Expand(`
   432  					logging:
   433  						levels:
   434  							clientError: 42
   435  				`)
   436  				tt.wantErr = []string{
   437  					"error decoding 'logging.levels.clientError':",
   438  					"could not decode Zap log level:",
   439  					"expected type 'string', got unconvertible type 'int'",
   440  				}
   441  				return
   442  			},
   443  		},
   444  		{
   445  			desc: "client error, invalid level",
   446  			test: func(*testing.T, *gomock.Controller) (tt testCase) {
   447  				tt.give = whitespace.Expand(`
   448  					logging:
   449  						levels:
   450  							clientError: not a level
   451  				`)
   452  				tt.wantErr = []string{
   453  					"error decoding 'logging.levels.clientError':",
   454  					"could not decode Zap log level:",
   455  					`unrecognized level: "not a level"`,
   456  				}
   457  				return
   458  			},
   459  		},
   460  		{
   461  			desc: "invalid usage of application error with server error",
   462  			test: func(*testing.T, *gomock.Controller) (tt testCase) {
   463  				tt.give = whitespace.Expand(`
   464  					logging:
   465  						levels:
   466  							applicationError: debug
   467  							serverError: debug
   468  				`)
   469  				tt.wantErr = []string{
   470  					"invalid logging configuration, failure/applicationError configuration can not be used with serverError/clientError",
   471  				}
   472  				return
   473  			},
   474  		},
   475  		{
   476  			desc: "invalid usage of outbound application error with client error",
   477  			test: func(*testing.T, *gomock.Controller) (tt testCase) {
   478  				tt.give = whitespace.Expand(`
   479  					logging:
   480  						levels:
   481  						  outbound:
   482  								applicationError: debug
   483  								clientError: debug
   484  				`)
   485  				tt.wantErr = []string{
   486  					"invalid outbound logging configuration, failure/applicationError configuration can not be used with serverError/clientError",
   487  				}
   488  				return
   489  			},
   490  		},
   491  		{
   492  			desc: "invalid usage of outbound failure with server error",
   493  			test: func(*testing.T, *gomock.Controller) (tt testCase) {
   494  				tt.give = whitespace.Expand(`
   495  					logging:
   496  						levels:
   497  						  outbound:
   498  							  failure: debug
   499  							  serverError: debug
   500  				`)
   501  				tt.wantErr = []string{
   502  					"invalid outbound logging configuration, failure/applicationError configuration can not be used with serverError/clientError",
   503  				}
   504  				return
   505  			},
   506  		},
   507  		{
   508  			desc: "invalid usage of outbound failure with client error",
   509  			test: func(*testing.T, *gomock.Controller) (tt testCase) {
   510  				tt.give = whitespace.Expand(`
   511  					logging:
   512  						levels:
   513  						  outbound:
   514  							  failure: debug
   515  							  clientError: debug
   516  				`)
   517  				tt.wantErr = []string{
   518  					"invalid outbound logging configuration, failure/applicationError configuration can not be used with serverError/clientError",
   519  				}
   520  				return
   521  			},
   522  		},
   523  
   524  		{
   525  			desc: "invalid usage of outbound application error with server error",
   526  			test: func(*testing.T, *gomock.Controller) (tt testCase) {
   527  				tt.give = whitespace.Expand(`
   528  					logging:
   529  						levels:
   530  						  outbound:
   531  							  applicationError: debug
   532  							  serverError: debug
   533  				`)
   534  				tt.wantErr = []string{
   535  					"invalid outbound logging configuration, failure/applicationError configuration can not be used with serverError/clientError",
   536  				}
   537  				return
   538  			},
   539  		},
   540  		{
   541  			desc: "invalid usage of outbound application error with client error",
   542  			test: func(*testing.T, *gomock.Controller) (tt testCase) {
   543  				tt.give = whitespace.Expand(`
   544  					logging:
   545  						levels:
   546  						  outbound:
   547  							  applicationError: debug
   548  							  clientError: debug
   549  				`)
   550  				tt.wantErr = []string{
   551  					"invalid outbound logging configuration, failure/applicationError configuration can not be used with serverError/clientError",
   552  				}
   553  				return
   554  			},
   555  		},
   556  		{
   557  			desc: "invalid usage of outbound failure with server error",
   558  			test: func(*testing.T, *gomock.Controller) (tt testCase) {
   559  				tt.give = whitespace.Expand(`
   560  					logging:
   561  						levels:
   562  						  outbound:
   563  							  failure: debug
   564  							  serverError: debug
   565  				`)
   566  				tt.wantErr = []string{
   567  					"invalid outbound logging configuration, failure/applicationError configuration can not be used with serverError/clientError",
   568  				}
   569  				return
   570  			},
   571  		},
   572  		{
   573  			desc: "invalid usage of outbound failure with client error",
   574  			test: func(*testing.T, *gomock.Controller) (tt testCase) {
   575  				tt.give = whitespace.Expand(`
   576  					logging:
   577  						levels:
   578  						  outbound:
   579  							  failure: debug
   580  							  clientError: debug
   581  				`)
   582  				tt.wantErr = []string{
   583  					"invalid outbound logging configuration, failure/applicationError configuration can not be used with serverError/clientError",
   584  				}
   585  				return
   586  			},
   587  		},
   588  
   589  		{
   590  			desc: "invalid usage of inbound application error with client error",
   591  			test: func(*testing.T, *gomock.Controller) (tt testCase) {
   592  				tt.give = whitespace.Expand(`
   593  					logging:
   594  						levels:
   595  						  inbound:
   596  								applicationError: debug
   597  								clientError: debug
   598  				`)
   599  				tt.wantErr = []string{
   600  					"invalid inbound logging configuration, failure/applicationError configuration can not be used with serverError/clientError",
   601  				}
   602  				return
   603  			},
   604  		},
   605  		{
   606  			desc: "invalid usage of inbound failure with server error",
   607  			test: func(*testing.T, *gomock.Controller) (tt testCase) {
   608  				tt.give = whitespace.Expand(`
   609  					logging:
   610  						levels:
   611  						  inbound:
   612  							  failure: debug
   613  							  serverError: debug
   614  				`)
   615  				tt.wantErr = []string{
   616  					"invalid inbound logging configuration, failure/applicationError configuration can not be used with serverError/clientError",
   617  				}
   618  				return
   619  			},
   620  		},
   621  		{
   622  			desc: "invalid usage of inbound failure with client error",
   623  			test: func(*testing.T, *gomock.Controller) (tt testCase) {
   624  				tt.give = whitespace.Expand(`
   625  					logging:
   626  						levels:
   627  						  inbound:
   628  							  failure: debug
   629  							  clientError: debug
   630  				`)
   631  				tt.wantErr = []string{
   632  					"invalid inbound logging configuration, failure/applicationError configuration can not be used with serverError/clientError",
   633  				}
   634  				return
   635  			},
   636  		},
   637  
   638  		{
   639  			desc: "invalid usage of inbound application error with server error",
   640  			test: func(*testing.T, *gomock.Controller) (tt testCase) {
   641  				tt.give = whitespace.Expand(`
   642  					logging:
   643  						levels:
   644  						  inbound:
   645  							  applicationError: debug
   646  							  serverError: debug
   647  				`)
   648  				tt.wantErr = []string{
   649  					"invalid inbound logging configuration, failure/applicationError configuration can not be used with serverError/clientError",
   650  				}
   651  				return
   652  			},
   653  		},
   654  		{
   655  			desc: "invalid usage of inbound application error with client error",
   656  			test: func(*testing.T, *gomock.Controller) (tt testCase) {
   657  				tt.give = whitespace.Expand(`
   658  					logging:
   659  						levels:
   660  						  inbound:
   661  							  applicationError: debug
   662  							  clientError: debug
   663  				`)
   664  				tt.wantErr = []string{
   665  					"invalid inbound logging configuration, failure/applicationError configuration can not be used with serverError/clientError",
   666  				}
   667  				return
   668  			},
   669  		},
   670  		{
   671  			desc: "invalid usage of inbound failure with server error",
   672  			test: func(*testing.T, *gomock.Controller) (tt testCase) {
   673  				tt.give = whitespace.Expand(`
   674  					logging:
   675  						levels:
   676  						  inbound:
   677  							  failure: debug
   678  							  serverError: debug
   679  				`)
   680  				tt.wantErr = []string{
   681  					"invalid inbound logging configuration, failure/applicationError configuration can not be used with serverError/clientError",
   682  				}
   683  				return
   684  			},
   685  		},
   686  		{
   687  			desc: "invalid usage of inbound failure with client error",
   688  			test: func(*testing.T, *gomock.Controller) (tt testCase) {
   689  				tt.give = whitespace.Expand(`
   690  					logging:
   691  						levels:
   692  						  inbound:
   693  							  failure: debug
   694  							  clientError: debug
   695  				`)
   696  				tt.wantErr = []string{
   697  					"invalid inbound logging configuration, failure/applicationError configuration can not be used with serverError/clientError",
   698  				}
   699  				return
   700  			},
   701  		},
   702  		{
   703  			desc: "unknown inbound",
   704  			test: func(*testing.T, *gomock.Controller) (tt testCase) {
   705  				tt.give = whitespace.Expand(`
   706  					inbounds:
   707  						bar: {}
   708  				`)
   709  				tt.wantErr = []string{
   710  					"failed to load inbound",
   711  					`unknown transport "bar"`,
   712  				}
   713  				return
   714  			},
   715  		},
   716  		{
   717  			desc: "unknown implicit outbound",
   718  			test: func(*testing.T, *gomock.Controller) (tt testCase) {
   719  				tt.give = whitespace.Expand(`
   720  					outbounds:
   721  						myservice:
   722  							http: {url: "http://localhost:8080/yarpc"}
   723  				`)
   724  				tt.wantErr = []string{
   725  					`failed to load configuration for outbound "myservice"`,
   726  					`unknown transport "http"`,
   727  				}
   728  				return
   729  			},
   730  		},
   731  		{
   732  			desc: "unknown unary outbound",
   733  			test: func(*testing.T, *gomock.Controller) (tt testCase) {
   734  				tt.give = whitespace.Expand(`
   735  					outbounds:
   736  						someservice:
   737  							unary:
   738  								tchannel:
   739  									address: localhost:4040
   740  				`)
   741  				tt.wantErr = []string{
   742  					`failed to load configuration for outbound "someservice"`,
   743  					`unknown transport "tchannel"`,
   744  				}
   745  				return
   746  			},
   747  		},
   748  		{
   749  			desc: "unknown oneway outbound",
   750  			test: func(*testing.T, *gomock.Controller) (tt testCase) {
   751  				tt.give = whitespace.Expand(`
   752  					outbounds:
   753  						keyvalue:
   754  							oneway:
   755  								kafka: {queue: requests}
   756  				`)
   757  				tt.wantErr = []string{
   758  					`failed to load configuration for outbound "keyvalue"`,
   759  					`unknown transport "kafka"`,
   760  				}
   761  				return
   762  			},
   763  		},
   764  		{
   765  			desc: "unknown stream outbound",
   766  			test: func(*testing.T, *gomock.Controller) (tt testCase) {
   767  				tt.give = whitespace.Expand(`
   768  					outbounds:
   769  						keyvalue:
   770  							stream:
   771  								kafka: {queue: requests}
   772  				`)
   773  				tt.wantErr = []string{
   774  					`failed to load configuration for outbound "keyvalue"`,
   775  					`unknown transport "kafka"`,
   776  				}
   777  				return
   778  			},
   779  		},
   780  		{
   781  			desc: "unused transport",
   782  			test: func(t *testing.T, mockCtrl *gomock.Controller) (tt testCase) {
   783  				type fooTransportConfig struct{ Items []int }
   784  
   785  				tt.serviceName = "foo"
   786  				tt.give = whitespace.Expand(`
   787  					transports:
   788  						bar:
   789  							items: [1, 2, 3]
   790  				`)
   791  
   792  				foo := mockTransportSpecBuilder{
   793  					Name:            "bar",
   794  					TransportConfig: reflect.TypeOf(&fooTransportConfig{}),
   795  				}.Build(mockCtrl)
   796  
   797  				tt.specs = []TransportSpec{foo.Spec()}
   798  				tt.wantConfig = yarpc.Config{Name: "foo"}
   799  
   800  				return
   801  			},
   802  		},
   803  		{
   804  			desc: "transport config error",
   805  			test: func(t *testing.T, mockCtrl *gomock.Controller) (tt testCase) {
   806  				type transportConfig struct{ KeepAlive time.Duration }
   807  				type inboundConfig struct{ Address string }
   808  
   809  				tt.give = whitespace.Expand(`
   810  					inbounds:
   811  						http: {address: ":80"}
   812  					transports:
   813  						http:
   814  							keepAlive: thirty
   815  				`)
   816  
   817  				http := mockTransportSpecBuilder{
   818  					Name:            "http",
   819  					TransportConfig: reflect.TypeOf(&transportConfig{}),
   820  					InboundConfig:   reflect.TypeOf(&inboundConfig{}),
   821  				}.Build(mockCtrl)
   822  				tt.specs = []TransportSpec{http.Spec()}
   823  
   824  				tt.wantErr = []string{
   825  					"failed to decode transport configuration:",
   826  					"error decoding 'KeepAlive'",
   827  					`invalid duration`,
   828  					`thirty`,
   829  				}
   830  
   831  				return
   832  			},
   833  		},
   834  		{
   835  			desc: "inbound",
   836  			test: func(t *testing.T, mockCtrl *gomock.Controller) (tt testCase) {
   837  				type inboundConfig struct{ Address string }
   838  				tt.serviceName = "myservice"
   839  				tt.give = whitespace.Expand(`
   840  					inbounds:
   841  						http: {address: ":80"}
   842  				`)
   843  
   844  				http := mockTransportSpecBuilder{
   845  					Name:            "http",
   846  					TransportConfig: _typeOfEmptyStruct,
   847  					InboundConfig:   reflect.TypeOf(&inboundConfig{}),
   848  				}.Build(mockCtrl)
   849  
   850  				transport := transporttest.NewMockTransport(mockCtrl)
   851  				inbound := transporttest.NewMockInbound(mockCtrl)
   852  
   853  				http.EXPECT().
   854  					BuildTransport(struct{}{}, kitMatcher{ServiceName: "myservice"}).
   855  					Return(transport, nil)
   856  				http.EXPECT().
   857  					BuildInbound(
   858  						&inboundConfig{Address: ":80"}, transport,
   859  						kitMatcher{ServiceName: "myservice"}).
   860  					Return(inbound, nil)
   861  
   862  				tt.specs = []TransportSpec{http.Spec()}
   863  				tt.wantConfig = yarpc.Config{
   864  					Name:     "myservice",
   865  					Inbounds: yarpc.Inbounds{inbound},
   866  				}
   867  				return
   868  			},
   869  		},
   870  		{
   871  			desc: "inbounds unsupported",
   872  			test: func(t *testing.T, mockCtrl *gomock.Controller) (tt testCase) {
   873  				tt.give = whitespace.Expand(`
   874  					inbounds:
   875  						outgoing-only:
   876  							foo: bar
   877  				`)
   878  
   879  				spec := mockTransportSpecBuilder{
   880  					Name:            "outgoing-only",
   881  					TransportConfig: _typeOfEmptyStruct,
   882  				}.Build(mockCtrl)
   883  				tt.specs = []TransportSpec{spec.Spec()}
   884  				tt.wantErr = []string{
   885  					`transport "outgoing-only" does not support inbound requests`,
   886  				}
   887  
   888  				return
   889  			},
   890  		},
   891  		{
   892  			desc: "duplicate inbounds",
   893  			test: func(t *testing.T, mockCtrl *gomock.Controller) (tt testCase) {
   894  				type inboundConfig struct{ Address string }
   895  				tt.serviceName = "foo"
   896  				tt.give = whitespace.Expand(`
   897  					inbounds:
   898  						http:
   899  							address: ":8080"
   900  						http2:
   901  							type: http
   902  							address: ":8081"
   903  				`)
   904  
   905  				http := mockTransportSpecBuilder{
   906  					Name:            "http",
   907  					TransportConfig: _typeOfEmptyStruct,
   908  					InboundConfig:   reflect.TypeOf(&inboundConfig{}),
   909  				}.Build(mockCtrl)
   910  				transport := transporttest.NewMockTransport(mockCtrl)
   911  				http.EXPECT().
   912  					BuildTransport(struct{}{}, kitMatcher{ServiceName: "foo"}).
   913  					Return(transport, nil)
   914  
   915  				inbound := transporttest.NewMockInbound(mockCtrl)
   916  				inbound2 := transporttest.NewMockInbound(mockCtrl)
   917  
   918  				http.EXPECT().
   919  					BuildInbound(
   920  						&inboundConfig{Address: ":8080"},
   921  						transport,
   922  						kitMatcher{ServiceName: "foo"}).
   923  					Return(inbound, nil)
   924  				http.EXPECT().
   925  					BuildInbound(
   926  						&inboundConfig{Address: ":8081"},
   927  						transport,
   928  						kitMatcher{ServiceName: "foo"}).
   929  					Return(inbound2, nil)
   930  
   931  				tt.specs = []TransportSpec{http.Spec()}
   932  				tt.wantConfig = yarpc.Config{
   933  					Name:     "foo",
   934  					Inbounds: yarpc.Inbounds{inbound, inbound2},
   935  				}
   936  
   937  				return
   938  			},
   939  		},
   940  		{
   941  			desc: "disabled inbound",
   942  			test: func(t *testing.T, mockCtrl *gomock.Controller) (tt testCase) {
   943  				type inboundConfig struct{ Address string }
   944  				tt.serviceName = "foo"
   945  				tt.give = whitespace.Expand(`
   946  					inbounds:
   947  						http:
   948  							disabled: true
   949  							address: ":8080"
   950  						http2:
   951  							type: http
   952  							address: ":8081"
   953  				`)
   954  
   955  				http := mockTransportSpecBuilder{
   956  					Name:            "http",
   957  					TransportConfig: _typeOfEmptyStruct,
   958  					InboundConfig:   reflect.TypeOf(&inboundConfig{}),
   959  				}.Build(mockCtrl)
   960  
   961  				transport := transporttest.NewMockTransport(mockCtrl)
   962  				inbound := transporttest.NewMockInbound(mockCtrl)
   963  
   964  				http.EXPECT().
   965  					BuildTransport(struct{}{}, kitMatcher{ServiceName: "foo"}).
   966  					Return(transport, nil)
   967  				http.EXPECT().
   968  					BuildInbound(
   969  						&inboundConfig{Address: ":8081"},
   970  						transport,
   971  						kitMatcher{ServiceName: "foo"}).
   972  					Return(inbound, nil)
   973  
   974  				tt.specs = []TransportSpec{http.Spec()}
   975  				tt.wantConfig = yarpc.Config{
   976  					Name:     "foo",
   977  					Inbounds: yarpc.Inbounds{inbound},
   978  				}
   979  
   980  				return
   981  			},
   982  		},
   983  		{
   984  			desc: "inbound error",
   985  			test: func(t *testing.T, mockCtrl *gomock.Controller) (tt testCase) {
   986  				tt.serviceName = "foo"
   987  				tt.give = whitespace.Expand(`
   988  					inbounds:
   989  						foo:
   990  							unexpected: bar
   991  				`)
   992  
   993  				foo := mockTransportSpecBuilder{
   994  					Name:            "foo",
   995  					TransportConfig: _typeOfEmptyStruct,
   996  					InboundConfig:   _typeOfEmptyStruct,
   997  				}.Build(mockCtrl)
   998  				tt.specs = []TransportSpec{foo.Spec()}
   999  				tt.wantErr = []string{
  1000  					"failed to decode inbound configuration: failed to decode struct",
  1001  					"invalid keys: unexpected",
  1002  				}
  1003  
  1004  				return
  1005  			},
  1006  		},
  1007  		{
  1008  			desc: "implicit outbound no support",
  1009  			test: func(t *testing.T, mockCtrl *gomock.Controller) (tt testCase) {
  1010  				tt.give = whitespace.Expand(`
  1011  					outbounds:
  1012  						myservice:
  1013  							sink:
  1014  								foo: bar
  1015  				`)
  1016  
  1017  				sink := mockTransportSpecBuilder{
  1018  					Name:            "sink",
  1019  					TransportConfig: _typeOfEmptyStruct,
  1020  					InboundConfig:   _typeOfEmptyStruct,
  1021  				}.Build(mockCtrl)
  1022  
  1023  				tt.specs = []TransportSpec{sink.Spec()}
  1024  				tt.wantErr = []string{`transport "sink" does not support outbound requests`}
  1025  				return
  1026  			},
  1027  		},
  1028  		{
  1029  			desc: "implicit outbound unary",
  1030  			test: func(t *testing.T, mockCtrl *gomock.Controller) (tt testCase) {
  1031  				type outboundConfig struct{ Address string }
  1032  				tt.serviceName = "foo"
  1033  				tt.give = whitespace.Expand(`
  1034  					outbounds:
  1035  						bar:
  1036  							tchannel:
  1037  								address: localhost:4040
  1038  				`)
  1039  
  1040  				tchan := mockTransportSpecBuilder{
  1041  					Name:                "tchannel",
  1042  					TransportConfig:     _typeOfEmptyStruct,
  1043  					UnaryOutboundConfig: reflect.TypeOf(&outboundConfig{}),
  1044  				}.Build(mockCtrl)
  1045  
  1046  				transport := transporttest.NewMockTransport(mockCtrl)
  1047  				outbound := transporttest.NewMockUnaryOutbound(mockCtrl)
  1048  
  1049  				tchan.EXPECT().
  1050  					BuildTransport(struct{}{}, kitMatcher{ServiceName: "foo"}).
  1051  					Return(transport, nil)
  1052  				tchan.EXPECT().
  1053  					BuildUnaryOutbound(
  1054  						&outboundConfig{Address: "localhost:4040"},
  1055  						transport,
  1056  						kitMatcher{ServiceName: "foo", OutboundServiceName: "bar"}).
  1057  					Return(outbound, nil)
  1058  
  1059  				tt.specs = []TransportSpec{tchan.Spec()}
  1060  				tt.wantConfig = yarpc.Config{
  1061  					Name: "foo",
  1062  					Outbounds: yarpc.Outbounds{
  1063  						"bar": {
  1064  							Unary: outbound,
  1065  						},
  1066  					},
  1067  				}
  1068  
  1069  				return
  1070  			},
  1071  		},
  1072  		{
  1073  			desc: "implicit outbound oneway",
  1074  			test: func(t *testing.T, mockCtrl *gomock.Controller) (tt testCase) {
  1075  				type transportConfig struct{ Address string }
  1076  				type outboundConfig struct{ Queue string }
  1077  				tt.serviceName = "foo"
  1078  				tt.give = whitespace.Expand(`
  1079  					outbounds:
  1080  						bar:
  1081  							redis:
  1082  								queue: requests
  1083  					transports:
  1084  						redis:
  1085  							address: localhost:6379
  1086  				`)
  1087  
  1088  				redis := mockTransportSpecBuilder{
  1089  					Name:                 "redis",
  1090  					TransportConfig:      reflect.TypeOf(transportConfig{}),
  1091  					OnewayOutboundConfig: reflect.TypeOf(&outboundConfig{}),
  1092  				}.Build(mockCtrl)
  1093  
  1094  				transport := transporttest.NewMockTransport(mockCtrl)
  1095  				outbound := transporttest.NewMockOnewayOutbound(mockCtrl)
  1096  
  1097  				redis.EXPECT().
  1098  					BuildTransport(
  1099  						transportConfig{Address: "localhost:6379"},
  1100  						kitMatcher{ServiceName: "foo"}).
  1101  					Return(transport, nil)
  1102  				redis.EXPECT().
  1103  					BuildOnewayOutbound(
  1104  						&outboundConfig{Queue: "requests"},
  1105  						transport,
  1106  						kitMatcher{ServiceName: "foo", OutboundServiceName: "bar"}).
  1107  					Return(outbound, nil)
  1108  
  1109  				tt.specs = []TransportSpec{redis.Spec()}
  1110  				tt.wantConfig = yarpc.Config{
  1111  					Name: "foo",
  1112  					Outbounds: yarpc.Outbounds{
  1113  						"bar": {
  1114  							Oneway: outbound,
  1115  						},
  1116  					},
  1117  				}
  1118  
  1119  				return
  1120  			},
  1121  		},
  1122  		{
  1123  			desc: "implicit outbound stream",
  1124  			test: func(t *testing.T, mockCtrl *gomock.Controller) (tt testCase) {
  1125  				type transportConfig struct{ Address string }
  1126  				type outboundConfig struct{ Queue string }
  1127  				tt.serviceName = "foo"
  1128  				tt.give = whitespace.Expand(`
  1129  					outbounds:
  1130  						bar:
  1131  							fake-stream-transport:
  1132  								queue: requests
  1133  					transports:
  1134  						fake-stream-transport:
  1135  							address: localhost:6379
  1136  				`)
  1137  
  1138  				redis := mockTransportSpecBuilder{
  1139  					Name:                 "fake-stream-transport",
  1140  					TransportConfig:      reflect.TypeOf(transportConfig{}),
  1141  					StreamOutboundConfig: reflect.TypeOf(&outboundConfig{}),
  1142  				}.Build(mockCtrl)
  1143  
  1144  				transport := transporttest.NewMockTransport(mockCtrl)
  1145  				outbound := transporttest.NewMockStreamOutbound(mockCtrl)
  1146  
  1147  				redis.EXPECT().
  1148  					BuildTransport(
  1149  						transportConfig{Address: "localhost:6379"},
  1150  						kitMatcher{ServiceName: "foo"}).
  1151  					Return(transport, nil)
  1152  				redis.EXPECT().
  1153  					BuildStreamOutbound(
  1154  						&outboundConfig{Queue: "requests"},
  1155  						transport,
  1156  						kitMatcher{ServiceName: "foo", OutboundServiceName: "bar"}).
  1157  					Return(outbound, nil)
  1158  
  1159  				tt.specs = []TransportSpec{redis.Spec()}
  1160  				tt.wantConfig = yarpc.Config{
  1161  					Name: "foo",
  1162  					Outbounds: yarpc.Outbounds{
  1163  						"bar": {
  1164  							Stream: outbound,
  1165  						},
  1166  					},
  1167  				}
  1168  
  1169  				return
  1170  			},
  1171  		},
  1172  		{
  1173  			desc: "implicit outbound unary, oneway, stream",
  1174  			test: func(t *testing.T, mockCtrl *gomock.Controller) (tt testCase) {
  1175  				type transportConfig struct{ KeepAlive time.Duration }
  1176  				type outboundConfig struct{ URL string }
  1177  				tt.serviceName = "foo"
  1178  				tt.give = whitespace.Expand(`
  1179  					outbounds:
  1180  						baz:
  1181  							http:
  1182  								url: http://localhost:8080/yarpc
  1183  					transports:
  1184  						http:
  1185  							keepAlive: 60s
  1186  				`)
  1187  
  1188  				http := mockTransportSpecBuilder{
  1189  					Name:                 "http",
  1190  					TransportConfig:      reflect.TypeOf(&transportConfig{}),
  1191  					OnewayOutboundConfig: reflect.TypeOf(&outboundConfig{}),
  1192  					StreamOutboundConfig: reflect.TypeOf(&outboundConfig{}),
  1193  					UnaryOutboundConfig:  reflect.TypeOf(&outboundConfig{}),
  1194  				}.Build(mockCtrl)
  1195  
  1196  				transport := transporttest.NewMockTransport(mockCtrl)
  1197  				unary := transporttest.NewMockUnaryOutbound(mockCtrl)
  1198  				oneway := transporttest.NewMockOnewayOutbound(mockCtrl)
  1199  				stream := transporttest.NewMockStreamOutbound(mockCtrl)
  1200  
  1201  				http.EXPECT().
  1202  					BuildTransport(
  1203  						&transportConfig{KeepAlive: time.Minute},
  1204  						kitMatcher{ServiceName: "foo"}).
  1205  					Return(transport, nil)
  1206  
  1207  				outcfg := outboundConfig{URL: "http://localhost:8080/yarpc"}
  1208  				http.EXPECT().
  1209  					BuildUnaryOutbound(&outcfg, transport, kitMatcher{ServiceName: "foo", OutboundServiceName: "baz"}).
  1210  					Return(unary, nil)
  1211  				http.EXPECT().
  1212  					BuildOnewayOutbound(&outcfg, transport, kitMatcher{ServiceName: "foo", OutboundServiceName: "baz"}).
  1213  					Return(oneway, nil)
  1214  				http.EXPECT().
  1215  					BuildStreamOutbound(&outcfg, transport, kitMatcher{ServiceName: "foo", OutboundServiceName: "baz"}).
  1216  					Return(stream, nil)
  1217  
  1218  				tt.specs = []TransportSpec{http.Spec()}
  1219  				tt.wantConfig = yarpc.Config{
  1220  					Name: "foo",
  1221  					Outbounds: yarpc.Outbounds{
  1222  						"baz": {
  1223  							Unary:  unary,
  1224  							Oneway: oneway,
  1225  							Stream: stream,
  1226  						},
  1227  					},
  1228  				}
  1229  
  1230  				return
  1231  			},
  1232  		},
  1233  		{
  1234  			desc: "implicit outbound error",
  1235  			test: func(t *testing.T, mockCtrl *gomock.Controller) (tt testCase) {
  1236  				type outboundConfig struct{ URL string }
  1237  				tt.give = whitespace.Expand(`
  1238  					outbounds:
  1239  						qux:
  1240  							http:
  1241  								uri: http://localhost:8080/yarpc
  1242  				`)
  1243  
  1244  				http := mockTransportSpecBuilder{
  1245  					Name:                 "http",
  1246  					TransportConfig:      _typeOfEmptyStruct,
  1247  					OnewayOutboundConfig: reflect.TypeOf(&outboundConfig{}),
  1248  					UnaryOutboundConfig:  reflect.TypeOf(&outboundConfig{}),
  1249  					StreamOutboundConfig: reflect.TypeOf(&outboundConfig{}),
  1250  				}.Build(mockCtrl)
  1251  
  1252  				tt.specs = []TransportSpec{http.Spec()}
  1253  				tt.wantErr = []string{
  1254  					`failed to add outbound "qux"`,
  1255  					"failed to decode oneway outbound configuration",
  1256  					"failed to decode unary outbound configuration",
  1257  					"failed to decode stream outbound configuration",
  1258  					"invalid keys: uri",
  1259  				}
  1260  
  1261  				return
  1262  			},
  1263  		},
  1264  		{
  1265  			desc: "explicit outbounds",
  1266  			test: func(t *testing.T, mockCtrl *gomock.Controller) (tt testCase) {
  1267  				type (
  1268  					httpOutboundConfig   struct{ URL string }
  1269  					httpTransportConfig  struct{ KeepAlive time.Duration }
  1270  					redisOutboundConfig  struct{ Queue string }
  1271  					redisTransportConfig struct{ Address string }
  1272  				)
  1273  
  1274  				tt.serviceName = "myservice"
  1275  				tt.give = whitespace.Expand(`
  1276  					transports:
  1277  						http:
  1278  							keepAlive: 5m
  1279  						redis:
  1280  							address: "127.0.0.1:6379"
  1281  					outbounds:
  1282  						foo:
  1283  							unary:
  1284  								http:
  1285  									url: http://localhost:8080/yarpc/v1
  1286  							oneway:
  1287  								http:
  1288  									url: http://localhost:8081/yarpc/v2
  1289  							stream:
  1290  								http:
  1291  									url: http://localhost:8081/yarpc/v3
  1292  						bar:
  1293  							oneway:
  1294  								redis:
  1295  									queue: requests
  1296  				`)
  1297  
  1298  				http := mockTransportSpecBuilder{
  1299  					Name:                 "http",
  1300  					TransportConfig:      reflect.TypeOf(httpTransportConfig{}),
  1301  					OnewayOutboundConfig: reflect.TypeOf(httpOutboundConfig{}),
  1302  					StreamOutboundConfig: reflect.TypeOf(httpOutboundConfig{}),
  1303  					UnaryOutboundConfig:  reflect.TypeOf(httpOutboundConfig{}),
  1304  				}.Build(mockCtrl)
  1305  
  1306  				redis := mockTransportSpecBuilder{
  1307  					Name:                 "redis",
  1308  					TransportConfig:      reflect.TypeOf(redisTransportConfig{}),
  1309  					OnewayOutboundConfig: reflect.TypeOf(redisOutboundConfig{}),
  1310  				}.Build(mockCtrl)
  1311  
  1312  				httpTransport := transporttest.NewMockTransport(mockCtrl)
  1313  				httpUnary := transporttest.NewMockUnaryOutbound(mockCtrl)
  1314  				httpOneway := transporttest.NewMockOnewayOutbound(mockCtrl)
  1315  				httpStream := transporttest.NewMockStreamOutbound(mockCtrl)
  1316  				http.EXPECT().
  1317  					BuildTransport(
  1318  						httpTransportConfig{KeepAlive: 5 * time.Minute},
  1319  						kitMatcher{ServiceName: "myservice"}).
  1320  					Return(httpTransport, nil)
  1321  
  1322  				redisTransport := transporttest.NewMockTransport(mockCtrl)
  1323  				redisOneway := transporttest.NewMockOnewayOutbound(mockCtrl)
  1324  				redis.EXPECT().
  1325  					BuildTransport(
  1326  						redisTransportConfig{Address: "127.0.0.1:6379"},
  1327  						kitMatcher{ServiceName: "myservice"}).
  1328  					Return(redisTransport, nil)
  1329  
  1330  				http.EXPECT().
  1331  					BuildUnaryOutbound(
  1332  						httpOutboundConfig{URL: "http://localhost:8080/yarpc/v1"},
  1333  						httpTransport,
  1334  						kitMatcher{ServiceName: "myservice", OutboundServiceName: "foo"}).
  1335  					Return(httpUnary, nil)
  1336  				http.EXPECT().
  1337  					BuildOnewayOutbound(
  1338  						httpOutboundConfig{URL: "http://localhost:8081/yarpc/v2"},
  1339  						httpTransport,
  1340  						kitMatcher{ServiceName: "myservice", OutboundServiceName: "foo"}).
  1341  					Return(httpOneway, nil)
  1342  				http.EXPECT().
  1343  					BuildStreamOutbound(
  1344  						httpOutboundConfig{URL: "http://localhost:8081/yarpc/v3"},
  1345  						httpTransport,
  1346  						kitMatcher{ServiceName: "myservice", OutboundServiceName: "foo"}).
  1347  					Return(httpStream, nil)
  1348  
  1349  				redis.EXPECT().
  1350  					BuildOnewayOutbound(
  1351  						redisOutboundConfig{Queue: "requests"},
  1352  						redisTransport,
  1353  						kitMatcher{ServiceName: "myservice", OutboundServiceName: "bar"}).
  1354  					Return(redisOneway, nil)
  1355  
  1356  				tt.specs = []TransportSpec{http.Spec(), redis.Spec()}
  1357  				tt.wantConfig = yarpc.Config{
  1358  					Name: "myservice",
  1359  					Outbounds: yarpc.Outbounds{
  1360  						"foo": {Unary: httpUnary, Oneway: httpOneway, Stream: httpStream},
  1361  						"bar": {Oneway: redisOneway},
  1362  					},
  1363  				}
  1364  
  1365  				return
  1366  			},
  1367  		},
  1368  		{
  1369  			desc: "explicit unary error",
  1370  			test: func(t *testing.T, mockCtrl *gomock.Controller) (tt testCase) {
  1371  				type outboundConfig struct{ URL string }
  1372  				tt.give = whitespace.Expand(`
  1373  					outbounds:
  1374  						hello:
  1375  							unary:
  1376  								http:
  1377  									scheme: https
  1378  									host: localhost
  1379  									port: 8088
  1380  									path: /yarpc
  1381  				`)
  1382  
  1383  				http := mockTransportSpecBuilder{
  1384  					Name:                 "http",
  1385  					TransportConfig:      _typeOfEmptyStruct,
  1386  					OnewayOutboundConfig: reflect.TypeOf(&outboundConfig{}),
  1387  					UnaryOutboundConfig:  reflect.TypeOf(&outboundConfig{}),
  1388  				}.Build(mockCtrl)
  1389  
  1390  				tt.specs = []TransportSpec{http.Spec()}
  1391  				tt.wantErr = []string{
  1392  					"failed to decode unary outbound configuration",
  1393  					"invalid keys: host, path, port, scheme",
  1394  				}
  1395  
  1396  				return
  1397  			},
  1398  		},
  1399  		{
  1400  			desc: "explicit oneway error",
  1401  			test: func(t *testing.T, mockCtrl *gomock.Controller) (tt testCase) {
  1402  				type outboundConfig struct{ Address string }
  1403  				tt.give = whitespace.Expand(`
  1404  					outbounds:
  1405  						hello:
  1406  							oneway:
  1407  								redis:
  1408  									host: localhost
  1409  									port: 6379
  1410  				`)
  1411  
  1412  				redis := mockTransportSpecBuilder{
  1413  					Name:                 "redis",
  1414  					TransportConfig:      _typeOfEmptyStruct,
  1415  					OnewayOutboundConfig: reflect.TypeOf(&outboundConfig{}),
  1416  				}.Build(mockCtrl)
  1417  
  1418  				tt.specs = []TransportSpec{redis.Spec()}
  1419  				tt.wantErr = []string{
  1420  					"failed to decode oneway outbound configuration",
  1421  					"invalid keys: host, port",
  1422  				}
  1423  
  1424  				return
  1425  			},
  1426  		},
  1427  		{
  1428  			desc: "explicit stream error",
  1429  			test: func(t *testing.T, mockCtrl *gomock.Controller) (tt testCase) {
  1430  				type outboundConfig struct{ Address string }
  1431  				tt.give = whitespace.Expand(`
  1432  					outbounds:
  1433  						hello:
  1434  							stream:
  1435  								redis:
  1436  									host: localhost
  1437  									port: 6379
  1438  				`)
  1439  
  1440  				redis := mockTransportSpecBuilder{
  1441  					Name:                 "redis",
  1442  					TransportConfig:      _typeOfEmptyStruct,
  1443  					StreamOutboundConfig: reflect.TypeOf(&outboundConfig{}),
  1444  				}.Build(mockCtrl)
  1445  
  1446  				tt.specs = []TransportSpec{redis.Spec()}
  1447  				tt.wantErr = []string{
  1448  					"failed to decode stream outbound configuration",
  1449  					"invalid keys: host, port",
  1450  				}
  1451  
  1452  				return
  1453  			},
  1454  		},
  1455  		{
  1456  			desc: "explicit unary not supported",
  1457  			test: func(t *testing.T, mockCtrl *gomock.Controller) (tt testCase) {
  1458  				type outboundConfig struct{ Queue string }
  1459  				tt.give = whitespace.Expand(`
  1460  					outbounds:
  1461  						bar:
  1462  							unary:
  1463  								redis:
  1464  									queue: requests
  1465  				`)
  1466  
  1467  				redis := mockTransportSpecBuilder{
  1468  					Name:                 "redis",
  1469  					TransportConfig:      _typeOfEmptyStruct,
  1470  					OnewayOutboundConfig: reflect.TypeOf(&outboundConfig{}),
  1471  				}.Build(mockCtrl)
  1472  
  1473  				tt.specs = []TransportSpec{redis.Spec()}
  1474  				tt.wantErr = []string{
  1475  					`transport "redis" does not support unary outbound requests`,
  1476  				}
  1477  
  1478  				return
  1479  			},
  1480  		},
  1481  		{
  1482  			desc: "explicit oneway not supported",
  1483  			test: func(t *testing.T, mockCtrl *gomock.Controller) (tt testCase) {
  1484  				type outboundConfig struct{ Address string }
  1485  				tt.give = whitespace.Expand(`
  1486  					outbounds:
  1487  						bar:
  1488  							oneway:
  1489  								tchannel:
  1490  									address: localhost:4040
  1491  				`)
  1492  
  1493  				tchan := mockTransportSpecBuilder{
  1494  					Name:                "tchannel",
  1495  					TransportConfig:     _typeOfEmptyStruct,
  1496  					UnaryOutboundConfig: reflect.TypeOf(&outboundConfig{}),
  1497  				}.Build(mockCtrl)
  1498  
  1499  				tt.specs = []TransportSpec{tchan.Spec()}
  1500  				tt.wantErr = []string{
  1501  					`transport "tchannel" does not support oneway outbound requests`,
  1502  				}
  1503  
  1504  				return
  1505  			},
  1506  		},
  1507  		{
  1508  			desc: "explicit stream not supported",
  1509  			test: func(t *testing.T, mockCtrl *gomock.Controller) (tt testCase) {
  1510  				type outboundConfig struct{ Address string }
  1511  				tt.give = whitespace.Expand(`
  1512  					outbounds:
  1513  						bar:
  1514  							stream:
  1515  								tchannel:
  1516  									address: localhost:4040
  1517  				`)
  1518  
  1519  				tchan := mockTransportSpecBuilder{
  1520  					Name:                "tchannel",
  1521  					TransportConfig:     _typeOfEmptyStruct,
  1522  					UnaryOutboundConfig: reflect.TypeOf(&outboundConfig{}),
  1523  				}.Build(mockCtrl)
  1524  
  1525  				tt.specs = []TransportSpec{tchan.Spec()}
  1526  				tt.wantErr = []string{
  1527  					`transport "tchannel" does not support stream outbound requests`,
  1528  				}
  1529  
  1530  				return
  1531  			},
  1532  		},
  1533  		{
  1534  			desc: "implicit outbound service name override",
  1535  			test: func(t *testing.T, mockCtrl *gomock.Controller) (tt testCase) {
  1536  				type outboundConfig struct{ URL string }
  1537  				tt.serviceName = "foo"
  1538  				tt.give = whitespace.Expand(`
  1539  					outbounds:
  1540  						bar:
  1541  							http:
  1542  								url: http://localhost:8080/bar
  1543  						bar-staging:
  1544  							service: bar
  1545  							http:
  1546  								url: http://localhost:8081/bar
  1547  				`)
  1548  
  1549  				http := mockTransportSpecBuilder{
  1550  					Name:                 "http",
  1551  					TransportConfig:      _typeOfEmptyStruct,
  1552  					UnaryOutboundConfig:  reflect.TypeOf(outboundConfig{}),
  1553  					OnewayOutboundConfig: reflect.TypeOf(outboundConfig{}),
  1554  				}.Build(mockCtrl)
  1555  
  1556  				transport := transporttest.NewMockTransport(mockCtrl)
  1557  				unary := transporttest.NewMockUnaryOutbound(mockCtrl)
  1558  				oneway := transporttest.NewMockOnewayOutbound(mockCtrl)
  1559  				unaryStaging := transporttest.NewMockUnaryOutbound(mockCtrl)
  1560  				onewayStaging := transporttest.NewMockOnewayOutbound(mockCtrl)
  1561  
  1562  				http.EXPECT().
  1563  					BuildTransport(struct{}{}, kitMatcher{ServiceName: "foo"}).
  1564  					Return(transport, nil)
  1565  
  1566  				http.EXPECT().
  1567  					BuildUnaryOutbound(
  1568  						outboundConfig{URL: "http://localhost:8080/bar"},
  1569  						transport,
  1570  						kitMatcher{ServiceName: "foo", OutboundServiceName: "bar"}).
  1571  					Return(unary, nil)
  1572  				http.EXPECT().
  1573  					BuildOnewayOutbound(
  1574  						outboundConfig{URL: "http://localhost:8080/bar"},
  1575  						transport,
  1576  						kitMatcher{ServiceName: "foo", OutboundServiceName: "bar"}).
  1577  					Return(oneway, nil)
  1578  
  1579  				http.EXPECT().
  1580  					BuildUnaryOutbound(
  1581  						outboundConfig{URL: "http://localhost:8081/bar"},
  1582  						transport,
  1583  						kitMatcher{ServiceName: "foo", OutboundServiceName: "bar"}).
  1584  					Return(unaryStaging, nil)
  1585  				http.EXPECT().
  1586  					BuildOnewayOutbound(
  1587  						outboundConfig{URL: "http://localhost:8081/bar"},
  1588  						transport,
  1589  						kitMatcher{ServiceName: "foo", OutboundServiceName: "bar"}).
  1590  					Return(onewayStaging, nil)
  1591  
  1592  				tt.specs = []TransportSpec{http.Spec()}
  1593  				tt.wantConfig = yarpc.Config{
  1594  					Name: "foo",
  1595  					Outbounds: yarpc.Outbounds{
  1596  						"bar": {Unary: unary, Oneway: oneway},
  1597  						"bar-staging": {
  1598  							ServiceName: "bar",
  1599  							Unary:       unaryStaging,
  1600  							Oneway:      onewayStaging,
  1601  						},
  1602  					},
  1603  				}
  1604  
  1605  				return
  1606  			},
  1607  		},
  1608  		{
  1609  			desc: "interpolated string",
  1610  			test: func(t *testing.T, mockCtrl *gomock.Controller) (tt testCase) {
  1611  				type transportConfig struct {
  1612  					ServerAddress string `config:",interpolate"`
  1613  				}
  1614  
  1615  				type outboundConfig struct {
  1616  					QueueName string `config:"queue,interpolate"`
  1617  				}
  1618  
  1619  				tt.serviceName = "foo"
  1620  				tt.give = whitespace.Expand(`
  1621  					transports:
  1622  						redis:
  1623  							serverAddress: ${REDIS_ADDRESS}:${REDIS_PORT}
  1624  					outbounds:
  1625  						myservice:
  1626  							redis:
  1627  								queue: /${MYSERVICE_QUEUE}/inbound
  1628  				`)
  1629  				tt.env = map[string]string{
  1630  					"REDIS_ADDRESS":   "127.0.0.1",
  1631  					"REDIS_PORT":      "6379",
  1632  					"MYSERVICE_QUEUE": "myservice",
  1633  				}
  1634  
  1635  				redis := mockTransportSpecBuilder{
  1636  					Name:                 "redis",
  1637  					TransportConfig:      reflect.TypeOf(transportConfig{}),
  1638  					OnewayOutboundConfig: reflect.TypeOf(outboundConfig{}),
  1639  				}.Build(mockCtrl)
  1640  
  1641  				kit := kitMatcher{ServiceName: "foo"}
  1642  				transport := transporttest.NewMockTransport(mockCtrl)
  1643  				oneway := transporttest.NewMockOnewayOutbound(mockCtrl)
  1644  
  1645  				redis.EXPECT().
  1646  					BuildTransport(transportConfig{ServerAddress: "127.0.0.1:6379"}, kit).
  1647  					Return(transport, nil)
  1648  				redis.EXPECT().
  1649  					BuildOnewayOutbound(outboundConfig{QueueName: "/myservice/inbound"}, transport,
  1650  						kitMatcher{ServiceName: "foo", OutboundServiceName: "myservice"}).
  1651  					Return(oneway, nil)
  1652  
  1653  				tt.specs = []TransportSpec{redis.Spec()}
  1654  				tt.wantConfig = yarpc.Config{
  1655  					Name:      "foo",
  1656  					Outbounds: yarpc.Outbounds{"myservice": {Oneway: oneway}},
  1657  				}
  1658  
  1659  				return
  1660  			},
  1661  		},
  1662  		{
  1663  			desc: "interpolated integer",
  1664  			test: func(t *testing.T, mockCtrl *gomock.Controller) (tt testCase) {
  1665  				type inboundConfig struct {
  1666  					Port int `config:",interpolate"`
  1667  				}
  1668  
  1669  				tt.serviceName = "hi"
  1670  				tt.give = whitespace.Expand(`
  1671  					inbounds:
  1672  						http:
  1673  							port: 1${HTTP_PORT}
  1674  				`)
  1675  				tt.env = map[string]string{"HTTP_PORT": "8080"}
  1676  
  1677  				http := mockTransportSpecBuilder{
  1678  					Name:            "http",
  1679  					TransportConfig: _typeOfEmptyStruct,
  1680  					InboundConfig:   reflect.TypeOf(inboundConfig{}),
  1681  				}.Build(mockCtrl)
  1682  
  1683  				kit := kitMatcher{ServiceName: "hi"}
  1684  				transport := transporttest.NewMockTransport(mockCtrl)
  1685  				inbound := transporttest.NewMockInbound(mockCtrl)
  1686  
  1687  				http.EXPECT().BuildTransport(struct{}{}, kit).Return(transport, nil)
  1688  				http.EXPECT().
  1689  					BuildInbound(inboundConfig{Port: 18080}, transport, kit).
  1690  					Return(inbound, nil)
  1691  
  1692  				tt.specs = []TransportSpec{http.Spec()}
  1693  				tt.wantConfig = yarpc.Config{
  1694  					Name:     "hi",
  1695  					Inbounds: yarpc.Inbounds{inbound},
  1696  				}
  1697  
  1698  				return
  1699  			},
  1700  		},
  1701  		{
  1702  			desc: "intepolate non-string",
  1703  			test: func(t *testing.T, mockCtrl *gomock.Controller) (tt testCase) {
  1704  				type inboundConfig struct {
  1705  					Port int `config:",interpolate"`
  1706  				}
  1707  
  1708  				tt.serviceName = "foo"
  1709  				tt.give = whitespace.Expand(`
  1710  					inbounds:
  1711  						http:
  1712  							port: 80
  1713  				`)
  1714  
  1715  				http := mockTransportSpecBuilder{
  1716  					Name:            "http",
  1717  					TransportConfig: _typeOfEmptyStruct,
  1718  					InboundConfig:   reflect.TypeOf(inboundConfig{}),
  1719  				}.Build(mockCtrl)
  1720  
  1721  				kit := kitMatcher{ServiceName: "foo"}
  1722  				transport := transporttest.NewMockTransport(mockCtrl)
  1723  				inbound := transporttest.NewMockInbound(mockCtrl)
  1724  
  1725  				http.EXPECT().BuildTransport(struct{}{}, kit).Return(transport, nil)
  1726  				http.EXPECT().
  1727  					BuildInbound(inboundConfig{Port: 80}, transport, kit).
  1728  					Return(inbound, nil)
  1729  
  1730  				tt.specs = []TransportSpec{http.Spec()}
  1731  				tt.wantConfig = yarpc.Config{
  1732  					Name:     "foo",
  1733  					Inbounds: yarpc.Inbounds{inbound},
  1734  				}
  1735  
  1736  				return
  1737  			},
  1738  		},
  1739  		{
  1740  			desc: "bad interpolation string",
  1741  			test: func(t *testing.T, mockCtrl *gomock.Controller) (tt testCase) {
  1742  				type inboundConfig struct {
  1743  					Address string `config:",interpolate"`
  1744  				}
  1745  
  1746  				tt.serviceName = "hi"
  1747  				tt.give = whitespace.Expand(`
  1748  					inbounds:
  1749  						http:
  1750  							address: :${HTTP_PORT
  1751  				`)
  1752  				tt.env = map[string]string{"HTTP_PORT": "8080"}
  1753  
  1754  				http := mockTransportSpecBuilder{
  1755  					Name:            "http",
  1756  					TransportConfig: _typeOfEmptyStruct,
  1757  					InboundConfig:   reflect.TypeOf(inboundConfig{}),
  1758  				}.Build(mockCtrl)
  1759  
  1760  				tt.specs = []TransportSpec{http.Spec()}
  1761  				tt.wantErr = []string{
  1762  					"failed to decode inbound configuration:",
  1763  					`error reading into field "Address":`,
  1764  					`failed to parse ":${HTTP_PORT" for interpolation`,
  1765  				}
  1766  
  1767  				return
  1768  			},
  1769  		},
  1770  		{
  1771  			desc: "missing envvar",
  1772  			test: func(t *testing.T, mockCtrl *gomock.Controller) (tt testCase) {
  1773  				type inboundConfig struct {
  1774  					Address string `config:",interpolate"`
  1775  				}
  1776  
  1777  				tt.serviceName = "hi"
  1778  				tt.give = whitespace.Expand(`
  1779  					inbounds:
  1780  						http:
  1781  							address: :${HTTP_PORT}
  1782  				`)
  1783  
  1784  				http := mockTransportSpecBuilder{
  1785  					Name:            "http",
  1786  					TransportConfig: _typeOfEmptyStruct,
  1787  					InboundConfig:   reflect.TypeOf(inboundConfig{}),
  1788  				}.Build(mockCtrl)
  1789  
  1790  				tt.specs = []TransportSpec{http.Spec()}
  1791  				tt.wantErr = []string{
  1792  					"failed to decode inbound configuration:",
  1793  					`error reading into field "Address":`,
  1794  					`failed to render ":${HTTP_PORT}" with environment variables:`,
  1795  					`variable "HTTP_PORT" does not have a value or a default`,
  1796  				}
  1797  
  1798  				return
  1799  			},
  1800  		},
  1801  		{
  1802  			desc: "time.Duration from env",
  1803  			test: func(t *testing.T, mockCtrl *gomock.Controller) (tt testCase) {
  1804  				type inboundConfig struct {
  1805  					Timeout time.Duration `config:",interpolate"`
  1806  				}
  1807  
  1808  				tt.serviceName = "foo"
  1809  				tt.give = whitespace.Expand(`
  1810  					inbounds:
  1811  						http:
  1812  							timeout: ${TIMEOUT}
  1813  				`)
  1814  				tt.env = map[string]string{"TIMEOUT": "5s"}
  1815  
  1816  				http := mockTransportSpecBuilder{
  1817  					Name:            "http",
  1818  					TransportConfig: _typeOfEmptyStruct,
  1819  					InboundConfig:   reflect.TypeOf(inboundConfig{}),
  1820  				}.Build(mockCtrl)
  1821  
  1822  				kit := kitMatcher{ServiceName: "foo"}
  1823  				transport := transporttest.NewMockTransport(mockCtrl)
  1824  				inbound := transporttest.NewMockInbound(mockCtrl)
  1825  
  1826  				http.EXPECT().BuildTransport(struct{}{}, kit).Return(transport, nil)
  1827  				http.EXPECT().
  1828  					BuildInbound(inboundConfig{Timeout: 5 * time.Second}, transport, kit).
  1829  					Return(inbound, nil)
  1830  
  1831  				tt.specs = []TransportSpec{http.Spec()}
  1832  				tt.wantConfig = yarpc.Config{
  1833  					Name:     "foo",
  1834  					Inbounds: yarpc.Inbounds{inbound},
  1835  				}
  1836  
  1837  				return
  1838  			},
  1839  		},
  1840  	}
  1841  
  1842  	// We want to parameterize all tests over YAML and non-YAML modes. To
  1843  	// avoid two layers of nesting, we let this helper function call our test
  1844  	// runner.
  1845  	runTest := func(name string, f func(t *testing.T, useYAML bool)) {
  1846  		t.Run(name, func(t *testing.T) {
  1847  			t.Run("yaml", func(t *testing.T) { f(t, true) })
  1848  			t.Run("direct", func(t *testing.T) { f(t, false) })
  1849  		})
  1850  	}
  1851  
  1852  	for _, tc := range tests {
  1853  		runTest(tc.desc, func(t *testing.T, useYAML bool) {
  1854  			mockCtrl := gomock.NewController(t)
  1855  			defer mockCtrl.Finish()
  1856  
  1857  			tt := tc.test(t, mockCtrl)
  1858  			cfg := New(InterpolationResolver(mapVariableResolver(tt.env)))
  1859  
  1860  			if tt.specs != nil {
  1861  				for _, spec := range tt.specs {
  1862  					err := cfg.RegisterTransport(spec)
  1863  					require.NoError(t, err, "failed to register transport %q", spec.Name)
  1864  				}
  1865  			}
  1866  
  1867  			var (
  1868  				gotConfig yarpc.Config
  1869  				err       error
  1870  			)
  1871  			if useYAML {
  1872  				gotConfig, err = cfg.LoadConfigFromYAML(tt.serviceName, strings.NewReader(tt.give))
  1873  			} else {
  1874  				var data map[string]interface{}
  1875  				require.NoError(t, yaml.Unmarshal([]byte(tt.give), &data), "failed to parse YAML")
  1876  
  1877  				gotConfig, err = cfg.LoadConfig(tt.serviceName, data)
  1878  			}
  1879  
  1880  			if len(tt.wantErr) > 0 {
  1881  				require.Error(t, err, "expected failure")
  1882  				for _, msg := range tt.wantErr {
  1883  					assert.Contains(t, err.Error(), msg)
  1884  				}
  1885  				return
  1886  			}
  1887  
  1888  			require.NoError(t, err, "expected success")
  1889  			assert.Equal(t, tt.wantConfig, gotConfig, "config did not match")
  1890  		})
  1891  	}
  1892  }
  1893  
  1894  func mapVariableResolver(m map[string]string) interpolate.VariableResolver {
  1895  	return func(name string) (value string, ok bool) {
  1896  		value, ok = m[name]
  1897  		return
  1898  	}
  1899  }