github.com/orange-cloudfoundry/cli@v7.1.0+incompatible/api/cloudcontroller/ccv3/package_test.go (about)

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