github.com/mhilton/juju-juju@v0.0.0-20150901100907-a94dd2c73455/cmd/juju/action/do_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.v1"
    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 DoSuite struct {
    41  	BaseActionSuite
    42  	subcommand *action.DoCommand
    43  	dir        string
    44  }
    45  
    46  var _ = gc.Suite(&DoSuite{})
    47  
    48  func (s *DoSuite) SetUpTest(c *gc.C) {
    49  	s.BaseActionSuite.SetUpTest(c)
    50  	s.dir = c.MkDir()
    51  	c.Assert(utf8.ValidString(validParamsYaml), jc.IsTrue)
    52  	c.Assert(utf8.ValidString(invalidParamsYaml), jc.IsTrue)
    53  	c.Assert(utf8.ValidString(invalidUTFYaml), jc.IsFalse)
    54  	setupValueFile(c, s.dir, "validParams.yml", validParamsYaml)
    55  	setupValueFile(c, s.dir, "invalidParams.yml", invalidParamsYaml)
    56  	setupValueFile(c, s.dir, "invalidUTF.yml", invalidUTFYaml)
    57  }
    58  
    59  func (s *DoSuite) TestHelp(c *gc.C) {
    60  	s.checkHelp(c, s.subcommand)
    61  }
    62  
    63  func (s *DoSuite) TestInit(c *gc.C) {
    64  	tests := []struct {
    65  		should               string
    66  		args                 []string
    67  		expectUnit           names.UnitTag
    68  		expectAction         string
    69  		expectParamsYamlPath string
    70  		expectParseStrings   bool
    71  		expectKVArgs         [][]string
    72  		expectOutput         string
    73  		expectError          string
    74  	}{{
    75  		should:      "fail with missing args",
    76  		args:        []string{},
    77  		expectError: "no unit specified",
    78  	}, {
    79  		should:      "fail with no action specified",
    80  		args:        []string{validUnitId},
    81  		expectError: "no action specified",
    82  	}, {
    83  		should:      "fail with invalid unit tag",
    84  		args:        []string{invalidUnitId, "valid-action-name"},
    85  		expectError: "invalid unit name \"something-strange-\"",
    86  	}, {
    87  		should:      "fail with invalid action name",
    88  		args:        []string{validUnitId, "BadName"},
    89  		expectError: "invalid action name \"BadName\"",
    90  	}, {
    91  		should:      "fail with wrong formatting of k-v args",
    92  		args:        []string{validUnitId, "valid-action-name", "uh"},
    93  		expectError: "argument \"uh\" must be of the form key...=value",
    94  	}, {
    95  		should:      "fail with wrong formatting of k-v args",
    96  		args:        []string{validUnitId, "valid-action-name", "foo.Baz=3"},
    97  		expectError: "key \"Baz\" must start and end with lowercase alphanumeric, and contain only lowercase alphanumeric and hyphens",
    98  	}, {
    99  		should:      "fail with wrong formatting of k-v args",
   100  		args:        []string{validUnitId, "valid-action-name", "no-go?od=3"},
   101  		expectError: "key \"no-go\\?od\" must start and end with lowercase alphanumeric, and contain only lowercase alphanumeric and hyphens",
   102  	}, {
   103  		should:       "work with empty values",
   104  		args:         []string{validUnitId, "valid-action-name", "ok="},
   105  		expectUnit:   names.NewUnitTag(validUnitId),
   106  		expectAction: "valid-action-name",
   107  		expectKVArgs: [][]string{{"ok", ""}},
   108  	}, {
   109  		should:             "handle --parse-strings",
   110  		args:               []string{validUnitId, "valid-action-name", "--string-args"},
   111  		expectUnit:         names.NewUnitTag(validUnitId),
   112  		expectAction:       "valid-action-name",
   113  		expectParseStrings: true,
   114  	}, {
   115  		// cf. worker/uniter/runner/jujuc/action-set_test.go per @fwereade
   116  		should:       "work with multiple '=' signs",
   117  		args:         []string{validUnitId, "valid-action-name", "ok=this=is=weird="},
   118  		expectUnit:   names.NewUnitTag(validUnitId),
   119  		expectAction: "valid-action-name",
   120  		expectKVArgs: [][]string{{"ok", "this=is=weird="}},
   121  	}, {
   122  		should:       "init properly with no params",
   123  		args:         []string{validUnitId, "valid-action-name"},
   124  		expectUnit:   names.NewUnitTag(validUnitId),
   125  		expectAction: "valid-action-name",
   126  	}, {
   127  		should:               "handle --params properly",
   128  		args:                 []string{validUnitId, "valid-action-name", "--params=foo.yml"},
   129  		expectUnit:           names.NewUnitTag(validUnitId),
   130  		expectAction:         "valid-action-name",
   131  		expectParamsYamlPath: "foo.yml",
   132  	}, {
   133  		should: "handle --params and key-value args",
   134  		args: []string{
   135  			validUnitId,
   136  			"valid-action-name",
   137  			"--params=foo.yml",
   138  			"foo.bar=2",
   139  			"foo.baz.bo=3",
   140  			"bar.foo=hello",
   141  		},
   142  		expectUnit:           names.NewUnitTag(validUnitId),
   143  		expectAction:         "valid-action-name",
   144  		expectParamsYamlPath: "foo.yml",
   145  		expectKVArgs: [][]string{
   146  			{"foo", "bar", "2"},
   147  			{"foo", "baz", "bo", "3"},
   148  			{"bar", "foo", "hello"},
   149  		},
   150  	}, {
   151  		should: "handle key-value args with no --params",
   152  		args: []string{
   153  			validUnitId,
   154  			"valid-action-name",
   155  			"foo.bar=2",
   156  			"foo.baz.bo=y",
   157  			"bar.foo=hello",
   158  		},
   159  		expectUnit:   names.NewUnitTag(validUnitId),
   160  		expectAction: "valid-action-name",
   161  		expectKVArgs: [][]string{
   162  			{"foo", "bar", "2"},
   163  			{"foo", "baz", "bo", "y"},
   164  			{"bar", "foo", "hello"},
   165  		},
   166  	}}
   167  
   168  	for i, t := range tests {
   169  		s.subcommand = &action.DoCommand{}
   170  		c.Logf("test %d: should %s:\n$ juju actions do %s\n", i,
   171  			t.should, strings.Join(t.args, " "))
   172  		err := testing.InitCommand(s.subcommand, t.args)
   173  		if t.expectError == "" {
   174  			c.Check(s.subcommand.UnitTag(), gc.Equals, t.expectUnit)
   175  			c.Check(s.subcommand.ActionName(), gc.Equals, t.expectAction)
   176  			c.Check(s.subcommand.ParamsYAMLPath(), gc.Equals, t.expectParamsYamlPath)
   177  			c.Check(s.subcommand.KeyValueDoArgs(), jc.DeepEquals, t.expectKVArgs)
   178  			c.Check(s.subcommand.ParseStrings(), gc.Equals, t.expectParseStrings)
   179  		} else {
   180  			c.Check(err, gc.ErrorMatches, t.expectError)
   181  		}
   182  	}
   183  }
   184  
   185  func (s *DoSuite) TestRun(c *gc.C) {
   186  	tests := []struct {
   187  		should                 string
   188  		withArgs               []string
   189  		withAPIErr             string
   190  		withActionResults      []params.ActionResult
   191  		expectedActionEnqueued params.Action
   192  		expectedErr            string
   193  	}{{
   194  		should:   "fail with multiple results",
   195  		withArgs: []string{validUnitId, "some-action"},
   196  		withActionResults: []params.ActionResult{
   197  			{Action: &params.Action{Tag: validActionTagString}},
   198  			{Action: &params.Action{Tag: validActionTagString}},
   199  		},
   200  		expectedErr: "illegal number of results returned",
   201  	}, {
   202  		should:   "fail with API error",
   203  		withArgs: []string{validUnitId, "some-action"},
   204  		withActionResults: []params.ActionResult{{
   205  			Action: &params.Action{Tag: validActionTagString}},
   206  		},
   207  		withAPIErr:  "something wrong in API",
   208  		expectedErr: "something wrong in API",
   209  	}, {
   210  		should:   "fail with error in result",
   211  		withArgs: []string{validUnitId, "some-action"},
   212  		withActionResults: []params.ActionResult{{
   213  			Action: &params.Action{Tag: validActionTagString},
   214  			Error:  common.ServerError(errors.New("database error")),
   215  		}},
   216  		expectedErr: "database error",
   217  	}, {
   218  		should:   "fail with invalid tag in result",
   219  		withArgs: []string{validUnitId, "some-action"},
   220  		withActionResults: []params.ActionResult{{
   221  			Action: &params.Action{Tag: invalidActionTagString},
   222  		}},
   223  		expectedErr: "\"" + invalidActionTagString + "\" is not a valid action tag",
   224  	}, {
   225  		should: "fail with missing file passed",
   226  		withArgs: []string{validUnitId, "some-action",
   227  			"--params", s.dir + "/" + "missing.yml",
   228  		},
   229  		expectedErr: "open .*missing.yml: " + utils.NoSuchFileErrRegexp,
   230  	}, {
   231  		should: "fail with invalid yaml in file",
   232  		withArgs: []string{validUnitId, "some-action",
   233  			"--params", s.dir + "/" + "invalidParams.yml",
   234  		},
   235  		expectedErr: "YAML error: line 3: mapping values are not allowed in this context",
   236  	}, {
   237  		should: "fail with invalid UTF in file",
   238  		withArgs: []string{validUnitId, "some-action",
   239  			"--params", s.dir + "/" + "invalidUTF.yml",
   240  		},
   241  		expectedErr: "YAML error: invalid leading UTF-8 octet",
   242  	}, {
   243  		should:      "fail with invalid YAML passed as arg and no --string-args",
   244  		withArgs:    []string{validUnitId, "some-action", "foo.bar=\""},
   245  		expectedErr: "YAML error: found unexpected end of stream",
   246  	}, {
   247  		should:   "enqueue a basic action with no params",
   248  		withArgs: []string{validUnitId, "some-action"},
   249  		withActionResults: []params.ActionResult{{
   250  			Action: &params.Action{Tag: validActionTagString},
   251  		}},
   252  		expectedActionEnqueued: params.Action{
   253  			Name:       "some-action",
   254  			Parameters: map[string]interface{}{},
   255  			Receiver:   names.NewUnitTag(validUnitId).String(),
   256  		},
   257  	}, {
   258  		should: "enqueue an action with some explicit params",
   259  		withArgs: []string{validUnitId, "some-action",
   260  			"out.name=bar",
   261  			"out.kind=tmpfs",
   262  			"out.num=3",
   263  			"out.boolval=y",
   264  		},
   265  		withActionResults: []params.ActionResult{{
   266  			Action: &params.Action{Tag: validActionTagString},
   267  		}},
   268  		expectedActionEnqueued: params.Action{
   269  			Name:     "some-action",
   270  			Receiver: names.NewUnitTag(validUnitId).String(),
   271  			Parameters: map[string]interface{}{
   272  				"out": map[string]interface{}{
   273  					"name":    "bar",
   274  					"kind":    "tmpfs",
   275  					"num":     3,
   276  					"boolval": true,
   277  				},
   278  			},
   279  		},
   280  	}, {
   281  		should: "enqueue an action with some raw string params",
   282  		withArgs: []string{validUnitId, "some-action", "--string-args",
   283  			"out.name=bar",
   284  			"out.kind=tmpfs",
   285  			"out.num=3",
   286  			"out.boolval=y",
   287  		},
   288  		withActionResults: []params.ActionResult{{
   289  			Action: &params.Action{Tag: validActionTagString},
   290  		}},
   291  		expectedActionEnqueued: params.Action{
   292  			Name:     "some-action",
   293  			Receiver: names.NewUnitTag(validUnitId).String(),
   294  			Parameters: map[string]interface{}{
   295  				"out": map[string]interface{}{
   296  					"name":    "bar",
   297  					"kind":    "tmpfs",
   298  					"num":     "3",
   299  					"boolval": "y",
   300  				},
   301  			},
   302  		},
   303  	}, {
   304  		should: "enqueue an action with file params plus CLI args",
   305  		withArgs: []string{validUnitId, "some-action",
   306  			"--params", s.dir + "/" + "validParams.yml",
   307  			"compression.kind=gz",
   308  			"compression.fast=true",
   309  		},
   310  		withActionResults: []params.ActionResult{{
   311  			Action: &params.Action{Tag: validActionTagString},
   312  		}},
   313  		expectedActionEnqueued: params.Action{
   314  			Name:     "some-action",
   315  			Receiver: names.NewUnitTag(validUnitId).String(),
   316  			Parameters: map[string]interface{}{
   317  				"out": "name",
   318  				"compression": map[string]interface{}{
   319  					"kind":    "gz",
   320  					"quality": "high",
   321  					"fast":    true,
   322  				},
   323  			},
   324  		},
   325  	}, {
   326  		should: "enqueue an action with file params and explicit params",
   327  		withArgs: []string{validUnitId, "some-action",
   328  			"out.name=bar",
   329  			"out.kind=tmpfs",
   330  			"compression.quality.speed=high",
   331  			"compression.quality.size=small",
   332  			"--params", s.dir + "/" + "validParams.yml",
   333  		},
   334  		withActionResults: []params.ActionResult{{
   335  			Action: &params.Action{Tag: validActionTagString},
   336  		}},
   337  		expectedActionEnqueued: params.Action{
   338  			Name:     "some-action",
   339  			Receiver: names.NewUnitTag(validUnitId).String(),
   340  			Parameters: map[string]interface{}{
   341  				"out": map[string]interface{}{
   342  					"name": "bar",
   343  					"kind": "tmpfs",
   344  				},
   345  				"compression": map[string]interface{}{
   346  					"kind": "xz",
   347  					"quality": map[string]interface{}{
   348  						"speed": "high",
   349  						"size":  "small",
   350  					},
   351  				},
   352  			},
   353  		},
   354  	}}
   355  
   356  	for i, t := range tests {
   357  		func() {
   358  			c.Logf("test %d: should %s:\n$ juju actions do %s\n", i,
   359  				t.should, strings.Join(t.withArgs, " "))
   360  			fakeClient := &fakeAPIClient{
   361  				actionResults: t.withActionResults,
   362  			}
   363  			if t.withAPIErr != "" {
   364  				fakeClient.apiErr = errors.New(t.withAPIErr)
   365  			}
   366  			restore := s.patchAPIClient(fakeClient)
   367  			defer restore()
   368  
   369  			s.subcommand = &action.DoCommand{}
   370  			ctx, err := testing.RunCommand(c, s.subcommand, t.withArgs...)
   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  }