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  })