github.com/loggregator/cli@v6.33.1-0.20180224010324-82334f081791+incompatible/api/cloudcontroller/ccv3/application_test.go (about)

     1  package ccv3_test
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"net/http"
     7  
     8  	"code.cloudfoundry.org/cli/api/cloudcontroller/ccerror"
     9  	. "code.cloudfoundry.org/cli/api/cloudcontroller/ccv3"
    10  	"code.cloudfoundry.org/cli/api/cloudcontroller/ccv3/constant"
    11  	. "github.com/onsi/ginkgo"
    12  	. "github.com/onsi/gomega"
    13  	. "github.com/onsi/gomega/ghttp"
    14  )
    15  
    16  var _ = Describe("Application", func() {
    17  	var client *Client
    18  
    19  	BeforeEach(func() {
    20  		client = NewTestClient()
    21  	})
    22  
    23  	Describe("Application", func() {
    24  		Describe("MarshalJSON", func() {
    25  			var (
    26  				app      Application
    27  				appBytes []byte
    28  				err      error
    29  			)
    30  
    31  			BeforeEach(func() {
    32  				app = Application{}
    33  			})
    34  
    35  			JustBeforeEach(func() {
    36  				appBytes, err = app.MarshalJSON()
    37  				Expect(err).ToNot(HaveOccurred())
    38  			})
    39  
    40  			Context("when no lifecycle is provided", func() {
    41  				BeforeEach(func() {
    42  					app = Application{}
    43  				})
    44  
    45  				It("omits the lifecycle from the JSON", func() {
    46  					Expect(string(appBytes)).To(Equal("{}"))
    47  				})
    48  			})
    49  
    50  			Context("when lifecycle type docker is provided", func() {
    51  				BeforeEach(func() {
    52  					app = Application{
    53  						LifecycleType: constant.DockerAppLifecycleType,
    54  					}
    55  				})
    56  
    57  				It("sets lifecycle type to docker with empty data", func() {
    58  					Expect(string(appBytes)).To(MatchJSON(`{"lifecycle":{"type":"docker","data":{}}}`))
    59  				})
    60  			})
    61  
    62  			Context("when lifecycle type buildpack is provided", func() {
    63  				BeforeEach(func() {
    64  					app.LifecycleType = constant.BuildpackAppLifecycleType
    65  				})
    66  
    67  				Context("when no buildpacks are provided", func() {
    68  					It("omits the lifecycle from the JSON", func() {
    69  						Expect(string(appBytes)).To(Equal("{}"))
    70  					})
    71  				})
    72  
    73  				Context("when default buildpack is provided", func() {
    74  					BeforeEach(func() {
    75  						app.LifecycleBuildpacks = []string{"default"}
    76  					})
    77  
    78  					It("sets the lifecycle buildpack to be empty in the JSON", func() {
    79  						Expect(string(appBytes)).To(Equal(`{"lifecycle":{"data":{"buildpacks":null},"type":"buildpack"}}`))
    80  					})
    81  				})
    82  
    83  				Context("when null buildpack is provided", func() {
    84  					BeforeEach(func() {
    85  						app.LifecycleBuildpacks = []string{"null"}
    86  					})
    87  
    88  					It("sets the Lifecycle buildpack to be empty in the JSON", func() {
    89  						Expect(string(appBytes)).To(Equal(`{"lifecycle":{"data":{"buildpacks":null},"type":"buildpack"}}`))
    90  					})
    91  				})
    92  
    93  				Context("when other buildpacks are provided", func() {
    94  					BeforeEach(func() {
    95  						app.LifecycleBuildpacks = []string{"some-buildpack"}
    96  					})
    97  
    98  					It("sets them in the JSON", func() {
    99  						Expect(string(appBytes)).To(Equal(`{"lifecycle":{"data":{"buildpacks":["some-buildpack"]},"type":"buildpack"}}`))
   100  					})
   101  				})
   102  			})
   103  		})
   104  
   105  		Describe("UnmarshalJSON", func() {
   106  			var (
   107  				app      Application
   108  				appBytes []byte
   109  				err      error
   110  			)
   111  
   112  			BeforeEach(func() {
   113  				appBytes = []byte("{}")
   114  			})
   115  
   116  			JustBeforeEach(func() {
   117  				err = json.Unmarshal(appBytes, &app)
   118  				Expect(err).ToNot(HaveOccurred())
   119  			})
   120  
   121  			Context("when no lifecycle is provided", func() {
   122  				BeforeEach(func() {
   123  					appBytes = []byte("{}")
   124  				})
   125  
   126  				It("omits the lifecycle from the JSON", func() {
   127  					Expect(app).To(Equal(Application{}))
   128  				})
   129  			})
   130  
   131  			Context("when lifecycle type docker is provided", func() {
   132  				BeforeEach(func() {
   133  					appBytes = []byte(`{"lifecycle":{"type":"docker","data":{}}}`)
   134  				})
   135  				It("sets the lifecycle type to docker with empty data", func() {
   136  					Expect(app).To(Equal(Application{
   137  						LifecycleType: constant.DockerAppLifecycleType,
   138  					}))
   139  				})
   140  			})
   141  
   142  			Context("when lifecycle type buildpack is provided", func() {
   143  
   144  				Context("when other buildpacks are provided", func() {
   145  					BeforeEach(func() {
   146  						appBytes = []byte(`{"lifecycle":{"data":{"buildpacks":["some-buildpack"]},"type":"buildpack"}}`)
   147  					})
   148  
   149  					It("sets them in the JSON", func() {
   150  						Expect(app).To(Equal(Application{
   151  							LifecycleType:       constant.BuildpackAppLifecycleType,
   152  							LifecycleBuildpacks: []string{"some-buildpack"},
   153  						}))
   154  					})
   155  				})
   156  			})
   157  		})
   158  	})
   159  
   160  	Describe("GetApplications", func() {
   161  		Context("when applications exist", func() {
   162  			BeforeEach(func() {
   163  				response1 := fmt.Sprintf(`{
   164  	"pagination": {
   165  		"next": {
   166  			"href": "%s/v3/apps?space_guids=some-space-guid&names=some-app-name&page=2&per_page=2"
   167  		}
   168  	},
   169    "resources": [
   170      {
   171        "name": "app-name-1",
   172        "guid": "app-guid-1",
   173  			"lifecycle": {
   174  				"type": "buildpack",
   175  				"data": {
   176  					"buildpacks": ["some-buildpack"],
   177  					"stack": "some-stack"
   178  				}
   179  			}
   180      },
   181      {
   182        "name": "app-name-2",
   183        "guid": "app-guid-2"
   184      }
   185    ]
   186  }`, server.URL())
   187  				response2 := `{
   188  	"pagination": {
   189  		"next": null
   190  	},
   191  	"resources": [
   192  	  {
   193        "name": "app-name-3",
   194  		  "guid": "app-guid-3"
   195  		}
   196  	]
   197  }`
   198  				server.AppendHandlers(
   199  					CombineHandlers(
   200  						VerifyRequest(http.MethodGet, "/v3/apps", "space_guids=some-space-guid&names=some-app-name"),
   201  						RespondWith(http.StatusOK, response1, http.Header{"X-Cf-Warnings": {"this is a warning"}}),
   202  					),
   203  				)
   204  				server.AppendHandlers(
   205  					CombineHandlers(
   206  						VerifyRequest(http.MethodGet, "/v3/apps", "space_guids=some-space-guid&names=some-app-name&page=2&per_page=2"),
   207  						RespondWith(http.StatusOK, response2, http.Header{"X-Cf-Warnings": {"this is another warning"}}),
   208  					),
   209  				)
   210  			})
   211  
   212  			It("returns the queried applications and all warnings", func() {
   213  				apps, warnings, err := client.GetApplications(
   214  					Query{Key: SpaceGUIDFilter, Values: []string{"some-space-guid"}},
   215  					Query{Key: NameFilter, Values: []string{"some-app-name"}},
   216  				)
   217  				Expect(err).NotTo(HaveOccurred())
   218  
   219  				Expect(apps).To(ConsistOf(
   220  					Application{
   221  						Name:                "app-name-1",
   222  						GUID:                "app-guid-1",
   223  						LifecycleType:       constant.BuildpackAppLifecycleType,
   224  						LifecycleBuildpacks: []string{"some-buildpack"},
   225  					},
   226  					Application{Name: "app-name-2", GUID: "app-guid-2"},
   227  					Application{Name: "app-name-3", GUID: "app-guid-3"},
   228  				))
   229  				Expect(warnings).To(ConsistOf("this is a warning", "this is another warning"))
   230  			})
   231  		})
   232  
   233  		Context("when the cloud controller returns errors and warnings", func() {
   234  			BeforeEach(func() {
   235  				response := `{
   236    "errors": [
   237      {
   238        "code": 10008,
   239        "detail": "The request is semantically invalid: command presence",
   240        "title": "CF-UnprocessableEntity"
   241      },
   242      {
   243        "code": 10010,
   244        "detail": "App not found",
   245        "title": "CF-ResourceNotFound"
   246      }
   247    ]
   248  }`
   249  				server.AppendHandlers(
   250  					CombineHandlers(
   251  						VerifyRequest(http.MethodGet, "/v3/apps"),
   252  						RespondWith(http.StatusTeapot, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}),
   253  					),
   254  				)
   255  			})
   256  
   257  			It("returns the error and all warnings", func() {
   258  				_, warnings, err := client.GetApplications()
   259  				Expect(err).To(MatchError(ccerror.V3UnexpectedResponseError{
   260  					ResponseCode: http.StatusTeapot,
   261  					V3ErrorResponse: ccerror.V3ErrorResponse{
   262  						Errors: []ccerror.V3Error{
   263  							{
   264  								Code:   10008,
   265  								Detail: "The request is semantically invalid: command presence",
   266  								Title:  "CF-UnprocessableEntity",
   267  							},
   268  							{
   269  								Code:   10010,
   270  								Detail: "App not found",
   271  								Title:  "CF-ResourceNotFound",
   272  							},
   273  						},
   274  					},
   275  				}))
   276  				Expect(warnings).To(ConsistOf("this is a warning"))
   277  			})
   278  		})
   279  	})
   280  
   281  	Describe("CreateApplicationActionsApplyManifestByApplication", func() {
   282  		Context("when the manifest application is successful", func() {
   283  			var expectedBody []byte
   284  			var expectedJobURL string
   285  
   286  			BeforeEach(func() {
   287  				expectedBody = []byte("fake-yaml-body")
   288  				expectedJobURL = "i-am-a-job-url"
   289  				response := ""
   290  
   291  				server.AppendHandlers(
   292  					CombineHandlers(
   293  						VerifyRequest(http.MethodPost, "/v3/apps/some-app-guid/actions/apply_manifest"),
   294  						VerifyHeaderKV("Content-type", "application/x-yaml"),
   295  						VerifyBody(expectedBody),
   296  						RespondWith(http.StatusAccepted, response, http.Header{
   297  							"X-Cf-Warnings": {"this is a warning"},
   298  							"Location":      {expectedJobURL},
   299  						}),
   300  					),
   301  				)
   302  			})
   303  
   304  			It("returns the job URL and warnings", func() {
   305  				jobURL, warnings, err := client.CreateApplicationActionsApplyManifestByApplication(
   306  					expectedBody,
   307  					"some-app-guid",
   308  				)
   309  
   310  				Expect(err).NotTo(HaveOccurred())
   311  				Expect(warnings).To(ConsistOf("this is a warning"))
   312  
   313  				Expect(jobURL).To(Equal(expectedJobURL))
   314  			})
   315  		})
   316  
   317  		Context("when the manifest application fails", func() {
   318  			BeforeEach(func() {
   319  				response := `{
   320    "errors": [
   321      {
   322        "code": 1001,
   323        "detail": "Request invalid due to parse error: invalid request body",
   324        "title": "CF-MessageParseError"
   325      },
   326      {
   327        "code": 10010,
   328        "detail": "App not found",
   329        "title": "CF-ResourceNotFound"
   330      }
   331    ]
   332  }`
   333  				server.AppendHandlers(
   334  					CombineHandlers(
   335  						VerifyRequest(http.MethodPost, "/v3/apps/some-app-guid/actions/apply_manifest"),
   336  						VerifyHeaderKV("Content-type", "application/x-yaml"),
   337  						RespondWith(http.StatusTeapot, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}),
   338  					),
   339  				)
   340  			})
   341  
   342  			It("returns the error and all warnings", func() {
   343  				_, warnings, err := client.CreateApplicationActionsApplyManifestByApplication(nil, "some-app-guid")
   344  				Expect(err).To(MatchError(ccerror.V3UnexpectedResponseError{
   345  					ResponseCode: http.StatusTeapot,
   346  					V3ErrorResponse: ccerror.V3ErrorResponse{
   347  						Errors: []ccerror.V3Error{
   348  							{
   349  								Code:   1001,
   350  								Detail: "Request invalid due to parse error: invalid request body",
   351  								Title:  "CF-MessageParseError",
   352  							},
   353  							{
   354  								Code:   10010,
   355  								Detail: "App not found",
   356  								Title:  "CF-ResourceNotFound",
   357  							},
   358  						},
   359  					},
   360  				}))
   361  				Expect(warnings).To(ConsistOf("this is a warning"))
   362  			})
   363  		})
   364  	})
   365  
   366  	Describe("UpdateApplication", func() {
   367  		Context("when the application successfully is updated", func() {
   368  			BeforeEach(func() {
   369  				response := `{
   370  					"guid": "some-app-guid",
   371  					"name": "some-app-name"
   372  				}`
   373  
   374  				expectedBody := map[string]interface{}{
   375  					"name": "some-app-name",
   376  					"lifecycle": map[string]interface{}{
   377  						"type": "buildpack",
   378  						"data": map[string]interface{}{
   379  							"buildpacks": []string{"some-buildpack"},
   380  						},
   381  					},
   382  					"relationships": map[string]interface{}{
   383  						"space": map[string]interface{}{
   384  							"data": map[string]string{
   385  								"guid": "some-space-guid",
   386  							},
   387  						},
   388  					},
   389  				}
   390  				server.AppendHandlers(
   391  					CombineHandlers(
   392  						VerifyRequest(http.MethodPatch, "/v3/apps/some-app-guid"),
   393  						VerifyJSONRepresenting(expectedBody),
   394  						RespondWith(http.StatusOK, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}),
   395  					),
   396  				)
   397  			})
   398  
   399  			It("returns the updated app and warnings", func() {
   400  				app, warnings, err := client.UpdateApplication(Application{
   401  					GUID:                "some-app-guid",
   402  					Name:                "some-app-name",
   403  					LifecycleType:       constant.BuildpackAppLifecycleType,
   404  					LifecycleBuildpacks: []string{"some-buildpack"},
   405  					Relationships: Relationships{
   406  						constant.SpaceRelationship: Relationship{GUID: "some-space-guid"},
   407  					},
   408  				})
   409  
   410  				Expect(err).NotTo(HaveOccurred())
   411  				Expect(warnings).To(ConsistOf("this is a warning"))
   412  
   413  				Expect(app).To(Equal(Application{
   414  					Name: "some-app-name",
   415  					GUID: "some-app-guid",
   416  				}))
   417  			})
   418  		})
   419  
   420  		Context("when cc returns back an error or warnings", func() {
   421  			BeforeEach(func() {
   422  				response := `{
   423    "errors": [
   424      {
   425        "code": 10008,
   426        "detail": "The request is semantically invalid: command presence",
   427        "title": "CF-UnprocessableEntity"
   428      },
   429      {
   430        "code": 10010,
   431        "detail": "App not found",
   432        "title": "CF-ResourceNotFound"
   433      }
   434    ]
   435  }`
   436  				server.AppendHandlers(
   437  					CombineHandlers(
   438  						VerifyRequest(http.MethodPatch, "/v3/apps/some-app-guid"),
   439  						RespondWith(http.StatusTeapot, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}),
   440  					),
   441  				)
   442  			})
   443  
   444  			It("returns the error and all warnings", func() {
   445  				_, warnings, err := client.UpdateApplication(Application{GUID: "some-app-guid"})
   446  				Expect(err).To(MatchError(ccerror.V3UnexpectedResponseError{
   447  					ResponseCode: http.StatusTeapot,
   448  					V3ErrorResponse: ccerror.V3ErrorResponse{
   449  						Errors: []ccerror.V3Error{
   450  							{
   451  								Code:   10008,
   452  								Detail: "The request is semantically invalid: command presence",
   453  								Title:  "CF-UnprocessableEntity",
   454  							},
   455  							{
   456  								Code:   10010,
   457  								Detail: "App not found",
   458  								Title:  "CF-ResourceNotFound",
   459  							},
   460  						},
   461  					},
   462  				}))
   463  				Expect(warnings).To(ConsistOf("this is a warning"))
   464  			})
   465  		})
   466  	})
   467  
   468  	Describe("CreateApplication", func() {
   469  		Context("when the application successfully is created", func() {
   470  			BeforeEach(func() {
   471  				response := `{
   472  					"guid": "some-app-guid",
   473  					"name": "some-app-name"
   474  				}`
   475  
   476  				expectedBody := map[string]interface{}{
   477  					"name": "some-app-name",
   478  					"relationships": map[string]interface{}{
   479  						"space": map[string]interface{}{
   480  							"data": map[string]string{
   481  								"guid": "some-space-guid",
   482  							},
   483  						},
   484  					},
   485  				}
   486  				server.AppendHandlers(
   487  					CombineHandlers(
   488  						VerifyRequest(http.MethodPost, "/v3/apps"),
   489  						VerifyJSONRepresenting(expectedBody),
   490  						RespondWith(http.StatusCreated, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}),
   491  					),
   492  				)
   493  			})
   494  
   495  			It("returns the created app and warnings", func() {
   496  				app, warnings, err := client.CreateApplication(Application{
   497  					Name: "some-app-name",
   498  					Relationships: Relationships{
   499  						constant.SpaceRelationship: Relationship{GUID: "some-space-guid"},
   500  					},
   501  				})
   502  
   503  				Expect(err).NotTo(HaveOccurred())
   504  				Expect(warnings).To(ConsistOf("this is a warning"))
   505  
   506  				Expect(app).To(Equal(Application{
   507  					Name: "some-app-name",
   508  					GUID: "some-app-guid",
   509  				}))
   510  			})
   511  		})
   512  
   513  		Context("when the caller specifies a buildpack", func() {
   514  			BeforeEach(func() {
   515  				response := `{
   516  					"guid": "some-app-guid",
   517  					"name": "some-app-name",
   518  					"lifecycle": {
   519  						"type": "buildpack",
   520  						"data": {
   521  							"buildpacks": ["some-buildpack"]
   522  					  }
   523  					}
   524  				}`
   525  
   526  				expectedBody := map[string]interface{}{
   527  					"name": "some-app-name",
   528  					"lifecycle": map[string]interface{}{
   529  						"type": "buildpack",
   530  						"data": map[string]interface{}{
   531  							"buildpacks": []string{"some-buildpack"},
   532  						},
   533  					},
   534  					"relationships": map[string]interface{}{
   535  						"space": map[string]interface{}{
   536  							"data": map[string]string{
   537  								"guid": "some-space-guid",
   538  							},
   539  						},
   540  					},
   541  				}
   542  				server.AppendHandlers(
   543  					CombineHandlers(
   544  						VerifyRequest(http.MethodPost, "/v3/apps"),
   545  						VerifyJSONRepresenting(expectedBody),
   546  						RespondWith(http.StatusCreated, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}),
   547  					),
   548  				)
   549  			})
   550  
   551  			It("returns the created app and warnings", func() {
   552  				app, warnings, err := client.CreateApplication(Application{
   553  					Name:                "some-app-name",
   554  					LifecycleType:       constant.BuildpackAppLifecycleType,
   555  					LifecycleBuildpacks: []string{"some-buildpack"},
   556  					Relationships: Relationships{
   557  						constant.SpaceRelationship: Relationship{GUID: "some-space-guid"},
   558  					},
   559  				})
   560  
   561  				Expect(err).NotTo(HaveOccurred())
   562  				Expect(warnings).To(ConsistOf("this is a warning"))
   563  
   564  				Expect(app).To(Equal(Application{
   565  					Name:                "some-app-name",
   566  					GUID:                "some-app-guid",
   567  					LifecycleType:       constant.BuildpackAppLifecycleType,
   568  					LifecycleBuildpacks: []string{"some-buildpack"},
   569  				}))
   570  			})
   571  		})
   572  
   573  		Context("when cc returns back an error or warnings", func() {
   574  			BeforeEach(func() {
   575  				response := `{
   576    "errors": [
   577      {
   578        "code": 10008,
   579        "detail": "The request is semantically invalid: command presence",
   580        "title": "CF-UnprocessableEntity"
   581      },
   582      {
   583        "code": 10010,
   584        "detail": "App not found",
   585        "title": "CF-ResourceNotFound"
   586      }
   587    ]
   588  }`
   589  				server.AppendHandlers(
   590  					CombineHandlers(
   591  						VerifyRequest(http.MethodPost, "/v3/apps"),
   592  						RespondWith(http.StatusTeapot, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}),
   593  					),
   594  				)
   595  			})
   596  
   597  			It("returns the error and all warnings", func() {
   598  				_, warnings, err := client.CreateApplication(Application{})
   599  				Expect(err).To(MatchError(ccerror.V3UnexpectedResponseError{
   600  					ResponseCode: http.StatusTeapot,
   601  					V3ErrorResponse: ccerror.V3ErrorResponse{
   602  						Errors: []ccerror.V3Error{
   603  							{
   604  								Code:   10008,
   605  								Detail: "The request is semantically invalid: command presence",
   606  								Title:  "CF-UnprocessableEntity",
   607  							},
   608  							{
   609  								Code:   10010,
   610  								Detail: "App not found",
   611  								Title:  "CF-ResourceNotFound",
   612  							},
   613  						},
   614  					},
   615  				}))
   616  				Expect(warnings).To(ConsistOf("this is a warning"))
   617  			})
   618  		})
   619  	})
   620  
   621  	Describe("DeleteApplication", func() {
   622  		Context("when the application is deleted successfully", func() {
   623  			BeforeEach(func() {
   624  				server.AppendHandlers(
   625  					CombineHandlers(
   626  						VerifyRequest(http.MethodDelete, "/v3/apps/some-app-guid"),
   627  						RespondWith(http.StatusAccepted, ``,
   628  							http.Header{
   629  								"X-Cf-Warnings": {"some-warning"},
   630  								"Location":      {"/v3/jobs/some-location"},
   631  							},
   632  						),
   633  					),
   634  				)
   635  			})
   636  
   637  			It("returns all warnings", func() {
   638  				jobLocation, warnings, err := client.DeleteApplication("some-app-guid")
   639  				Expect(err).ToNot(HaveOccurred())
   640  				Expect(jobLocation).To(Equal("/v3/jobs/some-location"))
   641  				Expect(warnings).To(ConsistOf("some-warning"))
   642  			})
   643  		})
   644  
   645  		Context("when deleting the application returns an error", func() {
   646  			BeforeEach(func() {
   647  				server.AppendHandlers(
   648  					CombineHandlers(
   649  						VerifyRequest(http.MethodDelete, "/v3/apps/some-app-guid"),
   650  						RespondWith(http.StatusBadRequest, `{}`,
   651  							http.Header{
   652  								"X-Cf-Warnings": {"some-warning"},
   653  							},
   654  						),
   655  					),
   656  				)
   657  			})
   658  
   659  			It("returns all warnings", func() {
   660  				_, warnings, err := client.DeleteApplication("some-app-guid")
   661  				Expect(err).To(MatchError(ccerror.V3UnexpectedResponseError{ResponseCode: 400}))
   662  				Expect(warnings).To(ConsistOf("some-warning"))
   663  			})
   664  		})
   665  	})
   666  
   667  	Describe("SetApplicationDroplet", func() {
   668  		Context("it sets the droplet", func() {
   669  			BeforeEach(func() {
   670  				response := `
   671  {
   672    "data": {
   673      "guid": "some-droplet-guid"
   674    },
   675    "links": {
   676      "self": {
   677        "href": "https://api.example.org/v3/apps/some-app-guid/relationships/current_droplet"
   678      },
   679      "related": {
   680        "href": "https://api.example.org/v3/apps/some-app-guid/droplets/current"
   681      }
   682    }
   683  }`
   684  				requestBody := map[string]interface{}{
   685  					"data": map[string]string{
   686  						"guid": "some-droplet-guid",
   687  					},
   688  				}
   689  
   690  				server.AppendHandlers(
   691  					CombineHandlers(
   692  						VerifyRequest(http.MethodPatch, "/v3/apps/some-app-guid/relationships/current_droplet"),
   693  						VerifyJSONRepresenting(requestBody),
   694  						RespondWith(http.StatusOK, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}),
   695  					),
   696  				)
   697  			})
   698  
   699  			It("returns warnings and no error", func() {
   700  				relationship, warnings, err := client.SetApplicationDroplet("some-app-guid", "some-droplet-guid")
   701  				Expect(err).ToNot(HaveOccurred())
   702  				Expect(warnings).To(ConsistOf("this is a warning"))
   703  				Expect(relationship.GUID).To(Equal("some-droplet-guid"))
   704  			})
   705  		})
   706  	})
   707  	Context("when setting the app to the new droplet returns errors and warnings", func() {
   708  		BeforeEach(func() {
   709  			response := `{
   710    "errors": [
   711      {
   712        "code": 10008,
   713        "detail": "The request is semantically invalid: command presence",
   714        "title": "CF-UnprocessableEntity"
   715      },
   716      {
   717        "code": 10010,
   718        "detail": "App not found",
   719        "title": "CF-ResourceNotFound"
   720      }
   721    ]
   722  }`
   723  			requestBody := map[string]interface{}{
   724  				"data": map[string]string{
   725  					"guid": "some-droplet-guid",
   726  				},
   727  			}
   728  
   729  			server.AppendHandlers(
   730  				CombineHandlers(
   731  					VerifyRequest(http.MethodPatch, "/v3/apps/no-such-app-guid/relationships/current_droplet"),
   732  					VerifyJSONRepresenting(requestBody),
   733  					RespondWith(http.StatusTeapot, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}),
   734  				),
   735  			)
   736  
   737  		})
   738  
   739  		It("returns the error and all warnings", func() {
   740  			_, warnings, err := client.SetApplicationDroplet("no-such-app-guid", "some-droplet-guid")
   741  			Expect(err).To(MatchError(ccerror.V3UnexpectedResponseError{
   742  				ResponseCode: http.StatusTeapot,
   743  				V3ErrorResponse: ccerror.V3ErrorResponse{
   744  					Errors: []ccerror.V3Error{
   745  						{
   746  							Code:   10008,
   747  							Detail: "The request is semantically invalid: command presence",
   748  							Title:  "CF-UnprocessableEntity",
   749  						},
   750  						{
   751  							Code:   10010,
   752  							Detail: "App not found",
   753  							Title:  "CF-ResourceNotFound",
   754  						},
   755  					},
   756  				},
   757  			}))
   758  			Expect(warnings).To(ConsistOf("this is a warning"))
   759  		})
   760  	})
   761  
   762  	Describe("StopApplication", func() {
   763  		Context("when the response succeeds", func() {
   764  			BeforeEach(func() {
   765  				response := `
   766  {
   767  	"guid": "some-app-guid",
   768  	"name": "some-app",
   769  	"state": "STOPPED"
   770  }`
   771  				server.AppendHandlers(
   772  					CombineHandlers(
   773  						VerifyRequest(http.MethodPost, "/v3/apps/some-app-guid/actions/stop"),
   774  						RespondWith(http.StatusOK, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}),
   775  					),
   776  				)
   777  			})
   778  
   779  			It("returns the application, warnings, and no error", func() {
   780  				responseApp, warnings, err := client.StopApplication("some-app-guid")
   781  				Expect(responseApp).To(Equal(Application{
   782  					GUID:  "some-app-guid",
   783  					Name:  "some-app",
   784  					State: constant.ApplicationStopped,
   785  				}))
   786  				Expect(err).ToNot(HaveOccurred())
   787  				Expect(warnings).To(ConsistOf("this is a warning"))
   788  			})
   789  		})
   790  	})
   791  
   792  	Context("when stopping the app returns errors and warnings", func() {
   793  		BeforeEach(func() {
   794  			response := `{
   795    "errors": [
   796      {
   797        "code": 10008,
   798        "detail": "The request is semantically invalid: command presence",
   799        "title": "CF-UnprocessableEntity"
   800      },
   801      {
   802        "code": 10010,
   803        "detail": "App not found",
   804        "title": "CF-ResourceNotFound"
   805      }
   806    ]
   807  }`
   808  			server.AppendHandlers(
   809  				CombineHandlers(
   810  					VerifyRequest(http.MethodPost, "/v3/apps/no-such-app-guid/actions/stop"),
   811  					RespondWith(http.StatusTeapot, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}),
   812  				),
   813  			)
   814  
   815  		})
   816  
   817  		It("returns no app, the error and all warnings", func() {
   818  			responseApp, warnings, err := client.StopApplication("no-such-app-guid")
   819  			Expect(responseApp).To(BeZero())
   820  			Expect(err).To(MatchError(ccerror.V3UnexpectedResponseError{
   821  				ResponseCode: http.StatusTeapot,
   822  				V3ErrorResponse: ccerror.V3ErrorResponse{
   823  					Errors: []ccerror.V3Error{
   824  						{
   825  							Code:   10008,
   826  							Detail: "The request is semantically invalid: command presence",
   827  							Title:  "CF-UnprocessableEntity",
   828  						},
   829  						{
   830  							Code:   10010,
   831  							Detail: "App not found",
   832  							Title:  "CF-ResourceNotFound",
   833  						},
   834  					},
   835  				},
   836  			}))
   837  			Expect(warnings).To(ConsistOf("this is a warning"))
   838  		})
   839  	})
   840  
   841  	Describe("StartApplication", func() {
   842  		Context("when the response succeeds", func() {
   843  			BeforeEach(func() {
   844  				response := `
   845  {
   846  	"guid": "some-app-guid",
   847  	"name": "some-app"
   848  }`
   849  				server.AppendHandlers(
   850  					CombineHandlers(
   851  						VerifyRequest(http.MethodPost, "/v3/apps/some-app-guid/actions/start"),
   852  						RespondWith(http.StatusOK, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}),
   853  					),
   854  				)
   855  			})
   856  
   857  			It("returns warnings and no error", func() {
   858  				app, warnings, err := client.StartApplication("some-app-guid")
   859  				Expect(err).ToNot(HaveOccurred())
   860  				Expect(warnings).To(ConsistOf("this is a warning"))
   861  				Expect(app.GUID).To(Equal("some-app-guid"))
   862  			})
   863  		})
   864  	})
   865  	Context("when starting the app returns errors and warnings", func() {
   866  		BeforeEach(func() {
   867  			response := `{
   868    "errors": [
   869      {
   870        "code": 10008,
   871        "detail": "The request is semantically invalid: command presence",
   872        "title": "CF-UnprocessableEntity"
   873      },
   874      {
   875        "code": 10010,
   876        "detail": "App not found",
   877        "title": "CF-ResourceNotFound"
   878      }
   879    ]
   880  }`
   881  			server.AppendHandlers(
   882  				CombineHandlers(
   883  					VerifyRequest(http.MethodPost, "/v3/apps/no-such-app-guid/actions/start"),
   884  					RespondWith(http.StatusTeapot, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}),
   885  				),
   886  			)
   887  
   888  		})
   889  
   890  		It("returns the error and all warnings", func() {
   891  			_, warnings, err := client.StartApplication("no-such-app-guid")
   892  			Expect(err).To(MatchError(ccerror.V3UnexpectedResponseError{
   893  				ResponseCode: http.StatusTeapot,
   894  				V3ErrorResponse: ccerror.V3ErrorResponse{
   895  					Errors: []ccerror.V3Error{
   896  						{
   897  							Code:   10008,
   898  							Detail: "The request is semantically invalid: command presence",
   899  							Title:  "CF-UnprocessableEntity",
   900  						},
   901  						{
   902  							Code:   10010,
   903  							Detail: "App not found",
   904  							Title:  "CF-ResourceNotFound",
   905  						},
   906  					},
   907  				},
   908  			}))
   909  			Expect(warnings).To(ConsistOf("this is a warning"))
   910  		})
   911  	})
   912  })