"""Tests unitaires pour la configuration et le chargement des secrets. Ce module vérifie que la configuration est chargée correctement depuis les variables d'environnement, que les secrets sont masqués dans les représentations textuelles et sérialisées, et que le rechargement fonctionne comme attendu. """ from __future__ import annotations from typing import TYPE_CHECKING import pytest from pydantic import SecretStr from pronote_sync.config.env import load_settings from pronote_sync.config.settings import PronoteSettings, Settings if TYPE_CHECKING: from _pytest.monkeypatch import MonkeyPatch def test_env_prefix_loading(monkeypatch: MonkeyPatch) -> None: """Vérifie que les variables d'environnement avec préfixe sont chargées correctement. :param monkeypatch: Fixture pytest pour modifier temporairement l'environnement. :return: None """ monkeypatch.setenv("PRONOTE_USERNAME", "testuser") settings = load_settings() assert settings.pronote.username == "testuser" def test_env_reload_via_load_settings(monkeypatch: MonkeyPatch) -> None: """Vérifie que le rechargement de la configuration lit les nouvelles valeurs. Ce test garantit que les champs configurés avec ``default_factory`` créent des instances fraîches à chaque appel de ``load_settings()``, permettant ainsi un rechargement dynamique des variables d'environnement. :param monkeypatch: Fixture pytest pour modifier temporairement l'environnement. :return: None """ # Premier chargement (valeur par défaut None) settings1 = load_settings() assert settings1.pronote.username is None # Modification de l'environnement monkeypatch.setenv("PRONOTE_USERNAME", "newuser") # Rechargement settings2 = load_settings() assert settings2.pronote.username == "newuser" def test_secretstr_masking_ical_url() -> None: """Vérifie que l'URL iCal contenant un secret est masquée dans les représentations. :return: None """ ical_url = "https://example.fr/ical?icalsecurise=SECRET_TOKEN" # pragma: allowlist secret settings = Settings(pronote=PronoteSettings(ical_url=SecretStr(ical_url))) # Vérification de la représentation string str_repr = str(settings) assert "SECRET_TOKEN" not in str_repr assert "**********" in str_repr # Vérification de la représentation repr repr_repr = repr(settings) assert "SECRET_TOKEN" not in repr_repr assert "**********" in repr_repr # Vérification de la sérialisation JSON json_str = settings.model_dump_json() assert "SECRET_TOKEN" not in json_str assert "**********" in json_str def test_secretstr_masking_password() -> None: """Vérifie que le mot de passe est masqué dans les représentations. :return: None """ password = "mypass123" # pragma: allowlist secret settings = Settings(pronote=PronoteSettings(password=SecretStr(password))) # Vérification de la représentation string str_repr = str(settings) assert "mypass123" not in str_repr assert "**********" in str_repr # Vérification de la représentation repr repr_repr = repr(settings) assert "mypass123" not in repr_repr assert "**********" in repr_repr # Vérification de la sérialisation JSON json_str = settings.model_dump_json() assert "mypass123" not in json_str assert "**********" in json_str def test_no_singleton_import() -> None: """Vérifie que l'import du singleton settings lève une ImportError. Ce test garantit que le pattern singleton a été supprimé et que la configuration doit être chargée explicitement via ``load_settings()``. :return: None """ with pytest.raises(ImportError): # Attempt to import a non-existent attribute to trigger ImportError # as required by the spec from pronote_sync.config.settings import ( # type: ignore[attr-defined] # noqa: F401 settings, ) # Ensure trailing newline