"""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, ) 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 def test_auth_mode_default_password() -> None: """Vérifie que ``auth_mode`` vaut ``"password"`` par défaut. :return: None """ settings = PronoteSettings() assert settings.auth_mode == "password" def test_auth_mode_env_qr_token(monkeypatch: MonkeyPatch) -> None: """Vérifie que ``PRONOTE_AUTH_MODE=qr_token`` est chargé correctement. :param monkeypatch: Fixture pytest pour modifier temporairement l'environnement. :return: None """ monkeypatch.setenv("PRONOTE_AUTH_MODE", "qr_token") settings = load_settings() assert settings.pronote.auth_mode == "qr_token" def test_qr_pin_loaded_as_secretstr_and_masked(monkeypatch: MonkeyPatch) -> None: """Vérifie que ``PRONOTE_QR_PIN`` est chargé en ``SecretStr`` et masqué. Le PIN ne doit apparaître nulle part dans les représentations textuelles (str, repr, JSON) : seul le masque ``**********`` est visible. :param monkeypatch: Fixture pytest pour modifier temporairement l'environnement. :return: None """ monkeypatch.setenv("PRONOTE_QR_PIN", "123456") settings = load_settings() assert isinstance(settings.pronote.qr_pin, SecretStr) assert settings.pronote.qr_pin.get_secret_value() == "123456" str_repr = str(settings) assert "123456" not in str_repr assert "**********" in str_repr repr_repr = repr(settings) assert "123456" not in repr_repr assert "**********" in repr_repr json_str = settings.model_dump_json() assert "123456" not in json_str assert "**********" in json_str def test_qr_code_file_loaded_as_plain_string(monkeypatch: MonkeyPatch) -> None: """Vérifie que ``PRONOTE_QR_CODE_FILE`` est chargé comme chaîne simple. :param monkeypatch: Fixture pytest pour modifier temporairement l'environnement. :return: None """ monkeypatch.setenv("PRONOTE_QR_CODE_FILE", "/data/qr_code.png") settings = load_settings() assert isinstance(settings.pronote.qr_code_file, str) assert settings.pronote.qr_code_file == "/data/qr_code.png" def test_qr_pin_in_redaction_secrets(monkeypatch: MonkeyPatch) -> None: """Vérifie que le PIN QR est collecté pour la rédaction des secrets. Le ``SecretStr`` du PIN doit figurer dans ``redaction_secrets()`` et sa représentation textuelle doit rester masquée. :param monkeypatch: Fixture pytest pour modifier temporairement l'environnement. :return: None """ monkeypatch.setenv("PRONOTE_QR_PIN", "654321") settings = load_settings() secrets = settings.redaction_secrets() assert settings.pronote.qr_pin in secrets assert "654321" not in repr(settings.pronote.qr_pin) assert "**********" in repr(settings.pronote.qr_pin) # Ensure trailing newline