github.com/cloudfoundry-community/cloudfoundry-cli@v6.44.1-0.20240130060226-cda5ed8e89a5+incompatible/api/cloudcontroller/ccv3/package_test.go (about) 1 package ccv3_test 2 3 import ( 4 "bytes" 5 "code.cloudfoundry.org/cli/resources" 6 "encoding/json" 7 "errors" 8 "fmt" 9 "io" 10 "io/ioutil" 11 "mime/multipart" 12 "net/http" 13 "os" 14 "strings" 15 16 "code.cloudfoundry.org/cli/api/cloudcontroller" 17 "code.cloudfoundry.org/cli/api/cloudcontroller/ccerror" 18 . "code.cloudfoundry.org/cli/api/cloudcontroller/ccv3" 19 "code.cloudfoundry.org/cli/api/cloudcontroller/ccv3/ccv3fakes" 20 "code.cloudfoundry.org/cli/api/cloudcontroller/ccv3/constant" 21 "code.cloudfoundry.org/cli/api/cloudcontroller/wrapper" 22 . "github.com/onsi/ginkgo" 23 . "github.com/onsi/gomega" 24 . "github.com/onsi/gomega/gbytes" 25 . "github.com/onsi/gomega/ghttp" 26 ) 27 28 var _ = Describe("Package", func() { 29 var client *Client 30 31 BeforeEach(func() { 32 client, _ = NewTestClient() 33 }) 34 35 Describe("CreatePackage", func() { 36 var ( 37 inputPackage resources.Package 38 39 pkg resources.Package 40 warnings Warnings 41 executeErr error 42 ) 43 44 JustBeforeEach(func() { 45 pkg, warnings, executeErr = client.CreatePackage(inputPackage) 46 }) 47 48 When("the package successfully is created", func() { 49 When("creating a docker package", func() { 50 BeforeEach(func() { 51 inputPackage = resources.Package{ 52 Type: constant.PackageTypeDocker, 53 Relationships: resources.Relationships{ 54 constant.RelationshipTypeApplication: resources.Relationship{GUID: "some-app-guid"}, 55 }, 56 DockerImage: "some-docker-image", 57 DockerUsername: "some-username", 58 DockerPassword: "some-password", 59 } 60 61 response := `{ 62 "data": { 63 "image": "some-docker-image", 64 "username": "some-username", 65 "password": "some-password" 66 }, 67 "guid": "some-pkg-guid", 68 "type": "docker", 69 "state": "PROCESSING_UPLOAD", 70 "links": { 71 "upload": { 72 "href": "some-package-upload-url", 73 "method": "POST" 74 } 75 } 76 }` 77 78 expectedBody := map[string]interface{}{ 79 "type": "docker", 80 "data": map[string]string{ 81 "image": "some-docker-image", 82 "username": "some-username", 83 "password": "some-password", 84 }, 85 "relationships": map[string]interface{}{ 86 "app": map[string]interface{}{ 87 "data": map[string]string{ 88 "guid": "some-app-guid", 89 }, 90 }, 91 }, 92 } 93 server.AppendHandlers( 94 CombineHandlers( 95 VerifyRequest(http.MethodPost, "/v3/packages"), 96 VerifyJSONRepresenting(expectedBody), 97 RespondWith(http.StatusCreated, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 98 ), 99 ) 100 }) 101 102 It("returns the created package and warnings", func() { 103 Expect(executeErr).NotTo(HaveOccurred()) 104 Expect(warnings).To(ConsistOf("this is a warning")) 105 106 expectedPackage := resources.Package{ 107 GUID: "some-pkg-guid", 108 Type: constant.PackageTypeDocker, 109 State: constant.PackageProcessingUpload, 110 Links: map[string]resources.APILink{ 111 "upload": resources.APILink{HREF: "some-package-upload-url", Method: http.MethodPost}, 112 }, 113 DockerImage: "some-docker-image", 114 DockerUsername: "some-username", 115 DockerPassword: "some-password", 116 } 117 Expect(pkg).To(Equal(expectedPackage)) 118 }) 119 }) 120 121 When("creating a bits package", func() { 122 BeforeEach(func() { 123 inputPackage = resources.Package{ 124 Type: constant.PackageTypeBits, 125 Relationships: resources.Relationships{ 126 constant.RelationshipTypeApplication: resources.Relationship{GUID: "some-app-guid"}, 127 }, 128 } 129 response := `{ 130 "guid": "some-pkg-guid", 131 "type": "bits", 132 "state": "PROCESSING_UPLOAD", 133 "links": { 134 "upload": { 135 "href": "some-package-upload-url", 136 "method": "POST" 137 } 138 } 139 }` 140 141 expectedBody := map[string]interface{}{ 142 "type": "bits", 143 "relationships": map[string]interface{}{ 144 "app": map[string]interface{}{ 145 "data": map[string]string{ 146 "guid": "some-app-guid", 147 }, 148 }, 149 }, 150 } 151 server.AppendHandlers( 152 CombineHandlers( 153 VerifyRequest(http.MethodPost, "/v3/packages"), 154 VerifyJSONRepresenting(expectedBody), 155 RespondWith(http.StatusCreated, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 156 ), 157 ) 158 }) 159 160 It("omits data, and returns the created package and warnings", func() { 161 Expect(executeErr).NotTo(HaveOccurred()) 162 Expect(warnings).To(ConsistOf("this is a warning")) 163 164 expectedPackage := resources.Package{ 165 GUID: "some-pkg-guid", 166 Type: constant.PackageTypeBits, 167 State: constant.PackageProcessingUpload, 168 Links: map[string]resources.APILink{ 169 "upload": resources.APILink{HREF: "some-package-upload-url", Method: http.MethodPost}, 170 }, 171 } 172 Expect(pkg).To(Equal(expectedPackage)) 173 }) 174 }) 175 }) 176 177 When("cc returns back an error or warnings", func() { 178 BeforeEach(func() { 179 inputPackage = resources.Package{} 180 response := ` { 181 "errors": [ 182 { 183 "code": 10008, 184 "detail": "The request is semantically invalid: command presence", 185 "title": "CF-UnprocessableEntity" 186 }, 187 { 188 "code": 10010, 189 "detail": "Package not found", 190 "title": "CF-ResourceNotFound" 191 } 192 ] 193 }` 194 server.AppendHandlers( 195 CombineHandlers( 196 VerifyRequest(http.MethodPost, "/v3/packages"), 197 RespondWith(http.StatusTeapot, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 198 ), 199 ) 200 }) 201 202 It("returns the error and all warnings", func() { 203 Expect(executeErr).To(MatchError(ccerror.MultiError{ 204 ResponseCode: http.StatusTeapot, 205 Errors: []ccerror.V3Error{ 206 { 207 Code: 10008, 208 Detail: "The request is semantically invalid: command presence", 209 Title: "CF-UnprocessableEntity", 210 }, 211 { 212 Code: 10010, 213 Detail: "Package not found", 214 Title: "CF-ResourceNotFound", 215 }, 216 }, 217 })) 218 Expect(warnings).To(ConsistOf("this is a warning")) 219 }) 220 }) 221 }) 222 223 Describe("GetPackage", func() { 224 var ( 225 pkg resources.Package 226 warnings Warnings 227 executeErr error 228 ) 229 230 JustBeforeEach(func() { 231 pkg, warnings, executeErr = client.GetPackage("some-pkg-guid") 232 }) 233 234 When("the package exists", func() { 235 BeforeEach(func() { 236 response := `{ 237 "guid": "some-pkg-guid", 238 "state": "PROCESSING_UPLOAD", 239 "links": { 240 "upload": { 241 "href": "some-package-upload-url", 242 "method": "POST" 243 } 244 } 245 }` 246 server.AppendHandlers( 247 CombineHandlers( 248 VerifyRequest(http.MethodGet, "/v3/packages/some-pkg-guid"), 249 RespondWith(http.StatusOK, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 250 ), 251 ) 252 }) 253 254 It("returns the queried package and all warnings", func() { 255 Expect(executeErr).NotTo(HaveOccurred()) 256 257 expectedPackage := resources.Package{ 258 GUID: "some-pkg-guid", 259 State: constant.PackageProcessingUpload, 260 Links: map[string]resources.APILink{ 261 "upload": resources.APILink{HREF: "some-package-upload-url", Method: http.MethodPost}, 262 }, 263 } 264 Expect(pkg).To(Equal(expectedPackage)) 265 Expect(warnings).To(ConsistOf("this is a warning")) 266 }) 267 }) 268 269 When("the cloud controller returns errors and warnings", func() { 270 BeforeEach(func() { 271 response := `{ 272 "errors": [ 273 { 274 "code": 10008, 275 "detail": "The request is semantically invalid: command presence", 276 "title": "CF-UnprocessableEntity" 277 }, 278 { 279 "code": 10010, 280 "detail": "Package not found", 281 "title": "CF-ResourceNotFound" 282 } 283 ] 284 }` 285 server.AppendHandlers( 286 CombineHandlers( 287 VerifyRequest(http.MethodGet, "/v3/packages/some-pkg-guid"), 288 RespondWith(http.StatusTeapot, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 289 ), 290 ) 291 }) 292 293 It("returns the error and all warnings", func() { 294 Expect(executeErr).To(MatchError(ccerror.MultiError{ 295 ResponseCode: http.StatusTeapot, 296 Errors: []ccerror.V3Error{ 297 { 298 Code: 10008, 299 Detail: "The request is semantically invalid: command presence", 300 Title: "CF-UnprocessableEntity", 301 }, 302 { 303 Code: 10010, 304 Detail: "Package not found", 305 Title: "CF-ResourceNotFound", 306 }, 307 }, 308 })) 309 Expect(warnings).To(ConsistOf("this is a warning")) 310 }) 311 }) 312 }) 313 314 Describe("GetPackages", func() { 315 var ( 316 pkgs []resources.Package 317 warnings Warnings 318 executeErr error 319 ) 320 321 JustBeforeEach(func() { 322 pkgs, warnings, executeErr = client.GetPackages(Query{Key: AppGUIDFilter, Values: []string{"some-app-guid"}}) 323 }) 324 325 When("cloud controller returns list of packages", func() { 326 BeforeEach(func() { 327 response := `{ 328 "resources": [ 329 { 330 "guid": "some-pkg-guid-1", 331 "type": "bits", 332 "state": "PROCESSING_UPLOAD", 333 "created_at": "2017-08-14T21:16:12Z", 334 "links": { 335 "upload": { 336 "href": "some-pkg-upload-url-1", 337 "method": "POST" 338 } 339 } 340 }, 341 { 342 "guid": "some-pkg-guid-2", 343 "type": "bits", 344 "state": "READY", 345 "created_at": "2017-08-14T21:20:13Z", 346 "links": { 347 "upload": { 348 "href": "some-pkg-upload-url-2", 349 "method": "POST" 350 } 351 } 352 } 353 ] 354 }` 355 server.AppendHandlers( 356 CombineHandlers( 357 VerifyRequest(http.MethodGet, "/v3/packages", "app_guids=some-app-guid"), 358 RespondWith(http.StatusOK, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 359 ), 360 ) 361 }) 362 363 It("returns the queried packages and all warnings", func() { 364 Expect(executeErr).NotTo(HaveOccurred()) 365 366 Expect(pkgs).To(Equal([]resources.Package{ 367 { 368 GUID: "some-pkg-guid-1", 369 Type: constant.PackageTypeBits, 370 State: constant.PackageProcessingUpload, 371 CreatedAt: "2017-08-14T21:16:12Z", 372 Links: map[string]resources.APILink{ 373 "upload": {HREF: "some-pkg-upload-url-1", Method: http.MethodPost}, 374 }, 375 }, 376 { 377 GUID: "some-pkg-guid-2", 378 Type: constant.PackageTypeBits, 379 State: constant.PackageReady, 380 CreatedAt: "2017-08-14T21:20:13Z", 381 Links: map[string]resources.APILink{ 382 "upload": {HREF: "some-pkg-upload-url-2", Method: http.MethodPost}, 383 }, 384 }, 385 })) 386 Expect(warnings).To(ConsistOf("this is a warning")) 387 }) 388 }) 389 390 When("the cloud controller returns errors and warnings", func() { 391 BeforeEach(func() { 392 response := `{ 393 "errors": [ 394 { 395 "code": 10008, 396 "detail": "The request is semantically invalid: command presence", 397 "title": "CF-UnprocessableEntity" 398 }, 399 { 400 "code": 10010, 401 "detail": "Package not found", 402 "title": "CF-ResourceNotFound" 403 } 404 ] 405 }` 406 server.AppendHandlers( 407 CombineHandlers( 408 VerifyRequest(http.MethodGet, "/v3/packages", "app_guids=some-app-guid"), 409 RespondWith(http.StatusTeapot, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 410 ), 411 ) 412 }) 413 414 It("returns the error and all warnings", func() { 415 Expect(executeErr).To(MatchError(ccerror.MultiError{ 416 ResponseCode: http.StatusTeapot, 417 Errors: []ccerror.V3Error{ 418 { 419 Code: 10008, 420 Detail: "The request is semantically invalid: command presence", 421 Title: "CF-UnprocessableEntity", 422 }, 423 { 424 Code: 10010, 425 Detail: "Package not found", 426 Title: "CF-ResourceNotFound", 427 }, 428 }, 429 })) 430 Expect(warnings).To(ConsistOf("this is a warning")) 431 }) 432 }) 433 }) 434 435 Describe("UploadBitsPackage", func() { 436 var ( 437 inputPackage resources.Package 438 ) 439 440 BeforeEach(func() { 441 client, _ = NewTestClient() 442 443 inputPackage = resources.Package{ 444 Links: map[string]resources.APILink{ 445 "upload": { 446 HREF: fmt.Sprintf("%s/v3/my-special-endpoint/some-pkg-guid/upload", server.URL()), 447 Method: http.MethodPost, 448 }, 449 }, 450 } 451 }) 452 453 When("the upload is successful", func() { 454 var ( 455 resourcesList []Resource 456 readerBody []byte 457 verifyHeaderAndBody func(http.ResponseWriter, *http.Request) 458 ) 459 460 BeforeEach(func() { 461 resourcesList = []Resource{ 462 {FilePath: "foo"}, 463 {FilePath: "bar"}, 464 } 465 466 response := `{ 467 "guid": "some-package-guid", 468 "type": "bits", 469 "state": "PROCESSING_UPLOAD" 470 }` 471 472 server.AppendHandlers( 473 CombineHandlers( 474 VerifyRequest(http.MethodPost, "/v3/my-special-endpoint/some-pkg-guid/upload"), 475 func(writer http.ResponseWriter, req *http.Request) { 476 verifyHeaderAndBody(writer, req) 477 }, 478 RespondWith(http.StatusOK, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 479 ), 480 ) 481 }) 482 483 When("the upload has application bits to upload", func() { 484 var reader io.Reader 485 486 BeforeEach(func() { 487 readerBody = []byte("hello world") 488 reader = bytes.NewReader(readerBody) 489 490 verifyHeaderAndBody = func(_ http.ResponseWriter, req *http.Request) { 491 contentType := req.Header.Get("Content-Type") 492 Expect(contentType).To(MatchRegexp("multipart/form-data; boundary=[\\w\\d]+")) 493 494 defer req.Body.Close() 495 requestReader := multipart.NewReader(req.Body, contentType[30:]) 496 497 // Verify that matched resourcesList are sent properly 498 resourcesPart, err := requestReader.NextPart() 499 Expect(err).NotTo(HaveOccurred()) 500 501 Expect(resourcesPart.FormName()).To(Equal("resourcesList")) 502 503 defer resourcesPart.Close() 504 expectedJSON, err := json.Marshal(resourcesList) 505 Expect(err).NotTo(HaveOccurred()) 506 Expect(ioutil.ReadAll(resourcesPart)).To(MatchJSON(expectedJSON)) 507 508 // Verify that the application bits are sent properly 509 resourcesPart, err = requestReader.NextPart() 510 Expect(err).NotTo(HaveOccurred()) 511 512 Expect(resourcesPart.FormName()).To(Equal("bits")) 513 Expect(resourcesPart.FileName()).To(Equal("package.zip")) 514 515 defer resourcesPart.Close() 516 Expect(ioutil.ReadAll(resourcesPart)).To(Equal(readerBody)) 517 } 518 }) 519 520 It("returns the created job and warnings", func() { 521 pkg, warnings, err := client.UploadBitsPackage(inputPackage, resourcesList, reader, int64(len(readerBody))) 522 Expect(err).NotTo(HaveOccurred()) 523 Expect(warnings).To(ConsistOf("this is a warning")) 524 Expect(pkg).To(Equal(resources.Package{ 525 GUID: "some-package-guid", 526 Type: constant.PackageTypeBits, 527 State: constant.PackageProcessingUpload, 528 })) 529 }) 530 }) 531 532 When("there are no application bits to upload", func() { 533 BeforeEach(func() { 534 verifyHeaderAndBody = func(_ http.ResponseWriter, req *http.Request) { 535 contentType := req.Header.Get("Content-Type") 536 Expect(contentType).To(MatchRegexp("multipart/form-data; boundary=[\\w\\d]+")) 537 538 defer req.Body.Close() 539 requestReader := multipart.NewReader(req.Body, contentType[30:]) 540 541 // Verify that matched resourcesList are sent properly 542 resourcesPart, err := requestReader.NextPart() 543 Expect(err).NotTo(HaveOccurred()) 544 545 Expect(resourcesPart.FormName()).To(Equal("resourcesList")) 546 547 defer resourcesPart.Close() 548 expectedJSON, err := json.Marshal(resourcesList) 549 Expect(err).NotTo(HaveOccurred()) 550 Expect(ioutil.ReadAll(resourcesPart)).To(MatchJSON(expectedJSON)) 551 552 // Verify that the application bits are not sent 553 _, err = requestReader.NextPart() 554 Expect(err).To(MatchError(io.EOF)) 555 } 556 }) 557 558 It("does not send the application bits", func() { 559 pkg, warnings, err := client.UploadBitsPackage(inputPackage, resourcesList, nil, 33513531353) 560 Expect(err).NotTo(HaveOccurred()) 561 Expect(warnings).To(ConsistOf("this is a warning")) 562 Expect(pkg).To(Equal(resources.Package{ 563 GUID: "some-package-guid", 564 Type: constant.PackageTypeBits, 565 State: constant.PackageProcessingUpload, 566 })) 567 }) 568 }) 569 }) 570 571 When("the CC returns an error", func() { 572 BeforeEach(func() { 573 response := ` { 574 "errors": [ 575 { 576 "code": 10008, 577 "detail": "Banana", 578 "title": "CF-Banana" 579 } 580 ] 581 }` 582 583 server.AppendHandlers( 584 CombineHandlers( 585 VerifyRequest(http.MethodPost, "/v3/my-special-endpoint/some-pkg-guid/upload"), 586 RespondWith(http.StatusNotFound, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 587 ), 588 ) 589 }) 590 591 It("returns the error", func() { 592 _, warnings, err := client.UploadBitsPackage(inputPackage, []Resource{}, bytes.NewReader(nil), 0) 593 Expect(err).To(MatchError(ccerror.ResourceNotFoundError{Message: "Banana"})) 594 Expect(warnings).To(ConsistOf("this is a warning")) 595 }) 596 }) 597 598 When("passed a nil resources", func() { 599 It("returns a NilObjectError", func() { 600 _, _, err := client.UploadBitsPackage(inputPackage, nil, bytes.NewReader(nil), 0) 601 Expect(err).To(MatchError(ccerror.NilObjectError{Object: "matchedResources"})) 602 }) 603 }) 604 605 When("an error is returned from the new resources reader", func() { 606 var ( 607 fakeReader *ccv3fakes.FakeReader 608 expectedErr error 609 ) 610 611 BeforeEach(func() { 612 expectedErr = errors.New("some read error") 613 fakeReader = new(ccv3fakes.FakeReader) 614 fakeReader.ReadReturns(0, expectedErr) 615 616 server.AppendHandlers( 617 VerifyRequest(http.MethodPost, "/v3/my-special-endpoint/some-pkg-guid/upload"), 618 ) 619 }) 620 621 It("returns the error", func() { 622 _, _, err := client.UploadBitsPackage(inputPackage, []Resource{}, fakeReader, 3) 623 Expect(err).To(MatchError(expectedErr)) 624 }) 625 }) 626 627 When("a retryable error occurs", func() { 628 BeforeEach(func() { 629 wrapper := &wrapper.CustomWrapper{ 630 CustomMake: func(connection cloudcontroller.Connection, request *cloudcontroller.Request, response *cloudcontroller.Response) error { 631 defer GinkgoRecover() // Since this will be running in a thread 632 633 if strings.HasSuffix(request.URL.String(), "/v3/my-special-endpoint/some-pkg-guid/upload") { 634 _, err := ioutil.ReadAll(request.Body) 635 Expect(err).ToNot(HaveOccurred()) 636 Expect(request.Body.Close()).ToNot(HaveOccurred()) 637 return request.ResetBody() 638 } 639 return connection.Make(request, response) 640 }, 641 } 642 643 client, _ = NewTestClient(Config{Wrappers: []ConnectionWrapper{wrapper}}) 644 }) 645 646 It("returns the PipeSeekError", func() { 647 _, _, err := client.UploadBitsPackage(inputPackage, []Resource{}, strings.NewReader("hello world"), 3) 648 Expect(err).To(MatchError(ccerror.PipeSeekError{})) 649 }) 650 }) 651 652 When("an http error occurs mid-transfer", func() { 653 var expectedErr error 654 const UploadSize = 33 * 1024 655 656 BeforeEach(func() { 657 expectedErr = errors.New("some read error") 658 659 wrapper := &wrapper.CustomWrapper{ 660 CustomMake: func(connection cloudcontroller.Connection, request *cloudcontroller.Request, response *cloudcontroller.Response) error { 661 defer GinkgoRecover() // Since this will be running in a thread 662 663 if strings.HasSuffix(request.URL.String(), "/v3/my-special-endpoint/some-pkg-guid/upload") { 664 defer request.Body.Close() 665 readBytes, err := ioutil.ReadAll(request.Body) 666 Expect(err).ToNot(HaveOccurred()) 667 Expect(len(readBytes)).To(BeNumerically(">", UploadSize)) 668 return expectedErr 669 } 670 return connection.Make(request, response) 671 }, 672 } 673 674 client, _ = NewTestClient(Config{Wrappers: []ConnectionWrapper{wrapper}}) 675 }) 676 677 It("returns the http error", func() { 678 _, _, err := client.UploadBitsPackage(inputPackage, []Resource{}, strings.NewReader(strings.Repeat("a", UploadSize)), 3) 679 Expect(err).To(MatchError(expectedErr)) 680 }) 681 }) 682 683 When("the input package does not have an upload link", func() { 684 It("returns an UploadLinkNotFoundError", func() { 685 _, _, err := client.UploadBitsPackage(resources.Package{GUID: "some-pkg-guid"}, nil, nil, 0) 686 Expect(err).To(MatchError(ccerror.UploadLinkNotFoundError{PackageGUID: "some-pkg-guid"})) 687 }) 688 }) 689 }) 690 691 Describe("UploadPackage", func() { 692 var ( 693 inputPackage resources.Package 694 fileToUpload string 695 696 pkg resources.Package 697 warnings Warnings 698 executeErr error 699 ) 700 701 JustBeforeEach(func() { 702 pkg, warnings, executeErr = client.UploadPackage(inputPackage, fileToUpload) 703 }) 704 705 When("the package successfully is created", func() { 706 var tempFile *os.File 707 708 BeforeEach(func() { 709 var err error 710 711 inputPackage = resources.Package{ 712 State: constant.PackageAwaitingUpload, 713 Links: map[string]resources.APILink{ 714 "upload": { 715 HREF: fmt.Sprintf("%s/v3/my-special-endpoint/some-pkg-guid/upload", server.URL()), 716 Method: http.MethodPost, 717 }, 718 }, 719 } 720 721 tempFile, err = ioutil.TempFile("", "package-upload") 722 Expect(err).ToNot(HaveOccurred()) 723 defer tempFile.Close() 724 725 fileToUpload = tempFile.Name() 726 727 fileSize := 1024 728 contents := strings.Repeat("A", fileSize) 729 err = ioutil.WriteFile(tempFile.Name(), []byte(contents), 0666) 730 Expect(err).NotTo(HaveOccurred()) 731 732 verifyHeaderAndBody := func(_ http.ResponseWriter, req *http.Request) { 733 contentType := req.Header.Get("Content-Type") 734 Expect(contentType).To(MatchRegexp("multipart/form-data; boundary=[\\w\\d]+")) 735 736 boundary := contentType[30:] 737 738 defer req.Body.Close() 739 rawBody, err := ioutil.ReadAll(req.Body) 740 Expect(err).NotTo(HaveOccurred()) 741 body := BufferWithBytes(rawBody) 742 Expect(body).To(Say("--%s", boundary)) 743 Expect(body).To(Say(`name="bits"`)) 744 Expect(body).To(Say(contents)) 745 Expect(body).To(Say("--%s--", boundary)) 746 } 747 748 response := `{ 749 "guid": "some-pkg-guid", 750 "state": "PROCESSING_UPLOAD", 751 "links": { 752 "upload": { 753 "href": "some-package-upload-url", 754 "method": "POST" 755 } 756 } 757 }` 758 759 server.AppendHandlers( 760 CombineHandlers( 761 VerifyRequest(http.MethodPost, "/v3/my-special-endpoint/some-pkg-guid/upload"), 762 verifyHeaderAndBody, 763 RespondWith(http.StatusOK, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 764 ), 765 ) 766 }) 767 768 AfterEach(func() { 769 if tempFile != nil { 770 Expect(os.RemoveAll(tempFile.Name())).ToNot(HaveOccurred()) 771 } 772 }) 773 774 It("returns the created package and warnings", func() { 775 Expect(executeErr).NotTo(HaveOccurred()) 776 777 expectedPackage := resources.Package{ 778 GUID: "some-pkg-guid", 779 State: constant.PackageProcessingUpload, 780 Links: map[string]resources.APILink{ 781 "upload": {HREF: "some-package-upload-url", Method: http.MethodPost}, 782 }, 783 } 784 Expect(pkg).To(Equal(expectedPackage)) 785 Expect(warnings).To(ConsistOf("this is a warning")) 786 }) 787 }) 788 789 When("the package does not have an upload link", func() { 790 BeforeEach(func() { 791 inputPackage = resources.Package{GUID: "some-pkg-guid", State: constant.PackageAwaitingUpload} 792 fileToUpload = "/path/to/foo" 793 }) 794 795 It("returns an UploadLinkNotFoundError", func() { 796 Expect(executeErr).To(MatchError(ccerror.UploadLinkNotFoundError{PackageGUID: "some-pkg-guid"})) 797 }) 798 }) 799 800 When("cc returns back an error or warnings", func() { 801 var tempFile *os.File 802 803 BeforeEach(func() { 804 var err error 805 806 inputPackage = resources.Package{ 807 State: constant.PackageAwaitingUpload, 808 Links: map[string]resources.APILink{ 809 "upload": { 810 HREF: fmt.Sprintf("%s/v3/my-special-endpoint/some-pkg-guid/upload", server.URL()), 811 Method: http.MethodPost, 812 }, 813 }, 814 } 815 816 tempFile, err = ioutil.TempFile("", "package-upload") 817 Expect(err).ToNot(HaveOccurred()) 818 defer tempFile.Close() 819 820 fileToUpload = tempFile.Name() 821 822 fileSize := 1024 823 contents := strings.Repeat("A", fileSize) 824 err = ioutil.WriteFile(tempFile.Name(), []byte(contents), 0666) 825 Expect(err).NotTo(HaveOccurred()) 826 827 response := ` { 828 "errors": [ 829 { 830 "code": 10008, 831 "detail": "The request is semantically invalid: command presence", 832 "title": "CF-UnprocessableEntity" 833 }, 834 { 835 "code": 10008, 836 "detail": "The request is semantically invalid: command presence", 837 "title": "CF-UnprocessableEntity" 838 } 839 ] 840 }` 841 842 server.AppendHandlers( 843 CombineHandlers( 844 VerifyRequest(http.MethodPost, "/v3/my-special-endpoint/some-pkg-guid/upload"), 845 RespondWith(http.StatusTeapot, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 846 ), 847 ) 848 }) 849 850 AfterEach(func() { 851 if tempFile != nil { 852 Expect(os.RemoveAll(tempFile.Name())).ToNot(HaveOccurred()) 853 } 854 }) 855 856 It("returns the error and all warnings", func() { 857 Expect(executeErr).To(MatchError(ccerror.MultiError{ 858 ResponseCode: http.StatusTeapot, 859 Errors: []ccerror.V3Error{ 860 { 861 Code: 10008, 862 Detail: "The request is semantically invalid: command presence", 863 Title: "CF-UnprocessableEntity", 864 }, 865 { 866 Code: 10008, 867 Detail: "The request is semantically invalid: command presence", 868 Title: "CF-UnprocessableEntity", 869 }, 870 }, 871 })) 872 Expect(warnings).To(ConsistOf("this is a warning")) 873 }) 874 875 }) 876 }) 877 })