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

     1  package ccv3_test
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"io"
     7  	"net/http"
     8  	"strings"
     9  
    10  	"code.cloudfoundry.org/cli/api/cloudcontroller/ccerror"
    11  	. "code.cloudfoundry.org/cli/api/cloudcontroller/ccv3"
    12  	"code.cloudfoundry.org/cli/api/cloudcontroller/ccv3/constant"
    13  	"code.cloudfoundry.org/cli/api/cloudcontroller/ccv3/internal"
    14  	"code.cloudfoundry.org/cli/resources"
    15  	. "code.cloudfoundry.org/cli/resources"
    16  	"code.cloudfoundry.org/cli/types"
    17  	. "github.com/onsi/ginkgo"
    18  	. "github.com/onsi/gomega"
    19  	. "github.com/onsi/gomega/ghttp"
    20  )
    21  
    22  var _ = Describe("shared request helpers", func() {
    23  	var client *Client
    24  
    25  	BeforeEach(func() {
    26  		client, _ = NewTestClient()
    27  	})
    28  
    29  	Describe("MakeRequest", func() {
    30  		var (
    31  			requestParams RequestParams
    32  
    33  			jobURL     JobURL
    34  			warnings   Warnings
    35  			executeErr error
    36  		)
    37  
    38  		BeforeEach(func() {
    39  			requestParams = RequestParams{}
    40  		})
    41  
    42  		JustBeforeEach(func() {
    43  			jobURL, warnings, executeErr = client.MakeRequest(requestParams)
    44  		})
    45  
    46  		Context("GET single resource", func() {
    47  			var (
    48  				responseBody Organization
    49  			)
    50  
    51  			BeforeEach(func() {
    52  				requestParams = RequestParams{
    53  					RequestName:  internal.GetOrganizationRequest,
    54  					URIParams:    internal.Params{"organization_guid": "some-org-guid"},
    55  					ResponseBody: &responseBody,
    56  				}
    57  			})
    58  
    59  			When("organization exists", func() {
    60  				BeforeEach(func() {
    61  					response := `{
    62  					"name": "some-org-name",
    63  					"guid": "some-org-guid",
    64  					"relationships": {
    65  						"quota": {
    66  							"data": {
    67  								"guid": "some-org-quota-guid"
    68  							}
    69  						}
    70  					}
    71  				}`
    72  
    73  					server.AppendHandlers(
    74  						CombineHandlers(
    75  							VerifyRequest(http.MethodGet, "/v3/organizations/some-org-guid"),
    76  							RespondWith(http.StatusOK, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}),
    77  						),
    78  					)
    79  				})
    80  
    81  				It("returns the queried organization and all warnings", func() {
    82  					Expect(executeErr).NotTo(HaveOccurred())
    83  					Expect(responseBody).To(Equal(Organization{
    84  						Name:      "some-org-name",
    85  						GUID:      "some-org-guid",
    86  						QuotaGUID: "some-org-quota-guid",
    87  					}))
    88  					Expect(warnings).To(ConsistOf("this is a warning"))
    89  				})
    90  			})
    91  
    92  			When("the cloud controller returns errors and warnings", func() {
    93  				BeforeEach(func() {
    94  					response := `{
    95  				  "errors": [
    96  					{
    97  					  "code": 10008,
    98  					  "detail": "The request is semantically invalid: command presence",
    99  					  "title": "CF-UnprocessableEntity"
   100  					},
   101  					{
   102  					  "code": 10010,
   103  					  "detail": "Org not found",
   104  					  "title": "CF-ResourceNotFound"
   105  					}
   106  				  ]
   107  				}`
   108  
   109  					server.AppendHandlers(
   110  						CombineHandlers(
   111  							VerifyRequest(http.MethodGet, "/v3/organizations/some-org-guid"),
   112  							RespondWith(http.StatusTeapot, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}),
   113  						),
   114  					)
   115  				})
   116  
   117  				It("returns the error and all warnings", func() {
   118  					Expect(executeErr).To(MatchError(ccerror.MultiError{
   119  						ResponseCode: http.StatusTeapot,
   120  						Errors: []ccerror.V3Error{
   121  							{
   122  								Code:   10008,
   123  								Detail: "The request is semantically invalid: command presence",
   124  								Title:  "CF-UnprocessableEntity",
   125  							},
   126  							{
   127  								Code:   10010,
   128  								Detail: "Org not found",
   129  								Title:  "CF-ResourceNotFound",
   130  							},
   131  						},
   132  					}))
   133  					Expect(warnings).To(ConsistOf("this is a warning"))
   134  				})
   135  			})
   136  		})
   137  
   138  		Context("POST resource", func() {
   139  			var (
   140  				requestBody  Buildpack
   141  				responseBody Buildpack
   142  			)
   143  
   144  			BeforeEach(func() {
   145  				requestBody = Buildpack{
   146  					Name:  "some-buildpack",
   147  					Stack: "some-stack",
   148  				}
   149  
   150  				requestParams = RequestParams{
   151  					RequestName:  internal.PostBuildpackRequest,
   152  					RequestBody:  requestBody,
   153  					ResponseBody: &responseBody,
   154  				}
   155  			})
   156  
   157  			When("the resource is successfully created", func() {
   158  				BeforeEach(func() {
   159  					response := `{
   160  						"guid": "some-bp-guid",
   161  						"created_at": "2016-03-18T23:26:46Z",
   162  						"updated_at": "2016-10-17T20:00:42Z",
   163  						"name": "some-buildpack",
   164  						"state": "AWAITING_UPLOAD",
   165  						"filename": null,
   166  						"stack": "some-stack",
   167  						"position": 42,
   168  						"enabled": true,
   169  						"locked": false,
   170  						"links": {
   171  						  "self": {
   172  							"href": "/v3/buildpacks/some-bp-guid"
   173  						  },
   174  							"upload": {
   175  								"href": "/v3/buildpacks/some-bp-guid/upload",
   176  								"method": "POST"
   177  							}
   178  						}
   179  					}`
   180  
   181  					expectedBody := map[string]interface{}{
   182  						"name":  "some-buildpack",
   183  						"stack": "some-stack",
   184  					}
   185  
   186  					server.AppendHandlers(
   187  						CombineHandlers(
   188  							VerifyRequest(http.MethodPost, "/v3/buildpacks"),
   189  							VerifyJSONRepresenting(expectedBody),
   190  							RespondWith(http.StatusCreated, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}),
   191  						),
   192  					)
   193  				})
   194  
   195  				It("returns the resource and warnings", func() {
   196  					Expect(jobURL).To(Equal(JobURL("")))
   197  					Expect(executeErr).NotTo(HaveOccurred())
   198  					Expect(warnings).To(ConsistOf("this is a warning"))
   199  
   200  					expectedBuildpack := Buildpack{
   201  						GUID:     "some-bp-guid",
   202  						Name:     "some-buildpack",
   203  						Stack:    "some-stack",
   204  						Enabled:  types.NullBool{Value: true, IsSet: true},
   205  						Filename: "",
   206  						Locked:   types.NullBool{Value: false, IsSet: true},
   207  						State:    constant.BuildpackAwaitingUpload,
   208  						Position: types.NullInt{Value: 42, IsSet: true},
   209  						Links: APILinks{
   210  							"upload": APILink{
   211  								Method: "POST",
   212  								HREF:   "/v3/buildpacks/some-bp-guid/upload",
   213  							},
   214  							"self": APILink{
   215  								HREF: "/v3/buildpacks/some-bp-guid",
   216  							},
   217  						},
   218  					}
   219  
   220  					Expect(responseBody).To(Equal(expectedBuildpack))
   221  				})
   222  			})
   223  
   224  			When("the resource returns all errors and warnings", func() {
   225  				BeforeEach(func() {
   226  					response := ` {
   227    "errors": [
   228      {
   229        "code": 10008,
   230        "detail": "The request is semantically invalid: command presence",
   231        "title": "CF-UnprocessableEntity"
   232      },
   233      {
   234        "code": 10010,
   235        "detail": "Buildpack not found",
   236        "title": "CF-ResourceNotFound"
   237      }
   238    ]
   239  }`
   240  					server.AppendHandlers(
   241  						CombineHandlers(
   242  							VerifyRequest(http.MethodPost, "/v3/buildpacks"),
   243  							RespondWith(http.StatusTeapot, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}),
   244  						),
   245  					)
   246  				})
   247  
   248  				It("returns the error and all warnings", func() {
   249  					Expect(executeErr).To(MatchError(ccerror.MultiError{
   250  						ResponseCode: http.StatusTeapot,
   251  						Errors: []ccerror.V3Error{
   252  							{
   253  								Code:   10008,
   254  								Detail: "The request is semantically invalid: command presence",
   255  								Title:  "CF-UnprocessableEntity",
   256  							},
   257  							{
   258  								Code:   10010,
   259  								Detail: "Buildpack not found",
   260  								Title:  "CF-ResourceNotFound",
   261  							},
   262  						},
   263  					}))
   264  					Expect(warnings).To(ConsistOf("this is a warning"))
   265  				})
   266  			})
   267  		})
   268  
   269  		Context("DELETE resource", func() {
   270  			BeforeEach(func() {
   271  				requestParams = RequestParams{
   272  					RequestName: internal.DeleteSpaceRequest,
   273  					URIParams:   internal.Params{"space_guid": "space-guid"},
   274  				}
   275  			})
   276  
   277  			When("no errors are encountered", func() {
   278  				BeforeEach(func() {
   279  
   280  					server.AppendHandlers(
   281  						CombineHandlers(
   282  							VerifyRequest(http.MethodDelete, "/v3/spaces/space-guid"),
   283  							RespondWith(http.StatusAccepted, nil, http.Header{"X-Cf-Warnings": {"warning-1, warning-2"}, "Location": []string{"job-url"}}),
   284  						))
   285  				})
   286  
   287  				It("deletes the Space and returns all warnings", func() {
   288  					Expect(executeErr).NotTo(HaveOccurred())
   289  					Expect(warnings).To(ConsistOf(Warnings{"warning-1", "warning-2"}))
   290  					Expect(jobURL).To(Equal(JobURL("job-url")))
   291  				})
   292  			})
   293  
   294  			When("an error is encountered", func() {
   295  				BeforeEach(func() {
   296  					response := `{
   297     "errors": [
   298        {
   299           "detail": "Space not found",
   300           "title": "CF-ResourceNotFound",
   301           "code": 10010
   302        }
   303     ]
   304  }`
   305  					server.AppendHandlers(
   306  						CombineHandlers(
   307  							VerifyRequest(http.MethodDelete, "/v3/spaces/space-guid"),
   308  							RespondWith(http.StatusNotFound, response, http.Header{"X-Cf-Warnings": {"warning-1, warning-2"}}),
   309  						))
   310  				})
   311  
   312  				It("returns an error and all warnings", func() {
   313  					Expect(executeErr).To(MatchError(ccerror.ResourceNotFoundError{
   314  						Message: "Space not found",
   315  					}))
   316  					Expect(warnings).To(ConsistOf(Warnings{"warning-1", "warning-2"}))
   317  				})
   318  			})
   319  		})
   320  
   321  		Context("PATCH resource", func() {
   322  			var (
   323  				responseBody resources.Application
   324  			)
   325  
   326  			BeforeEach(func() {
   327  				requestBody := resources.Application{
   328  					GUID:                "some-app-guid",
   329  					Name:                "some-app-name",
   330  					StackName:           "some-stack-name",
   331  					LifecycleType:       constant.AppLifecycleTypeBuildpack,
   332  					LifecycleBuildpacks: []string{"some-buildpack"},
   333  					SpaceGUID:           "some-space-guid",
   334  				}
   335  				requestParams = RequestParams{
   336  					RequestName:  internal.PatchApplicationRequest,
   337  					URIParams:    internal.Params{"app_guid": requestBody.GUID},
   338  					RequestBody:  requestBody,
   339  					ResponseBody: &responseBody,
   340  				}
   341  
   342  			})
   343  
   344  			When("the application successfully is updated", func() {
   345  				BeforeEach(func() {
   346  
   347  					response := `{
   348  					"guid": "some-app-guid",
   349  					"name": "some-app-name",
   350  					"lifecycle": {
   351  						"type": "buildpack",
   352  						"data": {
   353  							"buildpacks": ["some-buildpack"],
   354  							"stack": "some-stack-name"
   355  						}
   356  					}
   357  				}`
   358  
   359  					expectedBody := map[string]interface{}{
   360  						"name": "some-app-name",
   361  						"lifecycle": map[string]interface{}{
   362  							"type": "buildpack",
   363  							"data": map[string]interface{}{
   364  								"buildpacks": []string{"some-buildpack"},
   365  								"stack":      "some-stack-name",
   366  							},
   367  						},
   368  						"relationships": map[string]interface{}{
   369  							"space": map[string]interface{}{
   370  								"data": map[string]string{
   371  									"guid": "some-space-guid",
   372  								},
   373  							},
   374  						},
   375  					}
   376  					server.AppendHandlers(
   377  						CombineHandlers(
   378  							VerifyRequest(http.MethodPatch, "/v3/apps/some-app-guid"),
   379  							VerifyJSONRepresenting(expectedBody),
   380  							RespondWith(http.StatusOK, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}),
   381  						),
   382  					)
   383  				})
   384  
   385  				It("returns the updated app and warnings", func() {
   386  					Expect(executeErr).NotTo(HaveOccurred())
   387  					Expect(warnings).To(ConsistOf("this is a warning"))
   388  
   389  					Expect(responseBody).To(Equal(resources.Application{
   390  						GUID:                "some-app-guid",
   391  						StackName:           "some-stack-name",
   392  						LifecycleBuildpacks: []string{"some-buildpack"},
   393  						LifecycleType:       constant.AppLifecycleTypeBuildpack,
   394  						Name:                "some-app-name",
   395  					}))
   396  				})
   397  			})
   398  
   399  			When("cc returns back an error or warnings", func() {
   400  				BeforeEach(func() {
   401  					response := `{
   402    "errors": [
   403      {
   404        "code": 10008,
   405        "detail": "The request is semantically invalid: command presence",
   406        "title": "CF-UnprocessableEntity"
   407      },
   408      {
   409        "code": 10010,
   410        "detail": "App not found",
   411        "title": "CF-ResourceNotFound"
   412      }
   413    ]
   414  }`
   415  					server.AppendHandlers(
   416  						CombineHandlers(
   417  							VerifyRequest(http.MethodPatch, "/v3/apps/some-app-guid"),
   418  							RespondWith(http.StatusTeapot, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}),
   419  						),
   420  					)
   421  				})
   422  
   423  				It("returns the error and all warnings", func() {
   424  					Expect(executeErr).To(MatchError(ccerror.MultiError{
   425  						ResponseCode: http.StatusTeapot,
   426  						Errors: []ccerror.V3Error{
   427  							{
   428  								Code:   10008,
   429  								Detail: "The request is semantically invalid: command presence",
   430  								Title:  "CF-UnprocessableEntity",
   431  							},
   432  							{
   433  								Code:   10010,
   434  								Detail: "App not found",
   435  								Title:  "CF-ResourceNotFound",
   436  							},
   437  						},
   438  					}))
   439  					Expect(warnings).To(ConsistOf("this is a warning"))
   440  				})
   441  			})
   442  		})
   443  	})
   444  
   445  	Describe("MakeListRequest", func() {
   446  		var (
   447  			requestParams RequestParams
   448  
   449  			includedResources IncludedResources
   450  			warnings          Warnings
   451  			executeErr        error
   452  		)
   453  
   454  		JustBeforeEach(func() {
   455  			includedResources, warnings, executeErr = client.MakeListRequest(requestParams)
   456  		})
   457  
   458  		Context("with query params and included resources", func() {
   459  			var (
   460  				resourceList []resources.Role
   461  				query        []Query
   462  			)
   463  
   464  			BeforeEach(func() {
   465  				resourceList = []resources.Role{}
   466  				query = []Query{
   467  					{
   468  						Key:    OrganizationGUIDFilter,
   469  						Values: []string{"some-org-name"},
   470  					},
   471  					{
   472  						Key:    Include,
   473  						Values: []string{"users"},
   474  					},
   475  				}
   476  				requestParams = RequestParams{
   477  					RequestName:  internal.GetRolesRequest,
   478  					Query:        query,
   479  					ResponseBody: resources.Role{},
   480  					AppendToList: func(item interface{}) error {
   481  						resourceList = append(resourceList, item.(resources.Role))
   482  						return nil
   483  					},
   484  				}
   485  			})
   486  
   487  			When("the request succeeds", func() {
   488  				BeforeEach(func() {
   489  					response1 := fmt.Sprintf(`{
   490  	"pagination": {
   491  		"next": {
   492  			"href": "%s/v3/roles?organization_guids=some-org-name&page=2&per_page=1&include=users"
   493  		}
   494  	},
   495    "resources": [
   496      {
   497        "guid": "role-guid-1",
   498        "type": "organization_user"
   499      }
   500    ]
   501  }`, server.URL())
   502  					response2 := `{
   503  							"pagination": {
   504  								"next": null
   505  							},
   506  						 "resources": [
   507  						   {
   508  						     "guid": "role-guid-2",
   509  						     "type": "organization_manager"
   510  						   }
   511  						 ]
   512  						}`
   513  
   514  					server.AppendHandlers(
   515  						CombineHandlers(
   516  							VerifyRequest(http.MethodGet, "/v3/roles", "organization_guids=some-org-name&include=users"),
   517  							RespondWith(http.StatusOK, response1, http.Header{"X-Cf-Warnings": {"warning-1"}}),
   518  						),
   519  					)
   520  					server.AppendHandlers(
   521  						CombineHandlers(
   522  							VerifyRequest(http.MethodGet, "/v3/roles", "organization_guids=some-org-name&page=2&per_page=1&include=users"),
   523  							RespondWith(http.StatusOK, response2, http.Header{"X-Cf-Warnings": {"warning-2"}}),
   524  						),
   525  					)
   526  				})
   527  
   528  				It("returns the given resources and all warnings", func() {
   529  					Expect(executeErr).ToNot(HaveOccurred())
   530  					Expect(warnings).To(ConsistOf("warning-1", "warning-2"))
   531  					Expect(resourceList).To(Equal([]resources.Role{{
   532  						GUID: "role-guid-1",
   533  						Type: constant.OrgUserRole,
   534  					}, {
   535  						GUID: "role-guid-2",
   536  						Type: constant.OrgManagerRole,
   537  					}}))
   538  				})
   539  			})
   540  
   541  			When("the response includes other resources", func() {
   542  				BeforeEach(func() {
   543  					response1 := fmt.Sprintf(`{
   544  						"pagination": {
   545  							"next": {
   546  								"href": "%s/v3/roles?organization_guids=some-org-name&page=2&per_page=1&include=users"
   547  							}
   548  						},
   549  						"resources": [
   550  							{
   551  							  "guid": "role-guid-1",
   552  							  "type": "organization_user",
   553  							  "relationships": {
   554  								"user": {
   555  								  "data": {"guid": "user-guid-1"}
   556  								}
   557  							  }
   558  							}
   559  						],
   560  						"included": {
   561  							"users": [
   562  								{
   563  									"guid": "user-guid-1",
   564  									"username": "user-name-1",
   565  									"origin": "uaa"
   566  							  	}
   567  							],
   568  							"spaces": [
   569  								{
   570  									"guid": "space-guid-1",
   571  									"name": "space-name-1"
   572  							  	}
   573  							],
   574  							"organizations": [
   575  								{
   576  									"guid": "org-guid-1",
   577  									"name": "org-name-1"
   578  							  	}
   579  							]
   580  						}
   581  					}`, server.URL())
   582  
   583  					response2 := `{
   584  							"pagination": {
   585  								"next": null
   586  							},
   587  						 "resources": [
   588  						   {
   589  						     "guid": "role-guid-2",
   590  						     "type": "organization_manager",
   591  							  "relationships": {
   592  								"user": {
   593  								  "data": {"guid": "user-guid-2"}
   594  								}
   595  							  }
   596  						   }
   597  						 ],
   598  						"included": {
   599  							"users": [
   600  							  {
   601  								"guid": "user-guid-2",
   602  								"username": "user-name-2",
   603  								"origin": "uaa"
   604  							  }
   605  							],
   606  							"spaces": [
   607  								{
   608  									"guid": "space-guid-2",
   609  									"name": "space-name-2"
   610  							  	}
   611  							],
   612  							"organizations": [
   613  								{
   614  									"guid": "org-guid-2",
   615  									"name": "org-name-2"
   616  							  	}
   617  							]
   618  						  }
   619  						}`
   620  
   621  					server.AppendHandlers(
   622  						CombineHandlers(
   623  							VerifyRequest(http.MethodGet, "/v3/roles", "organization_guids=some-org-name&include=users"),
   624  							RespondWith(http.StatusOK, response1, http.Header{"X-Cf-Warnings": {"warning-1"}}),
   625  						),
   626  					)
   627  					server.AppendHandlers(
   628  						CombineHandlers(
   629  							VerifyRequest(http.MethodGet, "/v3/roles", "organization_guids=some-org-name&page=2&per_page=1&include=users"),
   630  							RespondWith(http.StatusOK, response2, http.Header{"X-Cf-Warnings": {"warning-2"}}),
   631  						),
   632  					)
   633  				})
   634  
   635  				It("returns the queried and additional resources", func() {
   636  					Expect(executeErr).ToNot(HaveOccurred())
   637  					Expect(warnings).To(ConsistOf("warning-1", "warning-2"))
   638  
   639  					Expect(resourceList).To(Equal([]resources.Role{{
   640  						GUID:     "role-guid-1",
   641  						Type:     constant.OrgUserRole,
   642  						UserGUID: "user-guid-1",
   643  					}, {
   644  						GUID:     "role-guid-2",
   645  						Type:     constant.OrgManagerRole,
   646  						UserGUID: "user-guid-2",
   647  					}}))
   648  
   649  					Expect(includedResources).To(Equal(IncludedResources{
   650  						Users: []resources.User{
   651  							{GUID: "user-guid-1", Username: "user-name-1", Origin: "uaa"},
   652  							{GUID: "user-guid-2", Username: "user-name-2", Origin: "uaa"},
   653  						},
   654  						Spaces: []Space{
   655  							{GUID: "space-guid-1", Name: "space-name-1"},
   656  							{GUID: "space-guid-2", Name: "space-name-2"},
   657  						},
   658  						Organizations: []Organization{
   659  							{GUID: "org-guid-1", Name: "org-name-1"},
   660  							{GUID: "org-guid-2", Name: "org-name-2"},
   661  						},
   662  					}))
   663  				})
   664  			})
   665  
   666  			When("the request has a URI parameter", func() {
   667  				var (
   668  					appGUID   string
   669  					resources []Process
   670  				)
   671  
   672  				BeforeEach(func() {
   673  					appGUID = "some-app-guid"
   674  
   675  					response1 := fmt.Sprintf(`{
   676  						"pagination": {
   677  							"next": {
   678  								"href": "%s/v3/apps/%s/processes?page=2"
   679  							}
   680  						},
   681  					  "resources": [
   682  							{
   683  							  "guid": "process-guid-1"
   684  							}
   685  					  	]
   686  					}`, server.URL(), appGUID)
   687  					response2 := `{
   688  							"pagination": {
   689  								"next": null
   690  							},
   691  							 "resources": [
   692  							   {
   693  								 "guid": "process-guid-2"
   694  							   }
   695  							 ]
   696  						}`
   697  
   698  					server.AppendHandlers(
   699  						CombineHandlers(
   700  							VerifyRequest(http.MethodGet, fmt.Sprintf("/v3/apps/%s/processes", appGUID)),
   701  							RespondWith(http.StatusOK, response1, http.Header{"X-Cf-Warnings": {"warning-1"}}),
   702  						),
   703  					)
   704  					server.AppendHandlers(
   705  						CombineHandlers(
   706  							VerifyRequest(http.MethodGet, fmt.Sprintf("/v3/apps/%s/processes", appGUID), "page=2"),
   707  							RespondWith(http.StatusOK, response2, http.Header{"X-Cf-Warnings": {"warning-2"}}),
   708  						),
   709  					)
   710  
   711  					requestParams = RequestParams{
   712  						RequestName:  internal.GetApplicationProcessesRequest,
   713  						URIParams:    internal.Params{"app_guid": appGUID},
   714  						ResponseBody: Process{},
   715  						AppendToList: func(item interface{}) error {
   716  							resources = append(resources, item.(Process))
   717  							return nil
   718  						},
   719  					}
   720  				})
   721  
   722  				It("returns the given resources and all warnings", func() {
   723  					Expect(executeErr).ToNot(HaveOccurred())
   724  					Expect(warnings).To(ConsistOf("warning-1", "warning-2"))
   725  					Expect(resources).To(Equal([]Process{{
   726  						GUID: "process-guid-1",
   727  					}, {
   728  						GUID: "process-guid-2",
   729  					}}))
   730  				})
   731  			})
   732  
   733  			When("the cloud controller returns errors and warnings", func() {
   734  				BeforeEach(func() {
   735  					response := `{
   736    "errors": [
   737      {
   738        "code": 10008,
   739        "detail": "The request is semantically invalid: command presence",
   740        "title": "CF-UnprocessableEntity"
   741      },
   742      {
   743        "code": 10010,
   744        "detail": "Org not found",
   745        "title": "CF-ResourceNotFound"
   746      }
   747    ]
   748  }`
   749  					server.AppendHandlers(
   750  						CombineHandlers(
   751  							VerifyRequest(http.MethodGet, "/v3/roles"),
   752  							RespondWith(http.StatusTeapot, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}),
   753  						),
   754  					)
   755  				})
   756  
   757  				It("returns the error and all warnings", func() {
   758  					Expect(executeErr).To(MatchError(ccerror.MultiError{
   759  						ResponseCode: http.StatusTeapot,
   760  						Errors: []ccerror.V3Error{
   761  							{
   762  								Code:   10008,
   763  								Detail: "The request is semantically invalid: command presence",
   764  								Title:  "CF-UnprocessableEntity",
   765  							},
   766  							{
   767  								Code:   10010,
   768  								Detail: "Org not found",
   769  								Title:  "CF-ResourceNotFound",
   770  							},
   771  						},
   772  					}))
   773  					Expect(warnings).To(ConsistOf("this is a warning"))
   774  				})
   775  			})
   776  		})
   777  	})
   778  
   779  	Describe("MakeRequestReceiveRaw", func() {
   780  		var (
   781  			requestName string
   782  			uriParams   internal.Params
   783  
   784  			rawResponseBody      []byte
   785  			warnings             Warnings
   786  			executeErr           error
   787  			responseBodyMimeType string
   788  		)
   789  
   790  		JustBeforeEach(func() {
   791  			rawResponseBody, warnings, executeErr = client.MakeRequestReceiveRaw(requestName, uriParams, responseBodyMimeType)
   792  		})
   793  
   794  		Context("GET raw bytes (YAML data)", func() {
   795  			var (
   796  				expectedResponseBody []byte
   797  			)
   798  
   799  			BeforeEach(func() {
   800  				requestName = internal.GetApplicationManifestRequest
   801  				responseBodyMimeType = "application/x-yaml"
   802  				uriParams = internal.Params{"app_guid": "some-app-guid"}
   803  			})
   804  
   805  			When("getting requested data is successful", func() {
   806  				BeforeEach(func() {
   807  					expectedResponseBody = []byte("---\n- banana")
   808  
   809  					server.AppendHandlers(
   810  						CombineHandlers(
   811  							CombineHandlers(
   812  								VerifyRequest(http.MethodGet, "/v3/apps/some-app-guid/manifest"),
   813  								VerifyHeaderKV("Accept", "application/x-yaml"),
   814  								RespondWith(
   815  									http.StatusOK,
   816  									expectedResponseBody,
   817  									http.Header{
   818  										"Content-Type":  {"application/x-yaml"},
   819  										"X-Cf-Warnings": {"this is a warning"},
   820  									}),
   821  							),
   822  						),
   823  					)
   824  				})
   825  
   826  				It("returns the raw response body and all warnings", func() {
   827  					Expect(executeErr).NotTo(HaveOccurred())
   828  					Expect(rawResponseBody).To(Equal(expectedResponseBody))
   829  					Expect(warnings).To(ConsistOf("this is a warning"))
   830  				})
   831  			})
   832  
   833  			When("the cloud controller returns errors and warnings", func() {
   834  				BeforeEach(func() {
   835  					response := `{
   836  				  "errors": [
   837  					{
   838  					  "code": 10008,
   839  					  "detail": "The request is semantically invalid: command presence",
   840  					  "title": "CF-UnprocessableEntity"
   841  					},
   842  					{
   843  					  "code": 10010,
   844  					  "detail": "Org not found",
   845  					  "title": "CF-ResourceNotFound"
   846  					}
   847  				  ]
   848  				}`
   849  
   850  					server.AppendHandlers(
   851  						CombineHandlers(
   852  							VerifyRequest(http.MethodGet, "/v3/apps/some-app-guid/manifest"),
   853  							VerifyHeaderKV("Accept", "application/x-yaml"),
   854  							RespondWith(http.StatusTeapot, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}),
   855  						),
   856  					)
   857  				})
   858  
   859  				It("returns the error and all warnings", func() {
   860  					Expect(executeErr).To(MatchError(ccerror.MultiError{
   861  						ResponseCode: http.StatusTeapot,
   862  						Errors: []ccerror.V3Error{
   863  							{
   864  								Code:   10008,
   865  								Detail: "The request is semantically invalid: command presence",
   866  								Title:  "CF-UnprocessableEntity",
   867  							},
   868  							{
   869  								Code:   10010,
   870  								Detail: "Org not found",
   871  								Title:  "CF-ResourceNotFound",
   872  							},
   873  						},
   874  					}))
   875  					Expect(warnings).To(ConsistOf("this is a warning"))
   876  				})
   877  			})
   878  		})
   879  	})
   880  
   881  	Describe("MakeRequestSendRaw", func() {
   882  		var (
   883  			requestName         string
   884  			uriParams           internal.Params
   885  			requestBodyMimeType string
   886  
   887  			requestBody      []byte
   888  			responseBody     Package
   889  			expectedJobURL   string
   890  			responseLocation string
   891  			warnings         Warnings
   892  			executeErr       error
   893  		)
   894  
   895  		JustBeforeEach(func() {
   896  			responseLocation, warnings, executeErr = client.MakeRequestSendRaw(requestName, uriParams, requestBody, requestBodyMimeType, &responseBody)
   897  		})
   898  
   899  		BeforeEach(func() {
   900  			requestBody = []byte("fake-package-file")
   901  			expectedJobURL = "apply-manifest-job-url"
   902  			responseBody = Package{}
   903  
   904  			requestName = internal.PostPackageBitsRequest
   905  			uriParams = internal.Params{"package_guid": "package-guid"}
   906  			requestBodyMimeType = "multipart/form-data"
   907  		})
   908  
   909  		When("the resource is successfully created", func() {
   910  			BeforeEach(func() {
   911  				response := `{
   912  					"guid": "some-pkg-guid",
   913  					"type": "docker",
   914  					"state": "PROCESSING_UPLOAD",
   915  					"links": {
   916  						"upload": {
   917  							"href": "some-package-upload-url",
   918  							"method": "POST"
   919  						}
   920  					}
   921  				}`
   922  
   923  				server.AppendHandlers(
   924  					CombineHandlers(
   925  						VerifyRequest(http.MethodPost, "/v3/packages/package-guid/upload"),
   926  						VerifyBody(requestBody),
   927  						VerifyHeaderKV("Content-Type", "multipart/form-data"),
   928  						RespondWith(http.StatusCreated, response, http.Header{
   929  							"X-Cf-Warnings": {"this is a warning"},
   930  							"Location":      {expectedJobURL},
   931  						}),
   932  					),
   933  				)
   934  			})
   935  
   936  			It("returns the resource and warnings", func() {
   937  				Expect(responseLocation).To(Equal(expectedJobURL))
   938  				Expect(executeErr).NotTo(HaveOccurred())
   939  				Expect(warnings).To(ConsistOf("this is a warning"))
   940  
   941  				expectedPackage := Package{
   942  					GUID:  "some-pkg-guid",
   943  					Type:  constant.PackageTypeDocker,
   944  					State: constant.PackageProcessingUpload,
   945  					Links: map[string]APILink{
   946  						"upload": APILink{HREF: "some-package-upload-url", Method: http.MethodPost},
   947  					},
   948  				}
   949  				Expect(responseBody).To(Equal(expectedPackage))
   950  			})
   951  		})
   952  
   953  		When("the resource returns all errors and warnings", func() {
   954  			BeforeEach(func() {
   955  				response := ` {
   956    "errors": [
   957      {
   958        "code": 10008,
   959        "detail": "The request is semantically invalid: command presence",
   960        "title": "CF-UnprocessableEntity"
   961      },
   962      {
   963        "code": 10010,
   964        "detail": "Hamster not found",
   965        "title": "CF-ResourceNotFound"
   966      }
   967    ]
   968  }`
   969  				server.AppendHandlers(
   970  					CombineHandlers(
   971  						VerifyRequest(http.MethodPost, "/v3/packages/package-guid/upload"),
   972  						RespondWith(http.StatusTeapot, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}),
   973  					),
   974  				)
   975  			})
   976  
   977  			It("returns the error and all warnings", func() {
   978  				Expect(executeErr).To(MatchError(ccerror.MultiError{
   979  					ResponseCode: http.StatusTeapot,
   980  					Errors: []ccerror.V3Error{
   981  						{
   982  							Code:   10008,
   983  							Detail: "The request is semantically invalid: command presence",
   984  							Title:  "CF-UnprocessableEntity",
   985  						},
   986  						{
   987  							Code:   10010,
   988  							Detail: "Hamster not found",
   989  							Title:  "CF-ResourceNotFound",
   990  						},
   991  					},
   992  				}))
   993  				Expect(warnings).To(ConsistOf("this is a warning"))
   994  			})
   995  		})
   996  	})
   997  
   998  	Describe("MakeRequestUploadAsync", func() {
   999  		var (
  1000  			requestName         string
  1001  			uriParams           internal.Params
  1002  			requestBodyMimeType string
  1003  			requestBody         io.ReadSeeker
  1004  			dataLength          int64
  1005  			writeErrors         chan error
  1006  
  1007  			responseLocation string
  1008  			responseBody     Package
  1009  			warning          string
  1010  			warnings         Warnings
  1011  			executeErr       error
  1012  		)
  1013  		BeforeEach(func() {
  1014  			warning = "upload-async-warning"
  1015  			content := "I love my cats!"
  1016  			requestBody = strings.NewReader(content)
  1017  			dataLength = int64(len(content))
  1018  			writeErrors = make(chan error)
  1019  
  1020  			response := `{
  1021  						"guid": "some-package-guid",
  1022  						"type": "bits",
  1023  						"state": "PROCESSING_UPLOAD"
  1024  					}`
  1025  
  1026  			server.AppendHandlers(
  1027  				CombineHandlers(
  1028  					VerifyRequest(http.MethodPost, "/v3/packages/package-guid/upload"),
  1029  					VerifyHeaderKV("Content-Type", "multipart/form-data"),
  1030  					VerifyBody([]byte(content)),
  1031  					RespondWith(http.StatusOK, response, http.Header{
  1032  						"X-Cf-Warnings": {warning},
  1033  						"Location":      {"something"},
  1034  					}),
  1035  				),
  1036  			)
  1037  		})
  1038  		JustBeforeEach(func() {
  1039  			responseBody = Package{}
  1040  			requestName = internal.PostPackageBitsRequest
  1041  			requestBodyMimeType = "multipart/form-data"
  1042  			uriParams = internal.Params{"package_guid": "package-guid"}
  1043  
  1044  			responseLocation, warnings, executeErr = client.MakeRequestUploadAsync(
  1045  				requestName,
  1046  				uriParams,
  1047  				requestBodyMimeType,
  1048  				requestBody,
  1049  				dataLength,
  1050  				&responseBody,
  1051  				writeErrors,
  1052  			)
  1053  		})
  1054  		When("there are no errors (happy path)", func() {
  1055  			BeforeEach(func() {
  1056  				go func() {
  1057  					close(writeErrors)
  1058  				}()
  1059  			})
  1060  			It("returns the location and any warnings and error", func() {
  1061  				Expect(executeErr).ToNot(HaveOccurred())
  1062  				Expect(responseLocation).To(Equal("something"))
  1063  				Expect(responseBody).To(Equal(Package{
  1064  					GUID:  "some-package-guid",
  1065  					State: "PROCESSING_UPLOAD",
  1066  					Type:  "bits",
  1067  				}))
  1068  				Expect(warnings).To(Equal(Warnings{warning}))
  1069  			})
  1070  		})
  1071  
  1072  		When("There are write errors", func() {
  1073  			BeforeEach(func() {
  1074  				go func() {
  1075  					writeErrors <- errors.New("first-error")
  1076  					writeErrors <- errors.New("second-error")
  1077  					close(writeErrors)
  1078  				}()
  1079  			})
  1080  			It("returns the first error", func() {
  1081  				Expect(executeErr).To(MatchError("first-error"))
  1082  			})
  1083  		})
  1084  
  1085  		When("there are HTTP connection errors", func() {
  1086  			BeforeEach(func() {
  1087  				server.Close()
  1088  				close(writeErrors)
  1089  			})
  1090  
  1091  			It("returns the first error", func() {
  1092  				_, ok := executeErr.(ccerror.RequestError)
  1093  				Expect(ok).To(BeTrue())
  1094  			})
  1095  		})
  1096  	})
  1097  })