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 }