package retry import ( "context" "crypto/tls" "fmt" "testing" "time" "github.com/go-logr/logr/testr" "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.etcd.io/etcd/api/v3/etcdserverpb" clientv3 "go.etcd.io/etcd/client/v3" ctrl "sigs.k8s.io/controller-runtime" "edge-infra.dev/pkg/sds/lib/etcd/client" "edge-infra.dev/pkg/sds/lib/etcd/client/mocks" ) var testMember = &etcdserverpb.Member{ ID: 123, Name: "test-name", PeerURLs: []string{"test-peer-url"}, ClientURLs: []string{"test-client-url"}, IsLearner: true, } var testMemberListResp = &clientv3.MemberListResponse{ Members: []*etcdserverpb.Member{testMember}, } var testMemberAddResp = &clientv3.MemberAddResponse{ Member: testMember, } var testMemberPromoteResp = &clientv3.MemberPromoteResponse{ Members: []*etcdserverpb.Member{testMember}, } var testMemberRemoveResp = &clientv3.MemberRemoveResponse{ Members: []*etcdserverpb.Member{testMember}, } func SetupTestCtx(t *testing.T) context.Context { logOptions := testr.Options{ LogTimestamp: true, Verbosity: -1, } ctx := ctrl.LoggerInto(context.Background(), testr.NewWithOptions(t, logOptions)) return ctx } func TestNew(t *testing.T) { testCases := map[string]struct { input Config expected Config }{ "Defaults": { input: Config{}, expected: Config{ RequestTimeout: 5 * time.Second, InitialBackoff: 500 * time.Millisecond, BackoffFactor: 1.5, MaxRetries: 3, }, }, "PartialOverwrite": { input: Config{ BackoffFactor: 2, MaxRetries: 5, }, expected: Config{ RequestTimeout: 5 * time.Second, InitialBackoff: 500 * time.Millisecond, BackoffFactor: 2, MaxRetries: 5, }, }, "Overwrite": { input: Config{ RequestTimeout: 10 * time.Second, InitialBackoff: 5 * time.Second, BackoffFactor: 2, MaxRetries: 5, }, expected: Config{ RequestTimeout: 10 * time.Second, InitialBackoff: 5 * time.Second, BackoffFactor: 2, MaxRetries: 5, }, }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { rcli := New(*createClient(nil), tc.input) assert.Equal(t, tc.expected, rcli.(*Client).Config) }) } } func TestWithRetry(t *testing.T) { var count int testCases := map[string]struct { client Retrier fn func(ctx context.Context) error expectError bool expectedAttempts int }{ "Success": { client: New(*createClient(nil), Config{}), fn: func(_ context.Context) error { count++ return nil }, expectError: false, expectedAttempts: 1, }, "SuccessAfterRetry": { client: New(*createClient(nil), Config{}), fn: func(_ context.Context) error { count++ if count < 3 { return fmt.Errorf("force retry") } return nil }, expectError: false, expectedAttempts: 3, }, "FailureAfterRetry": { client: New(*createClient(nil), Config{ MaxRetries: 2, }), fn: func(_ context.Context) error { count++ return fmt.Errorf("example failure") }, expectError: true, expectedAttempts: 3, }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { count = 0 cli := tc.client.(*Client) err := cli.withRetry(SetupTestCtx(t), tc.fn) switch tc.expectError { case true: assert.Error(t, err) case false: assert.NoError(t, err) } assert.Equal(t, tc.expectedAttempts, count) }) } } func TestSafeMemberList(t *testing.T) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() retryClient := getSafeMemberListMockClient(mockCtrl) resp, err := retryClient.SafeMemberList(SetupTestCtx(t)) require.NoError(t, err) require.Equal(t, resp, testMemberListResp) } func TestSafeMemberAddAsLearner(t *testing.T) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() retryClient := getSafeMemberAddAsLearnerMockClient(mockCtrl) resp, err := retryClient.SafeMemberAddAsLearner(SetupTestCtx(t), []string{}) require.NoError(t, err) require.Equal(t, resp, testMemberAddResp) } func TestSafeMemberPromote(t *testing.T) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() retryClient := getSafeMemberPromoteMockClient(mockCtrl) resp, err := retryClient.SafeMemberPromote(SetupTestCtx(t), 0) require.NoError(t, err) require.Equal(t, resp, testMemberPromoteResp) } func TestSafeMemberRemove(t *testing.T) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() retryClient := getSafeMemberRemoveMockClient(mockCtrl) resp, err := retryClient.SafeMemberRemove(SetupTestCtx(t), 0) require.NoError(t, err) require.Equal(t, resp, testMemberRemoveResp) } func getSafeMemberListMockClient(mockCtrl *gomock.Controller) Retrier { mockCluster := mocks.NewMockCluster(mockCtrl) mockCluster.EXPECT().MemberList(gomock.Any()).DoAndReturn(func(_ context.Context) (resp *clientv3.MemberListResponse, err error) { resp = testMemberListResp return resp, nil }) return New(*createClient(mockCluster), Config{}) } func getSafeMemberAddAsLearnerMockClient(mockCtrl *gomock.Controller) Retrier { mockCluster := mocks.NewMockCluster(mockCtrl) mockCluster.EXPECT().MemberAddAsLearner(gomock.Any(), gomock.Any()).DoAndReturn(func(_ context.Context, _ []string) (resp *clientv3.MemberAddResponse, err error) { resp = testMemberAddResp return resp, nil }) return New(*createClient(mockCluster), Config{}) } func getSafeMemberPromoteMockClient(mockCtrl *gomock.Controller) Retrier { mockCluster := mocks.NewMockCluster(mockCtrl) mockCluster.EXPECT().MemberPromote(gomock.Any(), gomock.Any()).DoAndReturn(func(_ context.Context, _ uint64) (resp *clientv3.MemberPromoteResponse, err error) { resp = testMemberPromoteResp return resp, nil }) return New(*createClient(mockCluster), Config{}) } func getSafeMemberRemoveMockClient(mockCtrl *gomock.Controller) Retrier { mockCluster := mocks.NewMockCluster(mockCtrl) mockCluster.EXPECT().MemberRemove(gomock.Any(), gomock.Any()).DoAndReturn(func(_ context.Context, _ uint64) (resp *clientv3.MemberRemoveResponse, err error) { resp = testMemberRemoveResp return resp, nil }) return New(*createClient(mockCluster), Config{}) } func createClient(mockCluster *mocks.MockCluster) *clientv3.Client { config := &tls.Config{ MinVersion: tls.VersionTLS12, } cli, err := client.New(config, 5*time.Second, "fake-ip:0") if err != nil { fmt.Println(err) } cli.Cluster = mockCluster return cli }