Files
college-infos/tests/unit/test_config.py
T
OpenCode 8c6a0e3f29 fix(config): refuser les fenêtres de synchronisation négatives
Contraint SYNC_PAST_DAYS et SYNC_FUTURE_DAYS à ge=0 et documente l'effet réel de 0 jour (le jour courant reste inclus).

Refs #14
2026-09-12 19:18:27 +02:00

328 lines
11 KiB
Python

"""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, ValidationError
from pronote_sync.config.env import load_settings
from pronote_sync.config.settings import AppSettings, 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)
def test_sync_past_days_negative_direct_instantiation() -> None:
"""Vérifie que ``sync_past_days`` négatif lève ``ValidationError`` à l'instanciation.
:return: None
"""
with pytest.raises(ValidationError):
AppSettings(sync_past_days=-1)
def test_sync_future_days_negative_direct_instantiation() -> None:
"""Vérifie que ``sync_future_days`` négatif lève ``ValidationError`` à l'instanciation.
:return: None
"""
with pytest.raises(ValidationError):
AppSettings(sync_future_days=-1)
def test_sync_past_days_zero_accepted() -> None:
"""Vérifie que ``sync_past_days=0`` est accepté.
:return: None
"""
settings = AppSettings(sync_past_days=0)
assert settings.sync_past_days == 0
def test_sync_future_days_zero_accepted() -> None:
"""Vérifie que ``sync_future_days=0`` est accepté.
:return: None
"""
settings = AppSettings(sync_future_days=0)
assert settings.sync_future_days == 0
def test_sync_past_days_positive_accepted() -> None:
"""Vérifie que ``sync_past_days`` positif est accepté.
:return: None
"""
settings = AppSettings(sync_past_days=7)
assert settings.sync_past_days == 7
def test_sync_future_days_positive_accepted() -> None:
"""Vérifie que ``sync_future_days`` positif est accepté.
:return: None
"""
settings = AppSettings(sync_future_days=30)
assert settings.sync_future_days == 30
def test_sync_past_days_negative_env_loading(monkeypatch: MonkeyPatch) -> None:
"""Vérifie que ``SYNC_PAST_DAYS=-1`` lève ``ValidationError`` via chargement env.
:param monkeypatch: Fixture pytest pour modifier temporairement l'environnement.
:return: None
"""
monkeypatch.setenv("SYNC_PAST_DAYS", "-1")
with pytest.raises(ValidationError):
load_settings()
def test_sync_future_days_negative_env_loading(monkeypatch: MonkeyPatch) -> None:
"""Vérifie que ``SYNC_FUTURE_DAYS=-1`` lève ``ValidationError`` via chargement env.
:param monkeypatch: Fixture pytest pour modifier temporairement l'environnement.
:return: None
"""
monkeypatch.setenv("SYNC_FUTURE_DAYS", "-1")
with pytest.raises(ValidationError):
load_settings()
def test_sync_past_days_zero_env_loading(monkeypatch: MonkeyPatch) -> None:
"""Vérifie que ``SYNC_PAST_DAYS=0`` est accepté via chargement env.
:param monkeypatch: Fixture pytest pour modifier temporairement l'environnement.
:return: None
"""
monkeypatch.setenv("SYNC_PAST_DAYS", "0")
settings = load_settings()
assert settings.app.sync_past_days == 0
def test_sync_future_days_zero_env_loading(monkeypatch: MonkeyPatch) -> None:
"""Vérifie que ``SYNC_FUTURE_DAYS=0`` est accepté via chargement env.
:param monkeypatch: Fixture pytest pour modifier temporairement l'environnement.
:return: None
"""
monkeypatch.setenv("SYNC_FUTURE_DAYS", "0")
settings = load_settings()
assert settings.app.sync_future_days == 0
def test_sync_past_days_positive_env_loading(monkeypatch: MonkeyPatch) -> None:
"""Vérifie que ``SYNC_PAST_DAYS`` positif est accepté via chargement env.
:param monkeypatch: Fixture pytest pour modifier temporairement l'environnement.
:return: None
"""
monkeypatch.setenv("SYNC_PAST_DAYS", "7")
settings = load_settings()
assert settings.app.sync_past_days == 7
def test_sync_future_days_positive_env_loading(monkeypatch: MonkeyPatch) -> None:
"""Vérifie que ``SYNC_FUTURE_DAYS`` positif est accepté via chargement env.
:param monkeypatch: Fixture pytest pour modifier temporairement l'environnement.
:return: None
"""
monkeypatch.setenv("SYNC_FUTURE_DAYS", "30")
settings = load_settings()
assert settings.app.sync_future_days == 30