github.com/wanddynosios/cli/v8@v8.7.9-0.20240221182337-1a92e3a7017f/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  		Context("with 'per_page' and 'page' query params", func() {
   943  			var (
   944  				resourceList []resources.Stack
   945  				query        []Query
   946  			)
   947  
   948  			BeforeEach(func() {
   949  				resourceList = []resources.Stack{}
   950  				query = []Query{
   951  					{
   952  						Key:    PerPage,
   953  						Values: []string{"1"},
   954  					},
   955  				}
   956  				requestParams = RequestParams{
   957  					RequestName:  internal.GetStacksRequest,
   958  					Query:        query,
   959  					ResponseBody: resources.Stack{},
   960  					AppendToList: func(item interface{}) error {
   961  						resourceList = append(resourceList, item.(resources.Stack))
   962  						return nil
   963  					},
   964  				}
   965  			})
   966  
   967  			When("requesting page=1", func() {
   968  				BeforeEach(func() {
   969  					requestParams.Query = append(requestParams.Query, Query{
   970  						Key: Page, Values: []string{"1"},
   971  					})
   972  
   973  					response1 := fmt.Sprintf(`{
   974  						"pagination": {
   975  							"next": {
   976  								"href": "%s/v3/stacks?per_page=1&page=2"
   977  							}
   978  						},
   979  						"resources": [
   980  							{
   981  								"guid": "stack-guid-1"
   982  							}
   983  						]
   984  					}`, server.URL())
   985  
   986  					server.AppendHandlers(
   987  						CombineHandlers(
   988  							VerifyRequest(http.MethodGet, "/v3/stacks", "per_page=1&page=1"),
   989  							RespondWith(http.StatusOK, response1),
   990  						),
   991  					)
   992  				})
   993  
   994  				It("returns only the resources from the specified page", func() {
   995  					Expect(executeErr).ToNot(HaveOccurred())
   996  					Expect(resourceList).To(Equal([]resources.Stack{{
   997  						GUID: "stack-guid-1",
   998  					}}))
   999  				})
  1000  			})
  1001  
  1002  			When("requesting page=2", func() {
  1003  				BeforeEach(func() {
  1004  					requestParams.Query = append(requestParams.Query, Query{
  1005  						Key: Page, Values: []string{"2"},
  1006  					})
  1007  
  1008  					response2 := `{
  1009  						"pagination": {
  1010  							"next": null
  1011  						},
  1012  						"resources": [
  1013  							{
  1014  								"guid": "stack-guid-2"
  1015  							}
  1016  						]
  1017  					}`
  1018  
  1019  					server.AppendHandlers(
  1020  						CombineHandlers(
  1021  							VerifyRequest(http.MethodGet, "/v3/stacks", "per_page=1&page=2"),
  1022  							RespondWith(http.StatusOK, response2),
  1023  						),
  1024  					)
  1025  				})
  1026  
  1027  				It("returns only the resources from the specified page", func() {
  1028  					Expect(executeErr).ToNot(HaveOccurred())
  1029  					Expect(resourceList).To(Equal([]resources.Stack{{
  1030  						GUID: "stack-guid-2",
  1031  					}}))
  1032  				})
  1033  			})
  1034  		})
  1035  	})
  1036  
  1037  	Describe("MakeRequestReceiveRaw", func() {
  1038  		var (
  1039  			requestName string
  1040  			uriParams   internal.Params
  1041  
  1042  			rawResponseBody      []byte
  1043  			warnings             Warnings
  1044  			executeErr           error
  1045  			responseBodyMimeType string
  1046  		)
  1047  
  1048  		JustBeforeEach(func() {
  1049  			rawResponseBody, warnings, executeErr = client.MakeRequestReceiveRaw(requestName, uriParams, responseBodyMimeType)
  1050  		})
  1051  
  1052  		Context("GET raw bytes (YAML data)", func() {
  1053  			var (
  1054  				expectedResponseBody []byte
  1055  			)
  1056  
  1057  			BeforeEach(func() {
  1058  				requestName = internal.GetApplicationManifestRequest
  1059  				responseBodyMimeType = "application/x-yaml"
  1060  				uriParams = internal.Params{"app_guid": "some-app-guid"}
  1061  			})
  1062  
  1063  			When("getting requested data is successful", func() {
  1064  				BeforeEach(func() {
  1065  					expectedResponseBody = []byte("---\n- banana")
  1066  
  1067  					server.AppendHandlers(
  1068  						CombineHandlers(
  1069  							CombineHandlers(
  1070  								VerifyRequest(http.MethodGet, "/v3/apps/some-app-guid/manifest"),
  1071  								VerifyHeaderKV("Accept", "application/x-yaml"),
  1072  								RespondWith(
  1073  									http.StatusOK,
  1074  									expectedResponseBody,
  1075  									http.Header{
  1076  										"Content-Type":  {"application/x-yaml"},
  1077  										"X-Cf-Warnings": {"this is a warning"},
  1078  									}),
  1079  							),
  1080  						),
  1081  					)
  1082  				})
  1083  
  1084  				It("returns the raw response body and all warnings", func() {
  1085  					Expect(executeErr).NotTo(HaveOccurred())
  1086  					Expect(rawResponseBody).To(Equal(expectedResponseBody))
  1087  					Expect(warnings).To(ConsistOf("this is a warning"))
  1088  				})
  1089  			})
  1090  
  1091  			When("the cloud controller returns errors and warnings", func() {
  1092  				BeforeEach(func() {
  1093  					response := `{
  1094  				  "errors": [
  1095  					{
  1096  					  "code": 10008,
  1097  					  "detail": "The request is semantically invalid: command presence",
  1098  					  "title": "CF-UnprocessableEntity"
  1099  					},
  1100  					{
  1101  					  "code": 10010,
  1102  					  "detail": "Org not found",
  1103  					  "title": "CF-ResourceNotFound"
  1104  					}
  1105  				  ]
  1106  				}`
  1107  
  1108  					server.AppendHandlers(
  1109  						CombineHandlers(
  1110  							VerifyRequest(http.MethodGet, "/v3/apps/some-app-guid/manifest"),
  1111  							VerifyHeaderKV("Accept", "application/x-yaml"),
  1112  							RespondWith(http.StatusTeapot, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}),
  1113  						),
  1114  					)
  1115  				})
  1116  
  1117  				It("returns the error and all warnings", func() {
  1118  					Expect(executeErr).To(MatchError(ccerror.MultiError{
  1119  						ResponseCode: http.StatusTeapot,
  1120  						Errors: []ccerror.V3Error{
  1121  							{
  1122  								Code:   10008,
  1123  								Detail: "The request is semantically invalid: command presence",
  1124  								Title:  "CF-UnprocessableEntity",
  1125  							},
  1126  							{
  1127  								Code:   10010,
  1128  								Detail: "Org not found",
  1129  								Title:  "CF-ResourceNotFound",
  1130  							},
  1131  						},
  1132  					}))
  1133  					Expect(warnings).To(ConsistOf("this is a warning"))
  1134  				})
  1135  			})
  1136  		})
  1137  	})
  1138  
  1139  	Describe("MakeRequestSendRaw", func() {
  1140  		var (
  1141  			requestName         string
  1142  			uriParams           internal.Params
  1143  			requestBodyMimeType string
  1144  
  1145  			requestBody      []byte
  1146  			responseBody     Package
  1147  			expectedJobURL   string
  1148  			responseLocation string
  1149  			warnings         Warnings
  1150  			executeErr       error
  1151  		)
  1152  
  1153  		JustBeforeEach(func() {
  1154  			responseLocation, warnings, executeErr = client.MakeRequestSendRaw(requestName, uriParams, requestBody, requestBodyMimeType, &responseBody)
  1155  		})
  1156  
  1157  		BeforeEach(func() {
  1158  			requestBody = []byte("fake-package-file")
  1159  			expectedJobURL = "apply-manifest-job-url"
  1160  			responseBody = Package{}
  1161  
  1162  			requestName = internal.PostPackageBitsRequest
  1163  			uriParams = internal.Params{"package_guid": "package-guid"}
  1164  			requestBodyMimeType = "multipart/form-data"
  1165  		})
  1166  
  1167  		When("the resource is successfully created", func() {
  1168  			BeforeEach(func() {
  1169  				response := `{
  1170  					"guid": "some-pkg-guid",
  1171  					"type": "docker",
  1172  					"state": "PROCESSING_UPLOAD",
  1173  					"links": {
  1174  						"upload": {
  1175  							"href": "some-package-upload-url",
  1176  							"method": "POST"
  1177  						}
  1178  					}
  1179  				}`
  1180  
  1181  				server.AppendHandlers(
  1182  					CombineHandlers(
  1183  						VerifyRequest(http.MethodPost, "/v3/packages/package-guid/upload"),
  1184  						VerifyBody(requestBody),
  1185  						VerifyHeaderKV("Content-Type", "multipart/form-data"),
  1186  						RespondWith(http.StatusCreated, response, http.Header{
  1187  							"X-Cf-Warnings": {"this is a warning"},
  1188  							"Location":      {expectedJobURL},
  1189  						}),
  1190  					),
  1191  				)
  1192  			})
  1193  
  1194  			It("returns the resource and warnings", func() {
  1195  				Expect(responseLocation).To(Equal(expectedJobURL))
  1196  				Expect(executeErr).NotTo(HaveOccurred())
  1197  				Expect(warnings).To(ConsistOf("this is a warning"))
  1198  
  1199  				expectedPackage := Package{
  1200  					GUID:  "some-pkg-guid",
  1201  					Type:  constant.PackageTypeDocker,
  1202  					State: constant.PackageProcessingUpload,
  1203  					Links: map[string]APILink{
  1204  						"upload": APILink{HREF: "some-package-upload-url", Method: http.MethodPost},
  1205  					},
  1206  				}
  1207  				Expect(responseBody).To(Equal(expectedPackage))
  1208  			})
  1209  		})
  1210  
  1211  		When("the resource returns all errors and warnings", func() {
  1212  			BeforeEach(func() {
  1213  				response := ` {
  1214    "errors": [
  1215      {
  1216        "code": 10008,
  1217        "detail": "The request is semantically invalid: command presence",
  1218        "title": "CF-UnprocessableEntity"
  1219      },
  1220      {
  1221        "code": 10010,
  1222        "detail": "Hamster not found",
  1223        "title": "CF-ResourceNotFound"
  1224      }
  1225    ]
  1226  }`
  1227  				server.AppendHandlers(
  1228  					CombineHandlers(
  1229  						VerifyRequest(http.MethodPost, "/v3/packages/package-guid/upload"),
  1230  						RespondWith(http.StatusTeapot, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}),
  1231  					),
  1232  				)
  1233  			})
  1234  
  1235  			It("returns the error and all warnings", func() {
  1236  				Expect(executeErr).To(MatchError(ccerror.MultiError{
  1237  					ResponseCode: http.StatusTeapot,
  1238  					Errors: []ccerror.V3Error{
  1239  						{
  1240  							Code:   10008,
  1241  							Detail: "The request is semantically invalid: command presence",
  1242  							Title:  "CF-UnprocessableEntity",
  1243  						},
  1244  						{
  1245  							Code:   10010,
  1246  							Detail: "Hamster not found",
  1247  							Title:  "CF-ResourceNotFound",
  1248  						},
  1249  					},
  1250  				}))
  1251  				Expect(warnings).To(ConsistOf("this is a warning"))
  1252  			})
  1253  		})
  1254  	})
  1255  
  1256  	Describe("MakeRequestUploadAsync", func() {
  1257  		var (
  1258  			requestName         string
  1259  			uriParams           internal.Params
  1260  			requestBodyMimeType string
  1261  			requestBody         io.ReadSeeker
  1262  			dataLength          int64
  1263  			writeErrors         chan error
  1264  
  1265  			responseLocation string
  1266  			responseBody     Package
  1267  			warning          string
  1268  			warnings         Warnings
  1269  			executeErr       error
  1270  		)
  1271  		BeforeEach(func() {
  1272  			warning = "upload-async-warning"
  1273  			content := "I love my cats!"
  1274  			requestBody = strings.NewReader(content)
  1275  			dataLength = int64(len(content))
  1276  			writeErrors = make(chan error)
  1277  
  1278  			response := `{
  1279  						"guid": "some-package-guid",
  1280  						"type": "bits",
  1281  						"state": "PROCESSING_UPLOAD"
  1282  					}`
  1283  
  1284  			server.AppendHandlers(
  1285  				CombineHandlers(
  1286  					VerifyRequest(http.MethodPost, "/v3/packages/package-guid/upload"),
  1287  					VerifyHeaderKV("Content-Type", "multipart/form-data"),
  1288  					VerifyBody([]byte(content)),
  1289  					RespondWith(http.StatusOK, response, http.Header{
  1290  						"X-Cf-Warnings": {warning},
  1291  						"Location":      {"something"},
  1292  					}),
  1293  				),
  1294  			)
  1295  		})
  1296  		JustBeforeEach(func() {
  1297  			responseBody = Package{}
  1298  			requestName = internal.PostPackageBitsRequest
  1299  			requestBodyMimeType = "multipart/form-data"
  1300  			uriParams = internal.Params{"package_guid": "package-guid"}
  1301  
  1302  			responseLocation, warnings, executeErr = client.MakeRequestUploadAsync(
  1303  				requestName,
  1304  				uriParams,
  1305  				requestBodyMimeType,
  1306  				requestBody,
  1307  				dataLength,
  1308  				&responseBody,
  1309  				writeErrors,
  1310  			)
  1311  		})
  1312  		When("there are no errors (happy path)", func() {
  1313  			BeforeEach(func() {
  1314  				go func() {
  1315  					close(writeErrors)
  1316  				}()
  1317  			})
  1318  			It("returns the location and any warnings and error", func() {
  1319  				Expect(executeErr).ToNot(HaveOccurred())
  1320  				Expect(responseLocation).To(Equal("something"))
  1321  				Expect(responseBody).To(Equal(Package{
  1322  					GUID:  "some-package-guid",
  1323  					State: "PROCESSING_UPLOAD",
  1324  					Type:  "bits",
  1325  				}))
  1326  				Expect(warnings).To(Equal(Warnings{warning}))
  1327  			})
  1328  		})
  1329  
  1330  		When("There are write errors", func() {
  1331  			BeforeEach(func() {
  1332  				go func() {
  1333  					writeErrors <- errors.New("first-error")
  1334  					writeErrors <- errors.New("second-error")
  1335  					close(writeErrors)
  1336  				}()
  1337  			})
  1338  			It("returns the first error", func() {
  1339  				Expect(executeErr).To(MatchError("first-error"))
  1340  			})
  1341  		})
  1342  
  1343  		When("there are HTTP connection errors", func() {
  1344  			BeforeEach(func() {
  1345  				server.Close()
  1346  				close(writeErrors)
  1347  			})
  1348  
  1349  			It("returns the first error", func() {
  1350  				_, ok := executeErr.(ccerror.RequestError)
  1351  				Expect(ok).To(BeTrue())
  1352  			})
  1353  		})
  1354  	})
  1355  })