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:
119
tests/unit/test_config.py
Normal file
119
tests/unit/test_config.py
Normal 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
|
||||
128
tests/unit/test_redaction.py
Normal file
128
tests/unit/test_redaction.py
Normal file
@@ -0,0 +1,128 @@
|
||||
"""Tests unitaires pour les utilitaires de masquage des secrets.
|
||||
|
||||
Ce module vérifie que les fonctions de masquage des URLs, des en-têtes
|
||||
et des secrets dans les textes fonctionnent correctement, sans fuite
|
||||
d'informations sensibles.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pronote_sync.utils.redaction import redact_exception, redact_secrets, redact_url
|
||||
|
||||
|
||||
def test_redact_url_with_sensitive_query_param() -> None:
|
||||
"""Vérifie que les paramètres sensibles dans une URL sont masqués.
|
||||
|
||||
:return: None
|
||||
"""
|
||||
url = "https://example.fr/ical?icalsecurise=TOKEN123" # pragma: allowlist secret
|
||||
redacted = redact_url(url)
|
||||
assert "TOKEN123" not in redacted
|
||||
assert "REDACTED" in redacted
|
||||
|
||||
|
||||
def test_redact_url_with_embedded_auth() -> None:
|
||||
"""Vérifie que les informations d'authentification intégrées dans une URL sont masquées.
|
||||
|
||||
:return: None
|
||||
"""
|
||||
url = "https://user:pass@host/path" # pragma: allowlist secret
|
||||
redacted = redact_url(url)
|
||||
assert "user" not in redacted
|
||||
assert "pass" not in redacted
|
||||
assert "REDACTED" in redacted
|
||||
|
||||
|
||||
def test_redact_url_with_uppercase_scheme() -> None:
|
||||
"""Vérifie que les URLs avec un schéma en majuscules sont traitées correctement.
|
||||
|
||||
:return: None
|
||||
"""
|
||||
url = "HTTPS://user:pass@host/path" # pragma: allowlist secret
|
||||
redacted = redact_url(url)
|
||||
assert "user" not in redacted
|
||||
assert "pass" not in redacted
|
||||
assert "REDACTED" in redacted
|
||||
|
||||
|
||||
def test_redact_secrets_authorization_bearer() -> None:
|
||||
"""Vérifie que les en-têtes Authorization: Bearer sont masqués.
|
||||
|
||||
:return: None
|
||||
"""
|
||||
text = "Authorization: Bearer mytoken123" # pragma: allowlist secret
|
||||
redacted = redact_secrets(text)
|
||||
assert "mytoken123" not in redacted
|
||||
assert "REDACTED" in redacted
|
||||
|
||||
|
||||
def test_redact_secrets_proxy_authorization_basic() -> None:
|
||||
"""Vérifie que les en-têtes Proxy-Authorization: Basic sont masqués.
|
||||
|
||||
:return: None
|
||||
"""
|
||||
text = "Proxy-Authorization: Basic dXNlcjpwYXNz" # pragma: allowlist secret
|
||||
redacted = redact_secrets(text)
|
||||
assert "dXNlcjpwYXNz" not in redacted
|
||||
assert "REDACTED" in redacted
|
||||
|
||||
|
||||
def test_redact_secrets_lowercase_authorization() -> None:
|
||||
"""Vérifie que les en-têtes authorization en minuscules sont masqués.
|
||||
|
||||
:return: None
|
||||
"""
|
||||
text = "authorization: Bearer tok" # pragma: allowlist secret
|
||||
redacted = redact_secrets(text)
|
||||
assert "tok" not in redacted
|
||||
assert "REDACTED" in redacted
|
||||
|
||||
|
||||
def test_redact_secrets_text_key_value() -> None:
|
||||
"""Vérifie que les affectations clé=valeur dans du texte sont masquées.
|
||||
|
||||
:return: None
|
||||
"""
|
||||
text = "password=mysecret" # pragma: allowlist secret
|
||||
redacted = redact_secrets(text)
|
||||
assert "mysecret" not in redacted
|
||||
assert "REDACTED" in redacted
|
||||
|
||||
|
||||
def test_redact_secrets_json_key_value() -> None:
|
||||
"""Vérifie que les secrets dans du JSON sont masqués.
|
||||
|
||||
:return: None
|
||||
"""
|
||||
text = '{"api_key": "myapikey"}' # pragma: allowlist secret
|
||||
redacted = redact_secrets(text)
|
||||
assert "myapikey" not in redacted
|
||||
assert "REDACTED" in redacted
|
||||
|
||||
|
||||
def test_redact_exception() -> None:
|
||||
"""Vérifie que les exceptions contenant des secrets sont masquées.
|
||||
|
||||
:return: None
|
||||
"""
|
||||
exc = Exception("Authorization: Bearer leaked_token") # pragma: allowlist secret
|
||||
redacted_msg = redact_exception(exc)
|
||||
assert "leaked_token" not in redacted_msg
|
||||
assert "REDACTED" in redacted_msg
|
||||
|
||||
|
||||
def test_redact_url_preserves_host_and_path() -> None:
|
||||
"""Vérifie que l'hôte et le chemin d'une URL sont préservés après masquage.
|
||||
|
||||
:return: None
|
||||
"""
|
||||
url = "https://user:pass@example.fr/path?icalsecurise=tok" # pragma: allowlist secret
|
||||
redacted = redact_url(url)
|
||||
assert "example.fr" in redacted
|
||||
assert "/path" in redacted
|
||||
assert "user" not in redacted
|
||||
assert "pass" not in redacted
|
||||
assert "tok" not in redacted
|
||||
|
||||
|
||||
# Ensure trailing newline
|
||||
Reference in New Issue
Block a user