~nskaggs/+junk/juju-packaging-test

« back to all changes in this revision

Viewing changes to src/gopkg.in/juju/charm.v6-unstable/actions_test.go

  • Committer: Nicholas Skaggs
  • Date: 2016-10-27 20:23:11 UTC
  • Revision ID: nicholas.skaggs@canonical.com-20161027202311-sux4jk2o73p1d6rg
Re-add src

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
// Copyright 2011-2015 Canonical Ltd.
 
2
// Licensed under the LGPLv3, see LICENCE file for details.
 
3
 
 
4
package charm
 
5
 
 
6
import (
 
7
        "bytes"
 
8
        "encoding/json"
 
9
 
 
10
        jc "github.com/juju/testing/checkers"
 
11
        gc "gopkg.in/check.v1"
 
12
)
 
13
 
 
14
type ActionsSuite struct{}
 
15
 
 
16
var _ = gc.Suite(&ActionsSuite{})
 
17
 
 
18
func (s *ActionsSuite) TestNewActions(c *gc.C) {
 
19
        emptyAction := NewActions()
 
20
        c.Assert(emptyAction, jc.DeepEquals, &Actions{})
 
21
}
 
22
 
 
23
func (s *ActionsSuite) TestValidateOk(c *gc.C) {
 
24
        for i, test := range []struct {
 
25
                description      string
 
26
                actionSpec       *ActionSpec
 
27
                objectToValidate map[string]interface{}
 
28
        }{{
 
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{}{
 
33
                                "title":       "snapshot",
 
34
                                "description": "Take a snapshot of the database.",
 
35
                                "type":        "object",
 
36
                                "properties": map[string]interface{}{
 
37
                                        "outfile": map[string]interface{}{
 
38
                                                "description": "The file to write out to.",
 
39
                                                "type":        "string"}}}},
 
40
                objectToValidate: nil,
 
41
        }, {
 
42
                description: "Validation of one required value.",
 
43
                actionSpec: &ActionSpec{
 
44
                        Description: "Take a snapshot of the database.",
 
45
                        Params: map[string]interface{}{
 
46
                                "title":       "snapshot",
 
47
                                "description": "Take a snapshot of the database.",
 
48
                                "type":        "object",
 
49
                                "properties": map[string]interface{}{
 
50
                                        "outfile": map[string]interface{}{
 
51
                                                "description": "The file to write out to.",
 
52
                                                "type":        "string"}},
 
53
                                "required": []interface{}{"outfile"}}},
 
54
                objectToValidate: map[string]interface{}{
 
55
                        "outfile": "out-2014-06-12.bz2",
 
56
                },
 
57
        }, {
 
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{}{
 
62
                                "title":       "snapshot",
 
63
                                "description": "Take a snapshot of the database.",
 
64
                                "type":        "object",
 
65
                                "properties": map[string]interface{}{
 
66
                                        "outfile": map[string]interface{}{
 
67
                                                "description": "The file to write out to.",
 
68
                                                "type":        "string"},
 
69
                                        "quality": map[string]interface{}{
 
70
                                                "description": "Compression quality",
 
71
                                                "type":        "integer",
 
72
                                                "minimum":     0,
 
73
                                                "maximum":     9}},
 
74
                                "required": []interface{}{"outfile"}}},
 
75
                objectToValidate: map[string]interface{}{
 
76
                        "outfile": "out-2014-06-12.bz2",
 
77
                },
 
78
        }, {
 
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{}{
 
83
                                "title":       "snapshot",
 
84
                                "description": "Take a snapshot of the database.",
 
85
                                "type":        "object",
 
86
                                "properties": map[string]interface{}{
 
87
                                        "outfile": map[string]interface{}{
 
88
                                                "description": "The file to write out to.",
 
89
                                                "type":        "string"},
 
90
                                        "quality": map[string]interface{}{
 
91
                                                "description": "Compression quality",
 
92
                                                "type":        "integer",
 
93
                                                "minimum":     0,
 
94
                                                "maximum":     9}},
 
95
                                "required": []interface{}{"outfile"}}},
 
96
                objectToValidate: map[string]interface{}{
 
97
                        "outfile": "out-2014-06-12.bz2",
 
98
                        "quality": 5,
 
99
                },
 
100
        }} {
 
101
                c.Logf("test %d: %s", i, test.description)
 
102
                err := test.actionSpec.ValidateParams(test.objectToValidate)
 
103
                c.Assert(err, jc.ErrorIsNil)
 
104
        }
 
105
}
 
106
 
 
107
func (s *ActionsSuite) TestValidateFail(c *gc.C) {
 
108
        var validActionTests = []struct {
 
109
                description   string
 
110
                actionSpec    *ActionSpec
 
111
                badActionJson string
 
112
                expectedError string
 
113
        }{{
 
114
                description: "Validation of one required value.",
 
115
                actionSpec: &ActionSpec{
 
116
                        Description: "Take a snapshot of the database.",
 
117
                        Params: map[string]interface{}{
 
118
                                "title":       "snapshot",
 
119
                                "description": "Take a snapshot of the database.",
 
120
                                "type":        "object",
 
121
                                "properties": map[string]interface{}{
 
122
                                        "outfile": map[string]interface{}{
 
123
                                                "description": "The file to write out to.",
 
124
                                                "type":        "string"}},
 
125
                                "required": []interface{}{"outfile"}}},
 
126
                badActionJson: `{"outfile": 5}`,
 
127
                expectedError: "validation failed: (root).outfile : must be of type string, given 5",
 
128
        }, {
 
129
                description: "Restrict to only one property",
 
130
                actionSpec: &ActionSpec{
 
131
                        Description: "Take a snapshot of the database.",
 
132
                        Params: map[string]interface{}{
 
133
                                "title":       "snapshot",
 
134
                                "description": "Take a snapshot of the database.",
 
135
                                "type":        "object",
 
136
                                "properties": map[string]interface{}{
 
137
                                        "outfile": map[string]interface{}{
 
138
                                                "description": "The file to write out to.",
 
139
                                                "type":        "string"}},
 
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\"}",
 
144
        }, {
 
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{}{
 
149
                                "title":       "snapshot",
 
150
                                "description": "Take a snapshot of the database.",
 
151
                                "type":        "object",
 
152
                                "properties": map[string]interface{}{
 
153
                                        "outfile": map[string]interface{}{
 
154
                                                "description": "The file to write out to.",
 
155
                                                "type":        "string"},
 
156
                                        "quality": map[string]interface{}{
 
157
                                                "description": "Compression quality",
 
158
                                                "type":        "integer",
 
159
                                                "minimum":     0,
 
160
                                                "maximum":     9}},
 
161
                                "required": []interface{}{"outfile"}}},
 
162
                badActionJson: `{"quality": 5}`,
 
163
                expectedError: "validation failed: (root) : \"outfile\" property is missing and required, given {\"quality\":5}",
 
164
        }, {
 
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{}{
 
169
                                "title":       "snapshot",
 
170
                                "description": "Take a snapshot of the database.",
 
171
                                "type":        "object",
 
172
                                "properties": map[string]interface{}{
 
173
                                        "outfile": map[string]interface{}{
 
174
                                                "description": "The file to write out to.",
 
175
                                                "type":        "string"},
 
176
                                        "quality": map[string]interface{}{
 
177
                                                "description": "Compression quality",
 
178
                                                "type":        "integer",
 
179
                                                "minimum":     0,
 
180
                                                "maximum":     9}},
 
181
                                "required": []interface{}{"outfile"}}},
 
182
                badActionJson: `
 
183
{ "outfile": "out-2014-06-12.bz2", "quality": "two" }`,
 
184
                expectedError: "validation failed: (root).quality : must be of type integer, given \"two\"",
 
185
        }}
 
186
 
 
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, &params)
 
192
                c.Assert(err, gc.IsNil)
 
193
                err = test.actionSpec.ValidateParams(params)
 
194
                c.Assert(err.Error(), gc.Equals, test.expectedError)
 
195
        }
 
196
}
 
197
 
 
198
func (s *ActionsSuite) TestCleanseOk(c *gc.C) {
 
199
 
 
200
        var goodInterfaceTests = []struct {
 
201
                description         string
 
202
                acceptableInterface map[string]interface{}
 
203
                expectedInterface   map[string]interface{}
 
204
        }{{
 
205
                description: "An interface requiring no changes.",
 
206
                acceptableInterface: map[string]interface{}{
 
207
                        "key1": "value1",
 
208
                        "key2": "value2",
 
209
                        "key3": map[string]interface{}{
 
210
                                "foo1": "val1",
 
211
                                "foo2": "val2"}},
 
212
                expectedInterface: map[string]interface{}{
 
213
                        "key1": "value1",
 
214
                        "key2": "value2",
 
215
                        "key3": map[string]interface{}{
 
216
                                "foo1": "val1",
 
217
                                "foo2": "val2"}},
 
218
        }, {
 
219
                description: "Substitute a single inner map[i]i.",
 
220
                acceptableInterface: map[string]interface{}{
 
221
                        "key1": "value1",
 
222
                        "key2": "value2",
 
223
                        "key3": map[interface{}]interface{}{
 
224
                                "foo1": "val1",
 
225
                                "foo2": "val2"}},
 
226
                expectedInterface: map[string]interface{}{
 
227
                        "key1": "value1",
 
228
                        "key2": "value2",
 
229
                        "key3": map[string]interface{}{
 
230
                                "foo1": "val1",
 
231
                                "foo2": "val2"}},
 
232
        }, {
 
233
                description: "Substitute nested inner map[i]i.",
 
234
                acceptableInterface: map[string]interface{}{
 
235
                        "key1a": "val1a",
 
236
                        "key2a": "val2a",
 
237
                        "key3a": map[interface{}]interface{}{
 
238
                                "key1b": "val1b",
 
239
                                "key2b": map[interface{}]interface{}{
 
240
                                        "key1c": "val1c"}}},
 
241
                expectedInterface: map[string]interface{}{
 
242
                        "key1a": "val1a",
 
243
                        "key2a": "val2a",
 
244
                        "key3a": map[string]interface{}{
 
245
                                "key1b": "val1b",
 
246
                                "key2b": map[string]interface{}{
 
247
                                        "key1c": "val1c"}}},
 
248
        }, {
 
249
                description: "Substitute nested map[i]i within []i.",
 
250
                acceptableInterface: map[string]interface{}{
 
251
                        "key1a": "val1a",
 
252
                        "key2a": []interface{}{5, "foo", map[string]interface{}{
 
253
                                "key1b": "val1b",
 
254
                                "key2b": map[interface{}]interface{}{
 
255
                                        "key1c": "val1c"}}}},
 
256
                expectedInterface: map[string]interface{}{
 
257
                        "key1a": "val1a",
 
258
                        "key2a": []interface{}{5, "foo", map[string]interface{}{
 
259
                                "key1b": "val1b",
 
260
                                "key2b": map[string]interface{}{
 
261
                                        "key1c": "val1c"}}}},
 
262
        }}
 
263
 
 
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)
 
269
        }
 
270
}
 
271
 
 
272
func (s *ActionsSuite) TestCleanseFail(c *gc.C) {
 
273
 
 
274
        var badInterfaceTests = []struct {
 
275
                description   string
 
276
                failInterface map[string]interface{}
 
277
                expectedError string
 
278
        }{{
 
279
                description: "An inner map[interface{}]interface{} with an int key.",
 
280
                failInterface: map[string]interface{}{
 
281
                        "key1": "value1",
 
282
                        "key2": "value2",
 
283
                        "key3": map[interface{}]interface{}{
 
284
                                "foo1": "val1",
 
285
                                5:      "val2"}},
 
286
                expectedError: "map keyed with non-string value",
 
287
        }, {
 
288
                description: "An inner []interface{} containing a map[i]i with an int key.",
 
289
                failInterface: map[string]interface{}{
 
290
                        "key1a": "val1b",
 
291
                        "key2a": "val2b",
 
292
                        "key3a": []interface{}{"foo1", 5, map[interface{}]interface{}{
 
293
                                "key1b": "val1b",
 
294
                                "key2b": map[interface{}]interface{}{
 
295
                                        "key1c": "val1c",
 
296
                                        5:       "val2c"}}}},
 
297
                expectedError: "map keyed with non-string value",
 
298
        }}
 
299
 
 
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)
 
305
        }
 
306
}
 
307
 
 
308
func (s *ActionsSuite) TestReadGoodActionsYaml(c *gc.C) {
 
309
        var goodActionsYamlTests = []struct {
 
310
                description     string
 
311
                yaml            string
 
312
                expectedActions *Actions
 
313
        }{{
 
314
                description: "A simple snapshot actions YAML with one parameter.",
 
315
                yaml: `
 
316
snapshot:
 
317
   description: Take a snapshot of the database.
 
318
   params:
 
319
      outfile:
 
320
         description: "The file to write out to."
 
321
         type: string
 
322
   required: ["outfile"]
 
323
`,
 
324
                expectedActions: &Actions{map[string]ActionSpec{
 
325
                        "snapshot": {
 
326
                                Description: "Take a snapshot of the database.",
 
327
                                Params: map[string]interface{}{
 
328
                                        "title":       "snapshot",
 
329
                                        "description": "Take a snapshot of the database.",
 
330
                                        "type":        "object",
 
331
                                        "properties": map[string]interface{}{
 
332
                                                "outfile": map[string]interface{}{
 
333
                                                        "description": "The file to write out to.",
 
334
                                                        "type":        "string"}},
 
335
                                        "required": []interface{}{"outfile"}}}}},
 
336
        }, {
 
337
                description: "An empty Actions definition.",
 
338
                yaml:        "",
 
339
                expectedActions: &Actions{
 
340
                        ActionSpecs: map[string]ActionSpec{},
 
341
                },
 
342
        }, {
 
343
                description: "A more complex schema with hyphenated names and multiple parameters.",
 
344
                yaml: `
 
345
snapshot:
 
346
   description: "Take a snapshot of the database."
 
347
   params:
 
348
      outfile:
 
349
         description: "The file to write out to."
 
350
         type: "string"
 
351
      compression-quality:
 
352
         description: "The compression quality."
 
353
         type: "integer"
 
354
         minimum: 0
 
355
         maximum: 9
 
356
         exclusiveMaximum: false
 
357
remote-sync:
 
358
   description: "Sync a file to a remote host."
 
359
   params:
 
360
      file:
 
361
         description: "The file to send out."
 
362
         type: "string"
 
363
         format: "uri"
 
364
      remote-uri:
 
365
         description: "The host to sync to."
 
366
         type: "string"
 
367
         format: "uri"
 
368
      util:
 
369
         description: "The util to perform the sync (rsync or scp.)"
 
370
         type: "string"
 
371
         enum: ["rsync", "scp"]
 
372
   required: ["file", "remote-uri"]
 
373
`,
 
374
                expectedActions: &Actions{map[string]ActionSpec{
 
375
                        "snapshot": {
 
376
                                Description: "Take a snapshot of the database.",
 
377
                                Params: map[string]interface{}{
 
378
                                        "title":       "snapshot",
 
379
                                        "description": "Take a snapshot of the database.",
 
380
                                        "type":        "object",
 
381
                                        "properties": map[string]interface{}{
 
382
                                                "outfile": map[string]interface{}{
 
383
                                                        "description": "The file to write out to.",
 
384
                                                        "type":        "string"},
 
385
                                                "compression-quality": map[string]interface{}{
 
386
                                                        "description":      "The compression quality.",
 
387
                                                        "type":             "integer",
 
388
                                                        "minimum":          0,
 
389
                                                        "maximum":          9,
 
390
                                                        "exclusiveMaximum": false}}}},
 
391
                        "remote-sync": {
 
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.",
 
396
                                        "type":        "object",
 
397
                                        "properties": map[string]interface{}{
 
398
                                                "file": map[string]interface{}{
 
399
                                                        "description": "The file to send out.",
 
400
                                                        "type":        "string",
 
401
                                                        "format":      "uri"},
 
402
                                                "remote-uri": map[string]interface{}{
 
403
                                                        "description": "The host to sync to.",
 
404
                                                        "type":        "string",
 
405
                                                        "format":      "uri"},
 
406
                                                "util": map[string]interface{}{
 
407
                                                        "description": "The util to perform the sync (rsync or scp.)",
 
408
                                                        "type":        "string",
 
409
                                                        "enum":        []interface{}{"rsync", "scp"}}},
 
410
                                        "required": []interface{}{"file", "remote-uri"}}}}},
 
411
        }, {
 
412
                description: "A schema with other keys, e.g. \"definitions\"",
 
413
                yaml: `
 
414
snapshot:
 
415
   description: "Take a snapshot of the database."
 
416
   params:
 
417
      outfile:
 
418
         description: "The file to write out to."
 
419
         type: "string"
 
420
      compression-quality:
 
421
         description: "The compression quality."
 
422
         type: "integer"
 
423
         minimum: 0
 
424
         maximum: 9
 
425
         exclusiveMaximum: false
 
426
   definitions:
 
427
      diskdevice: {}
 
428
      something-else: {}
 
429
`,
 
430
                expectedActions: &Actions{map[string]ActionSpec{
 
431
                        "snapshot": {
 
432
                                Description: "Take a snapshot of the database.",
 
433
                                Params: map[string]interface{}{
 
434
                                        "title":       "snapshot",
 
435
                                        "description": "Take a snapshot of the database.",
 
436
                                        "type":        "object",
 
437
                                        "properties": map[string]interface{}{
 
438
                                                "outfile": map[string]interface{}{
 
439
                                                        "description": "The file to write out to.",
 
440
                                                        "type":        "string",
 
441
                                                },
 
442
                                                "compression-quality": map[string]interface{}{
 
443
                                                        "description":      "The compression quality.",
 
444
                                                        "type":             "integer",
 
445
                                                        "minimum":          0,
 
446
                                                        "maximum":          9,
 
447
                                                        "exclusiveMaximum": false,
 
448
                                                },
 
449
                                        },
 
450
                                        "definitions": map[string]interface{}{
 
451
                                                "diskdevice":     map[string]interface{}{},
 
452
                                                "something-else": map[string]interface{}{},
 
453
                                        },
 
454
                                },
 
455
                        },
 
456
                }},
 
457
        }, {
 
458
                description: "A schema with no \"params\" key, implying no options.",
 
459
                yaml: `
 
460
snapshot:
 
461
   description: Take a snapshot of the database.
 
462
`,
 
463
 
 
464
                expectedActions: &Actions{map[string]ActionSpec{
 
465
                        "snapshot": {
 
466
                                Description: "Take a snapshot of the database.",
 
467
                                Params: map[string]interface{}{
 
468
                                        "description": "Take a snapshot of the database.",
 
469
                                        "title":       "snapshot",
 
470
                                        "type":        "object",
 
471
                                        "properties":  map[string]interface{}{},
 
472
                                }}}},
 
473
        }, {
 
474
                description: "A schema with no values at all, implying no options.",
 
475
                yaml: `
 
476
snapshot:
 
477
`,
 
478
 
 
479
                expectedActions: &Actions{map[string]ActionSpec{
 
480
                        "snapshot": {
 
481
                                Description: "No description",
 
482
                                Params: map[string]interface{}{
 
483
                                        "description": "No description",
 
484
                                        "title":       "snapshot",
 
485
                                        "type":        "object",
 
486
                                        "properties":  map[string]interface{}{},
 
487
                                }}}},
 
488
        }}
 
489
 
 
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)
 
497
        }
 
498
}
 
499
 
 
500
func (s *ActionsSuite) TestReadBadActionsYaml(c *gc.C) {
 
501
 
 
502
        var badActionsYamlTests = []struct {
 
503
                description   string
 
504
                yaml          string
 
505
                expectedError string
 
506
        }{{
 
507
                description: "Reject JSON-Schema containing references.",
 
508
                yaml: `
 
509
snapshot:
 
510
   description: Take a snapshot of the database.
 
511
   params:
 
512
      $schema: "http://json-schema.org/draft-03/schema#"
 
513
`,
 
514
                expectedError: "schema key \"$schema\" not compatible with this version of juju",
 
515
        }, {
 
516
                description: "Reject JSON-Schema containing references.",
 
517
                yaml: `
 
518
snapshot:
 
519
   description: Take a snapshot of the database.
 
520
   params:
 
521
      outfile: { $ref: "http://json-schema.org/draft-03/schema#" }
 
522
`,
 
523
                expectedError: "schema key \"$ref\" not compatible with this version of juju",
 
524
        }, {
 
525
                description: "Malformed YAML: missing key in \"outfile\".",
 
526
                yaml: `
 
527
snapshot:
 
528
   description: Take a snapshot of the database.
 
529
   params:
 
530
      outfile:
 
531
         The file to write out to.
 
532
         type: string
 
533
         default: foo.bz2
 
534
`,
 
535
 
 
536
                expectedError: "YAML error: line 6: mapping values are not allowed in this context",
 
537
        }, {
 
538
                description: "Malformed JSON-Schema: $schema element misplaced.",
 
539
                yaml: `
 
540
snapshot:
 
541
description: Take a snapshot of the database.
 
542
   params:
 
543
      outfile:
 
544
         $schema: http://json-schema.org/draft-03/schema#
 
545
         description: The file to write out to.
 
546
         type: string
 
547
         default: foo.bz2
 
548
`,
 
549
 
 
550
                expectedError: "YAML error: line 3: mapping values are not allowed in this context",
 
551
        }, {
 
552
                description: "Malformed Actions: hyphen at beginning of action name.",
 
553
                yaml: `
 
554
-snapshot:
 
555
   description: Take a snapshot of the database.
 
556
`,
 
557
 
 
558
                expectedError: "bad action name -snapshot",
 
559
        }, {
 
560
                description: "Malformed Actions: hyphen after action name.",
 
561
                yaml: `
 
562
snapshot-:
 
563
   description: Take a snapshot of the database.
 
564
`,
 
565
 
 
566
                expectedError: "bad action name snapshot-",
 
567
        }, {
 
568
                description: "Malformed Actions: caps in action name.",
 
569
                yaml: `
 
570
Snapshot:
 
571
   description: Take a snapshot of the database.
 
572
`,
 
573
 
 
574
                expectedError: "bad action name Snapshot",
 
575
        }, {
 
576
                description: "A non-string description fails to parse",
 
577
                yaml: `
 
578
snapshot:
 
579
   description: ["Take a snapshot of the database."]
 
580
`,
 
581
                expectedError: "value for schema key \"description\" must be a string",
 
582
        }, {
 
583
                description: "A non-list \"required\" key",
 
584
                yaml: `
 
585
snapshot:
 
586
   description: Take a snapshot of the database.
 
587
   params:
 
588
      outfile:
 
589
         description: "The file to write out to."
 
590
         type: string
 
591
   required: "outfile"
 
592
`,
 
593
                expectedError: "value for schema key \"required\" must be a YAML list",
 
594
        }, {
 
595
                description: "A schema with an empty \"params\" key fails to parse",
 
596
                yaml: `
 
597
snapshot:
 
598
   description: Take a snapshot of the database.
 
599
   params:
 
600
`,
 
601
                expectedError: "params failed to parse as a map",
 
602
        }, {
 
603
                description: "A schema with a non-map \"params\" value fails to parse",
 
604
                yaml: `
 
605
snapshot:
 
606
   description: Take a snapshot of the database.
 
607
   params: ["a", "b"]
 
608
`,
 
609
                expectedError: "params failed to parse as a map",
 
610
        }, {
 
611
                description: "\"definitions\" goes against JSON-Schema definition",
 
612
                yaml: `
 
613
snapshot:
 
614
   description: "Take a snapshot of the database."
 
615
   params:
 
616
      outfile:
 
617
         description: "The file to write out to."
 
618
         type: "string"
 
619
   definitions:
 
620
      diskdevice: ["a"]
 
621
      something-else: {"a": "b"}
 
622
`,
 
623
                expectedError: "invalid params schema for action schema snapshot: definitions must be of type array of schemas",
 
624
        }, {
 
625
                description: "excess keys not in the JSON-Schema spec will be rejected",
 
626
                yaml: `
 
627
snapshot:
 
628
   description: "Take a snapshot of the database."
 
629
   params:
 
630
      outfile:
 
631
         description: "The file to write out to."
 
632
         type: "string"
 
633
      compression-quality:
 
634
         description: "The compression quality."
 
635
         type: "integer"
 
636
         minimum: 0
 
637
         maximum: 9
 
638
         exclusiveMaximum: false
 
639
   definitions:
 
640
      diskdevice: {}
 
641
      something-else: {}
 
642
   other-key: ["some", "values"],
 
643
`,
 
644
                expectedError: "YAML error: line 16: did not find expected key",
 
645
        }}
 
646
 
 
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)
 
653
        }
 
654
}
 
655
 
 
656
func (s *ActionsSuite) TestRecurseMapOnKeys(c *gc.C) {
 
657
        tests := []struct {
 
658
                should     string
 
659
                givenKeys  []string
 
660
                givenMap   map[string]interface{}
 
661
                expected   interface{}
 
662
                shouldFail bool
 
663
        }{{
 
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{}{
 
668
                                "key": "value",
 
669
                        },
 
670
                },
 
671
                shouldFail: true,
 
672
        }, {
 
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{}{
 
677
                                5: "value",
 
678
                        },
 
679
                },
 
680
                shouldFail: true,
 
681
        }, {
 
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"},
 
686
                },
 
687
                shouldFail: true,
 
688
        }, {
 
689
                should:    "retrieve a good value",
 
690
                givenKeys: []string{"key", "key2"},
 
691
                givenMap: map[string]interface{}{
 
692
                        "key": map[string]interface{}{
 
693
                                "key2": "value",
 
694
                        },
 
695
                },
 
696
                expected: "value",
 
697
        }, {
 
698
                should:    "retrieve a map",
 
699
                givenKeys: []string{"key"},
 
700
                givenMap: map[string]interface{}{
 
701
                        "key": map[string]interface{}{
 
702
                                "key": "value",
 
703
                        },
 
704
                },
 
705
                expected: map[string]interface{}{
 
706
                        "key": "value",
 
707
                },
 
708
        }, {
 
709
                should:    "retrieve a slice",
 
710
                givenKeys: []string{"key"},
 
711
                givenMap: map[string]interface{}{
 
712
                        "key": []string{"a", "b", "c"},
 
713
                },
 
714
                expected: []string{"a", "b", "c"},
 
715
        }}
 
716
 
 
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)
 
721
                if !t.shouldFail {
 
722
                        c.Check(obtained, jc.DeepEquals, t.expected)
 
723
                }
 
724
        }
 
725
}
 
726
 
 
727
func (s *ActionsSuite) TestInsertDefaultValues(c *gc.C) {
 
728
        schemas := map[string]string{
 
729
                "simple": `
 
730
act:
 
731
  params:
 
732
    val:
 
733
      type: string
 
734
      default: somestr
 
735
`[1:],
 
736
                "complicated": `
 
737
act:
 
738
  params:
 
739
    val:
 
740
      type: object
 
741
      properties:
 
742
        foo:
 
743
          type: string
 
744
        bar:
 
745
          type: object
 
746
          properties:
 
747
            baz:
 
748
              type: string
 
749
              default: boz
 
750
`[1:],
 
751
                "default-object": `
 
752
act:
 
753
  params:
 
754
    val:
 
755
      type: object
 
756
      default:
 
757
        foo: bar
 
758
        bar:
 
759
          baz: woz
 
760
`[1:],
 
761
                "none": `
 
762
act:
 
763
  params:
 
764
    val:
 
765
      type: object
 
766
      properties: 
 
767
        var:
 
768
          type: object
 
769
          properties:
 
770
            x:
 
771
              type: string
 
772
`[1:]}
 
773
 
 
774
        for i, t := range []struct {
 
775
                should         string
 
776
                schema         string
 
777
                withParams     map[string]interface{}
 
778
                expectedResult map[string]interface{}
 
779
                expectedError  string
 
780
        }{{
 
781
                should:        "error with no schema",
 
782
                expectedError: "schema must be of type object",
 
783
        }, {
 
784
                should:         "create a map if handed nil",
 
785
                schema:         schemas["none"],
 
786
                withParams:     nil,
 
787
                expectedResult: map[string]interface{}{},
 
788
        }, {
 
789
                should:         "create and fill target if handed nil",
 
790
                schema:         schemas["simple"],
 
791
                withParams:     nil,
 
792
                expectedResult: map[string]interface{}{"val": "somestr"},
 
793
        }, {
 
794
                should:         "create a simple default value",
 
795
                schema:         schemas["simple"],
 
796
                withParams:     map[string]interface{}{},
 
797
                expectedResult: map[string]interface{}{"val": "somestr"},
 
798
        }, {
 
799
                should:         "do nothing for no default value",
 
800
                schema:         schemas["none"],
 
801
                withParams:     map[string]interface{}{},
 
802
                expectedResult: map[string]interface{}{},
 
803
        }, {
 
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{}{
 
810
                                        "baz": "boz",
 
811
                                }}},
 
812
        }, {
 
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{}{
 
818
                                "foo": "bar",
 
819
                                "bar": map[string]interface{}{
 
820
                                        "baz": "woz",
 
821
                                }}},
 
822
        }, {
 
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},
 
827
        }, {
 
828
                should: "interleave defaults into existing objects",
 
829
                schema: schemas["complicated"],
 
830
                withParams: map[string]interface{}{
 
831
                        "val": map[string]interface{}{
 
832
                                "foo": "bar",
 
833
                                "bar": map[string]interface{}{
 
834
                                        "faz": "foz",
 
835
                                }}},
 
836
                expectedResult: map[string]interface{}{
 
837
                        "val": map[string]interface{}{
 
838
                                "foo": "bar",
 
839
                                "bar": map[string]interface{}{
 
840
                                        "baz": "boz",
 
841
                                        "faz": "foz",
 
842
                                }}},
 
843
        }} {
 
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)
 
850
                        continue
 
851
                }
 
852
                c.Assert(err, jc.ErrorIsNil)
 
853
                c.Check(result, jc.DeepEquals, t.expectedResult)
 
854
        }
 
855
}
 
856
 
 
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"]
 
864
}