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