github.com/mook-as/cf-cli@v7.0.0-beta.28.0.20200120190804-b91c115fae48+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", "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  				serviceInstance.Type = "user_provided_service_instance"
   667  
   668  				err := repo.RenameService(serviceInstance, "new-name")
   669  				Expect(testHandler).To(HaveAllRequestsCalled())
   670  				Expect(err).NotTo(HaveOccurred())
   671  			})
   672  		})
   673  	})
   674  
   675  	Describe("FindServiceOfferingByLabelAndProvider", func() {
   676  		Context("when the service offering can be found", func() {
   677  			BeforeEach(func() {
   678  				setupTestServer(testnet.TestRequest{
   679  					Method: "GET",
   680  					Path:   fmt.Sprintf("/v2/services?q=%s", url.QueryEscape("label:offering-1;provider:provider-1")),
   681  					Response: testnet.TestResponse{
   682  						Status: 200,
   683  						Body: `
   684  						{
   685  							"next_url": null,
   686  							"resources": [
   687  								{
   688  								  "metadata": {
   689  									"guid": "offering-1-guid"
   690  								  },
   691  								  "entity": {
   692  									"label": "offering-1",
   693  									"provider": "provider-1",
   694  									"description": "offering 1 description",
   695  									"version" : "1.0",
   696  									"service_plans": []
   697  								  }
   698  								}
   699  							]
   700  						}`}})
   701  			})
   702  
   703  			It("finds service offerings by label and provider", func() {
   704  				offering, err := repo.FindServiceOfferingByLabelAndProvider("offering-1", "provider-1")
   705  				Expect(offering.GUID).To(Equal("offering-1-guid"))
   706  				Expect(err).NotTo(HaveOccurred())
   707  			})
   708  		})
   709  
   710  		Context("when the service offering cannot be found", func() {
   711  			BeforeEach(func() {
   712  				setupTestServer(testnet.TestRequest{
   713  					Method: "GET",
   714  					Path:   fmt.Sprintf("/v2/services?q=%s", url.QueryEscape("label:offering-1;provider:provider-1")),
   715  					Response: testnet.TestResponse{
   716  						Status: 200,
   717  						Body: `
   718  						{
   719  							"next_url": null,
   720  							"resources": []
   721  						}`,
   722  					},
   723  				})
   724  			})
   725  			It("returns a ModelNotFoundError", func() {
   726  				offering, err := repo.FindServiceOfferingByLabelAndProvider("offering-1", "provider-1")
   727  
   728  				Expect(err).To(BeAssignableToTypeOf(&errors.ModelNotFoundError{}))
   729  				Expect(offering.GUID).To(Equal(""))
   730  			})
   731  		})
   732  
   733  		It("handles api errors when finding service offerings", func() {
   734  			setupTestServer(testnet.TestRequest{
   735  				Method: "GET",
   736  				Path:   fmt.Sprintf("/v2/services?q=%s", url.QueryEscape("label:offering-1;provider:provider-1")),
   737  				Response: testnet.TestResponse{
   738  					Status: 400,
   739  					Body: `
   740  					{
   741              			"code": 10005,
   742              			"description": "The query parameter is invalid"
   743  					}`}})
   744  
   745  			_, err := repo.FindServiceOfferingByLabelAndProvider("offering-1", "provider-1")
   746  			Expect(err).To(HaveOccurred())
   747  			Expect(err.(errors.HTTPError).ErrorCode()).To(Equal("10005"))
   748  		})
   749  	})
   750  
   751  	Describe("FindServiceOfferingsByLabel", func() {
   752  		Context("when the service offering can be found", func() {
   753  			BeforeEach(func() {
   754  				setupTestServer(testnet.TestRequest{
   755  					Method: "GET",
   756  					Path:   fmt.Sprintf("/v2/services?q=%s", url.QueryEscape("label:offering-1")),
   757  					Response: testnet.TestResponse{
   758  						Status: 200,
   759  						Body: `
   760  						{
   761  							"next_url": null,
   762  							"resources": [
   763  								{
   764  								  "metadata": {
   765  									"guid": "offering-1-guid"
   766  								  },
   767  								  "entity": {
   768  									"label": "offering-1",
   769  									"provider": "provider-1",
   770  									"description": "offering 1 description",
   771  									"version" : "1.0",
   772  									"service_plans": [],
   773                    "service_broker_guid": "broker-1-guid"
   774  								  }
   775  								}
   776  							]
   777  						}`}})
   778  			})
   779  
   780  			It("finds service offerings by label", func() {
   781  				offerings, err := repo.FindServiceOfferingsByLabel("offering-1")
   782  				Expect(offerings[0].GUID).To(Equal("offering-1-guid"))
   783  				Expect(offerings[0].Label).To(Equal("offering-1"))
   784  				Expect(offerings[0].Provider).To(Equal("provider-1"))
   785  				Expect(offerings[0].Description).To(Equal("offering 1 description"))
   786  				Expect(offerings[0].Version).To(Equal("1.0"))
   787  				Expect(offerings[0].BrokerGUID).To(Equal("broker-1-guid"))
   788  				Expect(err).NotTo(HaveOccurred())
   789  			})
   790  		})
   791  
   792  		Context("when the service offering cannot be found", func() {
   793  			BeforeEach(func() {
   794  				setupTestServer(testnet.TestRequest{
   795  					Method: "GET",
   796  					Path:   fmt.Sprintf("/v2/services?q=%s", url.QueryEscape("label:offering-1")),
   797  					Response: testnet.TestResponse{
   798  						Status: 200,
   799  						Body: `
   800  						{
   801  							"next_url": null,
   802  							"resources": []
   803  						}`,
   804  					},
   805  				})
   806  			})
   807  
   808  			It("returns a ModelNotFoundError", func() {
   809  				offerings, err := repo.FindServiceOfferingsByLabel("offering-1")
   810  
   811  				Expect(err).To(BeAssignableToTypeOf(&errors.ModelNotFoundError{}))
   812  				Expect(offerings).To(Equal(models.ServiceOfferings{}))
   813  			})
   814  		})
   815  
   816  		It("handles api errors when finding service offerings", func() {
   817  			setupTestServer(testnet.TestRequest{
   818  				Method: "GET",
   819  				Path:   fmt.Sprintf("/v2/services?q=%s", url.QueryEscape("label:offering-1")),
   820  				Response: testnet.TestResponse{
   821  					Status: 400,
   822  					Body: `
   823  					{
   824              			"code": 10005,
   825              			"description": "The query parameter is invalid"
   826  					}`}})
   827  
   828  			_, err := repo.FindServiceOfferingsByLabel("offering-1")
   829  			Expect(err).To(HaveOccurred())
   830  			Expect(err.(errors.HTTPError).ErrorCode()).To(Equal("10005"))
   831  		})
   832  	})
   833  
   834  	Describe("GetServiceOfferingByGUID", func() {
   835  		Context("when the service offering can be found", func() {
   836  			BeforeEach(func() {
   837  				setupTestServer(testnet.TestRequest{
   838  					Method: "GET",
   839  					Path:   fmt.Sprintf("/v2/services/offering-1-guid"),
   840  					Response: testnet.TestResponse{
   841  						Status: 200,
   842  						Body: `
   843  								{
   844  								  "metadata": {
   845  									"guid": "offering-1-guid"
   846  								  },
   847  								  "entity": {
   848  									"label": "offering-1",
   849  									"provider": "provider-1",
   850  									"description": "offering 1 description",
   851  									"version" : "1.0",
   852  									"service_plans": [],
   853                    "service_broker_guid": "broker-1-guid"
   854  								  }
   855  								}`}})
   856  			})
   857  
   858  			It("finds service offerings by guid", func() {
   859  				offering, err := repo.GetServiceOfferingByGUID("offering-1-guid")
   860  				Expect(offering.GUID).To(Equal("offering-1-guid"))
   861  				Expect(offering.Label).To(Equal("offering-1"))
   862  				Expect(offering.Provider).To(Equal("provider-1"))
   863  				Expect(offering.Description).To(Equal("offering 1 description"))
   864  				Expect(offering.Version).To(Equal("1.0"))
   865  				Expect(offering.BrokerGUID).To(Equal("broker-1-guid"))
   866  				Expect(err).NotTo(HaveOccurred())
   867  			})
   868  		})
   869  
   870  		Context("when the service offering cannot be found", func() {
   871  			BeforeEach(func() {
   872  				setupTestServer(testnet.TestRequest{
   873  					Method: "GET",
   874  					Path:   fmt.Sprintf("/v2/services/offering-1-guid"),
   875  					Response: testnet.TestResponse{
   876  						Status: 404,
   877  						Body: `
   878  						{
   879  							"code": 120003,
   880  							"description": "The service could not be found: offering-1-guid",
   881                "error_code": "CF-ServiceNotFound"
   882  						}`,
   883  					},
   884  				})
   885  			})
   886  
   887  			It("returns a ModelNotFoundError", func() {
   888  				offering, err := repo.GetServiceOfferingByGUID("offering-1-guid")
   889  
   890  				Expect(err).To(BeAssignableToTypeOf(&errors.HTTPNotFoundError{}))
   891  				Expect(offering.GUID).To(Equal(""))
   892  			})
   893  		})
   894  	})
   895  
   896  	Describe("PurgeServiceOffering", func() {
   897  		It("purges service offerings", func() {
   898  			setupTestServer(testnet.TestRequest{
   899  				Method: "DELETE",
   900  				Path:   "/v2/services/the-service-guid?purge=true",
   901  				Response: testnet.TestResponse{
   902  					Status: 204,
   903  				}})
   904  
   905  			offering := models.ServiceOffering{ServiceOfferingFields: models.ServiceOfferingFields{
   906  				Label:       "the-offering",
   907  				GUID:        "the-service-guid",
   908  				Description: "some service description",
   909  			}}
   910  			offering.GUID = "the-service-guid"
   911  
   912  			err := repo.PurgeServiceOffering(offering)
   913  			Expect(err).NotTo(HaveOccurred())
   914  			Expect(testHandler).To(HaveAllRequestsCalled())
   915  		})
   916  	})
   917  
   918  	Describe("PurgeServiceInstance", func() {
   919  		It("purges service instances", func() {
   920  			setupTestServer(testnet.TestRequest{
   921  				Method: "DELETE",
   922  				Path:   "/v2/service_instances/instance-guid?purge=true",
   923  				Response: testnet.TestResponse{
   924  					Status: 204,
   925  				}})
   926  
   927  			instance := models.ServiceInstance{ServiceInstanceFields: models.ServiceInstanceFields{
   928  				Name: "schrodinger",
   929  				GUID: "instance-guid",
   930  			}}
   931  
   932  			err := repo.PurgeServiceInstance(instance)
   933  			Expect(err).NotTo(HaveOccurred())
   934  			Expect(testHandler).To(HaveAllRequestsCalled())
   935  		})
   936  	})
   937  
   938  	Describe("getting the count of service instances for a service plan", func() {
   939  		var planGUID = "abc123"
   940  
   941  		It("returns the number of service instances", func() {
   942  			setupTestServer(apifakes.NewCloudControllerTestRequest(testnet.TestRequest{
   943  				Method: "GET",
   944  				Path:   fmt.Sprintf("/v2/service_plans/%s/service_instances?results-per-page=1", planGUID),
   945  				Response: testnet.TestResponse{Status: http.StatusOK, Body: `
   946                      {
   947                        "total_results": 9,
   948                        "total_pages": 9,
   949                        "prev_url": null,
   950                        "next_url": "/v2/service_plans/abc123/service_instances?page=2&results-per-page=1",
   951                        "resources": [
   952                          {
   953                            "metadata": {
   954                              "guid": "def456",
   955                              "url": "/v2/service_instances/def456",
   956                              "created_at": "2013-06-06T02:42:55+00:00",
   957                              "updated_at": null
   958                            },
   959                            "entity": {
   960                              "name": "pet-db",
   961                              "credentials": { "name": "the_name" },
   962                              "service_plan_guid": "abc123",
   963                              "space_guid": "ghi789",
   964                              "dashboard_url": "https://example.com/dashboard",
   965                              "type": "managed_service_instance",
   966                              "space_url": "/v2/spaces/ghi789",
   967                              "service_plan_url": "/v2/service_plans/abc123",
   968                              "service_bindings_url": "/v2/service_instances/def456/service_bindings"
   969                            }
   970                          }
   971                        ]
   972                      }
   973                  `},
   974  			}))
   975  
   976  			count, err := repo.GetServiceInstanceCountForServicePlan(planGUID)
   977  			Expect(count).To(Equal(9))
   978  			Expect(err).NotTo(HaveOccurred())
   979  		})
   980  
   981  		It("returns the API error when one occurs", func() {
   982  			setupTestServer(apifakes.NewCloudControllerTestRequest(testnet.TestRequest{
   983  				Method:   "GET",
   984  				Path:     fmt.Sprintf("/v2/service_plans/%s/service_instances?results-per-page=1", planGUID),
   985  				Response: testnet.TestResponse{Status: http.StatusInternalServerError},
   986  			}))
   987  
   988  			_, err := repo.GetServiceInstanceCountForServicePlan(planGUID)
   989  			Expect(err).To(HaveOccurred())
   990  		})
   991  	})
   992  
   993  	Describe("finding a service plan", func() {
   994  		var planDescription resources.ServicePlanDescription
   995  
   996  		Context("when the service is a v1 service", func() {
   997  			BeforeEach(func() {
   998  				planDescription = resources.ServicePlanDescription{
   999  					ServiceLabel:    "v1-elephantsql",
  1000  					ServicePlanName: "v1-panda",
  1001  					ServiceProvider: "v1-elephantsql",
  1002  				}
  1003  
  1004  				setupTestServer(apifakes.NewCloudControllerTestRequest(testnet.TestRequest{
  1005  					Method: "GET",
  1006  					Path:   fmt.Sprintf("/v2/services?inline-relations-depth=1&q=%s", url.QueryEscape("label:v1-elephantsql;provider:v1-elephantsql")),
  1007  					Response: testnet.TestResponse{Status: http.StatusOK, Body: `
  1008                          {
  1009                            "resources": [
  1010                              {
  1011                                "metadata": {
  1012                                  "guid": "offering-1-guid"
  1013                                },
  1014                                "entity": {
  1015                                  "label": "v1-elephantsql",
  1016                                  "provider": "v1-elephantsql",
  1017                                  "description": "Offering 1 description",
  1018                                  "version" : "1.0",
  1019                                  "service_plans": [
  1020                                      {
  1021                                          "metadata": {"guid": "offering-1-plan-1-guid"},
  1022                                          "entity": {"name": "not-the-plan-youre-looking-for"}
  1023                                      },
  1024                                      {
  1025                                          "metadata": {"guid": "offering-1-plan-2-guid"},
  1026                                          "entity": {"name": "v1-panda"}
  1027                                      }
  1028                                  ]
  1029                                }
  1030                              }
  1031                            ]
  1032                          }`}}))
  1033  			})
  1034  
  1035  			It("returns the plan guid for a v1 plan", func() {
  1036  				guid, err := repo.FindServicePlanByDescription(planDescription)
  1037  
  1038  				Expect(guid).To(Equal("offering-1-plan-2-guid"))
  1039  				Expect(err).NotTo(HaveOccurred())
  1040  			})
  1041  		})
  1042  
  1043  		Context("when the service is a v2 service", func() {
  1044  			BeforeEach(func() {
  1045  				planDescription = resources.ServicePlanDescription{
  1046  					ServiceLabel:    "v2-elephantsql",
  1047  					ServicePlanName: "v2-panda",
  1048  				}
  1049  
  1050  				setupTestServer(apifakes.NewCloudControllerTestRequest(testnet.TestRequest{
  1051  					Method: "GET",
  1052  					Path:   fmt.Sprintf("/v2/services?inline-relations-depth=1&q=%s", url.QueryEscape("label:v2-elephantsql;provider:")),
  1053  					Response: testnet.TestResponse{Status: http.StatusOK, Body: `
  1054                          {
  1055                            "resources": [
  1056                              {
  1057                                "metadata": {
  1058                                  "guid": "offering-1-guid"
  1059                                },
  1060                                "entity": {
  1061                                  "label": "v2-elephantsql",
  1062                                  "provider": null,
  1063                                  "description": "Offering 1 description",
  1064                                  "version" : "1.0",
  1065                                  "service_plans": [
  1066                                      {
  1067                                          "metadata": {"guid": "offering-1-plan-1-guid"},
  1068                                          "entity": {"name": "not-the-plan-youre-looking-for"}
  1069                                      },
  1070                                      {
  1071                                          "metadata": {"guid": "offering-1-plan-2-guid"},
  1072                                          "entity": {"name": "v2-panda"}
  1073                                      }
  1074                                  ]
  1075                                }
  1076                              }
  1077                            ]
  1078                          }`}}))
  1079  			})
  1080  
  1081  			It("returns the plan guid for a v2 plan", func() {
  1082  				guid, err := repo.FindServicePlanByDescription(planDescription)
  1083  				Expect(err).NotTo(HaveOccurred())
  1084  				Expect(guid).To(Equal("offering-1-plan-2-guid"))
  1085  			})
  1086  		})
  1087  
  1088  		Context("when no service matches the description", func() {
  1089  			BeforeEach(func() {
  1090  				planDescription = resources.ServicePlanDescription{
  1091  					ServiceLabel:    "v2-service-label",
  1092  					ServicePlanName: "v2-plan-name",
  1093  				}
  1094  
  1095  				setupTestServer(apifakes.NewCloudControllerTestRequest(testnet.TestRequest{
  1096  					Method:   "GET",
  1097  					Path:     fmt.Sprintf("/v2/services?inline-relations-depth=1&q=%s", url.QueryEscape("label:v2-service-label;provider:")),
  1098  					Response: testnet.TestResponse{Status: http.StatusOK, Body: `{ "resources": [] }`},
  1099  				}))
  1100  			})
  1101  
  1102  			It("returns an error", func() {
  1103  				_, err := repo.FindServicePlanByDescription(planDescription)
  1104  				Expect(err).To(BeAssignableToTypeOf(&errors.ModelNotFoundError{}))
  1105  				Expect(err.Error()).To(ContainSubstring("Plan"))
  1106  				Expect(err.Error()).To(ContainSubstring("v2-service-label v2-plan-name"))
  1107  			})
  1108  		})
  1109  
  1110  		Context("when the described service has no matching plan", func() {
  1111  			BeforeEach(func() {
  1112  				planDescription = resources.ServicePlanDescription{
  1113  					ServiceLabel:    "v2-service-label",
  1114  					ServicePlanName: "v2-plan-name",
  1115  				}
  1116  
  1117  				setupTestServer(apifakes.NewCloudControllerTestRequest(testnet.TestRequest{
  1118  					Method: "GET",
  1119  					Path:   fmt.Sprintf("/v2/services?inline-relations-depth=1&q=%s", url.QueryEscape("label:v2-service-label;provider:")),
  1120  					Response: testnet.TestResponse{Status: http.StatusOK, Body: `
  1121                          {
  1122                            "resources": [
  1123                              {
  1124                                "metadata": {
  1125                                  "guid": "offering-1-guid"
  1126                                },
  1127                                "entity": {
  1128                                  "label": "v2-elephantsql",
  1129                                  "provider": null,
  1130                                  "description": "Offering 1 description",
  1131                                  "version" : "1.0",
  1132                                  "service_plans": [
  1133                                    {
  1134                                      "metadata": {"guid": "offering-1-plan-1-guid"},
  1135                                      "entity": {"name": "not-the-plan-youre-looking-for"}
  1136                                    },
  1137                                    {
  1138                                      "metadata": {"guid": "offering-1-plan-2-guid"},
  1139                                      "entity": {"name": "also-not-the-plan-youre-looking-for"}
  1140                                    }
  1141                                  ]
  1142                                }
  1143                              }
  1144                            ]
  1145                          }`}}))
  1146  			})
  1147  
  1148  			It("returns a ModelNotFoundError", func() {
  1149  				_, err := repo.FindServicePlanByDescription(planDescription)
  1150  
  1151  				Expect(err).To(BeAssignableToTypeOf(&errors.ModelNotFoundError{}))
  1152  				Expect(err.Error()).To(ContainSubstring("Plan"))
  1153  				Expect(err.Error()).To(ContainSubstring("v2-service-label v2-plan-name"))
  1154  			})
  1155  		})
  1156  
  1157  		Context("when we get an HTTP error", func() {
  1158  			BeforeEach(func() {
  1159  				planDescription = resources.ServicePlanDescription{
  1160  					ServiceLabel:    "v2-service-label",
  1161  					ServicePlanName: "v2-plan-name",
  1162  				}
  1163  
  1164  				setupTestServer(apifakes.NewCloudControllerTestRequest(testnet.TestRequest{
  1165  					Method: "GET",
  1166  					Path:   fmt.Sprintf("/v2/services?inline-relations-depth=1&q=%s", url.QueryEscape("label:v2-service-label;provider:")),
  1167  					Response: testnet.TestResponse{
  1168  						Status: http.StatusInternalServerError,
  1169  					}}))
  1170  			})
  1171  
  1172  			It("returns an error", func() {
  1173  				_, err := repo.FindServicePlanByDescription(planDescription)
  1174  
  1175  				Expect(err).To(HaveOccurred())
  1176  				Expect(err).To(BeAssignableToTypeOf(errors.NewHTTPError(500, "", "")))
  1177  			})
  1178  		})
  1179  	})
  1180  
  1181  	Describe("migrating service plans", func() {
  1182  		It("makes a request to CC to migrate the instances from v1 to v2", func() {
  1183  			setupTestServer(testnet.TestRequest{
  1184  				Method:   "PUT",
  1185  				Path:     "/v2/service_plans/v1-guid/service_instances",
  1186  				Matcher:  testnet.RequestBodyMatcher(`{"service_plan_guid":"v2-guid"}`),
  1187  				Response: testnet.TestResponse{Status: http.StatusOK, Body: `{"changed_count":3}`},
  1188  			})
  1189  
  1190  			changedCount, err := repo.MigrateServicePlanFromV1ToV2("v1-guid", "v2-guid")
  1191  			Expect(err).NotTo(HaveOccurred())
  1192  			Expect(changedCount).To(Equal(3))
  1193  		})
  1194  
  1195  		It("returns an error when migrating fails", func() {
  1196  			setupTestServer(apifakes.NewCloudControllerTestRequest(testnet.TestRequest{
  1197  				Method:   "PUT",
  1198  				Path:     "/v2/service_plans/v1-guid/service_instances",
  1199  				Matcher:  testnet.RequestBodyMatcher(`{"service_plan_guid":"v2-guid"}`),
  1200  				Response: testnet.TestResponse{Status: http.StatusInternalServerError},
  1201  			}))
  1202  
  1203  			_, err := repo.MigrateServicePlanFromV1ToV2("v1-guid", "v2-guid")
  1204  			Expect(err).To(HaveOccurred())
  1205  		})
  1206  	})
  1207  
  1208  	Describe("FindServiceOfferingsForSpaceByLabel", func() {
  1209  		It("finds service offerings within a space by label", func() {
  1210  			setupTestServer(
  1211  				testnet.TestRequest{
  1212  					Method: "GET",
  1213  					Path:   fmt.Sprintf("/v2/spaces/my-space-guid/services?q=%s", url.QueryEscape("label:offering-1")),
  1214  					Response: testnet.TestResponse{
  1215  						Status: 200,
  1216  						Body: `
  1217  						{
  1218  							"next_url": "/v2/spaces/my-space-guid/services?q=label%3Aoffering-1&page=2",
  1219  							"resources": [
  1220  								{
  1221  									"metadata": {
  1222  										"guid": "offering-1-guid"
  1223  									},
  1224  									"entity": {
  1225  										"label": "offering-1",
  1226  										"provider": "provider-1",
  1227  										"description": "offering 1 description",
  1228  										"version" : "1.0"
  1229  									  }
  1230  								}
  1231  							]
  1232  						}`}},
  1233  				testnet.TestRequest{
  1234  					Method: "GET",
  1235  					Path:   fmt.Sprintf("/v2/spaces/my-space-guid/services?q=%s", url.QueryEscape("label:offering-1")),
  1236  					Response: testnet.TestResponse{
  1237  						Status: 200,
  1238  						Body: `
  1239  						{
  1240  							"next_url": null,
  1241  							"resources": [
  1242  								{
  1243  									"metadata": {
  1244  										"guid": "offering-2-guid"
  1245  									},
  1246  									"entity": {
  1247  										"label": "offering-2",
  1248  										"provider": "provider-2",
  1249  										"description": "offering 2 description",
  1250  										"version" : "1.0"
  1251  									}
  1252  								}
  1253  							]
  1254  						}`}})
  1255  
  1256  			offerings, err := repo.FindServiceOfferingsForSpaceByLabel("my-space-guid", "offering-1")
  1257  			Expect(err).ToNot(HaveOccurred())
  1258  			Expect(offerings).To(HaveLen(2))
  1259  			Expect(offerings[0].GUID).To(Equal("offering-1-guid"))
  1260  		})
  1261  
  1262  		It("returns an error if the offering cannot be found", func() {
  1263  			setupTestServer(testnet.TestRequest{
  1264  				Method: "GET",
  1265  				Path:   fmt.Sprintf("/v2/spaces/my-space-guid/services?q=%s", url.QueryEscape("label:offering-1")),
  1266  				Response: testnet.TestResponse{
  1267  					Status: http.StatusOK,
  1268  					Body: `{
  1269  						"next_url": null,
  1270  						"resources": []
  1271  					}`,
  1272  				},
  1273  			})
  1274  
  1275  			offerings, err := repo.FindServiceOfferingsForSpaceByLabel("my-space-guid", "offering-1")
  1276  			Expect(err).To(BeAssignableToTypeOf(&errors.ModelNotFoundError{}))
  1277  			Expect(offerings).To(HaveLen(0))
  1278  		})
  1279  
  1280  		It("handles api errors when finding service offerings", func() {
  1281  			setupTestServer(testnet.TestRequest{
  1282  				Method: "GET",
  1283  				Path:   fmt.Sprintf("/v2/spaces/my-space-guid/services?q=%s", url.QueryEscape("label:offering-1")),
  1284  				Response: testnet.TestResponse{
  1285  					Status: http.StatusBadRequest,
  1286  					Body: `{
  1287  						"code": 9001,
  1288  						"description": "Something Happened"
  1289  					}`,
  1290  				},
  1291  			})
  1292  
  1293  			_, err := repo.FindServiceOfferingsForSpaceByLabel("my-space-guid", "offering-1")
  1294  			Expect(err).To(BeAssignableToTypeOf(errors.NewHTTPError(400, "", "")))
  1295  		})
  1296  
  1297  		Describe("when api returns query by label is invalid", func() {
  1298  			It("makes a backwards-compatible request", func() {
  1299  				failedRequestByQueryLabel := testnet.TestRequest{
  1300  					Method: "GET",
  1301  					Path:   fmt.Sprintf("/v2/spaces/my-space-guid/services?q=%s", url.QueryEscape("label:my-service-offering")),
  1302  					Response: testnet.TestResponse{
  1303  						Status: http.StatusBadRequest,
  1304  						Body:   `{"code": 10005,"description": "The query parameter is invalid"}`,
  1305  					},
  1306  				}
  1307  
  1308  				firstPaginatedRequest := testnet.TestRequest{
  1309  					Method: "GET",
  1310  					Path:   fmt.Sprintf("/v2/spaces/my-space-guid/services"),
  1311  					Response: testnet.TestResponse{
  1312  						Status: http.StatusOK,
  1313  						Body: `{
  1314  							"next_url": "/v2/spaces/my-space-guid/services?page=2",
  1315  							"resources": [
  1316  								{
  1317  								  "metadata": {
  1318  									"guid": "my-service-offering-guid"
  1319  								  },
  1320  								  "entity": {
  1321  									"label": "my-service-offering",
  1322  									"provider": "some-other-provider",
  1323  									"description": "a description that does not match your provider",
  1324  									"version" : "1.0"
  1325  								  }
  1326  								}
  1327  							]
  1328  						}`,
  1329  					},
  1330  				}
  1331  
  1332  				secondPaginatedRequest := testnet.TestRequest{
  1333  					Method: "GET",
  1334  					Path:   fmt.Sprintf("/v2/spaces/my-space-guid/services?page=2"),
  1335  					Response: testnet.TestResponse{
  1336  						Status: http.StatusOK,
  1337  						Body: `{"next_url": null,
  1338  									"resources": [
  1339  										{
  1340  										  "metadata": {
  1341  											"guid": "my-service-offering-guid"
  1342  										  },
  1343  										  "entity": {
  1344  											"label": "my-service-offering",
  1345  											"provider": "my-provider",
  1346  											"description": "offering 1 description",
  1347  											"version" : "1.0"
  1348  										  }
  1349  										}
  1350  									]}`,
  1351  					},
  1352  				}
  1353  
  1354  				setupTestServer(failedRequestByQueryLabel, firstPaginatedRequest, secondPaginatedRequest)
  1355  
  1356  				serviceOfferings, err := repo.FindServiceOfferingsForSpaceByLabel("my-space-guid", "my-service-offering")
  1357  				Expect(err).NotTo(HaveOccurred())
  1358  				Expect(len(serviceOfferings)).To(Equal(2))
  1359  			})
  1360  		})
  1361  	})
  1362  })
  1363  
  1364  var firstOfferingsResponse = testnet.TestResponse{Status: http.StatusOK, Body: `
  1365  {
  1366  	"next_url": "/v2/services?page=2",
  1367  	"resources": [
  1368  	{
  1369  		"metadata": {
  1370  			"guid": "first-offering-1-guid"
  1371  		},
  1372  		"entity": {
  1373  			"label": "first-Offering 1",
  1374  			"provider": "Offering 1 provider",
  1375  			"description": "first Offering 1 description",
  1376  			"version" : "1.0"
  1377  		}
  1378  	}
  1379    ]}`,
  1380  }
  1381  
  1382  var firstOfferingsForSpaceResponse = testnet.TestResponse{Status: http.StatusOK, Body: `
  1383  {
  1384  	"next_url": "/v2/spaces/my-space-guid/services?inline-relations-depth=1&page=2",
  1385  	"resources": [
  1386  		{
  1387  			"metadata": {
  1388  				"guid": "first-offering-1-guid"
  1389  			},
  1390  			"entity": {
  1391  				"label": "first-Offering 1",
  1392  				"provider": "Offering 1 provider",
  1393  				"description": "first Offering 1 description",
  1394  				"version" : "1.0"
  1395  			}
  1396  	    }
  1397      ]}`,
  1398  }
  1399  
  1400  var multipleOfferingsResponse = testnet.TestResponse{Status: http.StatusOK, Body: `
  1401  {
  1402  	"resources": [
  1403  		{
  1404  	    	"metadata": {
  1405  	        	"guid": "offering-1-guid"
  1406  			},
  1407        		"entity": {
  1408  		        "label": "Offering 1",
  1409  		        "provider": "Offering 1 provider",
  1410  		        "description": "Offering 1 description",
  1411  		        "version" : "1.0"
  1412  			}
  1413  	    },
  1414      	{
  1415        		"metadata": {
  1416          		"guid": "offering-2-guid"
  1417  	      	},
  1418  	      	"entity": {
  1419  		        "label": "Offering 2",
  1420  		        "provider": "Offering 2 provider",
  1421  		        "description": "Offering 2 description",
  1422  		        "version" : "1.5"
  1423  	        }
  1424      	}
  1425  	]}`,
  1426  }
  1427  
  1428  var serviceOfferingReq = apifakes.NewCloudControllerTestRequest(testnet.TestRequest{
  1429  	Method: "GET",
  1430  	Path:   "/v2/services/the-service-guid",
  1431  	Response: testnet.TestResponse{Status: http.StatusOK, Body: `
  1432  		{
  1433  		  "metadata": {
  1434  			"guid": "15790581-a293-489b-9efc-847ecf1b1339"
  1435  		  },
  1436  		  "entity": {
  1437  			"label": "mysql",
  1438  			"provider": "mysql",
  1439  		    "extra": "{\"documentationURL\":\"http://info.example.com\"}",
  1440  			"description": "MySQL database",
  1441  			"requires": ["route_forwarding"]
  1442  		  }
  1443  		}`,
  1444  	}})
  1445  
  1446  var serviceOfferingNullExtraReq = apifakes.NewCloudControllerTestRequest(testnet.TestRequest{
  1447  	Method: "GET",
  1448  	Path:   "/v2/services/the-service-guid",
  1449  	Response: testnet.TestResponse{Status: http.StatusOK, Body: `
  1450  		{
  1451  		  "metadata": {
  1452  			"guid": "15790581-a293-489b-9efc-847ecf1b1339"
  1453  		  },
  1454  		  "entity": {
  1455  			"label": "mysql",
  1456  			"provider": "mysql",
  1457  		    "extra": null,
  1458  			"description": "MySQL database"
  1459  		  }
  1460  		}`,
  1461  	}})
  1462  
  1463  var findServiceInstanceReq = apifakes.NewCloudControllerTestRequest(testnet.TestRequest{
  1464  	Method: "GET",
  1465  	Path:   "/v2/spaces/my-space-guid/service_instances?return_user_provided_service_instances=true&q=name%3Amy-service",
  1466  	Response: testnet.TestResponse{Status: http.StatusOK, Body: `
  1467  	{"resources": [
  1468          {
  1469            "metadata": {
  1470              "guid": "my-service-instance-guid"
  1471            },
  1472            "entity": {
  1473              "name": "my-service",
  1474  			"dashboard_url":"my-dashboard-url",
  1475              "service_bindings": [
  1476                {
  1477                  "metadata": {
  1478                    "guid": "service-binding-1-guid",
  1479                    "url": "/v2/service_bindings/service-binding-1-guid"
  1480                  },
  1481                  "entity": {
  1482                    "app_guid": "app-1-guid"
  1483                  }
  1484                },
  1485                {
  1486                  "metadata": {
  1487                    "guid": "service-binding-2-guid",
  1488                    "url": "/v2/service_bindings/service-binding-2-guid"
  1489                  },
  1490                  "entity": {
  1491                    "app_guid": "app-2-guid"
  1492                  }
  1493                }
  1494              ],
  1495              "service_plan": {
  1496                "metadata": {
  1497                  "guid": "plan-guid"
  1498                },
  1499                "entity": {
  1500                  "name": "plan-name",
  1501                  "service_guid": "the-service-guid"
  1502                }
  1503              }
  1504            }
  1505          }
  1506      ]}`}})