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:
@@ -13,6 +13,7 @@ PRONOTE_MESSAGES_SOURCE=pronotepy
|
||||
CALDAV_URL=https://caldav.example.com/calendars/user/pronote/
|
||||
CALDAV_USERNAME=user@example.com
|
||||
CALDAV_PASSWORD=your_caldav_password
|
||||
CALDAV_CALENDAR_PATH=/pronote-sync/
|
||||
|
||||
# Fenêtre de synchronisation (jours)
|
||||
SYNC_PAST_DAYS=7
|
||||
@@ -34,9 +35,10 @@ XMPP_TIMEOUT=30
|
||||
|
||||
# --- IA (optionnelle) ---
|
||||
AI_ENABLED=true
|
||||
AI_PROVIDER=openai
|
||||
AI_BASE_URL=https://api.openai.com/v1
|
||||
AI_API_KEY=your_ai_api_key
|
||||
AI_MODEL=gpt-4o-mini
|
||||
# AI_MODEL=gpt-4o-mini # exemple recommandé, non activé par défaut
|
||||
|
||||
# --- Blog ---
|
||||
BLOG_ENABLED=false
|
||||
|
||||
@@ -26,7 +26,7 @@ repos:
|
||||
name: mypy
|
||||
entry: mypy
|
||||
language: python
|
||||
additional_dependencies: ["mypy>=1.10.0", "pydantic>=2.0.0", "pydantic-settings>=2.0.0"]
|
||||
additional_dependencies: ["mypy>=1.10.0", "pydantic>=2.0.0", "pydantic-settings>=2.0.0", "pytest>=8.0.0"]
|
||||
types: [python]
|
||||
pass_filenames: true
|
||||
|
||||
|
||||
@@ -141,7 +141,7 @@
|
||||
"hashed_secret": "90bd1b48e958257948487b90bee080ba5ed00caa",
|
||||
"is_secret": false,
|
||||
"is_verified": true,
|
||||
"line_number": 5112
|
||||
"line_number": 5117
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -267,6 +267,8 @@ Le projet utilise **`pydantic-settings`** pour valider et charger la configurati
|
||||
| `CALDAV_URL` | URL du serveur CalDAV. | `https://caldav.example.com/calendars/...` | `str` |
|
||||
| `CALDAV_USERNAME` | Identifiant CalDAV. | `user@example.com` | `str` |
|
||||
| `CALDAV_PASSWORD` | Mot de passe CalDAV. | `SecretStr` (masqué) | `SecretStr` |
|
||||
| `CALDAV_CALENDAR_PATH` | Chemin du calendrier CalDAV de destination. | `/pronote-sync/` | `str` |
|
||||
| `CALDAV_CALENDAR_PATH` | Chemin du calendrier CalDAV de destination. | `/pronote-sync/` | `str` |
|
||||
| `XMPP_JID` | Identifiant XMPP (ex: `user@example.com`). | `user@example.com` | `str` |
|
||||
| `XMPP_PASSWORD` | Mot de passe XMPP. | `SecretStr` (masqué) | `SecretStr` |
|
||||
| `XMPP_RECIPIENT` | Destinataire XMPP (ex: `parent@example.com`). | `parent@example.com` | `str` |
|
||||
@@ -291,9 +293,10 @@ Le projet utilise **`pydantic-settings`** pour valider et charger la configurati
|
||||
> Leur placement dans `AppSettings` (sans préfixe) garantit un mappage correct avec `SYNC_PAST_DAYS` / `SYNC_FUTURE_DAYS`.
|
||||
| `THEORETICAL_AGENDA_PATH` | Chemin vers le fichier iCal/CSV de l'agenda théorique. | `None` | `str \| None`|
|
||||
| `AI_ENABLED` | Activer la synthèse IA. | `False` | `bool` |
|
||||
| `AI_PROVIDER` | Fournisseur IA (`openai` ou `litellm`). | `openai` | `str` |
|
||||
| `AI_BASE_URL` | URL de base pour l'API IA (ex: OpenAI compatible). | `None` | `str \| None`|
|
||||
| `AI_API_KEY` | Clé API pour l'API IA. | `None` | `SecretStr` |
|
||||
| `AI_MODEL` | Modèle IA à utiliser. | `gpt-4o-mini` | `str` |
|
||||
| `AI_MODEL` | Modèle IA à utiliser (exemple recommandé : `gpt-4o-mini`). | `None` | `str \| None`|
|
||||
| `DRY_RUN` | Mode dry-run (pas de modifications CalDAV/XMPP). | `False` | `bool` |
|
||||
| `LOG_LEVEL` | Niveau de log (`DEBUG`, `INFO`, `WARNING`, `ERROR`). | `INFO` | `str` |
|
||||
|
||||
@@ -316,6 +319,7 @@ PRONOTE_MESSAGES_SOURCE=pronotepy
|
||||
CALDAV_URL=https://caldav.example.com/calendars/user/pronote/
|
||||
CALDAV_USERNAME=user@example.com
|
||||
CALDAV_PASSWORD=your_caldav_password
|
||||
CALDAV_CALENDAR_PATH=/pronote-sync/
|
||||
|
||||
# Fenêtre de synchronisation (jours)
|
||||
SYNC_PAST_DAYS=7
|
||||
@@ -331,9 +335,10 @@ XMPP_RECIPIENT=parent@example.com
|
||||
|
||||
# --- IA (optionnelle) ---
|
||||
AI_ENABLED=true
|
||||
AI_PROVIDER=openai
|
||||
AI_BASE_URL=https://api.openai.com/v1
|
||||
AI_API_KEY=your_ai_api_key
|
||||
AI_MODEL=gpt-4o-mini
|
||||
# AI_MODEL=gpt-4o-mini # exemple recommandé, non activé par défaut
|
||||
|
||||
# --- Divers ---
|
||||
DRY_RUN=false
|
||||
@@ -1142,7 +1147,7 @@ Ajouter les variables suivantes dans la configuration :
|
||||
|
||||
| **Variable** | **Description** | **Valeur par défaut** | **Type** |
|
||||
|----------------------------|-------------------------------------------------------------------------------|-----------------------|-------------------|
|
||||
| `BLOG_RSS_ENABLED` | Activer la récupération du blog. | `False` | `bool` |
|
||||
| `BLOG_ENABLED` | Activer la récupération du blog. | `False` | `bool` |
|
||||
| `BLOG_RSS_URL` | URL du flux RSS du blog. | `https://blogpeda.ac-bordeaux.fr/cjeliote/?feed=rss2` | `str` |
|
||||
|
||||
#### 5 bis.9.2 Modèle Pydantic pour la configuration du blog
|
||||
@@ -1177,7 +1182,7 @@ class Settings(BaseSettings):
|
||||
|
||||
```ini
|
||||
# --- Blog du collège ---
|
||||
BLOG_RSS_ENABLED=true
|
||||
BLOG_ENABLED=true
|
||||
BLOG_RSS_URL=https://blogpeda.ac-bordeaux.fr/cjeliote/?feed=rss2
|
||||
```
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ from __future__ import annotations
|
||||
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import SecretStr
|
||||
from pydantic import Field, SecretStr, field_serializer
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ class PronoteSettings(BaseSettings):
|
||||
|
||||
model_config = SettingsConfigDict(env_file=".env", extra="ignore", env_prefix="PRONOTE_")
|
||||
|
||||
ical_url: str | None = None
|
||||
ical_url: SecretStr | None = None
|
||||
username: str | None = None
|
||||
password: SecretStr | None = None
|
||||
ent: str | None = None
|
||||
@@ -31,6 +31,18 @@ class PronoteSettings(BaseSettings):
|
||||
homework_source: Literal["auto", "ical", "pronotepy"] = "auto"
|
||||
messages_source: Literal["pronotepy"] = "pronotepy"
|
||||
|
||||
@field_serializer("ical_url")
|
||||
def _serialize_ical_url(self, value: SecretStr | None) -> str | None:
|
||||
"""Masque l'URL iCal lors de la sérialisation (repr, str, JSON).
|
||||
|
||||
:param value: Valeur du champ ``ical_url``.
|
||||
:return: ``"**********"`` si la valeur est définie, ``None`` sinon.
|
||||
:rtype: str | None
|
||||
"""
|
||||
if value is None:
|
||||
return None
|
||||
return "**********"
|
||||
|
||||
|
||||
class CalDAVSettings(BaseSettings):
|
||||
"""Paramètres d'accès au serveur CalDAV de destination.
|
||||
@@ -121,12 +133,9 @@ class Settings(BaseSettings):
|
||||
|
||||
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
|
||||
|
||||
pronote: PronoteSettings = PronoteSettings()
|
||||
caldav: CalDAVSettings = CalDAVSettings()
|
||||
xmpp: XmppSettings = XmppSettings()
|
||||
ai: AISettings = AISettings()
|
||||
blog: BlogSettings = BlogSettings()
|
||||
app: AppSettings = AppSettings()
|
||||
|
||||
|
||||
settings = Settings()
|
||||
pronote: PronoteSettings = Field(default_factory=PronoteSettings)
|
||||
caldav: CalDAVSettings = Field(default_factory=CalDAVSettings)
|
||||
xmpp: XmppSettings = Field(default_factory=XmppSettings)
|
||||
ai: AISettings = Field(default_factory=AISettings)
|
||||
blog: BlogSettings = Field(default_factory=BlogSettings)
|
||||
app: AppSettings = Field(default_factory=AppSettings)
|
||||
|
||||
@@ -24,7 +24,11 @@ _SENSITIVE_QUERY_KEYS = frozenset(
|
||||
"authorization",
|
||||
}
|
||||
)
|
||||
_URL_PATTERN = re.compile(r"https?://[^\s]+")
|
||||
_URL_PATTERN = re.compile(r"https?://[^\s]+", re.IGNORECASE)
|
||||
_AUTH_HEADER_PATTERN = re.compile(
|
||||
r"((?:Proxy-)?Authorization)\s*[:=]\s*\S[^\r\n]*",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
_ISOLATED_SECRET_PATTERN = re.compile(
|
||||
r"\b(icalsecurise|access_token|api_key|apikey|authorization|token|password|secret|key|auth)"
|
||||
r"(\s*['\"]?\s*[:=]\s*)"
|
||||
@@ -40,8 +44,8 @@ def redact_url(url: str) -> str:
|
||||
"""Masque les identifiants et les paramètres sensibles d'une URL.
|
||||
|
||||
Les informations d'authentification du netloc (``utilisateur:motdepasse@hôte``)
|
||||
sont masquées, ainsi que les paramètres sensibles de la requête
|
||||
(ex: ``icalsecurise``).
|
||||
sont entièrement masquées (utilisateur et mot de passe), ainsi que les
|
||||
paramètres sensibles de la requête (ex: ``icalsecurise``).
|
||||
|
||||
:param url: URL pouvant contenir des informations sensibles (ex: ``icalsecurise``).
|
||||
:return: URL avec les éléments sensibles remplacés par ``REDACTED``,
|
||||
@@ -51,15 +55,13 @@ def redact_url(url: str) -> str:
|
||||
try:
|
||||
parts = urlsplit(url)
|
||||
if parts.username is not None or parts.password is not None:
|
||||
# Netloc sûr : utilisateur:REDACTED@hôte:port. Le deux-point est
|
||||
# conservé même en l'absence de mot de passe explicite.
|
||||
userinfo = parts.username or ""
|
||||
userinfo += ":REDACTED"
|
||||
# Netloc sûr : REDACTED@hôte:port. L'utilisateur et le mot de
|
||||
# passe sont entièrement masqués.
|
||||
host = parts.hostname or ""
|
||||
if parts.port is not None:
|
||||
netloc = f"{userinfo}@{host}:{parts.port}"
|
||||
netloc = f"{_REDACTED}@{host}:{parts.port}"
|
||||
else:
|
||||
netloc = f"{userinfo}@{host}"
|
||||
netloc = f"{_REDACTED}@{host}"
|
||||
parts = parts._replace(netloc=netloc)
|
||||
query: list[tuple[str, str]] = parse_qsl(parts.query, keep_blank_values=True)
|
||||
redacted_query = [
|
||||
@@ -74,15 +76,18 @@ def redact_url(url: str) -> str:
|
||||
def redact_secrets(text: str) -> str:
|
||||
"""Masque les secrets présents dans un texte arbitraire.
|
||||
|
||||
Les URLs sont d'abord traitées par :func:`redact_url`, puis les affectations
|
||||
isolées de type ``cle=valeur`` ou ``cle:valeur`` (ex: ``icalsecurise=XXX``,
|
||||
``"token": "XXX"``) sont masquées, sans distinction de casse.
|
||||
Les URLs sont d'abord traitées par :func:`redact_url`, puis les en-têtes
|
||||
d'authentification (``Authorization``, ``Proxy-Authorization``) et les
|
||||
affectations isolées de type ``cle=valeur`` ou ``cle:valeur``
|
||||
(ex: ``icalsecurise=XXX``, ``"token": "XXX"``) sont masquées, sans
|
||||
distinction de casse.
|
||||
|
||||
:param text: Texte pouvant contenir des URLs ou des secrets en clair.
|
||||
:return: Texte avec les secrets remplacés par ``REDACTED``.
|
||||
:rtype: str
|
||||
"""
|
||||
redacted = _URL_PATTERN.sub(lambda match: redact_url(match.group(0)), text)
|
||||
redacted = _AUTH_HEADER_PATTERN.sub(r"\1: REDACTED", redacted)
|
||||
return _ISOLATED_SECRET_PATTERN.sub(r"\1\2\3REDACTED", redacted)
|
||||
|
||||
|
||||
|
||||
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