github.com/sleungcy/cli@v7.1.0+incompatible/api/cloudcontroller/ccv3/buildpack_test.go (about) 1 package ccv3_test 2 3 import ( 4 "errors" 5 "fmt" 6 "io" 7 "io/ioutil" 8 "mime/multipart" 9 "net/http" 10 "strings" 11 12 "code.cloudfoundry.org/cli/api/cloudcontroller" 13 "code.cloudfoundry.org/cli/api/cloudcontroller/ccv3/ccv3fakes" 14 "code.cloudfoundry.org/cli/api/cloudcontroller/ccv3/constant" 15 "code.cloudfoundry.org/cli/api/cloudcontroller/wrapper" 16 "code.cloudfoundry.org/cli/types" 17 18 "code.cloudfoundry.org/cli/api/cloudcontroller/ccerror" 19 . "code.cloudfoundry.org/cli/api/cloudcontroller/ccv3" 20 . "code.cloudfoundry.org/cli/resources" 21 . "github.com/onsi/ginkgo" 22 . "github.com/onsi/gomega" 23 . "github.com/onsi/gomega/ghttp" 24 ) 25 26 var _ = Describe("Buildpacks", func() { 27 var client *Client 28 29 BeforeEach(func() { 30 client, _ = NewTestClient() 31 }) 32 33 Describe("GetBuildpacks", func() { 34 var ( 35 query Query 36 37 buildpacks []Buildpack 38 warnings Warnings 39 executeErr error 40 ) 41 42 JustBeforeEach(func() { 43 buildpacks, warnings, executeErr = client.GetBuildpacks(query) 44 }) 45 46 When("buildpacks exist", func() { 47 BeforeEach(func() { 48 response1 := fmt.Sprintf(`{ 49 "pagination": { 50 "next": { 51 "href": "%s/v3/buildpacks?names=some-buildpack-name&page=2&per_page=2" 52 } 53 }, 54 "resources": [ 55 { 56 "guid": "guid1", 57 "name": "ruby_buildpack", 58 "state": "AWAITING_UPLOAD", 59 "stack": "windows64", 60 "position": 1, 61 "enabled": true, 62 "locked": false, 63 "metadata": { 64 "labels": {} 65 } 66 }, 67 { 68 "guid": "guid2", 69 "name": "staticfile_buildpack", 70 "state": "AWAITING_UPLOAD", 71 "stack": "cflinuxfs3", 72 "position": 2, 73 "enabled": false, 74 "locked": true, 75 "metadata": { 76 "labels": {} 77 } 78 } 79 ] 80 }`, server.URL()) 81 response2 := `{ 82 "pagination": { 83 "next": null 84 }, 85 "resources": [ 86 { 87 "guid": "guid3", 88 "name": "go_buildpack", 89 "state": "AWAITING_UPLOAD", 90 "stack": "cflinuxfs2", 91 "position": 3, 92 "enabled": true, 93 "locked": false, 94 "metadata": { 95 "labels": {} 96 } 97 } 98 ] 99 }` 100 101 server.AppendHandlers( 102 CombineHandlers( 103 VerifyRequest(http.MethodGet, "/v3/buildpacks", "names=some-buildpack-name"), 104 RespondWith(http.StatusOK, response1, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 105 ), 106 ) 107 server.AppendHandlers( 108 CombineHandlers( 109 VerifyRequest(http.MethodGet, "/v3/buildpacks", "names=some-buildpack-name&page=2&per_page=2"), 110 RespondWith(http.StatusOK, response2, http.Header{"X-Cf-Warnings": {"this is another warning"}}), 111 ), 112 ) 113 114 query = Query{ 115 Key: NameFilter, 116 Values: []string{"some-buildpack-name"}, 117 } 118 }) 119 120 It("returns the queried buildpacks and all warnings", func() { 121 Expect(executeErr).NotTo(HaveOccurred()) 122 123 Expect(buildpacks).To(ConsistOf( 124 Buildpack{ 125 Name: "ruby_buildpack", 126 GUID: "guid1", 127 Position: types.NullInt{Value: 1, IsSet: true}, 128 Enabled: types.NullBool{Value: true, IsSet: true}, 129 Locked: types.NullBool{Value: false, IsSet: true}, 130 Stack: "windows64", 131 State: "AWAITING_UPLOAD", 132 Metadata: &Metadata{Labels: map[string]types.NullString{}}, 133 }, 134 Buildpack{ 135 Name: "staticfile_buildpack", 136 GUID: "guid2", 137 Position: types.NullInt{Value: 2, IsSet: true}, 138 Enabled: types.NullBool{Value: false, IsSet: true}, 139 Locked: types.NullBool{Value: true, IsSet: true}, 140 Stack: "cflinuxfs3", 141 State: "AWAITING_UPLOAD", 142 Metadata: &Metadata{Labels: map[string]types.NullString{}}, 143 }, 144 Buildpack{ 145 Name: "go_buildpack", 146 GUID: "guid3", 147 Position: types.NullInt{Value: 3, IsSet: true}, 148 Enabled: types.NullBool{Value: true, IsSet: true}, 149 Locked: types.NullBool{Value: false, IsSet: true}, 150 Stack: "cflinuxfs2", 151 State: "AWAITING_UPLOAD", 152 Metadata: &Metadata{Labels: map[string]types.NullString{}}, 153 }, 154 )) 155 Expect(warnings).To(ConsistOf("this is a warning", "this is another warning")) 156 }) 157 }) 158 159 When("the cloud controller returns errors and warnings", func() { 160 BeforeEach(func() { 161 response := `{ 162 "errors": [ 163 { 164 "code": 10008, 165 "detail": "The request is semantically invalid: command presence", 166 "title": "CF-UnprocessableEntity" 167 }, 168 { 169 "code": 10010, 170 "detail": "buildpack not found", 171 "title": "CF-buildpackNotFound" 172 } 173 ] 174 }` 175 server.AppendHandlers( 176 CombineHandlers( 177 VerifyRequest(http.MethodGet, "/v3/buildpacks"), 178 RespondWith(http.StatusTeapot, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 179 ), 180 ) 181 }) 182 183 It("returns the error and all warnings", func() { 184 Expect(executeErr).To(MatchError(ccerror.MultiError{ 185 ResponseCode: http.StatusTeapot, 186 Errors: []ccerror.V3Error{ 187 { 188 Code: 10008, 189 Detail: "The request is semantically invalid: command presence", 190 Title: "CF-UnprocessableEntity", 191 }, 192 { 193 Code: 10010, 194 Detail: "buildpack not found", 195 Title: "CF-buildpackNotFound", 196 }, 197 }, 198 })) 199 Expect(warnings).To(ConsistOf("this is a warning")) 200 }) 201 }) 202 }) 203 204 Describe("CreateBuildpack", func() { 205 var ( 206 inputBuildpack Buildpack 207 208 bp Buildpack 209 warnings Warnings 210 executeErr error 211 ) 212 213 JustBeforeEach(func() { 214 bp, warnings, executeErr = client.CreateBuildpack(inputBuildpack) 215 }) 216 217 When("the buildpack is successfully created", func() { 218 BeforeEach(func() { 219 inputBuildpack = Buildpack{ 220 Name: "some-buildpack", 221 Stack: "some-stack", 222 } 223 response := `{ 224 "guid": "some-bp-guid", 225 "created_at": "2016-03-18T23:26:46Z", 226 "updated_at": "2016-10-17T20:00:42Z", 227 "name": "some-buildpack", 228 "state": "AWAITING_UPLOAD", 229 "filename": null, 230 "stack": "some-stack", 231 "position": 42, 232 "enabled": true, 233 "locked": false, 234 "links": { 235 "self": { 236 "href": "/v3/buildpacks/some-bp-guid" 237 }, 238 "upload": { 239 "href": "/v3/buildpacks/some-bp-guid/upload", 240 "method": "POST" 241 } 242 } 243 }` 244 245 expectedBody := map[string]interface{}{ 246 "name": "some-buildpack", 247 "stack": "some-stack", 248 } 249 server.AppendHandlers( 250 CombineHandlers( 251 VerifyRequest(http.MethodPost, "/v3/buildpacks"), 252 VerifyJSONRepresenting(expectedBody), 253 RespondWith(http.StatusCreated, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 254 ), 255 ) 256 }) 257 258 It("returns the created buildpack and warnings", func() { 259 Expect(executeErr).NotTo(HaveOccurred()) 260 Expect(warnings).To(ConsistOf("this is a warning")) 261 262 expectedBuildpack := Buildpack{ 263 GUID: "some-bp-guid", 264 Name: "some-buildpack", 265 Stack: "some-stack", 266 Enabled: types.NullBool{Value: true, IsSet: true}, 267 Filename: "", 268 Locked: types.NullBool{Value: false, IsSet: true}, 269 State: constant.BuildpackAwaitingUpload, 270 Position: types.NullInt{Value: 42, IsSet: true}, 271 Links: APILinks{ 272 "upload": APILink{ 273 Method: "POST", 274 HREF: "/v3/buildpacks/some-bp-guid/upload", 275 }, 276 "self": APILink{ 277 HREF: "/v3/buildpacks/some-bp-guid", 278 }, 279 }, 280 } 281 Expect(bp).To(Equal(expectedBuildpack)) 282 }) 283 }) 284 285 When("cc returns back an error or warnings", func() { 286 BeforeEach(func() { 287 inputBuildpack = Buildpack{} 288 response := ` { 289 "errors": [ 290 { 291 "code": 10008, 292 "detail": "The request is semantically invalid: command presence", 293 "title": "CF-UnprocessableEntity" 294 }, 295 { 296 "code": 10010, 297 "detail": "Buildpack not found", 298 "title": "CF-ResourceNotFound" 299 } 300 ] 301 }` 302 server.AppendHandlers( 303 CombineHandlers( 304 VerifyRequest(http.MethodPost, "/v3/buildpacks"), 305 RespondWith(http.StatusTeapot, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 306 ), 307 ) 308 }) 309 310 It("returns the error and all warnings", func() { 311 Expect(executeErr).To(MatchError(ccerror.MultiError{ 312 ResponseCode: http.StatusTeapot, 313 Errors: []ccerror.V3Error{ 314 { 315 Code: 10008, 316 Detail: "The request is semantically invalid: command presence", 317 Title: "CF-UnprocessableEntity", 318 }, 319 { 320 Code: 10010, 321 Detail: "Buildpack not found", 322 Title: "CF-ResourceNotFound", 323 }, 324 }, 325 })) 326 Expect(warnings).To(ConsistOf("this is a warning")) 327 }) 328 }) 329 }) 330 331 Describe("UploadBuildpack", func() { 332 var ( 333 jobURL JobURL 334 warnings Warnings 335 executeErr error 336 bpFile io.Reader 337 bpFilePath string 338 bpContent string 339 ) 340 341 BeforeEach(func() { 342 bpContent = "some-content" 343 bpFile = strings.NewReader(bpContent) 344 bpFilePath = "some/fake-buildpack.zip" 345 }) 346 347 JustBeforeEach(func() { 348 jobURL, warnings, executeErr = client.UploadBuildpack("some-buildpack-guid", bpFilePath, bpFile, int64(len(bpContent))) 349 }) 350 351 When("the upload is successful", func() { 352 BeforeEach(func() { 353 response := `{ 354 "metadata": { 355 "guid": "some-buildpack-guid", 356 "url": "/v3/buildpacks/buildpack-guid/upload" 357 }, 358 "entity": { 359 "guid": "some-buildpack-guid", 360 "status": "queued" 361 } 362 }` 363 364 verifyHeaderAndBody := func(_ http.ResponseWriter, req *http.Request) { 365 contentType := req.Header.Get("Content-Type") 366 Expect(contentType).To(MatchRegexp("multipart/form-data; boundary=[\\w\\d]+")) 367 368 defer req.Body.Close() 369 requestReader := multipart.NewReader(req.Body, contentType[30:]) 370 371 buildpackPart, err := requestReader.NextPart() 372 Expect(err).NotTo(HaveOccurred()) 373 374 Expect(buildpackPart.FormName()).To(Equal("bits")) 375 Expect(buildpackPart.FileName()).To(Equal("fake-buildpack.zip")) 376 377 defer buildpackPart.Close() 378 partContents, err := ioutil.ReadAll(buildpackPart) 379 Expect(err).ToNot(HaveOccurred()) 380 Expect(string(partContents)).To(Equal(bpContent)) 381 } 382 383 server.AppendHandlers( 384 CombineHandlers( 385 VerifyRequest(http.MethodPost, "/v3/buildpacks/some-buildpack-guid/upload"), 386 verifyHeaderAndBody, 387 RespondWith( 388 http.StatusAccepted, 389 response, 390 http.Header{ 391 "X-Cf-Warnings": {"this is a warning"}, 392 "Location": {"http://example.com/job-guid"}, 393 }, 394 ), 395 ), 396 ) 397 }) 398 399 It("returns the processing job URL and warnings", func() { 400 Expect(executeErr).ToNot(HaveOccurred()) 401 Expect(warnings).To(ConsistOf(Warnings{"this is a warning"})) 402 Expect(jobURL).To(Equal(JobURL("http://example.com/job-guid"))) 403 }) 404 }) 405 406 When("there is an error reading the buildpack", func() { 407 var ( 408 fakeReader *ccv3fakes.FakeReader 409 expectedErr error 410 ) 411 412 BeforeEach(func() { 413 expectedErr = errors.New("some read error") 414 fakeReader = new(ccv3fakes.FakeReader) 415 fakeReader.ReadReturns(0, expectedErr) 416 bpFile = fakeReader 417 418 server.AppendHandlers( 419 VerifyRequest(http.MethodPost, "/v3/buildpacks/some-buildpack-guid/upload"), 420 ) 421 }) 422 423 It("returns the error", func() { 424 Expect(executeErr).To(MatchError(expectedErr)) 425 }) 426 }) 427 428 When("the upload returns an error", func() { 429 BeforeEach(func() { 430 response := `{ 431 "errors": [{ 432 "detail": "The buildpack could not be found: some-buildpack-guid", 433 "title": "CF-ResourceNotFound", 434 "code": 10010 435 }] 436 }` 437 438 server.AppendHandlers( 439 CombineHandlers( 440 VerifyRequest(http.MethodPost, "/v3/buildpacks/some-buildpack-guid/upload"), 441 RespondWith(http.StatusNotFound, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 442 ), 443 ) 444 }) 445 446 It("returns the error and warnings", func() { 447 Expect(executeErr).To(MatchError( 448 ccerror.ResourceNotFoundError{ 449 Message: "The buildpack could not be found: some-buildpack-guid", 450 }, 451 )) 452 Expect(warnings).To(ConsistOf(Warnings{"this is a warning"})) 453 }) 454 }) 455 456 When("a retryable error occurs", func() { 457 BeforeEach(func() { 458 wrapper := &wrapper.CustomWrapper{ 459 CustomMake: func(connection cloudcontroller.Connection, request *cloudcontroller.Request, response *cloudcontroller.Response) error { 460 defer GinkgoRecover() // Since this will be running in a thread 461 462 if strings.HasSuffix(request.URL.String(), "/v3/buildpacks/some-buildpack-guid/upload") { 463 _, err := ioutil.ReadAll(request.Body) 464 Expect(err).ToNot(HaveOccurred()) 465 Expect(request.Body.Close()).ToNot(HaveOccurred()) 466 return request.ResetBody() 467 } 468 return connection.Make(request, response) 469 }, 470 } 471 472 client, _ = NewTestClient(Config{Wrappers: []ConnectionWrapper{wrapper}}) 473 }) 474 475 It("returns the PipeSeekError", func() { 476 Expect(executeErr).To(MatchError(ccerror.PipeSeekError{})) 477 }) 478 }) 479 480 When("an http error occurs mid-transfer", func() { 481 var expectedErr error 482 483 BeforeEach(func() { 484 expectedErr = errors.New("some read error") 485 486 wrapper := &wrapper.CustomWrapper{ 487 CustomMake: func(connection cloudcontroller.Connection, request *cloudcontroller.Request, response *cloudcontroller.Response) error { 488 defer GinkgoRecover() // Since this will be running in a thread 489 490 if strings.HasSuffix(request.URL.String(), "/v3/buildpacks/some-buildpack-guid/upload") { 491 defer request.Body.Close() 492 readBytes, err := ioutil.ReadAll(request.Body) 493 Expect(err).ToNot(HaveOccurred()) 494 Expect(len(readBytes)).To(BeNumerically(">", len(bpContent))) 495 return expectedErr 496 } 497 return connection.Make(request, response) 498 }, 499 } 500 501 client, _ = NewTestClient(Config{Wrappers: []ConnectionWrapper{wrapper}}) 502 }) 503 504 It("returns the http error", func() { 505 Expect(executeErr).To(MatchError(expectedErr)) 506 }) 507 }) 508 }) 509 510 Describe("UpdateBuildpack", func() { 511 var ( 512 inputBuildpack Buildpack 513 514 bp Buildpack 515 warnings Warnings 516 executeErr error 517 ) 518 519 JustBeforeEach(func() { 520 bp, warnings, executeErr = client.UpdateBuildpack(inputBuildpack) 521 }) 522 523 When("the buildpack is successfully created", func() { 524 BeforeEach(func() { 525 inputBuildpack = Buildpack{ 526 Name: "some-buildpack", 527 GUID: "some-bp-guid", 528 Stack: "some-stack", 529 Locked: types.NullBool{IsSet: true, Value: true}, 530 } 531 response := `{ 532 "guid": "some-bp-guid", 533 "created_at": "2016-03-18T23:26:46Z", 534 "updated_at": "2016-10-17T20:00:42Z", 535 "name": "some-buildpack", 536 "state": "AWAITING_UPLOAD", 537 "filename": null, 538 "stack": "some-stack", 539 "position": 42, 540 "enabled": true, 541 "locked": true, 542 "links": { 543 "self": { 544 "href": "/v3/buildpacks/some-bp-guid" 545 }, 546 "upload": { 547 "href": "/v3/buildpacks/some-bp-guid/upload", 548 "method": "POST" 549 } 550 } 551 }` 552 553 expectedBody := map[string]interface{}{ 554 "name": "some-buildpack", 555 "stack": "some-stack", 556 "locked": true, 557 } 558 server.AppendHandlers( 559 CombineHandlers( 560 VerifyRequest(http.MethodPatch, "/v3/buildpacks/some-bp-guid"), 561 VerifyJSONRepresenting(expectedBody), 562 RespondWith(http.StatusAccepted, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 563 ), 564 ) 565 }) 566 567 It("returns the created buildpack and warnings", func() { 568 Expect(executeErr).NotTo(HaveOccurred()) 569 Expect(warnings).To(ConsistOf("this is a warning")) 570 571 expectedBuildpack := Buildpack{ 572 GUID: "some-bp-guid", 573 Name: "some-buildpack", 574 Stack: "some-stack", 575 Enabled: types.NullBool{Value: true, IsSet: true}, 576 Filename: "", 577 Locked: types.NullBool{Value: true, IsSet: true}, 578 State: constant.BuildpackAwaitingUpload, 579 Position: types.NullInt{Value: 42, IsSet: true}, 580 Links: APILinks{ 581 "upload": APILink{ 582 Method: "POST", 583 HREF: "/v3/buildpacks/some-bp-guid/upload", 584 }, 585 "self": APILink{ 586 HREF: "/v3/buildpacks/some-bp-guid", 587 }, 588 }, 589 } 590 Expect(bp).To(Equal(expectedBuildpack)) 591 }) 592 }) 593 594 When("cc returns back an error or warnings", func() { 595 BeforeEach(func() { 596 inputBuildpack = Buildpack{ 597 GUID: "some-bp-guid", 598 } 599 response := ` { 600 "errors": [ 601 { 602 "code": 10008, 603 "detail": "The request is semantically invalid: command presence", 604 "title": "CF-UnprocessableEntity" 605 }, 606 { 607 "code": 10010, 608 "detail": "Buildpack not found", 609 "title": "CF-ResourceNotFound" 610 } 611 ] 612 }` 613 server.AppendHandlers( 614 CombineHandlers( 615 VerifyRequest(http.MethodPatch, "/v3/buildpacks/some-bp-guid"), 616 RespondWith(http.StatusTeapot, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 617 ), 618 ) 619 }) 620 621 It("returns the error and all warnings", func() { 622 Expect(executeErr).To(MatchError(ccerror.MultiError{ 623 ResponseCode: http.StatusTeapot, 624 Errors: []ccerror.V3Error{ 625 { 626 Code: 10008, 627 Detail: "The request is semantically invalid: command presence", 628 Title: "CF-UnprocessableEntity", 629 }, 630 { 631 Code: 10010, 632 Detail: "Buildpack not found", 633 Title: "CF-ResourceNotFound", 634 }, 635 }, 636 })) 637 Expect(warnings).To(ConsistOf("this is a warning")) 638 }) 639 }) 640 }) 641 642 Describe("DeleteBuildpacks", func() { 643 var ( 644 buildpackGUID = "some-guid" 645 646 jobURL JobURL 647 warnings Warnings 648 executeErr error 649 ) 650 651 JustBeforeEach(func() { 652 jobURL, warnings, executeErr = client.DeleteBuildpack(buildpackGUID) 653 }) 654 655 When("buildpacks exist", func() { 656 BeforeEach(func() { 657 server.AppendHandlers( 658 CombineHandlers( 659 VerifyRequest(http.MethodDelete, "/v3/buildpacks/"+buildpackGUID), 660 RespondWith(http.StatusAccepted, "{}", http.Header{"X-Cf-Warnings": {"this is a warning"}, "Location": {"some-job-url"}}), 661 ), 662 ) 663 }) 664 665 It("returns the delete job URL and all warnings", func() { 666 Expect(executeErr).NotTo(HaveOccurred()) 667 668 Expect(jobURL).To(Equal(JobURL("some-job-url"))) 669 Expect(warnings).To(ConsistOf("this is a warning")) 670 }) 671 }) 672 673 When("the cloud controller returns errors and warnings", func() { 674 BeforeEach(func() { 675 response := `{ 676 "errors": [ 677 { 678 "code": 10008, 679 "detail": "The request is semantically invalid: command presence", 680 "title": "CF-UnprocessableEntity" 681 }, 682 { 683 "code": 10010, 684 "detail": "buildpack not found", 685 "title": "CF-buildpackNotFound" 686 } 687 ] 688 }` 689 server.AppendHandlers( 690 CombineHandlers( 691 VerifyRequest(http.MethodDelete, "/v3/buildpacks/"+buildpackGUID), 692 RespondWith(http.StatusTeapot, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 693 ), 694 ) 695 }) 696 697 It("returns the error and all warnings", func() { 698 Expect(executeErr).To(MatchError(ccerror.MultiError{ 699 ResponseCode: http.StatusTeapot, 700 Errors: []ccerror.V3Error{ 701 { 702 Code: 10008, 703 Detail: "The request is semantically invalid: command presence", 704 Title: "CF-UnprocessableEntity", 705 }, 706 { 707 Code: 10010, 708 Detail: "buildpack not found", 709 Title: "CF-buildpackNotFound", 710 }, 711 }, 712 })) 713 Expect(warnings).To(ConsistOf("this is a warning")) 714 }) 715 }) 716 }) 717 })