fix(M2): configuration, secrets et rédaction — correction des écarts FIXME_M2

Sécurité :
- ical_url : str → SecretStr | None (masquage dans str/repr/model_dump_json)
- En-têtes Authorization/Proxy-Authorization : masquage complet de la valeur
- _URL_PATTERN : insensible à la casse (HTTPS:// reconnu)
- redact_url() : masquage du nom d'utilisateur (userinfo complet)

Configuration :
- Sous-configs : Field(default_factory=...) pour rechargement à chaque appel
- Suppression du singleton settings (injection de dépendances)
- .env.example : ajout AI_PROVIDER et CALDAV_CALENDAR_PATH
- Guide : BLOG_RSS_ENABLED → BLOG_ENABLED, AI_MODEL=None par défaut
- .secrets.baseline : ligne décalée 5112 → 5117 (faux positif audité)
- pre-commit : ajout de pytest aux additional_dependencies du hook mypy

Tests : 15 tests (config + redaction) couvrant rechargement, masquage
SecretStr, en-têtes Authorization, URL auth intégrée et casse variable.

Co-authored-by: opencode/coder <coder@agents.invalid>
Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
This commit is contained in:
2026-09-06 12:35:55 +02:00
parent a0ac75cec6
commit 2f6184df0b
8 changed files with 298 additions and 30 deletions

119
tests/unit/test_config.py Normal file
View File

@@ -0,0 +1,119 @@
"""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