github.com/DaAlbrecht/cf-cli@v0.0.0-20231128151943-1fe19bb400b9/api/cloudcontroller/ccv3/requester_test.go (about)

     1  package ccv3_test
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"net/http"
     9  	"strings"
    10  
    11  	"code.cloudfoundry.org/cli/api/cloudcontroller/ccerror"
    12  	. "code.cloudfoundry.org/cli/api/cloudcontroller/ccv3"
    13  	"code.cloudfoundry.org/cli/api/cloudcontroller/ccv3/constant"
    14  	"code.cloudfoundry.org/cli/api/cloudcontroller/ccv3/internal"
    15  	"code.cloudfoundry.org/cli/resources"
    16  	. "code.cloudfoundry.org/cli/resources"
    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("shared request helpers", func() {
    24  	var client *Client
    25  
    26  	BeforeEach(func() {
    27  		client, _ = NewTestClient()
    28  	})
    29  
    30  	Describe("MakeRequest", func() {
    31  		var (
    32  			requestParams RequestParams
    33  
    34  			jobURL     JobURL
    35  			warnings   Warnings
    36  			executeErr error
    37  		)
    38  
    39  		BeforeEach(func() {
    40  			requestParams = RequestParams{}
    41  		})
    42  
    43  		JustBeforeEach(func() {
    44  			jobURL, warnings, executeErr = client.MakeRequest(requestParams)
    45  		})
    46  
    47  		Context("GET single resource", func() {
    48  			var (
    49  				responseBody Organization
    50  			)
    51  
    52  			BeforeEach(func() {
    53  				requestParams = RequestParams{
    54  					RequestName:  internal.GetOrganizationRequest,
    55  					URIParams:    internal.Params{"organization_guid": "some-org-guid"},
    56  					ResponseBody: &responseBody,
    57  				}
    58  			})
    59  
    60  			When("organization exists", func() {
    61  				BeforeEach(func() {
    62  					response := `{
    63  					"name": "some-org-name",
    64  					"guid": "some-org-guid",
    65  					"relationships": {
    66  						"quota": {
    67  							"data": {
    68  								"guid": "some-org-quota-guid"
    69  							}
    70  						}
    71  					}
    72  				}`
    73  
    74  					server.AppendHandlers(
    75  						CombineHandlers(
    76  							VerifyRequest(http.MethodGet, "/v3/organizations/some-org-guid"),
    77  							RespondWith(http.StatusOK, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}),
    78  						),
    79  					)
    80  				})
    81  
    82  				It("returns the queried organization and all warnings", func() {
    83  					Expect(executeErr).NotTo(HaveOccurred())
    84  					Expect(responseBody).To(Equal(Organization{
    85  						Name:      "some-org-name",
    86  						GUID:      "some-org-guid",
    87  						QuotaGUID: "some-org-quota-guid",
    88  					}))
    89  					Expect(warnings).To(ConsistOf("this is a warning"))
    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("MakeRequestSendReceiveRaw", func() {
   446  		var (
   447  			method        string
   448  			url           string
   449  			headers       http.Header
   450  			requestBody   []byte
   451  			responseBytes []byte
   452  			httpResponse  *http.Response
   453  			executeErr    error
   454  		)
   455  		JustBeforeEach(func() {
   456  			responseBytes, httpResponse, executeErr = client.MakeRequestSendReceiveRaw(method, url, headers, requestBody)
   457  		})
   458  
   459  		Context("PATCH request with body", func() {
   460  			BeforeEach(func() {
   461  				method = "PATCH"
   462  				url = fmt.Sprintf("%s/v3/apps/%s", server.URL(), "some-app-guid")
   463  				headers = http.Header{}
   464  				headers.Set("Banana", "Plantain")
   465  
   466  				var err error
   467  				requestBody, err = json.Marshal(Application{
   468  					GUID:                "some-app-guid",
   469  					Name:                "some-app-name",
   470  					StackName:           "some-stack-name",
   471  					LifecycleType:       constant.AppLifecycleTypeBuildpack,
   472  					LifecycleBuildpacks: []string{"some-buildpack"},
   473  					SpaceGUID:           "some-space-guid",
   474  				})
   475  
   476  				Expect(err).NotTo(HaveOccurred())
   477  
   478  				response := `{
   479  					"guid": "some-app-guid",
   480  					"name": "some-app-name",
   481  					"lifecycle": {
   482  						"type": "buildpack",
   483  						"data": {
   484  							"buildpacks": ["some-buildpack"],
   485  							"stack": "some-stack-name"
   486  						}
   487  					}
   488  				}`
   489  
   490  				expectedBody := map[string]interface{}{
   491  					"name": "some-app-name",
   492  					"lifecycle": map[string]interface{}{
   493  						"type": "buildpack",
   494  						"data": map[string]interface{}{
   495  							"buildpacks": []string{"some-buildpack"},
   496  							"stack":      "some-stack-name",
   497  						},
   498  					},
   499  					"relationships": map[string]interface{}{
   500  						"space": map[string]interface{}{
   501  							"data": map[string]string{
   502  								"guid": "some-space-guid",
   503  							},
   504  						},
   505  					},
   506  				}
   507  				server.AppendHandlers(
   508  					CombineHandlers(
   509  						VerifyRequest(http.MethodPatch, "/v3/apps/some-app-guid"),
   510  						VerifyHeader(http.Header{"Banana": {"Plantain"}}),
   511  						VerifyJSONRepresenting(expectedBody),
   512  						RespondWith(http.StatusOK, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}),
   513  					),
   514  				)
   515  			})
   516  
   517  			It("successfully makes the request", func() {
   518  				Expect(executeErr).NotTo(HaveOccurred())
   519  				actualResponse := `{
   520  					"guid": "some-app-guid",
   521  					"name": "some-app-name",
   522  					"lifecycle": {
   523  						"type": "buildpack",
   524  						"data": {
   525  							"buildpacks": ["some-buildpack"],
   526  							"stack": "some-stack-name"
   527  						}
   528  					}
   529  				}`
   530  				Expect(string(responseBytes)).To(Equal(actualResponse))
   531  				Expect(httpResponse.Header["X-Cf-Warnings"][0]).To(Equal("this is a warning"))
   532  			})
   533  		})
   534  	})
   535  
   536  	Describe("MakeListRequest", func() {
   537  		var (
   538  			requestParams RequestParams
   539  
   540  			includedResources IncludedResources
   541  			warnings          Warnings
   542  			executeErr        error
   543  		)
   544  
   545  		JustBeforeEach(func() {
   546  			includedResources, warnings, executeErr = client.MakeListRequest(requestParams)
   547  		})
   548  
   549  		Context("with query params and included resources", func() {
   550  			var (
   551  				resourceList []resources.Role
   552  				query        []Query
   553  			)
   554  
   555  			BeforeEach(func() {
   556  				resourceList = []resources.Role{}
   557  				query = []Query{
   558  					{
   559  						Key:    OrganizationGUIDFilter,
   560  						Values: []string{"some-org-name"},
   561  					},
   562  					{
   563  						Key:    Include,
   564  						Values: []string{"users"},
   565  					},
   566  				}
   567  				requestParams = RequestParams{
   568  					RequestName:  internal.GetRolesRequest,
   569  					Query:        query,
   570  					ResponseBody: resources.Role{},
   571  					AppendToList: func(item interface{}) error {
   572  						resourceList = append(resourceList, item.(resources.Role))
   573  						return nil
   574  					},
   575  				}
   576  			})
   577  
   578  			When("the request succeeds", func() {
   579  				BeforeEach(func() {
   580  					response1 := fmt.Sprintf(`{
   581  	"pagination": {
   582  		"next": {
   583  			"href": "%s/v3/roles?organization_guids=some-org-name&page=2&per_page=1&include=users"
   584  		}
   585  	},
   586    "resources": [
   587      {
   588        "guid": "role-guid-1",
   589        "type": "organization_user"
   590      }
   591    ]
   592  }`, server.URL())
   593  					response2 := `{
   594  							"pagination": {
   595  								"next": null
   596  							},
   597  						 "resources": [
   598  						   {
   599  						     "guid": "role-guid-2",
   600  						     "type": "organization_manager"
   601  						   }
   602  						 ]
   603  						}`
   604  
   605  					server.AppendHandlers(
   606  						CombineHandlers(
   607  							VerifyRequest(http.MethodGet, "/v3/roles", "organization_guids=some-org-name&include=users"),
   608  							RespondWith(http.StatusOK, response1, http.Header{"X-Cf-Warnings": {"warning-1"}}),
   609  						),
   610  					)
   611  					server.AppendHandlers(
   612  						CombineHandlers(
   613  							VerifyRequest(http.MethodGet, "/v3/roles", "organization_guids=some-org-name&page=2&per_page=1&include=users"),
   614  							RespondWith(http.StatusOK, response2, http.Header{"X-Cf-Warnings": {"warning-2"}}),
   615  						),
   616  					)
   617  				})
   618  
   619  				It("returns the given resources and all warnings", func() {
   620  					Expect(executeErr).ToNot(HaveOccurred())
   621  					Expect(warnings).To(ConsistOf("warning-1", "warning-2"))
   622  					Expect(resourceList).To(Equal([]resources.Role{{
   623  						GUID: "role-guid-1",
   624  						Type: constant.OrgUserRole,
   625  					}, {
   626  						GUID: "role-guid-2",
   627  						Type: constant.OrgManagerRole,
   628  					}}))
   629  				})
   630  			})
   631  
   632  			When("the response includes other resources", func() {
   633  				BeforeEach(func() {
   634  					response1 := fmt.Sprintf(`{
   635  						"pagination": {
   636  							"next": {
   637  								"href": "%s/v3/roles?organization_guids=some-org-name&page=2&per_page=1&include=users"
   638  							}
   639  						},
   640  						"resources": [
   641  							{
   642  							  "guid": "role-guid-1",
   643  							  "type": "organization_user",
   644  							  "relationships": {
   645  								"user": {
   646  								  "data": {"guid": "user-guid-1"}
   647  								}
   648  							  }
   649  							}
   650  						],
   651  						"included": {
   652  							"apps": [
   653  								{
   654  									"guid": "app-guid-1",
   655  									"name": "app-name-1"
   656  								}
   657  							],
   658  							"users": [
   659  								{
   660  									"guid": "user-guid-1",
   661  									"username": "user-name-1",
   662  									"origin": "uaa"
   663  							  	}
   664  							],
   665  							"spaces": [
   666  								{
   667  									"guid": "space-guid-1",
   668  									"name": "space-name-1"
   669  							  	}
   670  							],
   671  							"organizations": [
   672  								{
   673  									"guid": "org-guid-1",
   674  									"name": "org-name-1"
   675  							  	}
   676  							],
   677  							"service_brokers": [
   678  								{
   679  									"guid": "broker-guid-1",
   680  									"name": "broker-name-1"
   681  								}
   682  							],
   683  							"service_instances": [
   684  								{
   685  									"guid": "service-instance-guid-1",
   686  									"name": "service-instance-name-1"
   687  								}
   688  							],
   689  							"service_offerings": [
   690  								{
   691  									"guid": "offering-guid-1",
   692  									"name": "offering-name-1"
   693  								}
   694  							],
   695  							"service_plans": [
   696  								{
   697  									"guid": "plan-guid-1",
   698  									"name": "plan-name-1"
   699  								}
   700  							]
   701  						}
   702  					}`, server.URL())
   703  
   704  					response2 := `{
   705  							"pagination": {
   706  								"next": null
   707  							},
   708  						 "resources": [
   709  						   {
   710  						     "guid": "role-guid-2",
   711  						     "type": "organization_manager",
   712  							  "relationships": {
   713  								"user": {
   714  								  "data": {"guid": "user-guid-2"}
   715  								}
   716  							  }
   717  						   }
   718  						 ],
   719  						"included": {
   720  							"users": [
   721  							  {
   722  								"guid": "user-guid-2",
   723  								"username": "user-name-2",
   724  								"origin": "uaa"
   725  							  }
   726  							],
   727  							"spaces": [
   728  								{
   729  									"guid": "space-guid-2",
   730  									"name": "space-name-2"
   731  							  	}
   732  							],
   733  							"organizations": [
   734  								{
   735  									"guid": "org-guid-2",
   736  									"name": "org-name-2"
   737  							  	}
   738  							],
   739  							"service_brokers": [
   740  								{
   741  									"guid": "broker-guid-2",
   742  									"name": "broker-name-2"
   743  								}
   744  							],
   745  							"service_instances": [
   746  								{
   747  									"guid": "service-instance-guid-2",
   748  									"name": "service-instance-name-2"
   749  								}
   750  							],
   751  							"service_offerings": [
   752  								{
   753  									"guid": "offering-guid-2",
   754  									"name": "offering-name-2"
   755  								}
   756  							],
   757  							"service_plans": [
   758  								{
   759  									"guid": "plan-guid-2",
   760  									"name": "plan-name-2"
   761  								}
   762  							]
   763  						  }
   764  						}`
   765  
   766  					server.AppendHandlers(
   767  						CombineHandlers(
   768  							VerifyRequest(http.MethodGet, "/v3/roles", "organization_guids=some-org-name&include=users"),
   769  							RespondWith(http.StatusOK, response1, http.Header{"X-Cf-Warnings": {"warning-1"}}),
   770  						),
   771  					)
   772  					server.AppendHandlers(
   773  						CombineHandlers(
   774  							VerifyRequest(http.MethodGet, "/v3/roles", "organization_guids=some-org-name&page=2&per_page=1&include=users"),
   775  							RespondWith(http.StatusOK, response2, http.Header{"X-Cf-Warnings": {"warning-2"}}),
   776  						),
   777  					)
   778  				})
   779  
   780  				It("returns the queried and additional resources", func() {
   781  					Expect(executeErr).ToNot(HaveOccurred())
   782  					Expect(warnings).To(ConsistOf("warning-1", "warning-2"))
   783  
   784  					Expect(resourceList).To(Equal([]resources.Role{{
   785  						GUID:     "role-guid-1",
   786  						Type:     constant.OrgUserRole,
   787  						UserGUID: "user-guid-1",
   788  					}, {
   789  						GUID:     "role-guid-2",
   790  						Type:     constant.OrgManagerRole,
   791  						UserGUID: "user-guid-2",
   792  					}}))
   793  
   794  					Expect(includedResources).To(Equal(IncludedResources{
   795  						Apps: []resources.Application{
   796  							{Name: "app-name-1", GUID: "app-guid-1"},
   797  						},
   798  						Users: []resources.User{
   799  							{GUID: "user-guid-1", Username: "user-name-1", Origin: "uaa"},
   800  							{GUID: "user-guid-2", Username: "user-name-2", Origin: "uaa"},
   801  						},
   802  						Spaces: []Space{
   803  							{GUID: "space-guid-1", Name: "space-name-1"},
   804  							{GUID: "space-guid-2", Name: "space-name-2"},
   805  						},
   806  						Organizations: []Organization{
   807  							{GUID: "org-guid-1", Name: "org-name-1"},
   808  							{GUID: "org-guid-2", Name: "org-name-2"},
   809  						},
   810  						ServiceBrokers: []ServiceBroker{
   811  							{Name: "broker-name-1", GUID: "broker-guid-1"},
   812  							{Name: "broker-name-2", GUID: "broker-guid-2"},
   813  						},
   814  						ServiceInstances: []resources.ServiceInstance{
   815  							{Name: "service-instance-name-1", GUID: "service-instance-guid-1"},
   816  							{Name: "service-instance-name-2", GUID: "service-instance-guid-2"},
   817  						},
   818  						ServiceOfferings: []ServiceOffering{
   819  							{Name: "offering-name-1", GUID: "offering-guid-1"},
   820  							{Name: "offering-name-2", GUID: "offering-guid-2"},
   821  						},
   822  						ServicePlans: []ServicePlan{
   823  							{Name: "plan-name-1", GUID: "plan-guid-1"},
   824  							{Name: "plan-name-2", GUID: "plan-guid-2"},
   825  						},
   826  					}))
   827  				})
   828  			})
   829  
   830  			When("the request has a URI parameter", func() {
   831  				var (
   832  					appGUID   string
   833  					resources []Process
   834  				)
   835  
   836  				BeforeEach(func() {
   837  					appGUID = "some-app-guid"
   838  
   839  					response1 := fmt.Sprintf(`{
   840  						"pagination": {
   841  							"next": {
   842  								"href": "%s/v3/apps/%s/processes?page=2"
   843  							}
   844  						},
   845  					  "resources": [
   846  							{
   847  							  "guid": "process-guid-1"
   848  							}
   849  					  	]
   850  					}`, server.URL(), appGUID)
   851  					response2 := `{
   852  							"pagination": {
   853  								"next": null
   854  							},
   855  							 "resources": [
   856  							   {
   857  								 "guid": "process-guid-2"
   858  							   }
   859  							 ]
   860  						}`
   861  
   862  					server.AppendHandlers(
   863  						CombineHandlers(
   864  							VerifyRequest(http.MethodGet, fmt.Sprintf("/v3/apps/%s/processes", appGUID)),
   865  							RespondWith(http.StatusOK, response1, http.Header{"X-Cf-Warnings": {"warning-1"}}),
   866  						),
   867  					)
   868  					server.AppendHandlers(
   869  						CombineHandlers(
   870  							VerifyRequest(http.MethodGet, fmt.Sprintf("/v3/apps/%s/processes", appGUID), "page=2"),
   871  							RespondWith(http.StatusOK, response2, http.Header{"X-Cf-Warnings": {"warning-2"}}),
   872  						),
   873  					)
   874  
   875  					requestParams = RequestParams{
   876  						RequestName:  internal.GetApplicationProcessesRequest,
   877  						URIParams:    internal.Params{"app_guid": appGUID},
   878  						ResponseBody: Process{},
   879  						AppendToList: func(item interface{}) error {
   880  							resources = append(resources, item.(Process))
   881  							return nil
   882  						},
   883  					}
   884  				})
   885  
   886  				It("returns the given resources and all warnings", func() {
   887  					Expect(executeErr).ToNot(HaveOccurred())
   888  					Expect(warnings).To(ConsistOf("warning-1", "warning-2"))
   889  					Expect(resources).To(Equal([]Process{{
   890  						GUID: "process-guid-1",
   891  					}, {
   892  						GUID: "process-guid-2",
   893  					}}))
   894  				})
   895  			})
   896  
   897  			When("the cloud controller returns errors and warnings", func() {
   898  				BeforeEach(func() {
   899  					response := `{
   900    "errors": [
   901      {
   902        "code": 10008,
   903        "detail": "The request is semantically invalid: command presence",
   904        "title": "CF-UnprocessableEntity"
   905      },
   906      {
   907        "code": 10010,
   908        "detail": "Org not found",
   909        "title": "CF-ResourceNotFound"
   910      }
   911    ]
   912  }`
   913  					server.AppendHandlers(
   914  						CombineHandlers(
   915  							VerifyRequest(http.MethodGet, "/v3/roles"),
   916  							RespondWith(http.StatusTeapot, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}),
   917  						),
   918  					)
   919  				})
   920  
   921  				It("returns the error and all warnings", func() {
   922  					Expect(executeErr).To(MatchError(ccerror.MultiError{
   923  						ResponseCode: http.StatusTeapot,
   924  						Errors: []ccerror.V3Error{
   925  							{
   926  								Code:   10008,
   927  								Detail: "The request is semantically invalid: command presence",
   928  								Title:  "CF-UnprocessableEntity",
   929  							},
   930  							{
   931  								Code:   10010,
   932  								Detail: "Org not found",
   933  								Title:  "CF-ResourceNotFound",
   934  							},
   935  						},
   936  					}))
   937  					Expect(warnings).To(ConsistOf("this is a warning"))
   938  				})
   939  			})
   940  		})
   941  	})
   942  
   943  	Describe("MakeRequestReceiveRaw", func() {
   944  		var (
   945  			requestName string
   946  			uriParams   internal.Params
   947  
   948  			rawResponseBody      []byte
   949  			warnings             Warnings
   950  			executeErr           error
   951  			responseBodyMimeType string
   952  		)
   953  
   954  		JustBeforeEach(func() {
   955  			rawResponseBody, warnings, executeErr = client.MakeRequestReceiveRaw(requestName, uriParams, responseBodyMimeType)
   956  		})
   957  
   958  		Context("GET raw bytes (YAML data)", func() {
   959  			var (
   960  				expectedResponseBody []byte
   961  			)
   962  
   963  			BeforeEach(func() {
   964  				requestName = internal.GetApplicationManifestRequest
   965  				responseBodyMimeType = "application/x-yaml"
   966  				uriParams = internal.Params{"app_guid": "some-app-guid"}
   967  			})
   968  
   969  			When("getting requested data is successful", func() {
   970  				BeforeEach(func() {
   971  					expectedResponseBody = []byte("---\n- banana")
   972  
   973  					server.AppendHandlers(
   974  						CombineHandlers(
   975  							CombineHandlers(
   976  								VerifyRequest(http.MethodGet, "/v3/apps/some-app-guid/manifest"),
   977  								VerifyHeaderKV("Accept", "application/x-yaml"),
   978  								RespondWith(
   979  									http.StatusOK,
   980  									expectedResponseBody,
   981  									http.Header{
   982  										"Content-Type":  {"application/x-yaml"},
   983  										"X-Cf-Warnings": {"this is a warning"},
   984  									}),
   985  							),
   986  						),
   987  					)
   988  				})
   989  
   990  				It("returns the raw response body and all warnings", func() {
   991  					Expect(executeErr).NotTo(HaveOccurred())
   992  					Expect(rawResponseBody).To(Equal(expectedResponseBody))
   993  					Expect(warnings).To(ConsistOf("this is a warning"))
   994  				})
   995  			})
   996  
   997  			When("the cloud controller returns errors and warnings", func() {
   998  				BeforeEach(func() {
   999  					response := `{
  1000  				  "errors": [
  1001  					{
  1002  					  "code": 10008,
  1003  					  "detail": "The request is semantically invalid: command presence",
  1004  					  "title": "CF-UnprocessableEntity"
  1005  					},
  1006  					{
  1007  					  "code": 10010,
  1008  					  "detail": "Org not found",
  1009  					  "title": "CF-ResourceNotFound"
  1010  					}
  1011  				  ]
  1012  				}`
  1013  
  1014  					server.AppendHandlers(
  1015  						CombineHandlers(
  1016  							VerifyRequest(http.MethodGet, "/v3/apps/some-app-guid/manifest"),
  1017  							VerifyHeaderKV("Accept", "application/x-yaml"),
  1018  							RespondWith(http.StatusTeapot, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}),
  1019  						),
  1020  					)
  1021  				})
  1022  
  1023  				It("returns the error and all warnings", func() {
  1024  					Expect(executeErr).To(MatchError(ccerror.MultiError{
  1025  						ResponseCode: http.StatusTeapot,
  1026  						Errors: []ccerror.V3Error{
  1027  							{
  1028  								Code:   10008,
  1029  								Detail: "The request is semantically invalid: command presence",
  1030  								Title:  "CF-UnprocessableEntity",
  1031  							},
  1032  							{
  1033  								Code:   10010,
  1034  								Detail: "Org not found",
  1035  								Title:  "CF-ResourceNotFound",
  1036  							},
  1037  						},
  1038  					}))
  1039  					Expect(warnings).To(ConsistOf("this is a warning"))
  1040  				})
  1041  			})
  1042  		})
  1043  	})
  1044  
  1045  	Describe("MakeRequestSendRaw", func() {
  1046  		var (
  1047  			requestName         string
  1048  			uriParams           internal.Params
  1049  			requestBodyMimeType string
  1050  
  1051  			requestBody      []byte
  1052  			responseBody     Package
  1053  			expectedJobURL   string
  1054  			responseLocation string
  1055  			warnings         Warnings
  1056  			executeErr       error
  1057  		)
  1058  
  1059  		JustBeforeEach(func() {
  1060  			responseLocation, warnings, executeErr = client.MakeRequestSendRaw(requestName, uriParams, requestBody, requestBodyMimeType, &responseBody)
  1061  		})
  1062  
  1063  		BeforeEach(func() {
  1064  			requestBody = []byte("fake-package-file")
  1065  			expectedJobURL = "apply-manifest-job-url"
  1066  			responseBody = Package{}
  1067  
  1068  			requestName = internal.PostPackageBitsRequest
  1069  			uriParams = internal.Params{"package_guid": "package-guid"}
  1070  			requestBodyMimeType = "multipart/form-data"
  1071  		})
  1072  
  1073  		When("the resource is successfully created", func() {
  1074  			BeforeEach(func() {
  1075  				response := `{
  1076  					"guid": "some-pkg-guid",
  1077  					"type": "docker",
  1078  					"state": "PROCESSING_UPLOAD",
  1079  					"links": {
  1080  						"upload": {
  1081  							"href": "some-package-upload-url",
  1082  							"method": "POST"
  1083  						}
  1084  					}
  1085  				}`
  1086  
  1087  				server.AppendHandlers(
  1088  					CombineHandlers(
  1089  						VerifyRequest(http.MethodPost, "/v3/packages/package-guid/upload"),
  1090  						VerifyBody(requestBody),
  1091  						VerifyHeaderKV("Content-Type", "multipart/form-data"),
  1092  						RespondWith(http.StatusCreated, response, http.Header{
  1093  							"X-Cf-Warnings": {"this is a warning"},
  1094  							"Location":      {expectedJobURL},
  1095  						}),
  1096  					),
  1097  				)
  1098  			})
  1099  
  1100  			It("returns the resource and warnings", func() {
  1101  				Expect(responseLocation).To(Equal(expectedJobURL))
  1102  				Expect(executeErr).NotTo(HaveOccurred())
  1103  				Expect(warnings).To(ConsistOf("this is a warning"))
  1104  
  1105  				expectedPackage := Package{
  1106  					GUID:  "some-pkg-guid",
  1107  					Type:  constant.PackageTypeDocker,
  1108  					State: constant.PackageProcessingUpload,
  1109  					Links: map[string]APILink{
  1110  						"upload": APILink{HREF: "some-package-upload-url", Method: http.MethodPost},
  1111  					},
  1112  				}
  1113  				Expect(responseBody).To(Equal(expectedPackage))
  1114  			})
  1115  		})
  1116  
  1117  		When("the resource returns all errors and warnings", func() {
  1118  			BeforeEach(func() {
  1119  				response := ` {
  1120    "errors": [
  1121      {
  1122        "code": 10008,
  1123        "detail": "The request is semantically invalid: command presence",
  1124        "title": "CF-UnprocessableEntity"
  1125      },
  1126      {
  1127        "code": 10010,
  1128        "detail": "Hamster not found",
  1129        "title": "CF-ResourceNotFound"
  1130      }
  1131    ]
  1132  }`
  1133  				server.AppendHandlers(
  1134  					CombineHandlers(
  1135  						VerifyRequest(http.MethodPost, "/v3/packages/package-guid/upload"),
  1136  						RespondWith(http.StatusTeapot, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}),
  1137  					),
  1138  				)
  1139  			})
  1140  
  1141  			It("returns the error and all warnings", func() {
  1142  				Expect(executeErr).To(MatchError(ccerror.MultiError{
  1143  					ResponseCode: http.StatusTeapot,
  1144  					Errors: []ccerror.V3Error{
  1145  						{
  1146  							Code:   10008,
  1147  							Detail: "The request is semantically invalid: command presence",
  1148  							Title:  "CF-UnprocessableEntity",
  1149  						},
  1150  						{
  1151  							Code:   10010,
  1152  							Detail: "Hamster not found",
  1153  							Title:  "CF-ResourceNotFound",
  1154  						},
  1155  					},
  1156  				}))
  1157  				Expect(warnings).To(ConsistOf("this is a warning"))
  1158  			})
  1159  		})
  1160  	})
  1161  
  1162  	Describe("MakeRequestUploadAsync", func() {
  1163  		var (
  1164  			requestName         string
  1165  			uriParams           internal.Params
  1166  			requestBodyMimeType string
  1167  			requestBody         io.ReadSeeker
  1168  			dataLength          int64
  1169  			writeErrors         chan error
  1170  
  1171  			responseLocation string
  1172  			responseBody     Package
  1173  			warning          string
  1174  			warnings         Warnings
  1175  			executeErr       error
  1176  		)
  1177  		BeforeEach(func() {
  1178  			warning = "upload-async-warning"
  1179  			content := "I love my cats!"
  1180  			requestBody = strings.NewReader(content)
  1181  			dataLength = int64(len(content))
  1182  			writeErrors = make(chan error)
  1183  
  1184  			response := `{
  1185  						"guid": "some-package-guid",
  1186  						"type": "bits",
  1187  						"state": "PROCESSING_UPLOAD"
  1188  					}`
  1189  
  1190  			server.AppendHandlers(
  1191  				CombineHandlers(
  1192  					VerifyRequest(http.MethodPost, "/v3/packages/package-guid/upload"),
  1193  					VerifyHeaderKV("Content-Type", "multipart/form-data"),
  1194  					VerifyBody([]byte(content)),
  1195  					RespondWith(http.StatusOK, response, http.Header{
  1196  						"X-Cf-Warnings": {warning},
  1197  						"Location":      {"something"},
  1198  					}),
  1199  				),
  1200  			)
  1201  		})
  1202  		JustBeforeEach(func() {
  1203  			responseBody = Package{}
  1204  			requestName = internal.PostPackageBitsRequest
  1205  			requestBodyMimeType = "multipart/form-data"
  1206  			uriParams = internal.Params{"package_guid": "package-guid"}
  1207  
  1208  			responseLocation, warnings, executeErr = client.MakeRequestUploadAsync(
  1209  				requestName,
  1210  				uriParams,
  1211  				requestBodyMimeType,
  1212  				requestBody,
  1213  				dataLength,
  1214  				&responseBody,
  1215  				writeErrors,
  1216  			)
  1217  		})
  1218  		When("there are no errors (happy path)", func() {
  1219  			BeforeEach(func() {
  1220  				go func() {
  1221  					close(writeErrors)
  1222  				}()
  1223  			})
  1224  			It("returns the location and any warnings and error", func() {
  1225  				Expect(executeErr).ToNot(HaveOccurred())
  1226  				Expect(responseLocation).To(Equal("something"))
  1227  				Expect(responseBody).To(Equal(Package{
  1228  					GUID:  "some-package-guid",
  1229  					State: "PROCESSING_UPLOAD",
  1230  					Type:  "bits",
  1231  				}))
  1232  				Expect(warnings).To(Equal(Warnings{warning}))
  1233  			})
  1234  		})
  1235  
  1236  		When("There are write errors", func() {
  1237  			BeforeEach(func() {
  1238  				go func() {
  1239  					writeErrors <- errors.New("first-error")
  1240  					writeErrors <- errors.New("second-error")
  1241  					close(writeErrors)
  1242  				}()
  1243  			})
  1244  			It("returns the first error", func() {
  1245  				Expect(executeErr).To(MatchError("first-error"))
  1246  			})
  1247  		})
  1248  
  1249  		When("there are HTTP connection errors", func() {
  1250  			BeforeEach(func() {
  1251  				server.Close()
  1252  				close(writeErrors)
  1253  			})
  1254  
  1255  			It("returns the first error", func() {
  1256  				_, ok := executeErr.(ccerror.RequestError)
  1257  				Expect(ok).To(BeTrue())
  1258  			})
  1259  		})
  1260  	})
  1261  })