@@ -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
3945func (f * fakeLeaderForwarder ) GetLeaderIP () string {
40- return ""
46+ return f . currentLeaderIP
4147}
4248
43- // Forward returns ok
4449func (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