Compare commits
3 Commits
main
...
fix/dry-ru
| Author | SHA1 | Date | |
|---|---|---|---|
| 384a730e73 | |||
| f4f71461ee | |||
| 5e69c4e049 |
@@ -126,6 +126,9 @@ class PipelineRunner:
|
|||||||
:rtype: PipelineRunner
|
:rtype: PipelineRunner
|
||||||
"""
|
"""
|
||||||
effective_dry_run = settings.app.dry_run if dry_run is None else dry_run
|
effective_dry_run = settings.app.dry_run if dry_run is None else dry_run
|
||||||
|
if effective_dry_run and settings.pronote.auth_mode == "qr_token":
|
||||||
|
raise ValueError("Le mode qr_token n'est pas compatible avec le dry-run.")
|
||||||
|
persistence_enabled = not effective_dry_run
|
||||||
theoretical_provider = get_theoretical_provider(
|
theoretical_provider = get_theoretical_provider(
|
||||||
settings.app.theoretical_agenda_path,
|
settings.app.theoretical_agenda_path,
|
||||||
settings.app.school_holidays_path,
|
settings.app.school_holidays_path,
|
||||||
@@ -136,7 +139,9 @@ class PipelineRunner:
|
|||||||
AgendaComparator(theoretical_provider) if theoretical_provider is not None else None
|
AgendaComparator(theoretical_provider) if theoretical_provider is not None else None
|
||||||
)
|
)
|
||||||
blog_client = BlogRSSClient(settings.blog.rss_url) if settings.blog.enabled else None
|
blog_client = BlogRSSClient(settings.blog.rss_url) if settings.blog.enabled else None
|
||||||
blog_state = BlogRSSState() if settings.blog.enabled else None
|
blog_state = (
|
||||||
|
BlogRSSState(persistence_enabled=persistence_enabled) if settings.blog.enabled else None
|
||||||
|
)
|
||||||
return cls(
|
return cls(
|
||||||
settings=settings,
|
settings=settings,
|
||||||
pronote_fetcher=PronoteFetcher(
|
pronote_fetcher=PronoteFetcher(
|
||||||
@@ -144,7 +149,9 @@ class PipelineRunner:
|
|||||||
PronoteClient(
|
PronoteClient(
|
||||||
settings.pronote,
|
settings.pronote,
|
||||||
auth_state=(
|
auth_state=(
|
||||||
PronoteAuthState() if settings.pronote.auth_mode == "qr_token" else None
|
PronoteAuthState(persistence_enabled=persistence_enabled)
|
||||||
|
if settings.pronote.auth_mode == "qr_token"
|
||||||
|
else None
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -37,15 +37,24 @@ class BlogRSSState:
|
|||||||
|
|
||||||
:param state_file: Chemin du fichier d'état JSON (``str`` ou
|
:param state_file: Chemin du fichier d'état JSON (``str`` ou
|
||||||
:class:`~pathlib.Path`). ``".blog_rss_state.json"`` par défaut.
|
:class:`~pathlib.Path`). ``".blog_rss_state.json"`` par défaut.
|
||||||
|
:param persistence_enabled: Si ``False``, charge l'état existant mais ne
|
||||||
|
modifie jamais le fichier d'état. ``True`` par défaut.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, state_file: Path | str = ".blog_rss_state.json") -> None:
|
def __init__(
|
||||||
|
self,
|
||||||
|
state_file: Path | str = ".blog_rss_state.json",
|
||||||
|
persistence_enabled: bool = True,
|
||||||
|
) -> None:
|
||||||
"""Initialise le gestionnaire d'état depuis le fichier JSON.
|
"""Initialise le gestionnaire d'état depuis le fichier JSON.
|
||||||
|
|
||||||
:param state_file: Chemin du fichier d'état JSON (``str`` ou
|
:param state_file: Chemin du fichier d'état JSON (``str`` ou
|
||||||
:class:`~pathlib.Path`). ``".blog_rss_state.json"`` par défaut.
|
:class:`~pathlib.Path`). ``".blog_rss_state.json"`` par défaut.
|
||||||
|
:param persistence_enabled: Si ``False``, charge l'état existant mais
|
||||||
|
désactive toutes les écritures sur disque. ``True`` par défaut.
|
||||||
"""
|
"""
|
||||||
self._state_file = Path(state_file)
|
self._state_file = Path(state_file)
|
||||||
|
self._persistence_enabled = persistence_enabled
|
||||||
self._known_guids: set[str] = set()
|
self._known_guids: set[str] = set()
|
||||||
self._etag: str | None = None
|
self._etag: str | None = None
|
||||||
self._last_modified: str | None = None
|
self._last_modified: str | None = None
|
||||||
@@ -98,6 +107,8 @@ class BlogRSSState:
|
|||||||
d'erreur d'écriture, une erreur est journalisée sans être
|
d'erreur d'écriture, une erreur est journalisée sans être
|
||||||
propagée et le fichier temporaire est supprimé.
|
propagée et le fichier temporaire est supprimé.
|
||||||
"""
|
"""
|
||||||
|
if not self._persistence_enabled:
|
||||||
|
return
|
||||||
payload = {
|
payload = {
|
||||||
"version": _STATE_VERSION,
|
"version": _STATE_VERSION,
|
||||||
"known_guids": sorted(self._known_guids),
|
"known_guids": sorted(self._known_guids),
|
||||||
|
|||||||
@@ -38,9 +38,15 @@ class PronoteAuthState:
|
|||||||
|
|
||||||
:param state_file: Chemin du fichier d'état JSON (``str`` ou
|
:param state_file: Chemin du fichier d'état JSON (``str`` ou
|
||||||
:class:`~pathlib.Path`). ``".pronote_auth_state.json"`` par défaut.
|
:class:`~pathlib.Path`). ``".pronote_auth_state.json"`` par défaut.
|
||||||
|
:param persistence_enabled: Si ``False``, charge l'état existant mais ne
|
||||||
|
modifie jamais le fichier d'état. ``True`` par défaut.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, state_file: Path | str = ".pronote_auth_state.json") -> None:
|
def __init__(
|
||||||
|
self,
|
||||||
|
state_file: Path | str = ".pronote_auth_state.json",
|
||||||
|
persistence_enabled: bool = True,
|
||||||
|
) -> None:
|
||||||
"""Initialise le gestionnaire d'état d'authentification Pronote.
|
"""Initialise le gestionnaire d'état d'authentification Pronote.
|
||||||
|
|
||||||
Le fichier d'état n'est pas créé à l'initialisation : il n'est écrit
|
Le fichier d'état n'est pas créé à l'initialisation : il n'est écrit
|
||||||
@@ -48,8 +54,14 @@ class PronoteAuthState:
|
|||||||
|
|
||||||
:param state_file: Chemin du fichier d'état JSON (``str`` ou
|
:param state_file: Chemin du fichier d'état JSON (``str`` ou
|
||||||
:class:`~pathlib.Path`). ``".pronote_auth_state.json"`` par défaut.
|
:class:`~pathlib.Path`). ``".pronote_auth_state.json"`` par défaut.
|
||||||
|
:param persistence_enabled: Si ``False``, charge l'état existant mais
|
||||||
|
désactive toutes les écritures ou suppressions sur disque. ``True``
|
||||||
|
par défaut.
|
||||||
"""
|
"""
|
||||||
self._state_file = Path(state_file)
|
self._state_file = Path(state_file)
|
||||||
|
self._persistence_enabled = persistence_enabled
|
||||||
|
self._in_memory_credentials: dict[str, str] | None = None
|
||||||
|
self._in_memory_state_cleared = False
|
||||||
|
|
||||||
def load(self) -> dict[str, str] | None:
|
def load(self) -> dict[str, str] | None:
|
||||||
"""Charge les credentials d'authentification depuis le fichier d'état.
|
"""Charge les credentials d'authentification depuis le fichier d'état.
|
||||||
@@ -65,6 +77,10 @@ class PronoteAuthState:
|
|||||||
aucun état valide n'est disponible.
|
aucun état valide n'est disponible.
|
||||||
:rtype: dict[str, str] | None
|
:rtype: dict[str, str] | None
|
||||||
"""
|
"""
|
||||||
|
if self._in_memory_state_cleared:
|
||||||
|
return None
|
||||||
|
if self._in_memory_credentials is not None:
|
||||||
|
return self._in_memory_credentials.copy()
|
||||||
if not self._state_file.exists():
|
if not self._state_file.exists():
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"Fichier d'état d'authentification Pronote %s absent, aucun token à charger.",
|
"Fichier d'état d'authentification Pronote %s absent, aucun token à charger.",
|
||||||
@@ -126,6 +142,10 @@ class PronoteAuthState:
|
|||||||
:raises PronoteSyncError: Si l'écriture ou le remplacement du fichier
|
:raises PronoteSyncError: Si l'écriture ou le remplacement du fichier
|
||||||
échoue.
|
échoue.
|
||||||
"""
|
"""
|
||||||
|
if not self._persistence_enabled:
|
||||||
|
self._in_memory_credentials = credentials.copy()
|
||||||
|
self._in_memory_state_cleared = False
|
||||||
|
return
|
||||||
payload: dict[str, Any] = {
|
payload: dict[str, Any] = {
|
||||||
"version": _STATE_VERSION,
|
"version": _STATE_VERSION,
|
||||||
"credentials": credentials,
|
"credentials": credentials,
|
||||||
@@ -182,10 +202,16 @@ class PronoteAuthState:
|
|||||||
"""Supprime le fichier d'état d'authentification.
|
"""Supprime le fichier d'état d'authentification.
|
||||||
|
|
||||||
Si le fichier n'existe pas, la méthode ne fait rien et aucune erreur
|
Si le fichier n'existe pas, la méthode ne fait rien et aucune erreur
|
||||||
n'est levée.
|
n'est levée. Lorsque la persistance est désactivée, elle efface
|
||||||
|
uniquement les credentials conservés en mémoire et ne modifie jamais
|
||||||
|
le fichier d'état.
|
||||||
|
|
||||||
:raises OSError: Si la suppression du fichier existant échoue.
|
:raises OSError: Si la suppression du fichier existant échoue.
|
||||||
"""
|
"""
|
||||||
|
self._in_memory_credentials = None
|
||||||
|
if not self._persistence_enabled:
|
||||||
|
self._in_memory_state_cleared = True
|
||||||
|
return
|
||||||
if not self._state_file.exists():
|
if not self._state_file.exists():
|
||||||
return
|
return
|
||||||
logger.debug(
|
logger.debug(
|
||||||
|
|||||||
@@ -8,7 +8,13 @@ from typing import Any, cast
|
|||||||
import pytest
|
import pytest
|
||||||
from pydantic import SecretStr
|
from pydantic import SecretStr
|
||||||
|
|
||||||
from pronote_sync.config.settings import AISettings, AppSettings, PronoteSettings, Settings
|
from pronote_sync.config.settings import (
|
||||||
|
AISettings,
|
||||||
|
AppSettings,
|
||||||
|
BlogSettings,
|
||||||
|
PronoteSettings,
|
||||||
|
Settings,
|
||||||
|
)
|
||||||
from pronote_sync.errors import PipelineCriticalError, PipelineWarning, PronoteAuthRotationError
|
from pronote_sync.errors import PipelineCriticalError, PipelineWarning, PronoteAuthRotationError
|
||||||
from pronote_sync.models.agenda import Lesson, LessonStatus, SchoolEvent
|
from pronote_sync.models.agenda import Lesson, LessonStatus, SchoolEvent
|
||||||
from pronote_sync.models.blog import BlogArticle
|
from pronote_sync.models.blog import BlogArticle
|
||||||
@@ -422,6 +428,47 @@ def test_from_settings_password_mode_passes_auth_state_none(
|
|||||||
assert constructed[0][1] is None
|
assert constructed[0][1] is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_from_settings_rejects_qr_token_dry_run_before_constructing_pronote_client(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
"""QR-token dry-run fails before authentication or data dependencies are created."""
|
||||||
|
import pronote_sync.pipeline.run as run_module
|
||||||
|
|
||||||
|
qr_pin_sentinel = "qr-pin-must-not-appear"
|
||||||
|
constructed: list[object] = []
|
||||||
|
|
||||||
|
class FailingClient:
|
||||||
|
"""Pronote client sentinel that makes unexpected construction explicit."""
|
||||||
|
|
||||||
|
def __init__(self, settings: PronoteSettings, *, auth_state: object) -> None:
|
||||||
|
"""Record and reject any unexpected client construction.
|
||||||
|
|
||||||
|
:param settings: Pronote settings supplied by the composition root.
|
||||||
|
:param auth_state: Authentication state supplied by the composition root.
|
||||||
|
"""
|
||||||
|
del settings, auth_state
|
||||||
|
constructed.append(object())
|
||||||
|
raise AssertionError("PronoteClient must not be constructed for QR-token dry-run")
|
||||||
|
|
||||||
|
monkeypatch.setattr(run_module, "PronoteClient", FailingClient)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError) as exc_info:
|
||||||
|
PipelineRunner.from_settings(
|
||||||
|
Settings(
|
||||||
|
app=AppSettings(dry_run=True),
|
||||||
|
pronote=PronoteSettings(
|
||||||
|
auth_mode="qr_token",
|
||||||
|
qr_pin=SecretStr(qr_pin_sentinel),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert "qr_token" in str(exc_info.value)
|
||||||
|
assert "dry-run" in str(exc_info.value)
|
||||||
|
assert qr_pin_sentinel not in str(exc_info.value)
|
||||||
|
assert constructed == []
|
||||||
|
|
||||||
|
|
||||||
def test_from_settings_qr_token_mode_passes_auth_state_instance(
|
def test_from_settings_qr_token_mode_passes_auth_state_instance(
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -449,6 +496,64 @@ def test_from_settings_qr_token_mode_passes_auth_state_instance(
|
|||||||
assert isinstance(constructed[0][1], PronoteAuthState)
|
assert isinstance(constructed[0][1], PronoteAuthState)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("dry_run", [False, True])
|
||||||
|
def test_from_settings_configures_source_state_persistence_for_dry_run(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
dry_run: bool,
|
||||||
|
) -> None:
|
||||||
|
"""Composition disables source-state persistence only in dry-run mode."""
|
||||||
|
import pronote_sync.pipeline.run as run_module
|
||||||
|
|
||||||
|
blog_persistence: list[bool] = []
|
||||||
|
auth_persistence: list[bool] = []
|
||||||
|
|
||||||
|
class RecordingBlogState:
|
||||||
|
"""Blog state factory recording its persistence configuration."""
|
||||||
|
|
||||||
|
def __init__(self, *, persistence_enabled: bool = True) -> None:
|
||||||
|
"""Record the requested persistence setting.
|
||||||
|
|
||||||
|
:param persistence_enabled: Whether disk writes are enabled.
|
||||||
|
"""
|
||||||
|
blog_persistence.append(persistence_enabled)
|
||||||
|
|
||||||
|
class RecordingAuthState:
|
||||||
|
"""Authentication state factory recording its persistence configuration."""
|
||||||
|
|
||||||
|
def __init__(self, *, persistence_enabled: bool = True) -> None:
|
||||||
|
"""Record the requested persistence setting.
|
||||||
|
|
||||||
|
:param persistence_enabled: Whether disk writes are enabled.
|
||||||
|
"""
|
||||||
|
auth_persistence.append(persistence_enabled)
|
||||||
|
|
||||||
|
class RecordingClient:
|
||||||
|
"""Pronote client constructor accepting the injected auth state."""
|
||||||
|
|
||||||
|
def __init__(self, settings: PronoteSettings, *, auth_state: object) -> None:
|
||||||
|
"""Accept the composition-root dependencies.
|
||||||
|
|
||||||
|
:param settings: Pronote settings.
|
||||||
|
:param auth_state: Injected authentication state.
|
||||||
|
"""
|
||||||
|
del settings, auth_state
|
||||||
|
|
||||||
|
monkeypatch.setattr(run_module, "BlogRSSState", RecordingBlogState)
|
||||||
|
monkeypatch.setattr(run_module, "PronoteAuthState", RecordingAuthState)
|
||||||
|
monkeypatch.setattr(run_module, "PronoteClient", RecordingClient)
|
||||||
|
|
||||||
|
PipelineRunner.from_settings(
|
||||||
|
Settings(
|
||||||
|
app=AppSettings(dry_run=dry_run),
|
||||||
|
blog=BlogSettings(enabled=True),
|
||||||
|
pronote=PronoteSettings(auth_mode="qr_token"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert blog_persistence == [not dry_run]
|
||||||
|
assert auth_persistence == [not dry_run]
|
||||||
|
|
||||||
|
|
||||||
def test_runner_reuses_ical_download_and_parse_within_one_run(
|
def test_runner_reuses_ical_download_and_parse_within_one_run(
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
pipeline_inputs: tuple[Lesson, Homework],
|
pipeline_inputs: tuple[Lesson, Homework],
|
||||||
|
|||||||
@@ -35,6 +35,49 @@ def test_state_file_absent_empty_state(tmp_path: Path) -> None:
|
|||||||
assert state.get_cache_headers() == (None, None)
|
assert state.get_cache_headers() == (None, None)
|
||||||
|
|
||||||
|
|
||||||
|
def test_disabled_persistence_keeps_updates_in_memory_without_creating_file(tmp_path: Path) -> None:
|
||||||
|
"""Vérifie que la persistance désactivée conserve l'état uniquement en mémoire.
|
||||||
|
|
||||||
|
:param tmp_path: Fixture pytest pour un répertoire temporaire.
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
state_file = tmp_path / "state.json"
|
||||||
|
state = BlogRSSState(state_file, persistence_enabled=False)
|
||||||
|
|
||||||
|
state.add_guids(["guid-1"])
|
||||||
|
state.update_cache_headers("etag-123", "Wed, 01 Sep 2026 GMT")
|
||||||
|
|
||||||
|
assert state.get_known_guids() == frozenset({"guid-1"})
|
||||||
|
assert state.get_cache_headers() == ("etag-123", "Wed, 01 Sep 2026 GMT")
|
||||||
|
assert not state_file.exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_disabled_persistence_preserves_existing_file(tmp_path: Path) -> None:
|
||||||
|
"""Vérifie que la persistance désactivée ne modifie pas l'état déjà stocké.
|
||||||
|
|
||||||
|
:param tmp_path: Fixture pytest pour un répertoire temporaire.
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
state_file = tmp_path / "state.json"
|
||||||
|
original_content = json.dumps(
|
||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"known_guids": ["existing-guid"],
|
||||||
|
"etag": "old-etag",
|
||||||
|
"last_modified": "Tue, 31 Aug 2026 GMT",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
state_file.write_text(original_content, encoding="utf-8")
|
||||||
|
state = BlogRSSState(state_file, persistence_enabled=False)
|
||||||
|
|
||||||
|
state.add_guids(["new-guid"])
|
||||||
|
state.update_cache_headers("new-etag", "Wed, 01 Sep 2026 GMT")
|
||||||
|
|
||||||
|
assert state.get_known_guids() == frozenset({"existing-guid", "new-guid"})
|
||||||
|
assert state.get_cache_headers() == ("new-etag", "Wed, 01 Sep 2026 GMT")
|
||||||
|
assert state_file.read_text(encoding="utf-8") == original_content
|
||||||
|
|
||||||
|
|
||||||
def test_add_guids_persists(tmp_path: Path) -> None:
|
def test_add_guids_persists(tmp_path: Path) -> None:
|
||||||
"""Vérifie que l'ajout de GUID persiste dans le fichier JSON.
|
"""Vérifie que l'ajout de GUID persiste dans le fichier JSON.
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,90 @@ def test_load_no_file_returns_none(tmp_path: Path) -> None:
|
|||||||
assert state.load() is None
|
assert state.load() is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_disabled_persistence_keeps_credentials_in_memory_without_creating_file(
|
||||||
|
tmp_path: Path,
|
||||||
|
) -> None:
|
||||||
|
"""Vérifie que la persistance désactivée conserve les credentials en mémoire.
|
||||||
|
|
||||||
|
:param tmp_path: Fixture pytest pour un répertoire temporaire.
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
state_file = tmp_path / "auth.json"
|
||||||
|
credentials = {
|
||||||
|
"pronote_url": "https://example.com/pronote",
|
||||||
|
"username": "parent-1",
|
||||||
|
"password": "token-123", # pragma: allowlist secret
|
||||||
|
"uuid": "uuid-456",
|
||||||
|
}
|
||||||
|
state = PronoteAuthState(state_file, persistence_enabled=False)
|
||||||
|
|
||||||
|
state.save(credentials)
|
||||||
|
|
||||||
|
assert state.load() == credentials
|
||||||
|
assert not state_file.exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_disabled_persistence_preserves_existing_file(tmp_path: Path) -> None:
|
||||||
|
"""Vérifie que la persistance désactivée garde les nouveaux credentials en mémoire.
|
||||||
|
|
||||||
|
:param tmp_path: Fixture pytest pour un répertoire temporaire.
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
state_file = tmp_path / "auth.json"
|
||||||
|
original_credentials = {
|
||||||
|
"pronote_url": "https://example.com/pronote",
|
||||||
|
"username": "parent-1",
|
||||||
|
"password": "old-token", # pragma: allowlist secret
|
||||||
|
"uuid": "old-uuid",
|
||||||
|
}
|
||||||
|
original_content = json.dumps({"version": 1, "credentials": original_credentials}).encode()
|
||||||
|
new_credentials = {
|
||||||
|
"pronote_url": "https://example.com/pronote",
|
||||||
|
"username": "parent-1",
|
||||||
|
"password": "new-token", # pragma: allowlist secret
|
||||||
|
"uuid": "new-uuid",
|
||||||
|
}
|
||||||
|
state_file.write_bytes(original_content)
|
||||||
|
state = PronoteAuthState(state_file, persistence_enabled=False)
|
||||||
|
|
||||||
|
assert state.load() == original_credentials
|
||||||
|
|
||||||
|
state.save(new_credentials)
|
||||||
|
|
||||||
|
assert state.load() == new_credentials
|
||||||
|
assert state_file.read_bytes() == original_content
|
||||||
|
|
||||||
|
|
||||||
|
def test_disabled_persistence_clear_discards_in_memory_credentials_only(tmp_path: Path) -> None:
|
||||||
|
"""Vérifie que clear oublie l'état en mémoire sans modifier le fichier existant.
|
||||||
|
|
||||||
|
:param tmp_path: Fixture pytest pour un répertoire temporaire.
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
state_file = tmp_path / "auth.json"
|
||||||
|
original_credentials = {
|
||||||
|
"pronote_url": "https://example.com/pronote",
|
||||||
|
"username": "parent-1",
|
||||||
|
"password": "old-token", # pragma: allowlist secret
|
||||||
|
"uuid": "old-uuid",
|
||||||
|
}
|
||||||
|
original_content = json.dumps({"version": 1, "credentials": original_credentials}).encode()
|
||||||
|
credentials = {
|
||||||
|
"pronote_url": "https://example.com/pronote",
|
||||||
|
"username": "parent-1",
|
||||||
|
"password": "new-token", # pragma: allowlist secret
|
||||||
|
"uuid": "new-uuid",
|
||||||
|
}
|
||||||
|
state_file.write_bytes(original_content)
|
||||||
|
state = PronoteAuthState(state_file, persistence_enabled=False)
|
||||||
|
|
||||||
|
state.save(credentials)
|
||||||
|
state.clear()
|
||||||
|
|
||||||
|
assert state.load() is None
|
||||||
|
assert state_file.read_bytes() == original_content
|
||||||
|
|
||||||
|
|
||||||
def test_save_then_load_roundtrip(tmp_path: Path) -> None:
|
def test_save_then_load_roundtrip(tmp_path: Path) -> None:
|
||||||
"""Vérifie que des credentials sauvegardés sont rechargés à l'identique.
|
"""Vérifie que des credentials sauvegardés sont rechargés à l'identique.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user