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