fix: PRONOTE_ENT optionnel pour les connexions pronotepy directes

PRONOTE_ENT était incorrectement traité comme obligatoire, bloquant
le pipeline quand l'ENT n'est pas configuré. Permet désormais la
connexion directe à Pronote sans ENT (ent=None), conformément à la
spécification (GUIDE_DEV_PYTHON.md §3.1.2).

Corrige deux bugs :
- Mode pronotepy explicite sans PRONOTE_ENT : erreur « ent sont requis »
- Mode auto sans iCal ni PRONOTE_ENT : erreur « ni la source iCal ni
  pronotepy n'est configurée » alors que les credentials sont présents

Changements :
- client.py : _connect() n'exige plus ent, résolution conditionnelle
- fallback.py : _is_pronotepy_configured() sans vérifier ent
- tests : 10 tests de régression (RED→GREEN)

Co-authored-by: opencode/coder litellm/coder@agents.invalid
Co-authored-by: opencode/test-engineer litellm/test-engineer@agents.invalid
This commit is contained in:
2026-09-08 20:53:10 +02:00
parent c851f67172
commit ff740bc28b
4 changed files with 385 additions and 17 deletions

View File

@@ -1088,6 +1088,102 @@ def test_homework_sources_auto_no_source_configured_raises(mock_fetcher: Pronote
assert "ni la source iCal ni pronotepy n'est configurée" in str(exc_info.value)
def test_fetch_agenda_auto_ical_configured_fails_fallback_to_pronotepy_without_ent(
mock_fetcher: PronoteFetcher,
) -> None:
"""Test le mode auto : iCal configuré mais échoue, repli sur pronotepy sans ent.
On mock iCal pour échouer, pronotepy configuré sans ent. On vérifie que pronotepy est appelé
et que le résultat provient de pronotepy, pas une erreur.
:param mock_fetcher: Fetcher de test.
: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 auto mode with ical_url configured but ent=None
mock_fetcher._settings.pronote.agenda_source = "auto"
mock_fetcher._settings.pronote.ent = None # Explicitly None
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.side_effect = OSError("iCal unreachable")
m_parse_ical.side_effect = OSError("iCal parse error")
client = MagicMock()
client.get_lessons.return_value = lessons
mock_fetcher._pronote_client = client
result_lessons, result_events = mock_fetcher.fetch_agenda()
assert result_lessons == lessons
assert result_events == []
client.get_lessons.assert_called_once()
def test_fetch_homework_auto_ical_configured_fails_fallback_to_pronotepy_without_ent(
mock_fetcher: PronoteFetcher,
) -> None:
"""Test le mode auto des devoirs : iCal configuré mais échoue, repli sur pronotepy sans ent.
On mock iCal pour échouer, pronotepy configuré sans ent. On vérifie que pronotepy est appelé
et que le résultat provient de pronotepy, pas une erreur.
:param mock_fetcher: Fetcher de test.
:return: None
:rtype: None
"""
target_date = date(2025, 9, 10)
homeworks = [
Homework(
id="hw1",
subject="Physique",
teachers=(),
assigned_on=None,
due_on=target_date,
text="TP à préparer",
html="TP à préparer",
)
]
# Override settings to use auto mode with ical_url configured but ent=None
mock_fetcher._settings.pronote.homework_source = "auto"
mock_fetcher._settings.pronote.ent = None # Explicitly None
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()
client.get_homeworks.return_value = homeworks
mock_fetcher._pronote_client = client
m_collect.return_value = homeworks
result = mock_fetcher.fetch_homework(target_date)
assert result == homeworks
client.get_homeworks.assert_called_once()
def test_fetch_homework_fallback_both_fail_raises_pipeline_critical_error(
mock_fetcher: PronoteFetcher,
) -> None:
@@ -1127,7 +1223,7 @@ def test_fetch_homework_fallback_both_fail_raises_pipeline_critical_error(
def test_fetch_homework_auto_fallback_returns_empty_logs_warning(
mock_fetcher: PronoteFetcher, caplog: pytest.LogCaptureFixture
) -> 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 est vide.
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.
@@ -1186,4 +1282,99 @@ def test_fetch_informations_logs_and_re_raises_secret(
)
def test_is_pronotepy_configured_without_ent_returns_true() -> None:
"""Test _is_pronotepy_configured() retourne True quand ent est None.
Les autres champs de la configuration pronotepy sont présents, donc la
fonction renvoie True.
Ce test valide que PRONOTE_ENT est optionnel pour pronotepy.
:return: None
"""
from pronote_sync.config.settings import PronoteSettings, Settings
from pronote_sync.sources.pronote.fallback import PronoteFetcher
# Créer des settings avec pronotepy configuré mais sans ent
settings = Settings(
pronote=PronoteSettings(
pronote_url="https://pronote.example.com",
username="testuser",
password=SecretStr("testpass"),
ent=None, # Explicitement None
agenda_source="pronotepy",
homework_source="pronotepy",
),
app=Settings().app,
)
client: _MockPronoteClientProtocol = MagicMock()
fetcher = PronoteFetcher(settings=settings, pronote_client=client)
# Should return True even without ent
assert fetcher._is_pronotepy_configured() is True
def test_agenda_sources_auto_without_ical_and_without_ent_returns_pronotepy() -> None:
"""Test _agenda_sources() en mode AUTO sans iCal URL et sans ent retourne pronotepy.
Ce test valide que le mode auto peut utiliser pronotepy même sans ent configuré.
:return: None
"""
from pronote_sync.config.settings import PronoteSettings, Settings
from pronote_sync.sources.pronote.fallback import PronoteFetcher
settings = Settings(
pronote=PronoteSettings(
pronote_url="https://pronote.example.com",
username="testuser",
password=SecretStr("testpass"),
ent=None, # Explicitement None
ical_url=None, # Pas de iCal URL
agenda_source="auto",
),
app=Settings().app,
)
client: _MockPronoteClientProtocol = MagicMock()
fetcher = PronoteFetcher(settings=settings, pronote_client=client)
primary, fallback = fetcher._agenda_sources()
assert primary == "pronotepy"
assert fallback is None
def test_homework_sources_auto_without_ical_and_without_ent_returns_pronotepy() -> None:
"""Test _homework_sources() en mode AUTO sans iCal URL et sans ent retourne pronotepy.
Ce test valide que le mode auto peut utiliser pronotepy pour les devoirs même sans ent configuré.
:return: None
"""
from pronote_sync.config.settings import PronoteSettings, Settings
from pronote_sync.sources.pronote.fallback import PronoteFetcher
settings = Settings(
pronote=PronoteSettings(
pronote_url="https://pronote.example.com",
username="testuser",
password=SecretStr("testpass"),
ent=None, # Explicitement None
ical_url=None, # Pas de iCal URL
homework_source="auto",
),
app=Settings().app,
)
client: _MockPronoteClientProtocol = MagicMock()
fetcher = PronoteFetcher(settings=settings, pronote_client=client)
primary, fallback = fetcher._homework_sources()
assert primary == "pronotepy"
assert fallback is None
# Ensure trailing newline