"""Unit tests for PronoteAuthRotationError propagation through each layer. These tests verify that the rotation error propagates correctly through the real call chain without being wrapped in PipelineCriticalError at any layer. """ from __future__ import annotations from datetime import date import pytest from pydantic import SecretStr from pronote_sync.config.settings import PronoteSettings, Settings from pronote_sync.errors import PipelineCriticalError, PronoteAuthRotationError from pronote_sync.models.agenda import Lesson, SchoolEvent from pronote_sync.models.homework import Homework from pronote_sync.models.message import Message from pronote_sync.pipeline.steps.fetch import fetch_step from pronote_sync.sources.pronote.fallback import PronoteFetcher class StubPronoteClientWithRotationError: """Stub PronoteClient that raises PronoteAuthRotationError from its methods.""" def get_lessons(self, start: date, end: date) -> list[Lesson]: """Raise rotation error when fetching lessons. :param start: Start date (unused). :param end: End date (unused). :return: Never returns. :raises PronoteAuthRotationError: Always. """ del start, end raise PronoteAuthRotationError("Token persisté expiré : ré-enrôlement requis") def get_homeworks(self, start: date, end: date) -> list[Homework]: """Raise rotation error when fetching homeworks. :param start: Start date (unused). :param end: End date (unused). :return: Never returns. :raises PronoteAuthRotationError: Always. """ del start, end raise PronoteAuthRotationError("Token persisté expiré : ré-enrôlement requis") def get_messages(self) -> list[Message]: """Return empty messages list. :return: Empty list. :rtype: list[Message] """ return [] def get_informations(self) -> list[Message]: """Return empty information messages list. :return: Empty list. :rtype: list[Message] """ return [] class StubSettings: """Minimal settings stub for PronoteFetcher.""" def __init__(self) -> None: """Initialize with minimal configuration.""" self.pronote = PronoteSettings( url="https://pronote.example.com", username="test", password=SecretStr("test_password"), ent="bordeaux", account_type="parent", agenda_source="pronotepy", homework_source="pronotepy", messages_source="pronotepy", auth_mode="password", qr_code_file=None, qr_pin=None, ical_url=None, ) self.app = type("AppSettings", (), {"sync_past_days": 7, "sync_future_days": 7})() class StubFetcherWithRotationError: """Stub PronoteFetcher that raises PronoteAuthRotationError from its methods.""" def __init__(self) -> None: """Initialize the stub fetcher.""" self._settings = StubSettings() self._client = StubPronoteClientWithRotationError() def fetch_agenda(self) -> tuple[list[Lesson], list[SchoolEvent]]: """Raise rotation error when fetching agenda. :return: Never returns. :rtype: tuple[list[Lesson], list[SchoolEvent]] :raises PronoteAuthRotationError: Always. """ raise PronoteAuthRotationError("Token persisté expiré : ré-enrôlement requis") def fetch_homework(self, target_date: date) -> list[Homework]: """Raise rotation error when fetching homework. :param target_date: Target date (unused). :return: Never returns. :rtype: list[Homework] :raises PronoteAuthRotationError: Always. """ del target_date raise PronoteAuthRotationError("Token persisté expiré : ré-enrôlement requis") def fetch_messages(self) -> list[Message]: """Return empty messages list. :return: Empty list. :rtype: list[Message] """ return [] def fetch_informations(self) -> list[Message]: """Return empty information messages list. :return: Empty list. :rtype: list[Message] """ return [] def test_pronote_fetcher_fetch_agenda_propagates_rotation_error() -> None: """PronoteFetcher.fetch_agenda() propagates PronoteAuthRotationError without wrapping. This test verifies that when the underlying PronoteClient raises PronoteAuthRotationError, the fetcher propagates it directly without converting it to PipelineCriticalError. """ settings = Settings(pronote=StubSettings().pronote) fetcher = PronoteFetcher(settings, StubPronoteClientWithRotationError()) with pytest.raises(PronoteAuthRotationError) as exc_info: fetcher.fetch_agenda() assert "Token persisté expiré" in str(exc_info.value) assert not isinstance(exc_info.value, PipelineCriticalError) def test_pronote_fetcher_fetch_homework_propagates_rotation_error() -> None: """PronoteFetcher.fetch_homework() propagates PronoteAuthRotationError without wrapping. This test verifies that when the underlying PronoteClient raises PronoteAuthRotationError, the fetcher propagates it directly without converting it to PipelineCriticalError. """ settings = Settings(pronote=StubSettings().pronote) fetcher = PronoteFetcher(settings, StubPronoteClientWithRotationError()) target_date = date(2026, 9, 9) with pytest.raises(PronoteAuthRotationError) as exc_info: fetcher.fetch_homework(target_date) assert "Token persisté expiré" in str(exc_info.value) assert not isinstance(exc_info.value, PipelineCriticalError) def test_fetch_step_propagates_rotation_error() -> None: """fetch_step() propagates PronoteAuthRotationError without wrapping. This test verifies that the pipeline step fetch_step() propagates PronoteAuthRotationError directly from the fetcher without converting it to PipelineCriticalError. """ fetcher = StubFetcherWithRotationError() with pytest.raises(PronoteAuthRotationError) as exc_info: fetch_step(fetcher, today=date(2026, 9, 8)) assert "Token persisté expiré" in str(exc_info.value) assert not isinstance(exc_info.value, PipelineCriticalError)