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