github.com/jasonkeene/cli@v6.14.1-0.20160816203908-ca5715166dfb+incompatible/cf/api/domains_test.go (about) 1 package api_test 2 3 import ( 4 "net/http" 5 "net/http/httptest" 6 "time" 7 8 "github.com/cloudfoundry/cli/cf/api/apifakes" 9 "github.com/cloudfoundry/cli/cf/api/strategy" 10 "github.com/cloudfoundry/cli/cf/configuration/coreconfig" 11 "github.com/cloudfoundry/cli/cf/errors" 12 "github.com/cloudfoundry/cli/cf/models" 13 "github.com/cloudfoundry/cli/cf/net" 14 "github.com/cloudfoundry/cli/cf/terminal/terminalfakes" 15 testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" 16 testnet "github.com/cloudfoundry/cli/testhelpers/net" 17 18 . "github.com/cloudfoundry/cli/cf/api" 19 "github.com/cloudfoundry/cli/cf/trace/tracefakes" 20 . "github.com/cloudfoundry/cli/testhelpers/matchers" 21 . "github.com/onsi/ginkgo" 22 . "github.com/onsi/gomega" 23 ) 24 25 var _ = Describe("DomainRepository", func() { 26 var ( 27 ts *httptest.Server 28 handler *testnet.TestHandler 29 repo DomainRepository 30 config coreconfig.ReadWriter 31 ) 32 33 BeforeEach(func() { 34 config = testconfig.NewRepositoryWithDefaults() 35 }) 36 37 JustBeforeEach(func() { 38 gateway := net.NewCloudControllerGateway(config, time.Now, new(terminalfakes.FakeUI), new(tracefakes.FakePrinter), "") 39 strategy := strategy.NewEndpointStrategy(config.APIVersion()) 40 repo = NewCloudControllerDomainRepository(config, gateway, strategy) 41 }) 42 43 AfterEach(func() { 44 ts.Close() 45 }) 46 47 var setupTestServer = func(reqs ...testnet.TestRequest) { 48 ts, handler = testnet.NewServer(reqs) 49 config.SetAPIEndpoint(ts.URL) 50 } 51 52 Describe("listing domains", func() { 53 BeforeEach(func() { 54 config.SetAPIVersion("2.2.0") 55 setupTestServer(firstPagePrivateDomainsRequest, secondPagePrivateDomainsRequest, firstPageSharedDomainsRequest, secondPageSharedDomainsRequest) 56 }) 57 58 It("uses the organization-scoped domains endpoints", func() { 59 receivedDomains := []models.DomainFields{} 60 apiErr := repo.ListDomainsForOrg("my-org-guid", func(d models.DomainFields) bool { 61 receivedDomains = append(receivedDomains, d) 62 return true 63 }) 64 65 Expect(apiErr).NotTo(HaveOccurred()) 66 Expect(len(receivedDomains)).To(Equal(6)) 67 Expect(receivedDomains[0].GUID).To(Equal("domain1-guid")) 68 Expect(receivedDomains[1].GUID).To(Equal("domain2-guid")) 69 Expect(receivedDomains[2].GUID).To(Equal("domain3-guid")) 70 Expect(receivedDomains[2].Shared).To(BeFalse()) 71 Expect(receivedDomains[3].GUID).To(Equal("shared-domain1-guid")) 72 Expect(receivedDomains[4].GUID).To(Equal("shared-domain2-guid")) 73 Expect(receivedDomains[5].GUID).To(Equal("shared-domain3-guid")) 74 Expect(handler).To(HaveAllRequestsCalled()) 75 }) 76 }) 77 78 Describe("getting default domain", func() { 79 BeforeEach(func() { 80 config.SetAPIVersion("2.2.0") 81 setupTestServer(firstPagePrivateDomainsRequest, secondPagePrivateDomainsRequest, firstPageSharedDomainsRequest, secondPageSharedDomainsRequest) 82 }) 83 84 It("should always return back the first shared domain", func() { 85 domain, apiErr := repo.FirstOrDefault("my-org-guid", nil) 86 87 Expect(apiErr).NotTo(HaveOccurred()) 88 Expect(domain.GUID).To(Equal("shared-domain1-guid")) 89 }) 90 }) 91 92 It("finds a shared domain by name", func() { 93 setupTestServer(apifakes.NewCloudControllerTestRequest(testnet.TestRequest{ 94 Method: "GET", 95 Path: "/v2/domains?inline-relations-depth=1&q=name%3Adomain2.cf-app.com", 96 Response: testnet.TestResponse{Status: http.StatusOK, Body: ` 97 { 98 "resources": [ 99 { 100 "metadata": { "guid": "domain2-guid" }, 101 "entity": { "name": "domain2.cf-app.com" } 102 } 103 ] 104 }`}, 105 })) 106 107 domain, apiErr := repo.FindSharedByName("domain2.cf-app.com") 108 Expect(handler).To(HaveAllRequestsCalled()) 109 Expect(apiErr).NotTo(HaveOccurred()) 110 111 Expect(domain.Name).To(Equal("domain2.cf-app.com")) 112 Expect(domain.GUID).To(Equal("domain2-guid")) 113 Expect(domain.Shared).To(BeTrue()) 114 }) 115 116 It("finds a private domain by name", func() { 117 setupTestServer(apifakes.NewCloudControllerTestRequest(testnet.TestRequest{ 118 Method: "GET", 119 Path: "/v2/domains?inline-relations-depth=1&q=name%3Adomain2.cf-app.com", 120 Response: testnet.TestResponse{Status: http.StatusOK, Body: ` 121 { 122 "resources": [ 123 { 124 "metadata": { "guid": "domain2-guid" }, 125 "entity": { "name": "domain2.cf-app.com", "owning_organization_guid": "some-guid" } 126 } 127 ] 128 }`}, 129 })) 130 131 domain, apiErr := repo.FindPrivateByName("domain2.cf-app.com") 132 Expect(handler).To(HaveAllRequestsCalled()) 133 Expect(apiErr).NotTo(HaveOccurred()) 134 135 Expect(domain.Name).To(Equal("domain2.cf-app.com")) 136 Expect(domain.GUID).To(Equal("domain2-guid")) 137 Expect(domain.Shared).To(BeFalse()) 138 }) 139 140 It("returns domains with router group types", func() { 141 setupTestServer(apifakes.NewCloudControllerTestRequest(testnet.TestRequest{ 142 Method: "GET", 143 Path: "/v2/domains?inline-relations-depth=1&q=name%3Adomain2.cf-app.com", 144 Response: testnet.TestResponse{Status: http.StatusOK, Body: ` 145 { 146 "resources": [ 147 { 148 "metadata": { "guid": "domain2-guid" }, 149 "entity": { 150 "name": "domain2.cf-app.com", 151 "router_group_guid": "my-random-guid", 152 "router_group_type": "tcp" 153 } 154 } 155 ] 156 }`}, 157 })) 158 159 domain, apiErr := repo.FindSharedByName("domain2.cf-app.com") 160 Expect(handler).To(HaveAllRequestsCalled()) 161 Expect(apiErr).NotTo(HaveOccurred()) 162 163 Expect(domain.Name).To(Equal("domain2.cf-app.com")) 164 Expect(domain.GUID).To(Equal("domain2-guid")) 165 Expect(domain.RouterGroupType).To(Equal("tcp")) 166 }) 167 168 Describe("finding a domain by name in an org", func() { 169 It("looks in the org's domains first", func() { 170 setupTestServer(apifakes.NewCloudControllerTestRequest(testnet.TestRequest{ 171 Method: "GET", 172 Path: "/v2/organizations/my-org-guid/domains?inline-relations-depth=1&q=name%3Adomain2.cf-app.com", 173 Response: testnet.TestResponse{Status: http.StatusOK, Body: ` 174 { 175 "resources": [ 176 { 177 "metadata": { "guid": "my-domain-guid" }, 178 "entity": { 179 "name": "my-example.com", 180 "owning_organization_guid": "my-org-guid" 181 } 182 } 183 ] 184 }`}, 185 })) 186 187 domain, apiErr := repo.FindByNameInOrg("domain2.cf-app.com", "my-org-guid") 188 Expect(handler).To(HaveAllRequestsCalled()) 189 Expect(apiErr).NotTo(HaveOccurred()) 190 191 Expect(domain.Name).To(Equal("my-example.com")) 192 Expect(domain.GUID).To(Equal("my-domain-guid")) 193 Expect(domain.Shared).To(BeFalse()) 194 }) 195 196 It("looks for shared domains if no there are no org-specific domains", func() { 197 setupTestServer( 198 apifakes.NewCloudControllerTestRequest(testnet.TestRequest{ 199 Method: "GET", 200 Path: "/v2/organizations/my-org-guid/domains?inline-relations-depth=1&q=name%3Adomain2.cf-app.com", 201 Response: testnet.TestResponse{Status: http.StatusOK, Body: `{"resources": []}`}, 202 }), 203 204 apifakes.NewCloudControllerTestRequest(testnet.TestRequest{ 205 Method: "GET", 206 Path: "/v2/domains?inline-relations-depth=1&q=name%3Adomain2.cf-app.com", 207 Response: testnet.TestResponse{Status: http.StatusOK, Body: ` 208 { 209 "resources": [ 210 { 211 "metadata": { "guid": "shared-domain-guid" }, 212 "entity": { 213 "name": "shared-example.com", 214 "owning_organization_guid": null 215 } 216 } 217 ] 218 }`}, 219 })) 220 221 domain, apiErr := repo.FindByNameInOrg("domain2.cf-app.com", "my-org-guid") 222 Expect(handler).To(HaveAllRequestsCalled()) 223 Expect(apiErr).NotTo(HaveOccurred()) 224 225 Expect(domain.Name).To(Equal("shared-example.com")) 226 Expect(domain.GUID).To(Equal("shared-domain-guid")) 227 Expect(domain.Shared).To(BeTrue()) 228 }) 229 230 It("returns not found when neither endpoint returns a domain", func() { 231 setupTestServer( 232 apifakes.NewCloudControllerTestRequest(testnet.TestRequest{ 233 Method: "GET", 234 Path: "/v2/organizations/my-org-guid/domains?inline-relations-depth=1&q=name%3Adomain2.cf-app.com", 235 Response: testnet.TestResponse{Status: http.StatusOK, Body: `{"resources": []}`}, 236 }), 237 238 apifakes.NewCloudControllerTestRequest(testnet.TestRequest{ 239 Method: "GET", 240 Path: "/v2/domains?inline-relations-depth=1&q=name%3Adomain2.cf-app.com", 241 Response: testnet.TestResponse{Status: http.StatusOK, Body: `{"resources": []}`}, 242 })) 243 244 _, apiErr := repo.FindByNameInOrg("domain2.cf-app.com", "my-org-guid") 245 Expect(handler).To(HaveAllRequestsCalled()) 246 Expect(apiErr.(*errors.ModelNotFoundError)).NotTo(BeNil()) 247 }) 248 249 It("returns not found when the global endpoint returns a non-shared domain", func() { 250 setupTestServer( 251 apifakes.NewCloudControllerTestRequest(testnet.TestRequest{ 252 Method: "GET", 253 Path: "/v2/organizations/my-org-guid/domains?inline-relations-depth=1&q=name%3Adomain2.cf-app.com", 254 Response: testnet.TestResponse{Status: http.StatusOK, Body: `{"resources": []}`}, 255 }), 256 257 apifakes.NewCloudControllerTestRequest(testnet.TestRequest{ 258 Method: "GET", 259 Path: "/v2/domains?inline-relations-depth=1&q=name%3Adomain2.cf-app.com", 260 Response: testnet.TestResponse{Status: http.StatusOK, Body: ` 261 { 262 "resources": [ 263 { 264 "metadata": { "guid": "shared-domain-guid" }, 265 "entity": { 266 "name": "shared-example.com", 267 "owning_organization_guid": "some-other-org-guid" 268 } 269 } 270 ] 271 }`}})) 272 273 _, apiErr := repo.FindByNameInOrg("domain2.cf-app.com", "my-org-guid") 274 Expect(handler).To(HaveAllRequestsCalled()) 275 Expect(apiErr.(*errors.ModelNotFoundError)).NotTo(BeNil()) 276 }) 277 }) 278 279 Describe("creating domains", func() { 280 Context("when the private domains endpoint is not available", func() { 281 BeforeEach(func() { 282 setupTestServer( 283 apifakes.NewCloudControllerTestRequest(testnet.TestRequest{ 284 Method: "POST", 285 Path: "/v2/domains", 286 Matcher: testnet.RequestBodyMatcher(`{"name":"example.com","owning_organization_guid":"org-guid", "wildcard": true}`), 287 Response: testnet.TestResponse{Status: http.StatusCreated, Body: ` 288 { 289 "metadata": { "guid": "abc-123" }, 290 "entity": { "name": "example.com" } 291 }`}, 292 }), 293 ) 294 }) 295 296 It("uses the general domains endpoint", func() { 297 createdDomain, apiErr := repo.Create("example.com", "org-guid") 298 299 Expect(handler).To(HaveAllRequestsCalled()) 300 Expect(apiErr).NotTo(HaveOccurred()) 301 Expect(createdDomain.GUID).To(Equal("abc-123")) 302 }) 303 }) 304 305 Context("when the private domains endpoint is available", func() { 306 BeforeEach(func() { 307 config.SetAPIVersion("2.2.1") 308 }) 309 310 It("uses that endpoint", func() { 311 setupTestServer( 312 apifakes.NewCloudControllerTestRequest(testnet.TestRequest{ 313 Method: "POST", 314 Path: "/v2/private_domains", 315 Matcher: testnet.RequestBodyMatcher(`{"name":"example.com","owning_organization_guid":"org-guid", "wildcard": true}`), 316 Response: testnet.TestResponse{Status: http.StatusCreated, Body: ` 317 { 318 "metadata": { "guid": "abc-123" }, 319 "entity": { "name": "example.com" } 320 }`}, 321 })) 322 323 createdDomain, apiErr := repo.Create("example.com", "org-guid") 324 325 Expect(handler).To(HaveAllRequestsCalled()) 326 Expect(apiErr).NotTo(HaveOccurred()) 327 Expect(createdDomain.GUID).To(Equal("abc-123")) 328 }) 329 }) 330 }) 331 332 Describe("creating shared domains", func() { 333 Context("targeting a newer cloud controller", func() { 334 BeforeEach(func() { 335 config.SetAPIVersion("2.2.0") 336 }) 337 338 It("uses the shared domains endpoint", func() { 339 setupTestServer( 340 apifakes.NewCloudControllerTestRequest(testnet.TestRequest{ 341 Method: "POST", 342 Path: "/v2/shared_domains", 343 Matcher: testnet.RequestBodyMatcher(`{"name":"example.com", "wildcard": true}`), 344 Response: testnet.TestResponse{Status: http.StatusCreated, Body: ` 345 { 346 "metadata": { "guid": "abc-123" }, 347 "entity": { "name": "example.com" } 348 }`}}), 349 ) 350 351 apiErr := repo.CreateSharedDomain("example.com", "") 352 353 Expect(handler).To(HaveAllRequestsCalled()) 354 Expect(apiErr).NotTo(HaveOccurred()) 355 }) 356 357 It("creates a shared domain with a router_group_guid", func() { 358 setupTestServer( 359 apifakes.NewCloudControllerTestRequest(testnet.TestRequest{ 360 Method: "POST", 361 Path: "/v2/shared_domains", 362 Matcher: testnet.RequestBodyMatcher(`{"name":"example.com", "router_group_guid": "tcp-group", "wildcard": true}`), 363 Response: testnet.TestResponse{Status: http.StatusCreated, Body: ` 364 { 365 "metadata": { "guid": "abc-123" }, 366 "entity": { "name": "example.com", "router_group_guid":"tcp-group" } 367 }`}}), 368 ) 369 370 apiErr := repo.CreateSharedDomain("example.com", "tcp-group") 371 372 Expect(handler).To(HaveAllRequestsCalled()) 373 Expect(apiErr).NotTo(HaveOccurred()) 374 }) 375 }) 376 377 Context("when targeting an older cloud controller", func() { 378 It("uses the general domains endpoint", func() { 379 setupTestServer( 380 apifakes.NewCloudControllerTestRequest(testnet.TestRequest{ 381 Method: "POST", 382 Path: "/v2/domains", 383 Matcher: testnet.RequestBodyMatcher(`{"name":"example.com", "wildcard": true}`), 384 Response: testnet.TestResponse{Status: http.StatusCreated, Body: ` 385 { 386 "metadata": { "guid": "abc-123" }, 387 "entity": { "name": "example.com" } 388 }`}, 389 }), 390 ) 391 392 apiErr := repo.CreateSharedDomain("example.com", "") 393 394 Expect(handler).To(HaveAllRequestsCalled()) 395 Expect(apiErr).NotTo(HaveOccurred()) 396 }) 397 }) 398 }) 399 400 Describe("deleting domains", func() { 401 Context("when the private domains endpoint is available", func() { 402 BeforeEach(func() { 403 config.SetAPIVersion("2.2.0") 404 setupTestServer(deleteDomainReq(http.StatusOK)) 405 }) 406 407 It("uses the private domains endpoint", func() { 408 apiErr := repo.Delete("my-domain-guid") 409 410 Expect(handler).To(HaveAllRequestsCalled()) 411 Expect(apiErr).NotTo(HaveOccurred()) 412 }) 413 }) 414 415 Context("when the private domains endpoint is NOT available", func() { 416 BeforeEach(func() { 417 setupTestServer( 418 apifakes.NewCloudControllerTestRequest(testnet.TestRequest{ 419 Method: "DELETE", 420 Path: "/v2/domains/my-domain-guid?recursive=true", 421 Response: testnet.TestResponse{Status: http.StatusOK}, 422 })) 423 }) 424 425 It("uses the general domains endpoint", func() { 426 apiErr := repo.Delete("my-domain-guid") 427 428 Expect(handler).To(HaveAllRequestsCalled()) 429 Expect(apiErr).NotTo(HaveOccurred()) 430 }) 431 }) 432 }) 433 434 Describe("deleting shared domains", func() { 435 Context("when the shared domains endpoint is available", func() { 436 BeforeEach(func() { 437 config.SetAPIVersion("2.2.0") 438 setupTestServer(deleteSharedDomainReq(http.StatusOK)) 439 }) 440 441 It("uses the shared domains endpoint", func() { 442 apiErr := repo.DeleteSharedDomain("my-domain-guid") 443 444 Expect(handler).To(HaveAllRequestsCalled()) 445 Expect(apiErr).NotTo(HaveOccurred()) 446 }) 447 448 It("returns an error when the delete fails", func() { 449 setupTestServer(deleteSharedDomainReq(http.StatusBadRequest)) 450 451 apiErr := repo.DeleteSharedDomain("my-domain-guid") 452 453 Expect(handler).To(HaveAllRequestsCalled()) 454 Expect(apiErr).NotTo(BeNil()) 455 }) 456 }) 457 458 Context("when the shared domains endpoint is not available", func() { 459 It("uses the old domains endpoint", func() { 460 setupTestServer( 461 apifakes.NewCloudControllerTestRequest(testnet.TestRequest{ 462 Method: "DELETE", 463 Path: "/v2/domains/my-domain-guid?recursive=true", 464 Response: testnet.TestResponse{Status: http.StatusOK}, 465 })) 466 467 apiErr := repo.DeleteSharedDomain("my-domain-guid") 468 469 Expect(handler).To(HaveAllRequestsCalled()) 470 Expect(apiErr).NotTo(HaveOccurred()) 471 }) 472 }) 473 }) 474 475 }) 476 477 var oldEndpointDomainsRequest = apifakes.NewCloudControllerTestRequest(testnet.TestRequest{ 478 Method: "GET", 479 Path: "/v2/domains", 480 Response: testnet.TestResponse{Status: http.StatusOK, Body: `{ 481 "resources": [ 482 { 483 "metadata": { 484 "guid": "domain-guid" 485 }, 486 "entity": { 487 "name": "example.com", 488 "owning_organization_guid": "my-org-guid" 489 } 490 } 491 ] 492 }`}}) 493 494 var firstPageDomainsRequest = apifakes.NewCloudControllerTestRequest(testnet.TestRequest{ 495 Method: "GET", 496 Path: "/v2/organizations/my-org-guid/private_domains", 497 Response: testnet.TestResponse{Status: http.StatusOK, Body: ` 498 { 499 "next_url": "/v2/organizations/my-org-guid/domains?page=2", 500 "resources": [ 501 { 502 "metadata": { 503 "guid": "domain1-guid", 504 }, 505 "entity": { 506 "name": "example.com", 507 "owning_organization_guid": "my-org-guid" 508 } 509 }, 510 { 511 "metadata": { 512 "guid": "domain2-guid" 513 }, 514 "entity": { 515 "name": "some-example.com", 516 "owning_organization_guid": "my-org-guid" 517 } 518 } 519 ] 520 }`}, 521 }) 522 523 var secondPageDomainsRequest = apifakes.NewCloudControllerTestRequest(testnet.TestRequest{ 524 Method: "GET", 525 Path: "/v2/organizations/my-org-guid/domains?page=2", 526 Response: testnet.TestResponse{Status: http.StatusOK, Body: ` 527 { 528 "resources": [ 529 { 530 "metadata": { 531 "guid": "domain3-guid" 532 }, 533 "entity": { 534 "name": "example.com", 535 "owning_organization_guid": "my-org-guid" 536 } 537 } 538 ] 539 }`}, 540 }) 541 542 var firstPageSharedDomainsRequest = apifakes.NewCloudControllerTestRequest(testnet.TestRequest{ 543 Method: "GET", 544 Path: "/v2/shared_domains", 545 Response: testnet.TestResponse{Status: http.StatusOK, Body: ` 546 { 547 "next_url": "/v2/shared_domains?page=2", 548 "resources": [ 549 { 550 "metadata": { 551 "guid": "shared-domain1-guid" 552 }, 553 "entity": { 554 "name": "sharedexample.com" 555 } 556 }, 557 { 558 "metadata": { 559 "guid": "shared-domain2-guid" 560 }, 561 "entity": { 562 "name": "some-other-shared-example.com" 563 } 564 } 565 ] 566 }`}, 567 }) 568 569 var secondPageSharedDomainsRequest = apifakes.NewCloudControllerTestRequest(testnet.TestRequest{ 570 Method: "GET", 571 Path: "/v2/shared_domains?page=2", 572 Response: testnet.TestResponse{Status: http.StatusOK, Body: ` 573 { 574 "resources": [ 575 { 576 "metadata": { 577 "guid": "shared-domain3-guid" 578 }, 579 "entity": { 580 "name": "yet-another-shared-example.com" 581 } 582 } 583 ] 584 }`}, 585 }) 586 587 var firstPagePrivateDomainsRequest = apifakes.NewCloudControllerTestRequest(testnet.TestRequest{ 588 Method: "GET", 589 Path: "/v2/organizations/my-org-guid/private_domains", 590 Response: testnet.TestResponse{Status: http.StatusOK, Body: ` 591 { 592 "next_url": "/v2/organizations/my-org-guid/private_domains?page=2", 593 "resources": [ 594 { 595 "metadata": { 596 "guid": "domain1-guid" 597 }, 598 "entity": { 599 "name": "example.com", 600 "owning_organization_guid": "my-org-guid" 601 } 602 }, 603 { 604 "metadata": { 605 "guid": "domain2-guid" 606 }, 607 "entity": { 608 "name": "some-example.com", 609 "owning_organization_guid": "my-org-guid" 610 } 611 } 612 ] 613 }`}, 614 }) 615 616 var secondPagePrivateDomainsRequest = apifakes.NewCloudControllerTestRequest(testnet.TestRequest{ 617 Method: "GET", 618 Path: "/v2/organizations/my-org-guid/private_domains?page=2", 619 Response: testnet.TestResponse{Status: http.StatusOK, Body: ` 620 { 621 "resources": [ 622 { 623 "metadata": { 624 "guid": "domain3-guid" 625 }, 626 "entity": { 627 "name": "example.com", 628 "owning_organization_guid": null, 629 "shared_organizations_url": "/v2/private_domains/domain3-guid/shared_organizations" 630 } 631 } 632 ] 633 }`}, 634 }) 635 636 func deleteDomainReq(statusCode int) testnet.TestRequest { 637 return apifakes.NewCloudControllerTestRequest(testnet.TestRequest{ 638 Method: "DELETE", 639 Path: "/v2/private_domains/my-domain-guid?recursive=true", 640 Response: testnet.TestResponse{Status: statusCode}, 641 }) 642 } 643 644 func deleteSharedDomainReq(statusCode int) testnet.TestRequest { 645 return apifakes.NewCloudControllerTestRequest(testnet.TestRequest{ 646 Method: "DELETE", 647 Path: "/v2/shared_domains/my-domain-guid?recursive=true", 648 Response: testnet.TestResponse{Status: statusCode}, 649 }) 650 }