github.com/wanddynosios/cli/v8@v8.7.9-0.20240221182337-1a92e3a7017f/api/cloudcontroller/ccv3/job_test.go (about) 1 package ccv3_test 2 3 import ( 4 "fmt" 5 "net/http" 6 "time" 7 8 "code.cloudfoundry.org/cli/api/cloudcontroller/ccerror" 9 . "code.cloudfoundry.org/cli/api/cloudcontroller/ccv3" 10 "code.cloudfoundry.org/cli/api/cloudcontroller/ccv3/ccv3fakes" 11 "code.cloudfoundry.org/cli/api/cloudcontroller/ccv3/constant" 12 . "github.com/onsi/ginkgo" 13 . "github.com/onsi/ginkgo/extensions/table" 14 . "github.com/onsi/gomega" 15 . "github.com/onsi/gomega/ghttp" 16 ) 17 18 var _ = Describe("Job", func() { 19 var client *Client 20 21 Describe("Job", func() { 22 DescribeTable("IsComplete", 23 func(status constant.JobState, expected bool) { 24 job := Job{State: status} 25 Expect(job.IsComplete()).To(Equal(expected)) 26 }, 27 28 Entry("when failed, it returns false", constant.JobFailed, false), 29 Entry("when complete, it returns true", constant.JobComplete, true), 30 Entry("when processing, it returns false", constant.JobProcessing, false), 31 ) 32 33 DescribeTable("HasFailed", 34 func(status constant.JobState, expected bool) { 35 job := Job{State: status} 36 Expect(job.HasFailed()).To(Equal(expected)) 37 }, 38 39 Entry("when failed, it returns true", constant.JobFailed, true), 40 Entry("when complete, it returns false", constant.JobComplete, false), 41 Entry("when processing, it returns false", constant.JobProcessing, false), 42 ) 43 44 Describe("IsAt", func() { 45 It("returns true when status matches", func() { 46 job := Job{State: constant.JobComplete} 47 Expect(job.IsAt(constant.JobComplete)).To(BeTrue()) 48 job = Job{State: constant.JobFailed} 49 Expect(job.IsAt(constant.JobFailed)).To(BeTrue()) 50 job = Job{State: constant.JobProcessing} 51 Expect(job.IsAt(constant.JobProcessing)).To(BeTrue()) 52 job = Job{State: constant.JobPolling} 53 Expect(job.IsAt(constant.JobPolling)).To(BeTrue()) 54 }) 55 It("returns false when status does not match", func() { 56 job := Job{State: constant.JobComplete} 57 Expect(job.IsAt(constant.JobFailed)).To(BeFalse()) 58 Expect(job.IsAt(constant.JobProcessing)).To(BeFalse()) 59 Expect(job.IsAt(constant.JobPolling)).To(BeFalse()) 60 job = Job{State: constant.JobPolling} 61 Expect(job.IsAt(constant.JobFailed)).To(BeFalse()) 62 Expect(job.IsAt(constant.JobProcessing)).To(BeFalse()) 63 Expect(job.IsAt(constant.JobComplete)).To(BeFalse()) 64 }) 65 }) 66 67 DescribeTable("Errors converts JobErrorDetails", 68 func(code int, expectedErrType error) { 69 rawErr := JobErrorDetails{ 70 Code: constant.JobErrorCode(code), 71 Detail: fmt.Sprintf("code %d", code), 72 Title: "some-err-title", 73 } 74 75 job := Job{ 76 GUID: "some-job-guid", 77 RawErrors: []JobErrorDetails{rawErr}, 78 } 79 80 Expect(job.Errors()).To(HaveLen(1)) 81 Expect(job.Errors()[0]).To(MatchError(expectedErrType)) 82 }, 83 84 Entry("BuildpackNameStackTaken", 290000, ccerror.BuildpackAlreadyExistsForStackError{Message: "code 290000"}), 85 Entry("BuildpackInvalid", 290003, ccerror.BuildpackInvalidError{Message: "code 290003"}), 86 Entry("BuildpackStacksDontMatch", 390011, ccerror.BuildpackStacksDontMatchError{Message: "code 390011"}), 87 Entry("BuildpackStackDoesNotExist", 390012, ccerror.BuildpackStackDoesNotExistError{Message: "code 390012"}), 88 Entry("BuildpackZipError", 390013, ccerror.BuildpackZipInvalidError{Message: "code 390013"}), 89 Entry("V3JobFailedError", 1111111, ccerror.V3JobFailedError{JobGUID: "some-job-guid", Code: constant.JobErrorCode(1111111), Detail: "code 1111111", Title: "some-err-title"}), 90 ) 91 }) 92 93 Describe("GetJob", func() { 94 var ( 95 jobLocation JobURL 96 97 job Job 98 warnings Warnings 99 executeErr error 100 ) 101 102 BeforeEach(func() { 103 client, _ = NewTestClient() 104 jobLocation = JobURL(fmt.Sprintf("%s/some-job-location", server.URL())) 105 }) 106 107 JustBeforeEach(func() { 108 job, warnings, executeErr = client.GetJob(jobLocation) 109 }) 110 111 When("no errors are encountered", func() { 112 BeforeEach(func() { 113 jsonResponse := `{ 114 "guid": "job-guid", 115 "created_at": "2016-06-08T16:41:27Z", 116 "updated_at": "2016-06-08T16:41:27Z", 117 "operation": "app.delete", 118 "state": "PROCESSING", 119 "warnings": [{"detail": "a warning"}, {"detail": "another warning"}], 120 "links": { 121 "self": { 122 "href": "/v3/jobs/job-guid" 123 } 124 } 125 } 126 }` 127 128 server.AppendHandlers( 129 CombineHandlers( 130 VerifyRequest(http.MethodGet, "/some-job-location"), 131 RespondWith(http.StatusOK, jsonResponse, http.Header{"X-Cf-Warnings": {"warning-1, warning-2"}}), 132 )) 133 }) 134 135 It("returns job with all warnings", func() { 136 Expect(executeErr).NotTo(HaveOccurred()) 137 Expect(warnings).To(ConsistOf(Warnings{"warning-1", "warning-2", "a warning", "another warning"})) 138 Expect(job.GUID).To(Equal("job-guid")) 139 Expect(job.State).To(Equal(constant.JobProcessing)) 140 }) 141 }) 142 143 When("the job fails", func() { 144 BeforeEach(func() { 145 jsonResponse := `{ 146 "guid": "job-guid", 147 "created_at": "2016-06-08T16:41:27Z", 148 "updated_at": "2016-06-08T16:41:27Z", 149 "operation": "delete", 150 "state": "FAILED", 151 "errors": [ 152 { 153 "detail": "blah blah", 154 "title": "CF-JobFail", 155 "code": 1234 156 } 157 ], 158 "links": { 159 "self": { 160 "href": "/v3/jobs/job-guid" 161 } 162 } 163 }` 164 165 server.AppendHandlers( 166 CombineHandlers( 167 VerifyRequest(http.MethodGet, "/some-job-location"), 168 RespondWith(http.StatusOK, jsonResponse, http.Header{"X-Cf-Warnings": {"warning-1, warning-2"}}), 169 )) 170 }) 171 172 It("returns job with all warnings", func() { 173 Expect(executeErr).NotTo(HaveOccurred()) 174 Expect(warnings).To(ConsistOf(Warnings{"warning-1", "warning-2"})) 175 Expect(job.GUID).To(Equal("job-guid")) 176 Expect(job.State).To(Equal(constant.JobFailed)) 177 Expect(job.RawErrors[0].Detail).To(Equal("blah blah")) 178 Expect(job.RawErrors[0].Title).To(Equal("CF-JobFail")) 179 Expect(job.RawErrors[0].Code).To(BeEquivalentTo(1234)) 180 }) 181 }) 182 }) 183 184 When("Polling jobs", func() { 185 var ( 186 jobLocation JobURL 187 188 warnings Warnings 189 executeErr error 190 191 startTime time.Time 192 193 jobPollingTimeout time.Duration 194 fakeClock *ccv3fakes.FakeClock 195 ) 196 197 appendHandler := func(state string, warnings Warnings) { 198 server.AppendHandlers( 199 CombineHandlers( 200 VerifyRequest(http.MethodGet, "/some-job-location"), 201 RespondWith(http.StatusAccepted, fmt.Sprintf(` 202 { 203 "guid": "job-guid", 204 "created_at": "2016-06-08T16:41:27Z", 205 "updated_at": "2016-06-08T16:41:27Z", 206 "operation": "app.delete", 207 "state": "%s", 208 "links": { 209 "self": { 210 "href": "/v3/jobs/job-guid" 211 } 212 } 213 }`, state), http.Header{"X-Cf-Warnings": warnings}), 214 ), 215 ) 216 } 217 218 appendFailureHandler := func(message string, code constant.JobErrorCode, warnings Warnings) { 219 server.AppendHandlers( 220 CombineHandlers( 221 VerifyRequest(http.MethodGet, "/some-job-location"), 222 RespondWith(http.StatusOK, fmt.Sprintf(`{ 223 "guid": "job-guid", 224 "created_at": "2016-06-08T16:41:27Z", 225 "updated_at": "2016-06-08T16:41:27Z", 226 "operation": "app.delete", 227 "state": "FAILED", 228 "errors": [ { 229 "detail": "%s", 230 "code": %d 231 } ], 232 "links": { 233 "self": { 234 "href": "/v3/jobs/job-guid" 235 } 236 } 237 }`, message, code), http.Header{"X-Cf-Warnings": warnings}), 238 ), 239 ) 240 } 241 242 itSkipsEmptyURLs := func() { 243 When("the job URL is empty", func() { 244 BeforeEach(func() { 245 jobLocation = "" 246 }) 247 248 It("immediately succeeds", func() { 249 Expect(executeErr).ToNot(HaveOccurred()) 250 Expect(warnings).To(BeEmpty()) 251 }) 252 }) 253 } 254 255 itFinishesWhenCompleteOrFailed := func() { 256 When("the job starts queued and then finishes successfully", func() { 257 BeforeEach(func() { 258 appendHandler("PROCESSING", Warnings{"warning-1"}) 259 appendHandler("PROCESSING", Warnings{"warning-2"}) 260 appendHandler("COMPLETE", Warnings{"warning-3", "warning-4"}) 261 }) 262 263 It("should poll until completion", func() { 264 Expect(executeErr).ToNot(HaveOccurred()) 265 Expect(warnings).To(ConsistOf("warning-1", "warning-2", "warning-3", "warning-4")) 266 }) 267 }) 268 269 When("the job starts queued and then fails", func() { 270 BeforeEach(func() { 271 appendHandler("PROCESSING", Warnings{"warning-1"}) 272 appendHandler("PROCESSING", Warnings{"warning-2", "warning-3"}) 273 }) 274 Context("with an error", func() { 275 BeforeEach(func() { 276 appendFailureHandler("some-message", constant.JobErrorCodeBuildpackAlreadyExistsForStack, Warnings{"warning-4"}) 277 }) 278 It("returns the first error", func() { 279 Expect(executeErr).To(MatchError(ccerror.BuildpackAlreadyExistsForStackError{ 280 Message: "some-message", 281 })) 282 Expect(warnings).To(ConsistOf("warning-1", "warning-2", "warning-3", "warning-4")) 283 }) 284 }) 285 Context("without an error", func() { 286 BeforeEach(func() { 287 server.AppendHandlers( 288 CombineHandlers( 289 VerifyRequest(http.MethodGet, "/some-job-location"), 290 RespondWith(http.StatusOK, `{ 291 "guid": "job-guid", 292 "created_at": "2016-06-08T16:41:27Z", 293 "updated_at": "2016-06-08T16:41:27Z", 294 "operation": "app.delete", 295 "state": "FAILED", 296 "errors": null, 297 "links": { 298 "self": { 299 "href": "/v3/jobs/job-guid" 300 } 301 } 302 }`, http.Header{"X-Cf-Warnings": []string{"warning-4"}}), 303 ), 304 ) 305 }) 306 307 It("returns the JobFailedNoErrorError", func() { 308 Expect(executeErr).To(MatchError(ccerror.JobFailedNoErrorError{ 309 JobGUID: "job-guid", 310 })) 311 Expect(warnings).To(ConsistOf("warning-1", "warning-2", "warning-3", "warning-4")) 312 }) 313 }) 314 }) 315 316 } 317 318 itRespectsTimeouts := func() { 319 Context("polling timeouts", func() { 320 When("the job runs longer than the OverallPollingTimeout", func() { 321 BeforeEach(func() { 322 jobPollingTimeout = 100 * time.Millisecond 323 client, fakeClock = NewTestClient(Config{ 324 JobPollingTimeout: jobPollingTimeout, 325 }) 326 327 clockTime := time.Now() 328 fakeClock.NowReturnsOnCall(0, clockTime) 329 fakeClock.NowReturnsOnCall(1, clockTime) 330 fakeClock.NowReturnsOnCall(2, clockTime.Add(60*time.Millisecond)) 331 fakeClock.NowReturnsOnCall(3, clockTime.Add(60*time.Millisecond*2)) 332 333 appendHandler("PROCESSING", Warnings{"warning-1"}) 334 appendHandler("PROCESSING", Warnings{"warning-2", "warning-3"}) 335 appendHandler("FINISHED", Warnings{"warning-4"}) 336 }) 337 338 It("raises a JobTimeoutError", func() { 339 Expect(executeErr).To(MatchError(ccerror.JobTimeoutError{ 340 Timeout: jobPollingTimeout, 341 JobGUID: "job-guid", 342 })) 343 Expect(warnings).To(ConsistOf("warning-1", "warning-2", "warning-3")) 344 }) 345 346 // Fuzzy test to ensure that the overall function time isn't [far] 347 // greater than the OverallPollingTimeout. Since this is partially 348 // dependent on the speed of the system, the expectation is that the 349 // function *should* never exceed three times the timeout. 350 It("does not run [too much] longer than the timeout", func() { 351 endTime := time.Now() 352 Expect(executeErr).To(HaveOccurred()) 353 354 // If the jobPollingTimeout is less than the PollingInterval, 355 // then the margin may be too small, we should not allow the 356 // jobPollingTimeout to be set to less than the PollingInterval 357 Expect(endTime).To(BeTemporally("~", startTime, 3*jobPollingTimeout)) 358 }) 359 }) 360 }) 361 } 362 363 BeforeEach(func() { 364 client, _ = NewTestClient(Config{JobPollingTimeout: time.Minute}) 365 jobLocation = JobURL(fmt.Sprintf("%s/some-job-location", server.URL())) 366 }) 367 368 Describe("PollJob", func() { 369 JustBeforeEach(func() { 370 startTime = time.Now() 371 warnings, executeErr = client.PollJob(jobLocation) 372 }) 373 374 itSkipsEmptyURLs() 375 itFinishesWhenCompleteOrFailed() 376 itRespectsTimeouts() 377 }) 378 379 Describe("PollJobForState", func() { 380 JustBeforeEach(func() { 381 startTime = time.Now() 382 warnings, executeErr = client.PollJobForState(jobLocation, constant.JobPolling) 383 }) 384 385 itSkipsEmptyURLs() 386 itFinishesWhenCompleteOrFailed() 387 itRespectsTimeouts() 388 389 When("Job reaches the required state", func() { 390 BeforeEach(func() { 391 appendHandler("PROCESSING", Warnings{"warning-1"}) 392 appendHandler("PROCESSING", Warnings{"warning-2"}) 393 appendHandler("POLLING", Warnings{"warning-3"}) 394 }) 395 396 It("should poll until the desired state", func() { 397 Expect(executeErr).ToNot(HaveOccurred()) 398 Expect(warnings).To(ConsistOf("warning-1", "warning-2", "warning-3")) 399 }) 400 }) 401 }) 402 403 Describe("PollJobToEventStream", func() { 404 var stream chan PollJobEvent 405 406 JustBeforeEach(func() { 407 startTime = time.Now() 408 stream = client.PollJobToEventStream(jobLocation) 409 }) 410 411 When("the job URL is empty", func() { 412 BeforeEach(func() { 413 jobLocation = "" 414 }) 415 416 It("closes the channel without events", func() { 417 Consistently(stream).ShouldNot(Receive()) 418 Eventually(stream).Should(BeClosed()) 419 }) 420 }) 421 422 When("the job starts queued and then finishes successfully", func() { 423 BeforeEach(func() { 424 appendHandler("PROCESSING", Warnings{"warning-1"}) 425 appendHandler("PROCESSING", Warnings{"warning-2"}) 426 appendHandler("COMPLETE", Warnings{"warning-3", "warning-4"}) 427 }) 428 429 It("should poll until completion", func() { 430 Eventually(stream).Should(Receive(Equal(PollJobEvent{ 431 State: constant.JobProcessing, 432 Err: nil, 433 Warnings: Warnings{"warning-1"}, 434 }))) 435 Eventually(stream).Should(Receive(Equal(PollJobEvent{ 436 State: constant.JobProcessing, 437 Err: nil, 438 Warnings: Warnings{"warning-2"}, 439 }))) 440 Eventually(stream).Should(Receive(Equal(PollJobEvent{ 441 State: constant.JobComplete, 442 Err: nil, 443 Warnings: Warnings{"warning-3", "warning-4"}, 444 }))) 445 Eventually(stream).Should(BeClosed()) 446 }) 447 }) 448 449 When("the job starts queued and then fails", func() { 450 BeforeEach(func() { 451 appendHandler("PROCESSING", Warnings{"warning-1"}) 452 appendHandler("PROCESSING", Warnings{"warning-2", "warning-3"}) 453 appendFailureHandler("some-message", constant.JobErrorCodeBuildpackAlreadyExistsForStack, Warnings{"warning-4"}) 454 }) 455 456 It("returns the first error", func() { 457 Eventually(stream).Should(Receive(Equal(PollJobEvent{ 458 State: constant.JobProcessing, 459 Err: nil, 460 Warnings: Warnings{"warning-1"}, 461 }))) 462 Eventually(stream).Should(Receive(Equal(PollJobEvent{ 463 State: constant.JobProcessing, 464 Err: nil, 465 Warnings: Warnings{"warning-2", "warning-3"}, 466 }))) 467 Eventually(stream).Should(Receive(Equal(PollJobEvent{ 468 State: constant.JobFailed, 469 Err: ccerror.BuildpackAlreadyExistsForStackError{Message: "some-message"}, 470 Warnings: Warnings{"warning-4"}, 471 }))) 472 Eventually(stream).Should(BeClosed()) 473 }) 474 }) 475 476 Context("polling timeouts", func() { 477 When("the job runs longer than the OverallPollingTimeout", func() { 478 BeforeEach(func() { 479 jobPollingTimeout = 100 * time.Millisecond 480 client, fakeClock = NewTestClient(Config{ 481 JobPollingTimeout: jobPollingTimeout, 482 }) 483 484 clockTime := time.Now() 485 fakeClock.NowReturnsOnCall(0, clockTime) 486 fakeClock.NowReturnsOnCall(1, clockTime) 487 fakeClock.NowReturnsOnCall(2, clockTime.Add(60*time.Millisecond)) 488 fakeClock.NowReturnsOnCall(3, clockTime.Add(60*time.Millisecond*2)) 489 490 appendHandler("PROCESSING", Warnings{"warning-1"}) 491 appendHandler("PROCESSING", Warnings{"warning-2", "warning-3"}) 492 appendHandler("PROCESSING", Warnings{"warning-4"}) 493 }) 494 495 It("returns a JobTimeoutError", func() { 496 Eventually(stream).Should(Receive(Equal(PollJobEvent{ 497 State: constant.JobProcessing, 498 Err: ccerror.JobTimeoutError{ 499 Timeout: jobPollingTimeout, 500 JobGUID: "job-guid", 501 }, 502 Warnings: Warnings{"warning-4"}, 503 }))) 504 Eventually(stream).Should(BeClosed()) 505 }) 506 507 // Fuzzy test to ensure that the overall function time isn't [far] 508 // greater than the OverallPollingTimeout. Since this is partially 509 // dependent on the speed of the system, the expectation is that the 510 // function *should* never exceed three times the timeout. 511 It("does not run [too much] longer than the timeout", func() { 512 Eventually(stream).Should(BeClosed()) 513 endTime := time.Now() 514 515 // If the jobPollingTimeout is less than the PollingInterval, 516 // then the margin may be too small, we should not allow the 517 // jobPollingTimeout to be set to less than the PollingInterval 518 Expect(endTime).To(BeTemporally("~", startTime, 3*jobPollingTimeout)) 519 }) 520 }) 521 }) 522 }) 523 }) 524 })