github.com/juju/charm/v11@v11.2.0/config_test.go (about) 1 // Copyright 2011, 2012, 2013 Canonical Ltd. 2 // Licensed under the LGPLv3, see LICENCE file for details. 3 4 package charm_test 5 6 import ( 7 "bytes" 8 "fmt" 9 "strings" 10 11 jc "github.com/juju/testing/checkers" 12 gc "gopkg.in/check.v1" 13 "gopkg.in/yaml.v2" 14 15 "github.com/juju/charm/v11" 16 ) 17 18 type ConfigSuite struct { 19 config *charm.Config 20 } 21 22 var _ = gc.Suite(&ConfigSuite{}) 23 24 func (s *ConfigSuite) SetUpSuite(c *gc.C) { 25 // Just use a single shared config for the whole suite. There's no use case 26 // for mutating a config, we assume that nobody will do so here. 27 var err error 28 s.config, err = charm.ReadConfig(bytes.NewBuffer([]byte(` 29 options: 30 title: 31 default: My Title 32 description: A descriptive title used for the application. 33 type: string 34 subtitle: 35 default: "" 36 description: An optional subtitle used for the application. 37 outlook: 38 description: No default outlook. 39 # type defaults to string in python 40 username: 41 default: admin001 42 description: The name of the initial account (given admin permissions). 43 type: string 44 skill-level: 45 description: A number indicating skill. 46 type: int 47 agility-ratio: 48 description: A number from 0 to 1 indicating agility. 49 type: float 50 reticulate-splines: 51 description: Whether to reticulate splines on launch, or not. 52 type: boolean 53 secret-foo: 54 description: A secret value. 55 type: secret 56 `))) 57 c.Assert(err, gc.IsNil) 58 } 59 60 func (s *ConfigSuite) TestReadSample(c *gc.C) { 61 c.Assert(s.config.Options, jc.DeepEquals, map[string]charm.Option{ 62 "title": { 63 Default: "My Title", 64 Description: "A descriptive title used for the application.", 65 Type: "string", 66 }, 67 "subtitle": { 68 Default: "", 69 Description: "An optional subtitle used for the application.", 70 Type: "string", 71 }, 72 "username": { 73 Default: "admin001", 74 Description: "The name of the initial account (given admin permissions).", 75 Type: "string", 76 }, 77 "outlook": { 78 Description: "No default outlook.", 79 Type: "string", 80 }, 81 "skill-level": { 82 Description: "A number indicating skill.", 83 Type: "int", 84 }, 85 "agility-ratio": { 86 Description: "A number from 0 to 1 indicating agility.", 87 Type: "float", 88 }, 89 "reticulate-splines": { 90 Description: "Whether to reticulate splines on launch, or not.", 91 Type: "boolean", 92 }, 93 "secret-foo": { 94 Description: "A secret value.", 95 Type: "secret", 96 }, 97 }) 98 } 99 100 func (s *ConfigSuite) TestDefaultSettings(c *gc.C) { 101 c.Assert(s.config.DefaultSettings(), jc.DeepEquals, charm.Settings{ 102 "title": "My Title", 103 "subtitle": "", 104 "username": "admin001", 105 "secret-foo": nil, 106 "outlook": nil, 107 "skill-level": nil, 108 "agility-ratio": nil, 109 "reticulate-splines": nil, 110 }) 111 } 112 113 func (s *ConfigSuite) TestFilterSettings(c *gc.C) { 114 settings := s.config.FilterSettings(charm.Settings{ 115 "title": "something valid", 116 "username": nil, 117 "unknown": "whatever", 118 "outlook": "", 119 "skill-level": 5.5, 120 "agility-ratio": true, 121 "reticulate-splines": "hullo", 122 }) 123 c.Assert(settings, jc.DeepEquals, charm.Settings{ 124 "title": "something valid", 125 "username": nil, 126 "outlook": "", 127 }) 128 } 129 130 func (s *ConfigSuite) TestValidateSettings(c *gc.C) { 131 for i, test := range []struct { 132 info string 133 input charm.Settings 134 expect charm.Settings 135 err string 136 }{ 137 { 138 info: "nil settings are valid", 139 expect: charm.Settings{}, 140 }, { 141 info: "empty settings are valid", 142 input: charm.Settings{}, 143 }, { 144 info: "unknown keys are not valid", 145 input: charm.Settings{"foo": nil}, 146 err: `unknown option "foo"`, 147 }, { 148 info: "nil is valid for every value type", 149 input: charm.Settings{ 150 "outlook": nil, 151 "skill-level": nil, 152 "agility-ratio": nil, 153 "reticulate-splines": nil, 154 }, 155 }, { 156 info: "correctly-typed values are valid", 157 input: charm.Settings{ 158 "outlook": "stormy", 159 "skill-level": int64(123), 160 "agility-ratio": 0.5, 161 "reticulate-splines": true, 162 }, 163 }, { 164 info: "empty string-typed values stay empty", 165 input: charm.Settings{"outlook": ""}, 166 expect: charm.Settings{"outlook": ""}, 167 }, { 168 info: "almost-correctly-typed values are valid", 169 input: charm.Settings{ 170 "skill-level": 123, 171 "agility-ratio": float32(0.5), 172 }, 173 expect: charm.Settings{ 174 "skill-level": int64(123), 175 "agility-ratio": 0.5, 176 }, 177 }, { 178 info: "bad string", 179 input: charm.Settings{"outlook": false}, 180 err: `option "outlook" expected string, got false`, 181 }, { 182 info: "bad int", 183 input: charm.Settings{"skill-level": 123.4}, 184 err: `option "skill-level" expected int, got 123.4`, 185 }, { 186 info: "bad float", 187 input: charm.Settings{"agility-ratio": "cheese"}, 188 err: `option "agility-ratio" expected float, got "cheese"`, 189 }, { 190 info: "bad boolean", 191 input: charm.Settings{"reticulate-splines": 101}, 192 err: `option "reticulate-splines" expected boolean, got 101`, 193 }, { 194 info: "invalid secret", 195 input: charm.Settings{"secret-foo": "cheese"}, 196 err: `option "secret-foo" expected secret, got "cheese"`, 197 }, { 198 info: "valid secret", 199 input: charm.Settings{"secret-foo": "secret:cj4v5vm78ohs79o84r4g"}, 200 expect: charm.Settings{"secret-foo": "secret:cj4v5vm78ohs79o84r4g"}, 201 }, 202 } { 203 c.Logf("test %d: %s", i, test.info) 204 result, err := s.config.ValidateSettings(test.input) 205 if test.err != "" { 206 c.Check(err, gc.ErrorMatches, test.err) 207 } else { 208 c.Check(err, gc.IsNil) 209 if test.expect == nil { 210 c.Check(result, jc.DeepEquals, test.input) 211 } else { 212 c.Check(result, jc.DeepEquals, test.expect) 213 } 214 } 215 } 216 } 217 218 var settingsWithNils = charm.Settings{ 219 "outlook": nil, 220 "skill-level": nil, 221 "agility-ratio": nil, 222 "reticulate-splines": nil, 223 } 224 225 var settingsWithValues = charm.Settings{ 226 "outlook": "whatever", 227 "skill-level": int64(123), 228 "agility-ratio": 2.22, 229 "reticulate-splines": true, 230 } 231 232 func (s *ConfigSuite) TestParseSettingsYAML(c *gc.C) { 233 for i, test := range []struct { 234 info string 235 yaml string 236 key string 237 expect charm.Settings 238 err string 239 }{{ 240 info: "bad structure", 241 yaml: "`", 242 err: `cannot parse settings data: .*`, 243 }, { 244 info: "bad key", 245 yaml: "{}", 246 key: "blah", 247 err: `no settings found for "blah"`, 248 }, { 249 info: "bad settings key", 250 yaml: "blah:\n ping: pong", 251 key: "blah", 252 err: `unknown option "ping"`, 253 }, { 254 info: "bad type for string", 255 yaml: "blah:\n outlook: 123", 256 key: "blah", 257 err: `option "outlook" expected string, got 123`, 258 }, { 259 info: "bad type for int", 260 yaml: "blah:\n skill-level: 12.345", 261 key: "blah", 262 err: `option "skill-level" expected int, got 12.345`, 263 }, { 264 info: "bad type for float", 265 yaml: "blah:\n agility-ratio: blob", 266 key: "blah", 267 err: `option "agility-ratio" expected float, got "blob"`, 268 }, { 269 info: "bad type for boolean", 270 yaml: "blah:\n reticulate-splines: 123", 271 key: "blah", 272 err: `option "reticulate-splines" expected boolean, got 123`, 273 }, { 274 info: "bad string for int", 275 yaml: "blah:\n skill-level: cheese", 276 key: "blah", 277 err: `option "skill-level" expected int, got "cheese"`, 278 }, { 279 info: "bad string for float", 280 yaml: "blah:\n agility-ratio: blob", 281 key: "blah", 282 err: `option "agility-ratio" expected float, got "blob"`, 283 }, { 284 info: "bad string for boolean", 285 yaml: "blah:\n reticulate-splines: cannonball", 286 key: "blah", 287 err: `option "reticulate-splines" expected boolean, got "cannonball"`, 288 }, { 289 info: "empty dict is valid", 290 yaml: "blah: {}", 291 key: "blah", 292 expect: charm.Settings{}, 293 }, { 294 info: "nil values are valid", 295 yaml: `blah: 296 outlook: null 297 skill-level: null 298 agility-ratio: null 299 reticulate-splines: null`, 300 key: "blah", 301 expect: settingsWithNils, 302 }, { 303 info: "empty strings for bool options are not accepted", 304 yaml: `blah: 305 outlook: "" 306 skill-level: 123 307 agility-ratio: 12.0 308 reticulate-splines: ""`, 309 key: "blah", 310 err: `option "reticulate-splines" expected boolean, got ""`, 311 }, { 312 info: "empty strings for int options are not accepted", 313 yaml: `blah: 314 outlook: "" 315 skill-level: "" 316 agility-ratio: 12.0 317 reticulate-splines: false`, 318 key: "blah", 319 err: `option "skill-level" expected int, got ""`, 320 }, { 321 info: "empty strings for float options are not accepted", 322 yaml: `blah: 323 outlook: "" 324 skill-level: 123 325 agility-ratio: "" 326 reticulate-splines: false`, 327 key: "blah", 328 err: `option "agility-ratio" expected float, got ""`, 329 }, { 330 info: "appropriate strings are valid", 331 yaml: `blah: 332 outlook: whatever 333 skill-level: "123" 334 agility-ratio: "2.22" 335 reticulate-splines: "true"`, 336 key: "blah", 337 expect: settingsWithValues, 338 }, { 339 info: "appropriate types are valid", 340 yaml: `blah: 341 outlook: whatever 342 skill-level: 123 343 agility-ratio: 2.22 344 reticulate-splines: y`, 345 key: "blah", 346 expect: settingsWithValues, 347 }} { 348 c.Logf("test %d: %s", i, test.info) 349 result, err := s.config.ParseSettingsYAML([]byte(test.yaml), test.key) 350 if test.err != "" { 351 c.Check(err, gc.ErrorMatches, test.err) 352 } else { 353 c.Check(err, gc.IsNil) 354 c.Check(result, jc.DeepEquals, test.expect) 355 } 356 } 357 } 358 359 func (s *ConfigSuite) TestParseSettingsStrings(c *gc.C) { 360 for i, test := range []struct { 361 info string 362 input map[string]string 363 expect charm.Settings 364 err string 365 }{{ 366 info: "nil map is valid", 367 expect: charm.Settings{}, 368 }, { 369 info: "empty map is valid", 370 input: map[string]string{}, 371 expect: charm.Settings{}, 372 }, { 373 info: "empty strings for string options are valid", 374 input: map[string]string{"outlook": ""}, 375 expect: charm.Settings{"outlook": ""}, 376 }, { 377 info: "empty strings for non-string options are invalid", 378 input: map[string]string{"skill-level": ""}, 379 err: `option "skill-level" expected int, got ""`, 380 }, { 381 info: "strings are converted", 382 input: map[string]string{ 383 "outlook": "whatever", 384 "skill-level": "123", 385 "agility-ratio": "2.22", 386 "reticulate-splines": "true", 387 }, 388 expect: settingsWithValues, 389 }, { 390 info: "bad string for int", 391 input: map[string]string{"skill-level": "cheese"}, 392 err: `option "skill-level" expected int, got "cheese"`, 393 }, { 394 info: "bad string for float", 395 input: map[string]string{"agility-ratio": "blob"}, 396 err: `option "agility-ratio" expected float, got "blob"`, 397 }, { 398 info: "bad string for boolean", 399 input: map[string]string{"reticulate-splines": "cannonball"}, 400 err: `option "reticulate-splines" expected boolean, got "cannonball"`, 401 }} { 402 c.Logf("test %d: %s", i, test.info) 403 result, err := s.config.ParseSettingsStrings(test.input) 404 if test.err != "" { 405 c.Check(err, gc.ErrorMatches, test.err) 406 } else { 407 c.Check(err, gc.IsNil) 408 c.Check(result, jc.DeepEquals, test.expect) 409 } 410 } 411 } 412 413 func (s *ConfigSuite) TestConfigError(c *gc.C) { 414 _, err := charm.ReadConfig(bytes.NewBuffer([]byte(`options: {t: {type: foo}}`))) 415 c.Assert(err, gc.ErrorMatches, `invalid config: option "t" has unknown type "foo"`) 416 } 417 418 func (s *ConfigSuite) TestConfigWithNoOptions(c *gc.C) { 419 _, err := charm.ReadConfig(strings.NewReader("other:\n")) 420 c.Assert(err, gc.ErrorMatches, "invalid config: empty configuration") 421 422 _, err = charm.ReadConfig(strings.NewReader("\n")) 423 c.Assert(err, gc.ErrorMatches, "invalid config: empty configuration") 424 425 _, err = charm.ReadConfig(strings.NewReader("null\n")) 426 c.Assert(err, gc.ErrorMatches, "invalid config: empty configuration") 427 428 _, err = charm.ReadConfig(strings.NewReader("options:\n")) 429 c.Assert(err, gc.IsNil) 430 } 431 432 func (s *ConfigSuite) TestDefaultType(c *gc.C) { 433 assertDefault := func(type_ string, value string, expected interface{}) { 434 config := fmt.Sprintf(`options: {x: {type: %s, default: %s}}`, type_, value) 435 result, err := charm.ReadConfig(bytes.NewBuffer([]byte(config))) 436 c.Assert(err, gc.IsNil) 437 c.Assert(result.Options["x"].Default, gc.Equals, expected) 438 } 439 440 assertDefault("boolean", "true", true) 441 assertDefault("string", "golden grahams", "golden grahams") 442 assertDefault("string", `""`, "") 443 assertDefault("float", "2.211", 2.211) 444 assertDefault("int", "99", int64(99)) 445 446 assertTypeError := func(type_, str, value string) { 447 config := fmt.Sprintf(`options: {t: {type: %s, default: %s}}`, type_, str) 448 _, err := charm.ReadConfig(bytes.NewBuffer([]byte(config))) 449 expected := fmt.Sprintf(`invalid config default: option "t" expected %s, got %s`, type_, value) 450 c.Assert(err, gc.ErrorMatches, expected) 451 } 452 453 assertTypeError("boolean", "henry", `"henry"`) 454 assertTypeError("string", "2.5", "2.5") 455 assertTypeError("float", "123a", `"123a"`) 456 assertTypeError("int", "true", "true") 457 } 458 459 // When an empty config is supplied an error should be returned 460 func (s *ConfigSuite) TestEmptyConfigReturnsError(c *gc.C) { 461 config := "" 462 result, err := charm.ReadConfig(bytes.NewBuffer([]byte(config))) 463 c.Assert(result, gc.IsNil) 464 c.Assert(err, gc.ErrorMatches, "invalid config: empty configuration") 465 } 466 467 func (s *ConfigSuite) TestYAMLMarshal(c *gc.C) { 468 cfg, err := charm.ReadConfig(strings.NewReader(` 469 options: 470 minimal: 471 type: string 472 withdescription: 473 type: int 474 description: d 475 withdefault: 476 type: boolean 477 description: d 478 default: true 479 `)) 480 c.Assert(err, gc.IsNil) 481 c.Assert(cfg.Options, gc.HasLen, 3) 482 483 newYAML, err := yaml.Marshal(cfg) 484 c.Assert(err, gc.IsNil) 485 486 newCfg, err := charm.ReadConfig(bytes.NewReader(newYAML)) 487 c.Assert(err, gc.IsNil) 488 c.Assert(newCfg, jc.DeepEquals, cfg) 489 } 490 491 func (s *ConfigSuite) TestErrorOnInvalidOptionTypes(c *gc.C) { 492 cfg := charm.Config{ 493 Options: map[string]charm.Option{"testOption": {Type: "invalid type"}}, 494 } 495 _, err := cfg.ParseSettingsYAML([]byte("testKey:\n testOption: 12.345"), "testKey") 496 c.Assert(err, gc.ErrorMatches, "option \"testOption\" has unknown type \"invalid type\"") 497 498 _, err = cfg.ParseSettingsYAML([]byte("testKey:\n testOption: \"some string value\""), "testKey") 499 c.Assert(err, gc.ErrorMatches, "option \"testOption\" has unknown type \"invalid type\"") 500 }