github.com/loggregator/cli@v6.33.1-0.20180224010324-82334f081791+incompatible/api/cloudcontroller/ccv2/job_test.go (about)

     1  package ccv2_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  	"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/ccv2"
    18  	"code.cloudfoundry.org/cli/api/cloudcontroller/ccv2/ccv2fakes"
    19  	"code.cloudfoundry.org/cli/api/cloudcontroller/ccv2/constant"
    20  	"code.cloudfoundry.org/cli/api/cloudcontroller/wrapper"
    21  	. "github.com/onsi/ginkgo"
    22  	. "github.com/onsi/ginkgo/extensions/table"
    23  	. "github.com/onsi/gomega"
    24  	. "github.com/onsi/gomega/ghttp"
    25  )
    26  
    27  var _ = Describe("Job", func() {
    28  	var client *Client
    29  
    30  	Describe("Job", func() {
    31  		DescribeTable("Finished",
    32  			func(status constant.JobStatus, expected bool) {
    33  				job := Job{Status: status}
    34  				Expect(job.Finished()).To(Equal(expected))
    35  			},
    36  
    37  			Entry("when failed, it returns false", constant.JobStatusFailed, false),
    38  			Entry("when finished, it returns true", constant.JobStatusFinished, true),
    39  			Entry("when queued, it returns false", constant.JobStatusQueued, false),
    40  			Entry("when running, it returns false", constant.JobStatusRunning, false),
    41  		)
    42  
    43  		DescribeTable("Failed",
    44  			func(status constant.JobStatus, expected bool) {
    45  				job := Job{Status: status}
    46  				Expect(job.Failed()).To(Equal(expected))
    47  			},
    48  
    49  			Entry("when failed, it returns true", constant.JobStatusFailed, true),
    50  			Entry("when finished, it returns false", constant.JobStatusFinished, false),
    51  			Entry("when queued, it returns false", constant.JobStatusQueued, false),
    52  			Entry("when running, it returns false", constant.JobStatusRunning, false),
    53  		)
    54  	})
    55  
    56  	Describe("PollJob", func() {
    57  		BeforeEach(func() {
    58  			client = NewTestClient(Config{JobPollingTimeout: time.Minute})
    59  		})
    60  
    61  		Context("when the job starts queued and then finishes successfully", func() {
    62  			BeforeEach(func() {
    63  				server.AppendHandlers(
    64  					CombineHandlers(
    65  						VerifyRequest(http.MethodGet, "/v2/jobs/some-job-guid"),
    66  						RespondWith(http.StatusAccepted, `{
    67  							"metadata": {
    68  								"guid": "some-job-guid",
    69  								"created_at": "2016-06-08T16:41:27Z",
    70  								"url": "/v2/jobs/some-job-guid"
    71  							},
    72  							"entity": {
    73  								"guid": "some-job-guid",
    74  								"status": "queued"
    75  							}
    76  						}`, http.Header{"X-Cf-Warnings": {"warning-1"}}),
    77  					))
    78  
    79  				server.AppendHandlers(
    80  					CombineHandlers(
    81  						VerifyRequest(http.MethodGet, "/v2/jobs/some-job-guid"),
    82  						RespondWith(http.StatusAccepted, `{
    83  							"metadata": {
    84  								"guid": "some-job-guid",
    85  								"created_at": "2016-06-08T16:41:28Z",
    86  								"url": "/v2/jobs/some-job-guid"
    87  							},
    88  							"entity": {
    89  								"guid": "some-job-guid",
    90  								"status": "running"
    91  							}
    92  						}`, http.Header{"X-Cf-Warnings": {"warning-2, warning-3"}}),
    93  					))
    94  
    95  				server.AppendHandlers(
    96  					CombineHandlers(
    97  						VerifyRequest(http.MethodGet, "/v2/jobs/some-job-guid"),
    98  						RespondWith(http.StatusAccepted, `{
    99  							"metadata": {
   100  								"guid": "some-job-guid",
   101  								"created_at": "2016-06-08T16:41:29Z",
   102  								"url": "/v2/jobs/some-job-guid"
   103  							},
   104  							"entity": {
   105  								"guid": "some-job-guid",
   106  								"status": "finished"
   107  							}
   108  						}`, http.Header{"X-Cf-Warnings": {"warning-4"}}),
   109  					))
   110  			})
   111  
   112  			It("should poll until completion", func() {
   113  				warnings, err := client.PollJob(Job{GUID: "some-job-guid"})
   114  				Expect(err).ToNot(HaveOccurred())
   115  				Expect(warnings).To(ConsistOf("warning-1", "warning-2", "warning-3", "warning-4"))
   116  			})
   117  		})
   118  
   119  		Context("when the job starts queued and then fails", func() {
   120  			var jobFailureMessage string
   121  			BeforeEach(func() {
   122  				jobFailureMessage = "I am a banana!!!"
   123  
   124  				server.AppendHandlers(
   125  					CombineHandlers(
   126  						VerifyRequest(http.MethodGet, "/v2/jobs/some-job-guid"),
   127  						RespondWith(http.StatusAccepted, `{
   128  							"metadata": {
   129  								"guid": "some-job-guid",
   130  								"created_at": "2016-06-08T16:41:27Z",
   131  								"url": "/v2/jobs/some-job-guid"
   132  							},
   133  							"entity": {
   134  								"guid": "some-job-guid",
   135  								"status": "queued"
   136  							}
   137  						}`, http.Header{"X-Cf-Warnings": {"warning-1"}}),
   138  					))
   139  
   140  				server.AppendHandlers(
   141  					CombineHandlers(
   142  						VerifyRequest(http.MethodGet, "/v2/jobs/some-job-guid"),
   143  						RespondWith(http.StatusAccepted, `{
   144  							"metadata": {
   145  								"guid": "some-job-guid",
   146  								"created_at": "2016-06-08T16:41:28Z",
   147  								"url": "/v2/jobs/some-job-guid"
   148  							},
   149  							"entity": {
   150  								"guid": "some-job-guid",
   151  								"status": "running"
   152  							}
   153  						}`, http.Header{"X-Cf-Warnings": {"warning-2, warning-3"}}),
   154  					))
   155  
   156  				server.AppendHandlers(
   157  					CombineHandlers(
   158  						VerifyRequest(http.MethodGet, "/v2/jobs/some-job-guid"),
   159  						RespondWith(http.StatusOK, fmt.Sprintf(`
   160  							{
   161  								"metadata": {
   162  									"guid": "some-job-guid",
   163  									"created_at": "2016-06-08T16:41:29Z",
   164  									"url": "/v2/jobs/some-job-guid"
   165  								},
   166  								"entity": {
   167  									"error": "Use of entity>error is deprecated in favor of entity>error_details.",
   168  									"error_details": {
   169  										"code": 160001,
   170  										"description": "%s",
   171  										"error_code": "CF-AppBitsUploadInvalid"
   172  									},
   173  									"guid": "job-guid",
   174  									"status": "failed"
   175  								}
   176  							}
   177  							`, jobFailureMessage), http.Header{"X-Cf-Warnings": {"warning-4"}}),
   178  					))
   179  			})
   180  
   181  			It("returns a JobFailedError", func() {
   182  				warnings, err := client.PollJob(Job{GUID: "some-job-guid"})
   183  				Expect(err).To(MatchError(ccerror.JobFailedError{
   184  					JobGUID: "some-job-guid",
   185  					Message: jobFailureMessage,
   186  				}))
   187  				Expect(warnings).To(ConsistOf("warning-1", "warning-2", "warning-3", "warning-4"))
   188  			})
   189  		})
   190  
   191  		Context("when retrieving the job errors", func() {
   192  			BeforeEach(func() {
   193  				server.AppendHandlers(
   194  					CombineHandlers(
   195  						VerifyRequest(http.MethodGet, "/v2/jobs/some-job-guid"),
   196  						RespondWith(http.StatusAccepted, `{
   197  							INVALID YAML
   198  						}`, http.Header{"X-Cf-Warnings": {"warning-1, warning-2"}}),
   199  					))
   200  			})
   201  
   202  			It("returns the CC error", func() {
   203  				warnings, err := client.PollJob(Job{GUID: "some-job-guid"})
   204  				Expect(warnings).To(ConsistOf("warning-1", "warning-2"))
   205  				Expect(err.Error()).To(MatchRegexp("invalid character"))
   206  			})
   207  		})
   208  
   209  		Describe("JobPollingTimeout", func() {
   210  			Context("when the job runs longer than the OverallPollingTimeout", func() {
   211  				var jobPollingTimeout time.Duration
   212  
   213  				BeforeEach(func() {
   214  					jobPollingTimeout = 100 * time.Millisecond
   215  					client = NewTestClient(Config{
   216  						JobPollingTimeout:  jobPollingTimeout,
   217  						JobPollingInterval: 60 * time.Millisecond,
   218  					})
   219  
   220  					server.AppendHandlers(
   221  						CombineHandlers(
   222  							VerifyRequest(http.MethodGet, "/v2/jobs/some-job-guid"),
   223  							RespondWith(http.StatusAccepted, `{
   224  							"metadata": {
   225  								"guid": "some-job-guid",
   226  								"created_at": "2016-06-08T16:41:27Z",
   227  								"url": "/v2/jobs/some-job-guid"
   228  							},
   229  							"entity": {
   230  								"guid": "some-job-guid",
   231  								"status": "queued"
   232  							}
   233  						}`, http.Header{"X-Cf-Warnings": {"warning-1"}}),
   234  						))
   235  
   236  					server.AppendHandlers(
   237  						CombineHandlers(
   238  							VerifyRequest(http.MethodGet, "/v2/jobs/some-job-guid"),
   239  							RespondWith(http.StatusAccepted, `{
   240  							"metadata": {
   241  								"guid": "some-job-guid",
   242  								"created_at": "2016-06-08T16:41:28Z",
   243  								"url": "/v2/jobs/some-job-guid"
   244  							},
   245  							"entity": {
   246  								"guid": "some-job-guid",
   247  								"status": "running"
   248  							}
   249  						}`, http.Header{"X-Cf-Warnings": {"warning-2, warning-3"}}),
   250  						))
   251  
   252  					server.AppendHandlers(
   253  						CombineHandlers(
   254  							VerifyRequest(http.MethodGet, "/v2/jobs/some-job-guid"),
   255  							RespondWith(http.StatusAccepted, `{
   256  							"metadata": {
   257  								"guid": "some-job-guid",
   258  								"created_at": "2016-06-08T16:41:29Z",
   259  								"url": "/v2/jobs/some-job-guid"
   260  							},
   261  							"entity": {
   262  								"guid": "some-job-guid",
   263  								"status": "finished"
   264  							}
   265  						}`, http.Header{"X-Cf-Warnings": {"warning-4"}}),
   266  						))
   267  				})
   268  
   269  				It("raises a JobTimeoutError", func() {
   270  					_, err := client.PollJob(Job{GUID: "some-job-guid"})
   271  
   272  					Expect(err).To(MatchError(ccerror.JobTimeoutError{
   273  						Timeout: jobPollingTimeout,
   274  						JobGUID: "some-job-guid",
   275  					}))
   276  				})
   277  
   278  				// Fuzzy test to ensure that the overall function time isn't [far]
   279  				// greater than the OverallPollingTimeout. Since this is partially
   280  				// dependent on the speed of the system, the expectation is that the
   281  				// function *should* never exceed three times the timeout.
   282  				It("does not run [too much] longer than the timeout", func() {
   283  					startTime := time.Now()
   284  					_, err := client.PollJob(Job{GUID: "some-job-guid"})
   285  					endTime := time.Now()
   286  					Expect(err).To(HaveOccurred())
   287  
   288  					// If the jobPollingTimeout is less than the PollingInterval,
   289  					// then the margin may be too small, we should not allow the
   290  					// jobPollingTimeout to be set to less than the PollingInterval
   291  					Expect(endTime).To(BeTemporally("~", startTime, 3*jobPollingTimeout))
   292  				})
   293  			})
   294  		})
   295  	})
   296  
   297  	Describe("GetJob", func() {
   298  		BeforeEach(func() {
   299  			client = NewTestClient()
   300  		})
   301  
   302  		Context("when no errors are encountered", func() {
   303  			BeforeEach(func() {
   304  				jsonResponse := `{
   305  					"metadata": {
   306  						"guid": "job-guid",
   307  						"created_at": "2016-06-08T16:41:27Z",
   308  						"url": "/v2/jobs/job-guid"
   309  					},
   310  					"entity": {
   311  						"guid": "job-guid",
   312  						"status": "queued"
   313  					}
   314  				}`
   315  
   316  				server.AppendHandlers(
   317  					CombineHandlers(
   318  						VerifyRequest(http.MethodGet, "/v2/jobs/job-guid"),
   319  						RespondWith(http.StatusOK, jsonResponse, http.Header{"X-Cf-Warnings": {"warning-1, warning-2"}}),
   320  					))
   321  			})
   322  
   323  			It("returns job with all warnings", func() {
   324  				job, warnings, err := client.GetJob("job-guid")
   325  
   326  				Expect(err).NotTo(HaveOccurred())
   327  				Expect(warnings).To(ConsistOf(Warnings{"warning-1", "warning-2"}))
   328  				Expect(job.GUID).To(Equal("job-guid"))
   329  				Expect(job.Status).To(Equal(constant.JobStatusQueued))
   330  			})
   331  		})
   332  
   333  		Context("when the job fails", func() {
   334  			BeforeEach(func() {
   335  				jsonResponse := `
   336  					{
   337  						"metadata": {
   338  							"guid": "some-job-guid",
   339  							"created_at": "2016-06-08T16:41:29Z",
   340  							"url": "/v2/jobs/some-job-guid"
   341  						},
   342  						"entity": {
   343  							"error": "Use of entity>error is deprecated in favor of entity>error_details.",
   344  							"error_details": {
   345  								"code": 160001,
   346  								"description": "some-error",
   347  								"error_code": "CF-AppBitsUploadInvalid"
   348  							},
   349  							"guid": "job-guid",
   350  							"status": "failed"
   351  						}
   352  					}
   353  					`
   354  				server.AppendHandlers(
   355  					CombineHandlers(
   356  						VerifyRequest(http.MethodGet, "/v2/jobs/job-guid"),
   357  						RespondWith(http.StatusOK, jsonResponse, http.Header{"X-Cf-Warnings": {"warning-1, warning-2"}}),
   358  					))
   359  			})
   360  
   361  			It("returns job with all warnings", func() {
   362  				job, warnings, err := client.GetJob("job-guid")
   363  
   364  				Expect(err).NotTo(HaveOccurred())
   365  				Expect(warnings).To(ConsistOf(Warnings{"warning-1", "warning-2"}))
   366  				Expect(job.GUID).To(Equal("job-guid"))
   367  				Expect(job.Status).To(Equal(constant.JobStatusFailed))
   368  				Expect(job.Error).To(Equal("Use of entity>error is deprecated in favor of entity>error_details."))
   369  				Expect(job.ErrorDetails.Description).To(Equal("some-error"))
   370  			})
   371  		})
   372  	})
   373  
   374  	Describe("DeleteOrganizationJob", func() {
   375  		var (
   376  			job        Job
   377  			warnings   Warnings
   378  			executeErr error
   379  		)
   380  
   381  		BeforeEach(func() {
   382  			client = NewTestClient()
   383  		})
   384  
   385  		JustBeforeEach(func() {
   386  			job, warnings, executeErr = client.DeleteOrganizationJob("some-organization-guid")
   387  		})
   388  
   389  		Context("when no errors are encountered", func() {
   390  			BeforeEach(func() {
   391  				jsonResponse := `{
   392  				"metadata": {
   393  					"guid": "job-guid",
   394  					"created_at": "2016-06-08T16:41:27Z",
   395  					"url": "/v2/jobs/job-guid"
   396  				},
   397  				"entity": {
   398  					"guid": "job-guid",
   399  					"status": "queued"
   400  				}
   401  			}`
   402  
   403  				server.AppendHandlers(
   404  					CombineHandlers(
   405  						VerifyRequest(http.MethodDelete, "/v2/organizations/some-organization-guid", "recursive=true&async=true"),
   406  						RespondWith(http.StatusAccepted, jsonResponse, http.Header{"X-Cf-Warnings": {"warning-1, warning-2"}}),
   407  					))
   408  			})
   409  
   410  			It("deletes the Organization and returns all warnings", func() {
   411  				Expect(executeErr).NotTo(HaveOccurred())
   412  				Expect(warnings).To(ConsistOf(Warnings{"warning-1", "warning-2"}))
   413  				Expect(job).To(Equal(Job{
   414  					GUID:   "job-guid",
   415  					Status: constant.JobStatusQueued,
   416  				}))
   417  			})
   418  		})
   419  
   420  		Context("when an error is encountered", func() {
   421  			BeforeEach(func() {
   422  				response := `{
   423  "code": 30003,
   424  "description": "The Organization could not be found: some-organization-guid",
   425  "error_code": "CF-OrganizationNotFound"
   426  }`
   427  				server.AppendHandlers(
   428  					CombineHandlers(
   429  						VerifyRequest(http.MethodDelete, "/v2/organizations/some-organization-guid", "recursive=true&async=true"),
   430  						RespondWith(http.StatusNotFound, response, http.Header{"X-Cf-Warnings": {"warning-1, warning-2"}}),
   431  					))
   432  			})
   433  
   434  			It("returns an error and all warnings", func() {
   435  				Expect(executeErr).To(MatchError(ccerror.ResourceNotFoundError{
   436  					Message: "The Organization could not be found: some-organization-guid",
   437  				}))
   438  				Expect(warnings).To(ConsistOf(Warnings{"warning-1", "warning-2"}))
   439  			})
   440  		})
   441  	})
   442  
   443  	Describe("DeleteSpaceJob", func() {
   444  		var (
   445  			job        Job
   446  			warnings   Warnings
   447  			executeErr error
   448  		)
   449  
   450  		BeforeEach(func() {
   451  			client = NewTestClient()
   452  		})
   453  
   454  		JustBeforeEach(func() {
   455  			job, warnings, executeErr = client.DeleteSpaceJob("some-space-guid")
   456  		})
   457  
   458  		Context("when no errors are encountered", func() {
   459  			BeforeEach(func() {
   460  				jsonResponse := `{
   461  				"metadata": {
   462  					"guid": "job-guid",
   463  					"created_at": "2016-06-08T16:41:27Z",
   464  					"url": "/v2/jobs/job-guid"
   465  				},
   466  				"entity": {
   467  					"guid": "job-guid",
   468  					"status": "queued"
   469  				}
   470  			}`
   471  
   472  				server.AppendHandlers(
   473  					CombineHandlers(
   474  						VerifyRequest(http.MethodDelete, "/v2/spaces/some-space-guid", "recursive=true&async=true"),
   475  						RespondWith(http.StatusAccepted, jsonResponse, http.Header{"X-Cf-Warnings": {"warning-1, warning-2"}}),
   476  					))
   477  			})
   478  
   479  			It("deletes the Space and returns all warnings", func() {
   480  				Expect(executeErr).NotTo(HaveOccurred())
   481  				Expect(warnings).To(ConsistOf(Warnings{"warning-1", "warning-2"}))
   482  				Expect(job).To(Equal(Job{
   483  					GUID:   "job-guid",
   484  					Status: constant.JobStatusQueued,
   485  				}))
   486  			})
   487  		})
   488  
   489  		Context("when an error is encountered", func() {
   490  			BeforeEach(func() {
   491  				response := `{
   492  "code": 30003,
   493  "description": "The Space could not be found: some-space-guid",
   494  "error_code": "CF-SpaceNotFound"
   495  }`
   496  				server.AppendHandlers(
   497  					CombineHandlers(
   498  						VerifyRequest(http.MethodDelete, "/v2/spaces/some-space-guid", "recursive=true&async=true"),
   499  						RespondWith(http.StatusNotFound, response, http.Header{"X-Cf-Warnings": {"warning-1, warning-2"}}),
   500  					))
   501  			})
   502  
   503  			It("returns an error and all warnings", func() {
   504  				Expect(executeErr).To(MatchError(ccerror.ResourceNotFoundError{
   505  					Message: "The Space could not be found: some-space-guid",
   506  				}))
   507  				Expect(warnings).To(ConsistOf(Warnings{"warning-1", "warning-2"}))
   508  			})
   509  		})
   510  	})
   511  
   512  	Describe("UploadApplicationPackage", func() {
   513  		BeforeEach(func() {
   514  			client = NewTestClient()
   515  		})
   516  
   517  		Context("when the upload is successful", func() {
   518  			var (
   519  				resources  []Resource
   520  				reader     io.Reader
   521  				readerBody []byte
   522  			)
   523  
   524  			Context("when the upload has application bits to upload", func() {
   525  				BeforeEach(func() {
   526  					resources = []Resource{
   527  						{Filename: "foo"},
   528  						{Filename: "bar"},
   529  					}
   530  
   531  					readerBody = []byte("hello world")
   532  					reader = bytes.NewReader(readerBody)
   533  
   534  					verifyHeaderAndBody := func(_ http.ResponseWriter, req *http.Request) {
   535  						contentType := req.Header.Get("Content-Type")
   536  						Expect(contentType).To(MatchRegexp("multipart/form-data; boundary=[\\w\\d]+"))
   537  
   538  						defer req.Body.Close()
   539  						requestReader := multipart.NewReader(req.Body, contentType[30:])
   540  
   541  						// Verify that matched resources are sent properly
   542  						resourcesPart, err := requestReader.NextPart()
   543  						Expect(err).NotTo(HaveOccurred())
   544  
   545  						Expect(resourcesPart.FormName()).To(Equal("resources"))
   546  
   547  						defer resourcesPart.Close()
   548  						expectedJSON, err := json.Marshal(resources)
   549  						Expect(err).NotTo(HaveOccurred())
   550  						Expect(ioutil.ReadAll(resourcesPart)).To(MatchJSON(expectedJSON))
   551  
   552  						// Verify that the application bits are sent properly
   553  						resourcesPart, err = requestReader.NextPart()
   554  						Expect(err).NotTo(HaveOccurred())
   555  
   556  						Expect(resourcesPart.FormName()).To(Equal("application"))
   557  						Expect(resourcesPart.FileName()).To(Equal("application.zip"))
   558  
   559  						defer resourcesPart.Close()
   560  						Expect(ioutil.ReadAll(resourcesPart)).To(Equal(readerBody))
   561  					}
   562  
   563  					response := `{
   564  					"metadata": {
   565  						"guid": "job-guid",
   566  						"url": "/v2/jobs/job-guid"
   567  					},
   568  					"entity": {
   569  						"guid": "job-guid",
   570  						"status": "queued"
   571  					}
   572  				}`
   573  
   574  					server.AppendHandlers(
   575  						CombineHandlers(
   576  							VerifyRequest(http.MethodPut, "/v2/apps/some-app-guid/bits", "async=true"),
   577  							verifyHeaderAndBody,
   578  							RespondWith(http.StatusOK, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}),
   579  						),
   580  					)
   581  				})
   582  
   583  				It("returns the created job and warnings", func() {
   584  					job, warnings, err := client.UploadApplicationPackage("some-app-guid", resources, reader, int64(len(readerBody)))
   585  					Expect(err).NotTo(HaveOccurred())
   586  					Expect(warnings).To(ConsistOf("this is a warning"))
   587  					Expect(job).To(Equal(Job{
   588  						GUID:   "job-guid",
   589  						Status: constant.JobStatusQueued,
   590  					}))
   591  				})
   592  			})
   593  
   594  			Context("when there are no application bits to upload", func() {
   595  				BeforeEach(func() {
   596  					resources = []Resource{
   597  						{Filename: "foo"},
   598  						{Filename: "bar"},
   599  					}
   600  
   601  					verifyHeaderAndBody := func(_ http.ResponseWriter, req *http.Request) {
   602  						contentType := req.Header.Get("Content-Type")
   603  						Expect(contentType).To(MatchRegexp("multipart/form-data; boundary=[\\w\\d]+"))
   604  
   605  						defer req.Body.Close()
   606  						requestReader := multipart.NewReader(req.Body, contentType[30:])
   607  
   608  						// Verify that matched resources are sent properly
   609  						resourcesPart, err := requestReader.NextPart()
   610  						Expect(err).NotTo(HaveOccurred())
   611  
   612  						Expect(resourcesPart.FormName()).To(Equal("resources"))
   613  
   614  						defer resourcesPart.Close()
   615  						expectedJSON, err := json.Marshal(resources)
   616  						Expect(err).NotTo(HaveOccurred())
   617  						Expect(ioutil.ReadAll(resourcesPart)).To(MatchJSON(expectedJSON))
   618  
   619  						// Verify that the application bits are not sent
   620  						resourcesPart, err = requestReader.NextPart()
   621  						Expect(err).To(MatchError(io.EOF))
   622  					}
   623  
   624  					response := `{
   625  					"metadata": {
   626  						"guid": "job-guid",
   627  						"url": "/v2/jobs/job-guid"
   628  					},
   629  					"entity": {
   630  						"guid": "job-guid",
   631  						"status": "queued"
   632  					}
   633  				}`
   634  
   635  					server.AppendHandlers(
   636  						CombineHandlers(
   637  							VerifyRequest(http.MethodPut, "/v2/apps/some-app-guid/bits", "async=true"),
   638  							verifyHeaderAndBody,
   639  							RespondWith(http.StatusOK, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}),
   640  						),
   641  					)
   642  				})
   643  
   644  				It("does not send the application bits", func() {
   645  					job, warnings, err := client.UploadApplicationPackage("some-app-guid", resources, nil, 33513531353)
   646  					Expect(err).NotTo(HaveOccurred())
   647  					Expect(warnings).To(ConsistOf("this is a warning"))
   648  					Expect(job).To(Equal(Job{
   649  						GUID:   "job-guid",
   650  						Status: constant.JobStatusQueued,
   651  					}))
   652  				})
   653  			})
   654  		})
   655  
   656  		Context("when the CC returns an error", func() {
   657  			BeforeEach(func() {
   658  				response := `{
   659  					"code": 30003,
   660  					"description": "Banana",
   661  					"error_code": "CF-Banana"
   662  				}`
   663  
   664  				server.AppendHandlers(
   665  					CombineHandlers(
   666  						VerifyRequest(http.MethodPut, "/v2/apps/some-app-guid/bits", "async=true"),
   667  						RespondWith(http.StatusNotFound, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}),
   668  					),
   669  				)
   670  			})
   671  
   672  			It("returns the error", func() {
   673  				_, warnings, err := client.UploadApplicationPackage("some-app-guid", []Resource{}, bytes.NewReader(nil), 0)
   674  				Expect(err).To(MatchError(ccerror.ResourceNotFoundError{Message: "Banana"}))
   675  				Expect(warnings).To(ConsistOf("this is a warning"))
   676  			})
   677  		})
   678  
   679  		Context("when passed a nil resources", func() {
   680  			It("returns a NilObjectError", func() {
   681  				_, _, err := client.UploadApplicationPackage("some-app-guid", nil, bytes.NewReader(nil), 0)
   682  				Expect(err).To(MatchError(ccerror.NilObjectError{Object: "existingResources"}))
   683  			})
   684  		})
   685  
   686  		Context("when an error is returned from the new resources reader", func() {
   687  			var (
   688  				fakeReader  *ccv2fakes.FakeReader
   689  				expectedErr error
   690  			)
   691  
   692  			BeforeEach(func() {
   693  				expectedErr = errors.New("some read error")
   694  				fakeReader = new(ccv2fakes.FakeReader)
   695  				fakeReader.ReadReturns(0, expectedErr)
   696  
   697  				server.AppendHandlers(
   698  					VerifyRequest(http.MethodPut, "/v2/apps/some-app-guid/bits", "async=true"),
   699  				)
   700  			})
   701  
   702  			It("returns the error", func() {
   703  				_, _, err := client.UploadApplicationPackage("some-app-guid", []Resource{}, fakeReader, 3)
   704  				Expect(err).To(MatchError(expectedErr))
   705  			})
   706  		})
   707  
   708  		Context("when a retryable error occurs", func() {
   709  			BeforeEach(func() {
   710  				wrapper := &wrapper.CustomWrapper{
   711  					CustomMake: func(connection cloudcontroller.Connection, request *cloudcontroller.Request, response *cloudcontroller.Response) error {
   712  						defer GinkgoRecover() // Since this will be running in a thread
   713  
   714  						if strings.HasSuffix(request.URL.String(), "/v2/apps/some-app-guid/bits?async=true") {
   715  							_, err := ioutil.ReadAll(request.Body)
   716  							Expect(err).ToNot(HaveOccurred())
   717  							Expect(request.Body.Close()).ToNot(HaveOccurred())
   718  							return request.ResetBody()
   719  						}
   720  						return connection.Make(request, response)
   721  					},
   722  				}
   723  
   724  				client = NewTestClient(Config{Wrappers: []ConnectionWrapper{wrapper}})
   725  			})
   726  
   727  			It("returns the PipeSeekError", func() {
   728  				_, _, err := client.UploadApplicationPackage("some-app-guid", []Resource{}, strings.NewReader("hello world"), 3)
   729  				Expect(err).To(MatchError(ccerror.PipeSeekError{}))
   730  			})
   731  		})
   732  
   733  		Context("when an http error occurs mid-transfer", func() {
   734  			var expectedErr error
   735  			const UploadSize = 33 * 1024
   736  
   737  			BeforeEach(func() {
   738  				expectedErr = errors.New("some read error")
   739  
   740  				wrapper := &wrapper.CustomWrapper{
   741  					CustomMake: func(connection cloudcontroller.Connection, request *cloudcontroller.Request, response *cloudcontroller.Response) error {
   742  						defer GinkgoRecover() // Since this will be running in a thread
   743  
   744  						if strings.HasSuffix(request.URL.String(), "/v2/apps/some-app-guid/bits?async=true") {
   745  							defer request.Body.Close()
   746  							readBytes, err := ioutil.ReadAll(request.Body)
   747  							Expect(err).ToNot(HaveOccurred())
   748  							Expect(len(readBytes)).To(BeNumerically(">", UploadSize))
   749  							return expectedErr
   750  						}
   751  						return connection.Make(request, response)
   752  					},
   753  				}
   754  
   755  				client = NewTestClient(Config{Wrappers: []ConnectionWrapper{wrapper}})
   756  			})
   757  
   758  			It("returns the http error", func() {
   759  				_, _, err := client.UploadApplicationPackage("some-app-guid", []Resource{}, strings.NewReader(strings.Repeat("a", UploadSize)), 3)
   760  				Expect(err).To(MatchError(expectedErr))
   761  			})
   762  		})
   763  	})
   764  
   765  	Describe("UploadDroplet", func() {
   766  		var (
   767  			appGUID    string
   768  			droplet    io.Reader
   769  			readerBody []byte
   770  		)
   771  
   772  		BeforeEach(func() {
   773  			client = NewTestClient()
   774  
   775  			appGUID = "some-app-guid"
   776  			readerBody = []byte("hello world")
   777  			droplet = bytes.NewReader(readerBody)
   778  		})
   779  
   780  		Context("when the Droplet is successful", func() {
   781  			BeforeEach(func() {
   782  				verifyHeaderAndBody := func(_ http.ResponseWriter, req *http.Request) {
   783  					contentType := req.Header.Get("Content-Type")
   784  					Expect(contentType).To(MatchRegexp("multipart/form-data; boundary=[\\w\\d]+"))
   785  
   786  					defer req.Body.Close()
   787  					requestReader := multipart.NewReader(req.Body, contentType[30:])
   788  
   789  					// Verify that matched resources are sent properly
   790  					resourcesPart, err := requestReader.NextPart()
   791  					Expect(err).NotTo(HaveOccurred())
   792  					defer resourcesPart.Close()
   793  
   794  					Expect(resourcesPart.FormName()).To(Equal("droplet"))
   795  					Expect(resourcesPart.FileName()).To(Equal("droplet.tgz"))
   796  					Expect(ioutil.ReadAll(resourcesPart)).To(Equal(readerBody))
   797  				}
   798  
   799  				response := `{
   800  					"metadata": {
   801  						"guid": "job-guid",
   802  						"url": "/v2/jobs/job-guid"
   803  					},
   804  					"entity": {
   805  						"guid": "job-guid",
   806  						"status": "queued"
   807  					}
   808  				}`
   809  
   810  				server.AppendHandlers(
   811  					CombineHandlers(
   812  						VerifyRequest(http.MethodPut, "/v2/apps/some-app-guid/droplet/upload"),
   813  						verifyHeaderAndBody,
   814  						RespondWith(http.StatusOK, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}),
   815  					),
   816  				)
   817  			})
   818  
   819  			It("returns the created job and warnings", func() {
   820  				job, warnings, err := client.UploadDroplet(appGUID, droplet, int64(len(readerBody)))
   821  				Expect(err).NotTo(HaveOccurred())
   822  				Expect(warnings).To(ConsistOf("this is a warning"))
   823  				Expect(job).To(Equal(Job{
   824  					GUID:   "job-guid",
   825  					Status: constant.JobStatusQueued,
   826  				}))
   827  			})
   828  		})
   829  
   830  		Context("when the CC returns an error", func() {
   831  			BeforeEach(func() {
   832  				response := `{
   833  					"code": 30003,
   834  					"description": "Banana",
   835  					"error_code": "CF-Banana"
   836  				}`
   837  
   838  				server.AppendHandlers(
   839  					CombineHandlers(
   840  						VerifyRequest(http.MethodPut, "/v2/apps/some-app-guid/droplet/upload"),
   841  						RespondWith(http.StatusNotFound, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}),
   842  					),
   843  				)
   844  			})
   845  
   846  			It("returns the error", func() {
   847  				_, warnings, err := client.UploadDroplet(appGUID, bytes.NewReader(nil), 0)
   848  				Expect(err).To(MatchError(ccerror.ResourceNotFoundError{Message: "Banana"}))
   849  				Expect(warnings).To(ConsistOf("this is a warning"))
   850  			})
   851  		})
   852  
   853  		Context("when there is an error reading the droplet", func() {
   854  			var (
   855  				fakeReader  *ccv2fakes.FakeReader
   856  				expectedErr error
   857  			)
   858  
   859  			BeforeEach(func() {
   860  				expectedErr = errors.New("some read error")
   861  				fakeReader = new(ccv2fakes.FakeReader)
   862  				fakeReader.ReadReturns(0, expectedErr)
   863  
   864  				server.AppendHandlers(
   865  					VerifyRequest(http.MethodPut, "/v2/apps/some-app-guid/droplet/upload"),
   866  				)
   867  			})
   868  
   869  			It("returns the error", func() {
   870  				_, _, err := client.UploadDroplet(appGUID, fakeReader, 3)
   871  				Expect(err).To(MatchError(expectedErr))
   872  			})
   873  		})
   874  
   875  		Context("when a retryable error occurs", func() {
   876  			BeforeEach(func() {
   877  				wrapper := &wrapper.CustomWrapper{
   878  					CustomMake: func(connection cloudcontroller.Connection, request *cloudcontroller.Request, response *cloudcontroller.Response) error {
   879  						defer GinkgoRecover() // Since this will be running in a thread
   880  
   881  						if strings.HasSuffix(request.URL.String(), "/v2/apps/some-app-guid/droplet/upload") {
   882  							_, err := ioutil.ReadAll(request.Body)
   883  							Expect(err).ToNot(HaveOccurred())
   884  							Expect(request.Body.Close()).ToNot(HaveOccurred())
   885  							return request.ResetBody()
   886  						}
   887  						return connection.Make(request, response)
   888  					},
   889  				}
   890  
   891  				client = NewTestClient(Config{Wrappers: []ConnectionWrapper{wrapper}})
   892  			})
   893  
   894  			It("returns the PipeSeekError", func() {
   895  				_, _, err := client.UploadDroplet(appGUID, strings.NewReader("hello world"), 3)
   896  				Expect(err).To(MatchError(ccerror.PipeSeekError{}))
   897  			})
   898  		})
   899  
   900  		Context("when an http error occurs mid-transfer", func() {
   901  			var expectedErr error
   902  			const UploadSize = 33 * 1024
   903  
   904  			BeforeEach(func() {
   905  				expectedErr = errors.New("some read error")
   906  
   907  				wrapper := &wrapper.CustomWrapper{
   908  					CustomMake: func(connection cloudcontroller.Connection, request *cloudcontroller.Request, response *cloudcontroller.Response) error {
   909  						defer GinkgoRecover() // Since this will be running in a thread
   910  
   911  						if strings.HasSuffix(request.URL.String(), "/v2/apps/some-app-guid/droplet/upload") {
   912  							defer request.Body.Close()
   913  							readBytes, err := ioutil.ReadAll(request.Body)
   914  							Expect(err).ToNot(HaveOccurred())
   915  							Expect(len(readBytes)).To(BeNumerically(">", UploadSize))
   916  							return expectedErr
   917  						}
   918  						return connection.Make(request, response)
   919  					},
   920  				}
   921  
   922  				client = NewTestClient(Config{Wrappers: []ConnectionWrapper{wrapper}})
   923  			})
   924  
   925  			It("returns the http error", func() {
   926  				_, _, err := client.UploadDroplet(appGUID, strings.NewReader(strings.Repeat("a", UploadSize)), UploadSize)
   927  				Expect(err).To(MatchError(expectedErr))
   928  			})
   929  		})
   930  	})
   931  })