From 3b38253575c6d6f9fb2f9f3342f52636fe85f533 Mon Sep 17 00:00:00 2001 From: Antoine Van Elstraete Date: Tue, 8 Sep 2026 21:24:10 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20PRONOTE=5FURL=20ignor=C3=A9=20=C3=A0=20c?= =?UTF-8?q?ause=20du=20double=20pr=C3=A9fixe=20env=5Fprefix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le champ pronote_url dans PronoteSettings avec env_prefix=PRONOTE_ produisait PRONOTE_PRONOTE_URL au lieu de PRONOTE_URL. Renomme le champ en url pour que le mécanisme standard produise PRONOTE_URL. Toutes les références mises à jour dans le code de production et les tests. Décision d'architecture : renommage préféré à un contournement par alias (mypy + dette technique). Co-authored-by: Antoine Van Elstraete Co-committed-by: Antoine Van Elstraete --- pronote_sync/config/settings.py | 2 +- pronote_sync/sources/pronote/client.py | 10 ++++----- pronote_sync/sources/pronote/fallback.py | 4 ++-- tests/unit/test_config.py | 15 +++++++++++++ tests/unit/test_fallback.py | 22 +++++++++---------- tests/unit/test_pronote_client.py | 28 +++++++++++------------- 6 files changed, 47 insertions(+), 34 deletions(-) diff --git a/pronote_sync/config/settings.py b/pronote_sync/config/settings.py index 86126c5..aa39a64 100644 --- a/pronote_sync/config/settings.py +++ b/pronote_sync/config/settings.py @@ -37,7 +37,7 @@ class PronoteSettings(BaseSettings): username: str | None = None password: SecretStr | None = None ent: str | None = None - pronote_url: str | None = None + url: str | None = None account_type: Literal["student", "parent"] = "parent" agenda_source: Literal["auto", "ical", "pronotepy"] = "auto" homework_source: Literal["auto", "ical", "pronotepy"] = "auto" diff --git a/pronote_sync/sources/pronote/client.py b/pronote_sync/sources/pronote/client.py index 7a63a4f..297f5f1 100644 --- a/pronote_sync/sources/pronote/client.py +++ b/pronote_sync/sources/pronote/client.py @@ -164,17 +164,17 @@ class PronoteClient: :return: Le client ``pronotepy`` connecté. :rtype: pronotepy.Client - :raises ValueError: Si ``pronote_url``, ``username`` ou ``password`` + :raises ValueError: Si ``url``, ``username`` ou ``password`` est manquant, ou si l'ENT fourni est inconnu. :raises pronotepy.PronoteAPIError: Si la connexion à Pronote échoue. """ if self._client is None: - pronote_url = self._settings.pronote_url + url = self._settings.url username = self._settings.username password = self._settings.password ent = self._settings.ent - if pronote_url is None or username is None or password is None: - raise ValueError("pronote_url, username et password sont requis pour pronotepy") + if url is None or username is None or password is None: + raise ValueError("url, username et password sont requis pour pronotepy") resolver = _resolve_ent(ent) if ent is not None else None client_class: type[pronotepy.Client] = ( pronotepy.ParentClient @@ -182,7 +182,7 @@ class PronoteClient: else pronotepy.Client ) self._client = client_class( - pronote_url=pronote_url, + pronote_url=url, username=username, password=password.get_secret_value(), ent=resolver, diff --git a/pronote_sync/sources/pronote/fallback.py b/pronote_sync/sources/pronote/fallback.py index 3fbdcf3..39ef545 100644 --- a/pronote_sync/sources/pronote/fallback.py +++ b/pronote_sync/sources/pronote/fallback.py @@ -149,13 +149,13 @@ class PronoteFetcher: def _is_pronotepy_configured(self) -> bool: """Vérifie que la source pronotepy est entièrement configurée. - :return: ``True`` si ``pronote_url``, ``username`` et ``password`` + :return: ``True`` si ``url``, ``username`` et ``password`` sont tous définis, ``False`` sinon. :rtype: bool """ pronote = self._settings.pronote return ( - pronote.pronote_url is not None + pronote.url is not None and pronote.username is not None and pronote.password is not None ) diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index 93b64ea..d5ea40e 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -116,4 +116,19 @@ def test_no_singleton_import() -> None: ) +def test_url_from_pronote_url_env_var(monkeypatch: MonkeyPatch) -> None: + """Vérifie que PRONOTE_URL mappe au champ url via le préfixe PRONOTE_. + + Ce test couvre la régression où PRONOTE_URL n'était pas mappé vers le + champ du modèle à cause du double préfixe PRONOTE_. + + :param monkeypatch: Fixture pytest pour modifier temporairement l'environnement. + :return: None + """ + test_url = "https://example.index-education.net/pronote/parent.html" + monkeypatch.setenv("PRONOTE_URL", test_url) + settings = load_settings() + assert settings.pronote.url == test_url + + # Ensure trailing newline diff --git a/tests/unit/test_fallback.py b/tests/unit/test_fallback.py index a71afd3..282fc4e 100644 --- a/tests/unit/test_fallback.py +++ b/tests/unit/test_fallback.py @@ -53,7 +53,7 @@ def fixture_mock_settings() -> Settings: """ return Settings( pronote=PronoteSettings( - pronote_url="https://pronote.example.com", + url="https://pronote.example.com", ical_url=SecretStr("file:///fake/ical.ics"), agenda_source="auto", homework_source="auto", @@ -251,7 +251,7 @@ def test_fetch_agenda_auto_both_fail(mock_fetcher: PronoteFetcher) -> None: :rtype: None """ # Disable pronotepy so fallback is None - mock_fetcher._settings.pronote.pronote_url = None + mock_fetcher._settings.pronote.url = None with ( patch("pronote_sync.sources.pronote.fallback.fetch_ical") as m_fetch_ical, @@ -280,7 +280,7 @@ def test_fetch_agenda_ical_mode_failure(mock_fetcher: PronoteFetcher) -> None: """ # Override settings to use ical mode explicitly and disable fallback mock_fetcher._settings.pronote.agenda_source = "ical" - mock_fetcher._settings.pronote.pronote_url = None + mock_fetcher._settings.pronote.url = None with ( patch("pronote_sync.sources.pronote.fallback.fetch_ical") as m_fetch_ical, @@ -438,7 +438,7 @@ def test_fetch_homework_auto_both_fail(mock_fetcher: PronoteFetcher) -> None: target_date = date(2025, 9, 10) # Disable pronotepy so fallback is None - mock_fetcher._settings.pronote.pronote_url = None + mock_fetcher._settings.pronote.url = None with ( patch("pronote_sync.sources.pronote.fallback.fetch_ical") as m_fetch_ical, @@ -531,7 +531,7 @@ def test_no_secrets_in_error_messages( :rtype: None """ # Disable pronotepy so fallback is None to trigger PipelineCriticalError - mock_fetcher._settings.pronote.pronote_url = None + mock_fetcher._settings.pronote.url = None with ( patch("pronote_sync.sources.pronote.fallback.fetch_ical") as m_fetch_ical, @@ -629,7 +629,7 @@ def test_fetch_agenda_no_source_configured_raises(mock_fetcher: PronoteFetcher) """ # Disable both sources mock_fetcher._settings.pronote.ical_url = None - mock_fetcher._settings.pronote.pronote_url = None + mock_fetcher._settings.pronote.url = None with pytest.raises(PipelineCriticalError) as exc_info: mock_fetcher.fetch_agenda() @@ -1020,7 +1020,7 @@ def test_homework_sources_explicit_ical_mode_strict(mock_fetcher: PronoteFetcher assert fallback is None # Without pronotepy configured - mock_fetcher._settings.pronote.pronote_url = None + mock_fetcher._settings.pronote.url = None primary, fallback = mock_fetcher._homework_sources() assert primary == "ical" assert fallback is None @@ -1080,7 +1080,7 @@ def test_homework_sources_auto_no_source_configured_raises(mock_fetcher: Pronote """ mock_fetcher._settings.pronote.homework_source = "auto" mock_fetcher._settings.pronote.ical_url = None - mock_fetcher._settings.pronote.pronote_url = None + mock_fetcher._settings.pronote.url = None with pytest.raises(PipelineCriticalError) as exc_info: mock_fetcher._homework_sources() @@ -1298,7 +1298,7 @@ def test_is_pronotepy_configured_without_ent_returns_true() -> None: # Créer des settings avec pronotepy configuré mais sans ent settings = Settings( pronote=PronoteSettings( - pronote_url="https://pronote.example.com", + url="https://pronote.example.com", username="testuser", password=SecretStr("testpass"), ent=None, # Explicitement None @@ -1327,7 +1327,7 @@ def test_agenda_sources_auto_without_ical_and_without_ent_returns_pronotepy() -> settings = Settings( pronote=PronoteSettings( - pronote_url="https://pronote.example.com", + url="https://pronote.example.com", username="testuser", password=SecretStr("testpass"), ent=None, # Explicitement None @@ -1358,7 +1358,7 @@ def test_homework_sources_auto_without_ical_and_without_ent_returns_pronotepy() settings = Settings( pronote=PronoteSettings( - pronote_url="https://pronote.example.com", + url="https://pronote.example.com", username="testuser", password=SecretStr("testpass"), ent=None, # Explicitement None diff --git a/tests/unit/test_pronote_client.py b/tests/unit/test_pronote_client.py index ce1d41e..e6e96d9 100644 --- a/tests/unit/test_pronote_client.py +++ b/tests/unit/test_pronote_client.py @@ -46,7 +46,7 @@ def pronote_settings() -> PronoteSettings: :rtype: PronoteSettings """ return PronoteSettings( - pronote_url="https://pronote.example.com", + url="https://pronote.example.com", username="testuser", password=SecretStr("testpass"), ent="bordeaux", @@ -309,9 +309,7 @@ def test_missing_credentials_raises(empty_pronote_settings: PronoteSettings) -> """ client = PronoteClient(empty_pronote_settings) - with pytest.raises( - ValueError, match="pronote_url, username et password sont requis pour pronotepy" - ): + with pytest.raises(ValueError, match="url, username et password sont requis pour pronotepy"): client._connect() @@ -384,7 +382,7 @@ def test_connect_without_ent_but_with_required_credentials( # Settings sans ent mais avec les autres champs requis settings = PronoteSettings( - pronote_url="https://pronote.example.com", + url="https://pronote.example.com", username="testuser", password=SecretStr("testpass"), ent=None, # Explicitement None @@ -414,7 +412,7 @@ def test_connect_without_ent_but_with_required_credentials( def test_connect_missing_required_credentials_still_raises( mocker: pytest_mock.MockerFixture, ) -> None: - """Vérifie que _connect() lève ValueError si pronote_url, username ou password manquent. + """Vérifie que _connect() lève ValueError si url, username ou password manquent. Ce test valide que l'erreur ne mentionne plus ent comme requis. @@ -424,9 +422,9 @@ def test_connect_missing_required_credentials_still_raises( from pronote_sync.config.settings import PronoteSettings from pronote_sync.sources.pronote.client import PronoteClient - # Settings avec ent mais sans pronote_url + # Settings avec ent mais sans url settings = PronoteSettings( - pronote_url=None, + url=None, username="testuser", password=SecretStr("testpass"), ent=None, @@ -439,7 +437,7 @@ def test_connect_missing_required_credentials_still_raises( client._connect() # Error should NOT mention ent as required - assert "pronote_url, username et password sont requis" in str(exc_info.value) + assert "url, username et password sont requis" in str(exc_info.value) assert "ent" not in str(exc_info.value) @@ -453,7 +451,7 @@ def test_connect_missing_username_raises(mocker: pytest_mock.MockerFixture) -> N from pronote_sync.sources.pronote.client import PronoteClient settings = PronoteSettings( - pronote_url="https://pronote.example.com", + url="https://pronote.example.com", username=None, password=SecretStr("testpass"), ent=None, @@ -465,7 +463,7 @@ def test_connect_missing_username_raises(mocker: pytest_mock.MockerFixture) -> N with pytest.raises(ValueError) as exc_info: client._connect() - assert "pronote_url, username et password sont requis" in str(exc_info.value) + assert "url, username et password sont requis" in str(exc_info.value) def test_connect_missing_password_raises(mocker: pytest_mock.MockerFixture) -> None: @@ -478,7 +476,7 @@ def test_connect_missing_password_raises(mocker: pytest_mock.MockerFixture) -> N from pronote_sync.sources.pronote.client import PronoteClient settings = PronoteSettings( - pronote_url="https://pronote.example.com", + url="https://pronote.example.com", username="testuser", password=None, ent=None, @@ -490,7 +488,7 @@ def test_connect_missing_password_raises(mocker: pytest_mock.MockerFixture) -> N with pytest.raises(ValueError) as exc_info: client._connect() - assert "pronote_url, username et password sont requis" in str(exc_info.value) + assert "url, username et password sont requis" in str(exc_info.value) def test_connect_student_account_type( @@ -507,7 +505,7 @@ def test_connect_student_account_type( from pronote_sync.sources.pronote.client import PronoteClient pronote_settings_student = PronoteSettings( - pronote_url="https://pronote.example.com", + url="https://pronote.example.com", username="testuser", password=SecretStr("testpass"), ent="bordeaux", @@ -542,7 +540,7 @@ def test_connect_with_ent_resolution_still_works( from pronote_sync.sources.pronote.client import PronoteClient settings = PronoteSettings( - pronote_url="https://pronote.example.com", + url="https://pronote.example.com", username="testuser", password=SecretStr("testpass"), ent="bordeaux", # ent est fourni