github.com/sleungcy/cli@v7.1.0+incompatible/api/cloudcontroller/ccv3/requester_test.go (about) 1 package ccv3_test 2 3 import ( 4 "errors" 5 "fmt" 6 "io" 7 "net/http" 8 "strings" 9 10 "code.cloudfoundry.org/cli/api/cloudcontroller/ccerror" 11 . "code.cloudfoundry.org/cli/api/cloudcontroller/ccv3" 12 "code.cloudfoundry.org/cli/api/cloudcontroller/ccv3/constant" 13 "code.cloudfoundry.org/cli/api/cloudcontroller/ccv3/internal" 14 "code.cloudfoundry.org/cli/resources" 15 . "code.cloudfoundry.org/cli/resources" 16 "code.cloudfoundry.org/cli/types" 17 . "github.com/onsi/ginkgo" 18 . "github.com/onsi/gomega" 19 . "github.com/onsi/gomega/ghttp" 20 ) 21 22 var _ = Describe("shared request helpers", func() { 23 var client *Client 24 25 BeforeEach(func() { 26 client, _ = NewTestClient() 27 }) 28 29 Describe("MakeRequest", func() { 30 var ( 31 requestParams RequestParams 32 33 jobURL JobURL 34 warnings Warnings 35 executeErr error 36 ) 37 38 BeforeEach(func() { 39 requestParams = RequestParams{} 40 }) 41 42 JustBeforeEach(func() { 43 jobURL, warnings, executeErr = client.MakeRequest(requestParams) 44 }) 45 46 Context("GET single resource", func() { 47 var ( 48 responseBody Organization 49 ) 50 51 BeforeEach(func() { 52 requestParams = RequestParams{ 53 RequestName: internal.GetOrganizationRequest, 54 URIParams: internal.Params{"organization_guid": "some-org-guid"}, 55 ResponseBody: &responseBody, 56 } 57 }) 58 59 When("organization exists", func() { 60 BeforeEach(func() { 61 response := `{ 62 "name": "some-org-name", 63 "guid": "some-org-guid", 64 "relationships": { 65 "quota": { 66 "data": { 67 "guid": "some-org-quota-guid" 68 } 69 } 70 } 71 }` 72 73 server.AppendHandlers( 74 CombineHandlers( 75 VerifyRequest(http.MethodGet, "/v3/organizations/some-org-guid"), 76 RespondWith(http.StatusOK, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 77 ), 78 ) 79 }) 80 81 It("returns the queried organization and all warnings", func() { 82 Expect(executeErr).NotTo(HaveOccurred()) 83 Expect(responseBody).To(Equal(Organization{ 84 Name: "some-org-name", 85 GUID: "some-org-guid", 86 QuotaGUID: "some-org-quota-guid", 87 })) 88 Expect(warnings).To(ConsistOf("this is a warning")) 89 }) 90 }) 91 92 When("the cloud controller returns errors and warnings", func() { 93 BeforeEach(func() { 94 response := `{ 95 "errors": [ 96 { 97 "code": 10008, 98 "detail": "The request is semantically invalid: command presence", 99 "title": "CF-UnprocessableEntity" 100 }, 101 { 102 "code": 10010, 103 "detail": "Org not found", 104 "title": "CF-ResourceNotFound" 105 } 106 ] 107 }` 108 109 server.AppendHandlers( 110 CombineHandlers( 111 VerifyRequest(http.MethodGet, "/v3/organizations/some-org-guid"), 112 RespondWith(http.StatusTeapot, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 113 ), 114 ) 115 }) 116 117 It("returns the error and all warnings", func() { 118 Expect(executeErr).To(MatchError(ccerror.MultiError{ 119 ResponseCode: http.StatusTeapot, 120 Errors: []ccerror.V3Error{ 121 { 122 Code: 10008, 123 Detail: "The request is semantically invalid: command presence", 124 Title: "CF-UnprocessableEntity", 125 }, 126 { 127 Code: 10010, 128 Detail: "Org not found", 129 Title: "CF-ResourceNotFound", 130 }, 131 }, 132 })) 133 Expect(warnings).To(ConsistOf("this is a warning")) 134 }) 135 }) 136 }) 137 138 Context("POST resource", func() { 139 var ( 140 requestBody Buildpack 141 responseBody Buildpack 142 ) 143 144 BeforeEach(func() { 145 requestBody = Buildpack{ 146 Name: "some-buildpack", 147 Stack: "some-stack", 148 } 149 150 requestParams = RequestParams{ 151 RequestName: internal.PostBuildpackRequest, 152 RequestBody: requestBody, 153 ResponseBody: &responseBody, 154 } 155 }) 156 157 When("the resource is successfully created", func() { 158 BeforeEach(func() { 159 response := `{ 160 "guid": "some-bp-guid", 161 "created_at": "2016-03-18T23:26:46Z", 162 "updated_at": "2016-10-17T20:00:42Z", 163 "name": "some-buildpack", 164 "state": "AWAITING_UPLOAD", 165 "filename": null, 166 "stack": "some-stack", 167 "position": 42, 168 "enabled": true, 169 "locked": false, 170 "links": { 171 "self": { 172 "href": "/v3/buildpacks/some-bp-guid" 173 }, 174 "upload": { 175 "href": "/v3/buildpacks/some-bp-guid/upload", 176 "method": "POST" 177 } 178 } 179 }` 180 181 expectedBody := map[string]interface{}{ 182 "name": "some-buildpack", 183 "stack": "some-stack", 184 } 185 186 server.AppendHandlers( 187 CombineHandlers( 188 VerifyRequest(http.MethodPost, "/v3/buildpacks"), 189 VerifyJSONRepresenting(expectedBody), 190 RespondWith(http.StatusCreated, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 191 ), 192 ) 193 }) 194 195 It("returns the resource and warnings", func() { 196 Expect(jobURL).To(Equal(JobURL(""))) 197 Expect(executeErr).NotTo(HaveOccurred()) 198 Expect(warnings).To(ConsistOf("this is a warning")) 199 200 expectedBuildpack := Buildpack{ 201 GUID: "some-bp-guid", 202 Name: "some-buildpack", 203 Stack: "some-stack", 204 Enabled: types.NullBool{Value: true, IsSet: true}, 205 Filename: "", 206 Locked: types.NullBool{Value: false, IsSet: true}, 207 State: constant.BuildpackAwaitingUpload, 208 Position: types.NullInt{Value: 42, IsSet: true}, 209 Links: APILinks{ 210 "upload": APILink{ 211 Method: "POST", 212 HREF: "/v3/buildpacks/some-bp-guid/upload", 213 }, 214 "self": APILink{ 215 HREF: "/v3/buildpacks/some-bp-guid", 216 }, 217 }, 218 } 219 220 Expect(responseBody).To(Equal(expectedBuildpack)) 221 }) 222 }) 223 224 When("the resource returns all errors and warnings", func() { 225 BeforeEach(func() { 226 response := ` { 227 "errors": [ 228 { 229 "code": 10008, 230 "detail": "The request is semantically invalid: command presence", 231 "title": "CF-UnprocessableEntity" 232 }, 233 { 234 "code": 10010, 235 "detail": "Buildpack not found", 236 "title": "CF-ResourceNotFound" 237 } 238 ] 239 }` 240 server.AppendHandlers( 241 CombineHandlers( 242 VerifyRequest(http.MethodPost, "/v3/buildpacks"), 243 RespondWith(http.StatusTeapot, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 244 ), 245 ) 246 }) 247 248 It("returns the error and all warnings", func() { 249 Expect(executeErr).To(MatchError(ccerror.MultiError{ 250 ResponseCode: http.StatusTeapot, 251 Errors: []ccerror.V3Error{ 252 { 253 Code: 10008, 254 Detail: "The request is semantically invalid: command presence", 255 Title: "CF-UnprocessableEntity", 256 }, 257 { 258 Code: 10010, 259 Detail: "Buildpack not found", 260 Title: "CF-ResourceNotFound", 261 }, 262 }, 263 })) 264 Expect(warnings).To(ConsistOf("this is a warning")) 265 }) 266 }) 267 }) 268 269 Context("DELETE resource", func() { 270 BeforeEach(func() { 271 requestParams = RequestParams{ 272 RequestName: internal.DeleteSpaceRequest, 273 URIParams: internal.Params{"space_guid": "space-guid"}, 274 } 275 }) 276 277 When("no errors are encountered", func() { 278 BeforeEach(func() { 279 280 server.AppendHandlers( 281 CombineHandlers( 282 VerifyRequest(http.MethodDelete, "/v3/spaces/space-guid"), 283 RespondWith(http.StatusAccepted, nil, http.Header{"X-Cf-Warnings": {"warning-1, warning-2"}, "Location": []string{"job-url"}}), 284 )) 285 }) 286 287 It("deletes the Space and returns all warnings", func() { 288 Expect(executeErr).NotTo(HaveOccurred()) 289 Expect(warnings).To(ConsistOf(Warnings{"warning-1", "warning-2"})) 290 Expect(jobURL).To(Equal(JobURL("job-url"))) 291 }) 292 }) 293 294 When("an error is encountered", func() { 295 BeforeEach(func() { 296 response := `{ 297 "errors": [ 298 { 299 "detail": "Space not found", 300 "title": "CF-ResourceNotFound", 301 "code": 10010 302 } 303 ] 304 }` 305 server.AppendHandlers( 306 CombineHandlers( 307 VerifyRequest(http.MethodDelete, "/v3/spaces/space-guid"), 308 RespondWith(http.StatusNotFound, response, http.Header{"X-Cf-Warnings": {"warning-1, warning-2"}}), 309 )) 310 }) 311 312 It("returns an error and all warnings", func() { 313 Expect(executeErr).To(MatchError(ccerror.ResourceNotFoundError{ 314 Message: "Space not found", 315 })) 316 Expect(warnings).To(ConsistOf(Warnings{"warning-1", "warning-2"})) 317 }) 318 }) 319 }) 320 321 Context("PATCH resource", func() { 322 var ( 323 responseBody resources.Application 324 ) 325 326 BeforeEach(func() { 327 requestBody := resources.Application{ 328 GUID: "some-app-guid", 329 Name: "some-app-name", 330 StackName: "some-stack-name", 331 LifecycleType: constant.AppLifecycleTypeBuildpack, 332 LifecycleBuildpacks: []string{"some-buildpack"}, 333 SpaceGUID: "some-space-guid", 334 } 335 requestParams = RequestParams{ 336 RequestName: internal.PatchApplicationRequest, 337 URIParams: internal.Params{"app_guid": requestBody.GUID}, 338 RequestBody: requestBody, 339 ResponseBody: &responseBody, 340 } 341 342 }) 343 344 When("the application successfully is updated", func() { 345 BeforeEach(func() { 346 347 response := `{ 348 "guid": "some-app-guid", 349 "name": "some-app-name", 350 "lifecycle": { 351 "type": "buildpack", 352 "data": { 353 "buildpacks": ["some-buildpack"], 354 "stack": "some-stack-name" 355 } 356 } 357 }` 358 359 expectedBody := map[string]interface{}{ 360 "name": "some-app-name", 361 "lifecycle": map[string]interface{}{ 362 "type": "buildpack", 363 "data": map[string]interface{}{ 364 "buildpacks": []string{"some-buildpack"}, 365 "stack": "some-stack-name", 366 }, 367 }, 368 "relationships": map[string]interface{}{ 369 "space": map[string]interface{}{ 370 "data": map[string]string{ 371 "guid": "some-space-guid", 372 }, 373 }, 374 }, 375 } 376 server.AppendHandlers( 377 CombineHandlers( 378 VerifyRequest(http.MethodPatch, "/v3/apps/some-app-guid"), 379 VerifyJSONRepresenting(expectedBody), 380 RespondWith(http.StatusOK, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 381 ), 382 ) 383 }) 384 385 It("returns the updated app and warnings", func() { 386 Expect(executeErr).NotTo(HaveOccurred()) 387 Expect(warnings).To(ConsistOf("this is a warning")) 388 389 Expect(responseBody).To(Equal(resources.Application{ 390 GUID: "some-app-guid", 391 StackName: "some-stack-name", 392 LifecycleBuildpacks: []string{"some-buildpack"}, 393 LifecycleType: constant.AppLifecycleTypeBuildpack, 394 Name: "some-app-name", 395 })) 396 }) 397 }) 398 399 When("cc returns back an error or warnings", func() { 400 BeforeEach(func() { 401 response := `{ 402 "errors": [ 403 { 404 "code": 10008, 405 "detail": "The request is semantically invalid: command presence", 406 "title": "CF-UnprocessableEntity" 407 }, 408 { 409 "code": 10010, 410 "detail": "App not found", 411 "title": "CF-ResourceNotFound" 412 } 413 ] 414 }` 415 server.AppendHandlers( 416 CombineHandlers( 417 VerifyRequest(http.MethodPatch, "/v3/apps/some-app-guid"), 418 RespondWith(http.StatusTeapot, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 419 ), 420 ) 421 }) 422 423 It("returns the error and all warnings", func() { 424 Expect(executeErr).To(MatchError(ccerror.MultiError{ 425 ResponseCode: http.StatusTeapot, 426 Errors: []ccerror.V3Error{ 427 { 428 Code: 10008, 429 Detail: "The request is semantically invalid: command presence", 430 Title: "CF-UnprocessableEntity", 431 }, 432 { 433 Code: 10010, 434 Detail: "App not found", 435 Title: "CF-ResourceNotFound", 436 }, 437 }, 438 })) 439 Expect(warnings).To(ConsistOf("this is a warning")) 440 }) 441 }) 442 }) 443 }) 444 445 Describe("MakeListRequest", func() { 446 var ( 447 requestParams RequestParams 448 449 includedResources IncludedResources 450 warnings Warnings 451 executeErr error 452 ) 453 454 JustBeforeEach(func() { 455 includedResources, warnings, executeErr = client.MakeListRequest(requestParams) 456 }) 457 458 Context("with query params and included resources", func() { 459 var ( 460 resourceList []resources.Role 461 query []Query 462 ) 463 464 BeforeEach(func() { 465 resourceList = []resources.Role{} 466 query = []Query{ 467 { 468 Key: OrganizationGUIDFilter, 469 Values: []string{"some-org-name"}, 470 }, 471 { 472 Key: Include, 473 Values: []string{"users"}, 474 }, 475 } 476 requestParams = RequestParams{ 477 RequestName: internal.GetRolesRequest, 478 Query: query, 479 ResponseBody: resources.Role{}, 480 AppendToList: func(item interface{}) error { 481 resourceList = append(resourceList, item.(resources.Role)) 482 return nil 483 }, 484 } 485 }) 486 487 When("the request succeeds", func() { 488 BeforeEach(func() { 489 response1 := fmt.Sprintf(`{ 490 "pagination": { 491 "next": { 492 "href": "%s/v3/roles?organization_guids=some-org-name&page=2&per_page=1&include=users" 493 } 494 }, 495 "resources": [ 496 { 497 "guid": "role-guid-1", 498 "type": "organization_user" 499 } 500 ] 501 }`, server.URL()) 502 response2 := `{ 503 "pagination": { 504 "next": null 505 }, 506 "resources": [ 507 { 508 "guid": "role-guid-2", 509 "type": "organization_manager" 510 } 511 ] 512 }` 513 514 server.AppendHandlers( 515 CombineHandlers( 516 VerifyRequest(http.MethodGet, "/v3/roles", "organization_guids=some-org-name&include=users"), 517 RespondWith(http.StatusOK, response1, http.Header{"X-Cf-Warnings": {"warning-1"}}), 518 ), 519 ) 520 server.AppendHandlers( 521 CombineHandlers( 522 VerifyRequest(http.MethodGet, "/v3/roles", "organization_guids=some-org-name&page=2&per_page=1&include=users"), 523 RespondWith(http.StatusOK, response2, http.Header{"X-Cf-Warnings": {"warning-2"}}), 524 ), 525 ) 526 }) 527 528 It("returns the given resources and all warnings", func() { 529 Expect(executeErr).ToNot(HaveOccurred()) 530 Expect(warnings).To(ConsistOf("warning-1", "warning-2")) 531 Expect(resourceList).To(Equal([]resources.Role{{ 532 GUID: "role-guid-1", 533 Type: constant.OrgUserRole, 534 }, { 535 GUID: "role-guid-2", 536 Type: constant.OrgManagerRole, 537 }})) 538 }) 539 }) 540 541 When("the response includes other resources", func() { 542 BeforeEach(func() { 543 response1 := fmt.Sprintf(`{ 544 "pagination": { 545 "next": { 546 "href": "%s/v3/roles?organization_guids=some-org-name&page=2&per_page=1&include=users" 547 } 548 }, 549 "resources": [ 550 { 551 "guid": "role-guid-1", 552 "type": "organization_user", 553 "relationships": { 554 "user": { 555 "data": {"guid": "user-guid-1"} 556 } 557 } 558 } 559 ], 560 "included": { 561 "users": [ 562 { 563 "guid": "user-guid-1", 564 "username": "user-name-1", 565 "origin": "uaa" 566 } 567 ], 568 "spaces": [ 569 { 570 "guid": "space-guid-1", 571 "name": "space-name-1" 572 } 573 ], 574 "organizations": [ 575 { 576 "guid": "org-guid-1", 577 "name": "org-name-1" 578 } 579 ] 580 } 581 }`, server.URL()) 582 583 response2 := `{ 584 "pagination": { 585 "next": null 586 }, 587 "resources": [ 588 { 589 "guid": "role-guid-2", 590 "type": "organization_manager", 591 "relationships": { 592 "user": { 593 "data": {"guid": "user-guid-2"} 594 } 595 } 596 } 597 ], 598 "included": { 599 "users": [ 600 { 601 "guid": "user-guid-2", 602 "username": "user-name-2", 603 "origin": "uaa" 604 } 605 ], 606 "spaces": [ 607 { 608 "guid": "space-guid-2", 609 "name": "space-name-2" 610 } 611 ], 612 "organizations": [ 613 { 614 "guid": "org-guid-2", 615 "name": "org-name-2" 616 } 617 ] 618 } 619 }` 620 621 server.AppendHandlers( 622 CombineHandlers( 623 VerifyRequest(http.MethodGet, "/v3/roles", "organization_guids=some-org-name&include=users"), 624 RespondWith(http.StatusOK, response1, http.Header{"X-Cf-Warnings": {"warning-1"}}), 625 ), 626 ) 627 server.AppendHandlers( 628 CombineHandlers( 629 VerifyRequest(http.MethodGet, "/v3/roles", "organization_guids=some-org-name&page=2&per_page=1&include=users"), 630 RespondWith(http.StatusOK, response2, http.Header{"X-Cf-Warnings": {"warning-2"}}), 631 ), 632 ) 633 }) 634 635 It("returns the queried and additional resources", func() { 636 Expect(executeErr).ToNot(HaveOccurred()) 637 Expect(warnings).To(ConsistOf("warning-1", "warning-2")) 638 639 Expect(resourceList).To(Equal([]resources.Role{{ 640 GUID: "role-guid-1", 641 Type: constant.OrgUserRole, 642 UserGUID: "user-guid-1", 643 }, { 644 GUID: "role-guid-2", 645 Type: constant.OrgManagerRole, 646 UserGUID: "user-guid-2", 647 }})) 648 649 Expect(includedResources).To(Equal(IncludedResources{ 650 Users: []resources.User{ 651 {GUID: "user-guid-1", Username: "user-name-1", Origin: "uaa"}, 652 {GUID: "user-guid-2", Username: "user-name-2", Origin: "uaa"}, 653 }, 654 Spaces: []Space{ 655 {GUID: "space-guid-1", Name: "space-name-1"}, 656 {GUID: "space-guid-2", Name: "space-name-2"}, 657 }, 658 Organizations: []Organization{ 659 {GUID: "org-guid-1", Name: "org-name-1"}, 660 {GUID: "org-guid-2", Name: "org-name-2"}, 661 }, 662 })) 663 }) 664 }) 665 666 When("the request has a URI parameter", func() { 667 var ( 668 appGUID string 669 resources []Process 670 ) 671 672 BeforeEach(func() { 673 appGUID = "some-app-guid" 674 675 response1 := fmt.Sprintf(`{ 676 "pagination": { 677 "next": { 678 "href": "%s/v3/apps/%s/processes?page=2" 679 } 680 }, 681 "resources": [ 682 { 683 "guid": "process-guid-1" 684 } 685 ] 686 }`, server.URL(), appGUID) 687 response2 := `{ 688 "pagination": { 689 "next": null 690 }, 691 "resources": [ 692 { 693 "guid": "process-guid-2" 694 } 695 ] 696 }` 697 698 server.AppendHandlers( 699 CombineHandlers( 700 VerifyRequest(http.MethodGet, fmt.Sprintf("/v3/apps/%s/processes", appGUID)), 701 RespondWith(http.StatusOK, response1, http.Header{"X-Cf-Warnings": {"warning-1"}}), 702 ), 703 ) 704 server.AppendHandlers( 705 CombineHandlers( 706 VerifyRequest(http.MethodGet, fmt.Sprintf("/v3/apps/%s/processes", appGUID), "page=2"), 707 RespondWith(http.StatusOK, response2, http.Header{"X-Cf-Warnings": {"warning-2"}}), 708 ), 709 ) 710 711 requestParams = RequestParams{ 712 RequestName: internal.GetApplicationProcessesRequest, 713 URIParams: internal.Params{"app_guid": appGUID}, 714 ResponseBody: Process{}, 715 AppendToList: func(item interface{}) error { 716 resources = append(resources, item.(Process)) 717 return nil 718 }, 719 } 720 }) 721 722 It("returns the given resources and all warnings", func() { 723 Expect(executeErr).ToNot(HaveOccurred()) 724 Expect(warnings).To(ConsistOf("warning-1", "warning-2")) 725 Expect(resources).To(Equal([]Process{{ 726 GUID: "process-guid-1", 727 }, { 728 GUID: "process-guid-2", 729 }})) 730 }) 731 }) 732 733 When("the cloud controller returns errors and warnings", func() { 734 BeforeEach(func() { 735 response := `{ 736 "errors": [ 737 { 738 "code": 10008, 739 "detail": "The request is semantically invalid: command presence", 740 "title": "CF-UnprocessableEntity" 741 }, 742 { 743 "code": 10010, 744 "detail": "Org not found", 745 "title": "CF-ResourceNotFound" 746 } 747 ] 748 }` 749 server.AppendHandlers( 750 CombineHandlers( 751 VerifyRequest(http.MethodGet, "/v3/roles"), 752 RespondWith(http.StatusTeapot, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 753 ), 754 ) 755 }) 756 757 It("returns the error and all warnings", func() { 758 Expect(executeErr).To(MatchError(ccerror.MultiError{ 759 ResponseCode: http.StatusTeapot, 760 Errors: []ccerror.V3Error{ 761 { 762 Code: 10008, 763 Detail: "The request is semantically invalid: command presence", 764 Title: "CF-UnprocessableEntity", 765 }, 766 { 767 Code: 10010, 768 Detail: "Org not found", 769 Title: "CF-ResourceNotFound", 770 }, 771 }, 772 })) 773 Expect(warnings).To(ConsistOf("this is a warning")) 774 }) 775 }) 776 }) 777 }) 778 779 Describe("MakeRequestReceiveRaw", func() { 780 var ( 781 requestName string 782 uriParams internal.Params 783 784 rawResponseBody []byte 785 warnings Warnings 786 executeErr error 787 responseBodyMimeType string 788 ) 789 790 JustBeforeEach(func() { 791 rawResponseBody, warnings, executeErr = client.MakeRequestReceiveRaw(requestName, uriParams, responseBodyMimeType) 792 }) 793 794 Context("GET raw bytes (YAML data)", func() { 795 var ( 796 expectedResponseBody []byte 797 ) 798 799 BeforeEach(func() { 800 requestName = internal.GetApplicationManifestRequest 801 responseBodyMimeType = "application/x-yaml" 802 uriParams = internal.Params{"app_guid": "some-app-guid"} 803 }) 804 805 When("getting requested data is successful", func() { 806 BeforeEach(func() { 807 expectedResponseBody = []byte("---\n- banana") 808 809 server.AppendHandlers( 810 CombineHandlers( 811 CombineHandlers( 812 VerifyRequest(http.MethodGet, "/v3/apps/some-app-guid/manifest"), 813 VerifyHeaderKV("Accept", "application/x-yaml"), 814 RespondWith( 815 http.StatusOK, 816 expectedResponseBody, 817 http.Header{ 818 "Content-Type": {"application/x-yaml"}, 819 "X-Cf-Warnings": {"this is a warning"}, 820 }), 821 ), 822 ), 823 ) 824 }) 825 826 It("returns the raw response body and all warnings", func() { 827 Expect(executeErr).NotTo(HaveOccurred()) 828 Expect(rawResponseBody).To(Equal(expectedResponseBody)) 829 Expect(warnings).To(ConsistOf("this is a warning")) 830 }) 831 }) 832 833 When("the cloud controller returns errors and warnings", func() { 834 BeforeEach(func() { 835 response := `{ 836 "errors": [ 837 { 838 "code": 10008, 839 "detail": "The request is semantically invalid: command presence", 840 "title": "CF-UnprocessableEntity" 841 }, 842 { 843 "code": 10010, 844 "detail": "Org not found", 845 "title": "CF-ResourceNotFound" 846 } 847 ] 848 }` 849 850 server.AppendHandlers( 851 CombineHandlers( 852 VerifyRequest(http.MethodGet, "/v3/apps/some-app-guid/manifest"), 853 VerifyHeaderKV("Accept", "application/x-yaml"), 854 RespondWith(http.StatusTeapot, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 855 ), 856 ) 857 }) 858 859 It("returns the error and all warnings", func() { 860 Expect(executeErr).To(MatchError(ccerror.MultiError{ 861 ResponseCode: http.StatusTeapot, 862 Errors: []ccerror.V3Error{ 863 { 864 Code: 10008, 865 Detail: "The request is semantically invalid: command presence", 866 Title: "CF-UnprocessableEntity", 867 }, 868 { 869 Code: 10010, 870 Detail: "Org not found", 871 Title: "CF-ResourceNotFound", 872 }, 873 }, 874 })) 875 Expect(warnings).To(ConsistOf("this is a warning")) 876 }) 877 }) 878 }) 879 }) 880 881 Describe("MakeRequestSendRaw", func() { 882 var ( 883 requestName string 884 uriParams internal.Params 885 requestBodyMimeType string 886 887 requestBody []byte 888 responseBody Package 889 expectedJobURL string 890 responseLocation string 891 warnings Warnings 892 executeErr error 893 ) 894 895 JustBeforeEach(func() { 896 responseLocation, warnings, executeErr = client.MakeRequestSendRaw(requestName, uriParams, requestBody, requestBodyMimeType, &responseBody) 897 }) 898 899 BeforeEach(func() { 900 requestBody = []byte("fake-package-file") 901 expectedJobURL = "apply-manifest-job-url" 902 responseBody = Package{} 903 904 requestName = internal.PostPackageBitsRequest 905 uriParams = internal.Params{"package_guid": "package-guid"} 906 requestBodyMimeType = "multipart/form-data" 907 }) 908 909 When("the resource is successfully created", func() { 910 BeforeEach(func() { 911 response := `{ 912 "guid": "some-pkg-guid", 913 "type": "docker", 914 "state": "PROCESSING_UPLOAD", 915 "links": { 916 "upload": { 917 "href": "some-package-upload-url", 918 "method": "POST" 919 } 920 } 921 }` 922 923 server.AppendHandlers( 924 CombineHandlers( 925 VerifyRequest(http.MethodPost, "/v3/packages/package-guid/upload"), 926 VerifyBody(requestBody), 927 VerifyHeaderKV("Content-Type", "multipart/form-data"), 928 RespondWith(http.StatusCreated, response, http.Header{ 929 "X-Cf-Warnings": {"this is a warning"}, 930 "Location": {expectedJobURL}, 931 }), 932 ), 933 ) 934 }) 935 936 It("returns the resource and warnings", func() { 937 Expect(responseLocation).To(Equal(expectedJobURL)) 938 Expect(executeErr).NotTo(HaveOccurred()) 939 Expect(warnings).To(ConsistOf("this is a warning")) 940 941 expectedPackage := Package{ 942 GUID: "some-pkg-guid", 943 Type: constant.PackageTypeDocker, 944 State: constant.PackageProcessingUpload, 945 Links: map[string]APILink{ 946 "upload": APILink{HREF: "some-package-upload-url", Method: http.MethodPost}, 947 }, 948 } 949 Expect(responseBody).To(Equal(expectedPackage)) 950 }) 951 }) 952 953 When("the resource returns all errors and warnings", func() { 954 BeforeEach(func() { 955 response := ` { 956 "errors": [ 957 { 958 "code": 10008, 959 "detail": "The request is semantically invalid: command presence", 960 "title": "CF-UnprocessableEntity" 961 }, 962 { 963 "code": 10010, 964 "detail": "Hamster not found", 965 "title": "CF-ResourceNotFound" 966 } 967 ] 968 }` 969 server.AppendHandlers( 970 CombineHandlers( 971 VerifyRequest(http.MethodPost, "/v3/packages/package-guid/upload"), 972 RespondWith(http.StatusTeapot, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 973 ), 974 ) 975 }) 976 977 It("returns the error and all warnings", func() { 978 Expect(executeErr).To(MatchError(ccerror.MultiError{ 979 ResponseCode: http.StatusTeapot, 980 Errors: []ccerror.V3Error{ 981 { 982 Code: 10008, 983 Detail: "The request is semantically invalid: command presence", 984 Title: "CF-UnprocessableEntity", 985 }, 986 { 987 Code: 10010, 988 Detail: "Hamster not found", 989 Title: "CF-ResourceNotFound", 990 }, 991 }, 992 })) 993 Expect(warnings).To(ConsistOf("this is a warning")) 994 }) 995 }) 996 }) 997 998 Describe("MakeRequestUploadAsync", func() { 999 var ( 1000 requestName string 1001 uriParams internal.Params 1002 requestBodyMimeType string 1003 requestBody io.ReadSeeker 1004 dataLength int64 1005 writeErrors chan error 1006 1007 responseLocation string 1008 responseBody Package 1009 warning string 1010 warnings Warnings 1011 executeErr error 1012 ) 1013 BeforeEach(func() { 1014 warning = "upload-async-warning" 1015 content := "I love my cats!" 1016 requestBody = strings.NewReader(content) 1017 dataLength = int64(len(content)) 1018 writeErrors = make(chan error) 1019 1020 response := `{ 1021 "guid": "some-package-guid", 1022 "type": "bits", 1023 "state": "PROCESSING_UPLOAD" 1024 }` 1025 1026 server.AppendHandlers( 1027 CombineHandlers( 1028 VerifyRequest(http.MethodPost, "/v3/packages/package-guid/upload"), 1029 VerifyHeaderKV("Content-Type", "multipart/form-data"), 1030 VerifyBody([]byte(content)), 1031 RespondWith(http.StatusOK, response, http.Header{ 1032 "X-Cf-Warnings": {warning}, 1033 "Location": {"something"}, 1034 }), 1035 ), 1036 ) 1037 }) 1038 JustBeforeEach(func() { 1039 responseBody = Package{} 1040 requestName = internal.PostPackageBitsRequest 1041 requestBodyMimeType = "multipart/form-data" 1042 uriParams = internal.Params{"package_guid": "package-guid"} 1043 1044 responseLocation, warnings, executeErr = client.MakeRequestUploadAsync( 1045 requestName, 1046 uriParams, 1047 requestBodyMimeType, 1048 requestBody, 1049 dataLength, 1050 &responseBody, 1051 writeErrors, 1052 ) 1053 }) 1054 When("there are no errors (happy path)", func() { 1055 BeforeEach(func() { 1056 go func() { 1057 close(writeErrors) 1058 }() 1059 }) 1060 It("returns the location and any warnings and error", func() { 1061 Expect(executeErr).ToNot(HaveOccurred()) 1062 Expect(responseLocation).To(Equal("something")) 1063 Expect(responseBody).To(Equal(Package{ 1064 GUID: "some-package-guid", 1065 State: "PROCESSING_UPLOAD", 1066 Type: "bits", 1067 })) 1068 Expect(warnings).To(Equal(Warnings{warning})) 1069 }) 1070 }) 1071 1072 When("There are write errors", func() { 1073 BeforeEach(func() { 1074 go func() { 1075 writeErrors <- errors.New("first-error") 1076 writeErrors <- errors.New("second-error") 1077 close(writeErrors) 1078 }() 1079 }) 1080 It("returns the first error", func() { 1081 Expect(executeErr).To(MatchError("first-error")) 1082 }) 1083 }) 1084 1085 When("there are HTTP connection errors", func() { 1086 BeforeEach(func() { 1087 server.Close() 1088 close(writeErrors) 1089 }) 1090 1091 It("returns the first error", func() { 1092 _, ok := executeErr.(ccerror.RequestError) 1093 Expect(ok).To(BeTrue()) 1094 }) 1095 }) 1096 }) 1097 })