github.com/MetalBlockchain/metalgo@v1.11.9/config/config_test.go (about) 1 // Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. 2 // See the file LICENSE for licensing terms. 3 4 package config 5 6 import ( 7 "encoding/base64" 8 "encoding/json" 9 "fmt" 10 "log" 11 "os" 12 "path/filepath" 13 "testing" 14 15 "github.com/spf13/pflag" 16 "github.com/spf13/viper" 17 "github.com/stretchr/testify/require" 18 19 "github.com/MetalBlockchain/metalgo/chains" 20 "github.com/MetalBlockchain/metalgo/ids" 21 "github.com/MetalBlockchain/metalgo/snow/consensus/snowball" 22 "github.com/MetalBlockchain/metalgo/subnets" 23 ) 24 25 const chainConfigFilenameExtention = ".ex" 26 27 func TestGetChainConfigsFromFiles(t *testing.T) { 28 tests := map[string]struct { 29 configs map[string]string 30 upgrades map[string]string 31 expected map[string]chains.ChainConfig 32 }{ 33 "no chain configs": { 34 configs: map[string]string{}, 35 upgrades: map[string]string{}, 36 expected: map[string]chains.ChainConfig{}, 37 }, 38 "valid chain-id": { 39 configs: map[string]string{"yH8D7ThNJkxmtkuv2jgBa4P1Rn3Qpr4pPr7QYNfcdoS6k6HWp": "hello", "2JVSBoinj9C2J33VntvzYtVJNZdN2NKiwwKjcumHUWEb5DbBrm": "world"}, 40 upgrades: map[string]string{"yH8D7ThNJkxmtkuv2jgBa4P1Rn3Qpr4pPr7QYNfcdoS6k6HWp": "helloUpgrades"}, 41 expected: func() map[string]chains.ChainConfig { 42 m := map[string]chains.ChainConfig{} 43 id1, err := ids.FromString("yH8D7ThNJkxmtkuv2jgBa4P1Rn3Qpr4pPr7QYNfcdoS6k6HWp") 44 require.NoError(t, err) 45 m[id1.String()] = chains.ChainConfig{Config: []byte("hello"), Upgrade: []byte("helloUpgrades")} 46 47 id2, err := ids.FromString("2JVSBoinj9C2J33VntvzYtVJNZdN2NKiwwKjcumHUWEb5DbBrm") 48 require.NoError(t, err) 49 m[id2.String()] = chains.ChainConfig{Config: []byte("world"), Upgrade: []byte(nil)} 50 51 return m 52 }(), 53 }, 54 "valid alias": { 55 configs: map[string]string{"C": "hello", "X": "world"}, 56 upgrades: map[string]string{"C": "upgradess"}, 57 expected: func() map[string]chains.ChainConfig { 58 m := map[string]chains.ChainConfig{} 59 m["C"] = chains.ChainConfig{Config: []byte("hello"), Upgrade: []byte("upgradess")} 60 m["X"] = chains.ChainConfig{Config: []byte("world"), Upgrade: []byte(nil)} 61 62 return m 63 }(), 64 }, 65 } 66 67 for name, test := range tests { 68 t.Run(name, func(t *testing.T) { 69 require := require.New(t) 70 root := t.TempDir() 71 configJSON := fmt.Sprintf(`{%q: %q}`, ChainConfigDirKey, root) 72 configFile := setupConfigJSON(t, root, configJSON) 73 chainsDir := root 74 // Create custom configs 75 for key, value := range test.configs { 76 chainDir := filepath.Join(chainsDir, key) 77 setupFile(t, chainDir, chainConfigFileName+chainConfigFilenameExtention, value) 78 } 79 for key, value := range test.upgrades { 80 chainDir := filepath.Join(chainsDir, key) 81 setupFile(t, chainDir, chainUpgradeFileName+chainConfigFilenameExtention, value) 82 } 83 84 v := setupViper(configFile) 85 86 // Parse config 87 require.Equal(root, v.GetString(ChainConfigDirKey)) 88 chainConfigs, err := getChainConfigs(v) 89 require.NoError(err) 90 require.Equal(test.expected, chainConfigs) 91 }) 92 } 93 } 94 95 func TestGetChainConfigsDirNotExist(t *testing.T) { 96 tests := map[string]struct { 97 structure string 98 file map[string]string 99 expectedErr error 100 expected map[string]chains.ChainConfig 101 }{ 102 "cdir not exist": { 103 structure: "/", 104 file: map[string]string{"config.ex": "noeffect"}, 105 expectedErr: errCannotReadDirectory, 106 expected: nil, 107 }, 108 "cdir is file ": { 109 structure: "/", 110 file: map[string]string{"cdir": "noeffect"}, 111 expectedErr: errCannotReadDirectory, 112 expected: nil, 113 }, 114 "chain subdir not exist": { 115 structure: "/cdir/", 116 file: map[string]string{"config.ex": "noeffect"}, 117 expectedErr: nil, 118 expected: map[string]chains.ChainConfig{}, 119 }, 120 "full structure": { 121 structure: "/cdir/C/", 122 file: map[string]string{"config.ex": "hello"}, 123 expectedErr: nil, 124 expected: map[string]chains.ChainConfig{"C": {Config: []byte("hello"), Upgrade: []byte(nil)}}, 125 }, 126 } 127 128 for name, test := range tests { 129 t.Run(name, func(t *testing.T) { 130 require := require.New(t) 131 root := t.TempDir() 132 chainConfigDir := filepath.Join(root, "cdir") 133 configJSON := fmt.Sprintf(`{%q: %q}`, ChainConfigDirKey, chainConfigDir) 134 configFile := setupConfigJSON(t, root, configJSON) 135 136 dirToCreate := filepath.Join(root, test.structure) 137 require.NoError(os.MkdirAll(dirToCreate, 0o700)) 138 139 for key, value := range test.file { 140 setupFile(t, dirToCreate, key, value) 141 } 142 v := setupViper(configFile) 143 144 // Parse config 145 require.Equal(chainConfigDir, v.GetString(ChainConfigDirKey)) 146 147 // don't read with getConfigFromViper since it's very slow. 148 chainConfigs, err := getChainConfigs(v) 149 require.ErrorIs(err, test.expectedErr) 150 require.Equal(test.expected, chainConfigs) 151 }) 152 } 153 } 154 155 func TestSetChainConfigDefaultDir(t *testing.T) { 156 require := require.New(t) 157 root := t.TempDir() 158 // changes internal package variable, since using defaultDir (under user home) is risky. 159 defaultChainConfigDir = filepath.Join(root, "cdir") 160 configFilePath := setupConfigJSON(t, root, "{}") 161 162 v := setupViper(configFilePath) 163 require.Equal(defaultChainConfigDir, v.GetString(ChainConfigDirKey)) 164 165 chainsDir := filepath.Join(defaultChainConfigDir, "C") 166 setupFile(t, chainsDir, chainConfigFileName+chainConfigFilenameExtention, "helloworld") 167 chainConfigs, err := getChainConfigs(v) 168 require.NoError(err) 169 expected := map[string]chains.ChainConfig{"C": {Config: []byte("helloworld"), Upgrade: []byte(nil)}} 170 require.Equal(expected, chainConfigs) 171 } 172 173 func TestGetChainConfigsFromFlags(t *testing.T) { 174 tests := map[string]struct { 175 fullConfigs map[string]chains.ChainConfig 176 expected map[string]chains.ChainConfig 177 }{ 178 "no chain configs": { 179 fullConfigs: map[string]chains.ChainConfig{}, 180 expected: map[string]chains.ChainConfig{}, 181 }, 182 "valid chain-id": { 183 fullConfigs: func() map[string]chains.ChainConfig { 184 m := map[string]chains.ChainConfig{} 185 id1, err := ids.FromString("yH8D7ThNJkxmtkuv2jgBa4P1Rn3Qpr4pPr7QYNfcdoS6k6HWp") 186 require.NoError(t, err) 187 m[id1.String()] = chains.ChainConfig{Config: []byte("hello"), Upgrade: []byte("helloUpgrades")} 188 189 id2, err := ids.FromString("2JVSBoinj9C2J33VntvzYtVJNZdN2NKiwwKjcumHUWEb5DbBrm") 190 require.NoError(t, err) 191 m[id2.String()] = chains.ChainConfig{Config: []byte("world"), Upgrade: []byte(nil)} 192 193 return m 194 }(), 195 expected: func() map[string]chains.ChainConfig { 196 m := map[string]chains.ChainConfig{} 197 id1, err := ids.FromString("yH8D7ThNJkxmtkuv2jgBa4P1Rn3Qpr4pPr7QYNfcdoS6k6HWp") 198 require.NoError(t, err) 199 m[id1.String()] = chains.ChainConfig{Config: []byte("hello"), Upgrade: []byte("helloUpgrades")} 200 201 id2, err := ids.FromString("2JVSBoinj9C2J33VntvzYtVJNZdN2NKiwwKjcumHUWEb5DbBrm") 202 require.NoError(t, err) 203 m[id2.String()] = chains.ChainConfig{Config: []byte("world"), Upgrade: []byte(nil)} 204 205 return m 206 }(), 207 }, 208 "valid alias": { 209 fullConfigs: map[string]chains.ChainConfig{ 210 "C": {Config: []byte("hello"), Upgrade: []byte("upgradess")}, 211 "X": {Config: []byte("world"), Upgrade: []byte(nil)}, 212 }, 213 expected: func() map[string]chains.ChainConfig { 214 m := map[string]chains.ChainConfig{} 215 m["C"] = chains.ChainConfig{Config: []byte("hello"), Upgrade: []byte("upgradess")} 216 m["X"] = chains.ChainConfig{Config: []byte("world"), Upgrade: []byte(nil)} 217 218 return m 219 }(), 220 }, 221 } 222 223 for name, test := range tests { 224 t.Run(name, func(t *testing.T) { 225 require := require.New(t) 226 jsonMaps, err := json.Marshal(test.fullConfigs) 227 require.NoError(err) 228 encodedFileContent := base64.StdEncoding.EncodeToString(jsonMaps) 229 230 // build viper config 231 v := setupViperFlags() 232 v.Set(ChainConfigContentKey, encodedFileContent) 233 234 // Parse config 235 chainConfigs, err := getChainConfigs(v) 236 require.NoError(err) 237 require.Equal(test.expected, chainConfigs) 238 }) 239 } 240 } 241 242 func TestGetVMAliasesFromFile(t *testing.T) { 243 tests := map[string]struct { 244 givenJSON string 245 expected map[ids.ID][]string 246 expectedErr error 247 }{ 248 "wrong vm id": { 249 givenJSON: `{"wrongVmId": ["vm1","vm2"]}`, 250 expected: nil, 251 expectedErr: errUnmarshalling, 252 }, 253 "vm id": { 254 givenJSON: `{"2Ctt6eGAeo4MLqTmGa7AdRecuVMPGWEX9wSsCLBYrLhX4a394i": ["vm1","vm2"], 255 "Gmt4fuNsGJAd2PX86LBvycGaBpgCYKbuULdCLZs3SEs1Jx1LU": ["vm3", "vm4"] }`, 256 expected: func() map[ids.ID][]string { 257 m := map[ids.ID][]string{} 258 id1, _ := ids.FromString("2Ctt6eGAeo4MLqTmGa7AdRecuVMPGWEX9wSsCLBYrLhX4a394i") 259 id2, _ := ids.FromString("Gmt4fuNsGJAd2PX86LBvycGaBpgCYKbuULdCLZs3SEs1Jx1LU") 260 m[id1] = []string{"vm1", "vm2"} 261 m[id2] = []string{"vm3", "vm4"} 262 return m 263 }(), 264 expectedErr: nil, 265 }, 266 } 267 268 for name, test := range tests { 269 t.Run(name, func(t *testing.T) { 270 require := require.New(t) 271 root := t.TempDir() 272 aliasPath := filepath.Join(root, "aliases.json") 273 configJSON := fmt.Sprintf(`{%q: %q}`, VMAliasesFileKey, aliasPath) 274 configFilePath := setupConfigJSON(t, root, configJSON) 275 setupFile(t, root, "aliases.json", test.givenJSON) 276 v := setupViper(configFilePath) 277 vmAliases, err := getVMAliases(v) 278 require.ErrorIs(err, test.expectedErr) 279 require.Equal(test.expected, vmAliases) 280 }) 281 } 282 } 283 284 func TestGetVMAliasesFromFlag(t *testing.T) { 285 tests := map[string]struct { 286 givenJSON string 287 expected map[ids.ID][]string 288 expectedErr error 289 }{ 290 "wrong vm id": { 291 givenJSON: `{"wrongVmId": ["vm1","vm2"]}`, 292 expected: nil, 293 expectedErr: errUnmarshalling, 294 }, 295 "vm id": { 296 givenJSON: `{"2Ctt6eGAeo4MLqTmGa7AdRecuVMPGWEX9wSsCLBYrLhX4a394i": ["vm1","vm2"], 297 "Gmt4fuNsGJAd2PX86LBvycGaBpgCYKbuULdCLZs3SEs1Jx1LU": ["vm3", "vm4"] }`, 298 expected: func() map[ids.ID][]string { 299 m := map[ids.ID][]string{} 300 id1, _ := ids.FromString("2Ctt6eGAeo4MLqTmGa7AdRecuVMPGWEX9wSsCLBYrLhX4a394i") 301 id2, _ := ids.FromString("Gmt4fuNsGJAd2PX86LBvycGaBpgCYKbuULdCLZs3SEs1Jx1LU") 302 m[id1] = []string{"vm1", "vm2"} 303 m[id2] = []string{"vm3", "vm4"} 304 return m 305 }(), 306 expectedErr: nil, 307 }, 308 } 309 310 for name, test := range tests { 311 t.Run(name, func(t *testing.T) { 312 require := require.New(t) 313 encodedFileContent := base64.StdEncoding.EncodeToString([]byte(test.givenJSON)) 314 315 // build viper config 316 v := setupViperFlags() 317 v.Set(VMAliasesContentKey, encodedFileContent) 318 319 vmAliases, err := getVMAliases(v) 320 require.ErrorIs(err, test.expectedErr) 321 require.Equal(test.expected, vmAliases) 322 }) 323 } 324 } 325 326 func TestGetVMAliasesDefaultDir(t *testing.T) { 327 require := require.New(t) 328 root := t.TempDir() 329 // changes internal package variable, since using defaultDir (under user home) is risky. 330 defaultVMAliasFilePath = filepath.Join(root, "aliases.json") 331 configFilePath := setupConfigJSON(t, root, "{}") 332 333 v := setupViper(configFilePath) 334 require.Equal(defaultVMAliasFilePath, v.GetString(VMAliasesFileKey)) 335 336 setupFile(t, root, "aliases.json", `{"2Ctt6eGAeo4MLqTmGa7AdRecuVMPGWEX9wSsCLBYrLhX4a394i": ["vm1","vm2"]}`) 337 vmAliases, err := getVMAliases(v) 338 require.NoError(err) 339 340 expected := map[ids.ID][]string{} 341 id, _ := ids.FromString("2Ctt6eGAeo4MLqTmGa7AdRecuVMPGWEX9wSsCLBYrLhX4a394i") 342 expected[id] = []string{"vm1", "vm2"} 343 require.Equal(expected, vmAliases) 344 } 345 346 func TestGetVMAliasesDirNotExists(t *testing.T) { 347 require := require.New(t) 348 root := t.TempDir() 349 aliasPath := "/not/exists" 350 // set it explicitly 351 configJSON := fmt.Sprintf(`{%q: %q}`, VMAliasesFileKey, aliasPath) 352 configFilePath := setupConfigJSON(t, root, configJSON) 353 v := setupViper(configFilePath) 354 vmAliases, err := getVMAliases(v) 355 require.ErrorIs(err, errFileDoesNotExist) 356 require.Nil(vmAliases) 357 358 // do not set it explicitly 359 configJSON = "{}" 360 configFilePath = setupConfigJSON(t, root, configJSON) 361 v = setupViper(configFilePath) 362 vmAliases, err = getVMAliases(v) 363 require.Nil(vmAliases) 364 require.NoError(err) 365 } 366 367 func TestGetSubnetConfigsFromFile(t *testing.T) { 368 subnetID, err := ids.FromString("2Ctt6eGAeo4MLqTmGa7AdRecuVMPGWEX9wSsCLBYrLhX4a394i") 369 require.NoError(t, err) 370 371 tests := map[string]struct { 372 fileName string 373 givenJSON string 374 testF func(*require.Assertions, map[ids.ID]subnets.Config) 375 expectedErr error 376 }{ 377 "wrong config": { 378 fileName: "2Ctt6eGAeo4MLqTmGa7AdRecuVMPGWEX9wSsCLBYrLhX4a394i.json", 379 givenJSON: `thisisnotjson`, 380 testF: func(require *require.Assertions, given map[ids.ID]subnets.Config) { 381 require.Nil(given) 382 }, 383 expectedErr: errUnmarshalling, 384 }, 385 "subnet is not tracked": { 386 fileName: "Gmt4fuNsGJAd2PX86LBvycGaBpgCYKbuULdCLZs3SEs1Jx1LU.json", 387 givenJSON: `{"validatorOnly": true}`, 388 testF: func(require *require.Assertions, given map[ids.ID]subnets.Config) { 389 require.Empty(given) 390 }, 391 expectedErr: nil, 392 }, 393 "wrong extension": { 394 fileName: "2Ctt6eGAeo4MLqTmGa7AdRecuVMPGWEX9wSsCLBYrLhX4a394i.yaml", 395 givenJSON: `{"validatorOnly": true}`, 396 testF: func(require *require.Assertions, given map[ids.ID]subnets.Config) { 397 require.Empty(given) 398 }, 399 expectedErr: nil, 400 }, 401 "invalid consensus parameters": { 402 fileName: "2Ctt6eGAeo4MLqTmGa7AdRecuVMPGWEX9wSsCLBYrLhX4a394i.json", 403 givenJSON: `{"consensusParameters":{"k": 111, "alphaPreference":1234} }`, 404 testF: func(require *require.Assertions, given map[ids.ID]subnets.Config) { 405 require.Nil(given) 406 }, 407 expectedErr: snowball.ErrParametersInvalid, 408 }, 409 "correct config": { 410 fileName: "2Ctt6eGAeo4MLqTmGa7AdRecuVMPGWEX9wSsCLBYrLhX4a394i.json", 411 givenJSON: `{"validatorOnly": true, "consensusParameters":{"alphaConfidence":16} }`, 412 testF: func(require *require.Assertions, given map[ids.ID]subnets.Config) { 413 id, _ := ids.FromString("2Ctt6eGAeo4MLqTmGa7AdRecuVMPGWEX9wSsCLBYrLhX4a394i") 414 config, ok := given[id] 415 require.True(ok) 416 417 require.True(config.ValidatorOnly) 418 require.Equal(16, config.ConsensusParameters.AlphaConfidence) 419 // must still respect defaults 420 require.Equal(20, config.ConsensusParameters.K) 421 }, 422 expectedErr: nil, 423 }, 424 } 425 426 for name, test := range tests { 427 t.Run(name, func(t *testing.T) { 428 require := require.New(t) 429 430 root := t.TempDir() 431 subnetPath := filepath.Join(root, "subnets") 432 433 configJSON := fmt.Sprintf(`{%q: %q}`, SubnetConfigDirKey, subnetPath) 434 configFilePath := setupConfigJSON(t, root, configJSON) 435 436 setupFile(t, subnetPath, test.fileName, test.givenJSON) 437 438 v := setupViper(configFilePath) 439 subnetConfigs, err := getSubnetConfigs(v, []ids.ID{subnetID}) 440 require.ErrorIs(err, test.expectedErr) 441 if test.expectedErr != nil { 442 return 443 } 444 test.testF(require, subnetConfigs) 445 }) 446 } 447 } 448 449 func TestGetSubnetConfigsFromFlags(t *testing.T) { 450 subnetID, err := ids.FromString("2Ctt6eGAeo4MLqTmGa7AdRecuVMPGWEX9wSsCLBYrLhX4a394i") 451 require.NoError(t, err) 452 453 tests := map[string]struct { 454 givenJSON string 455 testF func(*require.Assertions, map[ids.ID]subnets.Config) 456 expectedErr error 457 }{ 458 "no configs": { 459 givenJSON: `{}`, 460 testF: func(require *require.Assertions, given map[ids.ID]subnets.Config) { 461 require.Empty(given) 462 }, 463 expectedErr: nil, 464 }, 465 "entry with no config": { 466 givenJSON: `{"2Ctt6eGAeo4MLqTmGa7AdRecuVMPGWEX9wSsCLBYrLhX4a394i":{}}`, 467 testF: func(require *require.Assertions, given map[ids.ID]subnets.Config) { 468 require.Len(given, 1) 469 id, _ := ids.FromString("2Ctt6eGAeo4MLqTmGa7AdRecuVMPGWEX9wSsCLBYrLhX4a394i") 470 config, ok := given[id] 471 require.True(ok) 472 // should respect defaults 473 require.Equal(20, config.ConsensusParameters.K) 474 }, 475 expectedErr: nil, 476 }, 477 "subnet is not tracked": { 478 givenJSON: `{"Gmt4fuNsGJAd2PX86LBvycGaBpgCYKbuULdCLZs3SEs1Jx1LU":{"validatorOnly":true}}`, 479 testF: func(require *require.Assertions, given map[ids.ID]subnets.Config) { 480 require.Empty(given) 481 }, 482 expectedErr: nil, 483 }, 484 "invalid consensus parameters": { 485 givenJSON: `{ 486 "2Ctt6eGAeo4MLqTmGa7AdRecuVMPGWEX9wSsCLBYrLhX4a394i": { 487 "consensusParameters": { 488 "k": 111, 489 "alphaPreference": 1234 490 } 491 } 492 }`, 493 testF: func(require *require.Assertions, given map[ids.ID]subnets.Config) { 494 require.Empty(given) 495 }, 496 expectedErr: snowball.ErrParametersInvalid, 497 }, 498 "correct config": { 499 givenJSON: `{ 500 "2Ctt6eGAeo4MLqTmGa7AdRecuVMPGWEX9wSsCLBYrLhX4a394i": { 501 "consensusParameters": { 502 "k": 30, 503 "alphaPreference": 16, 504 "alphaConfidence": 20 505 }, 506 "validatorOnly": true 507 } 508 }`, 509 testF: func(require *require.Assertions, given map[ids.ID]subnets.Config) { 510 id, _ := ids.FromString("2Ctt6eGAeo4MLqTmGa7AdRecuVMPGWEX9wSsCLBYrLhX4a394i") 511 config, ok := given[id] 512 require.True(ok) 513 require.True(config.ValidatorOnly) 514 require.Equal(16, config.ConsensusParameters.AlphaPreference) 515 require.Equal(20, config.ConsensusParameters.AlphaConfidence) 516 require.Equal(30, config.ConsensusParameters.K) 517 // must still respect defaults 518 require.Equal(256, config.ConsensusParameters.MaxOutstandingItems) 519 }, 520 expectedErr: nil, 521 }, 522 } 523 524 for name, test := range tests { 525 t.Run(name, func(t *testing.T) { 526 require := require.New(t) 527 528 encodedFileContent := base64.StdEncoding.EncodeToString([]byte(test.givenJSON)) 529 530 // build viper config 531 v := setupViperFlags() 532 v.Set(SubnetConfigContentKey, encodedFileContent) 533 534 subnetConfigs, err := getSubnetConfigs(v, []ids.ID{subnetID}) 535 require.ErrorIs(err, test.expectedErr) 536 if test.expectedErr != nil { 537 return 538 } 539 test.testF(require, subnetConfigs) 540 }) 541 } 542 } 543 544 // setups config json file and writes content 545 func setupConfigJSON(t *testing.T, rootPath string, value string) string { 546 configFilePath := filepath.Join(rootPath, "config.json") 547 require.NoError(t, os.WriteFile(configFilePath, []byte(value), 0o600)) 548 return configFilePath 549 } 550 551 // setups file creates necessary path and writes value to it. 552 func setupFile(t *testing.T, path string, fileName string, value string) { 553 require := require.New(t) 554 555 require.NoError(os.MkdirAll(path, 0o700)) 556 filePath := filepath.Join(path, fileName) 557 require.NoError(os.WriteFile(filePath, []byte(value), 0o600)) 558 } 559 560 func setupViperFlags() *viper.Viper { 561 v := viper.New() 562 fs := BuildFlagSet() 563 pflag.Parse() 564 if err := v.BindPFlags(fs); err != nil { 565 log.Fatal(err) 566 } 567 return v 568 } 569 570 func setupViper(configFilePath string) *viper.Viper { 571 v := setupViperFlags() 572 v.SetConfigFile(configFilePath) 573 if err := v.ReadInConfig(); err != nil { 574 log.Fatal(err) 575 } 576 return v 577 }