fix(M7): corrections d'audit FIXME_M7 — sécurité, fenêtre, UID, timezone
Corrige les 5 constats de l'audit FIXME_M7 : #1 (Bloquant) — Protection des événements non marqués : - upsert_event() vérifie le marqueur X-PRONOTE-SYNC-MANAGED avant modification ; lève PronoteSyncError en cas de collision avec un événement non géré (aucune écriture) - delete_event() vérifie le marqueur ; no-op avec warning si non géré - Méthode privée _is_managed_event() factorisant le contrôle #2 (Bloquant) — Fenêtre de synchronisation : - Calcul en journées entières (minuit à minuit exclusif) - Filtrage des données locales (lessons, homeworks, school_events) avant passage au planner - Paramètre now injectable pour les tests #3 (Bloquant) — UID canonique vs brut : - list_managed_events() retourne (raw_uid, canonical_uid, vevent) - compute_plan() matche par UID canonique, route les raw UID vers *_to_remove, retourne le mapping remote_raw_by_canonical - executor.execute() utilise le raw UID pour les mises à jour (pas de doublon) - Pas de migration destructive des UID distants existants #4 (Correction) — Normalisation temporelle UTC : - normalize_datetime_to_utc() dans utils/uid.py : naïve → Europe/Paris → UTC ; consciente → UTC - Utilisée par generate_deterministic_uid() et component_to_signature() - Deux représentations du même instant → même UID et même signature #5 (Compatibilité) — date_search déprécié : - Remplacement par calendar.search(start, end, event=True, expand=True) Documentation : - GUIDE_DEV_PYTHON.md : suppression des références obsolètes à sync/state.py et état SQLite/JSON ; mise à jour de l'API CalDAV (search au lieu de date_search, upsert par UID) - TODO.md : M7 décoché (corrections en cours de validation) Tests : 390 passés, couverture 95.61% Co-authored-by: opencode/coder <coder@agents.invalid> Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid> Co-authored-by: opencode/tech-writer <tech-writer@agents.invalid>
This commit is contained in:
@@ -339,12 +339,69 @@ def test_real_update_calls_gateway(
|
||||
:return: None
|
||||
"""
|
||||
executor = CalDAVSyncExecutor(mock_gateway, dry_run=False)
|
||||
result = executor.execute(plan_with_lesson_update)
|
||||
result = executor.execute(
|
||||
plan_with_lesson_update,
|
||||
remote_raw_by_canonical={"L-1234": "L-1234"},
|
||||
)
|
||||
|
||||
mock_gateway.upsert_event.assert_called_once()
|
||||
assert result.updated == 1
|
||||
|
||||
|
||||
def test_update_uses_raw_uid_from_mapping(
|
||||
mock_gateway: MagicMock, plan_with_lesson_update: CalDAVSyncPlan
|
||||
) -> None:
|
||||
"""Vérifie que la mise à jour cible l'UID brut distant du mapping.
|
||||
|
||||
Pour une mise à jour, l'exécuteur doit appeler ``upsert_event`` avec
|
||||
l'UID brut fourni par ``remote_raw_by_canonical`` (celui stocké sur le
|
||||
serveur) et non l'UID canonique, afin d'éviter la création d'un doublon.
|
||||
|
||||
:param mock_gateway: Passerelle mockée.
|
||||
:param plan_with_lesson_update: Plan avec un cours à mettre à jour.
|
||||
:return: None
|
||||
"""
|
||||
raw_uid = "L-1234-20260905T080000Z-Index-Education"
|
||||
executor = CalDAVSyncExecutor(mock_gateway, dry_run=False)
|
||||
result = executor.execute(
|
||||
plan_with_lesson_update,
|
||||
remote_raw_by_canonical={"L-1234": raw_uid},
|
||||
)
|
||||
|
||||
call_args = mock_gateway.upsert_event.call_args
|
||||
assert call_args[0][0] is not None
|
||||
# Le contenu VEVENT porte l'UID canonique du modèle.
|
||||
assert "UID:L-1234" in call_args[0][0]
|
||||
# La cible (2e argument) est l'UID brut distant, pas l'UID canonique.
|
||||
assert call_args[0][1] == raw_uid
|
||||
assert result.updated == 1
|
||||
assert len(result.errors) == 0
|
||||
|
||||
|
||||
def test_update_without_mapping_raises_error(
|
||||
mock_gateway: MagicMock, plan_with_lesson_update: CalDAVSyncPlan
|
||||
) -> None:
|
||||
"""Vérifie qu'une mise à jour sans mapping distant consigne une erreur.
|
||||
|
||||
Si ``remote_raw_by_canonical`` est absent (ou sans clé pour l'UID
|
||||
canonique), l'exécuteur ne doit pas retomber silencieusement sur l'UID
|
||||
canonique (créant un doublon) : l'erreur est consignée dans
|
||||
``result.errors`` et le lot continue sans appeler la passerelle.
|
||||
|
||||
:param mock_gateway: Passerelle mockée.
|
||||
:param plan_with_lesson_update: Plan avec un cours à mettre à jour.
|
||||
:return: None
|
||||
"""
|
||||
executor = CalDAVSyncExecutor(mock_gateway, dry_run=False)
|
||||
result = executor.execute(plan_with_lesson_update)
|
||||
|
||||
mock_gateway.upsert_event.assert_not_called()
|
||||
assert len(result.errors) == 1
|
||||
assert "UID canonique sans correspondant distant" in result.errors[0]
|
||||
assert result.updated == 0
|
||||
assert result.status == CalDAVSyncStatus.FAILED
|
||||
|
||||
|
||||
def test_real_delete_calls_gateway(
|
||||
mock_gateway: MagicMock, plan_with_lesson_remove: CalDAVSyncPlan
|
||||
) -> None:
|
||||
@@ -380,7 +437,10 @@ def test_real_execution_all_operations(
|
||||
)
|
||||
|
||||
executor = CalDAVSyncExecutor(mock_gateway, dry_run=False)
|
||||
result = executor.execute(plan)
|
||||
result = executor.execute(
|
||||
plan,
|
||||
remote_raw_by_canonical={"L-0002": "L-0002"},
|
||||
)
|
||||
|
||||
# upsert_event appelé pour les ajouts et mises à jour
|
||||
# 1 lesson add + 1 lesson update + 1 homework add + 1 school event add = 4
|
||||
|
||||
Reference in New Issue
Block a user