@@ -27,6 +27,18 @@ func (m *mockMemberStatusChecker) MemberStatus(ctx context.Context, member *etcd
2727 return nil , errors .New ("member status not found" )
2828}
2929
30+ // mockLeaderMover implements LeaderMover for testing
31+ type mockLeaderMover struct {
32+ moveLeaderErrors map [uint64 ]error
33+ }
34+
35+ func (m * mockLeaderMover ) MoveLeader (ctx context.Context , toMember uint64 ) error {
36+ if err , exists := m .moveLeaderErrors [toMember ]; exists {
37+ return err
38+ }
39+ return nil
40+ }
41+
3042func TestFindLeader (t * testing.T ) {
3143 ctx := context .Background ()
3244
@@ -164,81 +176,140 @@ func TestFindLeader(t *testing.T) {
164176 }
165177}
166178
167- func TestFindLeader_ContextCancellation (t * testing.T ) {
168- ctx , cancel := context .WithCancel (context .Background ())
169- cancel () // Cancel immediately
170-
171- mockClient := & mockMemberStatusChecker {
172- memberStatuses : map [uint64 ]* clientv3.StatusResponse {
173- 1 : {Header : & etcdserverpb.ResponseHeader {MemberId : 1 }, Leader : 1 },
174- },
175- }
176-
177- memberList := []* etcdserverpb.Member {
178- {ID : 1 , Name : "etcd-1" , ClientURLs : []string {"https://10.0.0.1:2379" }},
179- }
180-
181- // The function should handle context cancellation gracefully
182- // Since our mock doesn't actually check context, this test verifies
183- // that the function signature accepts context and passes it through
184- leader , err := FindLeader (ctx , mockClient , memberList )
185- require .NoError (t , err )
186- require .NotNil (t , leader )
187- assert .Equal (t , uint64 (1 ), leader .ID )
188- }
189-
190- func TestFindLeader_EdgeCases (t * testing.T ) {
179+ func TestMoveLeaderToAnotherMember (t * testing.T ) {
191180 ctx := context .Background ()
192181
193- t .Run ("member with no client URLs" , func (t * testing.T ) {
194- mockClient := & mockMemberStatusChecker {
195- memberStatuses : map [uint64 ]* clientv3.StatusResponse {
196- 1 : {Header : & etcdserverpb.ResponseHeader {MemberId : 1 }, Leader : 1 },
182+ tests := []struct {
183+ name string
184+ leader * etcdserverpb.Member
185+ memberList []* etcdserverpb.Member
186+ moveLeaderErrors map [uint64 ]error
187+ expectedSuccess bool
188+ expectedError string
189+ }{
190+ {
191+ name : "successfully move leader to another member" ,
192+ leader : & etcdserverpb.Member {
193+ ID : 1 ,
194+ Name : "etcd-1" ,
195+ ClientURLs : []string {"https://10.0.0.1:2379" },
196+ PeerURLs : []string {"https://10.0.0.1:2380" },
197197 },
198- }
199-
200- memberList := []* etcdserverpb.Member {
201- {ID : 1 , Name : "etcd-1" , ClientURLs : []string {}},
202- }
203-
204- // This should work fine since our mock doesn't actually use ClientURLs
205- leader , err := FindLeader (ctx , mockClient , memberList )
206- require .NoError (t , err )
207- require .NotNil (t , leader )
208- assert .Equal (t , uint64 (1 ), leader .ID )
209- })
210-
211- t .Run ("member with nil client URLs" , func (t * testing.T ) {
212- mockClient := & mockMemberStatusChecker {
213- memberStatuses : map [uint64 ]* clientv3.StatusResponse {
214- 1 : {Header : & etcdserverpb.ResponseHeader {MemberId : 1 }, Leader : 1 },
198+ memberList : []* etcdserverpb.Member {
199+ {ID : 1 , Name : "etcd-1" , ClientURLs : []string {"https://10.0.0.1:2379" }, PeerURLs : []string {"https://10.0.0.1:2380" }},
200+ {ID : 2 , Name : "etcd-2" , ClientURLs : []string {"https://10.0.0.2:2379" }, PeerURLs : []string {"https://10.0.0.2:2380" }},
201+ {ID : 3 , Name : "etcd-3" , ClientURLs : []string {"https://10.0.0.3:2379" }, PeerURLs : []string {"https://10.0.0.3:2380" }},
215202 },
216- }
217-
218- memberList := []* etcdserverpb.Member {
219- {ID : 1 , Name : "etcd-1" , ClientURLs : nil },
220- }
203+ expectedSuccess : true ,
204+ },
205+ {
206+ name : "successfully move leader when leader is in middle of list" ,
207+ leader : & etcdserverpb.Member {
208+ ID : 2 ,
209+ Name : "etcd-2" ,
210+ ClientURLs : []string {"https://10.0.0.2:2379" },
211+ PeerURLs : []string {"https://10.0.0.2:2380" },
212+ },
213+ memberList : []* etcdserverpb.Member {
214+ {ID : 1 , Name : "etcd-1" , ClientURLs : []string {"https://10.0.0.1:2379" }, PeerURLs : []string {"https://10.0.0.1:2380" }},
215+ {ID : 2 , Name : "etcd-2" , ClientURLs : []string {"https://10.0.0.2:2379" }, PeerURLs : []string {"https://10.0.0.2:2380" }},
216+ {ID : 3 , Name : "etcd-3" , ClientURLs : []string {"https://10.0.0.3:2379" }, PeerURLs : []string {"https://10.0.0.3:2380" }},
217+ },
218+ expectedSuccess : true ,
219+ },
220+ {
221+ name : "successfully move leader when leader is last in list" ,
222+ leader : & etcdserverpb.Member {
223+ ID : 3 ,
224+ Name : "etcd-3" ,
225+ ClientURLs : []string {"https://10.0.0.3:2379" },
226+ PeerURLs : []string {"https://10.0.0.3:2380" },
227+ },
228+ memberList : []* etcdserverpb.Member {
229+ {ID : 1 , Name : "etcd-1" , ClientURLs : []string {"https://10.0.0.1:2379" }, PeerURLs : []string {"https://10.0.0.1:2380" }},
230+ {ID : 2 , Name : "etcd-2" , ClientURLs : []string {"https://10.0.0.2:2379" }, PeerURLs : []string {"https://10.0.0.2:2380" }},
231+ {ID : 3 , Name : "etcd-3" , ClientURLs : []string {"https://10.0.0.3:2379" }, PeerURLs : []string {"https://10.0.0.3:2380" }},
232+ },
233+ expectedSuccess : true ,
234+ },
235+ {
236+ name : "move leader fails with error" ,
237+ leader : & etcdserverpb.Member {
238+ ID : 1 ,
239+ Name : "etcd-1" ,
240+ ClientURLs : []string {"https://10.0.0.1:2379" },
241+ PeerURLs : []string {"https://10.0.0.1:2380" },
242+ },
243+ memberList : []* etcdserverpb.Member {
244+ {ID : 1 , Name : "etcd-1" , ClientURLs : []string {"https://10.0.0.1:2379" }, PeerURLs : []string {"https://10.0.0.1:2380" }},
245+ {ID : 2 , Name : "etcd-2" , ClientURLs : []string {"https://10.0.0.2:2379" }, PeerURLs : []string {"https://10.0.0.2:2380" }},
246+ },
247+ moveLeaderErrors : map [uint64 ]error {
248+ 2 : errors .New ("move leader failed: connection timeout" ),
249+ },
250+ expectedSuccess : false ,
251+ expectedError : "move leader failed: connection timeout" ,
252+ },
253+ {
254+ name : "no follower member found - single member cluster" ,
255+ leader : & etcdserverpb.Member {
256+ ID : 1 ,
257+ Name : "etcd-1" ,
258+ ClientURLs : []string {"https://10.0.0.1:2379" },
259+ PeerURLs : []string {"https://10.0.0.1:2380" },
260+ },
261+ memberList : []* etcdserverpb.Member {
262+ {ID : 1 , Name : "etcd-1" , ClientURLs : []string {"https://10.0.0.1:2379" }, PeerURLs : []string {"https://10.0.0.1:2380" }},
263+ },
264+ expectedSuccess : false ,
265+ expectedError : "no follower member found for the members: [ID:1 name:\" etcd-1\" peerURLs:\" https://10.0.0.1:2380\" clientURLs:\" https://10.0.0.1:2379\" ]" ,
266+ },
267+ {
268+ name : "no follower member found - all members have same ID" ,
269+ leader : & etcdserverpb.Member {
270+ ID : 1 ,
271+ Name : "etcd-1" ,
272+ ClientURLs : []string {"https://10.0.0.1:2379" },
273+ PeerURLs : []string {"https://10.0.0.1:2380" },
274+ },
275+ memberList : []* etcdserverpb.Member {
276+ {ID : 1 , Name : "etcd-1" , ClientURLs : []string {"https://10.0.0.1:2379" }, PeerURLs : []string {"https://10.0.0.1:2380" }},
277+ {ID : 1 , Name : "etcd-1-copy" , ClientURLs : []string {"https://10.0.0.1:2379" }, PeerURLs : []string {"https://10.0.0.1:2380" }},
278+ },
279+ expectedSuccess : false ,
280+ expectedError : "no follower member found for the members: [ID:1 name:\" etcd-1\" peerURLs:\" https://10.0.0.1:2380\" clientURLs:\" https://10.0.0.1:2379\" ID:1 name:\" etcd-1-copy\" peerURLs:\" https://10.0.0.1:2380\" clientURLs:\" https://10.0.0.1:2379\" ]" ,
281+ },
282+ {
283+ name : "empty member list" ,
284+ leader : & etcdserverpb.Member {
285+ ID : 1 ,
286+ Name : "etcd-1" ,
287+ ClientURLs : []string {"https://10.0.0.1:2379" },
288+ PeerURLs : []string {"https://10.0.0.1:2380" },
289+ },
290+ memberList : []* etcdserverpb.Member {},
291+ expectedSuccess : false ,
292+ expectedError : "no follower member found for the members: []" ,
293+ },
294+ }
221295
222- leader , err := FindLeader ( ctx , mockClient , memberList )
223- require . NoError ( t , err )
224- require . NotNil ( t , leader )
225- assert . Equal ( t , uint64 ( 1 ), leader . ID )
226- })
296+ for _ , tt := range tests {
297+ t . Run ( tt . name , func ( t * testing. T ) {
298+ mockClient := & mockLeaderMover {
299+ moveLeaderErrors : tt . moveLeaderErrors ,
300+ }
227301
228- t .Run ("member with zero ID" , func (t * testing.T ) {
229- mockClient := & mockMemberStatusChecker {
230- memberStatuses : map [uint64 ]* clientv3.StatusResponse {
231- 0 : {Header : & etcdserverpb.ResponseHeader {MemberId : 0 }, Leader : 0 },
232- },
233- }
302+ success , err := MoveLeaderToAnotherMember (ctx , mockClient , tt .leader , tt .memberList )
234303
235- memberList := []* etcdserverpb.Member {
236- {ID : 0 , Name : "etcd-0" , ClientURLs : []string {"https://10.0.0.0:2379" }},
237- }
304+ if tt .expectedError != "" {
305+ require .Error (t , err )
306+ assert .Contains (t , err .Error (), tt .expectedError )
307+ assert .False (t , success )
308+ return
309+ }
238310
239- leader , err := FindLeader (ctx , mockClient , memberList )
240- require .NoError (t , err )
241- require .NotNil (t , leader )
242- assert .Equal (t , uint64 (0 ), leader .ID )
243- })
311+ require .NoError (t , err )
312+ assert .Equal (t , tt .expectedSuccess , success )
313+ })
314+ }
244315}
0 commit comments