From 15857dfb340015c3ffcc51c045192436d33ba7a7 Mon Sep 17 00:00:00 2001 From: Weston Bell-Geddes Date: Sun, 23 Aug 2026 21:06:25 -0600 Subject: [PATCH] Fix stale OAuth sessions Invalidate failed OAuth tokens and run the normal disconnect cleanup before prompting for a fresh login. Fixes #1163 --- src/client/_clientwindow.py | 2 ++ src/oauth/oauth_flow.py | 1 + .../unit_tests/client/test_authentication.py | 28 +++++++++++++++++++ tests/unit_tests/oauth/test_oauth_flow.py | 11 ++++++++ 4 files changed, 42 insertions(+) create mode 100644 tests/unit_tests/client/test_authentication.py create mode 100644 tests/unit_tests/oauth/test_oauth_flow.py diff --git a/src/client/_clientwindow.py b/src/client/_clientwindow.py index 85e8377aa..2231c0a58 100644 --- a/src/client/_clientwindow.py +++ b/src/client/_clientwindow.py @@ -1210,7 +1210,9 @@ def try_to_auto_login(self) -> None: self.show_login_widget() def on_login_attempt_failed(self) -> None: + self.disconnect_() self.state = ClientState.DISCONNECTED + self._auto_relogin = False self.show_login_widget() def get_creds_and_login(self) -> None: diff --git a/src/oauth/oauth_flow.py b/src/oauth/oauth_flow.py index e27b9c335..0c95bd6fc 100644 --- a/src/oauth/oauth_flow.py +++ b/src/oauth/oauth_flow.py @@ -99,6 +99,7 @@ def check_token_scopes(self) -> None: def on_request_failed(self, error: QOAuth2AuthorizationCodeFlow.Error) -> None: self._logger.error("Request failed with an error: %s", error) + self.setToken("") self.stop_checking_expiration() def setup_credentials(self) -> None: diff --git a/tests/unit_tests/client/test_authentication.py b/tests/unit_tests/client/test_authentication.py new file mode 100644 index 000000000..1f0d10b97 --- /dev/null +++ b/tests/unit_tests/client/test_authentication.py @@ -0,0 +1,28 @@ +from unittest.mock import call + + +def test_login_failure_allows_fresh_connection(application, client_instance, mocker): + from src.client.clientstate import ClientState + from src.util.gameurl import GameUrl + + disconnect = mocker.patch.object(client_instance, "disconnect_") + show_login_widget = mocker.patch.object(client_instance, "show_login_widget") + mocker.patch.object(client_instance, "_state", ClientState.LOGGED_IN) + mocker.patch.object(client_instance, "_auto_relogin", True) + lifecycle = mocker.Mock() + lifecycle.attach_mock(disconnect, "disconnect") + lifecycle.attach_mock(show_login_widget, "show_login_widget") + + client_instance.on_login_attempt_failed() + + assert client_instance.state == ClientState.DISCONNECTED + assert not client_instance._auto_relogin + assert lifecycle.mock_calls == [call.disconnect(), call.show_login_widget()] + + mocker.patch.object(client_instance.replayServer, "doListen", return_value=True) + mocker.patch.object(client_instance.replayServer, "serverPort", return_value=12345) + connect = mocker.patch.object(client_instance.lobby_connection, "do_connect") + mocker.patch.object(GameUrl, "PORT", -1) + + assert client_instance.do_connect() + connect.assert_called_once_with() diff --git a/tests/unit_tests/oauth/test_oauth_flow.py b/tests/unit_tests/oauth/test_oauth_flow.py new file mode 100644 index 000000000..c6a8cdcde --- /dev/null +++ b/tests/unit_tests/oauth/test_oauth_flow.py @@ -0,0 +1,11 @@ +def test_request_failure_discards_access_token(application): + from PyQt6.QtNetworkAuth import QAbstractOAuth + + from src.oauth.oauth_flow import OAuth2Flow + + flow = OAuth2Flow() + flow.setToken("expired-token") + + flow.requestFailed.emit(QAbstractOAuth.Error.NetworkError) + + assert flow.token() == ""