github.com/cloudfoundry/cli@v7.1.0+incompatible/cf/api/services_test.go (about)

     1  package api_test
     2  
     3  import (
     4  	"fmt"
     5  	"net/http"
     6  	"net/http/httptest"
     7  	"net/url"
     8  	"time"
     9  
    10  	"code.cloudfoundry.org/cli/cf/api/apifakes"
    11  	"code.cloudfoundry.org/cli/cf/api/resources"
    12  	"code.cloudfoundry.org/cli/cf/configuration/coreconfig"
    13  	"code.cloudfoundry.org/cli/cf/errors"
    14  	"code.cloudfoundry.org/cli/cf/models"
    15  	"code.cloudfoundry.org/cli/cf/net"
    16  	"code.cloudfoundry.org/cli/cf/terminal/terminalfakes"
    17  	"code.cloudfoundry.org/cli/cf/trace/tracefakes"
    18  	testconfig "code.cloudfoundry.org/cli/cf/util/testhelpers/configuration"
    19  	testnet "code.cloudfoundry.org/cli/cf/util/testhelpers/net"
    20  
    21  	. "code.cloudfoundry.org/cli/cf/api"
    22  	. "code.cloudfoundry.org/cli/cf/util/testhelpers/matchers"
    23  	. "github.com/onsi/ginkgo"
    24  	. "github.com/onsi/gomega"
    25  )
    26  
    27  var _ = Describe("Services Repo", func() {
    28  	var (
    29  		testServer  *httptest.Server
    30  		testHandler *testnet.TestHandler
    31  		configRepo  coreconfig.ReadWriter
    32  		repo        ServiceRepository
    33  	)
    34  
    35  	setupTestServer := func(reqs ...testnet.TestRequest) {
    36  		testServer, testHandler = testnet.NewServer(reqs)
    37  		configRepo.SetAPIEndpoint(testServer.URL)
    38  	}
    39  
    40  	BeforeEach(func() {
    41  		configRepo = testconfig.NewRepositoryWithDefaults()
    42  		configRepo.SetAccessToken("BEARER my_access_token")
    43  
    44  		gateway := net.NewCloudControllerGateway(configRepo, time.Now, new(terminalfakes.FakeUI), new(tracefakes.FakePrinter), "")
    45  		repo = NewCloudControllerServiceRepository(configRepo, gateway)
    46  	})
    47  
    48  	AfterEach(func() {
    49  		if testServer != nil {
    50  			testServer.Close()
    51  		}
    52  	})
    53  
    54  	Describe("GetAllServiceOfferings", func() {
    55  		BeforeEach(func() {
    56  			setupTestServer(
    57  				apifakes.NewCloudControllerTestRequest(testnet.TestRequest{
    58  					Method:   "GET",
    59  					Path:     "/v2/services",
    60  					Response: firstOfferingsResponse,
    61  				}),
    62  				apifakes.NewCloudControllerTestRequest(testnet.TestRequest{
    63  					Method:   "GET",
    64  					Path:     "/v2/services",
    65  					Response: multipleOfferingsResponse,
    66  				}),
    67  			)
    68  		})
    69  
    70  		It("gets all public service offerings", func() {
    71  			offerings, err := repo.GetAllServiceOfferings()
    72  
    73  			Expect(testHandler).To(HaveAllRequestsCalled())
    74  			Expect(err).NotTo(HaveOccurred())
    75  			Expect(len(offerings)).To(Equal(3))
    76  
    77  			firstOffering := offerings[0]
    78  			Expect(firstOffering.Label).To(Equal("first-Offering 1"))
    79  			Expect(firstOffering.Version).To(Equal("1.0"))
    80  			Expect(firstOffering.Description).To(Equal("first Offering 1 description"))
    81  			Expect(firstOffering.Provider).To(Equal("Offering 1 provider"))
    82  			Expect(firstOffering.GUID).To(Equal("first-offering-1-guid"))
    83  		})
    84  	})
    85  
    86  	Describe("GetServiceOfferingsForSpace", func() {
    87  		It("gets all service offerings in a given space", func() {
    88  			setupTestServer(
    89  				apifakes.NewCloudControllerTestRequest(testnet.TestRequest{
    90  					Method:   "GET",
    91  					Path:     "/v2/spaces/my-space-guid/services",
    92  					Response: firstOfferingsForSpaceResponse,
    93  				}),
    94  				apifakes.NewCloudControllerTestRequest(testnet.TestRequest{
    95  					Method:   "GET",
    96  					Path:     "/v2/spaces/my-space-guid/services",
    97  					Response: multipleOfferingsResponse,
    98  				}))
    99  
   100  			offerings, err := repo.GetServiceOfferingsForSpace("my-space-guid")
   101  
   102  			Expect(testHandler).To(HaveAllRequestsCalled())
   103  			Expect(err).NotTo(HaveOccurred())
   104  
   105  			Expect(len(offerings)).To(Equal(3))
   106  
   107  			firstOffering := offerings[0]
   108  			Expect(firstOffering.Label).To(Equal("first-Offering 1"))
   109  			Expect(firstOffering.Version).To(Equal("1.0"))
   110  			Expect(firstOffering.Description).To(Equal("first Offering 1 description"))
   111  			Expect(firstOffering.Provider).To(Equal("Offering 1 provider"))
   112  			Expect(firstOffering.GUID).To(Equal("first-offering-1-guid"))
   113  			Expect(len(firstOffering.Plans)).To(Equal(0))
   114  
   115  			secondOffering := offerings[1]
   116  			Expect(secondOffering.Label).To(Equal("Offering 1"))
   117  			Expect(secondOffering.Version).To(Equal("1.0"))
   118  			Expect(secondOffering.Description).To(Equal("Offering 1 description"))
   119  			Expect(secondOffering.Provider).To(Equal("Offering 1 provider"))
   120  			Expect(secondOffering.GUID).To(Equal("offering-1-guid"))
   121  			Expect(len(secondOffering.Plans)).To(Equal(0))
   122  		})
   123  	})
   124  
   125  	Describe("find by service broker", func() {
   126  		BeforeEach(func() {
   127  			body1 := `
   128  {
   129     "total_results": 2,
   130     "total_pages": 2,
   131     "prev_url": null,
   132     "next_url": "/v2/services?q=service_broker_guid%3Amy-service-broker-guid&page=2",
   133     "resources": [
   134        {
   135           "metadata": {
   136              "guid": "my-service-guid"
   137           },
   138           "entity": {
   139              "label": "my-service",
   140              "provider": "androsterone-ensphere",
   141              "description": "Dummy addon that is cool",
   142              "version": "damageableness-preheat"
   143           }
   144        }
   145     ]
   146  }`
   147  			body2 := `
   148  {
   149     "total_results": 1,
   150     "total_pages": 1,
   151     "next_url": null,
   152     "resources": [
   153        {
   154           "metadata": {
   155              "guid": "my-service-guid2"
   156           },
   157           "entity": {
   158              "label": "my-service2",
   159              "provider": "androsterone-ensphere",
   160              "description": "Dummy addon that is cooler",
   161              "version": "seraphine-lowdah"
   162           }
   163        }
   164     ]
   165  }`
   166  
   167  			setupTestServer(
   168  				apifakes.NewCloudControllerTestRequest(
   169  					testnet.TestRequest{
   170  						Method:   "GET",
   171  						Path:     "/v2/services?q=service_broker_guid%3Amy-service-broker-guid",
   172  						Response: testnet.TestResponse{Status: http.StatusOK, Body: body1},
   173  					}),
   174  				apifakes.NewCloudControllerTestRequest(
   175  					testnet.TestRequest{
   176  						Method:   "GET",
   177  						Path:     "/v2/services?q=service_broker_guid%3Amy-service-broker-guid",
   178  						Response: testnet.TestResponse{Status: http.StatusOK, Body: body2},
   179  					}),
   180  			)
   181  		})
   182  
   183  		It("returns the service brokers services", func() {
   184  			services, err := repo.ListServicesFromBroker("my-service-broker-guid")
   185  
   186  			Expect(err).NotTo(HaveOccurred())
   187  			Expect(testHandler).To(HaveAllRequestsCalled())
   188  			Expect(len(services)).To(Equal(2))
   189  
   190  			Expect(services[0].GUID).To(Equal("my-service-guid"))
   191  			Expect(services[1].GUID).To(Equal("my-service-guid2"))
   192  		})
   193  	})
   194  
   195  	Describe("returning services for many brokers", func() {
   196  		path1 := "/v2/services?q=service_broker_guid%20IN%20my-service-broker-guid,my-service-broker-guid2"
   197  		body1 := `
   198  {
   199     "total_results": 2,
   200     "total_pages": 2,
   201     "prev_url": null,
   202  	 "next_url": "/v2/services?q=service_broker_guid%20IN%20my-service-broker-guid,my-service-broker-guid2&page=2",
   203     "resources": [
   204       {
   205           "metadata": {
   206              "guid": "my-service-guid"
   207           },
   208           "entity": {
   209              "label": "my-service",
   210              "provider": "androsterone-ensphere",
   211              "description": "Dummy addon that is cool",
   212              "version": "damageableness-preheat"
   213           }
   214  			 }
   215     ]
   216  }`
   217  		path2 := "/v2/services?q=service_broker_guid%20IN%20my-service-broker-guid,my-service-broker-guid2&page=2"
   218  		body2 := `
   219  {
   220     "total_results": 2,
   221     "total_pages": 2,
   222     "prev_url": "/v2/services?q=service_broker_guid%20IN%20my-service-broker-guid,my-service-broker-guid2",
   223  	 "next_url": null,
   224     "resources": [
   225        {
   226           "metadata": {
   227              "guid": "my-service-guid2"
   228           },
   229           "entity": {
   230              "label": "my-service2",
   231              "provider": "androsterone-ensphere",
   232              "description": "Dummy addon that is cool",
   233              "version": "damageableness-preheat"
   234           }
   235        }
   236     ]
   237  }`
   238  		BeforeEach(func() {
   239  			setupTestServer(
   240  				apifakes.NewCloudControllerTestRequest(
   241  					testnet.TestRequest{
   242  						Method:   "GET",
   243  						Path:     path1,
   244  						Response: testnet.TestResponse{Status: http.StatusOK, Body: body1},
   245  					}),
   246  				apifakes.NewCloudControllerTestRequest(
   247  					testnet.TestRequest{
   248  						Method:   "GET",
   249  						Path:     path2,
   250  						Response: testnet.TestResponse{Status: http.StatusOK, Body: body2},
   251  					}),
   252  			)
   253  		})
   254  
   255  		It("returns the service brokers services", func() {
   256  			brokerGUIDs := []string{"my-service-broker-guid", "my-service-broker-guid2"}
   257  			services, err := repo.ListServicesFromManyBrokers(brokerGUIDs)
   258  
   259  			Expect(err).NotTo(HaveOccurred())
   260  			Expect(testHandler).To(HaveAllRequestsCalled())
   261  			Expect(len(services)).To(Equal(2))
   262  
   263  			Expect(services[0].GUID).To(Equal("my-service-guid"))
   264  			Expect(services[1].GUID).To(Equal("my-service-guid2"))
   265  		})
   266  	})
   267  
   268  	Describe("creating a service instance", func() {
   269  		It("makes the right request", func() {
   270  			setupTestServer(apifakes.NewCloudControllerTestRequest(testnet.TestRequest{
   271  				Method:   "POST",
   272  				Path:     "/v2/service_instances?accepts_incomplete=true",
   273  				Matcher:  testnet.RequestBodyMatcher(`{"name":"instance-name","service_plan_guid":"plan-guid","space_guid":"my-space-guid"}`),
   274  				Response: testnet.TestResponse{Status: http.StatusCreated},
   275  			}))
   276  
   277  			err := repo.CreateServiceInstance("instance-name", "plan-guid", nil, nil)
   278  			Expect(testHandler).To(HaveAllRequestsCalled())
   279  			Expect(err).NotTo(HaveOccurred())
   280  		})
   281  
   282  		Context("when there are parameters", func() {
   283  			It("sends the parameters as part of the request body", func() {
   284  				setupTestServer(apifakes.NewCloudControllerTestRequest(testnet.TestRequest{
   285  					Method:   "POST",
   286  					Path:     "/v2/service_instances?accepts_incomplete=true",
   287  					Matcher:  testnet.RequestBodyMatcher(`{"name":"instance-name","service_plan_guid":"plan-guid","space_guid":"my-space-guid","parameters": {"data": "hello"}}`),
   288  					Response: testnet.TestResponse{Status: http.StatusCreated},
   289  				}))
   290  
   291  				paramsMap := make(map[string]interface{})
   292  				paramsMap["data"] = "hello"
   293  
   294  				err := repo.CreateServiceInstance("instance-name", "plan-guid", paramsMap, nil)
   295  				Expect(testHandler).To(HaveAllRequestsCalled())
   296  				Expect(err).NotTo(HaveOccurred())
   297  			})
   298  
   299  			Context("and there is a failure during serialization", func() {
   300  				It("returns the serialization error", func() {
   301  					paramsMap := make(map[string]interface{})
   302  					paramsMap["data"] = make(chan bool)
   303  
   304  					err := repo.CreateServiceInstance("instance-name", "plan-guid", paramsMap, nil)
   305  					Expect(err).To(MatchError("json: unsupported type: chan bool"))
   306  				})
   307  			})
   308  		})
   309  
   310  		Context("when there are tags", func() {
   311  			It("sends the tags as part of the request body", func() {
   312  				setupTestServer(apifakes.NewCloudControllerTestRequest(testnet.TestRequest{
   313  					Method:   "POST",
   314  					Path:     "/v2/service_instances?accepts_incomplete=true",
   315  					Matcher:  testnet.RequestBodyMatcher(`{"name":"instance-name","service_plan_guid":"plan-guid","space_guid":"my-space-guid","tags": ["foo", "bar"]}`),
   316  					Response: testnet.TestResponse{Status: http.StatusCreated},
   317  				}))
   318  
   319  				tags := []string{"foo", "bar"}
   320  
   321  				err := repo.CreateServiceInstance("instance-name", "plan-guid", nil, tags)
   322  				Expect(testHandler).To(HaveAllRequestsCalled())
   323  				Expect(err).NotTo(HaveOccurred())
   324  			})
   325  		})
   326  
   327  		Context("when the name is taken but an identical service exists", func() {
   328  			BeforeEach(func() {
   329  				setupTestServer(
   330  					apifakes.NewCloudControllerTestRequest(testnet.TestRequest{
   331  						Method:  "POST",
   332  						Path:    "/v2/service_instances?accepts_incomplete=true",
   333  						Matcher: testnet.RequestBodyMatcher(`{"name":"my-service","service_plan_guid":"plan-guid","space_guid":"my-space-guid"}`),
   334  						Response: testnet.TestResponse{
   335  							Status: http.StatusBadRequest,
   336  							Body:   `{"code":60002,"description":"The service instance name is taken: my-service"}`,
   337  						}}),
   338  					findServiceInstanceReq,
   339  					serviceOfferingReq)
   340  			})
   341  
   342  			It("returns a ModelAlreadyExistsError if the plan is the same", func() {
   343  				err := repo.CreateServiceInstance("my-service", "plan-guid", nil, nil)
   344  				Expect(testHandler).To(HaveAllRequestsCalled())
   345  				Expect(err).To(BeAssignableToTypeOf(&errors.ModelAlreadyExistsError{}))
   346  			})
   347  		})
   348  
   349  		Context("when the name is taken and no identical service instance exists", func() {
   350  			BeforeEach(func() {
   351  				setupTestServer(
   352  					apifakes.NewCloudControllerTestRequest(testnet.TestRequest{
   353  						Method:  "POST",
   354  						Path:    "/v2/service_instances?accepts_incomplete=true",
   355  						Matcher: testnet.RequestBodyMatcher(`{"name":"my-service","service_plan_guid":"different-plan-guid","space_guid":"my-space-guid"}`),
   356  						Response: testnet.TestResponse{
   357  							Status: http.StatusBadRequest,
   358  							Body:   `{"code":60002,"description":"The service instance name is taken: my-service"}`,
   359  						}}),
   360  					findServiceInstanceReq,
   361  					serviceOfferingReq)
   362  			})
   363  
   364  			It("fails if the plan is different", func() {
   365  				err := repo.CreateServiceInstance("my-service", "different-plan-guid", nil, nil)
   366  
   367  				Expect(testHandler).To(HaveAllRequestsCalled())
   368  				Expect(err).To(HaveOccurred())
   369  				Expect(err).To(BeAssignableToTypeOf(errors.NewHTTPError(400, "", "")))
   370  			})
   371  		})
   372  	})
   373  
   374  	Describe("UpdateServiceInstance", func() {
   375  		It("makes the right request", func() {
   376  			setupTestServer(apifakes.NewCloudControllerTestRequest(testnet.TestRequest{
   377  				Method:   "PUT",
   378  				Path:     "/v2/service_instances/instance-guid?accepts_incomplete=true",
   379  				Matcher:  testnet.RequestBodyMatcher(`{"service_plan_guid":"plan-guid"}`),
   380  				Response: testnet.TestResponse{Status: http.StatusOK},
   381  			}))
   382  
   383  			err := repo.UpdateServiceInstance("instance-guid", "plan-guid", nil, nil)
   384  			Expect(testHandler).To(HaveAllRequestsCalled())
   385  			Expect(err).NotTo(HaveOccurred())
   386  		})
   387  
   388  		Context("When the instance or plan is not found", func() {
   389  			It("fails", func() {
   390  				setupTestServer(apifakes.NewCloudControllerTestRequest(testnet.TestRequest{
   391  					Method:   "PUT",
   392  					Path:     "/v2/service_instances/instance-guid?accepts_incomplete=true",
   393  					Matcher:  testnet.RequestBodyMatcher(`{"service_plan_guid":"plan-guid"}`),
   394  					Response: testnet.TestResponse{Status: http.StatusNotFound},
   395  				}))
   396  
   397  				err := repo.UpdateServiceInstance("instance-guid", "plan-guid", nil, nil)
   398  
   399  				Expect(testHandler).To(HaveAllRequestsCalled())
   400  				Expect(err).To(HaveOccurred())
   401  			})
   402  		})
   403  
   404  		Context("when the user passes arbitrary params", func() {
   405  			It("passes the parameters in the correct field for the request", func() {
   406  				setupTestServer(apifakes.NewCloudControllerTestRequest(testnet.TestRequest{
   407  					Method:   "PUT",
   408  					Path:     "/v2/service_instances/instance-guid?accepts_incomplete=true",
   409  					Matcher:  testnet.RequestBodyMatcher(`{"parameters": {"foo": "bar"}}`),
   410  					Response: testnet.TestResponse{Status: http.StatusOK},
   411  				}))
   412  
   413  				paramsMap := map[string]interface{}{"foo": "bar"}
   414  
   415  				err := repo.UpdateServiceInstance("instance-guid", "", paramsMap, nil)
   416  				Expect(testHandler).To(HaveAllRequestsCalled())
   417  				Expect(err).NotTo(HaveOccurred())
   418  			})
   419  
   420  			Context("and there is a failure during serialization", func() {
   421  				It("returns the serialization error", func() {
   422  					paramsMap := make(map[string]interface{})
   423  					paramsMap["data"] = make(chan bool)
   424  
   425  					err := repo.UpdateServiceInstance("instance-guid", "", paramsMap, nil)
   426  					Expect(err).To(MatchError("json: unsupported type: chan bool"))
   427  				})
   428  			})
   429  		})
   430  
   431  		Context("when there are tags", func() {
   432  			It("sends the tags as part of the request body", func() {
   433  				setupTestServer(apifakes.NewCloudControllerTestRequest(testnet.TestRequest{
   434  					Method:   "PUT",
   435  					Path:     "/v2/service_instances/instance-guid?accepts_incomplete=true",
   436  					Matcher:  testnet.RequestBodyMatcher(`{"tags": ["foo", "bar"]}`),
   437  					Response: testnet.TestResponse{Status: http.StatusOK},
   438  				}))
   439  
   440  				tags := &[]string{"foo", "bar"}
   441  
   442  				err := repo.UpdateServiceInstance("instance-guid", "", nil, tags)
   443  				Expect(testHandler).To(HaveAllRequestsCalled())
   444  				Expect(err).NotTo(HaveOccurred())
   445  			})
   446  
   447  			It("sends empty tags", func() {
   448  				setupTestServer(apifakes.NewCloudControllerTestRequest(testnet.TestRequest{
   449  					Method:   "PUT",
   450  					Path:     "/v2/service_instances/instance-guid?accepts_incomplete=true",
   451  					Matcher:  testnet.RequestBodyMatcher(`{"tags": []}`),
   452  					Response: testnet.TestResponse{Status: http.StatusOK},
   453  				}))
   454  
   455  				tags := &[]string{}
   456  
   457  				err := repo.UpdateServiceInstance("instance-guid", "", nil, tags)
   458  				Expect(testHandler).To(HaveAllRequestsCalled())
   459  				Expect(err).NotTo(HaveOccurred())
   460  			})
   461  		})
   462  	})
   463  
   464  	Describe("finding service instances by name", func() {
   465  		It("returns the service instance", func() {
   466  			setupTestServer(findServiceInstanceReq, serviceOfferingReq)
   467  
   468  			instance, err := repo.FindInstanceByName("my-service")
   469  
   470  			Expect(testHandler).To(HaveAllRequestsCalled())
   471  			Expect(err).NotTo(HaveOccurred())
   472  
   473  			Expect(instance.Name).To(Equal("my-service"))
   474  			Expect(instance.GUID).To(Equal("my-service-instance-guid"))
   475  			Expect(instance.DashboardURL).To(Equal("my-dashboard-url"))
   476  			Expect(instance.ServiceOffering.Label).To(Equal("mysql"))
   477  			Expect(instance.ServiceOffering.DocumentationURL).To(Equal("http://info.example.com"))
   478  			Expect(instance.ServiceOffering.Description).To(Equal("MySQL database"))
   479  			Expect(instance.ServiceOffering.Requires).To(ContainElement("route_forwarding"))
   480  			Expect(instance.ServicePlan.Name).To(Equal("plan-name"))
   481  			Expect(len(instance.ServiceBindings)).To(Equal(2))
   482  
   483  			binding := instance.ServiceBindings[0]
   484  			Expect(binding.URL).To(Equal("/v2/service_bindings/service-binding-1-guid"))
   485  			Expect(binding.GUID).To(Equal("service-binding-1-guid"))
   486  			Expect(binding.AppGUID).To(Equal("app-1-guid"))
   487  		})
   488  
   489  		It("returns user provided services", func() {
   490  			setupTestServer(apifakes.NewCloudControllerTestRequest(testnet.TestRequest{
   491  				Method: "GET",
   492  				Path:   "/v2/spaces/my-space-guid/service_instances?return_user_provided_service_instances=true&q=name%3Amy-service",
   493  				Response: testnet.TestResponse{Status: http.StatusOK, Body: `
   494  				{
   495  					"resources": [
   496  						{
   497  						  "metadata": {
   498  							"guid": "my-service-instance-guid"
   499  						  },
   500  						  "entity": {
   501  							"name": "my-service",
   502  							"service_bindings": [
   503  							  {
   504  								"metadata": {
   505  								  "guid": "service-binding-1-guid",
   506  								  "url": "/v2/service_bindings/service-binding-1-guid"
   507  								},
   508  								"entity": {
   509  								  "app_guid": "app-1-guid"
   510  								}
   511  							  },
   512  							  {
   513  								"metadata": {
   514  								  "guid": "service-binding-2-guid",
   515  								  "url": "/v2/service_bindings/service-binding-2-guid"
   516  								},
   517  								"entity": {
   518  								  "app_guid": "app-2-guid"
   519  								}
   520  							  }
   521  							],
   522  							"service_plan_guid": null
   523  						  }
   524  						}
   525  					]
   526  				}`}}))
   527  
   528  			instance, err := repo.FindInstanceByName("my-service")
   529  
   530  			Expect(testHandler).To(HaveAllRequestsCalled())
   531  			Expect(err).NotTo(HaveOccurred())
   532  
   533  			Expect(instance.Name).To(Equal("my-service"))
   534  			Expect(instance.GUID).To(Equal("my-service-instance-guid"))
   535  			Expect(instance.ServiceOffering.Label).To(Equal(""))
   536  			Expect(instance.ServicePlan.Name).To(Equal(""))
   537  			Expect(len(instance.ServiceBindings)).To(Equal(2))
   538  
   539  			binding := instance.ServiceBindings[0]
   540  			Expect(binding.URL).To(Equal("/v2/service_bindings/service-binding-1-guid"))
   541  			Expect(binding.GUID).To(Equal("service-binding-1-guid"))
   542  			Expect(binding.AppGUID).To(Equal("app-1-guid"))
   543  		})
   544  
   545  		It("returns a failure response when the instance doesn't exist", func() {
   546  			setupTestServer(apifakes.NewCloudControllerTestRequest(testnet.TestRequest{
   547  				Method:   "GET",
   548  				Path:     "/v2/spaces/my-space-guid/service_instances?return_user_provided_service_instances=true&q=name%3Amy-service",
   549  				Response: testnet.TestResponse{Status: http.StatusOK, Body: `{ "resources": [] }`},
   550  			}))
   551  
   552  			_, err := repo.FindInstanceByName("my-service")
   553  
   554  			Expect(testHandler).To(HaveAllRequestsCalled())
   555  			Expect(err).To(BeAssignableToTypeOf(&errors.ModelNotFoundError{}))
   556  		})
   557  
   558  		It("should not fail to parse when extra is null", func() {
   559  			setupTestServer(findServiceInstanceReq, serviceOfferingNullExtraReq)
   560  
   561  			_, err := repo.FindInstanceByName("my-service")
   562  
   563  			Expect(testHandler).To(HaveAllRequestsCalled())
   564  			Expect(err).NotTo(HaveOccurred())
   565  		})
   566  	})
   567  
   568  	Describe("DeleteService", func() {
   569  		It("deletes the service when no apps and keys are bound", func() {
   570  			setupTestServer(apifakes.NewCloudControllerTestRequest(testnet.TestRequest{
   571  				Method:   "DELETE",
   572  				Path:     "/v2/service_instances/my-service-instance-guid?accepts_incomplete=true&async=true",
   573  				Response: testnet.TestResponse{Status: http.StatusOK},
   574  			}))
   575  
   576  			serviceInstance := models.ServiceInstance{}
   577  			serviceInstance.GUID = "my-service-instance-guid"
   578  
   579  			err := repo.DeleteService(serviceInstance)
   580  			Expect(testHandler).To(HaveAllRequestsCalled())
   581  			Expect(err).NotTo(HaveOccurred())
   582  		})
   583  
   584  		It("doesn't delete the service when apps are bound", func() {
   585  			setupTestServer()
   586  
   587  			serviceInstance := models.ServiceInstance{}
   588  			serviceInstance.GUID = "my-service-instance-guid"
   589  			serviceInstance.ServiceBindings = []models.ServiceBindingFields{
   590  				{
   591  					URL:     "/v2/service_bindings/service-binding-1-guid",
   592  					AppGUID: "app-1-guid",
   593  				},
   594  				{
   595  					URL:     "/v2/service_bindings/service-binding-2-guid",
   596  					AppGUID: "app-2-guid",
   597  				},
   598  			}
   599  
   600  			err := repo.DeleteService(serviceInstance)
   601  			Expect(err).To(HaveOccurred())
   602  			Expect(err).To(BeAssignableToTypeOf(&errors.ServiceAssociationError{}))
   603  		})
   604  
   605  		It("doesn't delete the service when keys are bound", func() {
   606  			setupTestServer()
   607  
   608  			serviceInstance := models.ServiceInstance{}
   609  			serviceInstance.GUID = "my-service-instance-guid"
   610  			serviceInstance.ServiceKeys = []models.ServiceKeyFields{
   611  				{
   612  					Name: "fake-service-key-1",
   613  					URL:  "/v2/service_keys/service-key-1-guid",
   614  					GUID: "service-key-1-guid",
   615  				},
   616  				{
   617  					Name: "fake-service-key-2",
   618  					URL:  "/v2/service_keys/service-key-2-guid",
   619  					GUID: "service-key-2-guid",
   620  				},
   621  			}
   622  
   623  			err := repo.DeleteService(serviceInstance)
   624  			Expect(err).To(HaveOccurred())
   625  			Expect(err).To(BeAssignableToTypeOf(&errors.ServiceAssociationError{}))
   626  		})
   627  	})
   628  
   629  	Describe("RenameService", func() {
   630  		Context("when the service is not user provided", func() {
   631  
   632  			BeforeEach(func() {
   633  				setupTestServer(apifakes.NewCloudControllerTestRequest(testnet.TestRequest{
   634  					Method:   "PUT",
   635  					Path:     "/v2/service_instances/my-service-instance-guid?accepts_incomplete=true",
   636  					Matcher:  testnet.RequestBodyMatcher(`{"name":"new-name"}`),
   637  					Response: testnet.TestResponse{Status: http.StatusCreated},
   638  				}))
   639  			})
   640  
   641  			It("renames the service", func() {
   642  				serviceInstance := models.ServiceInstance{}
   643  				serviceInstance.GUID = "my-service-instance-guid"
   644  				serviceInstance.ServicePlan = models.ServicePlanFields{
   645  					GUID: "some-plan-guid",
   646  				}
   647  
   648  				err := repo.RenameService(serviceInstance, "new-name")
   649  				Expect(testHandler).To(HaveAllRequestsCalled())
   650  				Expect(err).NotTo(HaveOccurred())
   651  			})
   652  		})
   653  
   654  		Context("when the service is user provided", func() {
   655  			BeforeEach(func() {
   656  				setupTestServer(apifakes.NewCloudControllerTestRequest(testnet.TestRequest{
   657  					Method:   "PUT",
   658  					Path:     "/v2/user_provided_service_instances/my-service-instance-guid",
   659  					Matcher:  testnet.RequestBodyMatcher(`{"name":"new-name"}`),
   660  					Response: testnet.TestResponse{Status: http.StatusCreated},
   661  				}))
   662  			})
   663  
   664  			It("renames the service", func() {
   665  				serviceInstance := models.ServiceInstance{}
   666  				serviceInstance.GUID = "my-service-instance-guid"
   667  				serviceInstance.Type = "user_provided_service_instance"
   668  
   669  				err := repo.RenameService(serviceInstance, "new-name")
   670  				Expect(testHandler).To(HaveAllRequestsCalled())
   671  				Expect(err).NotTo(HaveOccurred())
   672  			})
   673  		})
   674  	})
   675  
   676  	Describe("FindServiceOfferingByLabelAndProvider", func() {
   677  		Context("when the service offering can be found", func() {
   678  			BeforeEach(func() {
   679  				setupTestServer(testnet.TestRequest{
   680  					Method: "GET",
   681  					Path:   fmt.Sprintf("/v2/services?q=%s", url.QueryEscape("label:offering-1;provider:provider-1")),
   682  					Response: testnet.TestResponse{
   683  						Status: 200,
   684  						Body: `
   685  						{
   686  							"next_url": null,
   687  							"resources": [
   688  								{
   689  								  "metadata": {
   690  									"guid": "offering-1-guid"
   691  								  },
   692  								  "entity": {
   693  									"label": "offering-1",
   694  									"provider": "provider-1",
   695  									"description": "offering 1 description",
   696  									"version" : "1.0",
   697  									"service_plans": []
   698  								  }
   699  								}
   700  							]
   701  						}`}})
   702  			})
   703  
   704  			It("finds service offerings by label and provider", func() {
   705  				offering, err := repo.FindServiceOfferingByLabelAndProvider("offering-1", "provider-1")
   706  				Expect(offering.GUID).To(Equal("offering-1-guid"))
   707  				Expect(err).NotTo(HaveOccurred())
   708  			})
   709  		})
   710  
   711  		Context("when the service offering cannot be found", func() {
   712  			BeforeEach(func() {
   713  				setupTestServer(testnet.TestRequest{
   714  					Method: "GET",
   715  					Path:   fmt.Sprintf("/v2/services?q=%s", url.QueryEscape("label:offering-1;provider:provider-1")),
   716  					Response: testnet.TestResponse{
   717  						Status: 200,
   718  						Body: `
   719  						{
   720  							"next_url": null,
   721  							"resources": []
   722  						}`,
   723  					},
   724  				})
   725  			})
   726  			It("returns a ModelNotFoundError", func() {
   727  				offering, err := repo.FindServiceOfferingByLabelAndProvider("offering-1", "provider-1")
   728  
   729  				Expect(err).To(BeAssignableToTypeOf(&errors.ModelNotFoundError{}))
   730  				Expect(offering.GUID).To(Equal(""))
   731  			})
   732  		})
   733  
   734  		It("handles api errors when finding service offerings", func() {
   735  			setupTestServer(testnet.TestRequest{
   736  				Method: "GET",
   737  				Path:   fmt.Sprintf("/v2/services?q=%s", url.QueryEscape("label:offering-1;provider:provider-1")),
   738  				Response: testnet.TestResponse{
   739  					Status: 400,
   740  					Body: `
   741  					{
   742              			"code": 10005,
   743              			"description": "The query parameter is invalid"
   744  					}`}})
   745  
   746  			_, err := repo.FindServiceOfferingByLabelAndProvider("offering-1", "provider-1")
   747  			Expect(err).To(HaveOccurred())
   748  			Expect(err.(errors.HTTPError).ErrorCode()).To(Equal("10005"))
   749  		})
   750  	})
   751  
   752  	Describe("FindServiceOfferingsByLabel", func() {
   753  		Context("when the service offering can be found", func() {
   754  			BeforeEach(func() {
   755  				setupTestServer(testnet.TestRequest{
   756  					Method: "GET",
   757  					Path:   fmt.Sprintf("/v2/services?q=%s", url.QueryEscape("label:offering-1")),
   758  					Response: testnet.TestResponse{
   759  						Status: 200,
   760  						Body: `
   761  						{
   762  							"next_url": null,
   763  							"resources": [
   764  								{
   765  								  "metadata": {
   766  									"guid": "offering-1-guid"
   767  								  },
   768  								  "entity": {
   769  									"label": "offering-1",
   770  									"provider": "provider-1",
   771  									"description": "offering 1 description",
   772  									"version" : "1.0",
   773  									"service_plans": [],
   774                    "service_broker_guid": "broker-1-guid"
   775  								  }
   776  								}
   777  							]
   778  						}`}})
   779  			})
   780  
   781  			It("finds service offerings by label", func() {
   782  				offerings, err := repo.FindServiceOfferingsByLabel("offering-1")
   783  				Expect(offerings[0].GUID).To(Equal("offering-1-guid"))
   784  				Expect(offerings[0].Label).To(Equal("offering-1"))
   785  				Expect(offerings[0].Provider).To(Equal("provider-1"))
   786  				Expect(offerings[0].Description).To(Equal("offering 1 description"))
   787  				Expect(offerings[0].Version).To(Equal("1.0"))
   788  				Expect(offerings[0].BrokerGUID).To(Equal("broker-1-guid"))
   789  				Expect(err).NotTo(HaveOccurred())
   790  			})
   791  		})
   792  
   793  		Context("when the service offering cannot be found", func() {
   794  			BeforeEach(func() {
   795  				setupTestServer(testnet.TestRequest{
   796  					Method: "GET",
   797  					Path:   fmt.Sprintf("/v2/services?q=%s", url.QueryEscape("label:offering-1")),
   798  					Response: testnet.TestResponse{
   799  						Status: 200,
   800  						Body: `
   801  						{
   802  							"next_url": null,
   803  							"resources": []
   804  						}`,
   805  					},
   806  				})
   807  			})
   808  
   809  			It("returns a ModelNotFoundError", func() {
   810  				offerings, err := repo.FindServiceOfferingsByLabel("offering-1")
   811  
   812  				Expect(err).To(BeAssignableToTypeOf(&errors.ModelNotFoundError{}))
   813  				Expect(offerings).To(Equal(models.ServiceOfferings{}))
   814  			})
   815  		})
   816  
   817  		It("handles api errors when finding service offerings", func() {
   818  			setupTestServer(testnet.TestRequest{
   819  				Method: "GET",
   820  				Path:   fmt.Sprintf("/v2/services?q=%s", url.QueryEscape("label:offering-1")),
   821  				Response: testnet.TestResponse{
   822  					Status: 400,
   823  					Body: `
   824  					{
   825              			"code": 10005,
   826              			"description": "The query parameter is invalid"
   827  					}`}})
   828  
   829  			_, err := repo.FindServiceOfferingsByLabel("offering-1")
   830  			Expect(err).To(HaveOccurred())
   831  			Expect(err.(errors.HTTPError).ErrorCode()).To(Equal("10005"))
   832  		})
   833  	})
   834  
   835  	Describe("GetServiceOfferingByGUID", func() {
   836  		Context("when the service offering can be found", func() {
   837  			BeforeEach(func() {
   838  				setupTestServer(testnet.TestRequest{
   839  					Method: "GET",
   840  					Path:   fmt.Sprintf("/v2/services/offering-1-guid"),
   841  					Response: testnet.TestResponse{
   842  						Status: 200,
   843  						Body: `
   844  								{
   845  								  "metadata": {
   846  									"guid": "offering-1-guid"
   847  								  },
   848  								  "entity": {
   849  									"label": "offering-1",
   850  									"provider": "provider-1",
   851  									"description": "offering 1 description",
   852  									"version" : "1.0",
   853  									"service_plans": [],
   854                    "service_broker_guid": "broker-1-guid"
   855  								  }
   856  								}`}})
   857  			})
   858  
   859  			It("finds service offerings by guid", func() {
   860  				offering, err := repo.GetServiceOfferingByGUID("offering-1-guid")
   861  				Expect(offering.GUID).To(Equal("offering-1-guid"))
   862  				Expect(offering.Label).To(Equal("offering-1"))
   863  				Expect(offering.Provider).To(Equal("provider-1"))
   864  				Expect(offering.Description).To(Equal("offering 1 description"))
   865  				Expect(offering.Version).To(Equal("1.0"))
   866  				Expect(offering.BrokerGUID).To(Equal("broker-1-guid"))
   867  				Expect(err).NotTo(HaveOccurred())
   868  			})
   869  		})
   870  
   871  		Context("when the service offering cannot be found", func() {
   872  			BeforeEach(func() {
   873  				setupTestServer(testnet.TestRequest{
   874  					Method: "GET",
   875  					Path:   fmt.Sprintf("/v2/services/offering-1-guid"),
   876  					Response: testnet.TestResponse{
   877  						Status: 404,
   878  						Body: `
   879  						{
   880  							"code": 120003,
   881  							"description": "The service could not be found: offering-1-guid",
   882                "error_code": "CF-ServiceNotFound"
   883  						}`,
   884  					},
   885  				})
   886  			})
   887  
   888  			It("returns a ModelNotFoundError", func() {
   889  				offering, err := repo.GetServiceOfferingByGUID("offering-1-guid")
   890  
   891  				Expect(err).To(BeAssignableToTypeOf(&errors.HTTPNotFoundError{}))
   892  				Expect(offering.GUID).To(Equal(""))
   893  			})
   894  		})
   895  	})
   896  
   897  	Describe("PurgeServiceOffering", func() {
   898  		It("purges service offerings", func() {
   899  			setupTestServer(testnet.TestRequest{
   900  				Method: "DELETE",
   901  				Path:   "/v2/services/the-service-guid?purge=true",
   902  				Response: testnet.TestResponse{
   903  					Status: 204,
   904  				}})
   905  
   906  			offering := models.ServiceOffering{ServiceOfferingFields: models.ServiceOfferingFields{
   907  				Label:       "the-offering",
   908  				GUID:        "the-service-guid",
   909  				Description: "some service description",
   910  			}}
   911  			offering.GUID = "the-service-guid"
   912  
   913  			err := repo.PurgeServiceOffering(offering)
   914  			Expect(err).NotTo(HaveOccurred())
   915  			Expect(testHandler).To(HaveAllRequestsCalled())
   916  		})
   917  	})
   918  
   919  	Describe("PurgeServiceInstance", func() {
   920  		It("purges service instances", func() {
   921  			setupTestServer(testnet.TestRequest{
   922  				Method: "DELETE",
   923  				Path:   "/v2/service_instances/instance-guid?purge=true",
   924  				Response: testnet.TestResponse{
   925  					Status: 204,
   926  				}})
   927  
   928  			instance := models.ServiceInstance{ServiceInstanceFields: models.ServiceInstanceFields{
   929  				Name: "schrodinger",
   930  				GUID: "instance-guid",
   931  			}}
   932  
   933  			err := repo.PurgeServiceInstance(instance)
   934  			Expect(err).NotTo(HaveOccurred())
   935  			Expect(testHandler).To(HaveAllRequestsCalled())
   936  		})
   937  	})
   938  
   939  	Describe("getting the count of service instances for a service plan", func() {
   940  		var planGUID = "abc123"
   941  
   942  		It("returns the number of service instances", func() {
   943  			setupTestServer(apifakes.NewCloudControllerTestRequest(testnet.TestRequest{
   944  				Method: "GET",
   945  				Path:   fmt.Sprintf("/v2/service_plans/%s/service_instances?results-per-page=1", planGUID),
   946  				Response: testnet.TestResponse{Status: http.StatusOK, Body: `
   947                      {
   948                        "total_results": 9,
   949                        "total_pages": 9,
   950                        "prev_url": null,
   951                        "next_url": "/v2/service_plans/abc123/service_instances?page=2&results-per-page=1",
   952                        "resources": [
   953                          {
   954                            "metadata": {
   955                              "guid": "def456",
   956                              "url": "/v2/service_instances/def456",
   957                              "created_at": "2013-06-06T02:42:55+00:00",
   958                              "updated_at": null
   959                            },
   960                            "entity": {
   961                              "name": "pet-db",
   962                              "credentials": { "name": "the_name" },
   963                              "service_plan_guid": "abc123",
   964                              "space_guid": "ghi789",
   965                              "dashboard_url": "https://example.com/dashboard",
   966                              "type": "managed_service_instance",
   967                              "space_url": "/v2/spaces/ghi789",
   968                              "service_plan_url": "/v2/service_plans/abc123",
   969                              "service_bindings_url": "/v2/service_instances/def456/service_bindings"
   970                            }
   971                          }
   972                        ]
   973                      }
   974                  `},
   975  			}))
   976  
   977  			count, err := repo.GetServiceInstanceCountForServicePlan(planGUID)
   978  			Expect(count).To(Equal(9))
   979  			Expect(err).NotTo(HaveOccurred())
   980  		})
   981  
   982  		It("returns the API error when one occurs", func() {
   983  			setupTestServer(apifakes.NewCloudControllerTestRequest(testnet.TestRequest{
   984  				Method:   "GET",
   985  				Path:     fmt.Sprintf("/v2/service_plans/%s/service_instances?results-per-page=1", planGUID),
   986  				Response: testnet.TestResponse{Status: http.StatusInternalServerError},
   987  			}))
   988  
   989  			_, err := repo.GetServiceInstanceCountForServicePlan(planGUID)
   990  			Expect(err).To(HaveOccurred())
   991  		})
   992  	})
   993  
   994  	Describe("finding a service plan", func() {
   995  		var planDescription resources.ServicePlanDescription
   996  
   997  		Context("when the service is a v1 service", func() {
   998  			BeforeEach(func() {
   999  				planDescription = resources.ServicePlanDescription{
  1000  					ServiceLabel:    "v1-elephantsql",
  1001  					ServicePlanName: "v1-panda",
  1002  					ServiceProvider: "v1-elephantsql",
  1003  				}
  1004  
  1005  				setupTestServer(apifakes.NewCloudControllerTestRequest(testnet.TestRequest{
  1006  					Method: "GET",
  1007  					Path:   fmt.Sprintf("/v2/services?inline-relations-depth=1&q=%s", url.QueryEscape("label:v1-elephantsql;provider:v1-elephantsql")),
  1008  					Response: testnet.TestResponse{Status: http.StatusOK, Body: `
  1009                          {
  1010                            "resources": [
  1011                              {
  1012                                "metadata": {
  1013                                  "guid": "offering-1-guid"
  1014                                },
  1015                                "entity": {
  1016                                  "label": "v1-elephantsql",
  1017                                  "provider": "v1-elephantsql",
  1018                                  "description": "Offering 1 description",
  1019                                  "version" : "1.0",
  1020                                  "service_plans": [
  1021                                      {
  1022                                          "metadata": {"guid": "offering-1-plan-1-guid"},
  1023                                          "entity": {"name": "not-the-plan-youre-looking-for"}
  1024                                      },
  1025                                      {
  1026                                          "metadata": {"guid": "offering-1-plan-2-guid"},
  1027                                          "entity": {"name": "v1-panda"}
  1028                                      }
  1029                                  ]
  1030                                }
  1031                              }
  1032                            ]
  1033                          }`}}))
  1034  			})
  1035  
  1036  			It("returns the plan guid for a v1 plan", func() {
  1037  				guid, err := repo.FindServicePlanByDescription(planDescription)
  1038  
  1039  				Expect(guid).To(Equal("offering-1-plan-2-guid"))
  1040  				Expect(err).NotTo(HaveOccurred())
  1041  			})
  1042  		})
  1043  
  1044  		Context("when the service is a v2 service", func() {
  1045  			BeforeEach(func() {
  1046  				planDescription = resources.ServicePlanDescription{
  1047  					ServiceLabel:    "v2-elephantsql",
  1048  					ServicePlanName: "v2-panda",
  1049  				}
  1050  
  1051  				setupTestServer(apifakes.NewCloudControllerTestRequest(testnet.TestRequest{
  1052  					Method: "GET",
  1053  					Path:   fmt.Sprintf("/v2/services?inline-relations-depth=1&q=%s", url.QueryEscape("label:v2-elephantsql;provider:")),
  1054  					Response: testnet.TestResponse{Status: http.StatusOK, Body: `
  1055                          {
  1056                            "resources": [
  1057                              {
  1058                                "metadata": {
  1059                                  "guid": "offering-1-guid"
  1060                                },
  1061                                "entity": {
  1062                                  "label": "v2-elephantsql",
  1063                                  "provider": null,
  1064                                  "description": "Offering 1 description",
  1065                                  "version" : "1.0",
  1066                                  "service_plans": [
  1067                                      {
  1068                                          "metadata": {"guid": "offering-1-plan-1-guid"},
  1069                                          "entity": {"name": "not-the-plan-youre-looking-for"}
  1070                                      },
  1071                                      {
  1072                                          "metadata": {"guid": "offering-1-plan-2-guid"},
  1073                                          "entity": {"name": "v2-panda"}
  1074                                      }
  1075                                  ]
  1076                                }
  1077                              }
  1078                            ]
  1079                          }`}}))
  1080  			})
  1081  
  1082  			It("returns the plan guid for a v2 plan", func() {
  1083  				guid, err := repo.FindServicePlanByDescription(planDescription)
  1084  				Expect(err).NotTo(HaveOccurred())
  1085  				Expect(guid).To(Equal("offering-1-plan-2-guid"))
  1086  			})
  1087  		})
  1088  
  1089  		Context("when no service matches the description", func() {
  1090  			BeforeEach(func() {
  1091  				planDescription = resources.ServicePlanDescription{
  1092  					ServiceLabel:    "v2-service-label",
  1093  					ServicePlanName: "v2-plan-name",
  1094  				}
  1095  
  1096  				setupTestServer(apifakes.NewCloudControllerTestRequest(testnet.TestRequest{
  1097  					Method:   "GET",
  1098  					Path:     fmt.Sprintf("/v2/services?inline-relations-depth=1&q=%s", url.QueryEscape("label:v2-service-label;provider:")),
  1099  					Response: testnet.TestResponse{Status: http.StatusOK, Body: `{ "resources": [] }`},
  1100  				}))
  1101  			})
  1102  
  1103  			It("returns an error", func() {
  1104  				_, err := repo.FindServicePlanByDescription(planDescription)
  1105  				Expect(err).To(BeAssignableToTypeOf(&errors.ModelNotFoundError{}))
  1106  				Expect(err.Error()).To(ContainSubstring("Plan"))
  1107  				Expect(err.Error()).To(ContainSubstring("v2-service-label v2-plan-name"))
  1108  			})
  1109  		})
  1110  
  1111  		Context("when the described service has no matching plan", func() {
  1112  			BeforeEach(func() {
  1113  				planDescription = resources.ServicePlanDescription{
  1114  					ServiceLabel:    "v2-service-label",
  1115  					ServicePlanName: "v2-plan-name",
  1116  				}
  1117  
  1118  				setupTestServer(apifakes.NewCloudControllerTestRequest(testnet.TestRequest{
  1119  					Method: "GET",
  1120  					Path:   fmt.Sprintf("/v2/services?inline-relations-depth=1&q=%s", url.QueryEscape("label:v2-service-label;provider:")),
  1121  					Response: testnet.TestResponse{Status: http.StatusOK, Body: `
  1122                          {
  1123                            "resources": [
  1124                              {
  1125                                "metadata": {
  1126                                  "guid": "offering-1-guid"
  1127                                },
  1128                                "entity": {
  1129                                  "label": "v2-elephantsql",
  1130                                  "provider": null,
  1131                                  "description": "Offering 1 description",
  1132                                  "version" : "1.0",
  1133                                  "service_plans": [
  1134                                    {
  1135                                      "metadata": {"guid": "offering-1-plan-1-guid"},
  1136                                      "entity": {"name": "not-the-plan-youre-looking-for"}
  1137                                    },
  1138                                    {
  1139                                      "metadata": {"guid": "offering-1-plan-2-guid"},
  1140                                      "entity": {"name": "also-not-the-plan-youre-looking-for"}
  1141                                    }
  1142                                  ]
  1143                                }
  1144                              }
  1145                            ]
  1146                          }`}}))
  1147  			})
  1148  
  1149  			It("returns a ModelNotFoundError", func() {
  1150  				_, err := repo.FindServicePlanByDescription(planDescription)
  1151  
  1152  				Expect(err).To(BeAssignableToTypeOf(&errors.ModelNotFoundError{}))
  1153  				Expect(err.Error()).To(ContainSubstring("Plan"))
  1154  				Expect(err.Error()).To(ContainSubstring("v2-service-label v2-plan-name"))
  1155  			})
  1156  		})
  1157  
  1158  		Context("when we get an HTTP error", func() {
  1159  			BeforeEach(func() {
  1160  				planDescription = resources.ServicePlanDescription{
  1161  					ServiceLabel:    "v2-service-label",
  1162  					ServicePlanName: "v2-plan-name",
  1163  				}
  1164  
  1165  				setupTestServer(apifakes.NewCloudControllerTestRequest(testnet.TestRequest{
  1166  					Method: "GET",
  1167  					Path:   fmt.Sprintf("/v2/services?inline-relations-depth=1&q=%s", url.QueryEscape("label:v2-service-label;provider:")),
  1168  					Response: testnet.TestResponse{
  1169  						Status: http.StatusInternalServerError,
  1170  					}}))
  1171  			})
  1172  
  1173  			It("returns an error", func() {
  1174  				_, err := repo.FindServicePlanByDescription(planDescription)
  1175  
  1176  				Expect(err).To(HaveOccurred())
  1177  				Expect(err).To(BeAssignableToTypeOf(errors.NewHTTPError(500, "", "")))
  1178  			})
  1179  		})
  1180  	})
  1181  
  1182  	Describe("migrating service plans", func() {
  1183  		It("makes a request to CC to migrate the instances from v1 to v2", func() {
  1184  			setupTestServer(testnet.TestRequest{
  1185  				Method:   "PUT",
  1186  				Path:     "/v2/service_plans/v1-guid/service_instances",
  1187  				Matcher:  testnet.RequestBodyMatcher(`{"service_plan_guid":"v2-guid"}`),
  1188  				Response: testnet.TestResponse{Status: http.StatusOK, Body: `{"changed_count":3}`},
  1189  			})
  1190  
  1191  			changedCount, err := repo.MigrateServicePlanFromV1ToV2("v1-guid", "v2-guid")
  1192  			Expect(err).NotTo(HaveOccurred())
  1193  			Expect(changedCount).To(Equal(3))
  1194  		})
  1195  
  1196  		It("returns an error when migrating fails", func() {
  1197  			setupTestServer(apifakes.NewCloudControllerTestRequest(testnet.TestRequest{
  1198  				Method:   "PUT",
  1199  				Path:     "/v2/service_plans/v1-guid/service_instances",
  1200  				Matcher:  testnet.RequestBodyMatcher(`{"service_plan_guid":"v2-guid"}`),
  1201  				Response: testnet.TestResponse{Status: http.StatusInternalServerError},
  1202  			}))
  1203  
  1204  			_, err := repo.MigrateServicePlanFromV1ToV2("v1-guid", "v2-guid")
  1205  			Expect(err).To(HaveOccurred())
  1206  		})
  1207  	})
  1208  
  1209  	Describe("FindServiceOfferingsForSpaceByLabel", func() {
  1210  		It("finds service offerings within a space by label", func() {
  1211  			setupTestServer(
  1212  				testnet.TestRequest{
  1213  					Method: "GET",
  1214  					Path:   fmt.Sprintf("/v2/spaces/my-space-guid/services?q=%s", url.QueryEscape("label:offering-1")),
  1215  					Response: testnet.TestResponse{
  1216  						Status: 200,
  1217  						Body: `
  1218  						{
  1219  							"next_url": "/v2/spaces/my-space-guid/services?q=label%3Aoffering-1&page=2",
  1220  							"resources": [
  1221  								{
  1222  									"metadata": {
  1223  										"guid": "offering-1-guid"
  1224  									},
  1225  									"entity": {
  1226  										"label": "offering-1",
  1227  										"provider": "provider-1",
  1228  										"description": "offering 1 description",
  1229  										"version" : "1.0"
  1230  									  }
  1231  								}
  1232  							]
  1233  						}`}},
  1234  				testnet.TestRequest{
  1235  					Method: "GET",
  1236  					Path:   fmt.Sprintf("/v2/spaces/my-space-guid/services?q=%s", url.QueryEscape("label:offering-1")),
  1237  					Response: testnet.TestResponse{
  1238  						Status: 200,
  1239  						Body: `
  1240  						{
  1241  							"next_url": null,
  1242  							"resources": [
  1243  								{
  1244  									"metadata": {
  1245  										"guid": "offering-2-guid"
  1246  									},
  1247  									"entity": {
  1248  										"label": "offering-2",
  1249  										"provider": "provider-2",
  1250  										"description": "offering 2 description",
  1251  										"version" : "1.0"
  1252  									}
  1253  								}
  1254  							]
  1255  						}`}})
  1256  
  1257  			offerings, err := repo.FindServiceOfferingsForSpaceByLabel("my-space-guid", "offering-1")
  1258  			Expect(err).ToNot(HaveOccurred())
  1259  			Expect(offerings).To(HaveLen(2))
  1260  			Expect(offerings[0].GUID).To(Equal("offering-1-guid"))
  1261  		})
  1262  
  1263  		It("returns an error if the offering cannot be found", func() {
  1264  			setupTestServer(testnet.TestRequest{
  1265  				Method: "GET",
  1266  				Path:   fmt.Sprintf("/v2/spaces/my-space-guid/services?q=%s", url.QueryEscape("label:offering-1")),
  1267  				Response: testnet.TestResponse{
  1268  					Status: http.StatusOK,
  1269  					Body: `{
  1270  						"next_url": null,
  1271  						"resources": []
  1272  					}`,
  1273  				},
  1274  			})
  1275  
  1276  			offerings, err := repo.FindServiceOfferingsForSpaceByLabel("my-space-guid", "offering-1")
  1277  			Expect(err).To(BeAssignableToTypeOf(&errors.ModelNotFoundError{}))
  1278  			Expect(offerings).To(HaveLen(0))
  1279  		})
  1280  
  1281  		It("handles api errors when finding service offerings", func() {
  1282  			setupTestServer(testnet.TestRequest{
  1283  				Method: "GET",
  1284  				Path:   fmt.Sprintf("/v2/spaces/my-space-guid/services?q=%s", url.QueryEscape("label:offering-1")),
  1285  				Response: testnet.TestResponse{
  1286  					Status: http.StatusBadRequest,
  1287  					Body: `{
  1288  						"code": 9001,
  1289  						"description": "Something Happened"
  1290  					}`,
  1291  				},
  1292  			})
  1293  
  1294  			_, err := repo.FindServiceOfferingsForSpaceByLabel("my-space-guid", "offering-1")
  1295  			Expect(err).To(BeAssignableToTypeOf(errors.NewHTTPError(400, "", "")))
  1296  		})
  1297  
  1298  		Describe("when api returns query by label is invalid", func() {
  1299  			It("makes a backwards-compatible request", func() {
  1300  				failedRequestByQueryLabel := testnet.TestRequest{
  1301  					Method: "GET",
  1302  					Path:   fmt.Sprintf("/v2/spaces/my-space-guid/services?q=%s", url.QueryEscape("label:my-service-offering")),
  1303  					Response: testnet.TestResponse{
  1304  						Status: http.StatusBadRequest,
  1305  						Body:   `{"code": 10005,"description": "The query parameter is invalid"}`,
  1306  					},
  1307  				}
  1308  
  1309  				firstPaginatedRequest := testnet.TestRequest{
  1310  					Method: "GET",
  1311  					Path:   fmt.Sprintf("/v2/spaces/my-space-guid/services"),
  1312  					Response: testnet.TestResponse{
  1313  						Status: http.StatusOK,
  1314  						Body: `{
  1315  							"next_url": "/v2/spaces/my-space-guid/services?page=2",
  1316  							"resources": [
  1317  								{
  1318  								  "metadata": {
  1319  									"guid": "my-service-offering-guid"
  1320  								  },
  1321  								  "entity": {
  1322  									"label": "my-service-offering",
  1323  									"provider": "some-other-provider",
  1324  									"description": "a description that does not match your provider",
  1325  									"version" : "1.0"
  1326  								  }
  1327  								}
  1328  							]
  1329  						}`,
  1330  					},
  1331  				}
  1332  
  1333  				secondPaginatedRequest := testnet.TestRequest{
  1334  					Method: "GET",
  1335  					Path:   fmt.Sprintf("/v2/spaces/my-space-guid/services?page=2"),
  1336  					Response: testnet.TestResponse{
  1337  						Status: http.StatusOK,
  1338  						Body: `{"next_url": null,
  1339  									"resources": [
  1340  										{
  1341  										  "metadata": {
  1342  											"guid": "my-service-offering-guid"
  1343  										  },
  1344  										  "entity": {
  1345  											"label": "my-service-offering",
  1346  											"provider": "my-provider",
  1347  											"description": "offering 1 description",
  1348  											"version" : "1.0"
  1349  										  }
  1350  										}
  1351  									]}`,
  1352  					},
  1353  				}
  1354  
  1355  				setupTestServer(failedRequestByQueryLabel, firstPaginatedRequest, secondPaginatedRequest)
  1356  
  1357  				serviceOfferings, err := repo.FindServiceOfferingsForSpaceByLabel("my-space-guid", "my-service-offering")
  1358  				Expect(err).NotTo(HaveOccurred())
  1359  				Expect(len(serviceOfferings)).To(Equal(2))
  1360  			})
  1361  		})
  1362  	})
  1363  })
  1364  
  1365  var firstOfferingsResponse = testnet.TestResponse{Status: http.StatusOK, Body: `
  1366  {
  1367  	"next_url": "/v2/services?page=2",
  1368  	"resources": [
  1369  	{
  1370  		"metadata": {
  1371  			"guid": "first-offering-1-guid"
  1372  		},
  1373  		"entity": {
  1374  			"label": "first-Offering 1",
  1375  			"provider": "Offering 1 provider",
  1376  			"description": "first Offering 1 description",
  1377  			"version" : "1.0"
  1378  		}
  1379  	}
  1380    ]}`,
  1381  }
  1382  
  1383  var firstOfferingsForSpaceResponse = testnet.TestResponse{Status: http.StatusOK, Body: `
  1384  {
  1385  	"next_url": "/v2/spaces/my-space-guid/services?inline-relations-depth=1&page=2",
  1386  	"resources": [
  1387  		{
  1388  			"metadata": {
  1389  				"guid": "first-offering-1-guid"
  1390  			},
  1391  			"entity": {
  1392  				"label": "first-Offering 1",
  1393  				"provider": "Offering 1 provider",
  1394  				"description": "first Offering 1 description",
  1395  				"version" : "1.0"
  1396  			}
  1397  	    }
  1398      ]}`,
  1399  }
  1400  
  1401  var multipleOfferingsResponse = testnet.TestResponse{Status: http.StatusOK, Body: `
  1402  {
  1403  	"resources": [
  1404  		{
  1405  	    	"metadata": {
  1406  	        	"guid": "offering-1-guid"
  1407  			},
  1408        		"entity": {
  1409  		        "label": "Offering 1",
  1410  		        "provider": "Offering 1 provider",
  1411  		        "description": "Offering 1 description",
  1412  		        "version" : "1.0"
  1413  			}
  1414  	    },
  1415      	{
  1416        		"metadata": {
  1417          		"guid": "offering-2-guid"
  1418  	      	},
  1419  	      	"entity": {
  1420  		        "label": "Offering 2",
  1421  		        "provider": "Offering 2 provider",
  1422  		        "description": "Offering 2 description",
  1423  		        "version" : "1.5"
  1424  	        }
  1425      	}
  1426  	]}`,
  1427  }
  1428  
  1429  var serviceOfferingReq = apifakes.NewCloudControllerTestRequest(testnet.TestRequest{
  1430  	Method: "GET",
  1431  	Path:   "/v2/services/the-service-guid",
  1432  	Response: testnet.TestResponse{Status: http.StatusOK, Body: `
  1433  		{
  1434  		  "metadata": {
  1435  			"guid": "15790581-a293-489b-9efc-847ecf1b1339"
  1436  		  },
  1437  		  "entity": {
  1438  			"label": "mysql",
  1439  			"provider": "mysql",
  1440  		    "extra": "{\"documentationURL\":\"http://info.example.com\"}",
  1441  			"description": "MySQL database",
  1442  			"requires": ["route_forwarding"]
  1443  		  }
  1444  		}`,
  1445  	}})
  1446  
  1447  var serviceOfferingNullExtraReq = apifakes.NewCloudControllerTestRequest(testnet.TestRequest{
  1448  	Method: "GET",
  1449  	Path:   "/v2/services/the-service-guid",
  1450  	Response: testnet.TestResponse{Status: http.StatusOK, Body: `
  1451  		{
  1452  		  "metadata": {
  1453  			"guid": "15790581-a293-489b-9efc-847ecf1b1339"
  1454  		  },
  1455  		  "entity": {
  1456  			"label": "mysql",
  1457  			"provider": "mysql",
  1458  		    "extra": null,
  1459  			"description": "MySQL database"
  1460  		  }
  1461  		}`,
  1462  	}})
  1463  
  1464  var findServiceInstanceReq = apifakes.NewCloudControllerTestRequest(testnet.TestRequest{
  1465  	Method: "GET",
  1466  	Path:   "/v2/spaces/my-space-guid/service_instances?return_user_provided_service_instances=true&q=name%3Amy-service",
  1467  	Response: testnet.TestResponse{Status: http.StatusOK, Body: `
  1468  	{"resources": [
  1469          {
  1470            "metadata": {
  1471              "guid": "my-service-instance-guid"
  1472            },
  1473            "entity": {
  1474              "name": "my-service",
  1475  			"dashboard_url":"my-dashboard-url",
  1476              "service_bindings": [
  1477                {
  1478                  "metadata": {
  1479                    "guid": "service-binding-1-guid",
  1480                    "url": "/v2/service_bindings/service-binding-1-guid"
  1481                  },
  1482                  "entity": {
  1483                    "app_guid": "app-1-guid"
  1484                  }
  1485                },
  1486                {
  1487                  "metadata": {
  1488                    "guid": "service-binding-2-guid",
  1489                    "url": "/v2/service_bindings/service-binding-2-guid"
  1490                  },
  1491                  "entity": {
  1492                    "app_guid": "app-2-guid"
  1493                  }
  1494                }
  1495              ],
  1496              "service_plan": {
  1497                "metadata": {
  1498                  "guid": "plan-guid"
  1499                },
  1500                "entity": {
  1501                  "name": "plan-name",
  1502                  "service_guid": "the-service-guid"
  1503                }
  1504              }
  1505            }
  1506          }
  1507      ]}`}})