refactor(config): migrer les endpoints Pronote
Co-authored-by: Codex <codex@antoineve.me>
This commit is contained in:
+153
-3
@@ -13,7 +13,14 @@ import pytest
|
||||
from pydantic import SecretStr, ValidationError
|
||||
|
||||
from pronote_sync.config.env import load_settings
|
||||
from pronote_sync.config.settings import AppSettings, PronoteSettings, Settings
|
||||
from pronote_sync.config.settings import (
|
||||
AppSettings,
|
||||
BlogSettings,
|
||||
CalDAVSettings,
|
||||
ExternalEndpoint,
|
||||
PronoteSettings,
|
||||
Settings,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from _pytest.monkeypatch import MonkeyPatch
|
||||
@@ -73,7 +80,7 @@ def test_secretstr_masking_ical_url() -> None:
|
||||
# Vérification de la sérialisation JSON
|
||||
json_str = settings.model_dump_json()
|
||||
assert "SECRET_TOKEN" not in json_str
|
||||
assert "**********" in json_str
|
||||
assert "REDACTED" in json_str
|
||||
|
||||
|
||||
def test_secretstr_masking_password() -> None:
|
||||
@@ -128,7 +135,150 @@ def test_url_from_pronote_url_env_var(monkeypatch: MonkeyPatch) -> 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
|
||||
assert settings.pronote.endpoint is not None
|
||||
assert settings.pronote.endpoint.url.get_secret_value() == test_url
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"url",
|
||||
[
|
||||
"https://endpoint.example.test/api",
|
||||
"http://localhost:8080/test",
|
||||
"file:///tmp/fixture.ics",
|
||||
],
|
||||
)
|
||||
def test_external_endpoint_accepts_supported_schemes(url: str) -> None:
|
||||
"""Vérifie le socle commun des URL d'endpoints autorisés.
|
||||
|
||||
:param url: URL représentative du schéma à valider.
|
||||
:return: None
|
||||
"""
|
||||
endpoint = ExternalEndpoint(url=SecretStr(url))
|
||||
assert endpoint.url.get_secret_value() == url
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"url",
|
||||
["ftp://endpoint.example.test", "https:///missing-host", "https://host:bad", "file://"],
|
||||
)
|
||||
def test_external_endpoint_rejects_invalid_urls_without_leak(url: str) -> None:
|
||||
"""Vérifie que le socle rejette les URL invalides sans les afficher.
|
||||
|
||||
:param url: URL invalide à refuser.
|
||||
:return: None
|
||||
"""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
ExternalEndpoint(url=SecretStr(url))
|
||||
assert url not in str(exc_info.value)
|
||||
|
||||
|
||||
def test_external_endpoint_is_immutable_and_redacted() -> None:
|
||||
"""Vérifie le contrat immuable et expurgé du value object.
|
||||
|
||||
:return: None
|
||||
"""
|
||||
endpoint = ExternalEndpoint(url=SecretStr("https://user:secret@example.test/calendar"))
|
||||
with pytest.raises(ValidationError):
|
||||
endpoint.url = SecretStr("https://other.example.test")
|
||||
assert "secret" not in endpoint.model_dump_json()
|
||||
assert "REDACTED" in endpoint.model_dump_json()
|
||||
|
||||
|
||||
def test_caldav_endpoint_loads_from_nested_environment(monkeypatch: MonkeyPatch) -> None:
|
||||
"""Vérifie le chargement du nouvel endpoint CalDAV depuis l'environnement.
|
||||
|
||||
:param monkeypatch: Fixture pytest pour modifier temporairement l'environnement.
|
||||
:return: None
|
||||
"""
|
||||
monkeypatch.setenv("CALDAV_ENDPOINT__URL", "https://caldav.example.test/dav")
|
||||
settings = load_settings()
|
||||
assert settings.caldav.endpoint is not None
|
||||
assert settings.caldav.endpoint.url.get_secret_value() == "https://caldav.example.test/dav"
|
||||
|
||||
|
||||
def test_caldav_legacy_url_migrates_with_warning() -> None:
|
||||
"""Vérifie la migration temporaire du réglage CalDAV historique.
|
||||
|
||||
:return: None
|
||||
"""
|
||||
with pytest.warns(DeprecationWarning, match="CALDAV_URL"):
|
||||
settings = CalDAVSettings(url=SecretStr("https://caldav.example.test/dav"))
|
||||
assert settings.endpoint is not None
|
||||
assert settings.endpoint.url.get_secret_value() == "https://caldav.example.test/dav"
|
||||
|
||||
|
||||
def test_pronote_endpoints_load_from_nested_environment(monkeypatch: MonkeyPatch) -> None:
|
||||
"""Vérifie le chargement des deux endpoints Pronote depuis l'environnement.
|
||||
|
||||
:param monkeypatch: Fixture pytest pour modifier temporairement l'environnement.
|
||||
:return: None
|
||||
"""
|
||||
monkeypatch.setenv("PRONOTE_ENDPOINT__URL", "https://pronote.example.test/parent.html")
|
||||
monkeypatch.setenv("PRONOTE_ICAL_ENDPOINT__URL", "file:///tmp/pronote.ics")
|
||||
settings = load_settings()
|
||||
assert settings.pronote.endpoint is not None
|
||||
assert settings.pronote.ical_endpoint is not None
|
||||
assert (
|
||||
settings.pronote.endpoint.url.get_secret_value()
|
||||
== "https://pronote.example.test/parent.html"
|
||||
)
|
||||
assert settings.pronote.ical_endpoint.url.get_secret_value() == "file:///tmp/pronote.ics"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("field", "url", "message"),
|
||||
[
|
||||
("endpoint", "http://pronote.example.test", "HTTPS requis"),
|
||||
("ical_endpoint", "http://pronote.example.test/calendar", "HTTPS ou file requis"),
|
||||
],
|
||||
)
|
||||
def test_pronote_endpoint_policy_rejects_insecure_url(field: str, url: str, message: str) -> None:
|
||||
"""Vérifie la politique de transport des endpoints Pronote.
|
||||
|
||||
:param field: Nom du champ endpoint à alimenter.
|
||||
:param url: URL non sûre à refuser.
|
||||
:param message: Fragment attendu du message sûr.
|
||||
:return: None
|
||||
"""
|
||||
endpoint = ExternalEndpoint(url=SecretStr(url))
|
||||
with pytest.raises(ValidationError, match=message) as exc_info:
|
||||
if field == "endpoint":
|
||||
PronoteSettings(endpoint=endpoint)
|
||||
else:
|
||||
PronoteSettings(ical_endpoint=endpoint)
|
||||
assert url not in str(exc_info.value)
|
||||
|
||||
|
||||
def test_blog_endpoint_loads_from_nested_environment(monkeypatch: MonkeyPatch) -> None:
|
||||
"""Vérifie le chargement du nouvel endpoint RSS depuis l'environnement.
|
||||
|
||||
:param monkeypatch: Fixture pytest pour modifier temporairement l'environnement.
|
||||
:return: None
|
||||
"""
|
||||
monkeypatch.setenv("BLOG_ENDPOINT__URL", "https://blog.example.test/feed")
|
||||
settings = load_settings()
|
||||
assert settings.blog.endpoint.url.get_secret_value() == "https://blog.example.test/feed"
|
||||
|
||||
|
||||
def test_blog_legacy_rss_url_migrates_with_warning() -> None:
|
||||
"""Vérifie la migration temporaire du réglage RSS historique.
|
||||
|
||||
:return: None
|
||||
"""
|
||||
with pytest.warns(DeprecationWarning, match="BLOG_RSS_URL"):
|
||||
settings = BlogSettings(rss_url="https://blog.example.test/feed")
|
||||
assert settings.endpoint.url.get_secret_value() == "https://blog.example.test/feed"
|
||||
|
||||
|
||||
def test_blog_endpoint_rejects_insecure_url_without_leak() -> None:
|
||||
"""Vérifie que l'URL RSS HTTP est refusée sans être exposée.
|
||||
|
||||
:return: None
|
||||
"""
|
||||
url = "http://user:secret@blog.example.test/feed" # pragma: allowlist secret
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
BlogSettings(endpoint=ExternalEndpoint(url=SecretStr(url)))
|
||||
assert url not in str(exc_info.value)
|
||||
|
||||
|
||||
def test_auth_mode_default_password() -> None:
|
||||
|
||||
Reference in New Issue
Block a user