diff --git a/.secrets.baseline b/.secrets.baseline index e860616..8934a65 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -140,7 +140,7 @@ "filename": "GUIDE_DEV_PYTHON.md", "hashed_secret": "90bd1b48e958257948487b90bee080ba5ed00caa", "is_verified": true, - "line_number": 5084, + "line_number": 5085, "is_secret": false } ], @@ -177,5 +177,5 @@ } ] }, - "generated_at": "2026-09-10T19:26:08Z" + "generated_at": "2026-09-11T09:57:08Z" } diff --git a/GUIDE_DEV_PYTHON.md b/GUIDE_DEV_PYTHON.md index ad0e78b..f2a0ca8 100644 --- a/GUIDE_DEV_PYTHON.md +++ b/GUIDE_DEV_PYTHON.md @@ -310,7 +310,7 @@ d'un besoin réel et testé. | `AI_API_KEY` | Clé API pour l'API IA. | `None` | `SecretStr` | | `AI_MODEL` | Modèle IA à utiliser (exemple recommandé : `gpt-4o-mini`). | `None` | `str \| None`| | `AI_ALLOW_INSECURE_HTTP` | Autoriser HTTP (non sécurisé) pour `openai-compatible` uniquement. | `False` | `bool` | -| `DRY_RUN` | Mode dry-run (pas de modifications CalDAV/XMPP). | `False` | `bool` | +| `DRY_RUN` | Simulation sans sortie distante ni état local persistant ; incompatible avec `qr_token`. | `False` | `bool` | | `LOG_LEVEL` | Niveau de log (`DEBUG`, `INFO`, `WARNING`, `ERROR`). | `INFO` | `str` | > ⚠️ **Décision d'implémentation** : @@ -3241,7 +3241,8 @@ class CalDAVClient: ### 7.4 Points clés - **Différentielle** : La synchronisation compare les UID existants avec ceux à synchroniser. - **Idempotence** : Deux exécutions identiques ne modifient pas le calendrier. -- **Dry-run** : Mode obligatoire pour tester sans effet de bord. +- **Dry-run** : Les lectures sont autorisées, sans sortie distante ni état local persistant ; le + mode `qr_token` est refusé avant connexion car son authentification implique une rotation distante. - **Marquage** : Les événements gérés sont marqués avec `X-PRONOTE-SYNC-MANAGED: v1` pour éviter les conflits. - **Cours annulés** : Conservés avec `STATUS:CANCELLED` (ne pas supprimer). - **Plan explicite** : Le `CalDAVSyncPlan` est calculé avant l'exécution. @@ -5780,7 +5781,7 @@ repos: |----------------------------------------|-----------------------------------------------------------------------------------------------------|-----------------|------------| | Configuration des variables d'environnement | Vérifier que toutes les variables obligatoires sont définies (voir [Section 3.1](#31-variables-denvironnement)). | ✅ Oui | | | Vérification des secrets | Exécuter le script de vérification de sécurité (voir [Section 13.6](#136-exemple-de-script-de-vérification-de-sécurité)). | ✅ Oui | | -| Test en mode dry-run | Exécuter le pipeline avec `DRY_RUN=true` pour vérifier que tout fonctionne sans effet de bord. | ✅ Oui | | +| Test en mode dry-run | Exécuter le pipeline avec `DRY_RUN=true` sans sortie distante ni état local persistant (`qr_token` exclu). | ✅ Oui | | | Configuration des logs | Vérifier que les logs sont configurés avec masquage des secrets (voir [Section 4.2](#42-implémentation)). | ✅ Oui | | | Vérification des dépendances | Exécuter `pip check` pour vérifier que toutes les dépendances sont installées. | ✅ Oui | | | Configuration du cron (si planifié) | Configurer une tâche cron pour exécuter le script régulièrement (ex: tous les jours à 18h). | ⚠️ Non | | @@ -5960,7 +5961,7 @@ Exemple de ligne cron (exécution tous les jours à 18h) : | **CalDAV** | Protocole pour synchroniser des calendriers via HTTP. | | **XMPP** | Protocole de messagerie instantanée (anciennement Jabber). | | **UID** | Identifiant unique pour un événement iCal/CalDAV. | -| **Dry-run** | Mode de test où aucune modification n'est appliquée (lecture seule). | +| **Dry-run** | Simulation avec lectures autorisées, sans sortie distante ni état local persistant ; incompatible avec `qr_token`. | | **Idempotence** | Propriété d'une opération qui produit le même résultat si elle est exécutée plusieurs fois. | | **Reverse-engineering** | Technique consistant à analyser un logiciel pour en comprendre le fonctionnement interne. | @@ -6127,7 +6128,7 @@ Ce guide fournit une **base architecturale et technique solide** pour développe | **CalDAV** | Protocole pour synchroniser des calendriers via HTTP. | | **XMPP** | Protocole de messagerie instantanée (anciennement Jabber). | | **UID** | Identifiant unique pour un événement iCal/CalDAV. | -| **Dry-run** | Mode de test où aucune modification n'est appliquée (lecture seule). | +| **Dry-run** | Simulation avec lectures autorisées, sans sortie distante ni état local persistant ; incompatible avec `qr_token`. | | **Idempotence** | Propriété d'une opération qui produit le même résultat si elle est exécutée plusieurs fois. | | **Reverse-engineering** | Technique consistant à analyser un logiciel pour en comprendre le fonctionnement interne. | diff --git a/README.md b/README.md index 94c26de..1c93c94 100644 --- a/README.md +++ b/README.md @@ -36,10 +36,16 @@ pronote-sync --dry-run --log-level DEBUG ```bash pronote-sync # Exécute la synchronisation -pronote-sync --dry-run # Simulation sans écriture +pronote-sync --dry-run # Simulation : lectures autorisées, aucune écriture persistante ni sortie distante pronote-sync --log-level DEBUG # Verbosité des journaux ``` +En `--dry-run`, les données peuvent être lues pour construire la simulation, mais aucun état local +de source n'est enregistré : l'état RSS reste en mémoire pendant l'exécution. Les écritures CalDAV +et l'envoi XMPP sont également désactivés. Le mode +`PRONOTE_AUTH_MODE=qr_token` est refusé avant toute connexion, car la rotation de son token ne peut +pas garantir un état persistant cohérent pendant une simulation. + --- ## 🛠️ Déploiement diff --git a/TODO.md b/TODO.md index 240e4a5..c4db21d 100644 --- a/TODO.md +++ b/TODO.md @@ -147,7 +147,8 @@ Synchroniser différentiellement les événements Pronote vers le calendrier Cal - Le plan de sync est correctement calculé (données Pronote vs événements distants gérés). - Un changement de source iCal ↔ `pronotepy` ne crée ni doublon ni suppression/ajout artificiel pour un cours équivalent. -- Un run dry-run n'écrit rien ; deux runs identiques donnent un résultat identique. +- Un run dry-run autorise les lectures, mais ne produit aucune sortie distante ni écriture locale + persistante ; `qr_token` est refusé avant connexion. Deux runs identiques donnent le même résultat. - Les événements annulés restent (`STATUS:CANCELLED`) et sont marqués `MANAGED`. - Les événements non marqués ne sont jamais modifiés ni supprimés. @@ -223,7 +224,9 @@ Composer et orchestrer toutes les étapes avec gestion d'erreurs dégradée et m - [x] Créer les étapes `pipeline/steps/` : `fetch.py`, `normalize.py`, `compare.py`, `caldav_sync.py`, `synthesis.py`, `send.py`, `fetch_blog.py`. - [x] Créer `pipeline/run.py` : `PipelineRunner` (composition root) orchestrant fetch → normalize → fetch_blog → compare → caldav_sync → synthesis → send. - [x] Gérer les erreurs dégradées (continuer sauf critique) et renvoyer `(PronoteData, erreurs + warns)`. -- [x] Implémenter le mode `dry_run` (aucune écriture CalDAV/XMPP). +- [x] Implémenter le mode `dry_run` : lectures autorisées, aucune écriture CalDAV/XMPP ni écriture + persistante locale ; l'état RSS reste en mémoire pendant l'exécution, et + `PRONOTE_AUTH_MODE=qr_token` est refusé avant toute connexion. - [x] Câbler l'injection des dépendances (Protocol + composition root), sans singleton global. - [x] Réutiliser, dans une même exécution, un unique téléchargement/parsing iCal pour l'agenda et les devoirs lorsque les sources sélectionnées le permettent ; rester sur un cache local au run, sans cache global ni persistant. @@ -231,7 +234,9 @@ Composer et orchestrer toutes les étapes avec gestion d'erreurs dégradée et m - [x] Le pipeline complet s'exécute de bout en bout (mocks) dans le bon ordre. - [x] Une sélection iCal commune à l'agenda et aux devoirs ne déclenche qu'un téléchargement/parsing du flux par run. - [x] Une erreur non critique (ex : synthèse IA) n'empêche pas l'envoi XMPP. -- [x] `dry_run=True` n'effectue aucune écriture ; aucune source disponible → erreur critique explicite. +- [x] `dry_run=True` autorise les lectures mais n'effectue aucune écriture persistante locale ni + sortie distante ; aucune source disponible → erreur critique explicite. L'état RSS n'est pas + enregistré et `qr_token` est refusé avant toute connexion. - [x] Si `THEORETICAL_AGENDA_PATH` est absent, le pipeline produit un diff vide sans erreur et n'instancie pas `AgendaComparator` ; si présent, il instancie le comparateur et effectue la comparaison. - [x] Les erreurs critiques (`PipelineCriticalError`) propagées depuis une étape non-bloquante arrêtent le pipeline. @@ -247,7 +252,8 @@ Exposer le lancement du pipeline via une interface en ligne de commande. - [x] Gérer le code de retour et l'affichage des erreurs (redactées). ### Critères d'acceptation -- `pronote-sync --dry-run --log-level DEBUG` s'exécute sans effet de bord. +- `pronote-sync --dry-run --log-level DEBUG` s'exécute sans sortie distante ni état local persistant ; + le mode `qr_token`, qui implique une rotation distante, est refusé avant connexion. - Le script console est installable (`[project.scripts]` dans `pyproject.toml`). - Les erreurs affichées ne contiennent aucun secret, y compris avec l'affichage d'un traceback complet en mode debug. diff --git a/docs/exploitation.md b/docs/exploitation.md index 5c98729..dca4ebc 100644 --- a/docs/exploitation.md +++ b/docs/exploitation.md @@ -42,8 +42,11 @@ bloquent donc pas le déploiement. Il ne valide ni les valeurs ni les permission du fichier d'environnement. Pour analyser seulement le contenu indexé avant un commit, utilisez `scripts/check_secrets.py --staged`. -Le dry-run vérifie le pipeline sans appliquer les écritures de synchronisation ; -il ne remplace pas une vérification des paramètres réellement chargés. +Le dry-run autorise les lectures nécessaires à la simulation, mais n'applique aucune sortie +CalDAV/XMPP et ne modifie aucun état local persistant. L'état RSS reste limité à la mémoire du +processus. Le mode `PRONOTE_AUTH_MODE=qr_token` est incompatible avec cette garantie : la commande +le refuse avant toute connexion afin de ne pas désynchroniser le token local du token distant. +Le dry-run ne remplace pas une vérification des paramètres réellement chargés. ## Installation systemd diff --git a/pronote_sync/cli/main.py b/pronote_sync/cli/main.py index f91591b..e5b47f0 100644 --- a/pronote_sync/cli/main.py +++ b/pronote_sync/cli/main.py @@ -32,7 +32,10 @@ def _parse_arguments(arguments: Sequence[str] | None = None) -> argparse.Namespa "--dry-run", action="store_true", default=None, - help="Simule la synchronisation sans écrire vers CalDAV ni XMPP.", + help=( + "Simule la synchronisation sans sortie distante ni état local persistant " + "(incompatible avec PRONOTE_AUTH_MODE=qr_token)." + ), ) parser.add_argument( "--log-level", diff --git a/tests/e2e/test_cli.py b/tests/e2e/test_cli.py index 6428916..d298b1f 100644 --- a/tests/e2e/test_cli.py +++ b/tests/e2e/test_cli.py @@ -11,6 +11,23 @@ from pronote_sync.errors import PipelineCriticalError, PipelineWarning from pronote_sync.models.pronote import PronoteData +def test_help_documents_strict_dry_run_contract(capsys: pytest.CaptureFixture[str]) -> None: + """L'aide CLI expose le contrat strict et l'incompatibilité QR/token. + + :param capsys: Capture des sorties standard de pytest. + :return: None + """ + from pronote_sync.cli.main import main + + with pytest.raises(SystemExit) as exc_info: + main(["--help"]) + + output = " ".join(capsys.readouterr().out.split()) + assert exc_info.value.code == 0 + assert "sans sortie distante ni état local persistant" in output + assert "PRONOTE_AUTH_MODE=qr_token" in output + + def test_main_runs_composition_root_in_dry_run_with_requested_log_level( mocker: MockerFixture, ) -> None: diff --git a/tests/integration/test_pipeline_runner.py b/tests/integration/test_pipeline_runner.py index 18a5e48..562dad8 100644 --- a/tests/integration/test_pipeline_runner.py +++ b/tests/integration/test_pipeline_runner.py @@ -505,7 +505,6 @@ def test_from_settings_configures_source_state_persistence_for_dry_run( 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.""" @@ -517,16 +516,6 @@ def test_from_settings_configures_source_state_persistence_for_dry_run( """ 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.""" @@ -539,19 +528,16 @@ def test_from_settings_configures_source_state_persistence_for_dry_run( 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(