"""Tests unitaires pour le gestionnaire d'état d'authentification Pronote. Ce module valide le comportement de :class:`PronoteAuthState` dans :mod:`pronote_sync.sources.pronote.auth_state`. Les tests couvrent : - Le chargement des credentials (absent, corrompu, version invalide), - La persistance et le rechargement des credentials, - Les permissions ``0600`` du fichier d'état, - La suppression via :meth:`clear`, - L'absence de fuite des credentials dans les journaux. Tous les tests utilisent des fichiers temporaires via la fixture ``tmp_path``. """ from __future__ import annotations import json import logging import os from pathlib import Path import pytest from pronote_sync.sources.pronote.auth_state import PronoteAuthState def test_load_no_file_returns_none(tmp_path: Path) -> None: """Vérifie qu'un fichier d'état absent renvoie ``None``. :param tmp_path: Fixture pytest pour un répertoire temporaire. :return: None """ state = PronoteAuthState(tmp_path / "missing.json") assert state.load() is None def test_save_then_load_roundtrip(tmp_path: Path) -> None: """Vérifie que des credentials sauvegardés sont rechargés à l'identique. :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) state.save(credentials) loaded = PronoteAuthState(state_file).load() assert loaded == credentials def test_load_corrupted_json_returns_none(tmp_path: Path, caplog: pytest.LogCaptureFixture) -> None: """Vérifie qu'un fichier JSON corrompu renvoie ``None`` et journalise un avertissement. :param tmp_path: Fixture pytest pour un répertoire temporaire. :param caplog: Fixture pytest pour capturer les logs. :return: None """ state_file = tmp_path / "corrupt.json" state_file.write_text("not json{", encoding="utf-8") with caplog.at_level("WARNING"): result = PronoteAuthState(state_file).load() assert result is None assert "Impossible de charger le fichier d'état d'authentification Pronote" in caplog.text def test_load_wrong_version_returns_none(tmp_path: Path, caplog: pytest.LogCaptureFixture) -> None: """Vérifie qu'une version non supportée renvoie ``None`` et journalise un avertissement. :param tmp_path: Fixture pytest pour un répertoire temporaire. :param caplog: Fixture pytest pour capturer les logs. :return: None """ state_file = tmp_path / "wrong_version.json" state_file.write_text( json.dumps( { "version": 2, "credentials": { "pronote_url": "https://example.com", "username": "u", "password": "t", "uuid": "i", }, } ), encoding="utf-8", ) with caplog.at_level("WARNING"): result = PronoteAuthState(state_file).load() assert result is None assert "version absente ou non supportée" in caplog.text def test_load_missing_version_returns_none( tmp_path: Path, caplog: pytest.LogCaptureFixture ) -> None: """Vérifie qu'un fichier sans champ version renvoie ``None``. :param tmp_path: Fixture pytest pour un répertoire temporaire. :param caplog: Fixture pytest pour capturer les logs. :return: None """ state_file = tmp_path / "missing_version.json" state_file.write_text( json.dumps( { "credentials": { "pronote_url": "https://example.com", "username": "u", "password": "t", "uuid": "i", } } ), encoding="utf-8", ) with caplog.at_level("WARNING"): result = PronoteAuthState(state_file).load() assert result is None def test_clear_removes_file(tmp_path: Path) -> None: """Vérifie que clear supprime le fichier d'état existant. :param tmp_path: Fixture pytest pour un répertoire temporaire. :return: None """ state_file = tmp_path / "auth.json" state = PronoteAuthState(state_file) state.save({"pronote_url": "u", "username": "u", "password": "t", "uuid": "i"}) assert state_file.exists() state.clear() assert not state_file.exists() def test_clear_no_file_noop(tmp_path: Path) -> None: """Vérifie que clear ne fait rien quand le fichier n'existe pas. :param tmp_path: Fixture pytest pour un répertoire temporaire. :return: None """ state = PronoteAuthState(tmp_path / "missing.json") state.clear() def test_save_creates_file_with_0600_permissions(tmp_path: Path) -> None: """Vérifie que le fichier d'état est créé avec les permissions ``0600``. Le fichier contient un token vivant : il doit être lisible uniquement par le propriétaire. :param tmp_path: Fixture pytest pour un répertoire temporaire. :return: None """ state_file = tmp_path / "auth.json" state = PronoteAuthState(state_file) state.save({"pronote_url": "u", "username": "u", "password": "t", "uuid": "i"}) assert os.stat(state_file).st_mode & 0o777 == 0o600 def test_no_credentials_in_logs(tmp_path: Path, caplog: pytest.LogCaptureFixture) -> None: """Vérifie qu'aucun contenu des credentials n'apparaît dans les journaux. Des sentinelles distinctes sont utilisées pour ``pronote_url``, ``username``, ``password`` et ``uuid`` ; aucun de ces marqueurs ne doit apparaître dans les messages journalisés lors d'une sauvegarde, d'un chargement et d'une suppression. :param tmp_path: Fixture pytest pour un répertoire temporaire. :param caplog: Fixture pytest pour capturer les logs. :return: None """ state_file = tmp_path / "auth.json" credentials = { "pronote_url": "https://SENTINEL_URL_ZZZ.example/pronote", "username": "SENTINEL_USER_ZZZ", "password": "SENTINEL_PASSWORD_ZZZ", # pragma: allowlist secret "uuid": "SENTINEL_UUID_ZZZ", } state = PronoteAuthState(state_file) with caplog.at_level(logging.DEBUG): state.save(credentials) state.load() state.clear() assert "SENTINEL_URL_ZZZ" not in caplog.text assert "SENTINEL_USER_ZZZ" not in caplog.text assert "SENTINEL_PASSWORD_ZZZ" not in caplog.text assert "SENTINEL_UUID_ZZZ" not in caplog.text