1
// Copyright 2011-2015 Canonical Ltd.
2
// Licensed under the LGPLv3, see LICENCE file for details.
10
jc "github.com/juju/testing/checkers"
11
gc "gopkg.in/check.v1"
14
type ActionsSuite struct{}
16
var _ = gc.Suite(&ActionsSuite{})
18
func (s *ActionsSuite) TestNewActions(c *gc.C) {
19
emptyAction := NewActions()
20
c.Assert(emptyAction, jc.DeepEquals, &Actions{})
23
func (s *ActionsSuite) TestValidateOk(c *gc.C) {
24
for i, test := range []struct {
26
actionSpec *ActionSpec
27
objectToValidate map[string]interface{}
29
description: "Validation of an empty object is ok.",
30
actionSpec: &ActionSpec{
31
Description: "Take a snapshot of the database.",
32
Params: map[string]interface{}{
34
"description": "Take a snapshot of the database.",
36
"properties": map[string]interface{}{
37
"outfile": map[string]interface{}{
38
"description": "The file to write out to.",
40
objectToValidate: nil,
42
description: "Validation of one required value.",
43
actionSpec: &ActionSpec{
44
Description: "Take a snapshot of the database.",
45
Params: map[string]interface{}{
47
"description": "Take a snapshot of the database.",
49
"properties": map[string]interface{}{
50
"outfile": map[string]interface{}{
51
"description": "The file to write out to.",
53
"required": []interface{}{"outfile"}}},
54
objectToValidate: map[string]interface{}{
55
"outfile": "out-2014-06-12.bz2",
58
description: "Validation of one required and one optional value.",
59
actionSpec: &ActionSpec{
60
Description: "Take a snapshot of the database.",
61
Params: map[string]interface{}{
63
"description": "Take a snapshot of the database.",
65
"properties": map[string]interface{}{
66
"outfile": map[string]interface{}{
67
"description": "The file to write out to.",
69
"quality": map[string]interface{}{
70
"description": "Compression quality",
74
"required": []interface{}{"outfile"}}},
75
objectToValidate: map[string]interface{}{
76
"outfile": "out-2014-06-12.bz2",
79
description: "Validation of an optional, range limited value.",
80
actionSpec: &ActionSpec{
81
Description: "Take a snapshot of the database.",
82
Params: map[string]interface{}{
84
"description": "Take a snapshot of the database.",
86
"properties": map[string]interface{}{
87
"outfile": map[string]interface{}{
88
"description": "The file to write out to.",
90
"quality": map[string]interface{}{
91
"description": "Compression quality",
95
"required": []interface{}{"outfile"}}},
96
objectToValidate: map[string]interface{}{
97
"outfile": "out-2014-06-12.bz2",
101
c.Logf("test %d: %s", i, test.description)
102
err := test.actionSpec.ValidateParams(test.objectToValidate)
103
c.Assert(err, jc.ErrorIsNil)
107
func (s *ActionsSuite) TestValidateFail(c *gc.C) {
108
var validActionTests = []struct {
110
actionSpec *ActionSpec
114
description: "Validation of one required value.",
115
actionSpec: &ActionSpec{
116
Description: "Take a snapshot of the database.",
117
Params: map[string]interface{}{
119
"description": "Take a snapshot of the database.",
121
"properties": map[string]interface{}{
122
"outfile": map[string]interface{}{
123
"description": "The file to write out to.",
125
"required": []interface{}{"outfile"}}},
126
badActionJson: `{"outfile": 5}`,
127
expectedError: "validation failed: (root).outfile : must be of type string, given 5",
129
description: "Restrict to only one property",
130
actionSpec: &ActionSpec{
131
Description: "Take a snapshot of the database.",
132
Params: map[string]interface{}{
134
"description": "Take a snapshot of the database.",
136
"properties": map[string]interface{}{
137
"outfile": map[string]interface{}{
138
"description": "The file to write out to.",
140
"required": []interface{}{"outfile"},
141
"additionalProperties": false}},
142
badActionJson: `{"outfile": "foo.bz", "bar": "foo"}`,
143
expectedError: "validation failed: (root) : additional property \"bar\" is not allowed, given {\"bar\":\"foo\",\"outfile\":\"foo.bz\"}",
145
description: "Validation of one required and one optional value.",
146
actionSpec: &ActionSpec{
147
Description: "Take a snapshot of the database.",
148
Params: map[string]interface{}{
150
"description": "Take a snapshot of the database.",
152
"properties": map[string]interface{}{
153
"outfile": map[string]interface{}{
154
"description": "The file to write out to.",
156
"quality": map[string]interface{}{
157
"description": "Compression quality",
161
"required": []interface{}{"outfile"}}},
162
badActionJson: `{"quality": 5}`,
163
expectedError: "validation failed: (root) : \"outfile\" property is missing and required, given {\"quality\":5}",
165
description: "Validation of an optional, range limited value.",
166
actionSpec: &ActionSpec{
167
Description: "Take a snapshot of the database.",
168
Params: map[string]interface{}{
170
"description": "Take a snapshot of the database.",
172
"properties": map[string]interface{}{
173
"outfile": map[string]interface{}{
174
"description": "The file to write out to.",
176
"quality": map[string]interface{}{
177
"description": "Compression quality",
181
"required": []interface{}{"outfile"}}},
183
{ "outfile": "out-2014-06-12.bz2", "quality": "two" }`,
184
expectedError: "validation failed: (root).quality : must be of type integer, given \"two\"",
187
for i, test := range validActionTests {
188
c.Logf("test %d: %s", i, test.description)
189
var params map[string]interface{}
190
jsonBytes := []byte(test.badActionJson)
191
err := json.Unmarshal(jsonBytes, ¶ms)
192
c.Assert(err, gc.IsNil)
193
err = test.actionSpec.ValidateParams(params)
194
c.Assert(err.Error(), gc.Equals, test.expectedError)
198
func (s *ActionsSuite) TestCleanseOk(c *gc.C) {
200
var goodInterfaceTests = []struct {
202
acceptableInterface map[string]interface{}
203
expectedInterface map[string]interface{}
205
description: "An interface requiring no changes.",
206
acceptableInterface: map[string]interface{}{
209
"key3": map[string]interface{}{
212
expectedInterface: map[string]interface{}{
215
"key3": map[string]interface{}{
219
description: "Substitute a single inner map[i]i.",
220
acceptableInterface: map[string]interface{}{
223
"key3": map[interface{}]interface{}{
226
expectedInterface: map[string]interface{}{
229
"key3": map[string]interface{}{
233
description: "Substitute nested inner map[i]i.",
234
acceptableInterface: map[string]interface{}{
237
"key3a": map[interface{}]interface{}{
239
"key2b": map[interface{}]interface{}{
241
expectedInterface: map[string]interface{}{
244
"key3a": map[string]interface{}{
246
"key2b": map[string]interface{}{
249
description: "Substitute nested map[i]i within []i.",
250
acceptableInterface: map[string]interface{}{
252
"key2a": []interface{}{5, "foo", map[string]interface{}{
254
"key2b": map[interface{}]interface{}{
255
"key1c": "val1c"}}}},
256
expectedInterface: map[string]interface{}{
258
"key2a": []interface{}{5, "foo", map[string]interface{}{
260
"key2b": map[string]interface{}{
261
"key1c": "val1c"}}}},
264
for i, test := range goodInterfaceTests {
265
c.Logf("test %d: %s", i, test.description)
266
cleansedInterfaceMap, err := cleanse(test.acceptableInterface)
267
c.Assert(err, gc.IsNil)
268
c.Assert(cleansedInterfaceMap, jc.DeepEquals, test.expectedInterface)
272
func (s *ActionsSuite) TestCleanseFail(c *gc.C) {
274
var badInterfaceTests = []struct {
276
failInterface map[string]interface{}
279
description: "An inner map[interface{}]interface{} with an int key.",
280
failInterface: map[string]interface{}{
283
"key3": map[interface{}]interface{}{
286
expectedError: "map keyed with non-string value",
288
description: "An inner []interface{} containing a map[i]i with an int key.",
289
failInterface: map[string]interface{}{
292
"key3a": []interface{}{"foo1", 5, map[interface{}]interface{}{
294
"key2b": map[interface{}]interface{}{
297
expectedError: "map keyed with non-string value",
300
for i, test := range badInterfaceTests {
301
c.Logf("test %d: %s", i, test.description)
302
_, err := cleanse(test.failInterface)
303
c.Assert(err, gc.NotNil)
304
c.Assert(err.Error(), gc.Equals, test.expectedError)
308
func (s *ActionsSuite) TestReadGoodActionsYaml(c *gc.C) {
309
var goodActionsYamlTests = []struct {
312
expectedActions *Actions
314
description: "A simple snapshot actions YAML with one parameter.",
317
description: Take a snapshot of the database.
320
description: "The file to write out to."
322
required: ["outfile"]
324
expectedActions: &Actions{map[string]ActionSpec{
326
Description: "Take a snapshot of the database.",
327
Params: map[string]interface{}{
329
"description": "Take a snapshot of the database.",
331
"properties": map[string]interface{}{
332
"outfile": map[string]interface{}{
333
"description": "The file to write out to.",
335
"required": []interface{}{"outfile"}}}}},
337
description: "An empty Actions definition.",
339
expectedActions: &Actions{
340
ActionSpecs: map[string]ActionSpec{},
343
description: "A more complex schema with hyphenated names and multiple parameters.",
346
description: "Take a snapshot of the database."
349
description: "The file to write out to."
352
description: "The compression quality."
356
exclusiveMaximum: false
358
description: "Sync a file to a remote host."
361
description: "The file to send out."
365
description: "The host to sync to."
369
description: "The util to perform the sync (rsync or scp.)"
371
enum: ["rsync", "scp"]
372
required: ["file", "remote-uri"]
374
expectedActions: &Actions{map[string]ActionSpec{
376
Description: "Take a snapshot of the database.",
377
Params: map[string]interface{}{
379
"description": "Take a snapshot of the database.",
381
"properties": map[string]interface{}{
382
"outfile": map[string]interface{}{
383
"description": "The file to write out to.",
385
"compression-quality": map[string]interface{}{
386
"description": "The compression quality.",
390
"exclusiveMaximum": false}}}},
392
Description: "Sync a file to a remote host.",
393
Params: map[string]interface{}{
394
"title": "remote-sync",
395
"description": "Sync a file to a remote host.",
397
"properties": map[string]interface{}{
398
"file": map[string]interface{}{
399
"description": "The file to send out.",
402
"remote-uri": map[string]interface{}{
403
"description": "The host to sync to.",
406
"util": map[string]interface{}{
407
"description": "The util to perform the sync (rsync or scp.)",
409
"enum": []interface{}{"rsync", "scp"}}},
410
"required": []interface{}{"file", "remote-uri"}}}}},
412
description: "A schema with other keys, e.g. \"definitions\"",
415
description: "Take a snapshot of the database."
418
description: "The file to write out to."
421
description: "The compression quality."
425
exclusiveMaximum: false
430
expectedActions: &Actions{map[string]ActionSpec{
432
Description: "Take a snapshot of the database.",
433
Params: map[string]interface{}{
435
"description": "Take a snapshot of the database.",
437
"properties": map[string]interface{}{
438
"outfile": map[string]interface{}{
439
"description": "The file to write out to.",
442
"compression-quality": map[string]interface{}{
443
"description": "The compression quality.",
447
"exclusiveMaximum": false,
450
"definitions": map[string]interface{}{
451
"diskdevice": map[string]interface{}{},
452
"something-else": map[string]interface{}{},
458
description: "A schema with no \"params\" key, implying no options.",
461
description: Take a snapshot of the database.
464
expectedActions: &Actions{map[string]ActionSpec{
466
Description: "Take a snapshot of the database.",
467
Params: map[string]interface{}{
468
"description": "Take a snapshot of the database.",
471
"properties": map[string]interface{}{},
474
description: "A schema with no values at all, implying no options.",
479
expectedActions: &Actions{map[string]ActionSpec{
481
Description: "No description",
482
Params: map[string]interface{}{
483
"description": "No description",
486
"properties": map[string]interface{}{},
490
// Beginning of testing loop
491
for i, test := range goodActionsYamlTests {
492
c.Logf("test %d: %s", i, test.description)
493
reader := bytes.NewReader([]byte(test.yaml))
494
loadedAction, err := ReadActionsYaml(reader)
495
c.Assert(err, gc.IsNil)
496
c.Check(loadedAction, jc.DeepEquals, test.expectedActions)
500
func (s *ActionsSuite) TestReadBadActionsYaml(c *gc.C) {
502
var badActionsYamlTests = []struct {
507
description: "Reject JSON-Schema containing references.",
510
description: Take a snapshot of the database.
512
$schema: "http://json-schema.org/draft-03/schema#"
514
expectedError: "schema key \"$schema\" not compatible with this version of juju",
516
description: "Reject JSON-Schema containing references.",
519
description: Take a snapshot of the database.
521
outfile: { $ref: "http://json-schema.org/draft-03/schema#" }
523
expectedError: "schema key \"$ref\" not compatible with this version of juju",
525
description: "Malformed YAML: missing key in \"outfile\".",
528
description: Take a snapshot of the database.
531
The file to write out to.
536
expectedError: "YAML error: line 6: mapping values are not allowed in this context",
538
description: "Malformed JSON-Schema: $schema element misplaced.",
541
description: Take a snapshot of the database.
544
$schema: http://json-schema.org/draft-03/schema#
545
description: The file to write out to.
550
expectedError: "YAML error: line 3: mapping values are not allowed in this context",
552
description: "Malformed Actions: hyphen at beginning of action name.",
555
description: Take a snapshot of the database.
558
expectedError: "bad action name -snapshot",
560
description: "Malformed Actions: hyphen after action name.",
563
description: Take a snapshot of the database.
566
expectedError: "bad action name snapshot-",
568
description: "Malformed Actions: caps in action name.",
571
description: Take a snapshot of the database.
574
expectedError: "bad action name Snapshot",
576
description: "A non-string description fails to parse",
579
description: ["Take a snapshot of the database."]
581
expectedError: "value for schema key \"description\" must be a string",
583
description: "A non-list \"required\" key",
586
description: Take a snapshot of the database.
589
description: "The file to write out to."
593
expectedError: "value for schema key \"required\" must be a YAML list",
595
description: "A schema with an empty \"params\" key fails to parse",
598
description: Take a snapshot of the database.
601
expectedError: "params failed to parse as a map",
603
description: "A schema with a non-map \"params\" value fails to parse",
606
description: Take a snapshot of the database.
609
expectedError: "params failed to parse as a map",
611
description: "\"definitions\" goes against JSON-Schema definition",
614
description: "Take a snapshot of the database."
617
description: "The file to write out to."
621
something-else: {"a": "b"}
623
expectedError: "invalid params schema for action schema snapshot: definitions must be of type array of schemas",
625
description: "excess keys not in the JSON-Schema spec will be rejected",
628
description: "Take a snapshot of the database."
631
description: "The file to write out to."
634
description: "The compression quality."
638
exclusiveMaximum: false
642
other-key: ["some", "values"],
644
expectedError: "YAML error: line 16: did not find expected key",
647
for i, test := range badActionsYamlTests {
648
c.Logf("test %d: %s", i, test.description)
649
reader := bytes.NewReader([]byte(test.yaml))
650
_, err := ReadActionsYaml(reader)
651
c.Assert(err, gc.NotNil)
652
c.Check(err.Error(), gc.Equals, test.expectedError)
656
func (s *ActionsSuite) TestRecurseMapOnKeys(c *gc.C) {
660
givenMap map[string]interface{}
664
should: "fail if the specified key was not in the map",
665
givenKeys: []string{"key", "key2"},
666
givenMap: map[string]interface{}{
667
"key": map[string]interface{}{
673
should: "fail if a key was not a string",
674
givenKeys: []string{"key", "key2"},
675
givenMap: map[string]interface{}{
676
"key": map[interface{}]interface{}{
682
should: "fail if we have more keys but not a recursable val",
683
givenKeys: []string{"key", "key2"},
684
givenMap: map[string]interface{}{
685
"key": []string{"a", "b", "c"},
689
should: "retrieve a good value",
690
givenKeys: []string{"key", "key2"},
691
givenMap: map[string]interface{}{
692
"key": map[string]interface{}{
698
should: "retrieve a map",
699
givenKeys: []string{"key"},
700
givenMap: map[string]interface{}{
701
"key": map[string]interface{}{
705
expected: map[string]interface{}{
709
should: "retrieve a slice",
710
givenKeys: []string{"key"},
711
givenMap: map[string]interface{}{
712
"key": []string{"a", "b", "c"},
714
expected: []string{"a", "b", "c"},
717
for i, t := range tests {
718
c.Logf("test %d: should %s\n map: %#v\n keys: %#v", i, t.should, t.givenMap, t.givenKeys)
719
obtained, failed := recurseMapOnKeys(t.givenKeys, t.givenMap)
720
c.Assert(!failed, gc.Equals, t.shouldFail)
722
c.Check(obtained, jc.DeepEquals, t.expected)
727
func (s *ActionsSuite) TestInsertDefaultValues(c *gc.C) {
728
schemas := map[string]string{
774
for i, t := range []struct {
777
withParams map[string]interface{}
778
expectedResult map[string]interface{}
781
should: "error with no schema",
782
expectedError: "schema must be of type object",
784
should: "create a map if handed nil",
785
schema: schemas["none"],
787
expectedResult: map[string]interface{}{},
789
should: "create and fill target if handed nil",
790
schema: schemas["simple"],
792
expectedResult: map[string]interface{}{"val": "somestr"},
794
should: "create a simple default value",
795
schema: schemas["simple"],
796
withParams: map[string]interface{}{},
797
expectedResult: map[string]interface{}{"val": "somestr"},
799
should: "do nothing for no default value",
800
schema: schemas["none"],
801
withParams: map[string]interface{}{},
802
expectedResult: map[string]interface{}{},
804
should: "insert a default value within a nested map",
805
schema: schemas["complicated"],
806
withParams: map[string]interface{}{},
807
expectedResult: map[string]interface{}{
808
"val": map[string]interface{}{
809
"bar": map[string]interface{}{
813
should: "create a default value which is an object",
814
schema: schemas["default-object"],
815
withParams: map[string]interface{}{},
816
expectedResult: map[string]interface{}{
817
"val": map[string]interface{}{
819
"bar": map[string]interface{}{
823
should: "not overwrite existing values with default objects",
824
schema: schemas["default-object"],
825
withParams: map[string]interface{}{"val": 5},
826
expectedResult: map[string]interface{}{"val": 5},
828
should: "interleave defaults into existing objects",
829
schema: schemas["complicated"],
830
withParams: map[string]interface{}{
831
"val": map[string]interface{}{
833
"bar": map[string]interface{}{
836
expectedResult: map[string]interface{}{
837
"val": map[string]interface{}{
839
"bar": map[string]interface{}{
844
c.Logf("test %d: should %s", i, t.should)
845
schema := getSchemaForAction(c, t.schema)
846
// Testing this method
847
result, err := schema.InsertDefaults(t.withParams)
848
if t.expectedError != "" {
849
c.Check(err, gc.ErrorMatches, t.expectedError)
852
c.Assert(err, jc.ErrorIsNil)
853
c.Check(result, jc.DeepEquals, t.expectedResult)
857
func getSchemaForAction(c *gc.C, wholeSchema string) ActionSpec {
858
// Load up the YAML schema definition.
859
reader := bytes.NewReader([]byte(wholeSchema))
860
loadedActions, err := ReadActionsYaml(reader)
861
c.Assert(err, gc.IsNil)
862
// Same action name for all tests, "act".
863
return loadedActions.ActionSpecs["act"]