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