feat(M7): synchronisation différentielle CalDAV
Implémente la synchronisation des événements Pronote vers un calendrier CalDAV (Nextcloud) de façon idempotente et sécurisée. Production : - sync/serialization.py : sérialisation Lesson/Homework/SchoolEvent vers VEVENT, signature sémantique (exclut DTSTAMP/CREATED/LAST-MODIFIED), enveloppe VCALENDAR complète avec VERSION:2.0 et PRODID - sync/caldav.py : passerelle CalDAV isolant caldav>=1.3.0, résolution du calendrier via principal().calendars() avec boundary matching, upsert par UID (fetch-then-save), exceptions expurgées et __context__ propre, mot de passe non stocké en clair, context manager - sync/planner.py : calcul explicite du CalDAVSyncPlan (add/update/remove par comparaison de signatures sémantiques, routage par préfixe d'UID) - sync/executor.py : exécution du plan avec dry-run (aucune écriture), isolation des erreurs par événement, statut FAILED/SKIPPED/SUCCESS - sync/synchronizer.py : orchestration en trois phases (scan, plan, exécution), SKIPPED si CalDAV non configuré - sync/__init__.py : export synchronize() - sources/pronote/client.py : normalisation UID via normalize_pronote_uid/ generate_deterministic_uid (parité avec ical.py) - config/settings.py : CalDAVSettings durci (url SecretStr, validation HTTPS, allow_insecure_http pour localhost, serializer redact_url) Tests (381 passés, couverture 95.58%) : - tests/unit/test_sync_serialization.py (21 tests) - tests/unit/test_caldav_planner.py (16 tests) - tests/unit/test_caldav_executor.py (18 tests) - tests/unit/test_caldav_gateway.py (24 tests) - tests/unit/test_caldav_security.py (18 tests) - tests/unit/test_uid_equivalence.py (8 tests) - tests/integration/test_caldav_sync.py (11 tests, faux serveur en mémoire) - tests/conftest.py : fixtures partagées Documentation : - GUIDE_DEV_PYTHON.md §7 : API réelle caldav>=1.3.0, principal().calendars(), VCALENDAR complet, upsert par UID, pas d'état local, événements non gérés protégés, CalDAVSettings durci (SecretStr, HTTPS, allow_insecure_http) - TODO.md : M7 coché - .env.example : CALDAV_ALLOW_INSECURE_HTTP=false 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:
@@ -0,0 +1,99 @@
|
||||
"""Fixtures partagées pour les tests de pronote-sync."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date, datetime
|
||||
|
||||
import pytest
|
||||
from pydantic import SecretStr
|
||||
|
||||
from pronote_sync.config.settings import CalDAVSettings
|
||||
from pronote_sync.models.agenda import Lesson, LessonStatus, SchoolEvent, SchoolEventKind
|
||||
from pronote_sync.models.homework import Homework
|
||||
from pronote_sync.models.pronote import PronoteData
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_lesson() -> Lesson:
|
||||
"""Cours normal pour les tests."""
|
||||
return Lesson(
|
||||
id="L-1234-Normal",
|
||||
start=datetime(2026, 1, 15, 8, 0),
|
||||
end=datetime(2026, 1, 15, 9, 0),
|
||||
subject="Mathématiques",
|
||||
teachers=("Prof Dupont",),
|
||||
rooms=("Salle 101",),
|
||||
status=LessonStatus.NORMAL,
|
||||
group=None,
|
||||
content=None,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_cancelled_lesson() -> Lesson:
|
||||
"""Cours annulé pour les tests."""
|
||||
return Lesson(
|
||||
id="L-5678-Cancelled",
|
||||
start=datetime(2026, 1, 16, 10, 0),
|
||||
end=datetime(2026, 1, 16, 11, 0),
|
||||
subject="Français",
|
||||
teachers=("Prof Martin",),
|
||||
rooms=("Salle 202",),
|
||||
status=LessonStatus.CANCELLED,
|
||||
group=None,
|
||||
content=None,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_homework() -> Homework:
|
||||
"""Devoir pour les tests."""
|
||||
return Homework(
|
||||
id="hw-001",
|
||||
subject="Histoire",
|
||||
teachers=("Prof Bernard",),
|
||||
assigned_on=date(2026, 1, 15),
|
||||
due_on=date(2026, 1, 20),
|
||||
text="Lire le chapitre 5",
|
||||
html="<p>Lire le chapitre 5</p>",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_school_event() -> SchoolEvent:
|
||||
"""Événement scolaire pour les tests."""
|
||||
return SchoolEvent(
|
||||
kind=SchoolEventKind.HOLIDAY,
|
||||
label="Vacances de Noël",
|
||||
from_date=date(2026, 12, 20),
|
||||
to_date=date(2027, 1, 5),
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def caldav_settings() -> CalDAVSettings:
|
||||
"""Configuration CalDAV de test avec une URL HTTPS."""
|
||||
return CalDAVSettings(
|
||||
url=SecretStr("https://caldav.example.com/remote.php/dav/"),
|
||||
username="test-user",
|
||||
password=SecretStr("test-secret-password-12345"),
|
||||
calendar_path="/pronote-sync/",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def pronote_data(
|
||||
sample_lesson: Lesson,
|
||||
sample_cancelled_lesson: Lesson,
|
||||
sample_homework: Homework,
|
||||
sample_school_event: SchoolEvent,
|
||||
) -> PronoteData:
|
||||
"""Données Pronote de test avec des cours, devoirs et événements."""
|
||||
return PronoteData(
|
||||
lessons=[sample_lesson, sample_cancelled_lesson],
|
||||
homeworks=[sample_homework],
|
||||
school_events=[sample_school_event],
|
||||
messages=[],
|
||||
target_date=date(2026, 1, 15),
|
||||
generated_at=datetime(2026, 1, 15, 0, 0),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user