diff --git a/pronote_sync/sources/pronote/client.py b/pronote_sync/sources/pronote/client.py index 86719c1..57051c3 100644 --- a/pronote_sync/sources/pronote/client.py +++ b/pronote_sync/sources/pronote/client.py @@ -455,6 +455,7 @@ class PronoteClient: "Échec de la récupération des messages Pronote : %s", redact_exception(exc), ) + self._persist_credentials() return [] def get_informations(self) -> list[Message]: @@ -494,6 +495,7 @@ class PronoteClient: "Échec de la récupération des informations Pronote : %s", redact_exception(exc), ) + self._persist_credentials() return [] def get_lessons(self, start: date, end: date) -> list[Lesson]: diff --git a/tests/unit/test_pronote_client.py b/tests/unit/test_pronote_client.py index f5d5d1d..838ef2b 100644 --- a/tests/unit/test_pronote_client.py +++ b/tests/unit/test_pronote_client.py @@ -1397,4 +1397,97 @@ def test_persist_credentials_failure_does_not_crash( assert "persistance des credentials" in caplog.text +def test_persist_credentials_in_error_path_get_informations( + mocker: pytest_mock.MockerFixture, + pronote_settings: PronoteSettings, +) -> None: + """Vérifie que get_informations persiste les credentials même en mode dégradé. + + Le refresh pronotepy peut avoir roté le token en mémoire alors que la + requête échoue ; la persistance doit intervenir aussi dans le chemin + d'erreur, avant le ``return []``. + + :param mocker: Fixture pytest-mock pour le mocking. + :param pronote_settings: Paramètres Pronote valides. + :return: None + """ + mock_client = mocker.MagicMock() + mock_client.information_and_surveys.side_effect = pronotepy.PronoteAPIError("API error") + mock_client.export_credentials.return_value = { + "password": "token-apres-refresh", # pragma: allowlist secret + } + mocker.patch.object(PronoteClient, "_connect", return_value=mock_client) + + auth_state = _make_auth_state_mock(mocker) + client = PronoteClient(pronote_settings, auth_state=auth_state) + client._client = mock_client + messages = client.get_informations() + + assert messages == [] + auth_state.save.assert_called_once_with( + {"password": "token-apres-refresh"} # pragma: allowlist secret + ) + + +def test_persist_credentials_in_error_path_get_messages( + mocker: pytest_mock.MockerFixture, + pronote_settings: PronoteSettings, +) -> None: + """Vérifie que get_messages persiste les credentials même en mode dégradé. + + :param mocker: Fixture pytest-mock pour le mocking. + :param pronote_settings: Paramètres Pronote valides. + :return: None + """ + mock_client = mocker.MagicMock() + mock_client.discussions.side_effect = pronotepy.PronoteAPIError("API error") + mock_client.export_credentials.return_value = { + "password": "token-apres-refresh", # pragma: allowlist secret + } + mocker.patch.object(PronoteClient, "_connect", return_value=mock_client) + + auth_state = _make_auth_state_mock(mocker) + client = PronoteClient(pronote_settings, auth_state=auth_state) + client._client = mock_client + messages = client.get_messages() + + assert messages == [] + auth_state.save.assert_called_once_with( + {"password": "token-apres-refresh"} # pragma: allowlist secret + ) + + +def test_persist_credentials_saves_refreshed_token_on_error( + mocker: pytest_mock.MockerFixture, + pronote_settings: PronoteSettings, +) -> None: + """Vérifie que le token roté par le refresh pronotepy est persisté en cas d'erreur. + + ``export_credentials()`` retourne un mot de passe (token) différent du + token initial, simulant la rotation opérée par ``refresh()`` : c'est ce + nouveau credential qui doit être passé à ``save``. + + :param mocker: Fixture pytest-mock pour le mocking. + :param pronote_settings: Paramètres Pronote valides. + :return: None + """ + initial_creds = {"password": "token-initial"} # pragma: allowlist secret + refreshed_creds = {"password": "token-rafraichi"} # pragma: allowlist secret + mock_client = mocker.MagicMock() + mock_client.information_and_surveys.side_effect = pronotepy.PronoteAPIError("API error") + mock_client.export_credentials.return_value = refreshed_creds + mocker.patch.object(PronoteClient, "_connect", return_value=mock_client) + + auth_state = _make_auth_state_mock(mocker) + client = PronoteClient(pronote_settings, auth_state=auth_state) + client._client = mock_client + messages = client.get_informations() + + assert messages == [] + assert auth_state.save.call_count == 1 + saved = auth_state.save.call_args.args[0] + assert saved == refreshed_creds + assert saved != initial_creds + + # Ensure trailing newline