github.com/wanddynosios/cli/v8@v8.7.9-0.20240221182337-1a92e3a7017f/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 Context("with 'per_page' and 'page' query params", func() { 943 var ( 944 resourceList []resources.Stack 945 query []Query 946 ) 947 948 BeforeEach(func() { 949 resourceList = []resources.Stack{} 950 query = []Query{ 951 { 952 Key: PerPage, 953 Values: []string{"1"}, 954 }, 955 } 956 requestParams = RequestParams{ 957 RequestName: internal.GetStacksRequest, 958 Query: query, 959 ResponseBody: resources.Stack{}, 960 AppendToList: func(item interface{}) error { 961 resourceList = append(resourceList, item.(resources.Stack)) 962 return nil 963 }, 964 } 965 }) 966 967 When("requesting page=1", func() { 968 BeforeEach(func() { 969 requestParams.Query = append(requestParams.Query, Query{ 970 Key: Page, Values: []string{"1"}, 971 }) 972 973 response1 := fmt.Sprintf(`{ 974 "pagination": { 975 "next": { 976 "href": "%s/v3/stacks?per_page=1&page=2" 977 } 978 }, 979 "resources": [ 980 { 981 "guid": "stack-guid-1" 982 } 983 ] 984 }`, server.URL()) 985 986 server.AppendHandlers( 987 CombineHandlers( 988 VerifyRequest(http.MethodGet, "/v3/stacks", "per_page=1&page=1"), 989 RespondWith(http.StatusOK, response1), 990 ), 991 ) 992 }) 993 994 It("returns only the resources from the specified page", func() { 995 Expect(executeErr).ToNot(HaveOccurred()) 996 Expect(resourceList).To(Equal([]resources.Stack{{ 997 GUID: "stack-guid-1", 998 }})) 999 }) 1000 }) 1001 1002 When("requesting page=2", func() { 1003 BeforeEach(func() { 1004 requestParams.Query = append(requestParams.Query, Query{ 1005 Key: Page, Values: []string{"2"}, 1006 }) 1007 1008 response2 := `{ 1009 "pagination": { 1010 "next": null 1011 }, 1012 "resources": [ 1013 { 1014 "guid": "stack-guid-2" 1015 } 1016 ] 1017 }` 1018 1019 server.AppendHandlers( 1020 CombineHandlers( 1021 VerifyRequest(http.MethodGet, "/v3/stacks", "per_page=1&page=2"), 1022 RespondWith(http.StatusOK, response2), 1023 ), 1024 ) 1025 }) 1026 1027 It("returns only the resources from the specified page", func() { 1028 Expect(executeErr).ToNot(HaveOccurred()) 1029 Expect(resourceList).To(Equal([]resources.Stack{{ 1030 GUID: "stack-guid-2", 1031 }})) 1032 }) 1033 }) 1034 }) 1035 }) 1036 1037 Describe("MakeRequestReceiveRaw", func() { 1038 var ( 1039 requestName string 1040 uriParams internal.Params 1041 1042 rawResponseBody []byte 1043 warnings Warnings 1044 executeErr error 1045 responseBodyMimeType string 1046 ) 1047 1048 JustBeforeEach(func() { 1049 rawResponseBody, warnings, executeErr = client.MakeRequestReceiveRaw(requestName, uriParams, responseBodyMimeType) 1050 }) 1051 1052 Context("GET raw bytes (YAML data)", func() { 1053 var ( 1054 expectedResponseBody []byte 1055 ) 1056 1057 BeforeEach(func() { 1058 requestName = internal.GetApplicationManifestRequest 1059 responseBodyMimeType = "application/x-yaml" 1060 uriParams = internal.Params{"app_guid": "some-app-guid"} 1061 }) 1062 1063 When("getting requested data is successful", func() { 1064 BeforeEach(func() { 1065 expectedResponseBody = []byte("---\n- banana") 1066 1067 server.AppendHandlers( 1068 CombineHandlers( 1069 CombineHandlers( 1070 VerifyRequest(http.MethodGet, "/v3/apps/some-app-guid/manifest"), 1071 VerifyHeaderKV("Accept", "application/x-yaml"), 1072 RespondWith( 1073 http.StatusOK, 1074 expectedResponseBody, 1075 http.Header{ 1076 "Content-Type": {"application/x-yaml"}, 1077 "X-Cf-Warnings": {"this is a warning"}, 1078 }), 1079 ), 1080 ), 1081 ) 1082 }) 1083 1084 It("returns the raw response body and all warnings", func() { 1085 Expect(executeErr).NotTo(HaveOccurred()) 1086 Expect(rawResponseBody).To(Equal(expectedResponseBody)) 1087 Expect(warnings).To(ConsistOf("this is a warning")) 1088 }) 1089 }) 1090 1091 When("the cloud controller returns errors and warnings", func() { 1092 BeforeEach(func() { 1093 response := `{ 1094 "errors": [ 1095 { 1096 "code": 10008, 1097 "detail": "The request is semantically invalid: command presence", 1098 "title": "CF-UnprocessableEntity" 1099 }, 1100 { 1101 "code": 10010, 1102 "detail": "Org not found", 1103 "title": "CF-ResourceNotFound" 1104 } 1105 ] 1106 }` 1107 1108 server.AppendHandlers( 1109 CombineHandlers( 1110 VerifyRequest(http.MethodGet, "/v3/apps/some-app-guid/manifest"), 1111 VerifyHeaderKV("Accept", "application/x-yaml"), 1112 RespondWith(http.StatusTeapot, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 1113 ), 1114 ) 1115 }) 1116 1117 It("returns the error and all warnings", func() { 1118 Expect(executeErr).To(MatchError(ccerror.MultiError{ 1119 ResponseCode: http.StatusTeapot, 1120 Errors: []ccerror.V3Error{ 1121 { 1122 Code: 10008, 1123 Detail: "The request is semantically invalid: command presence", 1124 Title: "CF-UnprocessableEntity", 1125 }, 1126 { 1127 Code: 10010, 1128 Detail: "Org not found", 1129 Title: "CF-ResourceNotFound", 1130 }, 1131 }, 1132 })) 1133 Expect(warnings).To(ConsistOf("this is a warning")) 1134 }) 1135 }) 1136 }) 1137 }) 1138 1139 Describe("MakeRequestSendRaw", func() { 1140 var ( 1141 requestName string 1142 uriParams internal.Params 1143 requestBodyMimeType string 1144 1145 requestBody []byte 1146 responseBody Package 1147 expectedJobURL string 1148 responseLocation string 1149 warnings Warnings 1150 executeErr error 1151 ) 1152 1153 JustBeforeEach(func() { 1154 responseLocation, warnings, executeErr = client.MakeRequestSendRaw(requestName, uriParams, requestBody, requestBodyMimeType, &responseBody) 1155 }) 1156 1157 BeforeEach(func() { 1158 requestBody = []byte("fake-package-file") 1159 expectedJobURL = "apply-manifest-job-url" 1160 responseBody = Package{} 1161 1162 requestName = internal.PostPackageBitsRequest 1163 uriParams = internal.Params{"package_guid": "package-guid"} 1164 requestBodyMimeType = "multipart/form-data" 1165 }) 1166 1167 When("the resource is successfully created", func() { 1168 BeforeEach(func() { 1169 response := `{ 1170 "guid": "some-pkg-guid", 1171 "type": "docker", 1172 "state": "PROCESSING_UPLOAD", 1173 "links": { 1174 "upload": { 1175 "href": "some-package-upload-url", 1176 "method": "POST" 1177 } 1178 } 1179 }` 1180 1181 server.AppendHandlers( 1182 CombineHandlers( 1183 VerifyRequest(http.MethodPost, "/v3/packages/package-guid/upload"), 1184 VerifyBody(requestBody), 1185 VerifyHeaderKV("Content-Type", "multipart/form-data"), 1186 RespondWith(http.StatusCreated, response, http.Header{ 1187 "X-Cf-Warnings": {"this is a warning"}, 1188 "Location": {expectedJobURL}, 1189 }), 1190 ), 1191 ) 1192 }) 1193 1194 It("returns the resource and warnings", func() { 1195 Expect(responseLocation).To(Equal(expectedJobURL)) 1196 Expect(executeErr).NotTo(HaveOccurred()) 1197 Expect(warnings).To(ConsistOf("this is a warning")) 1198 1199 expectedPackage := Package{ 1200 GUID: "some-pkg-guid", 1201 Type: constant.PackageTypeDocker, 1202 State: constant.PackageProcessingUpload, 1203 Links: map[string]APILink{ 1204 "upload": APILink{HREF: "some-package-upload-url", Method: http.MethodPost}, 1205 }, 1206 } 1207 Expect(responseBody).To(Equal(expectedPackage)) 1208 }) 1209 }) 1210 1211 When("the resource returns all errors and warnings", func() { 1212 BeforeEach(func() { 1213 response := ` { 1214 "errors": [ 1215 { 1216 "code": 10008, 1217 "detail": "The request is semantically invalid: command presence", 1218 "title": "CF-UnprocessableEntity" 1219 }, 1220 { 1221 "code": 10010, 1222 "detail": "Hamster not found", 1223 "title": "CF-ResourceNotFound" 1224 } 1225 ] 1226 }` 1227 server.AppendHandlers( 1228 CombineHandlers( 1229 VerifyRequest(http.MethodPost, "/v3/packages/package-guid/upload"), 1230 RespondWith(http.StatusTeapot, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 1231 ), 1232 ) 1233 }) 1234 1235 It("returns the error and all warnings", func() { 1236 Expect(executeErr).To(MatchError(ccerror.MultiError{ 1237 ResponseCode: http.StatusTeapot, 1238 Errors: []ccerror.V3Error{ 1239 { 1240 Code: 10008, 1241 Detail: "The request is semantically invalid: command presence", 1242 Title: "CF-UnprocessableEntity", 1243 }, 1244 { 1245 Code: 10010, 1246 Detail: "Hamster not found", 1247 Title: "CF-ResourceNotFound", 1248 }, 1249 }, 1250 })) 1251 Expect(warnings).To(ConsistOf("this is a warning")) 1252 }) 1253 }) 1254 }) 1255 1256 Describe("MakeRequestUploadAsync", func() { 1257 var ( 1258 requestName string 1259 uriParams internal.Params 1260 requestBodyMimeType string 1261 requestBody io.ReadSeeker 1262 dataLength int64 1263 writeErrors chan error 1264 1265 responseLocation string 1266 responseBody Package 1267 warning string 1268 warnings Warnings 1269 executeErr error 1270 ) 1271 BeforeEach(func() { 1272 warning = "upload-async-warning" 1273 content := "I love my cats!" 1274 requestBody = strings.NewReader(content) 1275 dataLength = int64(len(content)) 1276 writeErrors = make(chan error) 1277 1278 response := `{ 1279 "guid": "some-package-guid", 1280 "type": "bits", 1281 "state": "PROCESSING_UPLOAD" 1282 }` 1283 1284 server.AppendHandlers( 1285 CombineHandlers( 1286 VerifyRequest(http.MethodPost, "/v3/packages/package-guid/upload"), 1287 VerifyHeaderKV("Content-Type", "multipart/form-data"), 1288 VerifyBody([]byte(content)), 1289 RespondWith(http.StatusOK, response, http.Header{ 1290 "X-Cf-Warnings": {warning}, 1291 "Location": {"something"}, 1292 }), 1293 ), 1294 ) 1295 }) 1296 JustBeforeEach(func() { 1297 responseBody = Package{} 1298 requestName = internal.PostPackageBitsRequest 1299 requestBodyMimeType = "multipart/form-data" 1300 uriParams = internal.Params{"package_guid": "package-guid"} 1301 1302 responseLocation, warnings, executeErr = client.MakeRequestUploadAsync( 1303 requestName, 1304 uriParams, 1305 requestBodyMimeType, 1306 requestBody, 1307 dataLength, 1308 &responseBody, 1309 writeErrors, 1310 ) 1311 }) 1312 When("there are no errors (happy path)", func() { 1313 BeforeEach(func() { 1314 go func() { 1315 close(writeErrors) 1316 }() 1317 }) 1318 It("returns the location and any warnings and error", func() { 1319 Expect(executeErr).ToNot(HaveOccurred()) 1320 Expect(responseLocation).To(Equal("something")) 1321 Expect(responseBody).To(Equal(Package{ 1322 GUID: "some-package-guid", 1323 State: "PROCESSING_UPLOAD", 1324 Type: "bits", 1325 })) 1326 Expect(warnings).To(Equal(Warnings{warning})) 1327 }) 1328 }) 1329 1330 When("There are write errors", func() { 1331 BeforeEach(func() { 1332 go func() { 1333 writeErrors <- errors.New("first-error") 1334 writeErrors <- errors.New("second-error") 1335 close(writeErrors) 1336 }() 1337 }) 1338 It("returns the first error", func() { 1339 Expect(executeErr).To(MatchError("first-error")) 1340 }) 1341 }) 1342 1343 When("there are HTTP connection errors", func() { 1344 BeforeEach(func() { 1345 server.Close() 1346 close(writeErrors) 1347 }) 1348 1349 It("returns the first error", func() { 1350 _, ok := executeErr.(ccerror.RequestError) 1351 Expect(ok).To(BeTrue()) 1352 }) 1353 }) 1354 }) 1355 })