github.com/ablease/cli@v6.37.1-0.20180613014814-3adbb7d7fb19+incompatible/api/cloudcontroller/ccv3/application_test.go (about) 1 package ccv3_test 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "net/http" 7 8 "code.cloudfoundry.org/cli/api/cloudcontroller/ccerror" 9 . "code.cloudfoundry.org/cli/api/cloudcontroller/ccv3" 10 "code.cloudfoundry.org/cli/api/cloudcontroller/ccv3/constant" 11 . "github.com/onsi/ginkgo" 12 . "github.com/onsi/gomega" 13 . "github.com/onsi/gomega/ghttp" 14 ) 15 16 var _ = Describe("Application", func() { 17 var client *Client 18 19 BeforeEach(func() { 20 client = NewTestClient() 21 }) 22 23 Describe("Application", func() { 24 Describe("MarshalJSON", func() { 25 var ( 26 app Application 27 appBytes []byte 28 err error 29 ) 30 31 BeforeEach(func() { 32 app = Application{} 33 }) 34 35 JustBeforeEach(func() { 36 appBytes, err = app.MarshalJSON() 37 Expect(err).ToNot(HaveOccurred()) 38 }) 39 40 Context("when no lifecycle is provided", func() { 41 BeforeEach(func() { 42 app = Application{} 43 }) 44 45 It("omits the lifecycle from the JSON", func() { 46 Expect(string(appBytes)).To(Equal("{}")) 47 }) 48 }) 49 50 Context("when lifecycle type docker is provided", func() { 51 BeforeEach(func() { 52 app = Application{ 53 LifecycleType: constant.AppLifecycleTypeDocker, 54 } 55 }) 56 57 It("sets lifecycle type to docker with empty data", func() { 58 Expect(string(appBytes)).To(MatchJSON(`{"lifecycle":{"type":"docker","data":{}}}`)) 59 }) 60 }) 61 62 Context("when lifecycle type buildpack is provided", func() { 63 BeforeEach(func() { 64 app.LifecycleType = constant.AppLifecycleTypeBuildpack 65 }) 66 67 Context("when no buildpacks are provided", func() { 68 It("omits the lifecycle from the JSON", func() { 69 Expect(string(appBytes)).To(Equal("{}")) 70 }) 71 }) 72 73 Context("when default buildpack is provided", func() { 74 BeforeEach(func() { 75 app.LifecycleBuildpacks = []string{"default"} 76 }) 77 78 It("sets the lifecycle buildpack to be empty in the JSON", func() { 79 Expect(string(appBytes)).To(Equal(`{"lifecycle":{"data":{"buildpacks":null},"type":"buildpack"}}`)) 80 }) 81 }) 82 83 Context("when null buildpack is provided", func() { 84 BeforeEach(func() { 85 app.LifecycleBuildpacks = []string{"null"} 86 }) 87 88 It("sets the Lifecycle buildpack to be empty in the JSON", func() { 89 Expect(string(appBytes)).To(Equal(`{"lifecycle":{"data":{"buildpacks":null},"type":"buildpack"}}`)) 90 }) 91 }) 92 93 Context("when other buildpacks are provided", func() { 94 BeforeEach(func() { 95 app.LifecycleBuildpacks = []string{"some-buildpack"} 96 }) 97 98 It("sets them in the JSON", func() { 99 Expect(string(appBytes)).To(Equal(`{"lifecycle":{"data":{"buildpacks":["some-buildpack"]},"type":"buildpack"}}`)) 100 }) 101 }) 102 }) 103 }) 104 105 Describe("UnmarshalJSON", func() { 106 var ( 107 app Application 108 appBytes []byte 109 err error 110 ) 111 112 BeforeEach(func() { 113 appBytes = []byte("{}") 114 }) 115 116 JustBeforeEach(func() { 117 err = json.Unmarshal(appBytes, &app) 118 Expect(err).ToNot(HaveOccurred()) 119 }) 120 121 Context("when no lifecycle is provided", func() { 122 BeforeEach(func() { 123 appBytes = []byte("{}") 124 }) 125 126 It("omits the lifecycle from the JSON", func() { 127 Expect(app).To(Equal(Application{})) 128 }) 129 }) 130 131 Context("when lifecycle type docker is provided", func() { 132 BeforeEach(func() { 133 appBytes = []byte(`{"lifecycle":{"type":"docker","data":{}}}`) 134 }) 135 It("sets the lifecycle type to docker with empty data", func() { 136 Expect(app).To(Equal(Application{ 137 LifecycleType: constant.AppLifecycleTypeDocker, 138 })) 139 }) 140 }) 141 142 Context("when lifecycle type buildpack is provided", func() { 143 144 Context("when other buildpacks are provided", func() { 145 BeforeEach(func() { 146 appBytes = []byte(`{"lifecycle":{"data":{"buildpacks":["some-buildpack"]},"type":"buildpack"}}`) 147 }) 148 149 It("sets them in the JSON", func() { 150 Expect(app).To(Equal(Application{ 151 LifecycleType: constant.AppLifecycleTypeBuildpack, 152 LifecycleBuildpacks: []string{"some-buildpack"}, 153 })) 154 }) 155 }) 156 }) 157 }) 158 }) 159 160 Describe("CreateApplication", func() { 161 var ( 162 appToCreate Application 163 164 createdApp Application 165 warnings Warnings 166 executeErr error 167 ) 168 169 JustBeforeEach(func() { 170 createdApp, warnings, executeErr = client.CreateApplication(appToCreate) 171 }) 172 173 Context("when the application successfully is created", func() { 174 BeforeEach(func() { 175 response := `{ 176 "guid": "some-app-guid", 177 "name": "some-app-name" 178 }` 179 180 expectedBody := map[string]interface{}{ 181 "name": "some-app-name", 182 "relationships": map[string]interface{}{ 183 "space": map[string]interface{}{ 184 "data": map[string]string{ 185 "guid": "some-space-guid", 186 }, 187 }, 188 }, 189 } 190 server.AppendHandlers( 191 CombineHandlers( 192 VerifyRequest(http.MethodPost, "/v3/apps"), 193 VerifyJSONRepresenting(expectedBody), 194 RespondWith(http.StatusCreated, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 195 ), 196 ) 197 198 appToCreate = Application{ 199 Name: "some-app-name", 200 Relationships: Relationships{ 201 constant.RelationshipTypeSpace: Relationship{GUID: "some-space-guid"}, 202 }, 203 } 204 }) 205 206 It("returns the created app and warnings", func() { 207 Expect(executeErr).NotTo(HaveOccurred()) 208 Expect(warnings).To(ConsistOf("this is a warning")) 209 210 Expect(createdApp).To(Equal(Application{ 211 Name: "some-app-name", 212 GUID: "some-app-guid", 213 })) 214 }) 215 }) 216 217 Context("when the caller specifies a buildpack", func() { 218 BeforeEach(func() { 219 response := `{ 220 "guid": "some-app-guid", 221 "name": "some-app-name", 222 "lifecycle": { 223 "type": "buildpack", 224 "data": { 225 "buildpacks": ["some-buildpack"] 226 } 227 } 228 }` 229 230 expectedBody := map[string]interface{}{ 231 "name": "some-app-name", 232 "lifecycle": map[string]interface{}{ 233 "type": "buildpack", 234 "data": map[string]interface{}{ 235 "buildpacks": []string{"some-buildpack"}, 236 }, 237 }, 238 "relationships": map[string]interface{}{ 239 "space": map[string]interface{}{ 240 "data": map[string]string{ 241 "guid": "some-space-guid", 242 }, 243 }, 244 }, 245 } 246 server.AppendHandlers( 247 CombineHandlers( 248 VerifyRequest(http.MethodPost, "/v3/apps"), 249 VerifyJSONRepresenting(expectedBody), 250 RespondWith(http.StatusCreated, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 251 ), 252 ) 253 254 appToCreate = Application{ 255 Name: "some-app-name", 256 LifecycleType: constant.AppLifecycleTypeBuildpack, 257 LifecycleBuildpacks: []string{"some-buildpack"}, 258 Relationships: Relationships{ 259 constant.RelationshipTypeSpace: Relationship{GUID: "some-space-guid"}, 260 }, 261 } 262 }) 263 264 It("returns the created app and warnings", func() { 265 Expect(executeErr).NotTo(HaveOccurred()) 266 Expect(warnings).To(ConsistOf("this is a warning")) 267 268 Expect(createdApp).To(Equal(Application{ 269 Name: "some-app-name", 270 GUID: "some-app-guid", 271 LifecycleType: constant.AppLifecycleTypeBuildpack, 272 LifecycleBuildpacks: []string{"some-buildpack"}, 273 })) 274 }) 275 }) 276 277 Context("when cc returns back an error or warnings", func() { 278 BeforeEach(func() { 279 response := `{ 280 "errors": [ 281 { 282 "code": 10008, 283 "detail": "The request is semantically invalid: command presence", 284 "title": "CF-UnprocessableEntity" 285 }, 286 { 287 "code": 10010, 288 "detail": "App not found", 289 "title": "CF-ResourceNotFound" 290 } 291 ] 292 }` 293 server.AppendHandlers( 294 CombineHandlers( 295 VerifyRequest(http.MethodPost, "/v3/apps"), 296 RespondWith(http.StatusTeapot, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 297 ), 298 ) 299 }) 300 301 It("returns the error and all warnings", func() { 302 Expect(executeErr).To(MatchError(ccerror.MultiError{ 303 ResponseCode: http.StatusTeapot, 304 Errors: []ccerror.V3Error{ 305 { 306 Code: 10008, 307 Detail: "The request is semantically invalid: command presence", 308 Title: "CF-UnprocessableEntity", 309 }, 310 { 311 Code: 10010, 312 Detail: "App not found", 313 Title: "CF-ResourceNotFound", 314 }, 315 }, 316 })) 317 Expect(warnings).To(ConsistOf("this is a warning")) 318 }) 319 }) 320 }) 321 322 Describe("GetApplications", func() { 323 var ( 324 filters []Query 325 326 apps []Application 327 warnings Warnings 328 executeErr error 329 ) 330 331 JustBeforeEach(func() { 332 apps, warnings, executeErr = client.GetApplications(filters...) 333 }) 334 335 Context("when applications exist", func() { 336 BeforeEach(func() { 337 response1 := fmt.Sprintf(`{ 338 "pagination": { 339 "next": { 340 "href": "%s/v3/apps?space_guids=some-space-guid&names=some-app-name&page=2&per_page=2" 341 } 342 }, 343 "resources": [ 344 { 345 "name": "app-name-1", 346 "guid": "app-guid-1", 347 "lifecycle": { 348 "type": "buildpack", 349 "data": { 350 "buildpacks": ["some-buildpack"], 351 "stack": "some-stack" 352 } 353 } 354 }, 355 { 356 "name": "app-name-2", 357 "guid": "app-guid-2" 358 } 359 ] 360 }`, server.URL()) 361 response2 := `{ 362 "pagination": { 363 "next": null 364 }, 365 "resources": [ 366 { 367 "name": "app-name-3", 368 "guid": "app-guid-3" 369 } 370 ] 371 }` 372 server.AppendHandlers( 373 CombineHandlers( 374 VerifyRequest(http.MethodGet, "/v3/apps", "space_guids=some-space-guid&names=some-app-name"), 375 RespondWith(http.StatusOK, response1, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 376 ), 377 ) 378 server.AppendHandlers( 379 CombineHandlers( 380 VerifyRequest(http.MethodGet, "/v3/apps", "space_guids=some-space-guid&names=some-app-name&page=2&per_page=2"), 381 RespondWith(http.StatusOK, response2, http.Header{"X-Cf-Warnings": {"this is another warning"}}), 382 ), 383 ) 384 385 filters = []Query{ 386 {Key: SpaceGUIDFilter, Values: []string{"some-space-guid"}}, 387 {Key: NameFilter, Values: []string{"some-app-name"}}, 388 } 389 }) 390 391 It("returns the queried applications and all warnings", func() { 392 Expect(executeErr).NotTo(HaveOccurred()) 393 Expect(warnings).To(ConsistOf("this is a warning", "this is another warning")) 394 395 Expect(apps).To(ConsistOf( 396 Application{ 397 Name: "app-name-1", 398 GUID: "app-guid-1", 399 LifecycleType: constant.AppLifecycleTypeBuildpack, 400 LifecycleBuildpacks: []string{"some-buildpack"}, 401 }, 402 Application{Name: "app-name-2", GUID: "app-guid-2"}, 403 Application{Name: "app-name-3", GUID: "app-guid-3"}, 404 )) 405 }) 406 }) 407 408 Context("when the cloud controller returns errors and warnings", func() { 409 BeforeEach(func() { 410 response := `{ 411 "errors": [ 412 { 413 "code": 10008, 414 "detail": "The request is semantically invalid: command presence", 415 "title": "CF-UnprocessableEntity" 416 }, 417 { 418 "code": 10010, 419 "detail": "App not found", 420 "title": "CF-ResourceNotFound" 421 } 422 ] 423 }` 424 server.AppendHandlers( 425 CombineHandlers( 426 VerifyRequest(http.MethodGet, "/v3/apps"), 427 RespondWith(http.StatusTeapot, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 428 ), 429 ) 430 }) 431 432 It("returns the error and all warnings", func() { 433 Expect(executeErr).To(MatchError(ccerror.MultiError{ 434 ResponseCode: http.StatusTeapot, 435 Errors: []ccerror.V3Error{ 436 { 437 Code: 10008, 438 Detail: "The request is semantically invalid: command presence", 439 Title: "CF-UnprocessableEntity", 440 }, 441 { 442 Code: 10010, 443 Detail: "App not found", 444 Title: "CF-ResourceNotFound", 445 }, 446 }, 447 })) 448 Expect(warnings).To(ConsistOf("this is a warning")) 449 }) 450 }) 451 }) 452 453 Describe("UpdateApplication", func() { 454 var ( 455 appToUpdate Application 456 457 updatedApp Application 458 warnings Warnings 459 executeErr error 460 ) 461 462 JustBeforeEach(func() { 463 updatedApp, warnings, executeErr = client.UpdateApplication(appToUpdate) 464 }) 465 466 Context("when the application successfully is updated", func() { 467 BeforeEach(func() { 468 response := `{ 469 "guid": "some-app-guid", 470 "name": "some-app-name", 471 "lifecycle": { 472 "type": "buildpack", 473 "data": { 474 "buildpacks": ["some-buildpack"] 475 } 476 } 477 }` 478 479 expectedBody := map[string]interface{}{ 480 "name": "some-app-name", 481 "lifecycle": map[string]interface{}{ 482 "type": "buildpack", 483 "data": map[string]interface{}{ 484 "buildpacks": []string{"some-buildpack"}, 485 }, 486 }, 487 "relationships": map[string]interface{}{ 488 "space": map[string]interface{}{ 489 "data": map[string]string{ 490 "guid": "some-space-guid", 491 }, 492 }, 493 }, 494 } 495 server.AppendHandlers( 496 CombineHandlers( 497 VerifyRequest(http.MethodPatch, "/v3/apps/some-app-guid"), 498 VerifyJSONRepresenting(expectedBody), 499 RespondWith(http.StatusOK, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 500 ), 501 ) 502 503 appToUpdate = Application{ 504 GUID: "some-app-guid", 505 Name: "some-app-name", 506 LifecycleType: constant.AppLifecycleTypeBuildpack, 507 LifecycleBuildpacks: []string{"some-buildpack"}, 508 Relationships: Relationships{ 509 constant.RelationshipTypeSpace: Relationship{GUID: "some-space-guid"}, 510 }, 511 } 512 }) 513 514 It("returns the updated app and warnings", func() { 515 Expect(executeErr).NotTo(HaveOccurred()) 516 Expect(warnings).To(ConsistOf("this is a warning")) 517 518 Expect(updatedApp).To(Equal(Application{ 519 GUID: "some-app-guid", 520 LifecycleBuildpacks: []string{"some-buildpack"}, 521 LifecycleType: constant.AppLifecycleTypeBuildpack, 522 Name: "some-app-name", 523 })) 524 }) 525 }) 526 527 Context("when cc returns back an error or warnings", func() { 528 BeforeEach(func() { 529 response := `{ 530 "errors": [ 531 { 532 "code": 10008, 533 "detail": "The request is semantically invalid: command presence", 534 "title": "CF-UnprocessableEntity" 535 }, 536 { 537 "code": 10010, 538 "detail": "App not found", 539 "title": "CF-ResourceNotFound" 540 } 541 ] 542 }` 543 server.AppendHandlers( 544 CombineHandlers( 545 VerifyRequest(http.MethodPatch, "/v3/apps/some-app-guid"), 546 RespondWith(http.StatusTeapot, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 547 ), 548 ) 549 550 appToUpdate = Application{ 551 GUID: "some-app-guid", 552 } 553 }) 554 555 It("returns the error and all warnings", func() { 556 Expect(executeErr).To(MatchError(ccerror.MultiError{ 557 ResponseCode: http.StatusTeapot, 558 Errors: []ccerror.V3Error{ 559 { 560 Code: 10008, 561 Detail: "The request is semantically invalid: command presence", 562 Title: "CF-UnprocessableEntity", 563 }, 564 { 565 Code: 10010, 566 Detail: "App not found", 567 Title: "CF-ResourceNotFound", 568 }, 569 }, 570 })) 571 Expect(warnings).To(ConsistOf("this is a warning")) 572 }) 573 }) 574 }) 575 576 Describe("UpdateApplicationStop", func() { 577 var ( 578 responseApp Application 579 warnings Warnings 580 executeErr error 581 ) 582 583 JustBeforeEach(func() { 584 responseApp, warnings, executeErr = client.UpdateApplicationStop("some-app-guid") 585 }) 586 587 Context("when the response succeeds", func() { 588 BeforeEach(func() { 589 response := ` 590 { 591 "guid": "some-app-guid", 592 "name": "some-app", 593 "state": "STOPPED" 594 }` 595 server.AppendHandlers( 596 CombineHandlers( 597 VerifyRequest(http.MethodPost, "/v3/apps/some-app-guid/actions/stop"), 598 RespondWith(http.StatusOK, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 599 ), 600 ) 601 }) 602 603 It("returns the application, warnings, and no error", func() { 604 Expect(responseApp).To(Equal(Application{ 605 GUID: "some-app-guid", 606 Name: "some-app", 607 State: constant.ApplicationStopped, 608 })) 609 Expect(executeErr).ToNot(HaveOccurred()) 610 Expect(warnings).To(ConsistOf("this is a warning")) 611 }) 612 }) 613 614 Context("when the CC returns an error", func() { 615 BeforeEach(func() { 616 response := `{ 617 "errors": [ 618 { 619 "code": 10008, 620 "detail": "The request is semantically invalid: command presence", 621 "title": "CF-UnprocessableEntity" 622 }, 623 { 624 "code": 10010, 625 "detail": "App not found", 626 "title": "CF-ResourceNotFound" 627 } 628 ] 629 }` 630 server.AppendHandlers( 631 CombineHandlers( 632 VerifyRequest(http.MethodPost, "/v3/apps/some-app-guid/actions/stop"), 633 RespondWith(http.StatusTeapot, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 634 ), 635 ) 636 }) 637 638 It("returns no app, the error and all warnings", func() { 639 Expect(executeErr).To(MatchError(ccerror.MultiError{ 640 ResponseCode: http.StatusTeapot, 641 Errors: []ccerror.V3Error{ 642 { 643 Code: 10008, 644 Detail: "The request is semantically invalid: command presence", 645 Title: "CF-UnprocessableEntity", 646 }, 647 { 648 Code: 10010, 649 Detail: "App not found", 650 Title: "CF-ResourceNotFound", 651 }, 652 }, 653 })) 654 Expect(warnings).To(ConsistOf("this is a warning")) 655 }) 656 }) 657 }) 658 659 Describe("UpdateApplicationStart", func() { 660 var ( 661 app Application 662 warnings Warnings 663 executeErr error 664 ) 665 666 JustBeforeEach(func() { 667 app, warnings, executeErr = client.UpdateApplicationStart("some-app-guid") 668 }) 669 670 Context("when the response succeeds", func() { 671 BeforeEach(func() { 672 response := ` 673 { 674 "guid": "some-app-guid", 675 "name": "some-app" 676 }` 677 server.AppendHandlers( 678 CombineHandlers( 679 VerifyRequest(http.MethodPost, "/v3/apps/some-app-guid/actions/start"), 680 RespondWith(http.StatusOK, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 681 ), 682 ) 683 }) 684 685 It("returns warnings and no error", func() { 686 Expect(executeErr).ToNot(HaveOccurred()) 687 Expect(warnings).To(ConsistOf("this is a warning")) 688 Expect(app.GUID).To(Equal("some-app-guid")) 689 }) 690 }) 691 692 Context("when cc returns back an error or warnings", func() { 693 BeforeEach(func() { 694 response := `{ 695 "errors": [ 696 { 697 "code": 10008, 698 "detail": "The request is semantically invalid: command presence", 699 "title": "CF-UnprocessableEntity" 700 }, 701 { 702 "code": 10010, 703 "detail": "App not found", 704 "title": "CF-ResourceNotFound" 705 } 706 ] 707 }` 708 server.AppendHandlers( 709 CombineHandlers( 710 VerifyRequest(http.MethodPost, "/v3/apps/some-app-guid/actions/start"), 711 RespondWith(http.StatusTeapot, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 712 ), 713 ) 714 }) 715 716 It("returns the error and all warnings", func() { 717 Expect(executeErr).To(MatchError(ccerror.MultiError{ 718 ResponseCode: http.StatusTeapot, 719 Errors: []ccerror.V3Error{ 720 { 721 Code: 10008, 722 Detail: "The request is semantically invalid: command presence", 723 Title: "CF-UnprocessableEntity", 724 }, 725 { 726 Code: 10010, 727 Detail: "App not found", 728 Title: "CF-ResourceNotFound", 729 }, 730 }, 731 })) 732 Expect(warnings).To(ConsistOf("this is a warning")) 733 }) 734 }) 735 }) 736 })