1 package cluster
2
3 import (
4 "context"
5 "errors"
6 "os"
7 "testing"
8 "time"
9
10 "github.com/go-logr/logr/testr"
11 "github.com/golang/mock/gomock"
12 "github.com/stretchr/testify/assert"
13 "github.com/stretchr/testify/require"
14 clientv3 "go.etcd.io/etcd/client/v3"
15 ctrl "sigs.k8s.io/controller-runtime"
16
17 "edge-infra.dev/pkg/sds/lib/etcd/client/retry"
18 "edge-infra.dev/pkg/sds/lib/etcd/client/retry/mocks"
19 )
20
21 func TestMain(m *testing.M) {
22 os.Exit(m.Run())
23 }
24
25 func setupTestCtx(t *testing.T) context.Context {
26 logOptions := testr.Options{
27 LogTimestamp: true,
28 Verbosity: -1,
29 }
30
31 return ctrl.LoggerInto(context.Background(), testr.NewWithOptions(t, logOptions))
32 }
33
34 func TestUpdateStatus(t *testing.T) {
35 mockCtrl := gomock.NewController(t)
36 defer mockCtrl.Finish()
37
38 tests := map[string]struct {
39 client retry.Retrier
40 initialLastHealthy time.Time
41 initialLastUnhealthy time.Time
42 wantHealthy bool
43 }{
44 "Healthy": {
45 client: getSafeStatusClient(mockCtrl, func(context.Context, string) (*clientv3.StatusResponse, error) {
46 return &clientv3.StatusResponse{}, nil
47 }),
48 initialLastHealthy: nowOffset(-1),
49 initialLastUnhealthy: nowOffset(0),
50 wantHealthy: true,
51 },
52 "Unhealthy_NoResponse": {
53 client: getSafeStatusClient(mockCtrl, func(context.Context, string) (*clientv3.StatusResponse, error) {
54 return nil, errors.New("No response")
55 }),
56 initialLastHealthy: nowOffset(0),
57 initialLastUnhealthy: nowOffset(-1),
58 wantHealthy: false,
59 },
60 "Unhealthy_ErrorResponse": {
61 client: getSafeStatusClient(mockCtrl, func(context.Context, string) (*clientv3.StatusResponse, error) {
62 return &clientv3.StatusResponse{
63 Errors: []string{"Not healthy"},
64 }, nil
65 }),
66 initialLastHealthy: nowOffset(0),
67 initialLastUnhealthy: nowOffset(-1),
68 wantHealthy: false,
69 },
70 }
71
72 for name, tc := range tests {
73 t.Run(name, func(t *testing.T) {
74 status := Status{
75 lastHealthy: tc.initialLastHealthy,
76 lastUnhealthy: tc.initialLastUnhealthy,
77 }
78 cluster := New("test-endpoint", 10*time.Minute, status)
79
80 require.Equal(t, !tc.wantHealthy, cluster.IsHealthy())
81 cluster.UpdateStatus(setupTestCtx(t), tc.client)
82 assert.Equal(t, tc.wantHealthy, cluster.IsHealthy())
83 })
84 }
85 }
86
87 func TestIsHealthy(t *testing.T) {
88 healthyStatus := Status{
89 lastHealthy: time.Now(),
90 lastUnhealthy: nowOffset(-1),
91 }
92 healthyCluster := New("", 10*time.Minute, healthyStatus)
93
94 unhealthyStatus := Status{
95 lastHealthy: nowOffset(-1),
96 lastUnhealthy: time.Now(),
97 }
98 unhealthyCluster := New("", 10*time.Minute, unhealthyStatus)
99
100 tests := map[string]struct {
101 cluster Cluster
102 want bool
103 }{
104 "Healthy": {
105 cluster: healthyCluster,
106 want: true,
107 },
108 "Unhealthy": {
109 cluster: unhealthyCluster,
110 want: false,
111 },
112 }
113
114 for name, tc := range tests {
115 t.Run(name, func(t *testing.T) {
116 healthy := tc.cluster.IsHealthy()
117 assert.Equal(t, tc.want, healthy)
118 })
119 }
120 }
121
122 func TestIsRecoveryRequired(t *testing.T) {
123 healthyStatus := Status{
124 lastHealthy: time.Now(),
125 lastUnhealthy: nowOffset(-1),
126 }
127 healthyCluster := New("", 10*time.Minute, healthyStatus)
128
129 unhealthyStatus := Status{
130 lastHealthy: nowOffset(-1),
131 lastUnhealthy: time.Now(),
132 }
133 unhealthyCluster := New("", 10*time.Minute, unhealthyStatus)
134
135 unhealthyForDurationStatus := Status{
136 lastHealthy: nowOffset(-10),
137 lastUnhealthy: time.Now(),
138 }
139 unhealthyForDurationCluster := New("", 10*time.Minute, unhealthyForDurationStatus)
140
141 tests := map[string]struct {
142 cluster Cluster
143 want bool
144 }{
145 "ResetNotRequired_Healthy": {
146 cluster: healthyCluster,
147 want: false,
148 },
149 "ResetNotRequired_Unhealthy": {
150 cluster: unhealthyCluster,
151 want: false,
152 },
153 "ResetRequired_UnhealthyForDuration": {
154 cluster: unhealthyForDurationCluster,
155 want: true,
156 },
157 }
158
159 for name, tc := range tests {
160 t.Run(name, func(t *testing.T) {
161 assert.Equal(t, tc.want, tc.cluster.IsResetRequired())
162 })
163 }
164 }
165
166 func TestResetTimer(t *testing.T) {
167 status := Status{
168 lastHealthy: nowOffset(-10),
169 lastUnhealthy: nowOffset(-5),
170 }
171 cluster := New("", 10*time.Minute, status)
172
173 assert.NotEqual(t, cluster.lastHealthy, cluster.lastUnhealthy)
174
175 cluster.ResetTimer()
176
177 assert.WithinRange(t, cluster.lastHealthy, nowOffset(-1), time.Now())
178 assert.WithinRange(t, cluster.lastUnhealthy, nowOffset(-1), time.Now())
179 assert.Equal(t, cluster.lastHealthy, cluster.lastUnhealthy)
180 }
181
182 func getSafeStatusClient(mockCtrl *gomock.Controller, retFn func(context.Context, string) (*clientv3.StatusResponse, error)) retry.Retrier {
183 mockRetrier := mocks.NewMockRetrier(mockCtrl)
184 mockRetrier.EXPECT().SafeStatus(gomock.Any(), gomock.Any()).DoAndReturn(retFn)
185
186 return mockRetrier
187 }
188
189 func nowOffset(mins time.Duration) time.Time {
190 return time.Now().Add(mins * time.Minute)
191 }
192
View as plain text