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