github.com/sleungcy/cli@v7.1.0+incompatible/api/cloudcontroller/ccv2/buildpack_test.go (about) 1 package ccv2_test 2 3 import ( 4 "errors" 5 "io" 6 "io/ioutil" 7 "mime/multipart" 8 "net/http" 9 "strings" 10 11 "code.cloudfoundry.org/cli/api/cloudcontroller" 12 "code.cloudfoundry.org/cli/api/cloudcontroller/ccerror" 13 . "code.cloudfoundry.org/cli/api/cloudcontroller/ccv2" 14 "code.cloudfoundry.org/cli/api/cloudcontroller/ccv2/ccv2fakes" 15 "code.cloudfoundry.org/cli/api/cloudcontroller/ccv2/constant" 16 "code.cloudfoundry.org/cli/api/cloudcontroller/wrapper" 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("Buildpack", func() { 24 var client *Client 25 26 BeforeEach(func() { 27 client = NewTestClient() 28 }) 29 30 Describe("CreateBuildpack", func() { 31 var ( 32 inputBuildpack Buildpack 33 34 resultBuildpack Buildpack 35 warnings Warnings 36 executeErr error 37 ) 38 39 JustBeforeEach(func() { 40 resultBuildpack, warnings, executeErr = client.CreateBuildpack(inputBuildpack) 41 }) 42 43 When("the creation is successful", func() { 44 When("all the properties are passed", func() { 45 BeforeEach(func() { 46 inputBuildpack = Buildpack{ 47 Name: "potato", 48 Position: types.NullInt{IsSet: true, Value: 1}, 49 Enabled: types.NullBool{IsSet: true, Value: true}, 50 Stack: "foobar", 51 } 52 53 response := ` 54 { 55 "metadata": { 56 "guid": "some-guid" 57 }, 58 "entity": { 59 "name": "potato", 60 "stack": "foobar", 61 "position": 1, 62 "enabled": true 63 } 64 }` 65 requestBody := map[string]interface{}{ 66 "name": "potato", 67 "position": 1, 68 "enabled": true, 69 "stack": "foobar", 70 } 71 server.AppendHandlers( 72 CombineHandlers( 73 VerifyRequest(http.MethodPost, "/v2/buildpacks"), 74 VerifyJSONRepresenting(requestBody), 75 RespondWith(http.StatusCreated, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 76 ), 77 ) 78 }) 79 80 It("creates a buildpack and returns it with any warnings", func() { 81 Expect(executeErr).ToNot(HaveOccurred()) 82 validateV2InfoPlusNumberOfRequests(2) 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(2) 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("UpdateBuildpack", func() { 322 var ( 323 buildpack Buildpack 324 updatedBuildpack Buildpack 325 warnings Warnings 326 executeErr error 327 ) 328 329 JustBeforeEach(func() { 330 updatedBuildpack, warnings, executeErr = client.UpdateBuildpack(buildpack) 331 }) 332 333 When("the buildpack exists", func() { 334 When("all the properties are provided", func() { 335 When("the provided properties are golang non-zero values", func() { 336 BeforeEach(func() { 337 buildpack = Buildpack{ 338 Name: "some-bp-name", 339 Position: types.NullInt{IsSet: true, Value: 10}, 340 Enabled: types.NullBool{IsSet: true, Value: true}, 341 Locked: types.NullBool{IsSet: true, Value: true}, 342 GUID: "some-bp-guid", 343 } 344 345 response := ` 346 { 347 "metadata": { 348 "guid": "some-bp-guid" 349 }, 350 "entity": { 351 "name": "some-bp-name", 352 "stack": null, 353 "position": 10, 354 "enabled": true, 355 "locked": true 356 } 357 } 358 ` 359 360 requestBody := map[string]interface{}{ 361 "name": "some-bp-name", 362 "position": 10, 363 "enabled": true, 364 "locked": true, 365 } 366 367 server.AppendHandlers( 368 CombineHandlers( 369 VerifyRequest(http.MethodPut, "/v2/buildpacks/some-bp-guid"), 370 VerifyJSONRepresenting(requestBody), 371 RespondWith(http.StatusOK, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 372 ), 373 ) 374 }) 375 376 It("updates and returns the updated buildpack", func() { 377 Expect(executeErr).ToNot(HaveOccurred()) 378 validateV2InfoPlusNumberOfRequests(2) 379 Expect(warnings).To(ConsistOf("this is a warning")) 380 Expect(updatedBuildpack).To(Equal(buildpack)) 381 }) 382 }) 383 384 When("the provided properties are golang zero values", func() { 385 BeforeEach(func() { 386 buildpack = Buildpack{ 387 Name: "some-bp-name", 388 GUID: "some-bp-guid", 389 Position: types.NullInt{IsSet: true, Value: 0}, 390 Enabled: types.NullBool{IsSet: true, Value: false}, 391 Locked: types.NullBool{IsSet: true, Value: false}, 392 } 393 394 response := ` 395 { 396 "metadata": { 397 "guid": "some-bp-guid" 398 }, 399 "entity": { 400 "name": "some-bp-name", 401 "stack": null, 402 "position": 0, 403 "enabled": false, 404 "locked": false 405 } 406 } 407 ` 408 requestBody := map[string]interface{}{ 409 "name": "some-bp-name", 410 "position": 0, 411 "enabled": false, 412 "locked": false, 413 } 414 415 server.AppendHandlers( 416 CombineHandlers( 417 VerifyRequest(http.MethodPut, "/v2/buildpacks/some-bp-guid"), 418 VerifyJSONRepresenting(requestBody), 419 RespondWith(http.StatusOK, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 420 ), 421 ) 422 }) 423 424 It("updates and returns the updated buildpack", func() { 425 Expect(executeErr).ToNot(HaveOccurred()) 426 validateV2InfoPlusNumberOfRequests(2) 427 Expect(warnings).To(ConsistOf("this is a warning")) 428 Expect(updatedBuildpack).To(Equal(buildpack)) 429 }) 430 }) 431 }) 432 }) 433 434 When("the buildpack does not exist", func() { 435 BeforeEach(func() { 436 response := `{ 437 "description": "buildpack not found", 438 "error_code": "CF-NotFound", 439 "code": 10000 440 }` 441 442 server.AppendHandlers( 443 CombineHandlers( 444 VerifyRequest(http.MethodPut, "/v2/buildpacks/some-bp-guid"), 445 RespondWith(http.StatusNotFound, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 446 ), 447 ) 448 449 buildpack = Buildpack{ 450 GUID: "some-bp-guid", 451 } 452 453 }) 454 455 It("returns the error and warnings", func() { 456 Expect(executeErr).To(MatchError(ccerror.ResourceNotFoundError{ 457 Message: "buildpack not found", 458 })) 459 Expect(warnings).To(ConsistOf(Warnings{"this is a warning"})) 460 }) 461 }) 462 463 When("the API errors", func() { 464 BeforeEach(func() { 465 response := `{ 466 "code": 10001, 467 "description": "Some Error", 468 "error_code": "CF-SomeError" 469 }` 470 471 server.AppendHandlers( 472 CombineHandlers( 473 VerifyRequest(http.MethodPut, "/v2/buildpacks/some-bp-guid"), 474 RespondWith(http.StatusTeapot, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 475 ), 476 ) 477 478 buildpack = Buildpack{ 479 GUID: "some-bp-guid", 480 } 481 }) 482 483 It("returns the error and warnings", func() { 484 Expect(executeErr).To(MatchError(ccerror.V2UnexpectedResponseError{ 485 ResponseCode: http.StatusTeapot, 486 V2ErrorResponse: ccerror.V2ErrorResponse{ 487 Code: 10001, 488 Description: "Some Error", 489 ErrorCode: "CF-SomeError", 490 }, 491 })) 492 Expect(warnings).To(ConsistOf(Warnings{"this is a warning"})) 493 }) 494 }) 495 }) 496 497 Describe("UploadBuildpack", func() { 498 var ( 499 warnings Warnings 500 executeErr error 501 bpFile io.Reader 502 bpFilePath string 503 bpContent string 504 ) 505 506 BeforeEach(func() { 507 bpContent = "some-content" 508 bpFile = strings.NewReader(bpContent) 509 bpFilePath = "some/fake-buildpack.zip" 510 }) 511 512 JustBeforeEach(func() { 513 warnings, executeErr = client.UploadBuildpack("some-buildpack-guid", bpFilePath, bpFile, int64(len(bpContent))) 514 }) 515 516 When("the upload is successful", func() { 517 BeforeEach(func() { 518 response := `{ 519 "metadata": { 520 "guid": "some-buildpack-guid", 521 "url": "/v2/buildpacks/buildpack-guid/bits" 522 }, 523 "entity": { 524 "guid": "some-buildpack-guid", 525 "status": "queued" 526 } 527 }` 528 529 verifyHeaderAndBody := func(_ http.ResponseWriter, req *http.Request) { 530 contentType := req.Header.Get("Content-Type") 531 Expect(contentType).To(MatchRegexp("multipart/form-data; boundary=[\\w\\d]+")) 532 533 defer req.Body.Close() 534 requestReader := multipart.NewReader(req.Body, contentType[30:]) 535 536 buildpackPart, err := requestReader.NextPart() 537 Expect(err).NotTo(HaveOccurred()) 538 539 Expect(buildpackPart.FormName()).To(Equal("buildpack")) 540 Expect(buildpackPart.FileName()).To(Equal("fake-buildpack.zip")) 541 542 defer buildpackPart.Close() 543 partContents, err := ioutil.ReadAll(buildpackPart) 544 Expect(err).ToNot(HaveOccurred()) 545 Expect(string(partContents)).To(Equal(bpContent)) 546 } 547 548 server.AppendHandlers( 549 CombineHandlers( 550 VerifyRequest(http.MethodPut, "/v2/buildpacks/some-buildpack-guid/bits"), 551 verifyHeaderAndBody, 552 RespondWith(http.StatusOK, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 553 ), 554 ) 555 }) 556 557 It("returns warnings", func() { 558 Expect(warnings).To(ConsistOf(Warnings{"this is a warning"})) 559 Expect(executeErr).ToNot(HaveOccurred()) 560 }) 561 }) 562 563 When("there is an error reading the buildpack", func() { 564 var ( 565 fakeReader *ccv2fakes.FakeReader 566 expectedErr error 567 ) 568 569 BeforeEach(func() { 570 expectedErr = errors.New("some read error") 571 fakeReader = new(ccv2fakes.FakeReader) 572 fakeReader.ReadReturns(0, expectedErr) 573 bpFile = fakeReader 574 575 server.AppendHandlers( 576 VerifyRequest(http.MethodPut, "/v2/buildpacks/some-buildpack-guid/bits"), 577 ) 578 }) 579 580 It("returns the error", func() { 581 Expect(executeErr).To(MatchError(expectedErr)) 582 }) 583 }) 584 585 When("the upload returns an error", func() { 586 BeforeEach(func() { 587 response := `{ 588 "code": 30003, 589 "description": "The buildpack could not be found: some-buildpack-guid", 590 "error_code": "CF-Banana" 591 }` 592 593 server.AppendHandlers( 594 CombineHandlers( 595 VerifyRequest(http.MethodPut, "/v2/buildpacks/some-buildpack-guid/bits"), 596 RespondWith(http.StatusNotFound, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 597 ), 598 ) 599 }) 600 601 It("returns the error and warnings", func() { 602 Expect(executeErr).To(MatchError(ccerror.ResourceNotFoundError{Message: "The buildpack could not be found: some-buildpack-guid"})) 603 Expect(warnings).To(ConsistOf(Warnings{"this is a warning"})) 604 }) 605 }) 606 607 When("a retryable error occurs", func() { 608 BeforeEach(func() { 609 wrapper := &wrapper.CustomWrapper{ 610 CustomMake: func(connection cloudcontroller.Connection, request *cloudcontroller.Request, response *cloudcontroller.Response) error { 611 defer GinkgoRecover() // Since this will be running in a thread 612 613 if strings.HasSuffix(request.URL.String(), "/v2/buildpacks/some-buildpack-guid/bits") { 614 _, err := ioutil.ReadAll(request.Body) 615 Expect(err).ToNot(HaveOccurred()) 616 Expect(request.Body.Close()).ToNot(HaveOccurred()) 617 return request.ResetBody() 618 } 619 return connection.Make(request, response) 620 }, 621 } 622 623 client = NewTestClient(Config{Wrappers: []ConnectionWrapper{wrapper}}) 624 }) 625 626 It("returns the PipeSeekError", func() { 627 Expect(executeErr).To(MatchError(ccerror.PipeSeekError{})) 628 }) 629 }) 630 631 When("an http error occurs mid-transfer", func() { 632 var expectedErr error 633 634 BeforeEach(func() { 635 expectedErr = errors.New("some read error") 636 637 wrapper := &wrapper.CustomWrapper{ 638 CustomMake: func(connection cloudcontroller.Connection, request *cloudcontroller.Request, response *cloudcontroller.Response) error { 639 defer GinkgoRecover() // Since this will be running in a thread 640 641 if strings.HasSuffix(request.URL.String(), "/v2/buildpacks/some-buildpack-guid/bits") { 642 defer request.Body.Close() 643 readBytes, err := ioutil.ReadAll(request.Body) 644 Expect(err).ToNot(HaveOccurred()) 645 Expect(len(readBytes)).To(BeNumerically(">", len(bpContent))) 646 return expectedErr 647 } 648 return connection.Make(request, response) 649 }, 650 } 651 652 client = NewTestClient(Config{Wrappers: []ConnectionWrapper{wrapper}}) 653 }) 654 655 It("returns the http error", func() { 656 Expect(executeErr).To(MatchError(expectedErr)) 657 }) 658 }) 659 }) 660 })