github.com/makyo/juju@v0.0.0-20160425123129-2608902037e9/cmd/juju/action/run_test.go (about)

     1  // Copyright 2014, 2015 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package action_test
     5  
     6  import (
     7  	"bytes"
     8  	"errors"
     9  	"strings"
    10  	"unicode/utf8"
    11  
    12  	"github.com/juju/names"
    13  	jc "github.com/juju/testing/checkers"
    14  	"github.com/juju/utils"
    15  	gc "gopkg.in/check.v1"
    16  	"gopkg.in/yaml.v2"
    17  
    18  	"github.com/juju/juju/apiserver/common"
    19  	"github.com/juju/juju/apiserver/params"
    20  	"github.com/juju/juju/cmd/juju/action"
    21  	"github.com/juju/juju/testing"
    22  )
    23  
    24  var (
    25  	validParamsYaml = `
    26  out: name
    27  compression:
    28    kind: xz
    29    quality: high
    30  `[1:]
    31  	invalidParamsYaml = `
    32  broken-map:
    33    foo:
    34      foo
    35      bar: baz
    36  `[1:]
    37  	invalidUTFYaml = "out: ok" + string([]byte{0xFF, 0xFF})
    38  )
    39  
    40  type RunSuite struct {
    41  	BaseActionSuite
    42  	dir string
    43  }
    44  
    45  var _ = gc.Suite(&RunSuite{})
    46  
    47  func (s *RunSuite) SetUpTest(c *gc.C) {
    48  	s.BaseActionSuite.SetUpTest(c)
    49  	s.dir = c.MkDir()
    50  	c.Assert(utf8.ValidString(validParamsYaml), jc.IsTrue)
    51  	c.Assert(utf8.ValidString(invalidParamsYaml), jc.IsTrue)
    52  	c.Assert(utf8.ValidString(invalidUTFYaml), jc.IsFalse)
    53  	setupValueFile(c, s.dir, "validParams.yml", validParamsYaml)
    54  	setupValueFile(c, s.dir, "invalidParams.yml", invalidParamsYaml)
    55  	setupValueFile(c, s.dir, "invalidUTF.yml", invalidUTFYaml)
    56  }
    57  
    58  func (s *RunSuite) TestInit(c *gc.C) {
    59  	tests := []struct {
    60  		should               string
    61  		args                 []string
    62  		expectUnit           names.UnitTag
    63  		expectAction         string
    64  		expectParamsYamlPath string
    65  		expectParseStrings   bool
    66  		expectKVArgs         [][]string
    67  		expectOutput         string
    68  		expectError          string
    69  	}{{
    70  		should:      "fail with missing args",
    71  		args:        []string{},
    72  		expectError: "no unit specified",
    73  	}, {
    74  		should:      "fail with no action specified",
    75  		args:        []string{validUnitId},
    76  		expectError: "no action specified",
    77  	}, {
    78  		should:      "fail with invalid unit tag",
    79  		args:        []string{invalidUnitId, "valid-action-name"},
    80  		expectError: "invalid unit name \"something-strange-\"",
    81  	}, {
    82  		should:      "fail with invalid action name",
    83  		args:        []string{validUnitId, "BadName"},
    84  		expectError: "invalid action name \"BadName\"",
    85  	}, {
    86  		should:      "fail with wrong formatting of k-v args",
    87  		args:        []string{validUnitId, "valid-action-name", "uh"},
    88  		expectError: "argument \"uh\" must be of the form key...=value",
    89  	}, {
    90  		should:      "fail with wrong formatting of k-v args",
    91  		args:        []string{validUnitId, "valid-action-name", "foo.Baz=3"},
    92  		expectError: "key \"Baz\" must start and end with lowercase alphanumeric, and contain only lowercase alphanumeric and hyphens",
    93  	}, {
    94  		should:      "fail with wrong formatting of k-v args",
    95  		args:        []string{validUnitId, "valid-action-name", "no-go?od=3"},
    96  		expectError: "key \"no-go\\?od\" must start and end with lowercase alphanumeric, and contain only lowercase alphanumeric and hyphens",
    97  	}, {
    98  		should:       "work with empty values",
    99  		args:         []string{validUnitId, "valid-action-name", "ok="},
   100  		expectUnit:   names.NewUnitTag(validUnitId),
   101  		expectAction: "valid-action-name",
   102  		expectKVArgs: [][]string{{"ok", ""}},
   103  	}, {
   104  		should:             "handle --parse-strings",
   105  		args:               []string{validUnitId, "valid-action-name", "--string-args"},
   106  		expectUnit:         names.NewUnitTag(validUnitId),
   107  		expectAction:       "valid-action-name",
   108  		expectParseStrings: true,
   109  	}, {
   110  		// cf. worker/uniter/runner/jujuc/action-set_test.go per @fwereade
   111  		should:       "work with multiple '=' signs",
   112  		args:         []string{validUnitId, "valid-action-name", "ok=this=is=weird="},
   113  		expectUnit:   names.NewUnitTag(validUnitId),
   114  		expectAction: "valid-action-name",
   115  		expectKVArgs: [][]string{{"ok", "this=is=weird="}},
   116  	}, {
   117  		should:       "init properly with no params",
   118  		args:         []string{validUnitId, "valid-action-name"},
   119  		expectUnit:   names.NewUnitTag(validUnitId),
   120  		expectAction: "valid-action-name",
   121  	}, {
   122  		should:               "handle --params properly",
   123  		args:                 []string{validUnitId, "valid-action-name", "--params=foo.yml"},
   124  		expectUnit:           names.NewUnitTag(validUnitId),
   125  		expectAction:         "valid-action-name",
   126  		expectParamsYamlPath: "foo.yml",
   127  	}, {
   128  		should: "handle --params and key-value args",
   129  		args: []string{
   130  			validUnitId,
   131  			"valid-action-name",
   132  			"--params=foo.yml",
   133  			"foo.bar=2",
   134  			"foo.baz.bo=3",
   135  			"bar.foo=hello",
   136  		},
   137  		expectUnit:           names.NewUnitTag(validUnitId),
   138  		expectAction:         "valid-action-name",
   139  		expectParamsYamlPath: "foo.yml",
   140  		expectKVArgs: [][]string{
   141  			{"foo", "bar", "2"},
   142  			{"foo", "baz", "bo", "3"},
   143  			{"bar", "foo", "hello"},
   144  		},
   145  	}, {
   146  		should: "handle key-value args with no --params",
   147  		args: []string{
   148  			validUnitId,
   149  			"valid-action-name",
   150  			"foo.bar=2",
   151  			"foo.baz.bo=y",
   152  			"bar.foo=hello",
   153  		},
   154  		expectUnit:   names.NewUnitTag(validUnitId),
   155  		expectAction: "valid-action-name",
   156  		expectKVArgs: [][]string{
   157  			{"foo", "bar", "2"},
   158  			{"foo", "baz", "bo", "y"},
   159  			{"bar", "foo", "hello"},
   160  		},
   161  	}}
   162  
   163  	for i, t := range tests {
   164  		for _, modelFlag := range s.modelFlags {
   165  			wrappedCommand, command := action.NewRunCommandForTest(s.store)
   166  			c.Logf("test %d: should %s:\n$ juju run-action %s\n", i,
   167  				t.should, strings.Join(t.args, " "))
   168  			args := append([]string{modelFlag, "admin"}, t.args...)
   169  			err := testing.InitCommand(wrappedCommand, args)
   170  			if t.expectError == "" {
   171  				c.Check(command.UnitTag(), gc.Equals, t.expectUnit)
   172  				c.Check(command.ActionName(), gc.Equals, t.expectAction)
   173  				c.Check(command.ParamsYAML().Path, gc.Equals, t.expectParamsYamlPath)
   174  				c.Check(command.Args(), jc.DeepEquals, t.expectKVArgs)
   175  				c.Check(command.ParseStrings(), gc.Equals, t.expectParseStrings)
   176  			} else {
   177  				c.Check(err, gc.ErrorMatches, t.expectError)
   178  			}
   179  		}
   180  	}
   181  }
   182  
   183  func (s *RunSuite) TestRun(c *gc.C) {
   184  	tests := []struct {
   185  		should                 string
   186  		withArgs               []string
   187  		withAPIErr             string
   188  		withActionResults      []params.ActionResult
   189  		expectedActionEnqueued params.Action
   190  		expectedErr            string
   191  	}{{
   192  		should:   "fail with multiple results",
   193  		withArgs: []string{validUnitId, "some-action"},
   194  		withActionResults: []params.ActionResult{
   195  			{Action: &params.Action{Tag: validActionTagString}},
   196  			{Action: &params.Action{Tag: validActionTagString}},
   197  		},
   198  		expectedErr: "illegal number of results returned",
   199  	}, {
   200  		should:   "fail with API error",
   201  		withArgs: []string{validUnitId, "some-action"},
   202  		withActionResults: []params.ActionResult{{
   203  			Action: &params.Action{Tag: validActionTagString}},
   204  		},
   205  		withAPIErr:  "something wrong in API",
   206  		expectedErr: "something wrong in API",
   207  	}, {
   208  		should:   "fail with error in result",
   209  		withArgs: []string{validUnitId, "some-action"},
   210  		withActionResults: []params.ActionResult{{
   211  			Action: &params.Action{Tag: validActionTagString},
   212  			Error:  common.ServerError(errors.New("database error")),
   213  		}},
   214  		expectedErr: "database error",
   215  	}, {
   216  		should:   "fail with invalid tag in result",
   217  		withArgs: []string{validUnitId, "some-action"},
   218  		withActionResults: []params.ActionResult{{
   219  			Action: &params.Action{Tag: invalidActionTagString},
   220  		}},
   221  		expectedErr: "\"" + invalidActionTagString + "\" is not a valid action tag",
   222  	}, {
   223  		should: "fail with missing file passed",
   224  		withArgs: []string{validUnitId, "some-action",
   225  			"--params", s.dir + "/" + "missing.yml",
   226  		},
   227  		expectedErr: "open .*missing.yml: " + utils.NoSuchFileErrRegexp,
   228  	}, {
   229  		should: "fail with invalid yaml in file",
   230  		withArgs: []string{validUnitId, "some-action",
   231  			"--params", s.dir + "/" + "invalidParams.yml",
   232  		},
   233  		expectedErr: "yaml: line 3: mapping values are not allowed in this context",
   234  	}, {
   235  		should: "fail with invalid UTF in file",
   236  		withArgs: []string{validUnitId, "some-action",
   237  			"--params", s.dir + "/" + "invalidUTF.yml",
   238  		},
   239  		expectedErr: "yaml: invalid leading UTF-8 octet",
   240  	}, {
   241  		should:      "fail with invalid YAML passed as arg and no --string-args",
   242  		withArgs:    []string{validUnitId, "some-action", "foo.bar=\""},
   243  		expectedErr: "yaml: found unexpected end of stream",
   244  	}, {
   245  		should:   "enqueue a basic action with no params",
   246  		withArgs: []string{validUnitId, "some-action"},
   247  		withActionResults: []params.ActionResult{{
   248  			Action: &params.Action{Tag: validActionTagString},
   249  		}},
   250  		expectedActionEnqueued: params.Action{
   251  			Name:       "some-action",
   252  			Parameters: map[string]interface{}{},
   253  			Receiver:   names.NewUnitTag(validUnitId).String(),
   254  		},
   255  	}, {
   256  		should: "enqueue an action with some explicit params",
   257  		withArgs: []string{validUnitId, "some-action",
   258  			"out.name=bar",
   259  			"out.kind=tmpfs",
   260  			"out.num=3",
   261  			"out.boolval=y",
   262  		},
   263  		withActionResults: []params.ActionResult{{
   264  			Action: &params.Action{Tag: validActionTagString},
   265  		}},
   266  		expectedActionEnqueued: params.Action{
   267  			Name:     "some-action",
   268  			Receiver: names.NewUnitTag(validUnitId).String(),
   269  			Parameters: map[string]interface{}{
   270  				"out": map[string]interface{}{
   271  					"name":    "bar",
   272  					"kind":    "tmpfs",
   273  					"num":     3,
   274  					"boolval": true,
   275  				},
   276  			},
   277  		},
   278  	}, {
   279  		should: "enqueue an action with some raw string params",
   280  		withArgs: []string{validUnitId, "some-action", "--string-args",
   281  			"out.name=bar",
   282  			"out.kind=tmpfs",
   283  			"out.num=3",
   284  			"out.boolval=y",
   285  		},
   286  		withActionResults: []params.ActionResult{{
   287  			Action: &params.Action{Tag: validActionTagString},
   288  		}},
   289  		expectedActionEnqueued: params.Action{
   290  			Name:     "some-action",
   291  			Receiver: names.NewUnitTag(validUnitId).String(),
   292  			Parameters: map[string]interface{}{
   293  				"out": map[string]interface{}{
   294  					"name":    "bar",
   295  					"kind":    "tmpfs",
   296  					"num":     "3",
   297  					"boolval": "y",
   298  				},
   299  			},
   300  		},
   301  	}, {
   302  		should: "enqueue an action with file params plus CLI args",
   303  		withArgs: []string{validUnitId, "some-action",
   304  			"--params", s.dir + "/" + "validParams.yml",
   305  			"compression.kind=gz",
   306  			"compression.fast=true",
   307  		},
   308  		withActionResults: []params.ActionResult{{
   309  			Action: &params.Action{Tag: validActionTagString},
   310  		}},
   311  		expectedActionEnqueued: params.Action{
   312  			Name:     "some-action",
   313  			Receiver: names.NewUnitTag(validUnitId).String(),
   314  			Parameters: map[string]interface{}{
   315  				"out": "name",
   316  				"compression": map[string]interface{}{
   317  					"kind":    "gz",
   318  					"quality": "high",
   319  					"fast":    true,
   320  				},
   321  			},
   322  		},
   323  	}, {
   324  		should: "enqueue an action with file params and explicit params",
   325  		withArgs: []string{validUnitId, "some-action",
   326  			"out.name=bar",
   327  			"out.kind=tmpfs",
   328  			"compression.quality.speed=high",
   329  			"compression.quality.size=small",
   330  			"--params", s.dir + "/" + "validParams.yml",
   331  		},
   332  		withActionResults: []params.ActionResult{{
   333  			Action: &params.Action{Tag: validActionTagString},
   334  		}},
   335  		expectedActionEnqueued: params.Action{
   336  			Name:     "some-action",
   337  			Receiver: names.NewUnitTag(validUnitId).String(),
   338  			Parameters: map[string]interface{}{
   339  				"out": map[string]interface{}{
   340  					"name": "bar",
   341  					"kind": "tmpfs",
   342  				},
   343  				"compression": map[string]interface{}{
   344  					"kind": "xz",
   345  					"quality": map[string]interface{}{
   346  						"speed": "high",
   347  						"size":  "small",
   348  					},
   349  				},
   350  			},
   351  		},
   352  	}}
   353  
   354  	for i, t := range tests {
   355  		for _, modelFlag := range s.modelFlags {
   356  			func() {
   357  				c.Logf("test %d: should %s:\n$ juju actions do %s\n", i,
   358  					t.should, strings.Join(t.withArgs, " "))
   359  				fakeClient := &fakeAPIClient{
   360  					actionResults: t.withActionResults,
   361  				}
   362  				if t.withAPIErr != "" {
   363  					fakeClient.apiErr = errors.New(t.withAPIErr)
   364  				}
   365  				restore := s.patchAPIClient(fakeClient)
   366  				defer restore()
   367  
   368  				wrappedCommand, _ := action.NewRunCommandForTest(s.store)
   369  				args := append([]string{modelFlag, "admin"}, t.withArgs...)
   370  				ctx, err := testing.RunCommand(c, wrappedCommand, args...)
   371  
   372  				if t.expectedErr != "" || t.withAPIErr != "" {
   373  					c.Check(err, gc.ErrorMatches, t.expectedErr)
   374  				} else {
   375  					c.Assert(err, gc.IsNil)
   376  					// Before comparing, double-check to avoid
   377  					// panics in malformed tests.
   378  					c.Assert(len(t.withActionResults), gc.Equals, 1)
   379  					// Make sure the test's expected Action was
   380  					// non-nil and correct.
   381  					c.Assert(t.withActionResults[0].Action, gc.NotNil)
   382  					expectedTag, err := names.ParseActionTag(t.withActionResults[0].Action.Tag)
   383  					c.Assert(err, gc.IsNil)
   384  					// Make sure the CLI responded with the expected tag
   385  					keyToCheck := "Action queued with id"
   386  					expectedMap := map[string]string{keyToCheck: expectedTag.Id()}
   387  					outputResult := ctx.Stdout.(*bytes.Buffer).Bytes()
   388  					resultMap := make(map[string]string)
   389  					err = yaml.Unmarshal(outputResult, &resultMap)
   390  					c.Assert(err, gc.IsNil)
   391  					c.Check(resultMap, jc.DeepEquals, expectedMap)
   392  					// Make sure the Action sent to the API to be
   393  					// enqueued was indeed the expected map
   394  					enqueued := fakeClient.EnqueuedActions()
   395  					c.Assert(enqueued.Actions, gc.HasLen, 1)
   396  					c.Check(enqueued.Actions[0], jc.DeepEquals, t.expectedActionEnqueued)
   397  				}
   398  			}()
   399  		}
   400  	}
   401  }