github.com/loggregator/cli@v6.33.1-0.20180224010324-82334f081791+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/util/testhelpers/configuration"
    19  	testnet "code.cloudfoundry.org/cli/util/testhelpers/net"
    20  
    21  	. "code.cloudfoundry.org/cli/cf/api"
    22  	. "code.cloudfoundry.org/cli/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", "tags": null}`),
   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", "tags": null}`),
   394  					Response: testnet.TestResponse{Status: http.StatusNotFound},
   395  				}))
   396  
   397  				err := repo.UpdateServiceInstance("instance-guid", "plan-guid", nil, nil)
   398  				Expect(testHandler).To(HaveAllRequestsCalled())
   399  				Expect(err).To(HaveOccurred())
   400  			})
   401  		})
   402  
   403  		Context("when the user passes arbitrary params", func() {
   404  			It("passes the parameters in the correct field for the request", func() {
   405  				setupTestServer(apifakes.NewCloudControllerTestRequest(testnet.TestRequest{
   406  					Method:   "PUT",
   407  					Path:     "/v2/service_instances/instance-guid?accepts_incomplete=true",
   408  					Matcher:  testnet.RequestBodyMatcher(`{"parameters": {"foo": "bar"}, "tags": null}`),
   409  					Response: testnet.TestResponse{Status: http.StatusOK},
   410  				}))
   411  
   412  				paramsMap := map[string]interface{}{"foo": "bar"}
   413  
   414  				err := repo.UpdateServiceInstance("instance-guid", "", paramsMap, nil)
   415  				Expect(testHandler).To(HaveAllRequestsCalled())
   416  				Expect(err).NotTo(HaveOccurred())
   417  			})
   418  
   419  			Context("and there is a failure during serialization", func() {
   420  				It("returns the serialization error", func() {
   421  					paramsMap := make(map[string]interface{})
   422  					paramsMap["data"] = make(chan bool)
   423  
   424  					err := repo.UpdateServiceInstance("instance-guid", "", paramsMap, nil)
   425  					Expect(err).To(MatchError("json: unsupported type: chan bool"))
   426  				})
   427  			})
   428  		})
   429  
   430  		Context("when there are tags", func() {
   431  			It("sends the tags as part of the request body", func() {
   432  				setupTestServer(apifakes.NewCloudControllerTestRequest(testnet.TestRequest{
   433  					Method:   "PUT",
   434  					Path:     "/v2/service_instances/instance-guid?accepts_incomplete=true",
   435  					Matcher:  testnet.RequestBodyMatcher(`{"tags": ["foo", "bar"]}`),
   436  					Response: testnet.TestResponse{Status: http.StatusOK},
   437  				}))
   438  
   439  				tags := []string{"foo", "bar"}
   440  
   441  				err := repo.UpdateServiceInstance("instance-guid", "", nil, tags)
   442  				Expect(testHandler).To(HaveAllRequestsCalled())
   443  				Expect(err).NotTo(HaveOccurred())
   444  			})
   445  
   446  			It("sends empty tags", func() {
   447  				setupTestServer(apifakes.NewCloudControllerTestRequest(testnet.TestRequest{
   448  					Method:   "PUT",
   449  					Path:     "/v2/service_instances/instance-guid?accepts_incomplete=true",
   450  					Matcher:  testnet.RequestBodyMatcher(`{"tags": []}`),
   451  					Response: testnet.TestResponse{Status: http.StatusOK},
   452  				}))
   453  
   454  				tags := []string{}
   455  
   456  				err := repo.UpdateServiceInstance("instance-guid", "", nil, tags)
   457  				Expect(testHandler).To(HaveAllRequestsCalled())
   458  				Expect(err).NotTo(HaveOccurred())
   459  			})
   460  		})
   461  	})
   462  
   463  	Describe("finding service instances by name", func() {
   464  		It("returns the service instance", func() {
   465  			setupTestServer(findServiceInstanceReq, serviceOfferingReq)
   466  
   467  			instance, err := repo.FindInstanceByName("my-service")
   468  
   469  			Expect(testHandler).To(HaveAllRequestsCalled())
   470  			Expect(err).NotTo(HaveOccurred())
   471  
   472  			Expect(instance.Name).To(Equal("my-service"))
   473  			Expect(instance.GUID).To(Equal("my-service-instance-guid"))
   474  			Expect(instance.DashboardURL).To(Equal("my-dashboard-url"))
   475  			Expect(instance.ServiceOffering.Label).To(Equal("mysql"))
   476  			Expect(instance.ServiceOffering.DocumentationURL).To(Equal("http://info.example.com"))
   477  			Expect(instance.ServiceOffering.Description).To(Equal("MySQL database"))
   478  			Expect(instance.ServiceOffering.Requires).To(ContainElement("route_forwarding"))
   479  			Expect(instance.ServicePlan.Name).To(Equal("plan-name"))
   480  			Expect(len(instance.ServiceBindings)).To(Equal(2))
   481  
   482  			binding := instance.ServiceBindings[0]
   483  			Expect(binding.URL).To(Equal("/v2/service_bindings/service-binding-1-guid"))
   484  			Expect(binding.GUID).To(Equal("service-binding-1-guid"))
   485  			Expect(binding.AppGUID).To(Equal("app-1-guid"))
   486  		})
   487  
   488  		It("returns user provided services", func() {
   489  			setupTestServer(apifakes.NewCloudControllerTestRequest(testnet.TestRequest{
   490  				Method: "GET",
   491  				Path:   "/v2/spaces/my-space-guid/service_instances?return_user_provided_service_instances=true&q=name%3Amy-service",
   492  				Response: testnet.TestResponse{Status: http.StatusOK, Body: `
   493  				{
   494  					"resources": [
   495  						{
   496  						  "metadata": {
   497  							"guid": "my-service-instance-guid"
   498  						  },
   499  						  "entity": {
   500  							"name": "my-service",
   501  							"service_bindings": [
   502  							  {
   503  								"metadata": {
   504  								  "guid": "service-binding-1-guid",
   505  								  "url": "/v2/service_bindings/service-binding-1-guid"
   506  								},
   507  								"entity": {
   508  								  "app_guid": "app-1-guid"
   509  								}
   510  							  },
   511  							  {
   512  								"metadata": {
   513  								  "guid": "service-binding-2-guid",
   514  								  "url": "/v2/service_bindings/service-binding-2-guid"
   515  								},
   516  								"entity": {
   517  								  "app_guid": "app-2-guid"
   518  								}
   519  							  }
   520  							],
   521  							"service_plan_guid": null
   522  						  }
   523  						}
   524  					]
   525  				}`}}))
   526  
   527  			instance, err := repo.FindInstanceByName("my-service")
   528  
   529  			Expect(testHandler).To(HaveAllRequestsCalled())
   530  			Expect(err).NotTo(HaveOccurred())
   531  
   532  			Expect(instance.Name).To(Equal("my-service"))
   533  			Expect(instance.GUID).To(Equal("my-service-instance-guid"))
   534  			Expect(instance.ServiceOffering.Label).To(Equal(""))
   535  			Expect(instance.ServicePlan.Name).To(Equal(""))
   536  			Expect(len(instance.ServiceBindings)).To(Equal(2))
   537  
   538  			binding := instance.ServiceBindings[0]
   539  			Expect(binding.URL).To(Equal("/v2/service_bindings/service-binding-1-guid"))
   540  			Expect(binding.GUID).To(Equal("service-binding-1-guid"))
   541  			Expect(binding.AppGUID).To(Equal("app-1-guid"))
   542  		})
   543  
   544  		It("returns a failure response when the instance doesn't exist", func() {
   545  			setupTestServer(apifakes.NewCloudControllerTestRequest(testnet.TestRequest{
   546  				Method:   "GET",
   547  				Path:     "/v2/spaces/my-space-guid/service_instances?return_user_provided_service_instances=true&q=name%3Amy-service",
   548  				Response: testnet.TestResponse{Status: http.StatusOK, Body: `{ "resources": [] }`},
   549  			}))
   550  
   551  			_, err := repo.FindInstanceByName("my-service")
   552  
   553  			Expect(testHandler).To(HaveAllRequestsCalled())
   554  			Expect(err).To(BeAssignableToTypeOf(&errors.ModelNotFoundError{}))
   555  		})
   556  
   557  		It("should not fail to parse when extra is null", func() {
   558  			setupTestServer(findServiceInstanceReq, serviceOfferingNullExtraReq)
   559  
   560  			_, err := repo.FindInstanceByName("my-service")
   561  
   562  			Expect(testHandler).To(HaveAllRequestsCalled())
   563  			Expect(err).NotTo(HaveOccurred())
   564  		})
   565  	})
   566  
   567  	Describe("DeleteService", func() {
   568  		It("deletes the service when no apps and keys are bound", func() {
   569  			setupTestServer(apifakes.NewCloudControllerTestRequest(testnet.TestRequest{
   570  				Method:   "DELETE",
   571  				Path:     "/v2/service_instances/my-service-instance-guid?accepts_incomplete=true&async=true",
   572  				Response: testnet.TestResponse{Status: http.StatusOK},
   573  			}))
   574  
   575  			serviceInstance := models.ServiceInstance{}
   576  			serviceInstance.GUID = "my-service-instance-guid"
   577  
   578  			err := repo.DeleteService(serviceInstance)
   579  			Expect(testHandler).To(HaveAllRequestsCalled())
   580  			Expect(err).NotTo(HaveOccurred())
   581  		})
   582  
   583  		It("doesn't delete the service when apps are bound", func() {
   584  			setupTestServer()
   585  
   586  			serviceInstance := models.ServiceInstance{}
   587  			serviceInstance.GUID = "my-service-instance-guid"
   588  			serviceInstance.ServiceBindings = []models.ServiceBindingFields{
   589  				{
   590  					URL:     "/v2/service_bindings/service-binding-1-guid",
   591  					AppGUID: "app-1-guid",
   592  				},
   593  				{
   594  					URL:     "/v2/service_bindings/service-binding-2-guid",
   595  					AppGUID: "app-2-guid",
   596  				},
   597  			}
   598  
   599  			err := repo.DeleteService(serviceInstance)
   600  			Expect(err).To(HaveOccurred())
   601  			Expect(err).To(BeAssignableToTypeOf(&errors.ServiceAssociationError{}))
   602  		})
   603  
   604  		It("doesn't delete the service when keys are bound", func() {
   605  			setupTestServer()
   606  
   607  			serviceInstance := models.ServiceInstance{}
   608  			serviceInstance.GUID = "my-service-instance-guid"
   609  			serviceInstance.ServiceKeys = []models.ServiceKeyFields{
   610  				{
   611  					Name: "fake-service-key-1",
   612  					URL:  "/v2/service_keys/service-key-1-guid",
   613  					GUID: "service-key-1-guid",
   614  				},
   615  				{
   616  					Name: "fake-service-key-2",
   617  					URL:  "/v2/service_keys/service-key-2-guid",
   618  					GUID: "service-key-2-guid",
   619  				},
   620  			}
   621  
   622  			err := repo.DeleteService(serviceInstance)
   623  			Expect(err).To(HaveOccurred())
   624  			Expect(err).To(BeAssignableToTypeOf(&errors.ServiceAssociationError{}))
   625  		})
   626  	})
   627  
   628  	Describe("RenameService", func() {
   629  		Context("when the service is not user provided", func() {
   630  
   631  			BeforeEach(func() {
   632  				setupTestServer(apifakes.NewCloudControllerTestRequest(testnet.TestRequest{
   633  					Method:   "PUT",
   634  					Path:     "/v2/service_instances/my-service-instance-guid?accepts_incomplete=true",
   635  					Matcher:  testnet.RequestBodyMatcher(`{"name":"new-name"}`),
   636  					Response: testnet.TestResponse{Status: http.StatusCreated},
   637  				}))
   638  			})
   639  
   640  			It("renames the service", func() {
   641  				serviceInstance := models.ServiceInstance{}
   642  				serviceInstance.GUID = "my-service-instance-guid"
   643  				serviceInstance.ServicePlan = models.ServicePlanFields{
   644  					GUID: "some-plan-guid",
   645  				}
   646  
   647  				err := repo.RenameService(serviceInstance, "new-name")
   648  				Expect(testHandler).To(HaveAllRequestsCalled())
   649  				Expect(err).NotTo(HaveOccurred())
   650  			})
   651  		})
   652  
   653  		Context("when the service is user provided", func() {
   654  			BeforeEach(func() {
   655  				setupTestServer(apifakes.NewCloudControllerTestRequest(testnet.TestRequest{
   656  					Method:   "PUT",
   657  					Path:     "/v2/user_provided_service_instances/my-service-instance-guid",
   658  					Matcher:  testnet.RequestBodyMatcher(`{"name":"new-name"}`),
   659  					Response: testnet.TestResponse{Status: http.StatusCreated},
   660  				}))
   661  			})
   662  
   663  			It("renames the service", func() {
   664  				serviceInstance := models.ServiceInstance{}
   665  				serviceInstance.GUID = "my-service-instance-guid"
   666  
   667  				err := repo.RenameService(serviceInstance, "new-name")
   668  				Expect(testHandler).To(HaveAllRequestsCalled())
   669  				Expect(err).NotTo(HaveOccurred())
   670  			})
   671  		})
   672  	})
   673  
   674  	Describe("FindServiceOfferingByLabelAndProvider", func() {
   675  		Context("when the service offering can be found", func() {
   676  			BeforeEach(func() {
   677  				setupTestServer(testnet.TestRequest{
   678  					Method: "GET",
   679  					Path:   fmt.Sprintf("/v2/services?q=%s", url.QueryEscape("label:offering-1;provider:provider-1")),
   680  					Response: testnet.TestResponse{
   681  						Status: 200,
   682  						Body: `
   683  						{
   684  							"next_url": null,
   685  							"resources": [
   686  								{
   687  								  "metadata": {
   688  									"guid": "offering-1-guid"
   689  								  },
   690  								  "entity": {
   691  									"label": "offering-1",
   692  									"provider": "provider-1",
   693  									"description": "offering 1 description",
   694  									"version" : "1.0",
   695  									"service_plans": []
   696  								  }
   697  								}
   698  							]
   699  						}`}})
   700  			})
   701  
   702  			It("finds service offerings by label and provider", func() {
   703  				offering, err := repo.FindServiceOfferingByLabelAndProvider("offering-1", "provider-1")
   704  				Expect(offering.GUID).To(Equal("offering-1-guid"))
   705  				Expect(err).NotTo(HaveOccurred())
   706  			})
   707  		})
   708  
   709  		Context("when the service offering cannot be found", func() {
   710  			BeforeEach(func() {
   711  				setupTestServer(testnet.TestRequest{
   712  					Method: "GET",
   713  					Path:   fmt.Sprintf("/v2/services?q=%s", url.QueryEscape("label:offering-1;provider:provider-1")),
   714  					Response: testnet.TestResponse{
   715  						Status: 200,
   716  						Body: `
   717  						{
   718  							"next_url": null,
   719  							"resources": []
   720  						}`,
   721  					},
   722  				})
   723  			})
   724  			It("returns a ModelNotFoundError", func() {
   725  				offering, err := repo.FindServiceOfferingByLabelAndProvider("offering-1", "provider-1")
   726  
   727  				Expect(err).To(BeAssignableToTypeOf(&errors.ModelNotFoundError{}))
   728  				Expect(offering.GUID).To(Equal(""))
   729  			})
   730  		})
   731  
   732  		It("handles api errors when finding service offerings", func() {
   733  			setupTestServer(testnet.TestRequest{
   734  				Method: "GET",
   735  				Path:   fmt.Sprintf("/v2/services?q=%s", url.QueryEscape("label:offering-1;provider:provider-1")),
   736  				Response: testnet.TestResponse{
   737  					Status: 400,
   738  					Body: `
   739  					{
   740              			"code": 10005,
   741              			"description": "The query parameter is invalid"
   742  					}`}})
   743  
   744  			_, err := repo.FindServiceOfferingByLabelAndProvider("offering-1", "provider-1")
   745  			Expect(err).To(HaveOccurred())
   746  			Expect(err.(errors.HTTPError).ErrorCode()).To(Equal("10005"))
   747  		})
   748  	})
   749  
   750  	Describe("FindServiceOfferingsByLabel", func() {
   751  		Context("when the service offering can be found", func() {
   752  			BeforeEach(func() {
   753  				setupTestServer(testnet.TestRequest{
   754  					Method: "GET",
   755  					Path:   fmt.Sprintf("/v2/services?q=%s", url.QueryEscape("label:offering-1")),
   756  					Response: testnet.TestResponse{
   757  						Status: 200,
   758  						Body: `
   759  						{
   760  							"next_url": null,
   761  							"resources": [
   762  								{
   763  								  "metadata": {
   764  									"guid": "offering-1-guid"
   765  								  },
   766  								  "entity": {
   767  									"label": "offering-1",
   768  									"provider": "provider-1",
   769  									"description": "offering 1 description",
   770  									"version" : "1.0",
   771  									"service_plans": [],
   772                    "service_broker_guid": "broker-1-guid"
   773  								  }
   774  								}
   775  							]
   776  						}`}})
   777  			})
   778  
   779  			It("finds service offerings by label", func() {
   780  				offerings, err := repo.FindServiceOfferingsByLabel("offering-1")
   781  				Expect(offerings[0].GUID).To(Equal("offering-1-guid"))
   782  				Expect(offerings[0].Label).To(Equal("offering-1"))
   783  				Expect(offerings[0].Provider).To(Equal("provider-1"))
   784  				Expect(offerings[0].Description).To(Equal("offering 1 description"))
   785  				Expect(offerings[0].Version).To(Equal("1.0"))
   786  				Expect(offerings[0].BrokerGUID).To(Equal("broker-1-guid"))
   787  				Expect(err).NotTo(HaveOccurred())
   788  			})
   789  		})
   790  
   791  		Context("when the service offering cannot be found", func() {
   792  			BeforeEach(func() {
   793  				setupTestServer(testnet.TestRequest{
   794  					Method: "GET",
   795  					Path:   fmt.Sprintf("/v2/services?q=%s", url.QueryEscape("label:offering-1")),
   796  					Response: testnet.TestResponse{
   797  						Status: 200,
   798  						Body: `
   799  						{
   800  							"next_url": null,
   801  							"resources": []
   802  						}`,
   803  					},
   804  				})
   805  			})
   806  
   807  			It("returns a ModelNotFoundError", func() {
   808  				offerings, err := repo.FindServiceOfferingsByLabel("offering-1")
   809  
   810  				Expect(err).To(BeAssignableToTypeOf(&errors.ModelNotFoundError{}))
   811  				Expect(offerings).To(Equal(models.ServiceOfferings{}))
   812  			})
   813  		})
   814  
   815  		It("handles api errors when finding service offerings", func() {
   816  			setupTestServer(testnet.TestRequest{
   817  				Method: "GET",
   818  				Path:   fmt.Sprintf("/v2/services?q=%s", url.QueryEscape("label:offering-1")),
   819  				Response: testnet.TestResponse{
   820  					Status: 400,
   821  					Body: `
   822  					{
   823              			"code": 10005,
   824              			"description": "The query parameter is invalid"
   825  					}`}})
   826  
   827  			_, err := repo.FindServiceOfferingsByLabel("offering-1")
   828  			Expect(err).To(HaveOccurred())
   829  			Expect(err.(errors.HTTPError).ErrorCode()).To(Equal("10005"))
   830  		})
   831  	})
   832  
   833  	Describe("GetServiceOfferingByGUID", func() {
   834  		Context("when the service offering can be found", func() {
   835  			BeforeEach(func() {
   836  				setupTestServer(testnet.TestRequest{
   837  					Method: "GET",
   838  					Path:   fmt.Sprintf("/v2/services/offering-1-guid"),
   839  					Response: testnet.TestResponse{
   840  						Status: 200,
   841  						Body: `
   842  								{
   843  								  "metadata": {
   844  									"guid": "offering-1-guid"
   845  								  },
   846  								  "entity": {
   847  									"label": "offering-1",
   848  									"provider": "provider-1",
   849  									"description": "offering 1 description",
   850  									"version" : "1.0",
   851  									"service_plans": [],
   852                    "service_broker_guid": "broker-1-guid"
   853  								  }
   854  								}`}})
   855  			})
   856  
   857  			It("finds service offerings by guid", func() {
   858  				offering, err := repo.GetServiceOfferingByGUID("offering-1-guid")
   859  				Expect(offering.GUID).To(Equal("offering-1-guid"))
   860  				Expect(offering.Label).To(Equal("offering-1"))
   861  				Expect(offering.Provider).To(Equal("provider-1"))
   862  				Expect(offering.Description).To(Equal("offering 1 description"))
   863  				Expect(offering.Version).To(Equal("1.0"))
   864  				Expect(offering.BrokerGUID).To(Equal("broker-1-guid"))
   865  				Expect(err).NotTo(HaveOccurred())
   866  			})
   867  		})
   868  
   869  		Context("when the service offering cannot be found", func() {
   870  			BeforeEach(func() {
   871  				setupTestServer(testnet.TestRequest{
   872  					Method: "GET",
   873  					Path:   fmt.Sprintf("/v2/services/offering-1-guid"),
   874  					Response: testnet.TestResponse{
   875  						Status: 404,
   876  						Body: `
   877  						{
   878  							"code": 120003,
   879  							"description": "The service could not be found: offering-1-guid",
   880                "error_code": "CF-ServiceNotFound"
   881  						}`,
   882  					},
   883  				})
   884  			})
   885  
   886  			It("returns a ModelNotFoundError", func() {
   887  				offering, err := repo.GetServiceOfferingByGUID("offering-1-guid")
   888  
   889  				Expect(err).To(BeAssignableToTypeOf(&errors.HTTPNotFoundError{}))
   890  				Expect(offering.GUID).To(Equal(""))
   891  			})
   892  		})
   893  	})
   894  
   895  	Describe("PurgeServiceOffering", func() {
   896  		It("purges service offerings", func() {
   897  			setupTestServer(testnet.TestRequest{
   898  				Method: "DELETE",
   899  				Path:   "/v2/services/the-service-guid?purge=true",
   900  				Response: testnet.TestResponse{
   901  					Status: 204,
   902  				}})
   903  
   904  			offering := models.ServiceOffering{ServiceOfferingFields: models.ServiceOfferingFields{
   905  				Label:       "the-offering",
   906  				GUID:        "the-service-guid",
   907  				Description: "some service description",
   908  			}}
   909  			offering.GUID = "the-service-guid"
   910  
   911  			err := repo.PurgeServiceOffering(offering)
   912  			Expect(err).NotTo(HaveOccurred())
   913  			Expect(testHandler).To(HaveAllRequestsCalled())
   914  		})
   915  	})
   916  
   917  	Describe("PurgeServiceInstance", func() {
   918  		It("purges service instances", func() {
   919  			setupTestServer(testnet.TestRequest{
   920  				Method: "DELETE",
   921  				Path:   "/v2/service_instances/instance-guid?purge=true",
   922  				Response: testnet.TestResponse{
   923  					Status: 204,
   924  				}})
   925  
   926  			instance := models.ServiceInstance{ServiceInstanceFields: models.ServiceInstanceFields{
   927  				Name: "schrodinger",
   928  				GUID: "instance-guid",
   929  			}}
   930  
   931  			err := repo.PurgeServiceInstance(instance)
   932  			Expect(err).NotTo(HaveOccurred())
   933  			Expect(testHandler).To(HaveAllRequestsCalled())
   934  		})
   935  	})
   936  
   937  	Describe("getting the count of service instances for a service plan", func() {
   938  		var planGUID = "abc123"
   939  
   940  		It("returns the number of service instances", func() {
   941  			setupTestServer(apifakes.NewCloudControllerTestRequest(testnet.TestRequest{
   942  				Method: "GET",
   943  				Path:   fmt.Sprintf("/v2/service_plans/%s/service_instances?results-per-page=1", planGUID),
   944  				Response: testnet.TestResponse{Status: http.StatusOK, Body: `
   945                      {
   946                        "total_results": 9,
   947                        "total_pages": 9,
   948                        "prev_url": null,
   949                        "next_url": "/v2/service_plans/abc123/service_instances?page=2&results-per-page=1",
   950                        "resources": [
   951                          {
   952                            "metadata": {
   953                              "guid": "def456",
   954                              "url": "/v2/service_instances/def456",
   955                              "created_at": "2013-06-06T02:42:55+00:00",
   956                              "updated_at": null
   957                            },
   958                            "entity": {
   959                              "name": "pet-db",
   960                              "credentials": { "name": "the_name" },
   961                              "service_plan_guid": "abc123",
   962                              "space_guid": "ghi789",
   963                              "dashboard_url": "https://example.com/dashboard",
   964                              "type": "managed_service_instance",
   965                              "space_url": "/v2/spaces/ghi789",
   966                              "service_plan_url": "/v2/service_plans/abc123",
   967                              "service_bindings_url": "/v2/service_instances/def456/service_bindings"
   968                            }
   969                          }
   970                        ]
   971                      }
   972                  `},
   973  			}))
   974  
   975  			count, err := repo.GetServiceInstanceCountForServicePlan(planGUID)
   976  			Expect(count).To(Equal(9))
   977  			Expect(err).NotTo(HaveOccurred())
   978  		})
   979  
   980  		It("returns the API error when one occurs", func() {
   981  			setupTestServer(apifakes.NewCloudControllerTestRequest(testnet.TestRequest{
   982  				Method:   "GET",
   983  				Path:     fmt.Sprintf("/v2/service_plans/%s/service_instances?results-per-page=1", planGUID),
   984  				Response: testnet.TestResponse{Status: http.StatusInternalServerError},
   985  			}))
   986  
   987  			_, err := repo.GetServiceInstanceCountForServicePlan(planGUID)
   988  			Expect(err).To(HaveOccurred())
   989  		})
   990  	})
   991  
   992  	Describe("finding a service plan", func() {
   993  		var planDescription resources.ServicePlanDescription
   994  
   995  		Context("when the service is a v1 service", func() {
   996  			BeforeEach(func() {
   997  				planDescription = resources.ServicePlanDescription{
   998  					ServiceLabel:    "v1-elephantsql",
   999  					ServicePlanName: "v1-panda",
  1000  					ServiceProvider: "v1-elephantsql",
  1001  				}
  1002  
  1003  				setupTestServer(apifakes.NewCloudControllerTestRequest(testnet.TestRequest{
  1004  					Method: "GET",
  1005  					Path:   fmt.Sprintf("/v2/services?inline-relations-depth=1&q=%s", url.QueryEscape("label:v1-elephantsql;provider:v1-elephantsql")),
  1006  					Response: testnet.TestResponse{Status: http.StatusOK, Body: `
  1007                          {
  1008                            "resources": [
  1009                              {
  1010                                "metadata": {
  1011                                  "guid": "offering-1-guid"
  1012                                },
  1013                                "entity": {
  1014                                  "label": "v1-elephantsql",
  1015                                  "provider": "v1-elephantsql",
  1016                                  "description": "Offering 1 description",
  1017                                  "version" : "1.0",
  1018                                  "service_plans": [
  1019                                      {
  1020                                          "metadata": {"guid": "offering-1-plan-1-guid"},
  1021                                          "entity": {"name": "not-the-plan-youre-looking-for"}
  1022                                      },
  1023                                      {
  1024                                          "metadata": {"guid": "offering-1-plan-2-guid"},
  1025                                          "entity": {"name": "v1-panda"}
  1026                                      }
  1027                                  ]
  1028                                }
  1029                              }
  1030                            ]
  1031                          }`}}))
  1032  			})
  1033  
  1034  			It("returns the plan guid for a v1 plan", func() {
  1035  				guid, err := repo.FindServicePlanByDescription(planDescription)
  1036  
  1037  				Expect(guid).To(Equal("offering-1-plan-2-guid"))
  1038  				Expect(err).NotTo(HaveOccurred())
  1039  			})
  1040  		})
  1041  
  1042  		Context("when the service is a v2 service", func() {
  1043  			BeforeEach(func() {
  1044  				planDescription = resources.ServicePlanDescription{
  1045  					ServiceLabel:    "v2-elephantsql",
  1046  					ServicePlanName: "v2-panda",
  1047  				}
  1048  
  1049  				setupTestServer(apifakes.NewCloudControllerTestRequest(testnet.TestRequest{
  1050  					Method: "GET",
  1051  					Path:   fmt.Sprintf("/v2/services?inline-relations-depth=1&q=%s", url.QueryEscape("label:v2-elephantsql;provider:")),
  1052  					Response: testnet.TestResponse{Status: http.StatusOK, Body: `
  1053                          {
  1054                            "resources": [
  1055                              {
  1056                                "metadata": {
  1057                                  "guid": "offering-1-guid"
  1058                                },
  1059                                "entity": {
  1060                                  "label": "v2-elephantsql",
  1061                                  "provider": null,
  1062                                  "description": "Offering 1 description",
  1063                                  "version" : "1.0",
  1064                                  "service_plans": [
  1065                                      {
  1066                                          "metadata": {"guid": "offering-1-plan-1-guid"},
  1067                                          "entity": {"name": "not-the-plan-youre-looking-for"}
  1068                                      },
  1069                                      {
  1070                                          "metadata": {"guid": "offering-1-plan-2-guid"},
  1071                                          "entity": {"name": "v2-panda"}
  1072                                      }
  1073                                  ]
  1074                                }
  1075                              }
  1076                            ]
  1077                          }`}}))
  1078  			})
  1079  
  1080  			It("returns the plan guid for a v2 plan", func() {
  1081  				guid, err := repo.FindServicePlanByDescription(planDescription)
  1082  				Expect(err).NotTo(HaveOccurred())
  1083  				Expect(guid).To(Equal("offering-1-plan-2-guid"))
  1084  			})
  1085  		})
  1086  
  1087  		Context("when no service matches the description", func() {
  1088  			BeforeEach(func() {
  1089  				planDescription = resources.ServicePlanDescription{
  1090  					ServiceLabel:    "v2-service-label",
  1091  					ServicePlanName: "v2-plan-name",
  1092  				}
  1093  
  1094  				setupTestServer(apifakes.NewCloudControllerTestRequest(testnet.TestRequest{
  1095  					Method:   "GET",
  1096  					Path:     fmt.Sprintf("/v2/services?inline-relations-depth=1&q=%s", url.QueryEscape("label:v2-service-label;provider:")),
  1097  					Response: testnet.TestResponse{Status: http.StatusOK, Body: `{ "resources": [] }`},
  1098  				}))
  1099  			})
  1100  
  1101  			It("returns an error", func() {
  1102  				_, err := repo.FindServicePlanByDescription(planDescription)
  1103  				Expect(err).To(BeAssignableToTypeOf(&errors.ModelNotFoundError{}))
  1104  				Expect(err.Error()).To(ContainSubstring("Plan"))
  1105  				Expect(err.Error()).To(ContainSubstring("v2-service-label v2-plan-name"))
  1106  			})
  1107  		})
  1108  
  1109  		Context("when the described service has no matching plan", func() {
  1110  			BeforeEach(func() {
  1111  				planDescription = resources.ServicePlanDescription{
  1112  					ServiceLabel:    "v2-service-label",
  1113  					ServicePlanName: "v2-plan-name",
  1114  				}
  1115  
  1116  				setupTestServer(apifakes.NewCloudControllerTestRequest(testnet.TestRequest{
  1117  					Method: "GET",
  1118  					Path:   fmt.Sprintf("/v2/services?inline-relations-depth=1&q=%s", url.QueryEscape("label:v2-service-label;provider:")),
  1119  					Response: testnet.TestResponse{Status: http.StatusOK, Body: `
  1120                          {
  1121                            "resources": [
  1122                              {
  1123                                "metadata": {
  1124                                  "guid": "offering-1-guid"
  1125                                },
  1126                                "entity": {
  1127                                  "label": "v2-elephantsql",
  1128                                  "provider": null,
  1129                                  "description": "Offering 1 description",
  1130                                  "version" : "1.0",
  1131                                  "service_plans": [
  1132                                    {
  1133                                      "metadata": {"guid": "offering-1-plan-1-guid"},
  1134                                      "entity": {"name": "not-the-plan-youre-looking-for"}
  1135                                    },
  1136                                    {
  1137                                      "metadata": {"guid": "offering-1-plan-2-guid"},
  1138                                      "entity": {"name": "also-not-the-plan-youre-looking-for"}
  1139                                    }
  1140                                  ]
  1141                                }
  1142                              }
  1143                            ]
  1144                          }`}}))
  1145  			})
  1146  
  1147  			It("returns a ModelNotFoundError", func() {
  1148  				_, err := repo.FindServicePlanByDescription(planDescription)
  1149  
  1150  				Expect(err).To(BeAssignableToTypeOf(&errors.ModelNotFoundError{}))
  1151  				Expect(err.Error()).To(ContainSubstring("Plan"))
  1152  				Expect(err.Error()).To(ContainSubstring("v2-service-label v2-plan-name"))
  1153  			})
  1154  		})
  1155  
  1156  		Context("when we get an HTTP error", func() {
  1157  			BeforeEach(func() {
  1158  				planDescription = resources.ServicePlanDescription{
  1159  					ServiceLabel:    "v2-service-label",
  1160  					ServicePlanName: "v2-plan-name",
  1161  				}
  1162  
  1163  				setupTestServer(apifakes.NewCloudControllerTestRequest(testnet.TestRequest{
  1164  					Method: "GET",
  1165  					Path:   fmt.Sprintf("/v2/services?inline-relations-depth=1&q=%s", url.QueryEscape("label:v2-service-label;provider:")),
  1166  					Response: testnet.TestResponse{
  1167  						Status: http.StatusInternalServerError,
  1168  					}}))
  1169  			})
  1170  
  1171  			It("returns an error", func() {
  1172  				_, err := repo.FindServicePlanByDescription(planDescription)
  1173  
  1174  				Expect(err).To(HaveOccurred())
  1175  				Expect(err).To(BeAssignableToTypeOf(errors.NewHTTPError(500, "", "")))
  1176  			})
  1177  		})
  1178  	})
  1179  
  1180  	Describe("migrating service plans", func() {
  1181  		It("makes a request to CC to migrate the instances from v1 to v2", func() {
  1182  			setupTestServer(testnet.TestRequest{
  1183  				Method:   "PUT",
  1184  				Path:     "/v2/service_plans/v1-guid/service_instances",
  1185  				Matcher:  testnet.RequestBodyMatcher(`{"service_plan_guid":"v2-guid"}`),
  1186  				Response: testnet.TestResponse{Status: http.StatusOK, Body: `{"changed_count":3}`},
  1187  			})
  1188  
  1189  			changedCount, err := repo.MigrateServicePlanFromV1ToV2("v1-guid", "v2-guid")
  1190  			Expect(err).NotTo(HaveOccurred())
  1191  			Expect(changedCount).To(Equal(3))
  1192  		})
  1193  
  1194  		It("returns an error when migrating fails", func() {
  1195  			setupTestServer(apifakes.NewCloudControllerTestRequest(testnet.TestRequest{
  1196  				Method:   "PUT",
  1197  				Path:     "/v2/service_plans/v1-guid/service_instances",
  1198  				Matcher:  testnet.RequestBodyMatcher(`{"service_plan_guid":"v2-guid"}`),
  1199  				Response: testnet.TestResponse{Status: http.StatusInternalServerError},
  1200  			}))
  1201  
  1202  			_, err := repo.MigrateServicePlanFromV1ToV2("v1-guid", "v2-guid")
  1203  			Expect(err).To(HaveOccurred())
  1204  		})
  1205  	})
  1206  
  1207  	Describe("FindServiceOfferingsForSpaceByLabel", func() {
  1208  		It("finds service offerings within a space by label", func() {
  1209  			setupTestServer(
  1210  				testnet.TestRequest{
  1211  					Method: "GET",
  1212  					Path:   fmt.Sprintf("/v2/spaces/my-space-guid/services?q=%s", url.QueryEscape("label:offering-1")),
  1213  					Response: testnet.TestResponse{
  1214  						Status: 200,
  1215  						Body: `
  1216  						{
  1217  							"next_url": "/v2/spaces/my-space-guid/services?q=label%3Aoffering-1&page=2",
  1218  							"resources": [
  1219  								{
  1220  									"metadata": {
  1221  										"guid": "offering-1-guid"
  1222  									},
  1223  									"entity": {
  1224  										"label": "offering-1",
  1225  										"provider": "provider-1",
  1226  										"description": "offering 1 description",
  1227  										"version" : "1.0"
  1228  									  }
  1229  								}
  1230  							]
  1231  						}`}},
  1232  				testnet.TestRequest{
  1233  					Method: "GET",
  1234  					Path:   fmt.Sprintf("/v2/spaces/my-space-guid/services?q=%s", url.QueryEscape("label:offering-1")),
  1235  					Response: testnet.TestResponse{
  1236  						Status: 200,
  1237  						Body: `
  1238  						{
  1239  							"next_url": null,
  1240  							"resources": [
  1241  								{
  1242  									"metadata": {
  1243  										"guid": "offering-2-guid"
  1244  									},
  1245  									"entity": {
  1246  										"label": "offering-2",
  1247  										"provider": "provider-2",
  1248  										"description": "offering 2 description",
  1249  										"version" : "1.0"
  1250  									}
  1251  								}
  1252  							]
  1253  						}`}})
  1254  
  1255  			offerings, err := repo.FindServiceOfferingsForSpaceByLabel("my-space-guid", "offering-1")
  1256  			Expect(err).ToNot(HaveOccurred())
  1257  			Expect(offerings).To(HaveLen(2))
  1258  			Expect(offerings[0].GUID).To(Equal("offering-1-guid"))
  1259  		})
  1260  
  1261  		It("returns an error if the offering cannot be found", func() {
  1262  			setupTestServer(testnet.TestRequest{
  1263  				Method: "GET",
  1264  				Path:   fmt.Sprintf("/v2/spaces/my-space-guid/services?q=%s", url.QueryEscape("label:offering-1")),
  1265  				Response: testnet.TestResponse{
  1266  					Status: http.StatusOK,
  1267  					Body: `{
  1268  						"next_url": null,
  1269  						"resources": []
  1270  					}`,
  1271  				},
  1272  			})
  1273  
  1274  			offerings, err := repo.FindServiceOfferingsForSpaceByLabel("my-space-guid", "offering-1")
  1275  			Expect(err).To(BeAssignableToTypeOf(&errors.ModelNotFoundError{}))
  1276  			Expect(offerings).To(HaveLen(0))
  1277  		})
  1278  
  1279  		It("handles api errors when finding service offerings", func() {
  1280  			setupTestServer(testnet.TestRequest{
  1281  				Method: "GET",
  1282  				Path:   fmt.Sprintf("/v2/spaces/my-space-guid/services?q=%s", url.QueryEscape("label:offering-1")),
  1283  				Response: testnet.TestResponse{
  1284  					Status: http.StatusBadRequest,
  1285  					Body: `{
  1286  						"code": 9001,
  1287  						"description": "Something Happened"
  1288  					}`,
  1289  				},
  1290  			})
  1291  
  1292  			_, err := repo.FindServiceOfferingsForSpaceByLabel("my-space-guid", "offering-1")
  1293  			Expect(err).To(BeAssignableToTypeOf(errors.NewHTTPError(400, "", "")))
  1294  		})
  1295  
  1296  		Describe("when api returns query by label is invalid", func() {
  1297  			It("makes a backwards-compatible request", func() {
  1298  				failedRequestByQueryLabel := testnet.TestRequest{
  1299  					Method: "GET",
  1300  					Path:   fmt.Sprintf("/v2/spaces/my-space-guid/services?q=%s", url.QueryEscape("label:my-service-offering")),
  1301  					Response: testnet.TestResponse{
  1302  						Status: http.StatusBadRequest,
  1303  						Body:   `{"code": 10005,"description": "The query parameter is invalid"}`,
  1304  					},
  1305  				}
  1306  
  1307  				firstPaginatedRequest := testnet.TestRequest{
  1308  					Method: "GET",
  1309  					Path:   fmt.Sprintf("/v2/spaces/my-space-guid/services"),
  1310  					Response: testnet.TestResponse{
  1311  						Status: http.StatusOK,
  1312  						Body: `{
  1313  							"next_url": "/v2/spaces/my-space-guid/services?page=2",
  1314  							"resources": [
  1315  								{
  1316  								  "metadata": {
  1317  									"guid": "my-service-offering-guid"
  1318  								  },
  1319  								  "entity": {
  1320  									"label": "my-service-offering",
  1321  									"provider": "some-other-provider",
  1322  									"description": "a description that does not match your provider",
  1323  									"version" : "1.0"
  1324  								  }
  1325  								}
  1326  							]
  1327  						}`,
  1328  					},
  1329  				}
  1330  
  1331  				secondPaginatedRequest := testnet.TestRequest{
  1332  					Method: "GET",
  1333  					Path:   fmt.Sprintf("/v2/spaces/my-space-guid/services"),
  1334  					Response: testnet.TestResponse{
  1335  						Status: http.StatusOK,
  1336  						Body: `{"next_url": null,
  1337  									"resources": [
  1338  										{
  1339  										  "metadata": {
  1340  											"guid": "my-service-offering-guid"
  1341  										  },
  1342  										  "entity": {
  1343  											"label": "my-service-offering",
  1344  											"provider": "my-provider",
  1345  											"description": "offering 1 description",
  1346  											"version" : "1.0"
  1347  										  }
  1348  										}
  1349  									]}`,
  1350  					},
  1351  				}
  1352  
  1353  				setupTestServer(failedRequestByQueryLabel, firstPaginatedRequest, secondPaginatedRequest)
  1354  
  1355  				serviceOfferings, err := repo.FindServiceOfferingsForSpaceByLabel("my-space-guid", "my-service-offering")
  1356  				Expect(err).NotTo(HaveOccurred())
  1357  				Expect(len(serviceOfferings)).To(Equal(2))
  1358  			})
  1359  		})
  1360  	})
  1361  })
  1362  
  1363  var firstOfferingsResponse = testnet.TestResponse{Status: http.StatusOK, Body: `
  1364  {
  1365  	"next_url": "/v2/services?page=2",
  1366  	"resources": [
  1367  	{
  1368  		"metadata": {
  1369  			"guid": "first-offering-1-guid"
  1370  		},
  1371  		"entity": {
  1372  			"label": "first-Offering 1",
  1373  			"provider": "Offering 1 provider",
  1374  			"description": "first Offering 1 description",
  1375  			"version" : "1.0"
  1376  		}
  1377  	}
  1378    ]}`,
  1379  }
  1380  
  1381  var firstOfferingsForSpaceResponse = testnet.TestResponse{Status: http.StatusOK, Body: `
  1382  {
  1383  	"next_url": "/v2/spaces/my-space-guid/services?inline-relations-depth=1&page=2",
  1384  	"resources": [
  1385  		{
  1386  			"metadata": {
  1387  				"guid": "first-offering-1-guid"
  1388  			},
  1389  			"entity": {
  1390  				"label": "first-Offering 1",
  1391  				"provider": "Offering 1 provider",
  1392  				"description": "first Offering 1 description",
  1393  				"version" : "1.0"
  1394  			}
  1395  	    }
  1396      ]}`,
  1397  }
  1398  
  1399  var multipleOfferingsResponse = testnet.TestResponse{Status: http.StatusOK, Body: `
  1400  {
  1401  	"resources": [
  1402  		{
  1403  	    	"metadata": {
  1404  	        	"guid": "offering-1-guid"
  1405  			},
  1406        		"entity": {
  1407  		        "label": "Offering 1",
  1408  		        "provider": "Offering 1 provider",
  1409  		        "description": "Offering 1 description",
  1410  		        "version" : "1.0"
  1411  			}
  1412  	    },
  1413      	{
  1414        		"metadata": {
  1415          		"guid": "offering-2-guid"
  1416  	      	},
  1417  	      	"entity": {
  1418  		        "label": "Offering 2",
  1419  		        "provider": "Offering 2 provider",
  1420  		        "description": "Offering 2 description",
  1421  		        "version" : "1.5"
  1422  	        }
  1423      	}
  1424  	]}`,
  1425  }
  1426  
  1427  var serviceOfferingReq = apifakes.NewCloudControllerTestRequest(testnet.TestRequest{
  1428  	Method: "GET",
  1429  	Path:   "/v2/services/the-service-guid",
  1430  	Response: testnet.TestResponse{Status: http.StatusOK, Body: `
  1431  		{
  1432  		  "metadata": {
  1433  			"guid": "15790581-a293-489b-9efc-847ecf1b1339"
  1434  		  },
  1435  		  "entity": {
  1436  			"label": "mysql",
  1437  			"provider": "mysql",
  1438  		    "extra": "{\"documentationURL\":\"http://info.example.com\"}",
  1439  			"description": "MySQL database",
  1440  			"requires": ["route_forwarding"]
  1441  		  }
  1442  		}`,
  1443  	}})
  1444  
  1445  var serviceOfferingNullExtraReq = apifakes.NewCloudControllerTestRequest(testnet.TestRequest{
  1446  	Method: "GET",
  1447  	Path:   "/v2/services/the-service-guid",
  1448  	Response: testnet.TestResponse{Status: http.StatusOK, Body: `
  1449  		{
  1450  		  "metadata": {
  1451  			"guid": "15790581-a293-489b-9efc-847ecf1b1339"
  1452  		  },
  1453  		  "entity": {
  1454  			"label": "mysql",
  1455  			"provider": "mysql",
  1456  		    "extra": null,
  1457  			"description": "MySQL database"
  1458  		  }
  1459  		}`,
  1460  	}})
  1461  
  1462  var findServiceInstanceReq = apifakes.NewCloudControllerTestRequest(testnet.TestRequest{
  1463  	Method: "GET",
  1464  	Path:   "/v2/spaces/my-space-guid/service_instances?return_user_provided_service_instances=true&q=name%3Amy-service",
  1465  	Response: testnet.TestResponse{Status: http.StatusOK, Body: `
  1466  	{"resources": [
  1467          {
  1468            "metadata": {
  1469              "guid": "my-service-instance-guid"
  1470            },
  1471            "entity": {
  1472              "name": "my-service",
  1473  			"dashboard_url":"my-dashboard-url",
  1474              "service_bindings": [
  1475                {
  1476                  "metadata": {
  1477                    "guid": "service-binding-1-guid",
  1478                    "url": "/v2/service_bindings/service-binding-1-guid"
  1479                  },
  1480                  "entity": {
  1481                    "app_guid": "app-1-guid"
  1482                  }
  1483                },
  1484                {
  1485                  "metadata": {
  1486                    "guid": "service-binding-2-guid",
  1487                    "url": "/v2/service_bindings/service-binding-2-guid"
  1488                  },
  1489                  "entity": {
  1490                    "app_guid": "app-2-guid"
  1491                  }
  1492                }
  1493              ],
  1494              "service_plan": {
  1495                "metadata": {
  1496                  "guid": "plan-guid"
  1497                },
  1498                "entity": {
  1499                  "name": "plan-name",
  1500                  "service_guid": "the-service-guid"
  1501                }
  1502              }
  1503            }
  1504          }
  1505      ]}`}})