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  })