github.com/DaAlbrecht/cf-cli@v0.0.0-20231128151943-1fe19bb400b9/api/cloudcontroller/ccv3/requester_test.go (about) 1 package ccv3_test 2 3 import ( 4 "encoding/json" 5 "errors" 6 "fmt" 7 "io" 8 "net/http" 9 "strings" 10 11 "code.cloudfoundry.org/cli/api/cloudcontroller/ccerror" 12 . "code.cloudfoundry.org/cli/api/cloudcontroller/ccv3" 13 "code.cloudfoundry.org/cli/api/cloudcontroller/ccv3/constant" 14 "code.cloudfoundry.org/cli/api/cloudcontroller/ccv3/internal" 15 "code.cloudfoundry.org/cli/resources" 16 . "code.cloudfoundry.org/cli/resources" 17 "code.cloudfoundry.org/cli/types" 18 . "github.com/onsi/ginkgo" 19 . "github.com/onsi/gomega" 20 . "github.com/onsi/gomega/ghttp" 21 ) 22 23 var _ = Describe("shared request helpers", func() { 24 var client *Client 25 26 BeforeEach(func() { 27 client, _ = NewTestClient() 28 }) 29 30 Describe("MakeRequest", func() { 31 var ( 32 requestParams RequestParams 33 34 jobURL JobURL 35 warnings Warnings 36 executeErr error 37 ) 38 39 BeforeEach(func() { 40 requestParams = RequestParams{} 41 }) 42 43 JustBeforeEach(func() { 44 jobURL, warnings, executeErr = client.MakeRequest(requestParams) 45 }) 46 47 Context("GET single resource", func() { 48 var ( 49 responseBody Organization 50 ) 51 52 BeforeEach(func() { 53 requestParams = RequestParams{ 54 RequestName: internal.GetOrganizationRequest, 55 URIParams: internal.Params{"organization_guid": "some-org-guid"}, 56 ResponseBody: &responseBody, 57 } 58 }) 59 60 When("organization exists", func() { 61 BeforeEach(func() { 62 response := `{ 63 "name": "some-org-name", 64 "guid": "some-org-guid", 65 "relationships": { 66 "quota": { 67 "data": { 68 "guid": "some-org-quota-guid" 69 } 70 } 71 } 72 }` 73 74 server.AppendHandlers( 75 CombineHandlers( 76 VerifyRequest(http.MethodGet, "/v3/organizations/some-org-guid"), 77 RespondWith(http.StatusOK, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 78 ), 79 ) 80 }) 81 82 It("returns the queried organization and all warnings", func() { 83 Expect(executeErr).NotTo(HaveOccurred()) 84 Expect(responseBody).To(Equal(Organization{ 85 Name: "some-org-name", 86 GUID: "some-org-guid", 87 QuotaGUID: "some-org-quota-guid", 88 })) 89 Expect(warnings).To(ConsistOf("this is a warning")) 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("MakeRequestSendReceiveRaw", func() { 446 var ( 447 method string 448 url string 449 headers http.Header 450 requestBody []byte 451 responseBytes []byte 452 httpResponse *http.Response 453 executeErr error 454 ) 455 JustBeforeEach(func() { 456 responseBytes, httpResponse, executeErr = client.MakeRequestSendReceiveRaw(method, url, headers, requestBody) 457 }) 458 459 Context("PATCH request with body", func() { 460 BeforeEach(func() { 461 method = "PATCH" 462 url = fmt.Sprintf("%s/v3/apps/%s", server.URL(), "some-app-guid") 463 headers = http.Header{} 464 headers.Set("Banana", "Plantain") 465 466 var err error 467 requestBody, err = json.Marshal(Application{ 468 GUID: "some-app-guid", 469 Name: "some-app-name", 470 StackName: "some-stack-name", 471 LifecycleType: constant.AppLifecycleTypeBuildpack, 472 LifecycleBuildpacks: []string{"some-buildpack"}, 473 SpaceGUID: "some-space-guid", 474 }) 475 476 Expect(err).NotTo(HaveOccurred()) 477 478 response := `{ 479 "guid": "some-app-guid", 480 "name": "some-app-name", 481 "lifecycle": { 482 "type": "buildpack", 483 "data": { 484 "buildpacks": ["some-buildpack"], 485 "stack": "some-stack-name" 486 } 487 } 488 }` 489 490 expectedBody := map[string]interface{}{ 491 "name": "some-app-name", 492 "lifecycle": map[string]interface{}{ 493 "type": "buildpack", 494 "data": map[string]interface{}{ 495 "buildpacks": []string{"some-buildpack"}, 496 "stack": "some-stack-name", 497 }, 498 }, 499 "relationships": map[string]interface{}{ 500 "space": map[string]interface{}{ 501 "data": map[string]string{ 502 "guid": "some-space-guid", 503 }, 504 }, 505 }, 506 } 507 server.AppendHandlers( 508 CombineHandlers( 509 VerifyRequest(http.MethodPatch, "/v3/apps/some-app-guid"), 510 VerifyHeader(http.Header{"Banana": {"Plantain"}}), 511 VerifyJSONRepresenting(expectedBody), 512 RespondWith(http.StatusOK, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 513 ), 514 ) 515 }) 516 517 It("successfully makes the request", func() { 518 Expect(executeErr).NotTo(HaveOccurred()) 519 actualResponse := `{ 520 "guid": "some-app-guid", 521 "name": "some-app-name", 522 "lifecycle": { 523 "type": "buildpack", 524 "data": { 525 "buildpacks": ["some-buildpack"], 526 "stack": "some-stack-name" 527 } 528 } 529 }` 530 Expect(string(responseBytes)).To(Equal(actualResponse)) 531 Expect(httpResponse.Header["X-Cf-Warnings"][0]).To(Equal("this is a warning")) 532 }) 533 }) 534 }) 535 536 Describe("MakeListRequest", func() { 537 var ( 538 requestParams RequestParams 539 540 includedResources IncludedResources 541 warnings Warnings 542 executeErr error 543 ) 544 545 JustBeforeEach(func() { 546 includedResources, warnings, executeErr = client.MakeListRequest(requestParams) 547 }) 548 549 Context("with query params and included resources", func() { 550 var ( 551 resourceList []resources.Role 552 query []Query 553 ) 554 555 BeforeEach(func() { 556 resourceList = []resources.Role{} 557 query = []Query{ 558 { 559 Key: OrganizationGUIDFilter, 560 Values: []string{"some-org-name"}, 561 }, 562 { 563 Key: Include, 564 Values: []string{"users"}, 565 }, 566 } 567 requestParams = RequestParams{ 568 RequestName: internal.GetRolesRequest, 569 Query: query, 570 ResponseBody: resources.Role{}, 571 AppendToList: func(item interface{}) error { 572 resourceList = append(resourceList, item.(resources.Role)) 573 return nil 574 }, 575 } 576 }) 577 578 When("the request succeeds", func() { 579 BeforeEach(func() { 580 response1 := fmt.Sprintf(`{ 581 "pagination": { 582 "next": { 583 "href": "%s/v3/roles?organization_guids=some-org-name&page=2&per_page=1&include=users" 584 } 585 }, 586 "resources": [ 587 { 588 "guid": "role-guid-1", 589 "type": "organization_user" 590 } 591 ] 592 }`, server.URL()) 593 response2 := `{ 594 "pagination": { 595 "next": null 596 }, 597 "resources": [ 598 { 599 "guid": "role-guid-2", 600 "type": "organization_manager" 601 } 602 ] 603 }` 604 605 server.AppendHandlers( 606 CombineHandlers( 607 VerifyRequest(http.MethodGet, "/v3/roles", "organization_guids=some-org-name&include=users"), 608 RespondWith(http.StatusOK, response1, http.Header{"X-Cf-Warnings": {"warning-1"}}), 609 ), 610 ) 611 server.AppendHandlers( 612 CombineHandlers( 613 VerifyRequest(http.MethodGet, "/v3/roles", "organization_guids=some-org-name&page=2&per_page=1&include=users"), 614 RespondWith(http.StatusOK, response2, http.Header{"X-Cf-Warnings": {"warning-2"}}), 615 ), 616 ) 617 }) 618 619 It("returns the given resources and all warnings", func() { 620 Expect(executeErr).ToNot(HaveOccurred()) 621 Expect(warnings).To(ConsistOf("warning-1", "warning-2")) 622 Expect(resourceList).To(Equal([]resources.Role{{ 623 GUID: "role-guid-1", 624 Type: constant.OrgUserRole, 625 }, { 626 GUID: "role-guid-2", 627 Type: constant.OrgManagerRole, 628 }})) 629 }) 630 }) 631 632 When("the response includes other resources", func() { 633 BeforeEach(func() { 634 response1 := fmt.Sprintf(`{ 635 "pagination": { 636 "next": { 637 "href": "%s/v3/roles?organization_guids=some-org-name&page=2&per_page=1&include=users" 638 } 639 }, 640 "resources": [ 641 { 642 "guid": "role-guid-1", 643 "type": "organization_user", 644 "relationships": { 645 "user": { 646 "data": {"guid": "user-guid-1"} 647 } 648 } 649 } 650 ], 651 "included": { 652 "apps": [ 653 { 654 "guid": "app-guid-1", 655 "name": "app-name-1" 656 } 657 ], 658 "users": [ 659 { 660 "guid": "user-guid-1", 661 "username": "user-name-1", 662 "origin": "uaa" 663 } 664 ], 665 "spaces": [ 666 { 667 "guid": "space-guid-1", 668 "name": "space-name-1" 669 } 670 ], 671 "organizations": [ 672 { 673 "guid": "org-guid-1", 674 "name": "org-name-1" 675 } 676 ], 677 "service_brokers": [ 678 { 679 "guid": "broker-guid-1", 680 "name": "broker-name-1" 681 } 682 ], 683 "service_instances": [ 684 { 685 "guid": "service-instance-guid-1", 686 "name": "service-instance-name-1" 687 } 688 ], 689 "service_offerings": [ 690 { 691 "guid": "offering-guid-1", 692 "name": "offering-name-1" 693 } 694 ], 695 "service_plans": [ 696 { 697 "guid": "plan-guid-1", 698 "name": "plan-name-1" 699 } 700 ] 701 } 702 }`, server.URL()) 703 704 response2 := `{ 705 "pagination": { 706 "next": null 707 }, 708 "resources": [ 709 { 710 "guid": "role-guid-2", 711 "type": "organization_manager", 712 "relationships": { 713 "user": { 714 "data": {"guid": "user-guid-2"} 715 } 716 } 717 } 718 ], 719 "included": { 720 "users": [ 721 { 722 "guid": "user-guid-2", 723 "username": "user-name-2", 724 "origin": "uaa" 725 } 726 ], 727 "spaces": [ 728 { 729 "guid": "space-guid-2", 730 "name": "space-name-2" 731 } 732 ], 733 "organizations": [ 734 { 735 "guid": "org-guid-2", 736 "name": "org-name-2" 737 } 738 ], 739 "service_brokers": [ 740 { 741 "guid": "broker-guid-2", 742 "name": "broker-name-2" 743 } 744 ], 745 "service_instances": [ 746 { 747 "guid": "service-instance-guid-2", 748 "name": "service-instance-name-2" 749 } 750 ], 751 "service_offerings": [ 752 { 753 "guid": "offering-guid-2", 754 "name": "offering-name-2" 755 } 756 ], 757 "service_plans": [ 758 { 759 "guid": "plan-guid-2", 760 "name": "plan-name-2" 761 } 762 ] 763 } 764 }` 765 766 server.AppendHandlers( 767 CombineHandlers( 768 VerifyRequest(http.MethodGet, "/v3/roles", "organization_guids=some-org-name&include=users"), 769 RespondWith(http.StatusOK, response1, http.Header{"X-Cf-Warnings": {"warning-1"}}), 770 ), 771 ) 772 server.AppendHandlers( 773 CombineHandlers( 774 VerifyRequest(http.MethodGet, "/v3/roles", "organization_guids=some-org-name&page=2&per_page=1&include=users"), 775 RespondWith(http.StatusOK, response2, http.Header{"X-Cf-Warnings": {"warning-2"}}), 776 ), 777 ) 778 }) 779 780 It("returns the queried and additional resources", func() { 781 Expect(executeErr).ToNot(HaveOccurred()) 782 Expect(warnings).To(ConsistOf("warning-1", "warning-2")) 783 784 Expect(resourceList).To(Equal([]resources.Role{{ 785 GUID: "role-guid-1", 786 Type: constant.OrgUserRole, 787 UserGUID: "user-guid-1", 788 }, { 789 GUID: "role-guid-2", 790 Type: constant.OrgManagerRole, 791 UserGUID: "user-guid-2", 792 }})) 793 794 Expect(includedResources).To(Equal(IncludedResources{ 795 Apps: []resources.Application{ 796 {Name: "app-name-1", GUID: "app-guid-1"}, 797 }, 798 Users: []resources.User{ 799 {GUID: "user-guid-1", Username: "user-name-1", Origin: "uaa"}, 800 {GUID: "user-guid-2", Username: "user-name-2", Origin: "uaa"}, 801 }, 802 Spaces: []Space{ 803 {GUID: "space-guid-1", Name: "space-name-1"}, 804 {GUID: "space-guid-2", Name: "space-name-2"}, 805 }, 806 Organizations: []Organization{ 807 {GUID: "org-guid-1", Name: "org-name-1"}, 808 {GUID: "org-guid-2", Name: "org-name-2"}, 809 }, 810 ServiceBrokers: []ServiceBroker{ 811 {Name: "broker-name-1", GUID: "broker-guid-1"}, 812 {Name: "broker-name-2", GUID: "broker-guid-2"}, 813 }, 814 ServiceInstances: []resources.ServiceInstance{ 815 {Name: "service-instance-name-1", GUID: "service-instance-guid-1"}, 816 {Name: "service-instance-name-2", GUID: "service-instance-guid-2"}, 817 }, 818 ServiceOfferings: []ServiceOffering{ 819 {Name: "offering-name-1", GUID: "offering-guid-1"}, 820 {Name: "offering-name-2", GUID: "offering-guid-2"}, 821 }, 822 ServicePlans: []ServicePlan{ 823 {Name: "plan-name-1", GUID: "plan-guid-1"}, 824 {Name: "plan-name-2", GUID: "plan-guid-2"}, 825 }, 826 })) 827 }) 828 }) 829 830 When("the request has a URI parameter", func() { 831 var ( 832 appGUID string 833 resources []Process 834 ) 835 836 BeforeEach(func() { 837 appGUID = "some-app-guid" 838 839 response1 := fmt.Sprintf(`{ 840 "pagination": { 841 "next": { 842 "href": "%s/v3/apps/%s/processes?page=2" 843 } 844 }, 845 "resources": [ 846 { 847 "guid": "process-guid-1" 848 } 849 ] 850 }`, server.URL(), appGUID) 851 response2 := `{ 852 "pagination": { 853 "next": null 854 }, 855 "resources": [ 856 { 857 "guid": "process-guid-2" 858 } 859 ] 860 }` 861 862 server.AppendHandlers( 863 CombineHandlers( 864 VerifyRequest(http.MethodGet, fmt.Sprintf("/v3/apps/%s/processes", appGUID)), 865 RespondWith(http.StatusOK, response1, http.Header{"X-Cf-Warnings": {"warning-1"}}), 866 ), 867 ) 868 server.AppendHandlers( 869 CombineHandlers( 870 VerifyRequest(http.MethodGet, fmt.Sprintf("/v3/apps/%s/processes", appGUID), "page=2"), 871 RespondWith(http.StatusOK, response2, http.Header{"X-Cf-Warnings": {"warning-2"}}), 872 ), 873 ) 874 875 requestParams = RequestParams{ 876 RequestName: internal.GetApplicationProcessesRequest, 877 URIParams: internal.Params{"app_guid": appGUID}, 878 ResponseBody: Process{}, 879 AppendToList: func(item interface{}) error { 880 resources = append(resources, item.(Process)) 881 return nil 882 }, 883 } 884 }) 885 886 It("returns the given resources and all warnings", func() { 887 Expect(executeErr).ToNot(HaveOccurred()) 888 Expect(warnings).To(ConsistOf("warning-1", "warning-2")) 889 Expect(resources).To(Equal([]Process{{ 890 GUID: "process-guid-1", 891 }, { 892 GUID: "process-guid-2", 893 }})) 894 }) 895 }) 896 897 When("the cloud controller returns errors and warnings", func() { 898 BeforeEach(func() { 899 response := `{ 900 "errors": [ 901 { 902 "code": 10008, 903 "detail": "The request is semantically invalid: command presence", 904 "title": "CF-UnprocessableEntity" 905 }, 906 { 907 "code": 10010, 908 "detail": "Org not found", 909 "title": "CF-ResourceNotFound" 910 } 911 ] 912 }` 913 server.AppendHandlers( 914 CombineHandlers( 915 VerifyRequest(http.MethodGet, "/v3/roles"), 916 RespondWith(http.StatusTeapot, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 917 ), 918 ) 919 }) 920 921 It("returns the error and all warnings", func() { 922 Expect(executeErr).To(MatchError(ccerror.MultiError{ 923 ResponseCode: http.StatusTeapot, 924 Errors: []ccerror.V3Error{ 925 { 926 Code: 10008, 927 Detail: "The request is semantically invalid: command presence", 928 Title: "CF-UnprocessableEntity", 929 }, 930 { 931 Code: 10010, 932 Detail: "Org not found", 933 Title: "CF-ResourceNotFound", 934 }, 935 }, 936 })) 937 Expect(warnings).To(ConsistOf("this is a warning")) 938 }) 939 }) 940 }) 941 }) 942 943 Describe("MakeRequestReceiveRaw", func() { 944 var ( 945 requestName string 946 uriParams internal.Params 947 948 rawResponseBody []byte 949 warnings Warnings 950 executeErr error 951 responseBodyMimeType string 952 ) 953 954 JustBeforeEach(func() { 955 rawResponseBody, warnings, executeErr = client.MakeRequestReceiveRaw(requestName, uriParams, responseBodyMimeType) 956 }) 957 958 Context("GET raw bytes (YAML data)", func() { 959 var ( 960 expectedResponseBody []byte 961 ) 962 963 BeforeEach(func() { 964 requestName = internal.GetApplicationManifestRequest 965 responseBodyMimeType = "application/x-yaml" 966 uriParams = internal.Params{"app_guid": "some-app-guid"} 967 }) 968 969 When("getting requested data is successful", func() { 970 BeforeEach(func() { 971 expectedResponseBody = []byte("---\n- banana") 972 973 server.AppendHandlers( 974 CombineHandlers( 975 CombineHandlers( 976 VerifyRequest(http.MethodGet, "/v3/apps/some-app-guid/manifest"), 977 VerifyHeaderKV("Accept", "application/x-yaml"), 978 RespondWith( 979 http.StatusOK, 980 expectedResponseBody, 981 http.Header{ 982 "Content-Type": {"application/x-yaml"}, 983 "X-Cf-Warnings": {"this is a warning"}, 984 }), 985 ), 986 ), 987 ) 988 }) 989 990 It("returns the raw response body and all warnings", func() { 991 Expect(executeErr).NotTo(HaveOccurred()) 992 Expect(rawResponseBody).To(Equal(expectedResponseBody)) 993 Expect(warnings).To(ConsistOf("this is a warning")) 994 }) 995 }) 996 997 When("the cloud controller returns errors and warnings", func() { 998 BeforeEach(func() { 999 response := `{ 1000 "errors": [ 1001 { 1002 "code": 10008, 1003 "detail": "The request is semantically invalid: command presence", 1004 "title": "CF-UnprocessableEntity" 1005 }, 1006 { 1007 "code": 10010, 1008 "detail": "Org not found", 1009 "title": "CF-ResourceNotFound" 1010 } 1011 ] 1012 }` 1013 1014 server.AppendHandlers( 1015 CombineHandlers( 1016 VerifyRequest(http.MethodGet, "/v3/apps/some-app-guid/manifest"), 1017 VerifyHeaderKV("Accept", "application/x-yaml"), 1018 RespondWith(http.StatusTeapot, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 1019 ), 1020 ) 1021 }) 1022 1023 It("returns the error and all warnings", func() { 1024 Expect(executeErr).To(MatchError(ccerror.MultiError{ 1025 ResponseCode: http.StatusTeapot, 1026 Errors: []ccerror.V3Error{ 1027 { 1028 Code: 10008, 1029 Detail: "The request is semantically invalid: command presence", 1030 Title: "CF-UnprocessableEntity", 1031 }, 1032 { 1033 Code: 10010, 1034 Detail: "Org not found", 1035 Title: "CF-ResourceNotFound", 1036 }, 1037 }, 1038 })) 1039 Expect(warnings).To(ConsistOf("this is a warning")) 1040 }) 1041 }) 1042 }) 1043 }) 1044 1045 Describe("MakeRequestSendRaw", func() { 1046 var ( 1047 requestName string 1048 uriParams internal.Params 1049 requestBodyMimeType string 1050 1051 requestBody []byte 1052 responseBody Package 1053 expectedJobURL string 1054 responseLocation string 1055 warnings Warnings 1056 executeErr error 1057 ) 1058 1059 JustBeforeEach(func() { 1060 responseLocation, warnings, executeErr = client.MakeRequestSendRaw(requestName, uriParams, requestBody, requestBodyMimeType, &responseBody) 1061 }) 1062 1063 BeforeEach(func() { 1064 requestBody = []byte("fake-package-file") 1065 expectedJobURL = "apply-manifest-job-url" 1066 responseBody = Package{} 1067 1068 requestName = internal.PostPackageBitsRequest 1069 uriParams = internal.Params{"package_guid": "package-guid"} 1070 requestBodyMimeType = "multipart/form-data" 1071 }) 1072 1073 When("the resource is successfully created", func() { 1074 BeforeEach(func() { 1075 response := `{ 1076 "guid": "some-pkg-guid", 1077 "type": "docker", 1078 "state": "PROCESSING_UPLOAD", 1079 "links": { 1080 "upload": { 1081 "href": "some-package-upload-url", 1082 "method": "POST" 1083 } 1084 } 1085 }` 1086 1087 server.AppendHandlers( 1088 CombineHandlers( 1089 VerifyRequest(http.MethodPost, "/v3/packages/package-guid/upload"), 1090 VerifyBody(requestBody), 1091 VerifyHeaderKV("Content-Type", "multipart/form-data"), 1092 RespondWith(http.StatusCreated, response, http.Header{ 1093 "X-Cf-Warnings": {"this is a warning"}, 1094 "Location": {expectedJobURL}, 1095 }), 1096 ), 1097 ) 1098 }) 1099 1100 It("returns the resource and warnings", func() { 1101 Expect(responseLocation).To(Equal(expectedJobURL)) 1102 Expect(executeErr).NotTo(HaveOccurred()) 1103 Expect(warnings).To(ConsistOf("this is a warning")) 1104 1105 expectedPackage := Package{ 1106 GUID: "some-pkg-guid", 1107 Type: constant.PackageTypeDocker, 1108 State: constant.PackageProcessingUpload, 1109 Links: map[string]APILink{ 1110 "upload": APILink{HREF: "some-package-upload-url", Method: http.MethodPost}, 1111 }, 1112 } 1113 Expect(responseBody).To(Equal(expectedPackage)) 1114 }) 1115 }) 1116 1117 When("the resource returns all errors and warnings", func() { 1118 BeforeEach(func() { 1119 response := ` { 1120 "errors": [ 1121 { 1122 "code": 10008, 1123 "detail": "The request is semantically invalid: command presence", 1124 "title": "CF-UnprocessableEntity" 1125 }, 1126 { 1127 "code": 10010, 1128 "detail": "Hamster not found", 1129 "title": "CF-ResourceNotFound" 1130 } 1131 ] 1132 }` 1133 server.AppendHandlers( 1134 CombineHandlers( 1135 VerifyRequest(http.MethodPost, "/v3/packages/package-guid/upload"), 1136 RespondWith(http.StatusTeapot, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 1137 ), 1138 ) 1139 }) 1140 1141 It("returns the error and all warnings", func() { 1142 Expect(executeErr).To(MatchError(ccerror.MultiError{ 1143 ResponseCode: http.StatusTeapot, 1144 Errors: []ccerror.V3Error{ 1145 { 1146 Code: 10008, 1147 Detail: "The request is semantically invalid: command presence", 1148 Title: "CF-UnprocessableEntity", 1149 }, 1150 { 1151 Code: 10010, 1152 Detail: "Hamster not found", 1153 Title: "CF-ResourceNotFound", 1154 }, 1155 }, 1156 })) 1157 Expect(warnings).To(ConsistOf("this is a warning")) 1158 }) 1159 }) 1160 }) 1161 1162 Describe("MakeRequestUploadAsync", func() { 1163 var ( 1164 requestName string 1165 uriParams internal.Params 1166 requestBodyMimeType string 1167 requestBody io.ReadSeeker 1168 dataLength int64 1169 writeErrors chan error 1170 1171 responseLocation string 1172 responseBody Package 1173 warning string 1174 warnings Warnings 1175 executeErr error 1176 ) 1177 BeforeEach(func() { 1178 warning = "upload-async-warning" 1179 content := "I love my cats!" 1180 requestBody = strings.NewReader(content) 1181 dataLength = int64(len(content)) 1182 writeErrors = make(chan error) 1183 1184 response := `{ 1185 "guid": "some-package-guid", 1186 "type": "bits", 1187 "state": "PROCESSING_UPLOAD" 1188 }` 1189 1190 server.AppendHandlers( 1191 CombineHandlers( 1192 VerifyRequest(http.MethodPost, "/v3/packages/package-guid/upload"), 1193 VerifyHeaderKV("Content-Type", "multipart/form-data"), 1194 VerifyBody([]byte(content)), 1195 RespondWith(http.StatusOK, response, http.Header{ 1196 "X-Cf-Warnings": {warning}, 1197 "Location": {"something"}, 1198 }), 1199 ), 1200 ) 1201 }) 1202 JustBeforeEach(func() { 1203 responseBody = Package{} 1204 requestName = internal.PostPackageBitsRequest 1205 requestBodyMimeType = "multipart/form-data" 1206 uriParams = internal.Params{"package_guid": "package-guid"} 1207 1208 responseLocation, warnings, executeErr = client.MakeRequestUploadAsync( 1209 requestName, 1210 uriParams, 1211 requestBodyMimeType, 1212 requestBody, 1213 dataLength, 1214 &responseBody, 1215 writeErrors, 1216 ) 1217 }) 1218 When("there are no errors (happy path)", func() { 1219 BeforeEach(func() { 1220 go func() { 1221 close(writeErrors) 1222 }() 1223 }) 1224 It("returns the location and any warnings and error", func() { 1225 Expect(executeErr).ToNot(HaveOccurred()) 1226 Expect(responseLocation).To(Equal("something")) 1227 Expect(responseBody).To(Equal(Package{ 1228 GUID: "some-package-guid", 1229 State: "PROCESSING_UPLOAD", 1230 Type: "bits", 1231 })) 1232 Expect(warnings).To(Equal(Warnings{warning})) 1233 }) 1234 }) 1235 1236 When("There are write errors", func() { 1237 BeforeEach(func() { 1238 go func() { 1239 writeErrors <- errors.New("first-error") 1240 writeErrors <- errors.New("second-error") 1241 close(writeErrors) 1242 }() 1243 }) 1244 It("returns the first error", func() { 1245 Expect(executeErr).To(MatchError("first-error")) 1246 }) 1247 }) 1248 1249 When("there are HTTP connection errors", func() { 1250 BeforeEach(func() { 1251 server.Close() 1252 close(writeErrors) 1253 }) 1254 1255 It("returns the first error", func() { 1256 _, ok := executeErr.(ccerror.RequestError) 1257 Expect(ok).To(BeTrue()) 1258 }) 1259 }) 1260 }) 1261 })