github.com/LukasHeimann/cloudfoundrycli@v7.1.0+incompatible/api/cloudcontroller/ccv3/buildpack_test.go (about)

     1  package ccv3_test
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"io"
     7  	"io/ioutil"
     8  	"mime/multipart"
     9  	"net/http"
    10  	"strings"
    11  
    12  	"code.cloudfoundry.org/cli/api/cloudcontroller"
    13  	"code.cloudfoundry.org/cli/api/cloudcontroller/ccv3/ccv3fakes"
    14  	"code.cloudfoundry.org/cli/api/cloudcontroller/ccv3/constant"
    15  	"code.cloudfoundry.org/cli/api/cloudcontroller/wrapper"
    16  	"code.cloudfoundry.org/cli/types"
    17  
    18  	"code.cloudfoundry.org/cli/api/cloudcontroller/ccerror"
    19  	. "code.cloudfoundry.org/cli/api/cloudcontroller/ccv3"
    20  	. "code.cloudfoundry.org/cli/resources"
    21  	. "github.com/onsi/ginkgo"
    22  	. "github.com/onsi/gomega"
    23  	. "github.com/onsi/gomega/ghttp"
    24  )
    25  
    26  var _ = Describe("Buildpacks", func() {
    27  	var client *Client
    28  
    29  	BeforeEach(func() {
    30  		client, _ = NewTestClient()
    31  	})
    32  
    33  	Describe("GetBuildpacks", func() {
    34  		var (
    35  			query Query
    36  
    37  			buildpacks []Buildpack
    38  			warnings   Warnings
    39  			executeErr error
    40  		)
    41  
    42  		JustBeforeEach(func() {
    43  			buildpacks, warnings, executeErr = client.GetBuildpacks(query)
    44  		})
    45  
    46  		When("buildpacks exist", func() {
    47  			BeforeEach(func() {
    48  				response1 := fmt.Sprintf(`{
    49  					"pagination": {
    50  						"next": {
    51  							"href": "%s/v3/buildpacks?names=some-buildpack-name&page=2&per_page=2"
    52  						}
    53  					},
    54  					"resources": [
    55  						{
    56  							"guid": "guid1",
    57  							"name": "ruby_buildpack",
    58  							"state": "AWAITING_UPLOAD",
    59  							"stack": "windows64",
    60  							"position": 1,
    61  							"enabled": true,
    62  							"locked": false,
    63  							"metadata": {
    64  								"labels": {}
    65  							}
    66  						},
    67  						{
    68  							"guid": "guid2",
    69  							"name": "staticfile_buildpack",
    70  							"state": "AWAITING_UPLOAD",
    71  							"stack": "cflinuxfs3",
    72  							"position": 2,
    73  							"enabled": false,
    74  							"locked": true,
    75  							"metadata": {
    76  								"labels": {}
    77  							}
    78  						}
    79  					]
    80  				}`, server.URL())
    81  				response2 := `{
    82  					"pagination": {
    83  						"next": null
    84  					},
    85  					"resources": [
    86  						{
    87  							"guid": "guid3",
    88  							"name": "go_buildpack",
    89  							"state": "AWAITING_UPLOAD",
    90  							"stack": "cflinuxfs2",
    91  							"position": 3,
    92  							"enabled": true,
    93  							"locked": false,
    94  							"metadata": {
    95  								"labels": {}
    96  							}
    97  						}
    98  					]
    99  				}`
   100  
   101  				server.AppendHandlers(
   102  					CombineHandlers(
   103  						VerifyRequest(http.MethodGet, "/v3/buildpacks", "names=some-buildpack-name"),
   104  						RespondWith(http.StatusOK, response1, http.Header{"X-Cf-Warnings": {"this is a warning"}}),
   105  					),
   106  				)
   107  				server.AppendHandlers(
   108  					CombineHandlers(
   109  						VerifyRequest(http.MethodGet, "/v3/buildpacks", "names=some-buildpack-name&page=2&per_page=2"),
   110  						RespondWith(http.StatusOK, response2, http.Header{"X-Cf-Warnings": {"this is another warning"}}),
   111  					),
   112  				)
   113  
   114  				query = Query{
   115  					Key:    NameFilter,
   116  					Values: []string{"some-buildpack-name"},
   117  				}
   118  			})
   119  
   120  			It("returns the queried buildpacks and all warnings", func() {
   121  				Expect(executeErr).NotTo(HaveOccurred())
   122  
   123  				Expect(buildpacks).To(ConsistOf(
   124  					Buildpack{
   125  						Name:     "ruby_buildpack",
   126  						GUID:     "guid1",
   127  						Position: types.NullInt{Value: 1, IsSet: true},
   128  						Enabled:  types.NullBool{Value: true, IsSet: true},
   129  						Locked:   types.NullBool{Value: false, IsSet: true},
   130  						Stack:    "windows64",
   131  						State:    "AWAITING_UPLOAD",
   132  						Metadata: &Metadata{Labels: map[string]types.NullString{}},
   133  					},
   134  					Buildpack{
   135  						Name:     "staticfile_buildpack",
   136  						GUID:     "guid2",
   137  						Position: types.NullInt{Value: 2, IsSet: true},
   138  						Enabled:  types.NullBool{Value: false, IsSet: true},
   139  						Locked:   types.NullBool{Value: true, IsSet: true},
   140  						Stack:    "cflinuxfs3",
   141  						State:    "AWAITING_UPLOAD",
   142  						Metadata: &Metadata{Labels: map[string]types.NullString{}},
   143  					},
   144  					Buildpack{
   145  						Name:     "go_buildpack",
   146  						GUID:     "guid3",
   147  						Position: types.NullInt{Value: 3, IsSet: true},
   148  						Enabled:  types.NullBool{Value: true, IsSet: true},
   149  						Locked:   types.NullBool{Value: false, IsSet: true},
   150  						Stack:    "cflinuxfs2",
   151  						State:    "AWAITING_UPLOAD",
   152  						Metadata: &Metadata{Labels: map[string]types.NullString{}},
   153  					},
   154  				))
   155  				Expect(warnings).To(ConsistOf("this is a warning", "this is another warning"))
   156  			})
   157  		})
   158  
   159  		When("the cloud controller returns errors and warnings", func() {
   160  			BeforeEach(func() {
   161  				response := `{
   162    "errors": [
   163      {
   164        "code": 10008,
   165        "detail": "The request is semantically invalid: command presence",
   166        "title": "CF-UnprocessableEntity"
   167      },
   168      {
   169        "code": 10010,
   170        "detail": "buildpack not found",
   171        "title": "CF-buildpackNotFound"
   172      }
   173    ]
   174  }`
   175  				server.AppendHandlers(
   176  					CombineHandlers(
   177  						VerifyRequest(http.MethodGet, "/v3/buildpacks"),
   178  						RespondWith(http.StatusTeapot, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}),
   179  					),
   180  				)
   181  			})
   182  
   183  			It("returns the error and all warnings", func() {
   184  				Expect(executeErr).To(MatchError(ccerror.MultiError{
   185  					ResponseCode: http.StatusTeapot,
   186  					Errors: []ccerror.V3Error{
   187  						{
   188  							Code:   10008,
   189  							Detail: "The request is semantically invalid: command presence",
   190  							Title:  "CF-UnprocessableEntity",
   191  						},
   192  						{
   193  							Code:   10010,
   194  							Detail: "buildpack not found",
   195  							Title:  "CF-buildpackNotFound",
   196  						},
   197  					},
   198  				}))
   199  				Expect(warnings).To(ConsistOf("this is a warning"))
   200  			})
   201  		})
   202  	})
   203  
   204  	Describe("CreateBuildpack", func() {
   205  		var (
   206  			inputBuildpack Buildpack
   207  
   208  			bp         Buildpack
   209  			warnings   Warnings
   210  			executeErr error
   211  		)
   212  
   213  		JustBeforeEach(func() {
   214  			bp, warnings, executeErr = client.CreateBuildpack(inputBuildpack)
   215  		})
   216  
   217  		When("the buildpack is successfully created", func() {
   218  			BeforeEach(func() {
   219  				inputBuildpack = Buildpack{
   220  					Name:  "some-buildpack",
   221  					Stack: "some-stack",
   222  				}
   223  				response := `{
   224      				"guid": "some-bp-guid",
   225      				"created_at": "2016-03-18T23:26:46Z",
   226      				"updated_at": "2016-10-17T20:00:42Z",
   227      				"name": "some-buildpack",
   228      				"state": "AWAITING_UPLOAD",
   229      				"filename": null,
   230      				"stack": "some-stack",
   231      				"position": 42,
   232      				"enabled": true,
   233      				"locked": false,
   234      				"links": {
   235      				  "self": {
   236      				    "href": "/v3/buildpacks/some-bp-guid"
   237      				  },
   238  						"upload": {
   239  							"href": "/v3/buildpacks/some-bp-guid/upload",
   240  							"method": "POST"
   241  						}
   242      				}
   243  				}`
   244  
   245  				expectedBody := map[string]interface{}{
   246  					"name":  "some-buildpack",
   247  					"stack": "some-stack",
   248  				}
   249  				server.AppendHandlers(
   250  					CombineHandlers(
   251  						VerifyRequest(http.MethodPost, "/v3/buildpacks"),
   252  						VerifyJSONRepresenting(expectedBody),
   253  						RespondWith(http.StatusCreated, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}),
   254  					),
   255  				)
   256  			})
   257  
   258  			It("returns the created buildpack and warnings", func() {
   259  				Expect(executeErr).NotTo(HaveOccurred())
   260  				Expect(warnings).To(ConsistOf("this is a warning"))
   261  
   262  				expectedBuildpack := Buildpack{
   263  					GUID:     "some-bp-guid",
   264  					Name:     "some-buildpack",
   265  					Stack:    "some-stack",
   266  					Enabled:  types.NullBool{Value: true, IsSet: true},
   267  					Filename: "",
   268  					Locked:   types.NullBool{Value: false, IsSet: true},
   269  					State:    constant.BuildpackAwaitingUpload,
   270  					Position: types.NullInt{Value: 42, IsSet: true},
   271  					Links: APILinks{
   272  						"upload": APILink{
   273  							Method: "POST",
   274  							HREF:   "/v3/buildpacks/some-bp-guid/upload",
   275  						},
   276  						"self": APILink{
   277  							HREF: "/v3/buildpacks/some-bp-guid",
   278  						},
   279  					},
   280  				}
   281  				Expect(bp).To(Equal(expectedBuildpack))
   282  			})
   283  		})
   284  
   285  		When("cc returns back an error or warnings", func() {
   286  			BeforeEach(func() {
   287  				inputBuildpack = Buildpack{}
   288  				response := ` {
   289    "errors": [
   290      {
   291        "code": 10008,
   292        "detail": "The request is semantically invalid: command presence",
   293        "title": "CF-UnprocessableEntity"
   294      },
   295      {
   296        "code": 10010,
   297        "detail": "Buildpack not found",
   298        "title": "CF-ResourceNotFound"
   299      }
   300    ]
   301  }`
   302  				server.AppendHandlers(
   303  					CombineHandlers(
   304  						VerifyRequest(http.MethodPost, "/v3/buildpacks"),
   305  						RespondWith(http.StatusTeapot, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}),
   306  					),
   307  				)
   308  			})
   309  
   310  			It("returns the error and all warnings", func() {
   311  				Expect(executeErr).To(MatchError(ccerror.MultiError{
   312  					ResponseCode: http.StatusTeapot,
   313  					Errors: []ccerror.V3Error{
   314  						{
   315  							Code:   10008,
   316  							Detail: "The request is semantically invalid: command presence",
   317  							Title:  "CF-UnprocessableEntity",
   318  						},
   319  						{
   320  							Code:   10010,
   321  							Detail: "Buildpack not found",
   322  							Title:  "CF-ResourceNotFound",
   323  						},
   324  					},
   325  				}))
   326  				Expect(warnings).To(ConsistOf("this is a warning"))
   327  			})
   328  		})
   329  	})
   330  
   331  	Describe("UploadBuildpack", func() {
   332  		var (
   333  			jobURL     JobURL
   334  			warnings   Warnings
   335  			executeErr error
   336  			bpFile     io.Reader
   337  			bpFilePath string
   338  			bpContent  string
   339  		)
   340  
   341  		BeforeEach(func() {
   342  			bpContent = "some-content"
   343  			bpFile = strings.NewReader(bpContent)
   344  			bpFilePath = "some/fake-buildpack.zip"
   345  		})
   346  
   347  		JustBeforeEach(func() {
   348  			jobURL, warnings, executeErr = client.UploadBuildpack("some-buildpack-guid", bpFilePath, bpFile, int64(len(bpContent)))
   349  		})
   350  
   351  		When("the upload is successful", func() {
   352  			BeforeEach(func() {
   353  				response := `{
   354  										"metadata": {
   355  											"guid": "some-buildpack-guid",
   356  											"url": "/v3/buildpacks/buildpack-guid/upload"
   357  										},
   358  										"entity": {
   359  											"guid": "some-buildpack-guid",
   360  											"status": "queued"
   361  										}
   362  									}`
   363  
   364  				verifyHeaderAndBody := func(_ http.ResponseWriter, req *http.Request) {
   365  					contentType := req.Header.Get("Content-Type")
   366  					Expect(contentType).To(MatchRegexp("multipart/form-data; boundary=[\\w\\d]+"))
   367  
   368  					defer req.Body.Close()
   369  					requestReader := multipart.NewReader(req.Body, contentType[30:])
   370  
   371  					buildpackPart, err := requestReader.NextPart()
   372  					Expect(err).NotTo(HaveOccurred())
   373  
   374  					Expect(buildpackPart.FormName()).To(Equal("bits"))
   375  					Expect(buildpackPart.FileName()).To(Equal("fake-buildpack.zip"))
   376  
   377  					defer buildpackPart.Close()
   378  					partContents, err := ioutil.ReadAll(buildpackPart)
   379  					Expect(err).ToNot(HaveOccurred())
   380  					Expect(string(partContents)).To(Equal(bpContent))
   381  				}
   382  
   383  				server.AppendHandlers(
   384  					CombineHandlers(
   385  						VerifyRequest(http.MethodPost, "/v3/buildpacks/some-buildpack-guid/upload"),
   386  						verifyHeaderAndBody,
   387  						RespondWith(
   388  							http.StatusAccepted,
   389  							response,
   390  							http.Header{
   391  								"X-Cf-Warnings": {"this is a warning"},
   392  								"Location":      {"http://example.com/job-guid"},
   393  							},
   394  						),
   395  					),
   396  				)
   397  			})
   398  
   399  			It("returns the processing job URL and warnings", func() {
   400  				Expect(executeErr).ToNot(HaveOccurred())
   401  				Expect(warnings).To(ConsistOf(Warnings{"this is a warning"}))
   402  				Expect(jobURL).To(Equal(JobURL("http://example.com/job-guid")))
   403  			})
   404  		})
   405  
   406  		When("there is an error reading the buildpack", func() {
   407  			var (
   408  				fakeReader  *ccv3fakes.FakeReader
   409  				expectedErr error
   410  			)
   411  
   412  			BeforeEach(func() {
   413  				expectedErr = errors.New("some read error")
   414  				fakeReader = new(ccv3fakes.FakeReader)
   415  				fakeReader.ReadReturns(0, expectedErr)
   416  				bpFile = fakeReader
   417  
   418  				server.AppendHandlers(
   419  					VerifyRequest(http.MethodPost, "/v3/buildpacks/some-buildpack-guid/upload"),
   420  				)
   421  			})
   422  
   423  			It("returns the error", func() {
   424  				Expect(executeErr).To(MatchError(expectedErr))
   425  			})
   426  		})
   427  
   428  		When("the upload returns an error", func() {
   429  			BeforeEach(func() {
   430  				response := `{
   431  					"errors": [{
   432                          "detail": "The buildpack could not be found: some-buildpack-guid",
   433                          "title": "CF-ResourceNotFound",
   434                          "code": 10010
   435                      }]
   436                  }`
   437  
   438  				server.AppendHandlers(
   439  					CombineHandlers(
   440  						VerifyRequest(http.MethodPost, "/v3/buildpacks/some-buildpack-guid/upload"),
   441  						RespondWith(http.StatusNotFound, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}),
   442  					),
   443  				)
   444  			})
   445  
   446  			It("returns the error and warnings", func() {
   447  				Expect(executeErr).To(MatchError(
   448  					ccerror.ResourceNotFoundError{
   449  						Message: "The buildpack could not be found: some-buildpack-guid",
   450  					},
   451  				))
   452  				Expect(warnings).To(ConsistOf(Warnings{"this is a warning"}))
   453  			})
   454  		})
   455  
   456  		When("a retryable error occurs", func() {
   457  			BeforeEach(func() {
   458  				wrapper := &wrapper.CustomWrapper{
   459  					CustomMake: func(connection cloudcontroller.Connection, request *cloudcontroller.Request, response *cloudcontroller.Response) error {
   460  						defer GinkgoRecover() // Since this will be running in a thread
   461  
   462  						if strings.HasSuffix(request.URL.String(), "/v3/buildpacks/some-buildpack-guid/upload") {
   463  							_, err := ioutil.ReadAll(request.Body)
   464  							Expect(err).ToNot(HaveOccurred())
   465  							Expect(request.Body.Close()).ToNot(HaveOccurred())
   466  							return request.ResetBody()
   467  						}
   468  						return connection.Make(request, response)
   469  					},
   470  				}
   471  
   472  				client, _ = NewTestClient(Config{Wrappers: []ConnectionWrapper{wrapper}})
   473  			})
   474  
   475  			It("returns the PipeSeekError", func() {
   476  				Expect(executeErr).To(MatchError(ccerror.PipeSeekError{}))
   477  			})
   478  		})
   479  
   480  		When("an http error occurs mid-transfer", func() {
   481  			var expectedErr error
   482  
   483  			BeforeEach(func() {
   484  				expectedErr = errors.New("some read error")
   485  
   486  				wrapper := &wrapper.CustomWrapper{
   487  					CustomMake: func(connection cloudcontroller.Connection, request *cloudcontroller.Request, response *cloudcontroller.Response) error {
   488  						defer GinkgoRecover() // Since this will be running in a thread
   489  
   490  						if strings.HasSuffix(request.URL.String(), "/v3/buildpacks/some-buildpack-guid/upload") {
   491  							defer request.Body.Close()
   492  							readBytes, err := ioutil.ReadAll(request.Body)
   493  							Expect(err).ToNot(HaveOccurred())
   494  							Expect(len(readBytes)).To(BeNumerically(">", len(bpContent)))
   495  							return expectedErr
   496  						}
   497  						return connection.Make(request, response)
   498  					},
   499  				}
   500  
   501  				client, _ = NewTestClient(Config{Wrappers: []ConnectionWrapper{wrapper}})
   502  			})
   503  
   504  			It("returns the http error", func() {
   505  				Expect(executeErr).To(MatchError(expectedErr))
   506  			})
   507  		})
   508  	})
   509  
   510  	Describe("UpdateBuildpack", func() {
   511  		var (
   512  			inputBuildpack Buildpack
   513  
   514  			bp         Buildpack
   515  			warnings   Warnings
   516  			executeErr error
   517  		)
   518  
   519  		JustBeforeEach(func() {
   520  			bp, warnings, executeErr = client.UpdateBuildpack(inputBuildpack)
   521  		})
   522  
   523  		When("the buildpack is successfully created", func() {
   524  			BeforeEach(func() {
   525  				inputBuildpack = Buildpack{
   526  					Name:   "some-buildpack",
   527  					GUID:   "some-bp-guid",
   528  					Stack:  "some-stack",
   529  					Locked: types.NullBool{IsSet: true, Value: true},
   530  				}
   531  				response := `{
   532      				"guid": "some-bp-guid",
   533      				"created_at": "2016-03-18T23:26:46Z",
   534      				"updated_at": "2016-10-17T20:00:42Z",
   535      				"name": "some-buildpack",
   536      				"state": "AWAITING_UPLOAD",
   537      				"filename": null,
   538      				"stack": "some-stack",
   539      				"position": 42,
   540      				"enabled": true,
   541      				"locked": true,
   542      				"links": {
   543      				  "self": {
   544      				    "href": "/v3/buildpacks/some-bp-guid"
   545      				  },
   546  						"upload": {
   547  							"href": "/v3/buildpacks/some-bp-guid/upload",
   548  							"method": "POST"
   549  						}
   550      				}
   551  				}`
   552  
   553  				expectedBody := map[string]interface{}{
   554  					"name":   "some-buildpack",
   555  					"stack":  "some-stack",
   556  					"locked": true,
   557  				}
   558  				server.AppendHandlers(
   559  					CombineHandlers(
   560  						VerifyRequest(http.MethodPatch, "/v3/buildpacks/some-bp-guid"),
   561  						VerifyJSONRepresenting(expectedBody),
   562  						RespondWith(http.StatusAccepted, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}),
   563  					),
   564  				)
   565  			})
   566  
   567  			It("returns the created buildpack and warnings", func() {
   568  				Expect(executeErr).NotTo(HaveOccurred())
   569  				Expect(warnings).To(ConsistOf("this is a warning"))
   570  
   571  				expectedBuildpack := Buildpack{
   572  					GUID:     "some-bp-guid",
   573  					Name:     "some-buildpack",
   574  					Stack:    "some-stack",
   575  					Enabled:  types.NullBool{Value: true, IsSet: true},
   576  					Filename: "",
   577  					Locked:   types.NullBool{Value: true, IsSet: true},
   578  					State:    constant.BuildpackAwaitingUpload,
   579  					Position: types.NullInt{Value: 42, IsSet: true},
   580  					Links: APILinks{
   581  						"upload": APILink{
   582  							Method: "POST",
   583  							HREF:   "/v3/buildpacks/some-bp-guid/upload",
   584  						},
   585  						"self": APILink{
   586  							HREF: "/v3/buildpacks/some-bp-guid",
   587  						},
   588  					},
   589  				}
   590  				Expect(bp).To(Equal(expectedBuildpack))
   591  			})
   592  		})
   593  
   594  		When("cc returns back an error or warnings", func() {
   595  			BeforeEach(func() {
   596  				inputBuildpack = Buildpack{
   597  					GUID: "some-bp-guid",
   598  				}
   599  				response := ` {
   600    "errors": [
   601      {
   602        "code": 10008,
   603        "detail": "The request is semantically invalid: command presence",
   604        "title": "CF-UnprocessableEntity"
   605      },
   606      {
   607        "code": 10010,
   608        "detail": "Buildpack not found",
   609        "title": "CF-ResourceNotFound"
   610      }
   611    ]
   612  }`
   613  				server.AppendHandlers(
   614  					CombineHandlers(
   615  						VerifyRequest(http.MethodPatch, "/v3/buildpacks/some-bp-guid"),
   616  						RespondWith(http.StatusTeapot, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}),
   617  					),
   618  				)
   619  			})
   620  
   621  			It("returns the error and all warnings", func() {
   622  				Expect(executeErr).To(MatchError(ccerror.MultiError{
   623  					ResponseCode: http.StatusTeapot,
   624  					Errors: []ccerror.V3Error{
   625  						{
   626  							Code:   10008,
   627  							Detail: "The request is semantically invalid: command presence",
   628  							Title:  "CF-UnprocessableEntity",
   629  						},
   630  						{
   631  							Code:   10010,
   632  							Detail: "Buildpack not found",
   633  							Title:  "CF-ResourceNotFound",
   634  						},
   635  					},
   636  				}))
   637  				Expect(warnings).To(ConsistOf("this is a warning"))
   638  			})
   639  		})
   640  	})
   641  
   642  	Describe("DeleteBuildpacks", func() {
   643  		var (
   644  			buildpackGUID = "some-guid"
   645  
   646  			jobURL     JobURL
   647  			warnings   Warnings
   648  			executeErr error
   649  		)
   650  
   651  		JustBeforeEach(func() {
   652  			jobURL, warnings, executeErr = client.DeleteBuildpack(buildpackGUID)
   653  		})
   654  
   655  		When("buildpacks exist", func() {
   656  			BeforeEach(func() {
   657  				server.AppendHandlers(
   658  					CombineHandlers(
   659  						VerifyRequest(http.MethodDelete, "/v3/buildpacks/"+buildpackGUID),
   660  						RespondWith(http.StatusAccepted, "{}", http.Header{"X-Cf-Warnings": {"this is a warning"}, "Location": {"some-job-url"}}),
   661  					),
   662  				)
   663  			})
   664  
   665  			It("returns the delete job URL and all warnings", func() {
   666  				Expect(executeErr).NotTo(HaveOccurred())
   667  
   668  				Expect(jobURL).To(Equal(JobURL("some-job-url")))
   669  				Expect(warnings).To(ConsistOf("this is a warning"))
   670  			})
   671  		})
   672  
   673  		When("the cloud controller returns errors and warnings", func() {
   674  			BeforeEach(func() {
   675  				response := `{
   676    "errors": [
   677      {
   678        "code": 10008,
   679        "detail": "The request is semantically invalid: command presence",
   680        "title": "CF-UnprocessableEntity"
   681      },
   682      {
   683        "code": 10010,
   684        "detail": "buildpack not found",
   685        "title": "CF-buildpackNotFound"
   686      }
   687    ]
   688  }`
   689  				server.AppendHandlers(
   690  					CombineHandlers(
   691  						VerifyRequest(http.MethodDelete, "/v3/buildpacks/"+buildpackGUID),
   692  						RespondWith(http.StatusTeapot, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}),
   693  					),
   694  				)
   695  			})
   696  
   697  			It("returns the error and all warnings", func() {
   698  				Expect(executeErr).To(MatchError(ccerror.MultiError{
   699  					ResponseCode: http.StatusTeapot,
   700  					Errors: []ccerror.V3Error{
   701  						{
   702  							Code:   10008,
   703  							Detail: "The request is semantically invalid: command presence",
   704  							Title:  "CF-UnprocessableEntity",
   705  						},
   706  						{
   707  							Code:   10010,
   708  							Detail: "buildpack not found",
   709  							Title:  "CF-buildpackNotFound",
   710  						},
   711  					},
   712  				}))
   713  				Expect(warnings).To(ConsistOf("this is a warning"))
   714  			})
   715  		})
   716  	})
   717  })