@@ -329,6 +329,98 @@ def test_relogin_calls_site_login_with_stored_credentials(self, mocker):
329329 )
330330
331331
332+ class TestReloginClearsStaleSessionState :
333+ """Regression test: _relogin() must clear stale cookies and cached tokens
334+ before calling login().
335+
336+ Without this, MediaWiki's API returns result='Aborted' with reason
337+ "Unable to continue login. Your session most likely timed out." when the
338+ server-side session has expired but the cookie jar still holds the old
339+ session cookie.
340+ """
341+
342+ def test_relogin_clears_cookies_and_tokens_before_login (self , mocker ):
343+ mock_mw_site = MagicMock ()
344+ mock_cred_mngr = MagicMock (spec = CredentialManager )
345+ mock_cred = MagicMock (spec = CredentialManager .UserPwdCredential )
346+ mock_cred .username = "TestUser"
347+ mock_cred .password = "TestPass"
348+ mock_cred_mngr .get_credential .return_value = mock_cred
349+
350+ wt_site = WtSite .__new__ (WtSite )
351+ wt_site ._cred_mngr = mock_cred_mngr
352+ wt_site ._iri = "stale-session.example.com"
353+ wt_site ._site = mock_mw_site
354+ wt_site ._page_cache = {}
355+ wt_site ._cache_enabled = False
356+
357+ call_order = []
358+ mock_mw_site .connection .cookies .clear .side_effect = lambda : call_order .append (
359+ "cookies.clear"
360+ )
361+ mock_mw_site .tokens .clear .side_effect = lambda : call_order .append (
362+ "tokens.clear"
363+ )
364+ mock_mw_site .login .side_effect = lambda ** _ : call_order .append ("login" )
365+
366+ wt_site ._relogin ()
367+
368+ mock_mw_site .connection .cookies .clear .assert_called_once_with ()
369+ mock_mw_site .tokens .clear .assert_called_once_with ()
370+ mock_mw_site .login .assert_called_once_with (
371+ username = "TestUser" , password = "TestPass"
372+ )
373+ # Cookies and tokens must be cleared BEFORE login is attempted.
374+ assert call_order .index ("cookies.clear" ) < call_order .index ("login" )
375+ assert call_order .index ("tokens.clear" ) < call_order .index ("login" )
376+
377+ def test_relogin_recovers_when_server_aborts_login_with_stale_cookies (self , mocker ):
378+ """Reproduces the production failure: server returns 'Aborted' on the
379+ first login call because stale cookies are still present, but succeeds
380+ once client-side state has been cleared.
381+ """
382+ mock_mw_site = MagicMock ()
383+ mock_cred_mngr = MagicMock (spec = CredentialManager )
384+ mock_cred = MagicMock (spec = CredentialManager .UserPwdCredential )
385+ mock_cred .username = "TestUser"
386+ mock_cred .password = "TestPass"
387+ mock_cred_mngr .get_credential .return_value = mock_cred
388+
389+ # Simulate a cookie jar that still holds a stale session cookie.
390+ fake_cookies = {"osl_session" : "stale-id" }
391+
392+ def fake_clear ():
393+ fake_cookies .clear ()
394+
395+ mock_mw_site .connection .cookies .clear .side_effect = fake_clear
396+
397+ # login() fails with the exact MediaWiki LoginError seen in production
398+ # if cookies are still present when called; succeeds otherwise.
399+ def fake_login (** _ ):
400+ if fake_cookies :
401+ raise mwclient .errors .LoginError (
402+ mock_mw_site ,
403+ "Aborted" ,
404+ "Unable to continue login. Your session most likely " "timed out." ,
405+ )
406+
407+ mock_mw_site .login .side_effect = fake_login
408+
409+ wt_site = WtSite .__new__ (WtSite )
410+ wt_site ._cred_mngr = mock_cred_mngr
411+ wt_site ._iri = "stale-session.example.com"
412+ wt_site ._site = mock_mw_site
413+ wt_site ._page_cache = {}
414+ wt_site ._cache_enabled = False
415+
416+ # Should not raise: _relogin clears the stale cookie before calling login
417+ wt_site ._relogin ()
418+
419+ mock_mw_site .login .assert_called_once_with (
420+ username = "TestUser" , password = "TestPass"
421+ )
422+
423+
332424# -- Live integration tests (require wiki credentials) --------------------
333425
334426
0 commit comments