github.com/cloudfoundry-community/cloudfoundry-cli@v6.44.1-0.20240130060226-cda5ed8e89a5+incompatible/api/cloudcontroller/ccv3/buildpack_test.go (about)

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