github.com/uber/kraken@v0.1.4/utils/configutil/config_test.go (about)

     1  // Copyright (c) 2016-2019 Uber Technologies, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  package configutil
    15  
    16  import (
    17  	"fmt"
    18  	"io/ioutil"
    19  	"os"
    20  	"path/filepath"
    21  	"testing"
    22  
    23  	"github.com/stretchr/testify/require"
    24  	"gopkg.in/validator.v2"
    25  )
    26  
    27  const (
    28  	goodConfig = `
    29  listen_address: localhost:4385
    30  buffer_space: 1024
    31  X:
    32    Y:
    33      V: val1
    34      Z:
    35        K1: v1
    36  servers:
    37      - somewhere-zone1:8090
    38      - somewhere-else-zone1:8010
    39  `
    40  
    41  	invalidConfig = `
    42  listen_address:
    43  buffer_space: 1
    44  servers:
    45  `
    46  	goodExtendsConfig = `
    47  extends: %s
    48  buffer_space: 512
    49  X:
    50    Y:
    51      Z:
    52        K2: v2
    53  servers:
    54      - somewhere-sjc2:8090
    55      - somewhere-else-sjc2:8010
    56  `
    57  	goodYetAnotherExtendsConfig = `
    58  extends: %s
    59  buffer_space: 256
    60  servers:
    61      - somewhere-sjc3:8090
    62      - somewhere-else-sjc3:8010
    63  `
    64  )
    65  
    66  type configuration struct {
    67  	ListenAddress string   `yaml:"listen_address" validate:"nonzero"`
    68  	BufferSpace   int      `yaml:"buffer_space" validate:"min=255"`
    69  	Servers       []string `validate:"nonzero"`
    70  	X             Xconfig  `yaml:"X"`
    71  	Nodes         map[string]string
    72  	Secret        string
    73  }
    74  
    75  type Xconfig struct {
    76  	Y Yconfig `yaml:"Y"`
    77  }
    78  
    79  type Yconfig struct {
    80  	V string  `yaml:"V"`
    81  	Z Zconfig `yaml:"Z"`
    82  }
    83  
    84  type Zconfig struct {
    85  	K1 string `yaml:"K1"`
    86  	K2 string `yaml:"K2"`
    87  }
    88  
    89  func writeFile(t *testing.T, contents string) string {
    90  	require := require.New(t)
    91  
    92  	f, err := ioutil.TempFile("", "configtest")
    93  	require.NoError(err)
    94  
    95  	defer f.Close()
    96  
    97  	_, err = f.Write([]byte(contents))
    98  	require.NoError(err)
    99  
   100  	return f.Name()
   101  }
   102  
   103  func TestLoad(t *testing.T) {
   104  	require := require.New(t)
   105  
   106  	fname := writeFile(t, goodConfig)
   107  	defer os.Remove(fname)
   108  
   109  	var cfg configuration
   110  	err := Load(fname, &cfg)
   111  	require.NoError(err)
   112  	require.Equal("localhost:4385", cfg.ListenAddress)
   113  	require.Equal(1024, cfg.BufferSpace)
   114  	require.Equal([]string{"somewhere-zone1:8090", "somewhere-else-zone1:8010"}, cfg.Servers)
   115  }
   116  
   117  func TestLoadFilesExtends(t *testing.T) {
   118  	require := require.New(t)
   119  
   120  	fname := writeFile(t, goodConfig)
   121  	defer os.Remove(fname)
   122  
   123  	partialConfig := "buffer_space: 8080"
   124  	partial := writeFile(t, partialConfig)
   125  	defer os.Remove(partial)
   126  
   127  	var cfg configuration
   128  	err := loadFiles(&cfg, []string{fname, partial})
   129  	require.NoError(err)
   130  
   131  	require.Equal(8080, cfg.BufferSpace)
   132  	require.Equal("localhost:4385", cfg.ListenAddress)
   133  }
   134  
   135  func TestLoadFilesValidateOnce(t *testing.T) {
   136  	require := require.New(t)
   137  
   138  	const invalidConfig1 = `
   139      listen_address:
   140      buffer_space: 256
   141      servers:
   142      `
   143  
   144  	const invalidConfig2 = `
   145      listen_address: "localhost:8080"
   146      servers:
   147        - somewhere-else-zone1:8010
   148      `
   149  
   150  	fname1 := writeFile(t, invalidConfig1)
   151  	defer os.Remove(fname1)
   152  
   153  	fname2 := writeFile(t, invalidConfig2)
   154  	defer os.Remove(invalidConfig2)
   155  
   156  	// Either config by itself will not pass validation.
   157  	var cfg1 configuration
   158  	err := Load(fname1, &cfg1)
   159  	require.Error(err)
   160  
   161  	verr, ok := err.(ValidationError)
   162  	require.True(ok)
   163  	require.NotEmpty(verr.Error())
   164  
   165  	require.Equal(validator.ErrorArray{validator.ErrZeroValue}, verr.ErrForField("ListenAddress"))
   166  	require.Equal(validator.ErrorArray{validator.ErrZeroValue}, verr.ErrForField("Servers"))
   167  
   168  	var cfg2 configuration
   169  	err = Load(fname2, &cfg2)
   170  	require.Error(err)
   171  
   172  	verr, ok = err.(ValidationError)
   173  	require.True(ok)
   174  	require.NotEmpty(verr.Error())
   175  
   176  	require.Equal(validator.ErrorArray{validator.ErrMin}, verr.ErrForField("BufferSpace"))
   177  
   178  	// But merging load has no error.
   179  	var mergedCfg configuration
   180  	err = loadFiles(&mergedCfg, []string{fname1, fname2})
   181  	require.NoError(err)
   182  
   183  	require.Equal("localhost:8080", mergedCfg.ListenAddress)
   184  	require.Equal(256, mergedCfg.BufferSpace)
   185  	require.Equal([]string{"somewhere-else-zone1:8010"}, mergedCfg.Servers)
   186  }
   187  
   188  func TestMissingFile(t *testing.T) {
   189  	require := require.New(t)
   190  
   191  	var cfg configuration
   192  	err := Load("./no-config.yaml", &cfg)
   193  	require.Error(err)
   194  }
   195  
   196  func TestInvalidYAML(t *testing.T) {
   197  	require := require.New(t)
   198  
   199  	var cfg configuration
   200  	err := Load("./config_test.go", &cfg)
   201  	require.Error(err)
   202  }
   203  
   204  func TestInvalidConfig(t *testing.T) {
   205  	require := require.New(t)
   206  
   207  	fname := writeFile(t, invalidConfig)
   208  	defer os.Remove(fname)
   209  
   210  	var cfg configuration
   211  	err := Load(fname, &cfg)
   212  	require.Error(err)
   213  
   214  	verr, ok := err.(ValidationError)
   215  	require.True(ok)
   216  
   217  	errors := map[string]validator.ErrorArray{
   218  		"BufferSpace":   {validator.ErrMin},
   219  		"ListenAddress": {validator.ErrZeroValue},
   220  		"Servers":       {validator.ErrZeroValue},
   221  	}
   222  
   223  	for field, errs := range errors {
   224  		fieldErr := verr.ErrForField(field)
   225  		require.NotNil(t, fieldErr, "Could not find field level error for %s", field)
   226  		require.Equal(errs, fieldErr)
   227  	}
   228  }
   229  
   230  func TestExtendsConfig(t *testing.T) {
   231  	require := require.New(t)
   232  
   233  	fname := writeFile(t, goodConfig)
   234  	defer os.Remove(fname)
   235  
   236  	extends := fmt.Sprintf(goodExtendsConfig, filepath.Base(fname))
   237  	extendsfn := writeFile(t, extends)
   238  	defer os.Remove(extendsfn)
   239  
   240  	var cfg configuration
   241  	err := Load(extendsfn, &cfg)
   242  	require.NoError(err)
   243  
   244  	require.Equal("localhost:4385", cfg.ListenAddress)
   245  	require.Equal(512, cfg.BufferSpace)
   246  	require.Equal([]string{"somewhere-sjc2:8090", "somewhere-else-sjc2:8010"}, cfg.Servers)
   247  	require.Equal("v1", cfg.X.Y.Z.K1)
   248  	require.Equal("v2", cfg.X.Y.Z.K2)
   249  
   250  	require.Equal("val1", cfg.X.Y.V)
   251  }
   252  
   253  func TestExtendsConfigDeep(t *testing.T) {
   254  	require := require.New(t)
   255  
   256  	fname := writeFile(t, goodConfig)
   257  	defer os.Remove(fname)
   258  
   259  	extends := fmt.Sprintf(goodExtendsConfig, filepath.Base(fname))
   260  	extendsfn := writeFile(t, extends)
   261  	defer os.Remove(extendsfn)
   262  
   263  	extends2 := fmt.Sprintf(goodYetAnotherExtendsConfig, filepath.Base(extends))
   264  	extendsfn2 := writeFile(t, extends2)
   265  	defer os.Remove(extendsfn2)
   266  
   267  	var cfg configuration
   268  	err := Load(extendsfn2, &cfg)
   269  	require.NoError(err)
   270  
   271  	require.Equal("localhost:4385", cfg.ListenAddress)
   272  	require.Equal(256, cfg.BufferSpace)
   273  	require.Equal([]string{"somewhere-sjc3:8090", "somewhere-else-sjc3:8010"}, cfg.Servers)
   274  }
   275  
   276  func TestExtendsConfigCircularRef(t *testing.T) {
   277  	require := require.New(t)
   278  
   279  	f1, err := ioutil.TempFile("", "configtest")
   280  	require.NoError(err)
   281  
   282  	f2, err := ioutil.TempFile("", "configtest")
   283  	require.NoError(err)
   284  
   285  	f3, err := ioutil.TempFile("", "configtest")
   286  	require.NoError(err)
   287  
   288  	defer f1.Close()
   289  	defer f2.Close()
   290  	defer f3.Close()
   291  
   292  	_, err = f1.Write([]byte(goodConfig))
   293  	require.NoError(err)
   294  	defer os.Remove(f1.Name())
   295  
   296  	extends := fmt.Sprintf(goodExtendsConfig, filepath.Base(f3.Name()))
   297  	_, err = f2.Write([]byte(extends))
   298  	require.NoError(err)
   299  
   300  	defer os.Remove(f2.Name())
   301  
   302  	extends2 := fmt.Sprintf(goodYetAnotherExtendsConfig, filepath.Base(f2.Name()))
   303  	_, err = f3.Write([]byte(extends2))
   304  	require.NoError(err)
   305  
   306  	defer os.Remove(f3.Name())
   307  
   308  	var cfg configuration
   309  	err = Load(f3.Name(), &cfg)
   310  	require.Error(err)
   311  	require.Contains(err.Error(), "cyclic reference in configuration extends detected")
   312  }
   313  
   314  func TestResolveExtends(t *testing.T) {
   315  	require := require.New(t)
   316  
   317  	tests := []struct {
   318  		fpath    string
   319  		extends  map[string]string
   320  		expected []string
   321  		err      error
   322  	}{
   323  		{
   324  			fpath:    "/configs/c1",
   325  			extends:  map[string]string{},
   326  			expected: []string{"/configs/c1"},
   327  		},
   328  		{
   329  			fpath:    "/configs/c1",
   330  			extends:  map[string]string{"/configs/c1": "/configs/c2"},
   331  			expected: []string{"/configs/c2", "/configs/c1"},
   332  		},
   333  		{
   334  			fpath:    "/configs/c1",
   335  			extends:  map[string]string{"/configs/c1": "c2"},
   336  			expected: []string{"/configs/c2", "/configs/c1"},
   337  		},
   338  		{
   339  			fpath:    "/configs/c1",
   340  			extends:  map[string]string{"/configs/c1": "c2", "/configs/c2": "c1"},
   341  			expected: nil,
   342  			err:      ErrCycleRef,
   343  		},
   344  		{
   345  			fpath:    "/configs/c1",
   346  			extends:  map[string]string{"/configs/c1": "/etc/c2", "/etc/c2": "c3"},
   347  			expected: []string{"/etc/c3", "/etc/c2", "/configs/c1"},
   348  		},
   349  	}
   350  
   351  	for _, tt := range tests {
   352  		fn := func(filename string) (string, error) {
   353  			target, found := tt.extends[filename]
   354  			if !found {
   355  				return "", nil
   356  			}
   357  			return target, nil
   358  		}
   359  		filenames, err := resolveExtends(tt.fpath, fn)
   360  		require.Equal(tt.err, err)
   361  		require.Equal(tt.expected, filenames)
   362  	}
   363  }