github.com/nimakaviani/cli@v6.37.1-0.20180619223813-e734901a73fa+incompatible/api/cloudcontroller/ccv3/package_test.go (about)

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