package cluster import ( "context" "errors" "os" "testing" "time" "github.com/go-logr/logr/testr" "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" clientv3 "go.etcd.io/etcd/client/v3" ctrl "sigs.k8s.io/controller-runtime" "edge-infra.dev/pkg/sds/lib/etcd/client/retry" "edge-infra.dev/pkg/sds/lib/etcd/client/retry/mocks" ) func TestMain(m *testing.M) { os.Exit(m.Run()) } func setupTestCtx(t *testing.T) context.Context { logOptions := testr.Options{ LogTimestamp: true, Verbosity: -1, } return ctrl.LoggerInto(context.Background(), testr.NewWithOptions(t, logOptions)) } func TestUpdateStatus(t *testing.T) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() tests := map[string]struct { client retry.Retrier initialLastHealthy time.Time initialLastUnhealthy time.Time wantHealthy bool }{ "Healthy": { client: getSafeStatusClient(mockCtrl, func(context.Context, string) (*clientv3.StatusResponse, error) { return &clientv3.StatusResponse{}, nil }), initialLastHealthy: nowOffset(-1), initialLastUnhealthy: nowOffset(0), wantHealthy: true, }, "Unhealthy_NoResponse": { client: getSafeStatusClient(mockCtrl, func(context.Context, string) (*clientv3.StatusResponse, error) { return nil, errors.New("No response") }), initialLastHealthy: nowOffset(0), initialLastUnhealthy: nowOffset(-1), wantHealthy: false, }, "Unhealthy_ErrorResponse": { client: getSafeStatusClient(mockCtrl, func(context.Context, string) (*clientv3.StatusResponse, error) { return &clientv3.StatusResponse{ Errors: []string{"Not healthy"}, }, nil }), initialLastHealthy: nowOffset(0), initialLastUnhealthy: nowOffset(-1), wantHealthy: false, }, } for name, tc := range tests { t.Run(name, func(t *testing.T) { status := Status{ lastHealthy: tc.initialLastHealthy, lastUnhealthy: tc.initialLastUnhealthy, } cluster := New("test-endpoint", 10*time.Minute, status) require.Equal(t, !tc.wantHealthy, cluster.IsHealthy()) cluster.UpdateStatus(setupTestCtx(t), tc.client) assert.Equal(t, tc.wantHealthy, cluster.IsHealthy()) }) } } func TestIsHealthy(t *testing.T) { healthyStatus := Status{ lastHealthy: time.Now(), lastUnhealthy: nowOffset(-1), } healthyCluster := New("", 10*time.Minute, healthyStatus) unhealthyStatus := Status{ lastHealthy: nowOffset(-1), lastUnhealthy: time.Now(), } unhealthyCluster := New("", 10*time.Minute, unhealthyStatus) tests := map[string]struct { cluster Cluster want bool }{ "Healthy": { cluster: healthyCluster, want: true, }, "Unhealthy": { cluster: unhealthyCluster, want: false, }, } for name, tc := range tests { t.Run(name, func(t *testing.T) { healthy := tc.cluster.IsHealthy() assert.Equal(t, tc.want, healthy) }) } } func TestIsRecoveryRequired(t *testing.T) { healthyStatus := Status{ lastHealthy: time.Now(), lastUnhealthy: nowOffset(-1), } healthyCluster := New("", 10*time.Minute, healthyStatus) unhealthyStatus := Status{ lastHealthy: nowOffset(-1), // offset of -1 is unhealthy but shouldn't trigger recovery lastUnhealthy: time.Now(), } unhealthyCluster := New("", 10*time.Minute, unhealthyStatus) unhealthyForDurationStatus := Status{ lastHealthy: nowOffset(-10), // offset of -10 is unhealthy and should trigger recovery lastUnhealthy: time.Now(), } unhealthyForDurationCluster := New("", 10*time.Minute, unhealthyForDurationStatus) tests := map[string]struct { cluster Cluster want bool }{ "ResetNotRequired_Healthy": { cluster: healthyCluster, want: false, }, "ResetNotRequired_Unhealthy": { cluster: unhealthyCluster, want: false, }, "ResetRequired_UnhealthyForDuration": { cluster: unhealthyForDurationCluster, want: true, }, } for name, tc := range tests { t.Run(name, func(t *testing.T) { assert.Equal(t, tc.want, tc.cluster.IsResetRequired()) }) } } func TestResetTimer(t *testing.T) { status := Status{ lastHealthy: nowOffset(-10), lastUnhealthy: nowOffset(-5), } cluster := New("", 10*time.Minute, status) assert.NotEqual(t, cluster.lastHealthy, cluster.lastUnhealthy) cluster.ResetTimer() assert.WithinRange(t, cluster.lastHealthy, nowOffset(-1), time.Now()) assert.WithinRange(t, cluster.lastUnhealthy, nowOffset(-1), time.Now()) assert.Equal(t, cluster.lastHealthy, cluster.lastUnhealthy) } func getSafeStatusClient(mockCtrl *gomock.Controller, retFn func(context.Context, string) (*clientv3.StatusResponse, error)) retry.Retrier { mockRetrier := mocks.NewMockRetrier(mockCtrl) mockRetrier.EXPECT().SafeStatus(gomock.Any(), gomock.Any()).DoAndReturn(retFn) return mockRetrier } func nowOffset(mins time.Duration) time.Time { return time.Now().Add(mins * time.Minute) }