github.com/sleungcy/cli@v7.1.0+incompatible/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 DescribeTable("Errors converts JobErrorDetails", 45 func(code int, expectedErrType error) { 46 rawErr := JobErrorDetails{ 47 Code: constant.JobErrorCode(code), 48 Detail: fmt.Sprintf("code %d", code), 49 Title: "some-err-title", 50 } 51 52 job := Job{ 53 GUID: "some-job-guid", 54 RawErrors: []JobErrorDetails{rawErr}, 55 } 56 57 Expect(job.Errors()).To(HaveLen(1)) 58 Expect(job.Errors()[0]).To(MatchError(expectedErrType)) 59 }, 60 61 Entry("BuildpackNameStackTaken", 290000, ccerror.BuildpackAlreadyExistsForStackError{Message: "code 290000"}), 62 Entry("BuildpackInvalid", 290003, ccerror.BuildpackInvalidError{Message: "code 290003"}), 63 Entry("BuildpackStacksDontMatch", 390011, ccerror.BuildpackStacksDontMatchError{Message: "code 390011"}), 64 Entry("BuildpackStackDoesNotExist", 390012, ccerror.BuildpackStackDoesNotExistError{Message: "code 390012"}), 65 Entry("BuildpackZipError", 390013, ccerror.BuildpackZipInvalidError{Message: "code 390013"}), 66 Entry("V3JobFailedError", 1111111, ccerror.V3JobFailedError{JobGUID: "some-job-guid", Code: constant.JobErrorCode(1111111), Detail: "code 1111111", Title: "some-err-title"}), 67 ) 68 }) 69 70 Describe("GetJob", func() { 71 var ( 72 jobLocation JobURL 73 74 job Job 75 warnings Warnings 76 executeErr error 77 ) 78 79 BeforeEach(func() { 80 client, _ = NewTestClient() 81 jobLocation = JobURL(fmt.Sprintf("%s/some-job-location", server.URL())) 82 }) 83 84 JustBeforeEach(func() { 85 job, warnings, executeErr = client.GetJob(jobLocation) 86 }) 87 88 When("no errors are encountered", func() { 89 BeforeEach(func() { 90 jsonResponse := `{ 91 "guid": "job-guid", 92 "created_at": "2016-06-08T16:41:27Z", 93 "updated_at": "2016-06-08T16:41:27Z", 94 "operation": "app.delete", 95 "state": "PROCESSING", 96 "warnings": [{"detail": "a warning"}, {"detail": "another warning"}], 97 "links": { 98 "self": { 99 "href": "/v3/jobs/job-guid" 100 } 101 } 102 } 103 }` 104 105 server.AppendHandlers( 106 CombineHandlers( 107 VerifyRequest(http.MethodGet, "/some-job-location"), 108 RespondWith(http.StatusOK, jsonResponse, http.Header{"X-Cf-Warnings": {"warning-1, warning-2"}}), 109 )) 110 }) 111 112 It("returns job with all warnings", func() { 113 Expect(executeErr).NotTo(HaveOccurred()) 114 Expect(warnings).To(ConsistOf(Warnings{"warning-1", "warning-2", "a warning", "another warning"})) 115 Expect(job.GUID).To(Equal("job-guid")) 116 Expect(job.State).To(Equal(constant.JobProcessing)) 117 }) 118 }) 119 120 When("the job fails", func() { 121 BeforeEach(func() { 122 jsonResponse := `{ 123 "guid": "job-guid", 124 "created_at": "2016-06-08T16:41:27Z", 125 "updated_at": "2016-06-08T16:41:27Z", 126 "operation": "delete", 127 "state": "FAILED", 128 "errors": [ 129 { 130 "detail": "blah blah", 131 "title": "CF-JobFail", 132 "code": 1234 133 } 134 ], 135 "links": { 136 "self": { 137 "href": "/v3/jobs/job-guid" 138 } 139 } 140 }` 141 142 server.AppendHandlers( 143 CombineHandlers( 144 VerifyRequest(http.MethodGet, "/some-job-location"), 145 RespondWith(http.StatusOK, jsonResponse, http.Header{"X-Cf-Warnings": {"warning-1, warning-2"}}), 146 )) 147 }) 148 149 It("returns job with all warnings", func() { 150 Expect(executeErr).NotTo(HaveOccurred()) 151 Expect(warnings).To(ConsistOf(Warnings{"warning-1", "warning-2"})) 152 Expect(job.GUID).To(Equal("job-guid")) 153 Expect(job.State).To(Equal(constant.JobFailed)) 154 Expect(job.RawErrors[0].Detail).To(Equal("blah blah")) 155 Expect(job.RawErrors[0].Title).To(Equal("CF-JobFail")) 156 Expect(job.RawErrors[0].Code).To(BeEquivalentTo(1234)) 157 }) 158 }) 159 }) 160 161 Describe("PollJob", func() { 162 var ( 163 jobLocation JobURL 164 165 warnings Warnings 166 executeErr error 167 168 startTime time.Time 169 ) 170 171 BeforeEach(func() { 172 client, _ = NewTestClient(Config{JobPollingTimeout: time.Minute}) 173 jobLocation = JobURL(fmt.Sprintf("%s/some-job-location", server.URL())) 174 }) 175 176 JustBeforeEach(func() { 177 startTime = time.Now() 178 warnings, executeErr = client.PollJob(jobLocation) 179 }) 180 181 When("the job starts queued and then finishes successfully", func() { 182 BeforeEach(func() { 183 server.AppendHandlers( 184 CombineHandlers( 185 VerifyRequest(http.MethodGet, "/some-job-location"), 186 RespondWith(http.StatusAccepted, `{ 187 "guid": "job-guid", 188 "created_at": "2016-06-08T16:41:27Z", 189 "updated_at": "2016-06-08T16:41:27Z", 190 "operation": "app.delete", 191 "state": "PROCESSING", 192 "links": { 193 "self": { 194 "href": "/v3/jobs/job-guid" 195 } 196 } 197 }`, http.Header{"X-Cf-Warnings": {"warning-1"}}), 198 )) 199 200 server.AppendHandlers( 201 CombineHandlers( 202 VerifyRequest(http.MethodGet, "/some-job-location"), 203 RespondWith(http.StatusAccepted, `{ 204 "guid": "job-guid", 205 "created_at": "2016-06-08T16:41:27Z", 206 "updated_at": "2016-06-08T16:41:27Z", 207 "operation": "app.delete", 208 "state": "PROCESSING", 209 "links": { 210 "self": { 211 "href": "/v3/jobs/job-guid" 212 } 213 } 214 }`, http.Header{"X-Cf-Warnings": {"warning-2"}}), 215 )) 216 217 server.AppendHandlers( 218 CombineHandlers( 219 VerifyRequest(http.MethodGet, "/some-job-location"), 220 RespondWith(http.StatusAccepted, `{ 221 "guid": "job-guid", 222 "created_at": "2016-06-08T16:41:27Z", 223 "updated_at": "2016-06-08T16:41:27Z", 224 "operation": "app.delete", 225 "state": "COMPLETE", 226 "links": { 227 "self": { 228 "href": "/v3/jobs/job-guid" 229 } 230 } 231 }`, http.Header{"X-Cf-Warnings": {"warning-3, warning-4"}}), 232 )) 233 }) 234 235 It("should poll until completion", func() { 236 Expect(executeErr).ToNot(HaveOccurred()) 237 Expect(warnings).To(ConsistOf("warning-1", "warning-2", "warning-3", "warning-4")) 238 }) 239 }) 240 241 When("the job starts queued and then fails", func() { 242 BeforeEach(func() { 243 server.AppendHandlers( 244 CombineHandlers( 245 VerifyRequest(http.MethodGet, "/some-job-location"), 246 RespondWith(http.StatusAccepted, `{ 247 "guid": "job-guid", 248 "created_at": "2016-06-08T16:41:27Z", 249 "updated_at": "2016-06-08T16:41:27Z", 250 "operation": "app.delete", 251 "state": "PROCESSING", 252 "links": { 253 "self": { 254 "href": "/v3/jobs/job-guid" 255 } 256 } 257 }`, http.Header{"X-Cf-Warnings": {"warning-1"}}), 258 )) 259 260 server.AppendHandlers( 261 CombineHandlers( 262 VerifyRequest(http.MethodGet, "/some-job-location"), 263 RespondWith(http.StatusAccepted, `{ 264 "guid": "job-guid", 265 "created_at": "2016-06-08T16:41:27Z", 266 "updated_at": "2016-06-08T16:41:27Z", 267 "operation": "app.delete", 268 "state": "PROCESSING", 269 "links": { 270 "self": { 271 "href": "/v3/jobs/job-guid" 272 } 273 } 274 }`, http.Header{"X-Cf-Warnings": {"warning-2, warning-3"}}), 275 )) 276 277 server.AppendHandlers( 278 CombineHandlers( 279 VerifyRequest(http.MethodGet, "/some-job-location"), 280 RespondWith(http.StatusOK, fmt.Sprintf(`{ 281 "guid": "job-guid", 282 "created_at": "2016-06-08T16:41:27Z", 283 "updated_at": "2016-06-08T16:41:27Z", 284 "operation": "app.delete", 285 "state": "FAILED", 286 "errors": [ { 287 "detail": "%s", 288 "code": %d 289 } ], 290 "links": { 291 "self": { 292 "href": "/v3/jobs/job-guid" 293 } 294 } 295 }`, "some-message", constant.JobErrorCodeBuildpackAlreadyExistsForStack), http.Header{"X-Cf-Warnings": {"warning-4"}}), 296 )) 297 }) 298 299 It("returns the first error", func() { 300 Expect(executeErr).To(MatchError(ccerror.BuildpackAlreadyExistsForStackError{ 301 Message: "some-message", 302 })) 303 Expect(warnings).To(ConsistOf("warning-1", "warning-2", "warning-3", "warning-4")) 304 }) 305 }) 306 307 Context("polling timeouts", func() { 308 When("the job runs longer than the OverallPollingTimeout", func() { 309 var ( 310 jobPollingTimeout time.Duration 311 fakeClock *ccv3fakes.FakeClock 312 ) 313 314 BeforeEach(func() { 315 jobPollingTimeout = 100 * time.Millisecond 316 client, fakeClock = NewTestClient(Config{ 317 JobPollingTimeout: jobPollingTimeout, 318 }) 319 320 clockTime := time.Now() 321 fakeClock.NowReturnsOnCall(0, clockTime) 322 fakeClock.NowReturnsOnCall(1, clockTime) 323 fakeClock.NowReturnsOnCall(2, clockTime.Add(60*time.Millisecond)) 324 fakeClock.NowReturnsOnCall(3, clockTime.Add(60*time.Millisecond*2)) 325 326 server.AppendHandlers( 327 CombineHandlers( 328 VerifyRequest(http.MethodGet, "/some-job-location"), 329 RespondWith(http.StatusAccepted, `{ 330 "guid": "job-guid", 331 "created_at": "2016-06-08T16:41:27Z", 332 "updated_at": "2016-06-08T16:41:27Z", 333 "operation": "app.delete", 334 "state": "PROCESSING", 335 "links": { 336 "self": { 337 "href": "/v3/jobs/job-guid" 338 } 339 } 340 }`, http.Header{"X-Cf-Warnings": {"warning-1"}}), 341 )) 342 343 server.AppendHandlers( 344 CombineHandlers( 345 VerifyRequest(http.MethodGet, "/some-job-location"), 346 RespondWith(http.StatusAccepted, `{ 347 "guid": "job-guid", 348 "created_at": "2016-06-08T16:41:27Z", 349 "updated_at": "2016-06-08T16:41:27Z", 350 "operation": "app.delete", 351 "state": "PROCESSING", 352 "links": { 353 "self": { 354 "href": "/v3/jobs/job-guid" 355 } 356 } 357 }`, http.Header{"X-Cf-Warnings": {"warning-2, warning-3"}}), 358 )) 359 360 server.AppendHandlers( 361 CombineHandlers( 362 VerifyRequest(http.MethodGet, "/some-job-location"), 363 RespondWith(http.StatusAccepted, `{ 364 "guid": "job-guid", 365 "created_at": "2016-06-08T16:41:27Z", 366 "updated_at": "2016-06-08T16:41:27Z", 367 "operation": "app.delete", 368 "state": "FINISHED", 369 "links": { 370 "self": { 371 "href": "/v3/jobs/job-guid" 372 } 373 } 374 }`, http.Header{"X-Cf-Warnings": {"warning-4"}}), 375 )) 376 }) 377 378 It("raises a JobTimeoutError", func() { 379 Expect(executeErr).To(MatchError(ccerror.JobTimeoutError{ 380 Timeout: jobPollingTimeout, 381 JobGUID: "job-guid", 382 })) 383 Expect(warnings).To(ConsistOf("warning-1", "warning-2", "warning-3")) 384 }) 385 386 // Fuzzy test to ensure that the overall function time isn't [far] 387 // greater than the OverallPollingTimeout. Since this is partially 388 // dependent on the speed of the system, the expectation is that the 389 // function *should* never exceed three times the timeout. 390 It("does not run [too much] longer than the timeout", func() { 391 endTime := time.Now() 392 Expect(executeErr).To(HaveOccurred()) 393 394 // If the jobPollingTimeout is less than the PollingInterval, 395 // then the margin may be too small, we should not allow the 396 // jobPollingTimeout to be set to less than the PollingInterval 397 Expect(endTime).To(BeTemporally("~", startTime, 3*jobPollingTimeout)) 398 }) 399 }) 400 }) 401 }) 402 })