fix(M4): modes explicites ical/pronotepy stricts — suppression du repli réciproque
Les modes explicites (ical, pronotepy) ne basculent plus silencieusement vers l'autre source en cas d'échec. Seul le mode auto conserve le repli iCal → pronotepy sur exception. Conforme au contrat de TODO.md M4 et AGENTS.md. - fallback.py : _agenda_sources() et _homework_sources() retournent (source, None) en mode explicite ; docstrings mises à jour. - test_fallback.py : 4 tests remplacés, 4 tests ajoutés (non-repli + empty-list-is-success), 4 tests ajustés ; :rtype: None ajouté. Co-authored-by: coder/coder coder@agents.invalid Co-authored-by: test-engineer/test-engineer test-engineer@agents.invalid
This commit is contained in:
@@ -4,9 +4,9 @@ Ce module fournit l'enum :class:`AgendaSource`, le protocole
|
|||||||
:class:`PronoteFetcherProtocol` consommé par le pipeline ainsi que la
|
:class:`PronoteFetcherProtocol` consommé par le pipeline ainsi que la
|
||||||
classe :class:`PronoteFetcher` qui sélectionne la source selon la
|
classe :class:`PronoteFetcher` qui sélectionne la source selon la
|
||||||
configuration (``PRONOTE_AGENDA_SOURCE`` / ``PRONOTE_HOMEWORK_SOURCE``).
|
configuration (``PRONOTE_AGENDA_SOURCE`` / ``PRONOTE_HOMEWORK_SOURCE``).
|
||||||
La logique applique un repli **unique** : une source primaire est
|
Contrat strict : les modes explicites n'utilisent que la source
|
||||||
essayée en premier et, en cas d'échec, une seule source de repli (jamais
|
configurée, sans aucun repli ; seul le mode ``auto`` applique un repli
|
||||||
réciproque ni itératif) est essayée si elle est configurée. Les messages
|
unique iCal → pronotepy, en cas d'exception uniquement. Les messages
|
||||||
et informations proviennent toujours de pronotepy. Toutes les erreurs
|
et informations proviennent toujours de pronotepy. Toutes les erreurs
|
||||||
sont journalisées avec des secrets masqués via
|
sont journalisées avec des secrets masqués via
|
||||||
:func:`~pronote_sync.utils.redaction.redact_exception` ; les exceptions
|
:func:`~pronote_sync.utils.redaction.redact_exception` ; les exceptions
|
||||||
@@ -82,13 +82,14 @@ class PronoteFetcherProtocol(Protocol):
|
|||||||
|
|
||||||
|
|
||||||
class PronoteFetcher:
|
class PronoteFetcher:
|
||||||
"""Récupère les données Pronote via iCal ou pronotepy avec repli unique.
|
"""Récupère les données Pronote via iCal ou pronotepy, repli réservé au mode ``auto``.
|
||||||
|
|
||||||
Unifie les sources iCal et pronotepy selon la source configurée
|
Unifie les sources iCal et pronotepy selon la source configurée
|
||||||
(``agenda_source`` / ``homework_source``) : la source primaire est
|
(``agenda_source`` / ``homework_source``) : les modes explicites
|
||||||
essayée en premier et, si elle échoue, une seule source de repli est
|
n'utilisent que la source configurée, sans aucun repli ; seul le mode
|
||||||
essayée lorsqu'elle est configurée. Les messages et informations
|
``auto`` essaie une source primaire puis, si elle échoue, une seule
|
||||||
proviennent toujours de pronotepy.
|
source de repli lorsqu'elle est configurée. Les messages et
|
||||||
|
informations proviennent toujours de pronotepy.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, settings: Settings, pronote_client: PronoteClientProtocol) -> None:
|
def __init__(self, settings: Settings, pronote_client: PronoteClientProtocol) -> None:
|
||||||
@@ -171,11 +172,10 @@ class PronoteFetcher:
|
|||||||
def _agenda_sources(self) -> tuple[_SourceName, _SourceName | None]:
|
def _agenda_sources(self) -> tuple[_SourceName, _SourceName | None]:
|
||||||
"""Sélectionne la source primaire et le repli unique pour l'agenda.
|
"""Sélectionne la source primaire et le repli unique pour l'agenda.
|
||||||
|
|
||||||
En mode ``AUTO``, iCal est primaire si ``ical_url`` est configuré
|
Les modes explicites ``ICAL`` et ``PRONOTEPY`` désignent la seule
|
||||||
(repli pronotepy si la configuration pronotepy est complète),
|
source utilisée, sans aucun repli. En mode ``AUTO``, iCal est
|
||||||
sinon pronotepy sans repli. Les modes explicites ``ICAL`` et
|
primaire si ``ical_url`` est configuré (repli pronotepy si la
|
||||||
``PRONOTEPY`` désignent la source primaire et tentent l'autre
|
configuration pronotepy est complète), sinon pronotepy sans repli.
|
||||||
source en repli unique si elle est configurée.
|
|
||||||
|
|
||||||
:return: Tuple ``(source primaire, source de repli ou ``None``)``.
|
:return: Tuple ``(source primaire, source de repli ou ``None``)``.
|
||||||
:rtype: tuple[_SourceName, _SourceName | None]
|
:rtype: tuple[_SourceName, _SourceName | None]
|
||||||
@@ -183,9 +183,9 @@ class PronoteFetcher:
|
|||||||
"""
|
"""
|
||||||
source = AgendaSource(self._settings.pronote.agenda_source)
|
source = AgendaSource(self._settings.pronote.agenda_source)
|
||||||
if source is AgendaSource.ICAL:
|
if source is AgendaSource.ICAL:
|
||||||
return "ical", "pronotepy" if self._is_pronotepy_configured() else None
|
return "ical", None
|
||||||
if source is AgendaSource.PRONOTEPY:
|
if source is AgendaSource.PRONOTEPY:
|
||||||
return "pronotepy", "ical" if self._is_ical_configured() else None
|
return "pronotepy", None
|
||||||
if self._is_ical_configured():
|
if self._is_ical_configured():
|
||||||
return "ical", "pronotepy" if self._is_pronotepy_configured() else None
|
return "ical", "pronotepy" if self._is_pronotepy_configured() else None
|
||||||
if self._is_pronotepy_configured():
|
if self._is_pronotepy_configured():
|
||||||
@@ -208,11 +208,13 @@ class PronoteFetcher:
|
|||||||
def fetch_agenda(self) -> tuple[list[Lesson], list[SchoolEvent]]:
|
def fetch_agenda(self) -> tuple[list[Lesson], list[SchoolEvent]]:
|
||||||
"""Récupère les cours et les événements scolaires selon la source configurée.
|
"""Récupère les cours et les événements scolaires selon la source configurée.
|
||||||
|
|
||||||
La source primaire est essayée en premier ; si elle échoue, la
|
En mode explicite (``ical`` ou ``pronotepy``), la source désignée
|
||||||
source de repli unique (l'autre source, si configurée) est
|
est la seule tentée : si elle échoue, une erreur critique est levée
|
||||||
essayée. Si la source primaire et le repli échouent — ou si
|
sans repli. En mode ``auto``, la source primaire est essayée en
|
||||||
aucune source n'est configurée en mode ``AUTO`` — une erreur
|
premier puis, si elle échoue, la source de repli unique (l'autre
|
||||||
critique est levée.
|
source, si configurée) l'est à son tour ; si la source primaire et
|
||||||
|
le repli échouent — ou si aucune source n'est configurée — une
|
||||||
|
erreur critique est levée.
|
||||||
|
|
||||||
:return: Tuple ``(cours, événements scolaires)``.
|
:return: Tuple ``(cours, événements scolaires)``.
|
||||||
:rtype: tuple[list[Lesson], list[SchoolEvent]]
|
:rtype: tuple[list[Lesson], list[SchoolEvent]]
|
||||||
@@ -289,11 +291,10 @@ class PronoteFetcher:
|
|||||||
def _homework_sources(self) -> tuple[_SourceName, _SourceName | None]:
|
def _homework_sources(self) -> tuple[_SourceName, _SourceName | None]:
|
||||||
"""Sélectionne la source primaire et le repli unique pour les devoirs.
|
"""Sélectionne la source primaire et le repli unique pour les devoirs.
|
||||||
|
|
||||||
En mode ``AUTO``, iCal est primaire si ``ical_url`` est configuré
|
Les modes explicites ``ICAL`` et ``PRONOTEPY`` désignent la seule
|
||||||
(repli pronotepy si la configuration pronotepy est complète),
|
source utilisée, sans aucun repli. En mode ``AUTO``, iCal est
|
||||||
sinon pronotepy sans repli. Les modes explicites ``ICAL`` et
|
primaire si ``ical_url`` est configuré (repli pronotepy si la
|
||||||
``PRONOTEPY`` désignent la source primaire et tentent l'autre
|
configuration pronotepy est complète), sinon pronotepy sans repli.
|
||||||
source en repli unique si elle est configurée.
|
|
||||||
|
|
||||||
:return: Tuple ``(source primaire, source de repli ou ``None``)``.
|
:return: Tuple ``(source primaire, source de repli ou ``None``)``.
|
||||||
:rtype: tuple[_SourceName, _SourceName | None]
|
:rtype: tuple[_SourceName, _SourceName | None]
|
||||||
@@ -301,9 +302,9 @@ class PronoteFetcher:
|
|||||||
"""
|
"""
|
||||||
source = AgendaSource(self._settings.pronote.homework_source)
|
source = AgendaSource(self._settings.pronote.homework_source)
|
||||||
if source is AgendaSource.ICAL:
|
if source is AgendaSource.ICAL:
|
||||||
return "ical", "pronotepy" if self._is_pronotepy_configured() else None
|
return "ical", None
|
||||||
if source is AgendaSource.PRONOTEPY:
|
if source is AgendaSource.PRONOTEPY:
|
||||||
return "pronotepy", "ical" if self._is_ical_configured() else None
|
return "pronotepy", None
|
||||||
if self._is_ical_configured():
|
if self._is_ical_configured():
|
||||||
return "ical", "pronotepy" if self._is_pronotepy_configured() else None
|
return "ical", "pronotepy" if self._is_pronotepy_configured() else None
|
||||||
if self._is_pronotepy_configured():
|
if self._is_pronotepy_configured():
|
||||||
@@ -327,11 +328,13 @@ class PronoteFetcher:
|
|||||||
def fetch_homework(self, target_date: date) -> list[Homework]:
|
def fetch_homework(self, target_date: date) -> list[Homework]:
|
||||||
"""Récupère les devoirs selon la source configurée.
|
"""Récupère les devoirs selon la source configurée.
|
||||||
|
|
||||||
La source primaire est essayée en premier ; si elle échoue, la
|
En mode explicite (``ical`` ou ``pronotepy``), la source désignée
|
||||||
source de repli unique (l'autre source, si configurée) est
|
est la seule tentée : si elle échoue, une erreur critique est levée
|
||||||
essayée. Si la source primaire et le repli échouent — ou si
|
sans repli. En mode ``auto``, la source primaire est essayée en
|
||||||
aucune source n'est configurée en mode ``AUTO`` — une erreur
|
premier puis, si elle échoue, la source de repli unique (l'autre
|
||||||
critique est levée.
|
source, si configurée) l'est à son tour ; si la source primaire et
|
||||||
|
le repli échouent — ou si aucune source n'est configurée — une
|
||||||
|
erreur critique est levée.
|
||||||
|
|
||||||
:param target_date: Date cible pour laquelle collecter les devoirs.
|
:param target_date: Date cible pour laquelle collecter les devoirs.
|
||||||
:return: Liste des devoirs.
|
:return: Liste des devoirs.
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ def test_fetch_agenda_ical_mode(mock_fetcher: PronoteFetcher) -> None:
|
|||||||
|
|
||||||
:param mock_fetcher: Fetcher de test.
|
:param mock_fetcher: Fetcher de test.
|
||||||
:return: None
|
:return: None
|
||||||
|
:rtype: None
|
||||||
"""
|
"""
|
||||||
start_dt = datetime(2025, 9, 1, 8, 0)
|
start_dt = datetime(2025, 9, 1, 8, 0)
|
||||||
end_dt = datetime(2025, 9, 1, 9, 30)
|
end_dt = datetime(2025, 9, 1, 9, 30)
|
||||||
@@ -133,6 +134,7 @@ def test_fetch_agenda_pronotepy_mode(mock_fetcher: PronoteFetcher) -> None:
|
|||||||
|
|
||||||
:param mock_fetcher: Fetcher de test.
|
:param mock_fetcher: Fetcher de test.
|
||||||
:return: None
|
:return: None
|
||||||
|
:rtype: None
|
||||||
"""
|
"""
|
||||||
start_dt = datetime(2025, 9, 1, 8, 0)
|
start_dt = datetime(2025, 9, 1, 8, 0)
|
||||||
end_dt = datetime(2025, 9, 1, 9, 30)
|
end_dt = datetime(2025, 9, 1, 9, 30)
|
||||||
@@ -168,6 +170,7 @@ def test_fetch_agenda_auto_ical_success(mock_fetcher: PronoteFetcher) -> None:
|
|||||||
|
|
||||||
:param mock_fetcher: Fetcher de test.
|
:param mock_fetcher: Fetcher de test.
|
||||||
:return: None
|
:return: None
|
||||||
|
:rtype: None
|
||||||
"""
|
"""
|
||||||
start_dt = datetime(2025, 9, 1, 8, 0)
|
start_dt = datetime(2025, 9, 1, 8, 0)
|
||||||
end_dt = datetime(2025, 9, 1, 9, 30)
|
end_dt = datetime(2025, 9, 1, 9, 30)
|
||||||
@@ -204,6 +207,7 @@ def test_fetch_agenda_auto_fallback_to_pronotepy(mock_fetcher: PronoteFetcher) -
|
|||||||
|
|
||||||
:param mock_fetcher: Fetcher de test.
|
:param mock_fetcher: Fetcher de test.
|
||||||
:return: None
|
:return: None
|
||||||
|
:rtype: None
|
||||||
"""
|
"""
|
||||||
start_dt = datetime(2025, 9, 1, 8, 0)
|
start_dt = datetime(2025, 9, 1, 8, 0)
|
||||||
end_dt = datetime(2025, 9, 1, 9, 30)
|
end_dt = datetime(2025, 9, 1, 9, 30)
|
||||||
@@ -244,6 +248,7 @@ def test_fetch_agenda_auto_both_fail(mock_fetcher: PronoteFetcher) -> None:
|
|||||||
|
|
||||||
:param mock_fetcher: Fetcher de test.
|
:param mock_fetcher: Fetcher de test.
|
||||||
:return: None
|
:return: None
|
||||||
|
:rtype: None
|
||||||
"""
|
"""
|
||||||
# Disable pronotepy so fallback is None
|
# Disable pronotepy so fallback is None
|
||||||
mock_fetcher._settings.pronote.pronote_url = None
|
mock_fetcher._settings.pronote.pronote_url = None
|
||||||
@@ -271,6 +276,7 @@ def test_fetch_agenda_ical_mode_failure(mock_fetcher: PronoteFetcher) -> None:
|
|||||||
|
|
||||||
:param mock_fetcher: Fetcher de test.
|
:param mock_fetcher: Fetcher de test.
|
||||||
:return: None
|
:return: None
|
||||||
|
:rtype: None
|
||||||
"""
|
"""
|
||||||
# Override settings to use ical mode explicitly and disable fallback
|
# Override settings to use ical mode explicitly and disable fallback
|
||||||
mock_fetcher._settings.pronote.agenda_source = "ical"
|
mock_fetcher._settings.pronote.agenda_source = "ical"
|
||||||
@@ -301,6 +307,7 @@ def test_fetch_agenda_pronotepy_mode_failure(mock_fetcher: PronoteFetcher) -> No
|
|||||||
|
|
||||||
:param mock_fetcher: Fetcher de test.
|
:param mock_fetcher: Fetcher de test.
|
||||||
:return: None
|
:return: None
|
||||||
|
:rtype: None
|
||||||
"""
|
"""
|
||||||
# Override settings to use pronotepy mode explicitly and disable fallback
|
# Override settings to use pronotepy mode explicitly and disable fallback
|
||||||
mock_fetcher._settings.pronote.agenda_source = "pronotepy"
|
mock_fetcher._settings.pronote.agenda_source = "pronotepy"
|
||||||
@@ -324,6 +331,7 @@ def test_fetch_homework_ical_mode(mock_fetcher: PronoteFetcher) -> None:
|
|||||||
|
|
||||||
:param mock_fetcher: Fetcher de test.
|
:param mock_fetcher: Fetcher de test.
|
||||||
:return: None
|
:return: None
|
||||||
|
:rtype: None
|
||||||
"""
|
"""
|
||||||
target_date = date(2025, 9, 5)
|
target_date = date(2025, 9, 5)
|
||||||
|
|
||||||
@@ -385,6 +393,7 @@ def test_fetch_homework_auto_fallback(mock_fetcher: PronoteFetcher) -> None:
|
|||||||
|
|
||||||
:param mock_fetcher: Fetcher de test.
|
:param mock_fetcher: Fetcher de test.
|
||||||
:return: None
|
:return: None
|
||||||
|
:rtype: None
|
||||||
"""
|
"""
|
||||||
target_date = date(2025, 9, 10)
|
target_date = date(2025, 9, 10)
|
||||||
homeworks = [
|
homeworks = [
|
||||||
@@ -424,6 +433,7 @@ def test_fetch_homework_auto_both_fail(mock_fetcher: PronoteFetcher) -> None:
|
|||||||
|
|
||||||
:param mock_fetcher: Fetcher de test.
|
:param mock_fetcher: Fetcher de test.
|
||||||
:return: None
|
:return: None
|
||||||
|
:rtype: None
|
||||||
"""
|
"""
|
||||||
target_date = date(2025, 9, 10)
|
target_date = date(2025, 9, 10)
|
||||||
|
|
||||||
@@ -453,6 +463,7 @@ def test_fetch_messages(mock_fetcher: PronoteFetcher) -> None:
|
|||||||
|
|
||||||
:param mock_fetcher: Fetcher de test.
|
:param mock_fetcher: Fetcher de test.
|
||||||
:return: None
|
:return: None
|
||||||
|
:rtype: None
|
||||||
"""
|
"""
|
||||||
messages = [
|
messages = [
|
||||||
Message(
|
Message(
|
||||||
@@ -483,6 +494,7 @@ def test_fetch_informations(mock_fetcher: PronoteFetcher) -> None:
|
|||||||
|
|
||||||
:param mock_fetcher: Fetcher de test.
|
:param mock_fetcher: Fetcher de test.
|
||||||
:return: None
|
:return: None
|
||||||
|
:rtype: None
|
||||||
"""
|
"""
|
||||||
infos = [
|
infos = [
|
||||||
Message(
|
Message(
|
||||||
@@ -516,6 +528,7 @@ def test_no_secrets_in_error_messages(
|
|||||||
:param mock_fetcher: Fetcher de test.
|
:param mock_fetcher: Fetcher de test.
|
||||||
:param caplog: Fixture pytest pour capturer les logs.
|
:param caplog: Fixture pytest pour capturer les logs.
|
||||||
:return: None
|
:return: None
|
||||||
|
:rtype: None
|
||||||
"""
|
"""
|
||||||
# Disable pronotepy so fallback is None to trigger PipelineCriticalError
|
# Disable pronotepy so fallback is None to trigger PipelineCriticalError
|
||||||
mock_fetcher._settings.pronote.pronote_url = None
|
mock_fetcher._settings.pronote.pronote_url = None
|
||||||
@@ -541,30 +554,18 @@ def test_no_secrets_in_error_messages(
|
|||||||
assert "icalsecurise=REDACTED" in caplog.text or "icalsecurise" not in caplog.text
|
assert "icalsecurise=REDACTED" in caplog.text or "icalsecurise" not in caplog.text
|
||||||
|
|
||||||
|
|
||||||
def test_fetch_agenda_ical_mode_fallback_to_pronotepy(mock_fetcher: PronoteFetcher) -> None:
|
def test_fetch_agenda_ical_mode_does_not_fallback_to_pronotepy(
|
||||||
"""Test le mode ICAL : échec iCal, repli sur pronotepy.
|
mock_fetcher: PronoteFetcher,
|
||||||
|
) -> None:
|
||||||
|
"""Test le mode ICAL : échec iCal, PAS de repli sur pronotepy.
|
||||||
|
|
||||||
On mock iCal pour échouer, pronotepy pour réussir. On vérifie que pronotepy est appelé.
|
On mock iCal pour échouer. On vérifie que pronotepy n'est PAS appelé
|
||||||
|
et qu'une PipelineCriticalError est levée.
|
||||||
|
|
||||||
:param mock_fetcher: Fetcher de test.
|
:param mock_fetcher: Fetcher de test.
|
||||||
:return: None
|
:return: None
|
||||||
|
:rtype: None
|
||||||
"""
|
"""
|
||||||
start_dt = datetime(2025, 9, 1, 8, 0)
|
|
||||||
end_dt = datetime(2025, 9, 1, 9, 30)
|
|
||||||
lessons = [
|
|
||||||
Lesson(
|
|
||||||
id="l1",
|
|
||||||
start=start_dt,
|
|
||||||
end=end_dt,
|
|
||||||
subject="Maths",
|
|
||||||
teachers=("Dupont",),
|
|
||||||
rooms=("S1",),
|
|
||||||
group="2ndeA",
|
|
||||||
status=LessonStatus.NORMAL,
|
|
||||||
content=None,
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
# Override settings to use ical mode explicitly
|
# Override settings to use ical mode explicitly
|
||||||
mock_fetcher._settings.pronote.agenda_source = "ical"
|
mock_fetcher._settings.pronote.agenda_source = "ical"
|
||||||
|
|
||||||
@@ -575,40 +576,28 @@ def test_fetch_agenda_ical_mode_fallback_to_pronotepy(mock_fetcher: PronoteFetch
|
|||||||
m_fetch_ical.side_effect = OSError("iCal unreachable")
|
m_fetch_ical.side_effect = OSError("iCal unreachable")
|
||||||
m_parse_ical.side_effect = OSError("iCal parse error")
|
m_parse_ical.side_effect = OSError("iCal parse error")
|
||||||
client = MagicMock()
|
client = MagicMock()
|
||||||
client.get_lessons.return_value = lessons
|
|
||||||
mock_fetcher._pronote_client = client
|
mock_fetcher._pronote_client = client
|
||||||
|
|
||||||
result_lessons, result_events = mock_fetcher.fetch_agenda()
|
with pytest.raises(PipelineCriticalError) as exc_info:
|
||||||
|
mock_fetcher.fetch_agenda()
|
||||||
|
|
||||||
assert result_lessons == lessons
|
assert "la source ical a échoué" in str(exc_info.value)
|
||||||
assert result_events == []
|
# Vérifie que pronotepy n'a PAS été appelé
|
||||||
client.get_lessons.assert_called_once()
|
client.get_lessons.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
def test_fetch_agenda_pronotepy_mode_fallback_to_ical(mock_fetcher: PronoteFetcher) -> None:
|
def test_fetch_agenda_pronotepy_mode_does_not_fallback_to_ical(
|
||||||
"""Test le mode PRONOTEPY : échec pronotepy, repli sur iCal.
|
mock_fetcher: PronoteFetcher,
|
||||||
|
) -> None:
|
||||||
|
"""Test le mode PRONOTEPY : échec pronotepy, PAS de repli sur iCal.
|
||||||
|
|
||||||
On mock pronotepy pour échouer, iCal pour réussir. On vérifie que iCal est appelé.
|
On mock pronotepy pour échouer. On vérifie que iCal n'est PAS appelé
|
||||||
|
et qu'une PipelineCriticalError est levée.
|
||||||
|
|
||||||
:param mock_fetcher: Fetcher de test.
|
:param mock_fetcher: Fetcher de test.
|
||||||
:return: None
|
:return: None
|
||||||
|
:rtype: None
|
||||||
"""
|
"""
|
||||||
start_dt = datetime(2025, 9, 1, 8, 0)
|
|
||||||
end_dt = datetime(2025, 9, 1, 9, 30)
|
|
||||||
lessons = [
|
|
||||||
Lesson(
|
|
||||||
id="l1",
|
|
||||||
start=start_dt,
|
|
||||||
end=end_dt,
|
|
||||||
subject="SVT",
|
|
||||||
teachers=("Durand",),
|
|
||||||
rooms=("S2",),
|
|
||||||
group="3emeC",
|
|
||||||
status=LessonStatus.NORMAL,
|
|
||||||
content=None,
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
# Override settings to use pronotepy mode explicitly
|
# Override settings to use pronotepy mode explicitly
|
||||||
mock_fetcher._settings.pronote.agenda_source = "pronotepy"
|
mock_fetcher._settings.pronote.agenda_source = "pronotepy"
|
||||||
|
|
||||||
@@ -619,15 +608,14 @@ def test_fetch_agenda_pronotepy_mode_fallback_to_ical(mock_fetcher: PronoteFetch
|
|||||||
client = MagicMock()
|
client = MagicMock()
|
||||||
client.get_lessons.side_effect = OSError("Pronote API error")
|
client.get_lessons.side_effect = OSError("Pronote API error")
|
||||||
mock_fetcher._pronote_client = client
|
mock_fetcher._pronote_client = client
|
||||||
m_fetch_ical.return_value = "BEGIN:VCALENDAR\n..."
|
|
||||||
m_parse_ical.return_value = (lessons, [], [])
|
|
||||||
|
|
||||||
result_lessons, result_events = mock_fetcher.fetch_agenda()
|
with pytest.raises(PipelineCriticalError) as exc_info:
|
||||||
|
mock_fetcher.fetch_agenda()
|
||||||
|
|
||||||
assert result_lessons == lessons
|
assert "la source pronotepy a échoué" in str(exc_info.value)
|
||||||
assert result_events == []
|
# Vérifie que iCal n'a PAS été appelé
|
||||||
m_fetch_ical.assert_called_once()
|
m_fetch_ical.assert_not_called()
|
||||||
m_parse_ical.assert_called_once()
|
m_parse_ical.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
def test_fetch_agenda_no_source_configured_raises(mock_fetcher: PronoteFetcher) -> None:
|
def test_fetch_agenda_no_source_configured_raises(mock_fetcher: PronoteFetcher) -> None:
|
||||||
@@ -637,6 +625,7 @@ def test_fetch_agenda_no_source_configured_raises(mock_fetcher: PronoteFetcher)
|
|||||||
|
|
||||||
:param mock_fetcher: Fetcher de test.
|
:param mock_fetcher: Fetcher de test.
|
||||||
:return: None
|
:return: None
|
||||||
|
:rtype: None
|
||||||
"""
|
"""
|
||||||
# Disable both sources
|
# Disable both sources
|
||||||
mock_fetcher._settings.pronote.ical_url = None
|
mock_fetcher._settings.pronote.ical_url = None
|
||||||
@@ -648,6 +637,130 @@ def test_fetch_agenda_no_source_configured_raises(mock_fetcher: PronoteFetcher)
|
|||||||
assert "ni la source iCal ni pronotepy n'est configurée" in str(exc_info.value)
|
assert "ni la source iCal ni pronotepy n'est configurée" in str(exc_info.value)
|
||||||
|
|
||||||
|
|
||||||
|
def test_fetch_agenda_ical_mode_empty_list_is_success(mock_fetcher: PronoteFetcher) -> None:
|
||||||
|
"""Test le mode ICAL : liste vide = succès valide, sans repli.
|
||||||
|
|
||||||
|
On mock iCal pour retourner une liste vide. On vérifie que le résultat
|
||||||
|
est ([], []) et qu'aucun repli n'est tenté.
|
||||||
|
|
||||||
|
:param mock_fetcher: Fetcher de test.
|
||||||
|
:return: None
|
||||||
|
:rtype: None
|
||||||
|
"""
|
||||||
|
mock_fetcher._settings.pronote.agenda_source = "ical"
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch("pronote_sync.sources.pronote.fallback.fetch_ical") as m_fetch_ical,
|
||||||
|
patch("pronote_sync.sources.pronote.fallback.parse_ical") as m_parse_ical,
|
||||||
|
):
|
||||||
|
m_fetch_ical.return_value = "BEGIN:VCALENDAR\n..."
|
||||||
|
m_parse_ical.return_value = ([], [], [])
|
||||||
|
client = MagicMock()
|
||||||
|
mock_fetcher._pronote_client = client
|
||||||
|
|
||||||
|
result_lessons, result_events = mock_fetcher.fetch_agenda()
|
||||||
|
|
||||||
|
assert result_lessons == []
|
||||||
|
assert result_events == []
|
||||||
|
# Vérifie que pronotepy n'a PAS été appelé
|
||||||
|
client.get_lessons.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
def test_fetch_agenda_pronotepy_mode_empty_list_is_success(
|
||||||
|
mock_fetcher: PronoteFetcher,
|
||||||
|
) -> None:
|
||||||
|
"""Test le mode PRONOTEPY : liste vide = succès valide, sans repli.
|
||||||
|
|
||||||
|
On mock pronotepy pour retourner une liste vide. On vérifie que le résultat
|
||||||
|
est ([], []) et qu'aucun repli n'est tenté.
|
||||||
|
|
||||||
|
:param mock_fetcher: Fetcher de test.
|
||||||
|
:return: None
|
||||||
|
:rtype: None
|
||||||
|
"""
|
||||||
|
mock_fetcher._settings.pronote.agenda_source = "pronotepy"
|
||||||
|
|
||||||
|
client = MagicMock()
|
||||||
|
client.get_lessons.return_value = []
|
||||||
|
mock_fetcher._pronote_client = client
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch("pronote_sync.sources.pronote.fallback.fetch_ical") as m_fetch_ical,
|
||||||
|
patch("pronote_sync.sources.pronote.fallback.parse_ical") as m_parse_ical,
|
||||||
|
):
|
||||||
|
result_lessons, result_events = mock_fetcher.fetch_agenda()
|
||||||
|
|
||||||
|
assert result_lessons == []
|
||||||
|
assert result_events == []
|
||||||
|
# Vérifie que iCal n'a PAS été appelé
|
||||||
|
m_fetch_ical.assert_not_called()
|
||||||
|
m_parse_ical.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
def test_fetch_homework_ical_mode_empty_list_is_success(
|
||||||
|
mock_fetcher: PronoteFetcher,
|
||||||
|
) -> None:
|
||||||
|
"""Test le mode ICAL des devoirs : liste vide = succès valide, sans repli.
|
||||||
|
|
||||||
|
On mock iCal pour retourner une liste vide. On vérifie que le résultat
|
||||||
|
est [] et qu'aucun repli n'est tenté.
|
||||||
|
|
||||||
|
:param mock_fetcher: Fetcher de test.
|
||||||
|
:return: None
|
||||||
|
:rtype: None
|
||||||
|
"""
|
||||||
|
target_date = date(2025, 9, 10)
|
||||||
|
mock_fetcher._settings.pronote.homework_source = "ical"
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch("pronote_sync.sources.pronote.fallback.fetch_ical") as m_fetch_ical,
|
||||||
|
patch("pronote_sync.sources.pronote.fallback.parse_ical") as m_parse_ical,
|
||||||
|
patch("pronote_sync.sources.pronote.fallback.collect_homeworks") as m_collect,
|
||||||
|
):
|
||||||
|
m_fetch_ical.return_value = "BEGIN:VCALENDAR\n..."
|
||||||
|
m_parse_ical.return_value = ([], [], [])
|
||||||
|
m_collect.return_value = []
|
||||||
|
client = MagicMock()
|
||||||
|
mock_fetcher._pronote_client = client
|
||||||
|
|
||||||
|
result = mock_fetcher.fetch_homework(target_date)
|
||||||
|
|
||||||
|
assert result == []
|
||||||
|
# Vérifie que pronotepy n'a PAS été appelé
|
||||||
|
client.get_homeworks.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
def test_fetch_homework_pronotepy_mode_empty_list_is_success(
|
||||||
|
mock_fetcher: PronoteFetcher,
|
||||||
|
) -> None:
|
||||||
|
"""Test le mode PRONOTEPY des devoirs : liste vide = succès valide, sans repli.
|
||||||
|
|
||||||
|
On mock pronotepy pour retourner une liste vide. On vérifie que le résultat
|
||||||
|
est [] et qu'aucun repli n'est tenté.
|
||||||
|
|
||||||
|
:param mock_fetcher: Fetcher de test.
|
||||||
|
:return: None
|
||||||
|
:rtype: None
|
||||||
|
"""
|
||||||
|
target_date = date(2025, 9, 10)
|
||||||
|
mock_fetcher._settings.pronote.homework_source = "pronotepy"
|
||||||
|
|
||||||
|
client = MagicMock()
|
||||||
|
client.get_homeworks.return_value = []
|
||||||
|
mock_fetcher._pronote_client = client
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch("pronote_sync.sources.pronote.fallback.fetch_ical") as m_fetch_ical,
|
||||||
|
patch("pronote_sync.sources.pronote.fallback.parse_ical") as m_parse_ical,
|
||||||
|
):
|
||||||
|
result = mock_fetcher.fetch_homework(target_date)
|
||||||
|
|
||||||
|
assert result == []
|
||||||
|
# Vérifie que iCal n'a PAS été appelé
|
||||||
|
m_fetch_ical.assert_not_called()
|
||||||
|
m_parse_ical.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
def test_fetch_homework_filters_by_target_date(mock_fetcher: PronoteFetcher) -> None:
|
def test_fetch_homework_filters_by_target_date(mock_fetcher: PronoteFetcher) -> None:
|
||||||
"""Test que les devoirs sont filtrés par date cible en mode pronotepy.
|
"""Test que les devoirs sont filtrés par date cible en mode pronotepy.
|
||||||
|
|
||||||
@@ -656,6 +769,7 @@ def test_fetch_homework_filters_by_target_date(mock_fetcher: PronoteFetcher) ->
|
|||||||
|
|
||||||
:param mock_fetcher: Fetcher de test.
|
:param mock_fetcher: Fetcher de test.
|
||||||
:return: None
|
:return: None
|
||||||
|
:rtype: None
|
||||||
"""
|
"""
|
||||||
target_date = date(2025, 9, 10)
|
target_date = date(2025, 9, 10)
|
||||||
other_date = date(2025, 9, 11)
|
other_date = date(2025, 9, 11)
|
||||||
@@ -693,6 +807,72 @@ def test_fetch_homework_filters_by_target_date(mock_fetcher: PronoteFetcher) ->
|
|||||||
client.get_homeworks.assert_called_once()
|
client.get_homeworks.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
def test_fetch_homework_ical_mode_does_not_fallback_to_pronotepy(
|
||||||
|
mock_fetcher: PronoteFetcher,
|
||||||
|
) -> None:
|
||||||
|
"""Test le mode ICAL des devoirs : échec iCal, PAS de repli sur pronotepy.
|
||||||
|
|
||||||
|
On mock iCal pour échouer. On vérifie que pronotepy n'est PAS appelé
|
||||||
|
et qu'une PipelineCriticalError est levée.
|
||||||
|
|
||||||
|
:param mock_fetcher: Fetcher de test.
|
||||||
|
:return: None
|
||||||
|
:rtype: None
|
||||||
|
"""
|
||||||
|
target_date = date(2025, 9, 10)
|
||||||
|
mock_fetcher._settings.pronote.homework_source = "ical"
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch("pronote_sync.sources.pronote.fallback.fetch_ical") as m_fetch_ical,
|
||||||
|
patch("pronote_sync.sources.pronote.fallback.parse_ical") as m_parse_ical,
|
||||||
|
patch("pronote_sync.sources.pronote.fallback.collect_homeworks") as m_collect,
|
||||||
|
):
|
||||||
|
m_fetch_ical.side_effect = OSError("iCal unreachable")
|
||||||
|
m_parse_ical.side_effect = OSError("iCal parse error")
|
||||||
|
client = MagicMock()
|
||||||
|
mock_fetcher._pronote_client = client
|
||||||
|
m_collect.side_effect = OSError("collect error")
|
||||||
|
|
||||||
|
with pytest.raises(PipelineCriticalError) as exc_info:
|
||||||
|
mock_fetcher.fetch_homework(target_date)
|
||||||
|
|
||||||
|
assert "la source ical a échoué" in str(exc_info.value)
|
||||||
|
# Vérifie que pronotepy n'a PAS été appelé
|
||||||
|
client.get_homeworks.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
def test_fetch_homework_pronotepy_mode_does_not_fallback_to_ical(
|
||||||
|
mock_fetcher: PronoteFetcher,
|
||||||
|
) -> None:
|
||||||
|
"""Test le mode PRONOTEPY des devoirs : échec pronotepy, PAS de repli sur iCal.
|
||||||
|
|
||||||
|
On mock pronotepy pour échouer. On vérifie que iCal n'est PAS appelé
|
||||||
|
et qu'une PipelineCriticalError est levée.
|
||||||
|
|
||||||
|
:param mock_fetcher: Fetcher de test.
|
||||||
|
:return: None
|
||||||
|
:rtype: None
|
||||||
|
"""
|
||||||
|
target_date = date(2025, 9, 10)
|
||||||
|
mock_fetcher._settings.pronote.homework_source = "pronotepy"
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch("pronote_sync.sources.pronote.fallback.fetch_ical") as m_fetch_ical,
|
||||||
|
patch("pronote_sync.sources.pronote.fallback.parse_ical") as m_parse_ical,
|
||||||
|
):
|
||||||
|
client = MagicMock()
|
||||||
|
client.get_homeworks.side_effect = OSError("Pronote API error")
|
||||||
|
mock_fetcher._pronote_client = client
|
||||||
|
|
||||||
|
with pytest.raises(PipelineCriticalError) as exc_info:
|
||||||
|
mock_fetcher.fetch_homework(target_date)
|
||||||
|
|
||||||
|
assert "la source pronotepy a échoué" in str(exc_info.value)
|
||||||
|
# Vérifie que iCal n'a PAS été appelé
|
||||||
|
m_fetch_ical.assert_not_called()
|
||||||
|
m_parse_ical.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
def test_fetch_messages_logs_and_re_raises(
|
def test_fetch_messages_logs_and_re_raises(
|
||||||
mock_fetcher: PronoteFetcher, caplog: pytest.LogCaptureFixture
|
mock_fetcher: PronoteFetcher, caplog: pytest.LogCaptureFixture
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -704,6 +884,7 @@ def test_fetch_messages_logs_and_re_raises(
|
|||||||
:param mock_fetcher: Fetcher de test.
|
:param mock_fetcher: Fetcher de test.
|
||||||
:param caplog: Fixture pytest pour capturer les logs.
|
:param caplog: Fixture pytest pour capturer les logs.
|
||||||
:return: None
|
:return: None
|
||||||
|
:rtype: None
|
||||||
"""
|
"""
|
||||||
client = MagicMock()
|
client = MagicMock()
|
||||||
error_msg = "Erreur Pronote : impossible de récupérer les messages https://pronote.example.com/messages?token=SECRET_TOKEN_456"
|
error_msg = "Erreur Pronote : impossible de récupérer les messages https://pronote.example.com/messages?token=SECRET_TOKEN_456"
|
||||||
@@ -729,6 +910,7 @@ def test_fetch_agenda_ical_url_none_raises_value_error(mock_fetcher: PronoteFetc
|
|||||||
|
|
||||||
:param mock_fetcher: Fetcher de test.
|
:param mock_fetcher: Fetcher de test.
|
||||||
:return: None
|
:return: None
|
||||||
|
:rtype: None
|
||||||
"""
|
"""
|
||||||
mock_fetcher._settings.pronote.ical_url = None
|
mock_fetcher._settings.pronote.ical_url = None
|
||||||
with pytest.raises(ValueError, match="PRONOTE_ICAL_URL est requis pour la source iCal"):
|
with pytest.raises(ValueError, match="PRONOTE_ICAL_URL est requis pour la source iCal"):
|
||||||
@@ -743,6 +925,7 @@ def test_agenda_sources_auto_only_pronotepy_configured(mock_fetcher: PronoteFetc
|
|||||||
|
|
||||||
:param mock_fetcher: Fetcher de test.
|
:param mock_fetcher: Fetcher de test.
|
||||||
:return: None
|
:return: None
|
||||||
|
:rtype: None
|
||||||
"""
|
"""
|
||||||
mock_fetcher._settings.pronote.agenda_source = "auto"
|
mock_fetcher._settings.pronote.agenda_source = "auto"
|
||||||
mock_fetcher._settings.pronote.ical_url = None
|
mock_fetcher._settings.pronote.ical_url = None
|
||||||
@@ -754,13 +937,14 @@ def test_agenda_sources_auto_only_pronotepy_configured(mock_fetcher: PronoteFetc
|
|||||||
def test_fetch_agenda_fallback_both_fail_raises_pipeline_critical_error(
|
def test_fetch_agenda_fallback_both_fail_raises_pipeline_critical_error(
|
||||||
mock_fetcher: PronoteFetcher,
|
mock_fetcher: PronoteFetcher,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test que fetch_agenda lève PipelineCriticalError si ICAL échoue et pronotepy aussi.
|
"""Test que fetch_agenda lève PipelineCriticalError en mode explicite ical.
|
||||||
|
|
||||||
On mock ICAL pour échouer, on garde pronotepy configuré mais on mock son échec.
|
On mock ICAL pour échouer. On vérifie que l'erreur critique est levée
|
||||||
On vérifie que l'erreur PipelineCriticalError est levée.
|
et que pronotepy n'est PAS appelé (pas de repli en mode explicite).
|
||||||
|
|
||||||
:param mock_fetcher: Fetcher de test.
|
:param mock_fetcher: Fetcher de test.
|
||||||
:return: None
|
:return: None
|
||||||
|
:rtype: None
|
||||||
"""
|
"""
|
||||||
mock_fetcher._settings.pronote.agenda_source = "ical"
|
mock_fetcher._settings.pronote.agenda_source = "ical"
|
||||||
|
|
||||||
@@ -777,21 +961,26 @@ def test_fetch_agenda_fallback_both_fail_raises_pipeline_critical_error(
|
|||||||
with pytest.raises(PipelineCriticalError) as exc_info:
|
with pytest.raises(PipelineCriticalError) as exc_info:
|
||||||
mock_fetcher.fetch_agenda()
|
mock_fetcher.fetch_agenda()
|
||||||
|
|
||||||
assert "les sources ical et pronotepy ont échoué" in str(exc_info.value)
|
# En mode explicite, seul l'échec de la source primaire est rapporté
|
||||||
|
assert "la source ical a échoué" in str(exc_info.value)
|
||||||
|
# Vérifie que pronotepy n'a PAS été appelé (pas de repli en mode explicite)
|
||||||
|
client.get_lessons.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
def test_fetch_agenda_fallback_returns_empty_logs_warning(
|
def test_fetch_agenda_auto_fallback_returns_empty_logs_warning(
|
||||||
mock_fetcher: PronoteFetcher, caplog: pytest.LogCaptureFixture
|
mock_fetcher: PronoteFetcher, caplog: pytest.LogCaptureFixture
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test que fetch_agenda retourne ([], []) et journalise un avertissement si le repli retourne vide.
|
"""Test que fetch_agenda retourne ([], []) et journalise un avertissement si le repli retourne vide.
|
||||||
|
|
||||||
On mock ICAL pour échouer, pronotepy configuré et retourne vide. On vérifie le retour et le log.
|
On mock ICAL pour échouer, pronotepy configuré et retourne vide. On vérifie le retour et le log.
|
||||||
|
Ce test utilise le mode AUTO pour tester le comportement de repli.
|
||||||
|
|
||||||
:param mock_fetcher: Fetcher de test.
|
:param mock_fetcher: Fetcher de test.
|
||||||
:param caplog: Fixture pytest pour capturer les logs.
|
:param caplog: Fixture pytest pour capturer les logs.
|
||||||
:return: None
|
:return: None
|
||||||
|
:rtype: None
|
||||||
"""
|
"""
|
||||||
mock_fetcher._settings.pronote.agenda_source = "ical"
|
mock_fetcher._settings.pronote.agenda_source = "auto"
|
||||||
|
|
||||||
with (
|
with (
|
||||||
patch("pronote_sync.sources.pronote.fallback.fetch_ical") as m_fetch_ical,
|
patch("pronote_sync.sources.pronote.fallback.fetch_ical") as m_fetch_ical,
|
||||||
@@ -813,21 +1002,22 @@ def test_fetch_agenda_fallback_returns_empty_logs_warning(
|
|||||||
assert "a retourné un résultat vide après l'échec de ical" in caplog.text
|
assert "a retourné un résultat vide après l'échec de ical" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
def test_homework_sources_explicit_ical_mode(mock_fetcher: PronoteFetcher) -> None:
|
def test_homework_sources_explicit_ical_mode_strict(mock_fetcher: PronoteFetcher) -> None:
|
||||||
"""Test _homework_sources en mode ICAL.
|
"""Test _homework_sources en mode ICAL : strict, sans repli.
|
||||||
|
|
||||||
On vérifie que la source primaire est ical et que le repli est pronotepy si configuré,
|
On vérifie que la source primaire est ical et que le repli est TOUJOURS None,
|
||||||
ou None sinon.
|
même si pronotepy est configuré.
|
||||||
|
|
||||||
:param mock_fetcher: Fetcher de test.
|
:param mock_fetcher: Fetcher de test.
|
||||||
:return: None
|
:return: None
|
||||||
|
:rtype: None
|
||||||
"""
|
"""
|
||||||
mock_fetcher._settings.pronote.homework_source = "ical"
|
mock_fetcher._settings.pronote.homework_source = "ical"
|
||||||
|
|
||||||
# With pronotepy configured
|
# With pronotepy configured
|
||||||
primary, fallback = mock_fetcher._homework_sources()
|
primary, fallback = mock_fetcher._homework_sources()
|
||||||
assert primary == "ical"
|
assert primary == "ical"
|
||||||
assert fallback == "pronotepy"
|
assert fallback is None
|
||||||
|
|
||||||
# Without pronotepy configured
|
# Without pronotepy configured
|
||||||
mock_fetcher._settings.pronote.pronote_url = None
|
mock_fetcher._settings.pronote.pronote_url = None
|
||||||
@@ -836,21 +1026,24 @@ def test_homework_sources_explicit_ical_mode(mock_fetcher: PronoteFetcher) -> No
|
|||||||
assert fallback is None
|
assert fallback is None
|
||||||
|
|
||||||
|
|
||||||
def test_homework_sources_explicit_pronotepy_mode(mock_fetcher: PronoteFetcher) -> None:
|
def test_homework_sources_explicit_pronotepy_mode_strict(
|
||||||
"""Test _homework_sources en mode PRONOTEPY.
|
mock_fetcher: PronoteFetcher,
|
||||||
|
) -> None:
|
||||||
|
"""Test _homework_sources en mode PRONOTEPY : strict, sans repli.
|
||||||
|
|
||||||
On vérifie que la source primaire est pronotepy et que le repli est ical si configuré,
|
On vérifie que la source primaire est pronotepy et que le repli est TOUJOURS None,
|
||||||
ou None sinon.
|
même si iCal est configuré.
|
||||||
|
|
||||||
:param mock_fetcher: Fetcher de test.
|
:param mock_fetcher: Fetcher de test.
|
||||||
:return: None
|
:return: None
|
||||||
|
:rtype: None
|
||||||
"""
|
"""
|
||||||
mock_fetcher._settings.pronote.homework_source = "pronotepy"
|
mock_fetcher._settings.pronote.homework_source = "pronotepy"
|
||||||
|
|
||||||
# With ical configured
|
# With ical configured
|
||||||
primary, fallback = mock_fetcher._homework_sources()
|
primary, fallback = mock_fetcher._homework_sources()
|
||||||
assert primary == "pronotepy"
|
assert primary == "pronotepy"
|
||||||
assert fallback == "ical"
|
assert fallback is None
|
||||||
|
|
||||||
# Without ical configured
|
# Without ical configured
|
||||||
mock_fetcher._settings.pronote.ical_url = None
|
mock_fetcher._settings.pronote.ical_url = None
|
||||||
@@ -867,6 +1060,7 @@ def test_homework_sources_auto_only_pronotepy_configured(mock_fetcher: PronoteFe
|
|||||||
|
|
||||||
:param mock_fetcher: Fetcher de test.
|
:param mock_fetcher: Fetcher de test.
|
||||||
:return: None
|
:return: None
|
||||||
|
:rtype: None
|
||||||
"""
|
"""
|
||||||
mock_fetcher._settings.pronote.homework_source = "auto"
|
mock_fetcher._settings.pronote.homework_source = "auto"
|
||||||
mock_fetcher._settings.pronote.ical_url = None
|
mock_fetcher._settings.pronote.ical_url = None
|
||||||
@@ -882,6 +1076,7 @@ def test_homework_sources_auto_no_source_configured_raises(mock_fetcher: Pronote
|
|||||||
|
|
||||||
:param mock_fetcher: Fetcher de test.
|
:param mock_fetcher: Fetcher de test.
|
||||||
:return: None
|
:return: None
|
||||||
|
:rtype: None
|
||||||
"""
|
"""
|
||||||
mock_fetcher._settings.pronote.homework_source = "auto"
|
mock_fetcher._settings.pronote.homework_source = "auto"
|
||||||
mock_fetcher._settings.pronote.ical_url = None
|
mock_fetcher._settings.pronote.ical_url = None
|
||||||
@@ -896,13 +1091,14 @@ def test_homework_sources_auto_no_source_configured_raises(mock_fetcher: Pronote
|
|||||||
def test_fetch_homework_fallback_both_fail_raises_pipeline_critical_error(
|
def test_fetch_homework_fallback_both_fail_raises_pipeline_critical_error(
|
||||||
mock_fetcher: PronoteFetcher,
|
mock_fetcher: PronoteFetcher,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test que fetch_homework lève PipelineCriticalError si ICAL échoue et pronotepy aussi.
|
"""Test que fetch_homework lève PipelineCriticalError en mode explicite ical.
|
||||||
|
|
||||||
On mock ICAL pour échouer, on garde pronotepy configuré mais on mock son échec.
|
On mock ICAL pour échouer. On vérifie que l'erreur critique est levée
|
||||||
On vérifie que l'erreur PipelineCriticalError est levée.
|
et que pronotepy n'est PAS appelé (pas de repli en mode explicite).
|
||||||
|
|
||||||
:param mock_fetcher: Fetcher de test.
|
:param mock_fetcher: Fetcher de test.
|
||||||
:return: None
|
:return: None
|
||||||
|
:rtype: None
|
||||||
"""
|
"""
|
||||||
target_date = date(2025, 9, 10)
|
target_date = date(2025, 9, 10)
|
||||||
mock_fetcher._settings.pronote.homework_source = "ical"
|
mock_fetcher._settings.pronote.homework_source = "ical"
|
||||||
@@ -922,22 +1118,27 @@ def test_fetch_homework_fallback_both_fail_raises_pipeline_critical_error(
|
|||||||
with pytest.raises(PipelineCriticalError) as exc_info:
|
with pytest.raises(PipelineCriticalError) as exc_info:
|
||||||
mock_fetcher.fetch_homework(target_date)
|
mock_fetcher.fetch_homework(target_date)
|
||||||
|
|
||||||
assert "les sources ical et pronotepy ont échoué" in str(exc_info.value)
|
# En mode explicite, seul l'échec de la source primaire est rapporté
|
||||||
|
assert "la source ical a échoué" in str(exc_info.value)
|
||||||
|
# Vérifie que pronotepy n'a PAS été appelé (pas de repli en mode explicite)
|
||||||
|
client.get_homeworks.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
def test_fetch_homework_fallback_returns_empty_logs_warning(
|
def test_fetch_homework_auto_fallback_returns_empty_logs_warning(
|
||||||
mock_fetcher: PronoteFetcher, caplog: pytest.LogCaptureFixture
|
mock_fetcher: PronoteFetcher, caplog: pytest.LogCaptureFixture
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test que fetch_homework retourne [] et journalise un avertissement si le repli retourne vide.
|
"""Test que fetch_homework retourne [] et journalise un avertissement si le repli retourne vide.
|
||||||
|
|
||||||
On mock ICAL pour échouer, pronotepy configuré et retourne vide. On vérifie le retour et le log.
|
On mock ICAL pour échouer, pronotepy configuré et retourne vide. On vérifie le retour et le log.
|
||||||
|
Ce test utilise le mode AUTO pour tester le comportement de repli.
|
||||||
|
|
||||||
:param mock_fetcher: Fetcher de test.
|
:param mock_fetcher: Fetcher de test.
|
||||||
:param caplog: Fixture pytest pour capturer les logs.
|
:param caplog: Fixture pytest pour capturer les logs.
|
||||||
:return: None
|
:return: None
|
||||||
|
:rtype: None
|
||||||
"""
|
"""
|
||||||
target_date = date(2025, 9, 10)
|
target_date = date(2025, 9, 10)
|
||||||
mock_fetcher._settings.pronote.homework_source = "ical"
|
mock_fetcher._settings.pronote.homework_source = "auto"
|
||||||
|
|
||||||
with (
|
with (
|
||||||
patch("pronote_sync.sources.pronote.fallback.fetch_ical") as m_fetch_ical,
|
patch("pronote_sync.sources.pronote.fallback.fetch_ical") as m_fetch_ical,
|
||||||
@@ -967,6 +1168,7 @@ def test_fetch_informations_logs_and_re_raises_secret(
|
|||||||
:param mock_fetcher: Fetcher de test.
|
:param mock_fetcher: Fetcher de test.
|
||||||
:param caplog: Fixture pytest pour capturer les logs.
|
:param caplog: Fixture pytest pour capturer les logs.
|
||||||
:return: None
|
:return: None
|
||||||
|
:rtype: None
|
||||||
"""
|
"""
|
||||||
client = MagicMock()
|
client = MagicMock()
|
||||||
error_msg = "Erreur Pronote : impossible de récupérer les informations https://pronote.example.com/infos?token=SECRET_TOKEN_789"
|
error_msg = "Erreur Pronote : impossible de récupérer les informations https://pronote.example.com/infos?token=SECRET_TOKEN_789"
|
||||||
|
|||||||
Reference in New Issue
Block a user