github.com/cloudfoundry/cli@v7.1.0+incompatible/actor/v2action/logging_test.go (about)

     1  package v2action_test
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"time"
     7  
     8  	"code.cloudfoundry.org/cli/actor/sharedaction"
     9  	"code.cloudfoundry.org/cli/actor/sharedaction/sharedactionfakes"
    10  	. "code.cloudfoundry.org/cli/actor/v2action"
    11  	"code.cloudfoundry.org/cli/actor/v2action/v2actionfakes"
    12  	"code.cloudfoundry.org/cli/api/cloudcontroller/ccv2"
    13  	"code.cloudfoundry.org/cli/api/uaa"
    14  	"code.cloudfoundry.org/cli/integration/helpers"
    15  	logcache "code.cloudfoundry.org/go-log-cache"
    16  	"code.cloudfoundry.org/go-loggregator/rpc/loggregator_v2"
    17  	. "github.com/onsi/ginkgo"
    18  	. "github.com/onsi/gomega"
    19  )
    20  
    21  var _ = Describe("Logging Actions", func() {
    22  	var (
    23  		actor                     *Actor
    24  		fakeCloudControllerClient *v2actionfakes.FakeCloudControllerClient
    25  		fakeUAAClient             *v2actionfakes.FakeUAAClient
    26  		fakeConfig                *v2actionfakes.FakeConfig
    27  
    28  		fakeLogCacheClient *sharedactionfakes.FakeLogCacheClient
    29  	)
    30  
    31  	BeforeEach(func() {
    32  		actor, fakeCloudControllerClient, fakeUAAClient, fakeConfig = NewTestActor()
    33  		fakeLogCacheClient = new(sharedactionfakes.FakeLogCacheClient)
    34  		fakeConfig.AccessTokenReturns("AccessTokenForTest")
    35  	})
    36  
    37  	Describe("LogMessage", func() {
    38  		Describe("Staging", func() {
    39  			When("the log is a staging log", func() {
    40  				It("returns true", func() {
    41  					message := sharedaction.NewLogMessage("", "OUT", time.Now(), "STG", "")
    42  					Expect(message.Staging()).To(BeTrue())
    43  				})
    44  			})
    45  
    46  			When("the log is any other kind of log", func() {
    47  				It("returns true", func() {
    48  					message := sharedaction.NewLogMessage("", "OUT", time.Now(), "APP", "")
    49  					Expect(message.Staging()).To(BeFalse())
    50  				})
    51  			})
    52  		})
    53  	})
    54  
    55  	Describe("GetStreamingLogs", func() {
    56  		var (
    57  			expectedAppGUID string
    58  
    59  			messages      <-chan sharedaction.LogMessage
    60  			errs          <-chan error
    61  			stopStreaming context.CancelFunc
    62  
    63  			message sharedaction.LogMessage
    64  		)
    65  
    66  		BeforeEach(func() {
    67  			expectedAppGUID = "some-app-guid"
    68  		})
    69  
    70  		AfterEach(func() {
    71  			Eventually(messages).Should(BeClosed())
    72  			Eventually(errs).Should(BeClosed())
    73  		})
    74  
    75  		JustBeforeEach(func() {
    76  			messages, errs, stopStreaming = actor.GetStreamingLogs(expectedAppGUID, fakeLogCacheClient)
    77  		})
    78  
    79  		When("receiving events", func() {
    80  			BeforeEach(func() {
    81  				fakeConfig.DialTimeoutReturns(60 * time.Minute)
    82  				fakeLogCacheClient.ReadStub = func(
    83  					ctx context.Context,
    84  					sourceID string,
    85  					start time.Time,
    86  					opts ...logcache.ReadOption,
    87  				) ([]*loggregator_v2.Envelope, error) {
    88  					if fakeLogCacheClient.ReadCallCount() > 2 {
    89  						stopStreaming()
    90  					}
    91  
    92  					return []*loggregator_v2.Envelope{{
    93  							// 3 seconds in the past to get past Walk delay
    94  							Timestamp:  time.Now().Add(-4 * time.Second).UnixNano(),
    95  							SourceId:   expectedAppGUID,
    96  							InstanceId: "some-source-instance",
    97  							Message: &loggregator_v2.Envelope_Log{
    98  								Log: &loggregator_v2.Log{
    99  									Payload: []byte("message-1"),
   100  									Type:    loggregator_v2.Log_OUT,
   101  								},
   102  							},
   103  							Tags: map[string]string{
   104  								"source_type": "some-source-type",
   105  							},
   106  						}, {
   107  							// 2 seconds in the past to get past Walk delay
   108  							Timestamp:  time.Now().Add(-3 * time.Second).UnixNano(),
   109  							SourceId:   expectedAppGUID,
   110  							InstanceId: "some-source-instance",
   111  							Message: &loggregator_v2.Envelope_Log{
   112  								Log: &loggregator_v2.Log{
   113  									Payload: []byte("message-2"),
   114  									Type:    loggregator_v2.Log_OUT,
   115  								},
   116  							},
   117  							Tags: map[string]string{
   118  								"source_type": "some-source-type",
   119  							},
   120  						}, {
   121  							Timestamp:  time.Now().Add(-2005 * time.Millisecond).UnixNano(),
   122  							SourceId:   expectedAppGUID,
   123  							InstanceId: "some-source-instance",
   124  							Message: &loggregator_v2.Envelope_Log{
   125  								Log: &loggregator_v2.Log{
   126  									Payload: []byte("message-3"),
   127  									Type:    loggregator_v2.Log_OUT,
   128  								},
   129  							},
   130  							Tags: map[string]string{
   131  								"source_type": "some-source-type",
   132  							},
   133  						}, {
   134  							Timestamp:  time.Now().Add(-1900 * time.Millisecond).UnixNano(),
   135  							SourceId:   expectedAppGUID,
   136  							InstanceId: "some-source-instance",
   137  							Message: &loggregator_v2.Envelope_Log{
   138  								Log: &loggregator_v2.Log{
   139  									Payload: []byte("message-4"),
   140  									Type:    loggregator_v2.Log_OUT,
   141  								},
   142  							},
   143  							Tags: map[string]string{
   144  								"source_type": "some-source-type",
   145  							},
   146  						},
   147  						},
   148  						ctx.Err()
   149  				}
   150  			})
   151  
   152  			It("converts them to log messages, does not sort them, and passes them through the messages channel", func() {
   153  				Eventually(messages).Should(Receive(&message))
   154  				Expect(message.Message()).To(Equal("message-1"))
   155  				Expect(message.Type()).To(Equal("OUT"))
   156  
   157  				Eventually(messages).Should(Receive(&message))
   158  				Expect(message.Message()).To(Equal("message-2"))
   159  				Expect(message.Type()).To(Equal("OUT"))
   160  
   161  				Eventually(messages).Should(Receive(&message))
   162  				Expect(message.Message()).To(Equal("message-3"))
   163  				Expect(message.Type()).To(Equal("OUT"))
   164  
   165  				Expect(message.SourceType()).To(Equal("some-source-type"))
   166  				Expect(message.SourceInstance()).To(Equal("some-source-instance"))
   167  
   168  				// The last message in the list is ignored because of the new WalkDelay of 2 seconds
   169  				Consistently(messages).ShouldNot(Receive(&message))
   170  			})
   171  		})
   172  	})
   173  
   174  	Describe("GetRecentLogsForApplicationByNameAndSpace", func() {
   175  		When("the application can be found", func() {
   176  			BeforeEach(func() {
   177  				fakeCloudControllerClient.GetApplicationsReturns(
   178  					[]ccv2.Application{
   179  						{
   180  							Name: "some-app",
   181  							GUID: "some-app-guid",
   182  						},
   183  					},
   184  					ccv2.Warnings{"some-app-warnings"},
   185  					nil,
   186  				)
   187  			})
   188  
   189  			When("LogCache returns logs", func() {
   190  				BeforeEach(func() {
   191  					fakeConfig.DialTimeoutReturns(60 * time.Minute)
   192  
   193  					fakeLogCacheClient.ReadReturns([]*loggregator_v2.Envelope{{
   194  						// 2 seconds in the past to get past Walk delay
   195  						Timestamp:  time.Now().Add(-3 * time.Second).UnixNano(),
   196  						SourceId:   "some-app-guid",
   197  						InstanceId: "some-source-instance",
   198  						Message: &loggregator_v2.Envelope_Log{
   199  							Log: &loggregator_v2.Log{
   200  								Payload: []byte("message-1"),
   201  								Type:    loggregator_v2.Log_OUT,
   202  							},
   203  						},
   204  						Tags: map[string]string{
   205  							"source_type": "some-source-type",
   206  						},
   207  					}, {
   208  						// 2 seconds in the past to get past Walk delay
   209  						Timestamp:  time.Now().Add(-2 * time.Second).UnixNano(),
   210  						SourceId:   "some-app-guid",
   211  						InstanceId: "some-source-instance",
   212  						Message: &loggregator_v2.Envelope_Log{
   213  							Log: &loggregator_v2.Log{
   214  								Payload: []byte("message-2"),
   215  								Type:    loggregator_v2.Log_OUT,
   216  							},
   217  						},
   218  						Tags: map[string]string{
   219  							"source_type": "some-source-type",
   220  						},
   221  					}},
   222  						nil,
   223  					)
   224  				})
   225  
   226  				It("returns all the recent logs and warnings", func() {
   227  					messages, warnings, err := actor.GetRecentLogsForApplicationByNameAndSpace("some-app", "some-space-guid", fakeLogCacheClient)
   228  					Expect(err).ToNot(HaveOccurred())
   229  					Expect(warnings).To(ConsistOf("some-app-warnings"))
   230  					Expect(messages[0].Message()).To(Equal("message-2"))
   231  					Expect(messages[0].Type()).To(Equal("OUT"))
   232  					Expect(messages[0].SourceType()).To(Equal("some-source-type"))
   233  					Expect(messages[0].SourceInstance()).To(Equal("some-source-instance"))
   234  
   235  					Expect(messages[1].Message()).To(Equal("message-1"))
   236  					Expect(messages[1].Type()).To(Equal("OUT"))
   237  					Expect(messages[1].SourceType()).To(Equal("some-source-type"))
   238  					Expect(messages[1].SourceInstance()).To(Equal("some-source-instance"))
   239  				})
   240  			})
   241  
   242  			When("LogCache errors", func() {
   243  				var expectedErr error
   244  
   245  				BeforeEach(func() {
   246  					expectedErr = errors.New("ZOMG")
   247  					fakeLogCacheClient.ReadReturns(nil, expectedErr)
   248  				})
   249  
   250  				It("returns error and warnings", func() {
   251  					_, warnings, err := actor.GetRecentLogsForApplicationByNameAndSpace("some-app", "some-space-guid", fakeLogCacheClient)
   252  					Expect(err).To(MatchError("Failed to retrieve logs from Log Cache: ZOMG"))
   253  					Expect(warnings).To(ConsistOf("some-app-warnings"))
   254  				})
   255  			})
   256  		})
   257  
   258  		When("finding the application errors", func() {
   259  			var expectedErr error
   260  
   261  			BeforeEach(func() {
   262  				expectedErr = errors.New("ZOMG")
   263  				fakeCloudControllerClient.GetApplicationsReturns(
   264  					nil,
   265  					ccv2.Warnings{"some-app-warnings"},
   266  					expectedErr,
   267  				)
   268  			})
   269  
   270  			It("returns error and warnings", func() {
   271  				_, warnings, err := actor.GetRecentLogsForApplicationByNameAndSpace("some-app", "some-space-guid", fakeLogCacheClient)
   272  				Expect(err).To(MatchError(expectedErr))
   273  				Expect(warnings).To(ConsistOf("some-app-warnings"))
   274  
   275  				Expect(fakeLogCacheClient.ReadCallCount()).To(Equal(0))
   276  			})
   277  		})
   278  	})
   279  
   280  	Describe("GetStreamingLogsForApplicationByNameAndSpace", func() {
   281  		When("the application can be found", func() {
   282  			var (
   283  				expectedAppGUID string
   284  
   285  				messages      <-chan sharedaction.LogMessage
   286  				logErrs       <-chan error
   287  				stopStreaming context.CancelFunc
   288  			)
   289  
   290  			AfterEach(func() {
   291  				Eventually(messages).Should(BeClosed())
   292  				Eventually(logErrs).Should(BeClosed())
   293  			})
   294  
   295  			BeforeEach(func() {
   296  				expectedAppGUID = "some-app-guid"
   297  
   298  				fakeCloudControllerClient.GetApplicationsReturns(
   299  					[]ccv2.Application{
   300  						{
   301  							Name: "some-app",
   302  							GUID: expectedAppGUID,
   303  						},
   304  					},
   305  					ccv2.Warnings{"some-app-warnings"},
   306  					nil,
   307  				)
   308  
   309  				fakeConfig.DialTimeoutReturns(60 * time.Minute)
   310  
   311  				fakeLogCacheClient.ReadStub = func(
   312  					ctx context.Context,
   313  					sourceID string,
   314  					start time.Time,
   315  					opts ...logcache.ReadOption,
   316  				) ([]*loggregator_v2.Envelope, error) {
   317  					if fakeLogCacheClient.ReadCallCount() > 2 {
   318  						stopStreaming()
   319  					}
   320  
   321  					return []*loggregator_v2.Envelope{{
   322  						// 2 seconds in the past to get past Walk delay
   323  						Timestamp:  time.Now().Add(-3 * time.Second).UnixNano(),
   324  						SourceId:   expectedAppGUID,
   325  						InstanceId: "some-source-instance",
   326  						Message: &loggregator_v2.Envelope_Log{
   327  							Log: &loggregator_v2.Log{
   328  								Payload: []byte("message-1"),
   329  								Type:    loggregator_v2.Log_OUT,
   330  							},
   331  						},
   332  						Tags: map[string]string{
   333  							"source_type": "some-source-type",
   334  						},
   335  					}, {
   336  						// 2 seconds in the past to get past Walk delay
   337  						Timestamp:  time.Now().Add(-2 * time.Second).UnixNano(),
   338  						SourceId:   expectedAppGUID,
   339  						InstanceId: "some-source-instance",
   340  						Message: &loggregator_v2.Envelope_Log{
   341  							Log: &loggregator_v2.Log{
   342  								Payload: []byte("message-2"),
   343  								Type:    loggregator_v2.Log_ERR,
   344  							},
   345  						},
   346  						Tags: map[string]string{
   347  							"source_type": "some-source-type",
   348  						},
   349  					}}, ctx.Err()
   350  				}
   351  			})
   352  
   353  			It("converts them to log messages and passes them through the messages channel", func() {
   354  				var err error
   355  				var warnings Warnings
   356  				var message sharedaction.LogMessage
   357  
   358  				messages, logErrs, stopStreaming, warnings, err = actor.GetStreamingLogsForApplicationByNameAndSpace("some-app", "some-space-guid", fakeLogCacheClient)
   359  
   360  				Expect(err).ToNot(HaveOccurred())
   361  				Expect(warnings).To(ConsistOf("some-app-warnings"))
   362  
   363  				Eventually(messages).Should(Receive(&message))
   364  				Expect(message.Message()).To(Equal("message-1"))
   365  				Expect(message.Type()).To(Equal("OUT"))
   366  				Expect(message.SourceType()).To(Equal("some-source-type"))
   367  				Expect(message.SourceInstance()).To(Equal("some-source-instance"))
   368  
   369  				Eventually(messages).Should(Receive(&message))
   370  				Expect(message.Message()).To(Equal("message-2"))
   371  				Expect(message.Type()).To(Equal("ERR"))
   372  				Expect(message.SourceType()).To(Equal("some-source-type"))
   373  				Expect(message.SourceInstance()).To(Equal("some-source-instance"))
   374  			})
   375  		})
   376  
   377  		When("finding the application errors", func() {
   378  			var expectedErr error
   379  
   380  			BeforeEach(func() {
   381  				expectedErr = errors.New("ZOMG")
   382  				fakeCloudControllerClient.GetApplicationsReturns(
   383  					nil,
   384  					ccv2.Warnings{"some-app-warnings"},
   385  					expectedErr,
   386  				)
   387  			})
   388  
   389  			It("returns error and warnings", func() {
   390  				_, _, _, warnings, err := actor.GetStreamingLogsForApplicationByNameAndSpace("some-app", "some-space-guid", fakeLogCacheClient)
   391  				Expect(err).To(MatchError(expectedErr))
   392  				Expect(warnings).To(ConsistOf("some-app-warnings"))
   393  
   394  				Expect(fakeLogCacheClient.ReadCallCount()).To(Equal(0))
   395  			})
   396  		})
   397  	})
   398  
   399  	Describe("ScheduleTokenRefresh", func() {
   400  		var (
   401  			stop                   chan struct{}
   402  			stoppedRefreshingToken chan struct{}
   403  			ticker                 chan time.Time
   404  			after                  func(time.Duration) <-chan time.Time
   405  			delay                  chan time.Duration
   406  			errChannel             <-chan error
   407  			err                    error
   408  		)
   409  
   410  		BeforeEach(func() {
   411  			fakeConfig.RefreshTokenReturns(helpers.BuildTokenString(time.Now().Add(5 * time.Minute)))
   412  			ticker = make(chan time.Time)
   413  			delay = make(chan time.Duration, 100)
   414  			after = func(t time.Duration) <-chan time.Time {
   415  				delay <- t
   416  				return ticker
   417  			}
   418  		})
   419  
   420  		JustBeforeEach(func() {
   421  			stop = make(chan struct{})
   422  			stoppedRefreshingToken = make(chan struct{})
   423  			errChannel, err = actor.ScheduleTokenRefresh(after, stop, stoppedRefreshingToken)
   424  		})
   425  
   426  		AfterEach(func() {
   427  			if stop != nil {
   428  				close(stop)
   429  			}
   430  		})
   431  
   432  		When("the access token is not expiring soon", func() {
   433  			BeforeEach(func() {
   434  				fakeConfig.AccessTokenReturns(helpers.BuildTokenString(time.Now().Add(5 * time.Minute)))
   435  			})
   436  
   437  			It("does not refresh the access token", func() {
   438  				Expect(err).NotTo(HaveOccurred())
   439  				Expect(fakeUAAClient.RefreshAccessTokenCallCount()).To(Equal(0))
   440  			})
   441  		})
   442  
   443  		When("the access token is expiring soon", func() {
   444  			BeforeEach(func() {
   445  				fakeConfig.AccessTokenReturns(helpers.BuildTokenString(time.Now().Add(30 * time.Second)))
   446  				fakeUAAClient.RefreshAccessTokenReturns(uaa.RefreshedTokens{
   447  					AccessToken: helpers.BuildTokenString(time.Now().Add(5 * time.Minute)),
   448  					Type:        "bearer",
   449  				}, nil)
   450  			})
   451  
   452  			It("refreshes the access token", func() {
   453  				Expect(err).NotTo(HaveOccurred())
   454  				Expect(fakeUAAClient.RefreshAccessTokenCallCount()).To(Equal(1))
   455  			})
   456  		})
   457  
   458  		When("the access token has already expired", func() {
   459  			BeforeEach(func() {
   460  				fakeConfig.AccessTokenReturns(helpers.BuildTokenString(time.Now().Add(-30 * time.Second)))
   461  				fakeUAAClient.RefreshAccessTokenReturns(uaa.RefreshedTokens{
   462  					AccessToken: helpers.BuildTokenString(time.Now().Add(5 * time.Minute)),
   463  					Type:        "bearer",
   464  				}, nil)
   465  			})
   466  
   467  			It("refreshes the access token", func() {
   468  				Expect(err).NotTo(HaveOccurred())
   469  				Expect(fakeUAAClient.RefreshAccessTokenCallCount()).To(Equal(1))
   470  			})
   471  
   472  			When("and the attempt to refresh the access token fails", func() {
   473  				BeforeEach(func() {
   474  					fakeUAAClient.RefreshAccessTokenReturns(uaa.RefreshedTokens{}, errors.New("some error"))
   475  				})
   476  
   477  				It("will not refresh the access token", func() {
   478  					Expect(err).To(MatchError("some error"))
   479  				})
   480  				It("closes a channel to indicate it has finished", func() {
   481  					close(stop)
   482  					Eventually(stoppedRefreshingToken).Should(BeClosed())
   483  				})
   484  				AfterEach(func() {
   485  					stop = nil
   486  				})
   487  			})
   488  		})
   489  
   490  		When("the access token is invalid", func() {
   491  			BeforeEach(func() {
   492  				fakeConfig.AccessTokenReturns("bogus-access-token")
   493  				fakeUAAClient.RefreshAccessTokenReturns(uaa.RefreshedTokens{
   494  					AccessToken: helpers.BuildTokenString(time.Now().Add(5 * time.Minute)),
   495  					Type:        "bearer",
   496  				}, nil)
   497  			})
   498  			It("refreshes the access token", func() {
   499  				Expect(err).NotTo(HaveOccurred())
   500  				Expect(fakeUAAClient.RefreshAccessTokenCallCount()).To(Equal(1))
   501  			})
   502  		})
   503  
   504  		When("the access token expires while we are streaming logs", func() {
   505  
   506  			BeforeEach(func() {
   507  				fakeConfig.AccessTokenReturns(helpers.BuildTokenString(time.Now().Add(2 * time.Minute)))
   508  				fakeConfig.RefreshTokenReturns(helpers.BuildTokenString(time.Now().Add(2 * time.Minute)))
   509  			})
   510  
   511  			JustBeforeEach(func() {
   512  				stop = make(chan struct{})
   513  				stoppedRefreshingToken = make(chan struct{})
   514  				errChannel, err = actor.ScheduleTokenRefresh(after, stop, stoppedRefreshingToken)
   515  			})
   516  
   517  			It("refreshes the access token", func() {
   518  				Expect(err).NotTo(HaveOccurred())
   519  
   520  				By("not initially refreshing the token when it is unnecessary")
   521  				Expect(fakeUAAClient.RefreshAccessTokenCallCount()).To(Equal(0))
   522  
   523  				By("refreshing the first access token when it is expiring soon")
   524  				fakeConfig.AccessTokenReturns(helpers.BuildTokenString(time.Now().Add(30 * time.Second)))
   525  				ticker <- time.Time{}
   526  				Eventually(fakeUAAClient.RefreshAccessTokenCallCount).Should(Equal(1))
   527  
   528  				By("not refreshing the second access token when it is not close to expiry")
   529  				fakeConfig.AccessTokenReturns(helpers.BuildTokenString(time.Now().Add(90 * time.Second)))
   530  				ticker <- time.Time{}
   531  				Consistently(fakeUAAClient.RefreshAccessTokenCallCount).Should(Equal(1))
   532  
   533  				By("refreshing the second access token when it is near to expiry")
   534  				fakeConfig.AccessTokenReturns(helpers.BuildTokenString(time.Now().Add(30 * time.Second)))
   535  				ticker <- time.Time{}
   536  				Eventually(fakeUAAClient.RefreshAccessTokenCallCount).Should(Equal(2))
   537  			})
   538  
   539  			It("sleeps until the token is approaching expiry", func() {
   540  				fakeConfig.AccessTokenReturns(helpers.BuildTokenString(time.Now().Add(30 * time.Second)))
   541  				Expect(err).NotTo(HaveOccurred())
   542  
   543  				By("sleeps until one minute before the new token is due to expire")
   544  				fakeUAAClient.RefreshAccessTokenReturns(uaa.RefreshedTokens{
   545  					AccessToken: helpers.BuildTokenString(time.Now().Add(5 * time.Minute)),
   546  					Type:        "bearer",
   547  				}, nil)
   548  
   549  				ticker <- time.Time{}
   550  				Eventually(fakeUAAClient.RefreshAccessTokenCallCount).Should(Equal(1))
   551  				Eventually(delay).Should(Receive(BeNumerically("~", 4*time.Minute, time.Second)))
   552  
   553  				By("refreshing a longer-lived access token")
   554  				fakeUAAClient.RefreshAccessTokenReturns(uaa.RefreshedTokens{
   555  					AccessToken: helpers.BuildTokenString(time.Now().Add(20 * time.Minute)),
   556  					Type:        "bearer",
   557  				}, nil)
   558  				ticker <- time.Time{}
   559  				Eventually(fakeUAAClient.RefreshAccessTokenCallCount).Should(Equal(2))
   560  				Eventually(delay).Should(Receive(BeNumerically("~", 19*time.Minute, time.Second)))
   561  
   562  				By("refreshing a short-lived access token at 90% of its lifetime")
   563  				fakeUAAClient.RefreshAccessTokenReturns(uaa.RefreshedTokens{
   564  					AccessToken: helpers.BuildTokenString(time.Now().Add(30 * time.Second)),
   565  					Type:        "bearer",
   566  				}, nil)
   567  				ticker <- time.Time{}
   568  				Eventually(fakeUAAClient.RefreshAccessTokenCallCount).Should(Equal(3))
   569  				Eventually(delay).Should(Receive(BeNumerically("~", 27*time.Second, time.Second)))
   570  
   571  				By("refreshing a very short-lived access token at 90% of its lifetime")
   572  				fakeUAAClient.RefreshAccessTokenReturns(uaa.RefreshedTokens{
   573  					AccessToken: helpers.BuildTokenString(time.Now().Add(3000 * time.Millisecond)),
   574  					Type:        "bearer",
   575  				}, nil)
   576  				ticker <- time.Time{}
   577  				Eventually(fakeUAAClient.RefreshAccessTokenCallCount).Should(Equal(4))
   578  				Eventually(delay).Should(Receive(BeNumerically("~", 2700*time.Millisecond, time.Second)))
   579  
   580  				By("refreshing a token with a little more than a minute left on it")
   581  				fakeUAAClient.RefreshAccessTokenReturns(uaa.RefreshedTokens{
   582  					AccessToken: helpers.BuildTokenString(time.Now().Add(90 * time.Second)),
   583  					Type:        "bearer",
   584  				}, nil)
   585  				ticker <- time.Time{}
   586  				Eventually(fakeUAAClient.RefreshAccessTokenCallCount).Should(Equal(5))
   587  				Eventually(delay).Should(Receive(BeNumerically("~", 81*time.Second, time.Second)))
   588  			})
   589  
   590  			When("and the token refresh errors", func() {
   591  				BeforeEach(func() {
   592  					fakeUAAClient.RefreshAccessTokenReturns(uaa.RefreshedTokens{}, errors.New("some error"))
   593  				})
   594  				It("propagates the error to the caller", func() {
   595  					Expect(err).NotTo(HaveOccurred())
   596  					Expect(fakeUAAClient.RefreshAccessTokenCallCount()).To(Equal(0))
   597  					fakeConfig.AccessTokenReturns(helpers.BuildTokenString(time.Now().Add(30 * time.Second)))
   598  					go func() {
   599  						defer GinkgoRecover()
   600  						Eventually(errChannel).Should(Receive(MatchError("some error")))
   601  					}()
   602  					ticker <- time.Time{}
   603  					Eventually(fakeUAAClient.RefreshAccessTokenCallCount).Should(Equal(1))
   604  				})
   605  			})
   606  
   607  			When("token refreshing is stopped", func() {
   608  				It("closes a channel to indicate it has finished", func() {
   609  					close(stop)
   610  					Eventually(stoppedRefreshingToken).Should(BeClosed())
   611  				})
   612  				AfterEach(func() {
   613  					stop = nil
   614  				})
   615  			})
   616  
   617  		})
   618  
   619  	})
   620  })