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