github.com/loggregator/cli@v6.33.1-0.20180224010324-82334f081791+incompatible/api/cloudcontroller/ccv2/job_test.go (about) 1 package ccv2_test 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "io" 9 "io/ioutil" 10 "mime/multipart" 11 "net/http" 12 "strings" 13 "time" 14 15 "code.cloudfoundry.org/cli/api/cloudcontroller" 16 "code.cloudfoundry.org/cli/api/cloudcontroller/ccerror" 17 . "code.cloudfoundry.org/cli/api/cloudcontroller/ccv2" 18 "code.cloudfoundry.org/cli/api/cloudcontroller/ccv2/ccv2fakes" 19 "code.cloudfoundry.org/cli/api/cloudcontroller/ccv2/constant" 20 "code.cloudfoundry.org/cli/api/cloudcontroller/wrapper" 21 . "github.com/onsi/ginkgo" 22 . "github.com/onsi/ginkgo/extensions/table" 23 . "github.com/onsi/gomega" 24 . "github.com/onsi/gomega/ghttp" 25 ) 26 27 var _ = Describe("Job", func() { 28 var client *Client 29 30 Describe("Job", func() { 31 DescribeTable("Finished", 32 func(status constant.JobStatus, expected bool) { 33 job := Job{Status: status} 34 Expect(job.Finished()).To(Equal(expected)) 35 }, 36 37 Entry("when failed, it returns false", constant.JobStatusFailed, false), 38 Entry("when finished, it returns true", constant.JobStatusFinished, true), 39 Entry("when queued, it returns false", constant.JobStatusQueued, false), 40 Entry("when running, it returns false", constant.JobStatusRunning, false), 41 ) 42 43 DescribeTable("Failed", 44 func(status constant.JobStatus, expected bool) { 45 job := Job{Status: status} 46 Expect(job.Failed()).To(Equal(expected)) 47 }, 48 49 Entry("when failed, it returns true", constant.JobStatusFailed, true), 50 Entry("when finished, it returns false", constant.JobStatusFinished, false), 51 Entry("when queued, it returns false", constant.JobStatusQueued, false), 52 Entry("when running, it returns false", constant.JobStatusRunning, false), 53 ) 54 }) 55 56 Describe("PollJob", func() { 57 BeforeEach(func() { 58 client = NewTestClient(Config{JobPollingTimeout: time.Minute}) 59 }) 60 61 Context("when the job starts queued and then finishes successfully", func() { 62 BeforeEach(func() { 63 server.AppendHandlers( 64 CombineHandlers( 65 VerifyRequest(http.MethodGet, "/v2/jobs/some-job-guid"), 66 RespondWith(http.StatusAccepted, `{ 67 "metadata": { 68 "guid": "some-job-guid", 69 "created_at": "2016-06-08T16:41:27Z", 70 "url": "/v2/jobs/some-job-guid" 71 }, 72 "entity": { 73 "guid": "some-job-guid", 74 "status": "queued" 75 } 76 }`, http.Header{"X-Cf-Warnings": {"warning-1"}}), 77 )) 78 79 server.AppendHandlers( 80 CombineHandlers( 81 VerifyRequest(http.MethodGet, "/v2/jobs/some-job-guid"), 82 RespondWith(http.StatusAccepted, `{ 83 "metadata": { 84 "guid": "some-job-guid", 85 "created_at": "2016-06-08T16:41:28Z", 86 "url": "/v2/jobs/some-job-guid" 87 }, 88 "entity": { 89 "guid": "some-job-guid", 90 "status": "running" 91 } 92 }`, http.Header{"X-Cf-Warnings": {"warning-2, warning-3"}}), 93 )) 94 95 server.AppendHandlers( 96 CombineHandlers( 97 VerifyRequest(http.MethodGet, "/v2/jobs/some-job-guid"), 98 RespondWith(http.StatusAccepted, `{ 99 "metadata": { 100 "guid": "some-job-guid", 101 "created_at": "2016-06-08T16:41:29Z", 102 "url": "/v2/jobs/some-job-guid" 103 }, 104 "entity": { 105 "guid": "some-job-guid", 106 "status": "finished" 107 } 108 }`, http.Header{"X-Cf-Warnings": {"warning-4"}}), 109 )) 110 }) 111 112 It("should poll until completion", func() { 113 warnings, err := client.PollJob(Job{GUID: "some-job-guid"}) 114 Expect(err).ToNot(HaveOccurred()) 115 Expect(warnings).To(ConsistOf("warning-1", "warning-2", "warning-3", "warning-4")) 116 }) 117 }) 118 119 Context("when the job starts queued and then fails", func() { 120 var jobFailureMessage string 121 BeforeEach(func() { 122 jobFailureMessage = "I am a banana!!!" 123 124 server.AppendHandlers( 125 CombineHandlers( 126 VerifyRequest(http.MethodGet, "/v2/jobs/some-job-guid"), 127 RespondWith(http.StatusAccepted, `{ 128 "metadata": { 129 "guid": "some-job-guid", 130 "created_at": "2016-06-08T16:41:27Z", 131 "url": "/v2/jobs/some-job-guid" 132 }, 133 "entity": { 134 "guid": "some-job-guid", 135 "status": "queued" 136 } 137 }`, http.Header{"X-Cf-Warnings": {"warning-1"}}), 138 )) 139 140 server.AppendHandlers( 141 CombineHandlers( 142 VerifyRequest(http.MethodGet, "/v2/jobs/some-job-guid"), 143 RespondWith(http.StatusAccepted, `{ 144 "metadata": { 145 "guid": "some-job-guid", 146 "created_at": "2016-06-08T16:41:28Z", 147 "url": "/v2/jobs/some-job-guid" 148 }, 149 "entity": { 150 "guid": "some-job-guid", 151 "status": "running" 152 } 153 }`, http.Header{"X-Cf-Warnings": {"warning-2, warning-3"}}), 154 )) 155 156 server.AppendHandlers( 157 CombineHandlers( 158 VerifyRequest(http.MethodGet, "/v2/jobs/some-job-guid"), 159 RespondWith(http.StatusOK, fmt.Sprintf(` 160 { 161 "metadata": { 162 "guid": "some-job-guid", 163 "created_at": "2016-06-08T16:41:29Z", 164 "url": "/v2/jobs/some-job-guid" 165 }, 166 "entity": { 167 "error": "Use of entity>error is deprecated in favor of entity>error_details.", 168 "error_details": { 169 "code": 160001, 170 "description": "%s", 171 "error_code": "CF-AppBitsUploadInvalid" 172 }, 173 "guid": "job-guid", 174 "status": "failed" 175 } 176 } 177 `, jobFailureMessage), http.Header{"X-Cf-Warnings": {"warning-4"}}), 178 )) 179 }) 180 181 It("returns a JobFailedError", func() { 182 warnings, err := client.PollJob(Job{GUID: "some-job-guid"}) 183 Expect(err).To(MatchError(ccerror.JobFailedError{ 184 JobGUID: "some-job-guid", 185 Message: jobFailureMessage, 186 })) 187 Expect(warnings).To(ConsistOf("warning-1", "warning-2", "warning-3", "warning-4")) 188 }) 189 }) 190 191 Context("when retrieving the job errors", func() { 192 BeforeEach(func() { 193 server.AppendHandlers( 194 CombineHandlers( 195 VerifyRequest(http.MethodGet, "/v2/jobs/some-job-guid"), 196 RespondWith(http.StatusAccepted, `{ 197 INVALID YAML 198 }`, http.Header{"X-Cf-Warnings": {"warning-1, warning-2"}}), 199 )) 200 }) 201 202 It("returns the CC error", func() { 203 warnings, err := client.PollJob(Job{GUID: "some-job-guid"}) 204 Expect(warnings).To(ConsistOf("warning-1", "warning-2")) 205 Expect(err.Error()).To(MatchRegexp("invalid character")) 206 }) 207 }) 208 209 Describe("JobPollingTimeout", func() { 210 Context("when the job runs longer than the OverallPollingTimeout", func() { 211 var jobPollingTimeout time.Duration 212 213 BeforeEach(func() { 214 jobPollingTimeout = 100 * time.Millisecond 215 client = NewTestClient(Config{ 216 JobPollingTimeout: jobPollingTimeout, 217 JobPollingInterval: 60 * time.Millisecond, 218 }) 219 220 server.AppendHandlers( 221 CombineHandlers( 222 VerifyRequest(http.MethodGet, "/v2/jobs/some-job-guid"), 223 RespondWith(http.StatusAccepted, `{ 224 "metadata": { 225 "guid": "some-job-guid", 226 "created_at": "2016-06-08T16:41:27Z", 227 "url": "/v2/jobs/some-job-guid" 228 }, 229 "entity": { 230 "guid": "some-job-guid", 231 "status": "queued" 232 } 233 }`, http.Header{"X-Cf-Warnings": {"warning-1"}}), 234 )) 235 236 server.AppendHandlers( 237 CombineHandlers( 238 VerifyRequest(http.MethodGet, "/v2/jobs/some-job-guid"), 239 RespondWith(http.StatusAccepted, `{ 240 "metadata": { 241 "guid": "some-job-guid", 242 "created_at": "2016-06-08T16:41:28Z", 243 "url": "/v2/jobs/some-job-guid" 244 }, 245 "entity": { 246 "guid": "some-job-guid", 247 "status": "running" 248 } 249 }`, http.Header{"X-Cf-Warnings": {"warning-2, warning-3"}}), 250 )) 251 252 server.AppendHandlers( 253 CombineHandlers( 254 VerifyRequest(http.MethodGet, "/v2/jobs/some-job-guid"), 255 RespondWith(http.StatusAccepted, `{ 256 "metadata": { 257 "guid": "some-job-guid", 258 "created_at": "2016-06-08T16:41:29Z", 259 "url": "/v2/jobs/some-job-guid" 260 }, 261 "entity": { 262 "guid": "some-job-guid", 263 "status": "finished" 264 } 265 }`, http.Header{"X-Cf-Warnings": {"warning-4"}}), 266 )) 267 }) 268 269 It("raises a JobTimeoutError", func() { 270 _, err := client.PollJob(Job{GUID: "some-job-guid"}) 271 272 Expect(err).To(MatchError(ccerror.JobTimeoutError{ 273 Timeout: jobPollingTimeout, 274 JobGUID: "some-job-guid", 275 })) 276 }) 277 278 // Fuzzy test to ensure that the overall function time isn't [far] 279 // greater than the OverallPollingTimeout. Since this is partially 280 // dependent on the speed of the system, the expectation is that the 281 // function *should* never exceed three times the timeout. 282 It("does not run [too much] longer than the timeout", func() { 283 startTime := time.Now() 284 _, err := client.PollJob(Job{GUID: "some-job-guid"}) 285 endTime := time.Now() 286 Expect(err).To(HaveOccurred()) 287 288 // If the jobPollingTimeout is less than the PollingInterval, 289 // then the margin may be too small, we should not allow the 290 // jobPollingTimeout to be set to less than the PollingInterval 291 Expect(endTime).To(BeTemporally("~", startTime, 3*jobPollingTimeout)) 292 }) 293 }) 294 }) 295 }) 296 297 Describe("GetJob", func() { 298 BeforeEach(func() { 299 client = NewTestClient() 300 }) 301 302 Context("when no errors are encountered", func() { 303 BeforeEach(func() { 304 jsonResponse := `{ 305 "metadata": { 306 "guid": "job-guid", 307 "created_at": "2016-06-08T16:41:27Z", 308 "url": "/v2/jobs/job-guid" 309 }, 310 "entity": { 311 "guid": "job-guid", 312 "status": "queued" 313 } 314 }` 315 316 server.AppendHandlers( 317 CombineHandlers( 318 VerifyRequest(http.MethodGet, "/v2/jobs/job-guid"), 319 RespondWith(http.StatusOK, jsonResponse, http.Header{"X-Cf-Warnings": {"warning-1, warning-2"}}), 320 )) 321 }) 322 323 It("returns job with all warnings", func() { 324 job, warnings, err := client.GetJob("job-guid") 325 326 Expect(err).NotTo(HaveOccurred()) 327 Expect(warnings).To(ConsistOf(Warnings{"warning-1", "warning-2"})) 328 Expect(job.GUID).To(Equal("job-guid")) 329 Expect(job.Status).To(Equal(constant.JobStatusQueued)) 330 }) 331 }) 332 333 Context("when the job fails", func() { 334 BeforeEach(func() { 335 jsonResponse := ` 336 { 337 "metadata": { 338 "guid": "some-job-guid", 339 "created_at": "2016-06-08T16:41:29Z", 340 "url": "/v2/jobs/some-job-guid" 341 }, 342 "entity": { 343 "error": "Use of entity>error is deprecated in favor of entity>error_details.", 344 "error_details": { 345 "code": 160001, 346 "description": "some-error", 347 "error_code": "CF-AppBitsUploadInvalid" 348 }, 349 "guid": "job-guid", 350 "status": "failed" 351 } 352 } 353 ` 354 server.AppendHandlers( 355 CombineHandlers( 356 VerifyRequest(http.MethodGet, "/v2/jobs/job-guid"), 357 RespondWith(http.StatusOK, jsonResponse, http.Header{"X-Cf-Warnings": {"warning-1, warning-2"}}), 358 )) 359 }) 360 361 It("returns job with all warnings", func() { 362 job, warnings, err := client.GetJob("job-guid") 363 364 Expect(err).NotTo(HaveOccurred()) 365 Expect(warnings).To(ConsistOf(Warnings{"warning-1", "warning-2"})) 366 Expect(job.GUID).To(Equal("job-guid")) 367 Expect(job.Status).To(Equal(constant.JobStatusFailed)) 368 Expect(job.Error).To(Equal("Use of entity>error is deprecated in favor of entity>error_details.")) 369 Expect(job.ErrorDetails.Description).To(Equal("some-error")) 370 }) 371 }) 372 }) 373 374 Describe("DeleteOrganizationJob", func() { 375 var ( 376 job Job 377 warnings Warnings 378 executeErr error 379 ) 380 381 BeforeEach(func() { 382 client = NewTestClient() 383 }) 384 385 JustBeforeEach(func() { 386 job, warnings, executeErr = client.DeleteOrganizationJob("some-organization-guid") 387 }) 388 389 Context("when no errors are encountered", func() { 390 BeforeEach(func() { 391 jsonResponse := `{ 392 "metadata": { 393 "guid": "job-guid", 394 "created_at": "2016-06-08T16:41:27Z", 395 "url": "/v2/jobs/job-guid" 396 }, 397 "entity": { 398 "guid": "job-guid", 399 "status": "queued" 400 } 401 }` 402 403 server.AppendHandlers( 404 CombineHandlers( 405 VerifyRequest(http.MethodDelete, "/v2/organizations/some-organization-guid", "recursive=true&async=true"), 406 RespondWith(http.StatusAccepted, jsonResponse, http.Header{"X-Cf-Warnings": {"warning-1, warning-2"}}), 407 )) 408 }) 409 410 It("deletes the Organization and returns all warnings", func() { 411 Expect(executeErr).NotTo(HaveOccurred()) 412 Expect(warnings).To(ConsistOf(Warnings{"warning-1", "warning-2"})) 413 Expect(job).To(Equal(Job{ 414 GUID: "job-guid", 415 Status: constant.JobStatusQueued, 416 })) 417 }) 418 }) 419 420 Context("when an error is encountered", func() { 421 BeforeEach(func() { 422 response := `{ 423 "code": 30003, 424 "description": "The Organization could not be found: some-organization-guid", 425 "error_code": "CF-OrganizationNotFound" 426 }` 427 server.AppendHandlers( 428 CombineHandlers( 429 VerifyRequest(http.MethodDelete, "/v2/organizations/some-organization-guid", "recursive=true&async=true"), 430 RespondWith(http.StatusNotFound, response, http.Header{"X-Cf-Warnings": {"warning-1, warning-2"}}), 431 )) 432 }) 433 434 It("returns an error and all warnings", func() { 435 Expect(executeErr).To(MatchError(ccerror.ResourceNotFoundError{ 436 Message: "The Organization could not be found: some-organization-guid", 437 })) 438 Expect(warnings).To(ConsistOf(Warnings{"warning-1", "warning-2"})) 439 }) 440 }) 441 }) 442 443 Describe("DeleteSpaceJob", func() { 444 var ( 445 job Job 446 warnings Warnings 447 executeErr error 448 ) 449 450 BeforeEach(func() { 451 client = NewTestClient() 452 }) 453 454 JustBeforeEach(func() { 455 job, warnings, executeErr = client.DeleteSpaceJob("some-space-guid") 456 }) 457 458 Context("when no errors are encountered", func() { 459 BeforeEach(func() { 460 jsonResponse := `{ 461 "metadata": { 462 "guid": "job-guid", 463 "created_at": "2016-06-08T16:41:27Z", 464 "url": "/v2/jobs/job-guid" 465 }, 466 "entity": { 467 "guid": "job-guid", 468 "status": "queued" 469 } 470 }` 471 472 server.AppendHandlers( 473 CombineHandlers( 474 VerifyRequest(http.MethodDelete, "/v2/spaces/some-space-guid", "recursive=true&async=true"), 475 RespondWith(http.StatusAccepted, jsonResponse, http.Header{"X-Cf-Warnings": {"warning-1, warning-2"}}), 476 )) 477 }) 478 479 It("deletes the Space and returns all warnings", func() { 480 Expect(executeErr).NotTo(HaveOccurred()) 481 Expect(warnings).To(ConsistOf(Warnings{"warning-1", "warning-2"})) 482 Expect(job).To(Equal(Job{ 483 GUID: "job-guid", 484 Status: constant.JobStatusQueued, 485 })) 486 }) 487 }) 488 489 Context("when an error is encountered", func() { 490 BeforeEach(func() { 491 response := `{ 492 "code": 30003, 493 "description": "The Space could not be found: some-space-guid", 494 "error_code": "CF-SpaceNotFound" 495 }` 496 server.AppendHandlers( 497 CombineHandlers( 498 VerifyRequest(http.MethodDelete, "/v2/spaces/some-space-guid", "recursive=true&async=true"), 499 RespondWith(http.StatusNotFound, response, http.Header{"X-Cf-Warnings": {"warning-1, warning-2"}}), 500 )) 501 }) 502 503 It("returns an error and all warnings", func() { 504 Expect(executeErr).To(MatchError(ccerror.ResourceNotFoundError{ 505 Message: "The Space could not be found: some-space-guid", 506 })) 507 Expect(warnings).To(ConsistOf(Warnings{"warning-1", "warning-2"})) 508 }) 509 }) 510 }) 511 512 Describe("UploadApplicationPackage", func() { 513 BeforeEach(func() { 514 client = NewTestClient() 515 }) 516 517 Context("when the upload is successful", func() { 518 var ( 519 resources []Resource 520 reader io.Reader 521 readerBody []byte 522 ) 523 524 Context("when the upload has application bits to upload", func() { 525 BeforeEach(func() { 526 resources = []Resource{ 527 {Filename: "foo"}, 528 {Filename: "bar"}, 529 } 530 531 readerBody = []byte("hello world") 532 reader = bytes.NewReader(readerBody) 533 534 verifyHeaderAndBody := func(_ http.ResponseWriter, req *http.Request) { 535 contentType := req.Header.Get("Content-Type") 536 Expect(contentType).To(MatchRegexp("multipart/form-data; boundary=[\\w\\d]+")) 537 538 defer req.Body.Close() 539 requestReader := multipart.NewReader(req.Body, contentType[30:]) 540 541 // Verify that matched resources are sent properly 542 resourcesPart, err := requestReader.NextPart() 543 Expect(err).NotTo(HaveOccurred()) 544 545 Expect(resourcesPart.FormName()).To(Equal("resources")) 546 547 defer resourcesPart.Close() 548 expectedJSON, err := json.Marshal(resources) 549 Expect(err).NotTo(HaveOccurred()) 550 Expect(ioutil.ReadAll(resourcesPart)).To(MatchJSON(expectedJSON)) 551 552 // Verify that the application bits are sent properly 553 resourcesPart, err = requestReader.NextPart() 554 Expect(err).NotTo(HaveOccurred()) 555 556 Expect(resourcesPart.FormName()).To(Equal("application")) 557 Expect(resourcesPart.FileName()).To(Equal("application.zip")) 558 559 defer resourcesPart.Close() 560 Expect(ioutil.ReadAll(resourcesPart)).To(Equal(readerBody)) 561 } 562 563 response := `{ 564 "metadata": { 565 "guid": "job-guid", 566 "url": "/v2/jobs/job-guid" 567 }, 568 "entity": { 569 "guid": "job-guid", 570 "status": "queued" 571 } 572 }` 573 574 server.AppendHandlers( 575 CombineHandlers( 576 VerifyRequest(http.MethodPut, "/v2/apps/some-app-guid/bits", "async=true"), 577 verifyHeaderAndBody, 578 RespondWith(http.StatusOK, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 579 ), 580 ) 581 }) 582 583 It("returns the created job and warnings", func() { 584 job, warnings, err := client.UploadApplicationPackage("some-app-guid", resources, reader, int64(len(readerBody))) 585 Expect(err).NotTo(HaveOccurred()) 586 Expect(warnings).To(ConsistOf("this is a warning")) 587 Expect(job).To(Equal(Job{ 588 GUID: "job-guid", 589 Status: constant.JobStatusQueued, 590 })) 591 }) 592 }) 593 594 Context("when there are no application bits to upload", func() { 595 BeforeEach(func() { 596 resources = []Resource{ 597 {Filename: "foo"}, 598 {Filename: "bar"}, 599 } 600 601 verifyHeaderAndBody := func(_ http.ResponseWriter, req *http.Request) { 602 contentType := req.Header.Get("Content-Type") 603 Expect(contentType).To(MatchRegexp("multipart/form-data; boundary=[\\w\\d]+")) 604 605 defer req.Body.Close() 606 requestReader := multipart.NewReader(req.Body, contentType[30:]) 607 608 // Verify that matched resources are sent properly 609 resourcesPart, err := requestReader.NextPart() 610 Expect(err).NotTo(HaveOccurred()) 611 612 Expect(resourcesPart.FormName()).To(Equal("resources")) 613 614 defer resourcesPart.Close() 615 expectedJSON, err := json.Marshal(resources) 616 Expect(err).NotTo(HaveOccurred()) 617 Expect(ioutil.ReadAll(resourcesPart)).To(MatchJSON(expectedJSON)) 618 619 // Verify that the application bits are not sent 620 resourcesPart, err = requestReader.NextPart() 621 Expect(err).To(MatchError(io.EOF)) 622 } 623 624 response := `{ 625 "metadata": { 626 "guid": "job-guid", 627 "url": "/v2/jobs/job-guid" 628 }, 629 "entity": { 630 "guid": "job-guid", 631 "status": "queued" 632 } 633 }` 634 635 server.AppendHandlers( 636 CombineHandlers( 637 VerifyRequest(http.MethodPut, "/v2/apps/some-app-guid/bits", "async=true"), 638 verifyHeaderAndBody, 639 RespondWith(http.StatusOK, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 640 ), 641 ) 642 }) 643 644 It("does not send the application bits", func() { 645 job, warnings, err := client.UploadApplicationPackage("some-app-guid", resources, nil, 33513531353) 646 Expect(err).NotTo(HaveOccurred()) 647 Expect(warnings).To(ConsistOf("this is a warning")) 648 Expect(job).To(Equal(Job{ 649 GUID: "job-guid", 650 Status: constant.JobStatusQueued, 651 })) 652 }) 653 }) 654 }) 655 656 Context("when the CC returns an error", func() { 657 BeforeEach(func() { 658 response := `{ 659 "code": 30003, 660 "description": "Banana", 661 "error_code": "CF-Banana" 662 }` 663 664 server.AppendHandlers( 665 CombineHandlers( 666 VerifyRequest(http.MethodPut, "/v2/apps/some-app-guid/bits", "async=true"), 667 RespondWith(http.StatusNotFound, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 668 ), 669 ) 670 }) 671 672 It("returns the error", func() { 673 _, warnings, err := client.UploadApplicationPackage("some-app-guid", []Resource{}, bytes.NewReader(nil), 0) 674 Expect(err).To(MatchError(ccerror.ResourceNotFoundError{Message: "Banana"})) 675 Expect(warnings).To(ConsistOf("this is a warning")) 676 }) 677 }) 678 679 Context("when passed a nil resources", func() { 680 It("returns a NilObjectError", func() { 681 _, _, err := client.UploadApplicationPackage("some-app-guid", nil, bytes.NewReader(nil), 0) 682 Expect(err).To(MatchError(ccerror.NilObjectError{Object: "existingResources"})) 683 }) 684 }) 685 686 Context("when an error is returned from the new resources reader", func() { 687 var ( 688 fakeReader *ccv2fakes.FakeReader 689 expectedErr error 690 ) 691 692 BeforeEach(func() { 693 expectedErr = errors.New("some read error") 694 fakeReader = new(ccv2fakes.FakeReader) 695 fakeReader.ReadReturns(0, expectedErr) 696 697 server.AppendHandlers( 698 VerifyRequest(http.MethodPut, "/v2/apps/some-app-guid/bits", "async=true"), 699 ) 700 }) 701 702 It("returns the error", func() { 703 _, _, err := client.UploadApplicationPackage("some-app-guid", []Resource{}, fakeReader, 3) 704 Expect(err).To(MatchError(expectedErr)) 705 }) 706 }) 707 708 Context("when a retryable error occurs", func() { 709 BeforeEach(func() { 710 wrapper := &wrapper.CustomWrapper{ 711 CustomMake: func(connection cloudcontroller.Connection, request *cloudcontroller.Request, response *cloudcontroller.Response) error { 712 defer GinkgoRecover() // Since this will be running in a thread 713 714 if strings.HasSuffix(request.URL.String(), "/v2/apps/some-app-guid/bits?async=true") { 715 _, err := ioutil.ReadAll(request.Body) 716 Expect(err).ToNot(HaveOccurred()) 717 Expect(request.Body.Close()).ToNot(HaveOccurred()) 718 return request.ResetBody() 719 } 720 return connection.Make(request, response) 721 }, 722 } 723 724 client = NewTestClient(Config{Wrappers: []ConnectionWrapper{wrapper}}) 725 }) 726 727 It("returns the PipeSeekError", func() { 728 _, _, err := client.UploadApplicationPackage("some-app-guid", []Resource{}, strings.NewReader("hello world"), 3) 729 Expect(err).To(MatchError(ccerror.PipeSeekError{})) 730 }) 731 }) 732 733 Context("when an http error occurs mid-transfer", func() { 734 var expectedErr error 735 const UploadSize = 33 * 1024 736 737 BeforeEach(func() { 738 expectedErr = errors.New("some read error") 739 740 wrapper := &wrapper.CustomWrapper{ 741 CustomMake: func(connection cloudcontroller.Connection, request *cloudcontroller.Request, response *cloudcontroller.Response) error { 742 defer GinkgoRecover() // Since this will be running in a thread 743 744 if strings.HasSuffix(request.URL.String(), "/v2/apps/some-app-guid/bits?async=true") { 745 defer request.Body.Close() 746 readBytes, err := ioutil.ReadAll(request.Body) 747 Expect(err).ToNot(HaveOccurred()) 748 Expect(len(readBytes)).To(BeNumerically(">", UploadSize)) 749 return expectedErr 750 } 751 return connection.Make(request, response) 752 }, 753 } 754 755 client = NewTestClient(Config{Wrappers: []ConnectionWrapper{wrapper}}) 756 }) 757 758 It("returns the http error", func() { 759 _, _, err := client.UploadApplicationPackage("some-app-guid", []Resource{}, strings.NewReader(strings.Repeat("a", UploadSize)), 3) 760 Expect(err).To(MatchError(expectedErr)) 761 }) 762 }) 763 }) 764 765 Describe("UploadDroplet", func() { 766 var ( 767 appGUID string 768 droplet io.Reader 769 readerBody []byte 770 ) 771 772 BeforeEach(func() { 773 client = NewTestClient() 774 775 appGUID = "some-app-guid" 776 readerBody = []byte("hello world") 777 droplet = bytes.NewReader(readerBody) 778 }) 779 780 Context("when the Droplet is successful", func() { 781 BeforeEach(func() { 782 verifyHeaderAndBody := func(_ http.ResponseWriter, req *http.Request) { 783 contentType := req.Header.Get("Content-Type") 784 Expect(contentType).To(MatchRegexp("multipart/form-data; boundary=[\\w\\d]+")) 785 786 defer req.Body.Close() 787 requestReader := multipart.NewReader(req.Body, contentType[30:]) 788 789 // Verify that matched resources are sent properly 790 resourcesPart, err := requestReader.NextPart() 791 Expect(err).NotTo(HaveOccurred()) 792 defer resourcesPart.Close() 793 794 Expect(resourcesPart.FormName()).To(Equal("droplet")) 795 Expect(resourcesPart.FileName()).To(Equal("droplet.tgz")) 796 Expect(ioutil.ReadAll(resourcesPart)).To(Equal(readerBody)) 797 } 798 799 response := `{ 800 "metadata": { 801 "guid": "job-guid", 802 "url": "/v2/jobs/job-guid" 803 }, 804 "entity": { 805 "guid": "job-guid", 806 "status": "queued" 807 } 808 }` 809 810 server.AppendHandlers( 811 CombineHandlers( 812 VerifyRequest(http.MethodPut, "/v2/apps/some-app-guid/droplet/upload"), 813 verifyHeaderAndBody, 814 RespondWith(http.StatusOK, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 815 ), 816 ) 817 }) 818 819 It("returns the created job and warnings", func() { 820 job, warnings, err := client.UploadDroplet(appGUID, droplet, int64(len(readerBody))) 821 Expect(err).NotTo(HaveOccurred()) 822 Expect(warnings).To(ConsistOf("this is a warning")) 823 Expect(job).To(Equal(Job{ 824 GUID: "job-guid", 825 Status: constant.JobStatusQueued, 826 })) 827 }) 828 }) 829 830 Context("when the CC returns an error", func() { 831 BeforeEach(func() { 832 response := `{ 833 "code": 30003, 834 "description": "Banana", 835 "error_code": "CF-Banana" 836 }` 837 838 server.AppendHandlers( 839 CombineHandlers( 840 VerifyRequest(http.MethodPut, "/v2/apps/some-app-guid/droplet/upload"), 841 RespondWith(http.StatusNotFound, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 842 ), 843 ) 844 }) 845 846 It("returns the error", func() { 847 _, warnings, err := client.UploadDroplet(appGUID, bytes.NewReader(nil), 0) 848 Expect(err).To(MatchError(ccerror.ResourceNotFoundError{Message: "Banana"})) 849 Expect(warnings).To(ConsistOf("this is a warning")) 850 }) 851 }) 852 853 Context("when there is an error reading the droplet", func() { 854 var ( 855 fakeReader *ccv2fakes.FakeReader 856 expectedErr error 857 ) 858 859 BeforeEach(func() { 860 expectedErr = errors.New("some read error") 861 fakeReader = new(ccv2fakes.FakeReader) 862 fakeReader.ReadReturns(0, expectedErr) 863 864 server.AppendHandlers( 865 VerifyRequest(http.MethodPut, "/v2/apps/some-app-guid/droplet/upload"), 866 ) 867 }) 868 869 It("returns the error", func() { 870 _, _, err := client.UploadDroplet(appGUID, fakeReader, 3) 871 Expect(err).To(MatchError(expectedErr)) 872 }) 873 }) 874 875 Context("when a retryable error occurs", func() { 876 BeforeEach(func() { 877 wrapper := &wrapper.CustomWrapper{ 878 CustomMake: func(connection cloudcontroller.Connection, request *cloudcontroller.Request, response *cloudcontroller.Response) error { 879 defer GinkgoRecover() // Since this will be running in a thread 880 881 if strings.HasSuffix(request.URL.String(), "/v2/apps/some-app-guid/droplet/upload") { 882 _, err := ioutil.ReadAll(request.Body) 883 Expect(err).ToNot(HaveOccurred()) 884 Expect(request.Body.Close()).ToNot(HaveOccurred()) 885 return request.ResetBody() 886 } 887 return connection.Make(request, response) 888 }, 889 } 890 891 client = NewTestClient(Config{Wrappers: []ConnectionWrapper{wrapper}}) 892 }) 893 894 It("returns the PipeSeekError", func() { 895 _, _, err := client.UploadDroplet(appGUID, strings.NewReader("hello world"), 3) 896 Expect(err).To(MatchError(ccerror.PipeSeekError{})) 897 }) 898 }) 899 900 Context("when an http error occurs mid-transfer", func() { 901 var expectedErr error 902 const UploadSize = 33 * 1024 903 904 BeforeEach(func() { 905 expectedErr = errors.New("some read error") 906 907 wrapper := &wrapper.CustomWrapper{ 908 CustomMake: func(connection cloudcontroller.Connection, request *cloudcontroller.Request, response *cloudcontroller.Response) error { 909 defer GinkgoRecover() // Since this will be running in a thread 910 911 if strings.HasSuffix(request.URL.String(), "/v2/apps/some-app-guid/droplet/upload") { 912 defer request.Body.Close() 913 readBytes, err := ioutil.ReadAll(request.Body) 914 Expect(err).ToNot(HaveOccurred()) 915 Expect(len(readBytes)).To(BeNumerically(">", UploadSize)) 916 return expectedErr 917 } 918 return connection.Make(request, response) 919 }, 920 } 921 922 client = NewTestClient(Config{Wrappers: []ConnectionWrapper{wrapper}}) 923 }) 924 925 It("returns the http error", func() { 926 _, _, err := client.UploadDroplet(appGUID, strings.NewReader(strings.Repeat("a", UploadSize)), UploadSize) 927 Expect(err).To(MatchError(expectedErr)) 928 }) 929 }) 930 }) 931 })