github.com/cloudfoundry-attic/cli-with-i18n@v6.32.1-0.20171002233121-7401370d3b85+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/wrapper" 20 . "github.com/onsi/ginkgo" 21 . "github.com/onsi/ginkgo/extensions/table" 22 . "github.com/onsi/gomega" 23 . "github.com/onsi/gomega/ghttp" 24 ) 25 26 var _ = Describe("Job", func() { 27 var client *Client 28 29 Describe("Job", func() { 30 DescribeTable("Finished", 31 func(status JobStatus, expected bool) { 32 job := Job{Status: status} 33 Expect(job.Finished()).To(Equal(expected)) 34 }, 35 36 Entry("when failed, it returns false", JobStatusFailed, false), 37 Entry("when finished, it returns true", JobStatusFinished, true), 38 Entry("when queued, it returns false", JobStatusQueued, false), 39 Entry("when running, it returns false", JobStatusRunning, false), 40 ) 41 42 DescribeTable("Failed", 43 func(status JobStatus, expected bool) { 44 job := Job{Status: status} 45 Expect(job.Failed()).To(Equal(expected)) 46 }, 47 48 Entry("when failed, it returns true", JobStatusFailed, true), 49 Entry("when finished, it returns false", JobStatusFinished, false), 50 Entry("when queued, it returns false", JobStatusQueued, false), 51 Entry("when running, it returns false", JobStatusRunning, false), 52 ) 53 }) 54 55 Describe("PollJob", func() { 56 BeforeEach(func() { 57 client = NewTestClient(Config{JobPollingTimeout: time.Minute}) 58 }) 59 60 Context("when the job starts queued and then finishes successfully", func() { 61 BeforeEach(func() { 62 server.AppendHandlers( 63 CombineHandlers( 64 VerifyRequest(http.MethodGet, "/v2/jobs/some-job-guid"), 65 RespondWith(http.StatusAccepted, `{ 66 "metadata": { 67 "guid": "some-job-guid", 68 "created_at": "2016-06-08T16:41:27Z", 69 "url": "/v2/jobs/some-job-guid" 70 }, 71 "entity": { 72 "guid": "some-job-guid", 73 "status": "queued" 74 } 75 }`, http.Header{"X-Cf-Warnings": {"warning-1"}}), 76 )) 77 78 server.AppendHandlers( 79 CombineHandlers( 80 VerifyRequest(http.MethodGet, "/v2/jobs/some-job-guid"), 81 RespondWith(http.StatusAccepted, `{ 82 "metadata": { 83 "guid": "some-job-guid", 84 "created_at": "2016-06-08T16:41:28Z", 85 "url": "/v2/jobs/some-job-guid" 86 }, 87 "entity": { 88 "guid": "some-job-guid", 89 "status": "running" 90 } 91 }`, http.Header{"X-Cf-Warnings": {"warning-2, warning-3"}}), 92 )) 93 94 server.AppendHandlers( 95 CombineHandlers( 96 VerifyRequest(http.MethodGet, "/v2/jobs/some-job-guid"), 97 RespondWith(http.StatusAccepted, `{ 98 "metadata": { 99 "guid": "some-job-guid", 100 "created_at": "2016-06-08T16:41:29Z", 101 "url": "/v2/jobs/some-job-guid" 102 }, 103 "entity": { 104 "guid": "some-job-guid", 105 "status": "finished" 106 } 107 }`, http.Header{"X-Cf-Warnings": {"warning-4"}}), 108 )) 109 }) 110 111 It("should poll until completion", func() { 112 warnings, err := client.PollJob(Job{GUID: "some-job-guid"}) 113 Expect(err).ToNot(HaveOccurred()) 114 Expect(warnings).To(ConsistOf("warning-1", "warning-2", "warning-3", "warning-4")) 115 }) 116 }) 117 118 Context("when the job starts queued and then fails", func() { 119 var jobFailureMessage string 120 BeforeEach(func() { 121 jobFailureMessage = "I am a banana!!!" 122 123 server.AppendHandlers( 124 CombineHandlers( 125 VerifyRequest(http.MethodGet, "/v2/jobs/some-job-guid"), 126 RespondWith(http.StatusAccepted, `{ 127 "metadata": { 128 "guid": "some-job-guid", 129 "created_at": "2016-06-08T16:41:27Z", 130 "url": "/v2/jobs/some-job-guid" 131 }, 132 "entity": { 133 "guid": "some-job-guid", 134 "status": "queued" 135 } 136 }`, http.Header{"X-Cf-Warnings": {"warning-1"}}), 137 )) 138 139 server.AppendHandlers( 140 CombineHandlers( 141 VerifyRequest(http.MethodGet, "/v2/jobs/some-job-guid"), 142 RespondWith(http.StatusAccepted, `{ 143 "metadata": { 144 "guid": "some-job-guid", 145 "created_at": "2016-06-08T16:41:28Z", 146 "url": "/v2/jobs/some-job-guid" 147 }, 148 "entity": { 149 "guid": "some-job-guid", 150 "status": "running" 151 } 152 }`, http.Header{"X-Cf-Warnings": {"warning-2, warning-3"}}), 153 )) 154 155 server.AppendHandlers( 156 CombineHandlers( 157 VerifyRequest(http.MethodGet, "/v2/jobs/some-job-guid"), 158 RespondWith(http.StatusOK, fmt.Sprintf(` 159 { 160 "metadata": { 161 "guid": "some-job-guid", 162 "created_at": "2016-06-08T16:41:29Z", 163 "url": "/v2/jobs/some-job-guid" 164 }, 165 "entity": { 166 "error": "Use of entity>error is deprecated in favor of entity>error_details.", 167 "error_details": { 168 "code": 160001, 169 "description": "%s", 170 "error_code": "CF-AppBitsUploadInvalid" 171 }, 172 "guid": "job-guid", 173 "status": "failed" 174 } 175 } 176 `, jobFailureMessage), http.Header{"X-Cf-Warnings": {"warning-4"}}), 177 )) 178 }) 179 180 It("returns a JobFailedError", func() { 181 warnings, err := client.PollJob(Job{GUID: "some-job-guid"}) 182 Expect(err).To(MatchError(ccerror.JobFailedError{ 183 JobGUID: "some-job-guid", 184 Message: jobFailureMessage, 185 })) 186 Expect(warnings).To(ConsistOf("warning-1", "warning-2", "warning-3", "warning-4")) 187 }) 188 }) 189 190 Context("when retrieving the job errors", func() { 191 BeforeEach(func() { 192 server.AppendHandlers( 193 CombineHandlers( 194 VerifyRequest(http.MethodGet, "/v2/jobs/some-job-guid"), 195 RespondWith(http.StatusAccepted, `{ 196 INVALID YAML 197 }`, http.Header{"X-Cf-Warnings": {"warning-1, warning-2"}}), 198 )) 199 }) 200 201 It("returns the CC error", func() { 202 warnings, err := client.PollJob(Job{GUID: "some-job-guid"}) 203 Expect(warnings).To(ConsistOf("warning-1", "warning-2")) 204 Expect(err.Error()).To(MatchRegexp("invalid character")) 205 }) 206 }) 207 208 Describe("JobPollingTimeout", func() { 209 Context("when the job runs longer than the OverallPollingTimeout", func() { 210 var jobPollingTimeout time.Duration 211 212 BeforeEach(func() { 213 jobPollingTimeout = 100 * time.Millisecond 214 client = NewTestClient(Config{ 215 JobPollingTimeout: jobPollingTimeout, 216 JobPollingInterval: 60 * time.Millisecond, 217 }) 218 219 server.AppendHandlers( 220 CombineHandlers( 221 VerifyRequest(http.MethodGet, "/v2/jobs/some-job-guid"), 222 RespondWith(http.StatusAccepted, `{ 223 "metadata": { 224 "guid": "some-job-guid", 225 "created_at": "2016-06-08T16:41:27Z", 226 "url": "/v2/jobs/some-job-guid" 227 }, 228 "entity": { 229 "guid": "some-job-guid", 230 "status": "queued" 231 } 232 }`, http.Header{"X-Cf-Warnings": {"warning-1"}}), 233 )) 234 235 server.AppendHandlers( 236 CombineHandlers( 237 VerifyRequest(http.MethodGet, "/v2/jobs/some-job-guid"), 238 RespondWith(http.StatusAccepted, `{ 239 "metadata": { 240 "guid": "some-job-guid", 241 "created_at": "2016-06-08T16:41:28Z", 242 "url": "/v2/jobs/some-job-guid" 243 }, 244 "entity": { 245 "guid": "some-job-guid", 246 "status": "running" 247 } 248 }`, http.Header{"X-Cf-Warnings": {"warning-2, warning-3"}}), 249 )) 250 251 server.AppendHandlers( 252 CombineHandlers( 253 VerifyRequest(http.MethodGet, "/v2/jobs/some-job-guid"), 254 RespondWith(http.StatusAccepted, `{ 255 "metadata": { 256 "guid": "some-job-guid", 257 "created_at": "2016-06-08T16:41:29Z", 258 "url": "/v2/jobs/some-job-guid" 259 }, 260 "entity": { 261 "guid": "some-job-guid", 262 "status": "finished" 263 } 264 }`, http.Header{"X-Cf-Warnings": {"warning-4"}}), 265 )) 266 }) 267 268 It("raises a JobTimeoutError", func() { 269 _, err := client.PollJob(Job{GUID: "some-job-guid"}) 270 271 Expect(err).To(MatchError(ccerror.JobTimeoutError{ 272 Timeout: jobPollingTimeout, 273 JobGUID: "some-job-guid", 274 })) 275 }) 276 277 // Fuzzy test to ensure that the overall function time isn't [far] 278 // greater than the OverallPollingTimeout. Since this is partially 279 // dependent on the speed of the system, the expectation is that the 280 // function *should* never exceed three times the timeout. 281 It("does not run [too much] longer than the timeout", func() { 282 startTime := time.Now() 283 _, err := client.PollJob(Job{GUID: "some-job-guid"}) 284 endTime := time.Now() 285 Expect(err).To(HaveOccurred()) 286 287 // If the jobPollingTimeout is less than the PollingInterval, 288 // then the margin may be too small, we should not allow the 289 // jobPollingTimeout to be set to less than the PollingInterval 290 Expect(endTime).To(BeTemporally("~", startTime, 3*jobPollingTimeout)) 291 }) 292 }) 293 }) 294 }) 295 296 Describe("GetJob", func() { 297 BeforeEach(func() { 298 client = NewTestClient() 299 }) 300 301 Context("when no errors are encountered", func() { 302 BeforeEach(func() { 303 jsonResponse := `{ 304 "metadata": { 305 "guid": "job-guid", 306 "created_at": "2016-06-08T16:41:27Z", 307 "url": "/v2/jobs/job-guid" 308 }, 309 "entity": { 310 "guid": "job-guid", 311 "status": "queued" 312 } 313 }` 314 315 server.AppendHandlers( 316 CombineHandlers( 317 VerifyRequest(http.MethodGet, "/v2/jobs/job-guid"), 318 RespondWith(http.StatusOK, jsonResponse, http.Header{"X-Cf-Warnings": {"warning-1, warning-2"}}), 319 )) 320 }) 321 322 It("returns job with all warnings", func() { 323 job, warnings, err := client.GetJob("job-guid") 324 325 Expect(err).NotTo(HaveOccurred()) 326 Expect(warnings).To(ConsistOf(Warnings{"warning-1", "warning-2"})) 327 Expect(job.GUID).To(Equal("job-guid")) 328 Expect(job.Status).To(Equal(JobStatusQueued)) 329 }) 330 }) 331 332 Context("when the job fails", func() { 333 BeforeEach(func() { 334 jsonResponse := ` 335 { 336 "metadata": { 337 "guid": "some-job-guid", 338 "created_at": "2016-06-08T16:41:29Z", 339 "url": "/v2/jobs/some-job-guid" 340 }, 341 "entity": { 342 "error": "Use of entity>error is deprecated in favor of entity>error_details.", 343 "error_details": { 344 "code": 160001, 345 "description": "some-error", 346 "error_code": "CF-AppBitsUploadInvalid" 347 }, 348 "guid": "job-guid", 349 "status": "failed" 350 } 351 } 352 ` 353 server.AppendHandlers( 354 CombineHandlers( 355 VerifyRequest(http.MethodGet, "/v2/jobs/job-guid"), 356 RespondWith(http.StatusOK, jsonResponse, http.Header{"X-Cf-Warnings": {"warning-1, warning-2"}}), 357 )) 358 }) 359 360 It("returns job with all warnings", func() { 361 job, warnings, err := client.GetJob("job-guid") 362 363 Expect(err).NotTo(HaveOccurred()) 364 Expect(warnings).To(ConsistOf(Warnings{"warning-1", "warning-2"})) 365 Expect(job.GUID).To(Equal("job-guid")) 366 Expect(job.Status).To(Equal(JobStatusFailed)) 367 Expect(job.Error).To(Equal("Use of entity>error is deprecated in favor of entity>error_details.")) 368 Expect(job.ErrorDetails.Description).To(Equal("some-error")) 369 }) 370 }) 371 }) 372 373 Describe("UploadApplicationPackage", func() { 374 BeforeEach(func() { 375 client = NewTestClient() 376 }) 377 378 Context("when the upload is successful", func() { 379 var ( 380 resources []Resource 381 reader io.Reader 382 readerBody []byte 383 ) 384 385 BeforeEach(func() { 386 resources = []Resource{ 387 {Filename: "foo"}, 388 {Filename: "bar"}, 389 } 390 391 readerBody = []byte("hello world") 392 reader = bytes.NewReader(readerBody) 393 394 verifyHeaderAndBody := func(_ http.ResponseWriter, req *http.Request) { 395 contentType := req.Header.Get("Content-Type") 396 Expect(contentType).To(MatchRegexp("multipart/form-data; boundary=[\\w\\d]+")) 397 398 defer req.Body.Close() 399 reader := multipart.NewReader(req.Body, contentType[30:]) 400 401 // Verify that matched resources are sent properly 402 resourcesPart, err := reader.NextPart() 403 Expect(err).NotTo(HaveOccurred()) 404 405 Expect(resourcesPart.FormName()).To(Equal("resources")) 406 407 defer resourcesPart.Close() 408 expectedJSON, err := json.Marshal(resources) 409 Expect(err).NotTo(HaveOccurred()) 410 Expect(ioutil.ReadAll(resourcesPart)).To(MatchJSON(expectedJSON)) 411 412 // Verify that the application bits are sent properly 413 resourcesPart, err = reader.NextPart() 414 Expect(err).NotTo(HaveOccurred()) 415 416 Expect(resourcesPart.FormName()).To(Equal("application")) 417 Expect(resourcesPart.FileName()).To(Equal("application.zip")) 418 419 defer resourcesPart.Close() 420 Expect(ioutil.ReadAll(resourcesPart)).To(Equal(readerBody)) 421 } 422 423 response := `{ 424 "metadata": { 425 "guid": "job-guid", 426 "url": "/v2/jobs/job-guid" 427 }, 428 "entity": { 429 "guid": "job-guid", 430 "status": "queued" 431 } 432 }` 433 434 server.AppendHandlers( 435 CombineHandlers( 436 VerifyRequest(http.MethodPut, "/v2/apps/some-app-guid/bits", "async=true"), 437 verifyHeaderAndBody, 438 RespondWith(http.StatusOK, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 439 ), 440 ) 441 }) 442 443 It("returns the created job and warnings", func() { 444 job, warnings, err := client.UploadApplicationPackage("some-app-guid", resources, reader, int64(len(readerBody))) 445 Expect(err).NotTo(HaveOccurred()) 446 Expect(warnings).To(ConsistOf("this is a warning")) 447 Expect(job).To(Equal(Job{ 448 GUID: "job-guid", 449 Status: JobStatusQueued, 450 })) 451 }) 452 }) 453 454 Context("when the CC returns an error", func() { 455 BeforeEach(func() { 456 response := `{ 457 "code": 30003, 458 "description": "Banana", 459 "error_code": "CF-Banana" 460 }` 461 462 server.AppendHandlers( 463 CombineHandlers( 464 VerifyRequest(http.MethodPut, "/v2/apps/some-app-guid/bits", "async=true"), 465 RespondWith(http.StatusNotFound, response, http.Header{"X-Cf-Warnings": {"this is a warning"}}), 466 ), 467 ) 468 }) 469 470 It("returns the error", func() { 471 _, warnings, err := client.UploadApplicationPackage("some-app-guid", []Resource{}, bytes.NewReader(nil), 0) 472 Expect(err).To(MatchError(ccerror.ResourceNotFoundError{Message: "Banana"})) 473 Expect(warnings).To(ConsistOf("this is a warning")) 474 }) 475 }) 476 477 Context("when passed a nil resources", func() { 478 It("returns a NilObjectError", func() { 479 _, _, err := client.UploadApplicationPackage("some-app-guid", nil, bytes.NewReader(nil), 0) 480 Expect(err).To(MatchError(ccerror.NilObjectError{Object: "existingResources"})) 481 }) 482 }) 483 484 Context("when passed a nil reader", func() { 485 It("returns a NilObjectError", func() { 486 _, _, err := client.UploadApplicationPackage("some-app-guid", []Resource{}, nil, 0) 487 Expect(err).To(MatchError(ccerror.NilObjectError{Object: "newResources"})) 488 }) 489 }) 490 491 Context("when an error is returned from the new resources reader", func() { 492 var ( 493 fakeReader *ccv2fakes.FakeReader 494 expectedErr error 495 ) 496 497 BeforeEach(func() { 498 expectedErr = errors.New("some read error") 499 fakeReader = new(ccv2fakes.FakeReader) 500 fakeReader.ReadReturns(0, expectedErr) 501 502 server.AppendHandlers( 503 VerifyRequest(http.MethodPut, "/v2/apps/some-app-guid/bits", "async=true"), 504 ) 505 }) 506 507 It("returns the error", func() { 508 _, _, err := client.UploadApplicationPackage("some-app-guid", []Resource{}, fakeReader, 3) 509 Expect(err).To(MatchError(expectedErr)) 510 }) 511 }) 512 513 Context("when a retryable error occurs", func() { 514 BeforeEach(func() { 515 wrapper := &wrapper.CustomWrapper{ 516 CustomMake: func(connection cloudcontroller.Connection, request *cloudcontroller.Request, response *cloudcontroller.Response) error { 517 defer GinkgoRecover() // Since this will be running in a thread 518 519 if strings.HasSuffix(request.URL.String(), "/v2/apps/some-app-guid/bits?async=true") { 520 _, err := ioutil.ReadAll(request.Body) 521 Expect(err).ToNot(HaveOccurred()) 522 Expect(request.Body.Close()).ToNot(HaveOccurred()) 523 return request.ResetBody() 524 } 525 return connection.Make(request, response) 526 }, 527 } 528 529 client = NewTestClient(Config{Wrappers: []ConnectionWrapper{wrapper}}) 530 }) 531 532 It("returns the PipeSeekError", func() { 533 _, _, err := client.UploadApplicationPackage("some-app-guid", []Resource{}, strings.NewReader("hello world"), 3) 534 Expect(err).To(MatchError(ccerror.PipeSeekError{})) 535 }) 536 }) 537 538 Context("when an http error occurs mid-transfer", func() { 539 var expectedErr error 540 const UploadSize = 33 * 1024 541 542 BeforeEach(func() { 543 expectedErr = errors.New("some read error") 544 545 wrapper := &wrapper.CustomWrapper{ 546 CustomMake: func(connection cloudcontroller.Connection, request *cloudcontroller.Request, response *cloudcontroller.Response) error { 547 defer GinkgoRecover() // Since this will be running in a thread 548 549 if strings.HasSuffix(request.URL.String(), "/v2/apps/some-app-guid/bits?async=true") { 550 defer request.Body.Close() 551 readBytes, err := ioutil.ReadAll(request.Body) 552 Expect(err).ToNot(HaveOccurred()) 553 Expect(len(readBytes)).To(BeNumerically(">", UploadSize)) 554 return expectedErr 555 } 556 return connection.Make(request, response) 557 }, 558 } 559 560 client = NewTestClient(Config{Wrappers: []ConnectionWrapper{wrapper}}) 561 }) 562 563 It("returns the http error", func() { 564 _, _, err := client.UploadApplicationPackage("some-app-guid", []Resource{}, strings.NewReader(strings.Repeat("a", UploadSize)), 3) 565 Expect(err).To(MatchError(expectedErr)) 566 }) 567 }) 568 }) 569 })