github.com/wanddynosios/cli/v8@v8.7.9-0.20240221182337-1a92e3a7017f/api/cloudcontroller/ccv3/package_test.go (about) 1 package ccv3_test 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "errors" 7 "io" 8 "io/ioutil" 9 "mime/multipart" 10 "net/http" 11 "os" 12 "strings" 13 "time" 14 15 "code.cloudfoundry.org/cli/api/cloudcontroller" 16 "code.cloudfoundry.org/cli/api/cloudcontroller/ccerror" 17 . "code.cloudfoundry.org/cli/api/cloudcontroller/ccv3" 18 "code.cloudfoundry.org/cli/api/cloudcontroller/ccv3/ccv3fakes" 19 "code.cloudfoundry.org/cli/api/cloudcontroller/ccv3/constant" 20 "code.cloudfoundry.org/cli/api/cloudcontroller/wrapper" 21 "code.cloudfoundry.org/cli/resources" 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": resources.APILink{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": resources.APILink{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 GUID: "package-guid", 445 } 446 }) 447 448 When("the upload is successful", func() { 449 var ( 450 inputResources []Resource 451 readerBody []byte 452 verifyHeaderAndBody func(http.ResponseWriter, *http.Request) 453 ) 454 455 BeforeEach(func() { 456 inputResources = []Resource{ 457 {FilePath: "foo"}, 458 {FilePath: "bar"}, 459 } 460 461 response := `{ 462 "guid": "some-package-guid", 463 "type": "bits", 464 "state": "PROCESSING_UPLOAD" 465 }` 466 467 server.AppendHandlers( 468 CombineHandlers( 469 VerifyRequest(http.MethodPost, "/v3/packages/package-guid/upload"), 470 func(writer http.ResponseWriter, req *http.Request) { 471 verifyHeaderAndBody(writer, req) 472 }, 473 RespondWith(http.StatusOK, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 474 ), 475 ) 476 }) 477 478 When("the upload has application bits to upload", func() { 479 var reader io.Reader 480 481 BeforeEach(func() { 482 readerBody = []byte("hello world") 483 reader = bytes.NewReader(readerBody) 484 485 verifyHeaderAndBody = func(_ http.ResponseWriter, req *http.Request) { 486 contentType := req.Header.Get("Content-Type") 487 Expect(contentType).To(MatchRegexp("multipart/form-data; boundary=[\\w\\d]+")) 488 489 defer req.Body.Close() 490 requestReader := multipart.NewReader(req.Body, contentType[30:]) 491 492 // Verify that matched resources are sent properly 493 resourcesPart, err := requestReader.NextPart() 494 Expect(err).NotTo(HaveOccurred()) 495 496 Expect(resourcesPart.FormName()).To(Equal("resources")) 497 498 defer resourcesPart.Close() 499 expectedJSON, err := json.Marshal(inputResources) 500 Expect(err).NotTo(HaveOccurred()) 501 Expect(ioutil.ReadAll(resourcesPart)).To(MatchJSON(expectedJSON)) 502 503 // Verify that the application bits are sent properly 504 resourcesPart, err = requestReader.NextPart() 505 Expect(err).NotTo(HaveOccurred()) 506 507 Expect(resourcesPart.FormName()).To(Equal("bits")) 508 Expect(resourcesPart.FileName()).To(Equal("package.zip")) 509 510 defer resourcesPart.Close() 511 Expect(ioutil.ReadAll(resourcesPart)).To(Equal(readerBody)) 512 } 513 }) 514 515 It("returns the created job and warnings", func() { 516 pkg, warnings, err := client.UploadBitsPackage(inputPackage, inputResources, reader, int64(len(readerBody))) 517 Expect(err).NotTo(HaveOccurred()) 518 Expect(warnings).To(ConsistOf("this is a warning")) 519 Expect(pkg).To(Equal(resources.Package{ 520 GUID: "some-package-guid", 521 Type: constant.PackageTypeBits, 522 State: constant.PackageProcessingUpload, 523 })) 524 }) 525 }) 526 527 When("there are no application bits to upload", func() { 528 BeforeEach(func() { 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 // Verify that matched resources are sent properly 537 resourcesPart, err := requestReader.NextPart() 538 Expect(err).NotTo(HaveOccurred()) 539 540 Expect(resourcesPart.FormName()).To(Equal("resources")) 541 542 defer resourcesPart.Close() 543 expectedJSON, err := json.Marshal(inputResources) 544 Expect(err).NotTo(HaveOccurred()) 545 Expect(ioutil.ReadAll(resourcesPart)).To(MatchJSON(expectedJSON)) 546 547 // Verify that the application bits are not sent 548 _, err = requestReader.NextPart() 549 Expect(err).To(MatchError(io.EOF)) 550 } 551 }) 552 553 It("does not send the application bits", func() { 554 pkg, warnings, err := client.UploadBitsPackage(inputPackage, inputResources, nil, 33513531353) 555 Expect(err).NotTo(HaveOccurred()) 556 Expect(warnings).To(ConsistOf("this is a warning")) 557 Expect(pkg).To(Equal(resources.Package{ 558 GUID: "some-package-guid", 559 Type: constant.PackageTypeBits, 560 State: constant.PackageProcessingUpload, 561 })) 562 }) 563 }) 564 }) 565 566 When("the CC returns an error", func() { 567 BeforeEach(func() { 568 response := ` { 569 "errors": [ 570 { 571 "code": 10008, 572 "detail": "Banana", 573 "title": "CF-Banana" 574 } 575 ] 576 }` 577 578 server.AppendHandlers( 579 CombineHandlers( 580 VerifyRequest(http.MethodPost, "/v3/packages/package-guid/upload"), 581 RespondWith(http.StatusNotFound, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 582 ), 583 ) 584 }) 585 586 It("returns the error", func() { 587 _, warnings, err := client.UploadBitsPackage(inputPackage, []Resource{}, bytes.NewReader(nil), 0) 588 Expect(err).To(MatchError(ccerror.ResourceNotFoundError{Message: "Banana"})) 589 Expect(warnings).To(ConsistOf("this is a warning")) 590 }) 591 }) 592 593 When("passed a nil resources", func() { 594 It("returns a NilObjectError", func() { 595 _, _, err := client.UploadBitsPackage(inputPackage, nil, bytes.NewReader(nil), 0) 596 Expect(err).To(MatchError(ccerror.NilObjectError{Object: "matchedResources"})) 597 }) 598 }) 599 600 When("an error is returned from the new resources reader", func() { 601 var ( 602 fakeReader *ccv3fakes.FakeReader 603 expectedErr error 604 ) 605 606 BeforeEach(func() { 607 expectedErr = errors.New("some read error") 608 fakeReader = new(ccv3fakes.FakeReader) 609 fakeReader.ReadReturns(0, expectedErr) 610 611 server.AppendHandlers( 612 VerifyRequest(http.MethodPost, "/v3/packages/package-guid/upload"), 613 ) 614 }) 615 616 It("returns the error", func() { 617 _, _, err := client.UploadBitsPackage(inputPackage, []Resource{}, fakeReader, 3) 618 Expect(err).To(MatchError(expectedErr)) 619 }) 620 }) 621 622 When("a retryable error occurs", func() { 623 BeforeEach(func() { 624 wrapper := &wrapper.CustomWrapper{ 625 CustomMake: func(connection cloudcontroller.Connection, request *cloudcontroller.Request, response *cloudcontroller.Response) error { 626 defer GinkgoRecover() // Since this will be running in a thread 627 628 if strings.HasSuffix(request.URL.String(), "/v3/packages/package-guid/upload") { 629 _, err := ioutil.ReadAll(request.Body) 630 Expect(err).ToNot(HaveOccurred()) 631 Expect(request.Body.Close()).ToNot(HaveOccurred()) 632 return request.ResetBody() 633 } 634 return connection.Make(request, response) 635 }, 636 } 637 638 client, _ = NewTestClient(Config{Wrappers: []ConnectionWrapper{wrapper}}) 639 }) 640 641 It("returns the PipeSeekError", func() { 642 _, _, err := client.UploadBitsPackage(inputPackage, []Resource{}, strings.NewReader("hello world"), 3) 643 Expect(err).To(MatchError(ccerror.PipeSeekError{})) 644 }) 645 }) 646 647 When("an http error occurs mid-transfer", func() { 648 var expectedErr error 649 const UploadSize = 33 * 1024 650 651 BeforeEach(func() { 652 expectedErr = errors.New("some read error") 653 654 wrapper := &wrapper.CustomWrapper{ 655 CustomMake: func(connection cloudcontroller.Connection, request *cloudcontroller.Request, response *cloudcontroller.Response) error { 656 defer GinkgoRecover() // Since this will be running in a thread 657 658 if strings.HasSuffix(request.URL.String(), "/v3/packages/package-guid/upload") { 659 defer request.Body.Close() 660 readBytes, err := ioutil.ReadAll(request.Body) 661 Expect(err).ToNot(HaveOccurred()) 662 Expect(len(readBytes)).To(BeNumerically(">", UploadSize)) 663 return expectedErr 664 } 665 return connection.Make(request, response) 666 }, 667 } 668 669 client, _ = NewTestClient(Config{Wrappers: []ConnectionWrapper{wrapper}}) 670 }) 671 672 It("returns the http error", func() { 673 _, _, err := client.UploadBitsPackage(inputPackage, []Resource{}, strings.NewReader(strings.Repeat("a", UploadSize)), 3) 674 Expect(err).To(MatchError(expectedErr)) 675 }) 676 }) 677 }) 678 679 Describe("UploadPackage", func() { 680 var ( 681 inputPackage resources.Package 682 fileToUpload string 683 684 pkg resources.Package 685 warnings Warnings 686 executeErr error 687 ) 688 689 JustBeforeEach(func() { 690 pkg, warnings, executeErr = client.UploadPackage(inputPackage, fileToUpload) 691 }) 692 693 When("the package successfully is created", func() { 694 var tempFile *os.File 695 696 BeforeEach(func() { 697 var err error 698 699 inputPackage = resources.Package{ 700 State: constant.PackageAwaitingUpload, 701 GUID: "package-guid", 702 } 703 704 tempFile, err = ioutil.TempFile("", "package-upload") 705 Expect(err).ToNot(HaveOccurred()) 706 defer tempFile.Close() 707 708 fileToUpload = tempFile.Name() 709 710 fileSize := 1024 711 contents := strings.Repeat("A", fileSize) 712 err = ioutil.WriteFile(tempFile.Name(), []byte(contents), 0666) 713 Expect(err).NotTo(HaveOccurred()) 714 715 verifyHeaderAndBody := func(_ http.ResponseWriter, req *http.Request) { 716 contentType := req.Header.Get("Content-Type") 717 Expect(contentType).To(MatchRegexp("multipart/form-data; boundary=[\\w\\d]+")) 718 719 boundary := contentType[30:] 720 721 defer req.Body.Close() 722 rawBody, err := ioutil.ReadAll(req.Body) 723 Expect(err).NotTo(HaveOccurred()) 724 body := BufferWithBytes(rawBody) 725 Expect(body).To(Say("--%s", boundary)) 726 Expect(body).To(Say(`name="bits"`)) 727 Expect(body).To(Say(contents)) 728 Expect(body).To(Say("--%s--", boundary)) 729 } 730 731 response := `{ 732 "guid": "some-pkg-guid", 733 "state": "PROCESSING_UPLOAD", 734 "links": { 735 "upload": { 736 "href": "some-package-upload-url", 737 "method": "POST" 738 } 739 } 740 }` 741 742 server.AppendHandlers( 743 CombineHandlers( 744 VerifyRequest(http.MethodPost, "/v3/packages/package-guid/upload"), 745 verifyHeaderAndBody, 746 RespondWith(http.StatusOK, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 747 ), 748 ) 749 }) 750 751 AfterEach(func() { 752 if tempFile != nil { 753 Expect(os.RemoveAll(tempFile.Name())).ToNot(HaveOccurred()) 754 } 755 }) 756 757 It("returns the created package and warnings", func() { 758 Expect(executeErr).NotTo(HaveOccurred()) 759 760 expectedPackage := resources.Package{ 761 GUID: "some-pkg-guid", 762 State: constant.PackageProcessingUpload, 763 Links: map[string]resources.APILink{ 764 "upload": resources.APILink{HREF: "some-package-upload-url", Method: http.MethodPost}, 765 }, 766 } 767 Expect(pkg).To(Equal(expectedPackage)) 768 Expect(warnings).To(ConsistOf("this is a warning")) 769 }) 770 }) 771 772 When("cc returns back an error or warnings", func() { 773 var tempFile *os.File 774 775 BeforeEach(func() { 776 var err error 777 778 inputPackage = resources.Package{ 779 GUID: "package-guid", 780 State: constant.PackageAwaitingUpload, 781 } 782 783 tempFile, err = ioutil.TempFile("", "package-upload") 784 Expect(err).ToNot(HaveOccurred()) 785 defer tempFile.Close() 786 787 fileToUpload = tempFile.Name() 788 789 fileSize := 1024 790 contents := strings.Repeat("A", fileSize) 791 err = ioutil.WriteFile(tempFile.Name(), []byte(contents), 0666) 792 Expect(err).NotTo(HaveOccurred()) 793 794 response := ` { 795 "errors": [ 796 { 797 "code": 10008, 798 "detail": "The request is semantically invalid: command presence", 799 "title": "CF-UnprocessableEntity" 800 }, 801 { 802 "code": 10008, 803 "detail": "The request is semantically invalid: command presence", 804 "title": "CF-UnprocessableEntity" 805 } 806 ] 807 }` 808 809 server.AppendHandlers( 810 CombineHandlers( 811 VerifyRequest(http.MethodPost, "/v3/packages/package-guid/upload"), 812 RespondWith(http.StatusTeapot, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 813 ), 814 ) 815 }) 816 817 AfterEach(func() { 818 if tempFile != nil { 819 Expect(os.RemoveAll(tempFile.Name())).ToNot(HaveOccurred()) 820 } 821 }) 822 823 It("returns the error and all warnings", func() { 824 Expect(executeErr).To(MatchError(ccerror.MultiError{ 825 ResponseCode: http.StatusTeapot, 826 Errors: []ccerror.V3Error{ 827 { 828 Code: 10008, 829 Detail: "The request is semantically invalid: command presence", 830 Title: "CF-UnprocessableEntity", 831 }, 832 { 833 Code: 10008, 834 Detail: "The request is semantically invalid: command presence", 835 Title: "CF-UnprocessableEntity", 836 }, 837 }, 838 })) 839 Expect(warnings).To(ConsistOf("this is a warning")) 840 }) 841 842 }) 843 }) 844 845 Describe("CopyPackage", func() { 846 var ( 847 sourcePackageGUID string 848 targetAppGUID string 849 850 targetPackage resources.Package 851 warnings Warnings 852 executeErr error 853 response string 854 ) 855 856 BeforeEach(func() { 857 sourcePackageGUID = "source-package-guid" 858 859 targetAppGUID = "target-app-guid" 860 response = `{ 861 "guid": "some-targetPackage-guid" 862 }` 863 864 expectedBody := map[string]interface{}{ 865 "relationships": map[string]interface{}{ 866 "app": map[string]interface{}{ 867 "data": map[string]string{ 868 "guid": targetAppGUID, 869 }, 870 }, 871 }, 872 } 873 server.AppendHandlers( 874 CombineHandlers( 875 VerifyRequest(http.MethodPost, "/v3/packages", "source_guid="+sourcePackageGUID), 876 VerifyJSONRepresenting(expectedBody), 877 RespondWith(http.StatusCreated, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 878 ), 879 ) 880 881 }) 882 883 JustBeforeEach(func() { 884 targetPackage, warnings, executeErr = client.CopyPackage(sourcePackageGUID, targetAppGUID) 885 }) 886 887 It("returns the created target package and warnings", func() { 888 Expect(executeErr).NotTo(HaveOccurred()) 889 Expect(warnings).To(ConsistOf("this is a warning")) 890 891 expectedPackage := resources.Package{ 892 GUID: "some-targetPackage-guid", 893 } 894 Expect(targetPackage).To(Equal(expectedPackage)) 895 }) 896 897 When("cc returns back an error or 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": "Package not found", 909 "title": "CF-ResourceNotFound" 910 } 911 ] 912 }` 913 server.Reset() 914 time.Sleep(10 * time.Millisecond) // guards against <ccerror.RequestError>: {Err: { ... { Err: {s: "EOF"}, 915 server.AppendHandlers( 916 CombineHandlers( 917 VerifyRequest(http.MethodPost, "/v3/packages", "source_guid="+sourcePackageGUID), 918 RespondWith(http.StatusTeapot, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 919 ), 920 ) 921 }) 922 923 It("returns the error and all warnings", func() { 924 Expect(executeErr).To(MatchError(ccerror.MultiError{ 925 ResponseCode: http.StatusTeapot, 926 Errors: []ccerror.V3Error{ 927 { 928 Code: 10008, 929 Detail: "The request is semantically invalid: command presence", 930 Title: "CF-UnprocessableEntity", 931 }, 932 { 933 Code: 10010, 934 Detail: "Package not found", 935 Title: "CF-ResourceNotFound", 936 }, 937 }, 938 })) 939 Expect(warnings).To(ConsistOf("this is a warning")) 940 }) 941 }) 942 }) 943 })