Skip to content

Commit 1827ade

Browse files
committed
Add unit test for testing cluster agent leader election
1 parent fcb5852 commit 1827ade

File tree

1 file changed

+165
-7
lines changed

1 file changed

+165
-7
lines changed

pkg/clusteragent/api/leader_handler_test.go

Lines changed: 165 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,19 +29,25 @@ func (m *mockLeaderEngine) GetLeaderIP() (string, error) {
2929
return m.leaderIP, nil
3030
}
3131

32-
// fakeLeaderForwarder is a fake implementation of the forwarder for testing purposes
33-
type fakeLeaderForwarder struct{}
32+
// fakeLeaderForwarder is a fake implementation of the forwarder for testing purposes.
33+
// It tracks leader IP changes and forward calls for verifying leadership transition behavior.
34+
type fakeLeaderForwarder struct {
35+
currentLeaderIP string
36+
leaderIPChanges []string
37+
forwardCallCount int
38+
}
3439

35-
// SetLeaderIP does nothing
36-
func (f *fakeLeaderForwarder) SetLeaderIP(_ string) {}
40+
func (f *fakeLeaderForwarder) SetLeaderIP(ip string) {
41+
f.currentLeaderIP = ip
42+
f.leaderIPChanges = append(f.leaderIPChanges, ip)
43+
}
3744

38-
// GetLeaderIP does nothing
3945
func (f *fakeLeaderForwarder) GetLeaderIP() string {
40-
return ""
46+
return f.currentLeaderIP
4147
}
4248

43-
// Forward returns ok
4449
func (f *fakeLeaderForwarder) Forward(w http.ResponseWriter, _ *http.Request) {
50+
f.forwardCallCount++
4551
w.WriteHeader(http.StatusOK)
4652
}
4753

@@ -92,3 +98,155 @@ func TestRejectOrForwardLeaderQuery_AsLeader(t *testing.T) {
9298

9399
assert.False(t, lph.rejectOrForwardLeaderQuery(rw, req))
94100
}
101+
102+
// TestRejectOrForwardLeaderQuery_LeaderToFollowerTransition tests the behavior when
103+
// leadership changes from leader to follower between requests.
104+
func TestRejectOrForwardLeaderQuery_LeaderToFollowerTransition(t *testing.T) {
105+
mockEngine := &mockLeaderEngine{
106+
isLeader: true,
107+
leaderIP: "1.1.1.1",
108+
}
109+
forwarder := &fakeLeaderForwarder{}
110+
111+
lph := &LeaderProxyHandler{
112+
leaderElectionEnabled: true,
113+
le: mockEngine,
114+
leaderForwarder: forwarder,
115+
}
116+
117+
// First request: we are the leader, should handle locally
118+
rw1 := httptest.NewRecorder()
119+
req1 := httptest.NewRequest("GET", "http://example.com/foo", nil)
120+
assert.False(t, lph.rejectOrForwardLeaderQuery(rw1, req1), "Should handle locally as leader")
121+
assert.Equal(t, 0, forwarder.forwardCallCount, "Should not forward when leader")
122+
123+
// Simulate leadership loss
124+
mockEngine.isLeader = false
125+
mockEngine.leaderIP = "2.2.2.2" // New leader IP
126+
127+
// Second request: we lost leadership, should forward to new leader
128+
rw2 := httptest.NewRecorder()
129+
req2 := httptest.NewRequest("GET", "http://example.com/foo", nil)
130+
assert.True(t, lph.rejectOrForwardLeaderQuery(rw2, req2), "Should forward as follower")
131+
assert.Equal(t, 1, forwarder.forwardCallCount, "Should forward once")
132+
assert.Equal(t, "2.2.2.2", forwarder.currentLeaderIP, "Should update to new leader IP")
133+
}
134+
135+
// TestRejectOrForwardLeaderQuery_FollowerToLeaderTransition tests the behavior when
136+
// leadership changes from follower to leader between requests.
137+
func TestRejectOrForwardLeaderQuery_FollowerToLeaderTransition(t *testing.T) {
138+
mockEngine := &mockLeaderEngine{
139+
isLeader: false,
140+
leaderIP: "1.1.1.1",
141+
}
142+
forwarder := &fakeLeaderForwarder{}
143+
144+
lph := &LeaderProxyHandler{
145+
leaderElectionEnabled: true,
146+
le: mockEngine,
147+
leaderForwarder: forwarder,
148+
}
149+
150+
// First request: we are a follower, should forward
151+
rw1 := httptest.NewRecorder()
152+
req1 := httptest.NewRequest("GET", "http://example.com/foo", nil)
153+
assert.True(t, lph.rejectOrForwardLeaderQuery(rw1, req1), "Should forward as follower")
154+
assert.Equal(t, 1, forwarder.forwardCallCount, "Should forward once")
155+
156+
// Simulate gaining leadership
157+
mockEngine.isLeader = true
158+
159+
// Second request: we became the leader, should handle locally
160+
rw2 := httptest.NewRecorder()
161+
req2 := httptest.NewRequest("GET", "http://example.com/foo", nil)
162+
assert.False(t, lph.rejectOrForwardLeaderQuery(rw2, req2), "Should handle locally as new leader")
163+
assert.Equal(t, 1, forwarder.forwardCallCount, "Should not forward additional requests")
164+
}
165+
166+
// TestRejectOrForwardLeaderQuery_LeaderIPChange tests that the forwarder is updated
167+
// when the leader IP changes while we remain a follower.
168+
func TestRejectOrForwardLeaderQuery_LeaderIPChange(t *testing.T) {
169+
mockEngine := &mockLeaderEngine{
170+
isLeader: false,
171+
leaderIP: "1.1.1.1",
172+
}
173+
forwarder := &fakeLeaderForwarder{
174+
currentLeaderIP: "1.1.1.1", // Already knows old leader
175+
}
176+
177+
lph := &LeaderProxyHandler{
178+
leaderElectionEnabled: true,
179+
le: mockEngine,
180+
leaderForwarder: forwarder,
181+
}
182+
183+
// First request: forward to current leader
184+
rw1 := httptest.NewRecorder()
185+
req1 := httptest.NewRequest("GET", "http://example.com/foo", nil)
186+
assert.True(t, lph.rejectOrForwardLeaderQuery(rw1, req1))
187+
assert.Equal(t, 1, forwarder.forwardCallCount)
188+
// IP didn't change, so SetLeaderIP should not have been called
189+
assert.Equal(t, 0, len(forwarder.leaderIPChanges), "Should not update IP when unchanged")
190+
191+
// Simulate leader failover - new leader elected
192+
mockEngine.leaderIP = "2.2.2.2"
193+
194+
// Second request: should detect IP change and update forwarder
195+
rw2 := httptest.NewRecorder()
196+
req2 := httptest.NewRequest("GET", "http://example.com/foo", nil)
197+
assert.True(t, lph.rejectOrForwardLeaderQuery(rw2, req2))
198+
assert.Equal(t, 2, forwarder.forwardCallCount)
199+
assert.Equal(t, 1, len(forwarder.leaderIPChanges), "Should update IP once")
200+
assert.Equal(t, "2.2.2.2", forwarder.currentLeaderIP, "Should have new leader IP")
201+
202+
// Third request: IP hasn't changed again
203+
rw3 := httptest.NewRecorder()
204+
req3 := httptest.NewRequest("GET", "http://example.com/foo", nil)
205+
assert.True(t, lph.rejectOrForwardLeaderQuery(rw3, req3))
206+
assert.Equal(t, 3, forwarder.forwardCallCount)
207+
assert.Equal(t, 1, len(forwarder.leaderIPChanges), "Should not update IP when unchanged")
208+
}
209+
210+
// TestRejectOrForwardLeaderQuery_MultipleLeaderChanges tests multiple leadership
211+
// transitions in sequence.
212+
func TestRejectOrForwardLeaderQuery_MultipleLeaderChanges(t *testing.T) {
213+
mockEngine := &mockLeaderEngine{
214+
isLeader: true,
215+
leaderIP: "1.1.1.1",
216+
}
217+
forwarder := &fakeLeaderForwarder{}
218+
219+
lph := &LeaderProxyHandler{
220+
leaderElectionEnabled: true,
221+
le: mockEngine,
222+
leaderForwarder: forwarder,
223+
}
224+
225+
// Sequence of leadership states to test
226+
transitions := []struct {
227+
isLeader bool
228+
leaderIP string
229+
expectForward bool
230+
expectedIPSets int
231+
}{
232+
{true, "1.1.1.1", false, 0}, // We are leader
233+
{false, "2.2.2.2", true, 1}, // Lost leadership, forward to 2.2.2.2
234+
{false, "2.2.2.2", true, 1}, // Still follower, same leader
235+
{false, "3.3.3.3", true, 2}, // Still follower, new leader 3.3.3.3
236+
{true, "1.1.1.1", false, 2}, // Regained leadership
237+
{false, "4.4.4.4", true, 3}, // Lost again, new leader 4.4.4.4
238+
}
239+
240+
for i, tr := range transitions {
241+
mockEngine.isLeader = tr.isLeader
242+
mockEngine.leaderIP = tr.leaderIP
243+
244+
rw := httptest.NewRecorder()
245+
req := httptest.NewRequest("GET", "http://example.com/foo", nil)
246+
247+
result := lph.rejectOrForwardLeaderQuery(rw, req)
248+
assert.Equal(t, tr.expectForward, result, "Transition %d: unexpected forward decision", i)
249+
assert.Equal(t, tr.expectedIPSets, len(forwarder.leaderIPChanges),
250+
"Transition %d: unexpected number of IP updates", i)
251+
}
252+
}

0 commit comments

Comments
 (0)