fix: persister le token même dans le chemin d'erreur de get_informations/get_messages
Cause racine : quand pronotepy reçoit une PronoteAPIError (ex. code 20 « page expirée »), il appelle refresh() en interne, qui peut roter le token en mémoire (client.password mis à jour avec un nouveau jetonConnexionAppliMobile). Le retry peut aussi échouer — l'exception atteint get_informations()/get_messages() qui l'attrapent et retournent [] (mode dégradé). Mais _persist_credentials() n'était appelé que dans le chemin de SUCCÈS — le token rafraîchi en mémoire n'était jamais persisté. Au run suivant, token_login utilisait le token périmé → échec KeyError 'dataSec' → PronoteAuthRotationError. Correction : appeler _persist_credentials() aussi dans le chemin d'erreur de get_informations() et get_messages(), avant le return []. L'ancien token est déjà invalidé côté serveur lors du refresh — ne pas persister le nouveau token garantit la perte du seul token valide. Tests : 3 nouveaux tests (persistance dans le chemin d'erreur, sauvegarde du token rafraîchi sur erreur). 694 passés, couverture 94.93%.
This commit is contained in:
@@ -455,6 +455,7 @@ class PronoteClient:
|
|||||||
"Échec de la récupération des messages Pronote : %s",
|
"Échec de la récupération des messages Pronote : %s",
|
||||||
redact_exception(exc),
|
redact_exception(exc),
|
||||||
)
|
)
|
||||||
|
self._persist_credentials()
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def get_informations(self) -> list[Message]:
|
def get_informations(self) -> list[Message]:
|
||||||
@@ -494,6 +495,7 @@ class PronoteClient:
|
|||||||
"Échec de la récupération des informations Pronote : %s",
|
"Échec de la récupération des informations Pronote : %s",
|
||||||
redact_exception(exc),
|
redact_exception(exc),
|
||||||
)
|
)
|
||||||
|
self._persist_credentials()
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def get_lessons(self, start: date, end: date) -> list[Lesson]:
|
def get_lessons(self, start: date, end: date) -> list[Lesson]:
|
||||||
|
|||||||
@@ -1397,4 +1397,97 @@ def test_persist_credentials_failure_does_not_crash(
|
|||||||
assert "persistance des credentials" in caplog.text
|
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
|
# Ensure trailing newline
|
||||||
|
|||||||
Reference in New Issue
Block a user