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  }