github.com/swisscom/cloudfoundry-cli@v7.1.0+incompatible/api/cloudcontroller/ccv2/buildpack_test.go (about)

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