293 lines
9.4 KiB
Python
293 lines
9.4 KiB
Python
"""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_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:
|
|
"""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
|