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 }