Skip to content

Commit 11e518f

Browse files
authored
Merge pull request #167 from cloudtrust/CLOUDTRUST-5525
[CLOUDTRUST-5525] Add TimeProvider to ease time-based testing
2 parents 905a008 + 82deaeb commit 11e518f

11 files changed

+137
-26
lines changed

healthcheck/audit.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ type auditEventsReporterChecker struct {
1717
failureCounter int
1818
}
1919

20-
func newAuditEventsReporterChecker(alias string, reporter events.AuditEventsReporterModule, timeout time.Duration, cacheDuration time.Duration, logger log.Logger) BasicChecker {
20+
func newAuditEventsReporterChecker(alias string, reporter events.AuditEventsReporterModule, timeout time.Duration, cacheDuration time.Duration, logger log.Logger, timeProvider TimeProvider) BasicChecker {
2121
healthStatusType := "auditEventreporter"
22-
response := HealthStatus{Name: &alias, Type: &healthStatusType, CacheDuration: cacheDuration}
22+
response := HealthStatus{Name: &alias, Type: &healthStatusType, CacheDuration: cacheDuration, TimeProvider: timeProvider}
2323
response.connection("init")
2424
response.stateUp()
2525
return &auditEventsReporterChecker{

healthcheck/audit_test.go

+4-2
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@ func TestAuditEventsReporterChecker(t *testing.T) {
1515
defer mockCtrl.Finish()
1616

1717
var mockAuditEventReporter = mock.NewAuditEventsReporterModule(mockCtrl)
18+
var mockTime = mock.NewTimeProvider(mockCtrl)
19+
mockTime.EXPECT().Now().Return(testTime).AnyTimes()
1820

1921
t.Run("Success ", func(t *testing.T) {
20-
var auditEventReporterChecker = newAuditEventsReporterChecker("alias", mockAuditEventReporter, 10*time.Second, 10*time.Second, log.NewNopLogger())
22+
var auditEventReporterChecker = newAuditEventsReporterChecker("alias", mockAuditEventReporter, 10*time.Second, 10*time.Second, log.NewNopLogger(), mockTime)
2123
internalChecker := auditEventReporterChecker.(*auditEventsReporterChecker)
2224
mockAuditEventReporter.EXPECT().ReportEvent(gomock.Any(), gomock.Any()).Times(1)
2325

@@ -28,7 +30,7 @@ func TestAuditEventsReporterChecker(t *testing.T) {
2830
})
2931

3032
t.Run("Failure ", func(t *testing.T) {
31-
var auditEventReporterChecker = newAuditEventsReporterChecker("alias", mockAuditEventReporter, 1*time.Second, 10*time.Second, log.NewNopLogger())
33+
var auditEventReporterChecker = newAuditEventsReporterChecker("alias", mockAuditEventReporter, 1*time.Second, 10*time.Second, log.NewNopLogger(), mockTime)
3234
internalChecker := auditEventReporterChecker.(*auditEventsReporterChecker)
3335
mockAuditEventReporter.EXPECT().ReportEvent(gomock.Any(), gomock.Any()).Do(func(arg0 any, arg1 any) {
3436
time.Sleep(2 * time.Second)

healthcheck/database.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@ type databaseChecker struct {
1414
}
1515

1616
// newDatabaseChecker creates a database health checker
17-
func newDatabaseChecker(alias string, dbase HealthDatabase, cacheDuration time.Duration) BasicChecker {
17+
func newDatabaseChecker(alias string, dbase HealthDatabase, cacheDuration time.Duration, timeProvider TimeProvider) BasicChecker {
1818
var database = "database"
1919
return &databaseChecker{
2020
alias: alias,
2121
dbase: dbase,
22-
response: HealthStatus{Name: &alias, Type: &database, CacheDuration: cacheDuration},
22+
response: HealthStatus{Name: &alias, Type: &database, CacheDuration: cacheDuration, TimeProvider: timeProvider},
2323
}
2424
}
2525

healthcheck/database_test.go

+4-2
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,19 @@ func TestDbHealthCheck(t *testing.T) {
1515
defer mockCtrl.Finish()
1616

1717
var mockDB = mock.NewHealthDatabase(mockCtrl)
18+
var mockTime = mock.NewTimeProvider(mockCtrl)
19+
mockTime.EXPECT().Now().Return(testTime).AnyTimes()
1820

1921
{
20-
var dbChecker = newDatabaseChecker("alias", mockDB, 10*time.Second)
22+
var dbChecker = newDatabaseChecker("alias", mockDB, 10*time.Second, mockTime)
2123
mockDB.EXPECT().Ping().Return(nil)
2224
var res = dbChecker.CheckStatus()
2325
assert.NotNil(t, res.Connection)
2426
assert.Equal(t, "established", *res.Connection)
2527
}
2628

2729
{
28-
var dbChecker = newDatabaseChecker("alias", mockDB, 10*time.Second)
30+
var dbChecker = newDatabaseChecker("alias", mockDB, 10*time.Second, mockTime)
2931
var errMsg = "Error message"
3032
var err = errors.New(errMsg)
3133

healthcheck/health.go

+6-5
Original file line numberDiff line numberDiff line change
@@ -50,14 +50,15 @@ type HealthStatus struct {
5050
Connection *string `json:"connection,omitempty"`
5151
ValideUntil time.Time `json:"-"`
5252
CacheDuration time.Duration `json:"-"`
53+
TimeProvider TimeProvider `json:"-"`
5354
}
5455

5556
func (hs *HealthStatus) hasExpired() bool {
56-
return time.Now().After(hs.ValideUntil)
57+
return hs.TimeProvider.Now().After(hs.ValideUntil)
5758
}
5859

5960
func (hs *HealthStatus) touch() {
60-
hs.ValideUntil = time.Now().Add(hs.CacheDuration)
61+
hs.ValideUntil = hs.TimeProvider.Now().Add(hs.CacheDuration)
6162
}
6263

6364
func (hs *HealthStatus) connection(status string) {
@@ -119,7 +120,7 @@ func (hc *healthchecker) AddHealthChecker(name string, checker BasicChecker) {
119120

120121
func (hc *healthchecker) AddHTTPEndpoint(name string, targetURL string, timeoutDuration time.Duration, expectedStatus int, cacheDuration time.Duration) {
121122
hc.logger.Info(context.Background(), "msg", "Adding HTTP endpoint", "processor", name, "url", targetURL)
122-
hc.AddHealthChecker(name, newHTTPEndpointChecker(name, targetURL, timeoutDuration, expectedStatus, cacheDuration))
123+
hc.AddHealthChecker(name, newHTTPEndpointChecker(name, targetURL, timeoutDuration, expectedStatus, cacheDuration, RealTimeProvider{}))
123124
}
124125

125126
func (hc *healthchecker) AddHTTPEndpoints(endpoints map[string]string, timeoutDuration time.Duration, expectedStatus int, cacheDuration time.Duration) {
@@ -130,12 +131,12 @@ func (hc *healthchecker) AddHTTPEndpoints(endpoints map[string]string, timeoutDu
130131

131132
func (hc *healthchecker) AddDatabase(name string, db HealthDatabase, cacheDuration time.Duration) {
132133
hc.logger.Info(context.Background(), "msg", "Adding database", "processor", name)
133-
hc.AddHealthChecker(name, newDatabaseChecker(name, db, cacheDuration))
134+
hc.AddHealthChecker(name, newDatabaseChecker(name, db, cacheDuration, RealTimeProvider{}))
134135
}
135136

136137
func (hc *healthchecker) AddAuditEventsReporterModule(name string, reporter events.AuditEventsReporterModule, timeout time.Duration, cacheDuration time.Duration) {
137138
hc.logger.Info(context.Background(), "msg", "Adding audit event reporter module", "processor", name)
138-
hc.AddHealthChecker(name, newAuditEventsReporterChecker(name, reporter, timeout, cacheDuration, hc.logger))
139+
hc.AddHealthChecker(name, newAuditEventsReporterChecker(name, reporter, timeout, cacheDuration, hc.logger, RealTimeProvider{}))
139140
}
140141

141142
// MakeHandler makes a HTTP handler that returns health check information

healthcheck/health_test.go

+36-8
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ import (
1515
"go.uber.org/mock/gomock"
1616
)
1717

18+
var (
19+
testDuration = 50 * time.Millisecond
20+
testTime = time.Date(1998, time.September, 3, 15, 0, 0, 0, time.UTC)
21+
)
22+
1823
func TestEmptyHealthCheck(t *testing.T) {
1924
var hc = NewHealthChecker("test-module", log.NewNopLogger())
2025
var res = hc.CheckStatus()
@@ -36,6 +41,9 @@ func TestHealthCheckHandler(t *testing.T) {
3641
var mockDB = mock.NewHealthDatabase(mockCtrl)
3742
mockDB.EXPECT().Ping().Return(nil).Times(1)
3843

44+
var mockTime = mock.NewTimeProvider(mockCtrl)
45+
mockTime.EXPECT().Now().Return(testTime).AnyTimes()
46+
3947
var alias1 = "alias-localhost"
4048
var alias2 = "alias-db"
4149
var hc = NewHealthChecker("http-test-module", log.NewNopLogger())
@@ -95,15 +103,35 @@ func httpGet(targetURL string) (string, int, error) {
95103
}
96104

97105
func TestHealthStatusCache(t *testing.T) {
98-
var hs = HealthStatus{CacheDuration: 50 * time.Millisecond}
99-
assert.True(t, hs.hasExpired())
106+
mockCtrl := gomock.NewController(t)
107+
defer mockCtrl.Finish()
100108

101-
hs.touch()
102-
assert.False(t, hs.hasExpired())
109+
mockTime := mock.NewTimeProvider(mockCtrl)
110+
111+
t.Run("untouched", func(t *testing.T) {
112+
hs := HealthStatus{CacheDuration: testDuration, TimeProvider: mockTime}
113+
mockTime.EXPECT().Now().Return(testTime)
114+
115+
assert.True(t, hs.hasExpired())
116+
})
103117

104-
time.Sleep(2 * time.Millisecond)
105-
assert.False(t, hs.hasExpired())
118+
t.Run("not expired", func(t *testing.T) {
119+
hs := HealthStatus{CacheDuration: testDuration, TimeProvider: mockTime}
120+
mockTime.EXPECT().Now().Return(testTime)
121+
hs.touch()
106122

107-
time.Sleep(50 * time.Millisecond)
108-
assert.True(t, hs.hasExpired())
123+
mockTime.EXPECT().Now().Return(testTime.Add(testDuration).Add(-time.Millisecond))
124+
125+
assert.False(t, hs.hasExpired())
126+
})
127+
128+
t.Run("expired", func(t *testing.T) {
129+
hs := HealthStatus{CacheDuration: testDuration, TimeProvider: mockTime}
130+
mockTime.EXPECT().Now().Return(testTime)
131+
hs.touch()
132+
133+
mockTime.EXPECT().Now().Return(testTime.Add(testDuration).Add(time.Millisecond))
134+
135+
assert.True(t, hs.hasExpired())
136+
})
109137
}

healthcheck/http.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ type httpChecker struct {
1515
response HealthStatus
1616
}
1717

18-
func newHTTPEndpointChecker(alias string, targetURL string, timeoutDuration time.Duration, expectedStatus int, cacheDuration time.Duration) BasicChecker {
18+
func newHTTPEndpointChecker(alias string, targetURL string, timeoutDuration time.Duration, expectedStatus int, cacheDuration time.Duration, timeProvider TimeProvider) BasicChecker {
1919
var httpClient = gentleman.New()
2020
{
2121
httpClient = httpClient.URL(targetURL)
@@ -26,7 +26,7 @@ func newHTTPEndpointChecker(alias string, targetURL string, timeoutDuration time
2626
alias: alias,
2727
httpClient: httpClient,
2828
expectedStatus: expectedStatus,
29-
response: HealthStatus{Name: &alias, Type: &http, CacheDuration: cacheDuration},
29+
response: HealthStatus{Name: &alias, Type: &http, CacheDuration: cacheDuration, TimeProvider: timeProvider},
3030
}
3131
}
3232

healthcheck/http_test.go

+11-3
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,20 @@ import (
55
"testing"
66
"time"
77

8+
"github.com/cloudtrust/common-service/v2/healthcheck/mock"
89
"github.com/stretchr/testify/assert"
10+
"go.uber.org/mock/gomock"
911
)
1012

1113
func TestHTTPHealth(t *testing.T) {
14+
mockCtrl := gomock.NewController(t)
15+
defer mockCtrl.Finish()
16+
17+
mockTime := mock.NewTimeProvider(mockCtrl)
18+
mockTime.EXPECT().Now().Return(testTime).AnyTimes()
19+
1220
{
13-
var checker = newHTTPEndpointChecker("github", "https://github.com/", 10*time.Second, 200, 10*time.Second)
21+
var checker = newHTTPEndpointChecker("github", "https://github.com/", 10*time.Second, 200, 10*time.Second, mockTime)
1422
var status = checker.CheckStatus()
1523
assert.Equal(t, *status.State, "UP")
1624
assert.Nil(t, status.Message)
@@ -21,15 +29,15 @@ func TestHTTPHealth(t *testing.T) {
2129
}
2230

2331
{
24-
var checker = newHTTPEndpointChecker("dummy", "https://dummy.server.elca.ch/", 10*time.Second, 200, 10*time.Second)
32+
var checker = newHTTPEndpointChecker("dummy", "https://dummy.server.elca.ch/", 10*time.Second, 200, 10*time.Second, mockTime)
2533
var status = checker.CheckStatus()
2634
assert.Equal(t, *status.State, "DOWN")
2735
assert.NotNil(t, status.Message)
2836
assert.True(t, strings.HasPrefix(*status.Message, "Can't hit target"))
2937
}
3038

3139
{
32-
var checker = newHTTPEndpointChecker("dummy", "https://github.com/not/found", 10*time.Second, 200, 10*time.Second)
40+
var checker = newHTTPEndpointChecker("dummy", "https://github.com/not/found", 10*time.Second, 200, 10*time.Second, mockTime)
3341
var status = checker.CheckStatus()
3442
assert.Equal(t, *status.State, "DOWN")
3543
assert.NotNil(t, status.Message)

healthcheck/mock/timeprovider.go

+55
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

healthcheck/mock_test.go

+1
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ package healthcheck
22

33
//go:generate mockgen --build_flags=--mod=mod -destination=./mock/healthcheck.go -package=mock -mock_names=HealthDatabase=HealthDatabase github.com/cloudtrust/common-service/v2/healthcheck HealthDatabase
44
//go:generate mockgen --build_flags=--mod=mod -destination=./mock/eventsreportermodule.go -package=mock -mock_names=AuditEventsReporterModule=AuditEventsReporterModule github.com/cloudtrust/common-service/v2/events AuditEventsReporterModule
5+
//go:generate mockgen --build_flags=--mod=mod -destination=./mock/timeprovider.go -package=mock -mock_names=TimeProvider=TimeProvider github.com/cloudtrust/common-service/v2/healthcheck TimeProvider

healthcheck/timeprovider.go

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package healthcheck
2+
3+
import "time"
4+
5+
// TimeProvider is the interface we use to provide real time to ease testing
6+
type TimeProvider interface {
7+
Now() time.Time
8+
}
9+
10+
type RealTimeProvider struct{}
11+
12+
func (tp RealTimeProvider) Now() time.Time {
13+
return time.Now()
14+
}

0 commit comments

Comments
 (0)