github.com/jdolitsky/cnab-go@v0.7.1-beta1/action/action_test.go (about)

     1  package action
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"strings"
     7  	"testing"
     8  	"time"
     9  
    10  	"github.com/deislabs/cnab-go/claim"
    11  	"github.com/deislabs/cnab-go/credentials"
    12  	"github.com/deislabs/cnab-go/driver"
    13  
    14  	"github.com/deislabs/cnab-go/bundle"
    15  	"github.com/deislabs/cnab-go/bundle/definition"
    16  
    17  	"github.com/stretchr/testify/assert"
    18  )
    19  
    20  type mockDriver struct {
    21  	shouldHandle bool
    22  	Operation    *driver.Operation
    23  	Result       driver.OperationResult
    24  	Error        error
    25  }
    26  
    27  func (d *mockDriver) Handles(imageType string) bool {
    28  	return d.shouldHandle
    29  }
    30  func (d *mockDriver) Run(op *driver.Operation) (driver.OperationResult, error) {
    31  	d.Operation = op
    32  	return d.Result, d.Error
    33  }
    34  
    35  var mockSet = credentials.Set{
    36  	"secret_one": "I'm a secret",
    37  	"secret_two": "I'm also a secret",
    38  }
    39  
    40  func newClaim() *claim.Claim {
    41  	now := time.Now()
    42  	return &claim.Claim{
    43  		Created:    now,
    44  		Modified:   now,
    45  		Name:       "name",
    46  		Revision:   "revision",
    47  		Bundle:     mockBundle(),
    48  		Parameters: map[string]interface{}{},
    49  	}
    50  }
    51  
    52  func mockBundle() *bundle.Bundle {
    53  	return &bundle.Bundle{
    54  		Name:    "bar",
    55  		Version: "0.1.0",
    56  		InvocationImages: []bundle.InvocationImage{
    57  			{
    58  				BaseImage: bundle.BaseImage{Image: "foo/bar:0.1.0", ImageType: "docker"},
    59  			},
    60  		},
    61  		Credentials: map[string]bundle.Credential{
    62  			"secret_one": {
    63  				Location: bundle.Location{
    64  					EnvironmentVariable: "SECRET_ONE",
    65  					Path:                "/foo/bar",
    66  				},
    67  			},
    68  			"secret_two": {
    69  				Location: bundle.Location{
    70  					EnvironmentVariable: "SECRET_TWO",
    71  					Path:                "/secret/two",
    72  				},
    73  			},
    74  		},
    75  		Definitions: map[string]*definition.Schema{
    76  			"ParamOne": {
    77  				Type:    "string",
    78  				Default: "one",
    79  			},
    80  			"ParamTwo": {
    81  				Type:    "string",
    82  				Default: "two",
    83  			},
    84  			"ParamThree": {
    85  				Type:    "string",
    86  				Default: "three",
    87  			},
    88  			"NullParam": {
    89  				Type: "null",
    90  			},
    91  			"BooleanParam": {
    92  				Type:    "boolean",
    93  				Default: true,
    94  			},
    95  			"ObjectParam": {
    96  				Type: "object",
    97  			},
    98  			"ArrayParam": {
    99  				Type: "array",
   100  			},
   101  			"NumberParam": {
   102  				Type: "number",
   103  			},
   104  			"IntegerParam": {
   105  				Type: "integer",
   106  			},
   107  			"StringParam": {
   108  				Type: "string",
   109  			},
   110  			"BooleanAndIntegerParam": {
   111  				Type: []interface{}{"boolean", "integer"},
   112  			},
   113  			"StringAndBooleanParam": {
   114  				Type: []interface{}{"string", "boolean"},
   115  			},
   116  		},
   117  		Outputs: map[string]bundle.Output{
   118  			"some-output": {
   119  				Path:       "/tmp/some/path",
   120  				Definition: "ParamOne",
   121  			},
   122  		},
   123  		Parameters: map[string]bundle.Parameter{
   124  			"param_one": {
   125  				Definition: "ParamOne",
   126  			},
   127  			"param_two": {
   128  				Definition: "ParamTwo",
   129  				Destination: &bundle.Location{
   130  					EnvironmentVariable: "PARAM_TWO",
   131  				},
   132  			},
   133  			"param_three": {
   134  				Definition: "ParamThree",
   135  				Destination: &bundle.Location{
   136  					Path: "/param/three",
   137  				},
   138  			},
   139  			"param_array": {
   140  				Definition: "ArrayParam",
   141  				Destination: &bundle.Location{
   142  					Path: "/param/array",
   143  				},
   144  			},
   145  			"param_object": {
   146  				Definition: "ObjectParam",
   147  				Destination: &bundle.Location{
   148  					Path: "/param/object",
   149  				},
   150  			},
   151  			"param_escaped_quotes": {
   152  				Definition: "StringParam",
   153  				Destination: &bundle.Location{
   154  					Path: "/param/param_escaped_quotes",
   155  				},
   156  			},
   157  			"param_quoted_string": {
   158  				Definition: "StringParam",
   159  				Destination: &bundle.Location{
   160  					Path: "/param/param_quoted_string",
   161  				},
   162  			},
   163  		},
   164  		Actions: map[string]bundle.Action{
   165  			"test": {Modifies: true},
   166  		},
   167  		Images: map[string]bundle.Image{
   168  			"image-a": {
   169  				BaseImage: bundle.BaseImage{
   170  					Image: "foo/bar:0.1.0", ImageType: "docker",
   171  				},
   172  				Description: "description",
   173  			},
   174  		},
   175  	}
   176  }
   177  
   178  func TestOpFromClaim(t *testing.T) {
   179  	c := newClaim()
   180  	c.Parameters = map[string]interface{}{
   181  		"param_one":   "oneval",
   182  		"param_two":   "twoval",
   183  		"param_three": "threeval",
   184  		"param_array": []string{"first-value", "second-value"},
   185  		"param_object": map[string]string{
   186  			"first-key":  "first-value",
   187  			"second-key": "second-value",
   188  		},
   189  		"param_escaped_quotes": `\"escaped value\"`,
   190  		"param_quoted_string":  `"quoted value"`,
   191  	}
   192  	invocImage := c.Bundle.InvocationImages[0]
   193  
   194  	op, err := opFromClaim(claim.ActionInstall, stateful, c, invocImage, mockSet)
   195  	if err != nil {
   196  		t.Fatal(err)
   197  	}
   198  
   199  	is := assert.New(t)
   200  
   201  	is.Equal(c.Name, op.Installation)
   202  	is.Equal(c.Revision, op.Revision)
   203  	is.Equal(invocImage.Image, op.Image.Image)
   204  	is.Equal(driver.ImageTypeDocker, op.Image.ImageType)
   205  	is.Equal(op.Environment["SECRET_ONE"], "I'm a secret")
   206  	is.Equal(op.Environment["PARAM_TWO"], "twoval")
   207  	is.Equal(op.Environment["CNAB_P_PARAM_ONE"], "oneval")
   208  	is.Equal(op.Files["/secret/two"], "I'm also a secret")
   209  	is.Equal(op.Files["/param/three"], "threeval")
   210  	is.Equal(op.Files["/param/array"], "[\"first-value\",\"second-value\"]")
   211  	is.Equal(op.Files["/param/object"], `{"first-key":"first-value","second-key":"second-value"}`)
   212  	is.Equal(op.Files["/param/param_escaped_quotes"], `\"escaped value\"`)
   213  	is.Equal(op.Files["/param/param_quoted_string"], `"quoted value"`)
   214  	is.Contains(op.Files, "/cnab/app/image-map.json")
   215  	is.Contains(op.Files, "/cnab/bundle.json")
   216  	is.Contains(op.Outputs, "/tmp/some/path")
   217  
   218  	var imgMap map[string]bundle.Image
   219  	is.NoError(json.Unmarshal([]byte(op.Files["/cnab/app/image-map.json"]), &imgMap))
   220  	is.Equal(c.Bundle.Images, imgMap)
   221  
   222  	var bundle *bundle.Bundle
   223  	is.NoError(json.Unmarshal([]byte(op.Files["/cnab/bundle.json"]), &bundle))
   224  	is.Equal(c.Bundle, bundle)
   225  
   226  	is.Len(op.Parameters, 7)
   227  	is.Nil(op.Out)
   228  }
   229  
   230  func TestOpFromClaim_NoOutputsOnBundle(t *testing.T) {
   231  	c := newClaim()
   232  	c.Bundle = mockBundle()
   233  	c.Bundle.Outputs = nil
   234  	invocImage := c.Bundle.InvocationImages[0]
   235  
   236  	op, err := opFromClaim(claim.ActionInstall, stateful, c, invocImage, mockSet)
   237  	if err != nil {
   238  		t.Fatal(err)
   239  	}
   240  
   241  	is := assert.New(t)
   242  
   243  	is.Equal(c.Name, op.Installation)
   244  	is.Equal(c.Revision, op.Revision)
   245  	is.Equal(invocImage.Image, op.Image.Image)
   246  	is.Equal(driver.ImageTypeDocker, op.Image.ImageType)
   247  	is.Equal(op.Environment["SECRET_ONE"], "I'm a secret")
   248  	is.Equal(op.Files["/secret/two"], "I'm also a secret")
   249  	is.Contains(op.Files, "/cnab/app/image-map.json")
   250  	var imgMap map[string]bundle.Image
   251  	is.NoError(json.Unmarshal([]byte(op.Files["/cnab/app/image-map.json"]), &imgMap))
   252  	is.Equal(c.Bundle.Images, imgMap)
   253  	is.Len(op.Parameters, 0)
   254  	is.Nil(op.Out)
   255  }
   256  
   257  func TestOpFromClaim_NoParameter(t *testing.T) {
   258  	c := newClaim()
   259  	c.Bundle = mockBundle()
   260  	c.Bundle.Parameters = nil
   261  	invocImage := c.Bundle.InvocationImages[0]
   262  
   263  	op, err := opFromClaim(claim.ActionInstall, stateful, c, invocImage, mockSet)
   264  	if err != nil {
   265  		t.Fatal(err)
   266  	}
   267  
   268  	is := assert.New(t)
   269  
   270  	is.Equal(c.Name, op.Installation)
   271  	is.Equal(c.Revision, op.Revision)
   272  	is.Equal(invocImage.Image, op.Image.Image)
   273  	is.Equal(driver.ImageTypeDocker, op.Image.ImageType)
   274  	is.Equal(op.Environment["SECRET_ONE"], "I'm a secret")
   275  	is.Equal(op.Files["/secret/two"], "I'm also a secret")
   276  	is.Contains(op.Files, "/cnab/app/image-map.json")
   277  	var imgMap map[string]bundle.Image
   278  	is.NoError(json.Unmarshal([]byte(op.Files["/cnab/app/image-map.json"]), &imgMap))
   279  	is.Equal(c.Bundle.Images, imgMap)
   280  	is.Len(op.Parameters, 0)
   281  	is.Nil(op.Out)
   282  }
   283  
   284  func TestOpFromClaim_UndefinedParams(t *testing.T) {
   285  	c := newClaim()
   286  	c.Parameters = map[string]interface{}{
   287  		"param_one":         "oneval",
   288  		"param_two":         "twoval",
   289  		"param_three":       "threeval",
   290  		"param_one_million": "this is not a valid parameter",
   291  	}
   292  	invocImage := c.Bundle.InvocationImages[0]
   293  
   294  	_, err := opFromClaim(claim.ActionInstall, stateful, c, invocImage, mockSet)
   295  	assert.Error(t, err)
   296  }
   297  
   298  func TestOpFromClaim_MissingRequiredParameter(t *testing.T) {
   299  	c := newClaim()
   300  	c.Parameters = map[string]interface{}{
   301  		"param_two":   "twoval",
   302  		"param_three": "threeval",
   303  	}
   304  	c.Bundle = mockBundle()
   305  	c.Bundle.Parameters["param_one"] = bundle.Parameter{Definition: "ParamOne", Required: true}
   306  	invocImage := c.Bundle.InvocationImages[0]
   307  
   308  	t.Run("missing required parameter fails", func(t *testing.T) {
   309  		_, err := opFromClaim(claim.ActionInstall, stateful, c, invocImage, mockSet)
   310  		assert.EqualError(t, err, `missing required parameter "param_one" for action "install"`)
   311  	})
   312  
   313  	t.Run("fill the missing parameter", func(t *testing.T) {
   314  		c.Parameters["param_one"] = "oneval"
   315  		_, err := opFromClaim(claim.ActionInstall, stateful, c, invocImage, mockSet)
   316  		assert.Nil(t, err)
   317  	})
   318  }
   319  
   320  func TestOpFromClaim_MissingRequiredParamSpecificToAction(t *testing.T) {
   321  	c := newClaim()
   322  	c.Parameters = map[string]interface{}{
   323  		"param_one":   "oneval",
   324  		"param_two":   "twoval",
   325  		"param_three": "threeval",
   326  	}
   327  	c.Bundle = mockBundle()
   328  	// Add a required parameter only defined for the test action
   329  	c.Bundle.Parameters["param_test"] = bundle.Parameter{
   330  		Definition: "StringParam",
   331  		Required:   true,
   332  		ApplyTo:    []string{"test"},
   333  	}
   334  	invocImage := c.Bundle.InvocationImages[0]
   335  
   336  	t.Run("if param is not required for this action, succeed", func(t *testing.T) {
   337  		_, err := opFromClaim(claim.ActionInstall, stateful, c, invocImage, mockSet)
   338  		assert.Nil(t, err)
   339  	})
   340  
   341  	t.Run("if param is required for this action and is missing, error", func(t *testing.T) {
   342  		_, err := opFromClaim("test", stateful, c, invocImage, mockSet)
   343  		assert.EqualError(t, err, `missing required parameter "param_test" for action "test"`)
   344  	})
   345  
   346  	t.Run("if param is required for this action and is set, succeed", func(t *testing.T) {
   347  		c.Parameters["param_test"] = "only for test action"
   348  		_, err := opFromClaim("test", stateful, c, invocImage, mockSet)
   349  		assert.Nil(t, err)
   350  	})
   351  }
   352  
   353  func TestSetOutputsOnClaim(t *testing.T) {
   354  	c := newClaim()
   355  	c.Bundle = mockBundle()
   356  
   357  	t.Run("any text in a file is a valid string", func(t *testing.T) {
   358  		output := map[string]string{
   359  			"/tmp/some/path": "a valid output",
   360  		}
   361  		outputErrors := setOutputsOnClaim(c, output)
   362  		assert.NoError(t, outputErrors)
   363  	})
   364  
   365  	t.Run("a non-string JSON value is still a string", func(t *testing.T) {
   366  		output := map[string]string{
   367  			"/tmp/some/path": "2",
   368  		}
   369  		outputErrors := setOutputsOnClaim(c, output)
   370  		assert.NoError(t, outputErrors)
   371  	})
   372  
   373  	// Types to check here: "null", "boolean", "object", "array", "number", or "integer"
   374  
   375  	// Non strings given a good type should also work
   376  	t.Run("null succeeds", func(t *testing.T) {
   377  		o := c.Bundle.Outputs["some-output"]
   378  		o.Definition = "NullParam"
   379  		c.Bundle.Outputs["some-output"] = o
   380  		output := map[string]string{
   381  			"/tmp/some/path": "null",
   382  		}
   383  		outputErrors := setOutputsOnClaim(c, output)
   384  		assert.NoError(t, outputErrors)
   385  	})
   386  
   387  	t.Run("boolean succeeds", func(t *testing.T) {
   388  		o := c.Bundle.Outputs["some-output"]
   389  		o.Definition = "BooleanParam"
   390  		c.Bundle.Outputs["some-output"] = o
   391  		output := map[string]string{
   392  			"/tmp/some/path": "true",
   393  		}
   394  		outputErrors := setOutputsOnClaim(c, output)
   395  		assert.NoError(t, outputErrors)
   396  	})
   397  
   398  	t.Run("object succeeds", func(t *testing.T) {
   399  		o := c.Bundle.Outputs["some-output"]
   400  		o.Definition = "ObjectParam"
   401  		c.Bundle.Outputs["some-output"] = o
   402  		output := map[string]string{
   403  			"/tmp/some/path": "{}",
   404  		}
   405  		outputErrors := setOutputsOnClaim(c, output)
   406  		assert.NoError(t, outputErrors)
   407  	})
   408  
   409  	t.Run("array succeeds", func(t *testing.T) {
   410  		field := c.Bundle.Outputs["some-output"]
   411  		field.Definition = "ArrayParam"
   412  		c.Bundle.Outputs["some-output"] = field
   413  		output := map[string]string{
   414  			"/tmp/some/path": "[]",
   415  		}
   416  		outputErrors := setOutputsOnClaim(c, output)
   417  		assert.NoError(t, outputErrors)
   418  	})
   419  
   420  	t.Run("number succeeds", func(t *testing.T) {
   421  		field := c.Bundle.Outputs["some-output"]
   422  		field.Definition = "NumberParam"
   423  		c.Bundle.Outputs["some-output"] = field
   424  		output := map[string]string{
   425  			"/tmp/some/path": "3.14",
   426  		}
   427  		outputErrors := setOutputsOnClaim(c, output)
   428  		assert.NoError(t, outputErrors)
   429  	})
   430  
   431  	t.Run("integer as number succeeds", func(t *testing.T) {
   432  		field := c.Bundle.Outputs["some-output"]
   433  		field.Definition = "NumberParam"
   434  		c.Bundle.Outputs["some-output"] = field
   435  		output := map[string]string{
   436  			"/tmp/some/path": "372",
   437  		}
   438  		outputErrors := setOutputsOnClaim(c, output)
   439  		assert.NoError(t, outputErrors)
   440  	})
   441  
   442  	t.Run("integer succeeds", func(t *testing.T) {
   443  		o := c.Bundle.Outputs["some-output"]
   444  		o.Definition = "IntegerParam"
   445  		c.Bundle.Outputs["some-output"] = o
   446  		output := map[string]string{
   447  			"/tmp/some/path": "372",
   448  		}
   449  		outputErrors := setOutputsOnClaim(c, output)
   450  		assert.NoError(t, outputErrors)
   451  	})
   452  }
   453  
   454  func TestSetOutputsOnClaim_MultipleTypes(t *testing.T) {
   455  	c := newClaim()
   456  	c.Bundle = mockBundle()
   457  	o := c.Bundle.Outputs["some-output"]
   458  	o.Definition = "BooleanAndIntegerParam"
   459  	c.Bundle.Outputs["some-output"] = o
   460  
   461  	t.Run("BooleanOrInteger, so boolean succeeds", func(t *testing.T) {
   462  		output := map[string]string{
   463  			"/tmp/some/path": "false",
   464  		}
   465  
   466  		outputErrors := setOutputsOnClaim(c, output)
   467  		assert.NoError(t, outputErrors)
   468  	})
   469  
   470  	t.Run("BooleanOrInteger, so integer succeeds", func(t *testing.T) {
   471  		output := map[string]string{
   472  			"/tmp/some/path": "5",
   473  		}
   474  
   475  		outputErrors := setOutputsOnClaim(c, output)
   476  		assert.NoError(t, outputErrors)
   477  	})
   478  }
   479  
   480  // Tests that strings accept anything even as part of a list of types.
   481  func TestSetOutputsOnClaim_MultipleTypesWithString(t *testing.T) {
   482  	c := newClaim()
   483  	c.Bundle = mockBundle()
   484  	o := c.Bundle.Outputs["some-output"]
   485  	o.Definition = "StringAndBooleanParam"
   486  	c.Bundle.Outputs["some-output"] = o
   487  
   488  	t.Run("null succeeds", func(t *testing.T) {
   489  		output := map[string]string{
   490  			"/tmp/some/path": "null",
   491  		}
   492  		outputErrors := setOutputsOnClaim(c, output)
   493  		assert.NoError(t, outputErrors)
   494  	})
   495  
   496  	t.Run("non-json string succeeds", func(t *testing.T) {
   497  		output := map[string]string{
   498  			"/tmp/some/path": "XYZ is not a JSON value",
   499  		}
   500  		outputErrors := setOutputsOnClaim(c, output)
   501  		assert.NoError(t, outputErrors)
   502  	})
   503  }
   504  
   505  func TestSetOutputsOnClaim_MismatchType(t *testing.T) {
   506  	c := newClaim()
   507  	c.Bundle = mockBundle()
   508  
   509  	o := c.Bundle.Outputs["some-output"]
   510  	o.Definition = "BooleanParam"
   511  	c.Bundle.Outputs["some-output"] = o
   512  
   513  	t.Run("error case: content type does not match output definition", func(t *testing.T) {
   514  		invalidParsableOutput := map[string]string{
   515  			"/tmp/some/path": "2",
   516  		}
   517  
   518  		outputErrors := setOutputsOnClaim(c, invalidParsableOutput)
   519  		assert.EqualError(t, outputErrors, `error: ["some-output" is not any of the expected types (boolean) because it is "integer"]`)
   520  	})
   521  
   522  	t.Run("error case: content is not valid JSON and definition is not string", func(t *testing.T) {
   523  		invalidNonParsableOutput := map[string]string{
   524  			"/tmp/some/path": "Not a boolean",
   525  		}
   526  
   527  		outputErrors := setOutputsOnClaim(c, invalidNonParsableOutput)
   528  		assert.EqualError(t, outputErrors, `error: [failed to parse "some-output": invalid character 'N' looking for beginning of value]`)
   529  	})
   530  }
   531  
   532  func TestSelectInvocationImage_EmptyInvocationImages(t *testing.T) {
   533  	c := &claim.Claim{
   534  		Bundle: &bundle.Bundle{},
   535  	}
   536  	_, err := selectInvocationImage(&driver.DebugDriver{}, c)
   537  	if err == nil {
   538  		t.Fatal("expected an error")
   539  	}
   540  	want := "no invocationImages are defined"
   541  	got := err.Error()
   542  	if !strings.Contains(got, want) {
   543  		t.Fatalf("expected an error containing %q but got %q", want, got)
   544  	}
   545  }
   546  
   547  func TestSelectInvocationImage_DriverIncompatible(t *testing.T) {
   548  	c := &claim.Claim{
   549  		Bundle: mockBundle(),
   550  	}
   551  	_, err := selectInvocationImage(&mockDriver{Error: errors.New("I always fail")}, c)
   552  	if err == nil {
   553  		t.Fatal("expected an error")
   554  	}
   555  	want := "driver is not compatible"
   556  	got := err.Error()
   557  	if !strings.Contains(got, want) {
   558  		t.Fatalf("expected an error containing %q but got %q", want, got)
   559  	}
   560  }