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:
581
tests/unit/test_caldav_planner.py
Normal file
581
tests/unit/test_caldav_planner.py
Normal file
@@ -0,0 +1,581 @@
|
||||
"""Tests unitaires pour le planificateur de synchronisation CalDAV.
|
||||
|
||||
Ce module vérifie que la fonction :func:`compute_plan` produit correctement
|
||||
les listes d'ajouts, mises à jour et suppressions pour chaque catégorie
|
||||
(cours, devoirs, événements scolaires) en comparant les données Pronote
|
||||
normalisées aux événements distants marqués comme gérés.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date, datetime
|
||||
|
||||
from icalendar import Event
|
||||
|
||||
from pronote_sync.models.agenda import Lesson, LessonStatus, SchoolEvent, SchoolEventKind
|
||||
from pronote_sync.models.homework import Homework
|
||||
from pronote_sync.models.pronote import PronoteData
|
||||
from pronote_sync.sync.planner import compute_plan
|
||||
from pronote_sync.sync.serialization import (
|
||||
lesson_to_vevent,
|
||||
)
|
||||
|
||||
# --- Helper fixtures ---
|
||||
|
||||
|
||||
def _make_lesson(
|
||||
lesson_id: str = "L-1234",
|
||||
subject: str = "Mathématiques",
|
||||
start: datetime | None = None,
|
||||
end: datetime | None = None,
|
||||
status: LessonStatus = LessonStatus.NORMAL,
|
||||
content: str | None = None,
|
||||
group: str | None = None,
|
||||
) -> Lesson:
|
||||
"""Fabrique un cours Pronote pour les tests.
|
||||
|
||||
:param lesson_id: Identifiant du cours.
|
||||
:param subject: Matière.
|
||||
:param start: Date/heure de début.
|
||||
:param end: Date/heure de fin.
|
||||
:param status: Statut du cours.
|
||||
:param content: Contenu pédagogique.
|
||||
:param group: Groupe.
|
||||
:return: Instance de Lesson.
|
||||
:rtype: Lesson
|
||||
"""
|
||||
if start is None:
|
||||
start = datetime(2026, 1, 15, 8, 0)
|
||||
if end is None:
|
||||
end = datetime(2026, 1, 15, 9, 0)
|
||||
return Lesson(
|
||||
id=lesson_id,
|
||||
start=start,
|
||||
end=end,
|
||||
subject=subject,
|
||||
status=status,
|
||||
content=content,
|
||||
group=group,
|
||||
)
|
||||
|
||||
|
||||
def _make_homework(
|
||||
homework_id: str = "HW-5678",
|
||||
subject: str = "Mathématiques",
|
||||
due_on: date | None = None,
|
||||
text: str = "Exercice 1 à 5",
|
||||
assigned_on: date | None = None,
|
||||
) -> Homework:
|
||||
"""Fabrique un devoir Pronote pour les tests.
|
||||
|
||||
:param homework_id: Identifiant du devoir.
|
||||
:param subject: Matière.
|
||||
:param due_on: Date d'échéance.
|
||||
:param text: Texte du devoir.
|
||||
:param assigned_on: Date de distribution.
|
||||
:return: Instance de Homework.
|
||||
:rtype: Homework
|
||||
"""
|
||||
if due_on is None:
|
||||
due_on = date(2026, 1, 20)
|
||||
return Homework(
|
||||
id=homework_id,
|
||||
subject=subject,
|
||||
due_on=due_on,
|
||||
text=text,
|
||||
assigned_on=assigned_on,
|
||||
)
|
||||
|
||||
|
||||
def _make_school_event(
|
||||
label: str = "Vacances de Noël",
|
||||
from_date: date | None = None,
|
||||
to_date: date | None = None,
|
||||
kind: SchoolEventKind = SchoolEventKind.HOLIDAY,
|
||||
) -> SchoolEvent:
|
||||
"""Fabrique un événement scolaire pour les tests.
|
||||
|
||||
:param label: Libellé de l'événement.
|
||||
:param from_date: Date de début.
|
||||
:param to_date: Date de fin.
|
||||
:param kind: Type d'événement.
|
||||
:return: Instance de SchoolEvent.
|
||||
:rtype: SchoolEvent
|
||||
"""
|
||||
if from_date is None:
|
||||
from_date = date(2026, 12, 20)
|
||||
if to_date is None:
|
||||
to_date = date(2027, 1, 5)
|
||||
return SchoolEvent(
|
||||
label=label,
|
||||
from_date=from_date,
|
||||
to_date=to_date,
|
||||
kind=kind,
|
||||
)
|
||||
|
||||
|
||||
def _make_vevent(
|
||||
uid: str,
|
||||
summary: str,
|
||||
dtstart: datetime,
|
||||
dtend: datetime,
|
||||
status: str = "CONFIRMED",
|
||||
categories: list[str] | None = None,
|
||||
) -> Event:
|
||||
"""Fabrique un VEVENT iCalendar pour les tests.
|
||||
|
||||
:param uid: UID de l'événement.
|
||||
:param summary: Résumé.
|
||||
:param dtstart: Date/heure de début.
|
||||
:param dtend: Date/heure de fin.
|
||||
:param status: Statut.
|
||||
:param categories: Catégories.
|
||||
:return: Instance de Event.
|
||||
:rtype: Event
|
||||
"""
|
||||
from icalendar import vDatetime
|
||||
|
||||
event = Event()
|
||||
event.add("UID", uid)
|
||||
event.add("SUMMARY", summary)
|
||||
event.add("DTSTART", vDatetime(dtstart))
|
||||
event.add("DTEND", vDatetime(dtend))
|
||||
event.add("STATUS", status)
|
||||
if categories:
|
||||
event.add("CATEGORIES", categories)
|
||||
# Ajouter le marqueur de gestion
|
||||
from pronote_sync.sync.serialization import MANAGED_PROPERTY, MANAGED_VALUE
|
||||
|
||||
event.add(MANAGED_PROPERTY, MANAGED_VALUE)
|
||||
return event
|
||||
|
||||
|
||||
def _make_pronote_data(
|
||||
lessons: list[Lesson] | None = None,
|
||||
homeworks: list[Homework] | None = None,
|
||||
school_events: list[SchoolEvent] | None = None,
|
||||
) -> PronoteData:
|
||||
"""Fabrique des données Pronote pour les tests.
|
||||
|
||||
:param lessons: Liste des cours.
|
||||
:param homeworks: Liste des devoirs.
|
||||
:param school_events: Liste des événements scolaires.
|
||||
:return: Instance de PronoteData.
|
||||
:rtype: PronoteData
|
||||
"""
|
||||
return PronoteData(
|
||||
lessons=lessons or [],
|
||||
homeworks=homeworks or [],
|
||||
school_events=school_events or [],
|
||||
messages=[],
|
||||
target_date=date(2026, 1, 15),
|
||||
generated_at=datetime(2026, 1, 15, 0, 0),
|
||||
)
|
||||
|
||||
|
||||
# --- Tests for lessons ---
|
||||
|
||||
|
||||
def test_lesson_add_when_not_in_remote() -> None:
|
||||
"""Vérifie qu'un cours non présent à distance va dans lessons_to_add.
|
||||
|
||||
:return: None
|
||||
"""
|
||||
lesson = _make_lesson(lesson_id="L-1234")
|
||||
pronote_data = _make_pronote_data(lessons=[lesson])
|
||||
remote_managed: list[tuple[str, Event]] = []
|
||||
|
||||
plan = compute_plan(pronote_data, remote_managed)
|
||||
|
||||
assert len(plan.lessons_to_add) == 1
|
||||
assert plan.lessons_to_add[0].id == "L-1234"
|
||||
assert len(plan.lessons_to_update) == 0
|
||||
assert len(plan.lessons_to_remove) == 0
|
||||
|
||||
|
||||
def test_lesson_update_when_signature_differs() -> None:
|
||||
"""Vérifie qu'un cours présent à distance avec une signature différente va dans lessons_to_update.
|
||||
|
||||
:return: None
|
||||
"""
|
||||
lesson = _make_lesson(lesson_id="L-1234", subject="Mathématiques")
|
||||
pronote_data = _make_pronote_data(lessons=[lesson])
|
||||
|
||||
# Créer un VEVENT distant avec un sujet différent
|
||||
remote_event = _make_vevent(
|
||||
uid="L-1234",
|
||||
summary="Physique", # Différent
|
||||
dtstart=datetime(2026, 1, 15, 8, 0),
|
||||
dtend=datetime(2026, 1, 15, 9, 0),
|
||||
)
|
||||
remote_managed: list[tuple[str, Event]] = [("L-1234", remote_event)]
|
||||
|
||||
plan = compute_plan(pronote_data, remote_managed)
|
||||
|
||||
assert len(plan.lessons_to_add) == 0
|
||||
assert len(plan.lessons_to_update) == 1
|
||||
assert plan.lessons_to_update[0].id == "L-1234"
|
||||
assert len(plan.lessons_to_remove) == 0
|
||||
|
||||
|
||||
def test_lesson_idempotent_when_signature_same() -> None:
|
||||
"""Vérifie qu'un cours présent à distance avec la même signature n'apparaît dans aucune liste.
|
||||
|
||||
:return: None
|
||||
"""
|
||||
lesson = _make_lesson(lesson_id="L-1234", subject="Mathématiques")
|
||||
pronote_data = _make_pronote_data(lessons=[lesson])
|
||||
|
||||
# Créer un VEVENT distant avec les mêmes propriétés
|
||||
remote_event = lesson_to_vevent(lesson)
|
||||
remote_managed: list[tuple[str, Event]] = [("L-1234", remote_event)]
|
||||
|
||||
plan = compute_plan(pronote_data, remote_managed)
|
||||
|
||||
assert len(plan.lessons_to_add) == 0
|
||||
assert len(plan.lessons_to_update) == 0
|
||||
assert len(plan.lessons_to_remove) == 0
|
||||
|
||||
|
||||
def test_lesson_remove_when_not_in_local() -> None:
|
||||
"""Vérifie qu'un UID distant non présent en local va dans lessons_to_remove.
|
||||
|
||||
:return: None
|
||||
"""
|
||||
pronote_data = _make_pronote_data(lessons=[])
|
||||
|
||||
remote_event = _make_vevent(
|
||||
uid="L-9999",
|
||||
summary="Ancien cours",
|
||||
dtstart=datetime(2026, 1, 15, 8, 0),
|
||||
dtend=datetime(2026, 1, 15, 9, 0),
|
||||
)
|
||||
remote_managed: list[tuple[str, Event]] = [("L-9999", remote_event)]
|
||||
|
||||
plan = compute_plan(pronote_data, remote_managed)
|
||||
|
||||
assert len(plan.lessons_to_add) == 0
|
||||
assert len(plan.lessons_to_update) == 0
|
||||
assert len(plan.lessons_to_remove) == 1
|
||||
assert plan.lessons_to_remove[0] == "L-9999"
|
||||
|
||||
|
||||
def test_lesson_cancelled_preserved() -> None:
|
||||
"""Vérifie qu'un cours annulé est traité normalement (ajout/mise à jour).
|
||||
|
||||
:return: None
|
||||
"""
|
||||
lesson = _make_lesson(lesson_id="L-1234", status=LessonStatus.CANCELLED)
|
||||
pronote_data = _make_pronote_data(lessons=[lesson])
|
||||
remote_managed: list[tuple[str, Event]] = []
|
||||
|
||||
plan = compute_plan(pronote_data, remote_managed)
|
||||
|
||||
# Un cours annulé doit aller dans lessons_to_add comme n'importe quel autre cours
|
||||
assert len(plan.lessons_to_add) == 1
|
||||
assert plan.lessons_to_add[0].id == "L-1234"
|
||||
assert plan.lessons_to_add[0].status == LessonStatus.CANCELLED
|
||||
|
||||
|
||||
# --- Tests for homeworks ---
|
||||
|
||||
|
||||
def test_homework_add_when_not_in_remote() -> None:
|
||||
"""Vérifie qu'un devoir non présent à distance va dans homeworks_to_add.
|
||||
|
||||
:return: None
|
||||
"""
|
||||
homework = _make_homework(homework_id="HW-5678")
|
||||
pronote_data = _make_pronote_data(homeworks=[homework])
|
||||
remote_managed: list[tuple[str, Event]] = []
|
||||
|
||||
plan = compute_plan(pronote_data, remote_managed)
|
||||
|
||||
assert len(plan.homeworks_to_add) == 1
|
||||
assert plan.homeworks_to_add[0].id == "HW-5678"
|
||||
assert len(plan.homeworks_to_update) == 0
|
||||
assert len(plan.homeworks_to_remove) == 0
|
||||
|
||||
|
||||
def test_homework_update_when_signature_differs() -> None:
|
||||
"""Vérifie qu'un devoir présent à distance avec une signature différente va dans homeworks_to_update.
|
||||
|
||||
:return: None
|
||||
"""
|
||||
homework = _make_homework(homework_id="HW-5678", subject="Mathématiques")
|
||||
pronote_data = _make_pronote_data(homeworks=[homework])
|
||||
|
||||
# Créer un VEVENT distant avec un sujet différent
|
||||
remote_event = _make_vevent(
|
||||
uid="homework-HW-5678",
|
||||
summary="Devoir: Physique", # Différent
|
||||
dtstart=datetime(2026, 1, 20, 8, 0),
|
||||
dtend=datetime(2026, 1, 20, 18, 0),
|
||||
)
|
||||
remote_managed: list[tuple[str, Event]] = [("homework-HW-5678", remote_event)]
|
||||
|
||||
plan = compute_plan(pronote_data, remote_managed)
|
||||
|
||||
assert len(plan.homeworks_to_add) == 0
|
||||
assert len(plan.homeworks_to_update) == 1
|
||||
assert plan.homeworks_to_update[0].id == "HW-5678"
|
||||
assert len(plan.homeworks_to_remove) == 0
|
||||
|
||||
|
||||
def test_homework_remove_when_not_in_local() -> None:
|
||||
"""Vérifie qu'un UID de devoir distant non présent en local va dans homeworks_to_remove.
|
||||
|
||||
:return: None
|
||||
"""
|
||||
pronote_data = _make_pronote_data(homeworks=[])
|
||||
|
||||
remote_event = _make_vevent(
|
||||
uid="homework-HW-9999",
|
||||
summary="Devoir: Ancien devoir",
|
||||
dtstart=datetime(2026, 1, 20, 8, 0),
|
||||
dtend=datetime(2026, 1, 20, 18, 0),
|
||||
)
|
||||
remote_managed: list[tuple[str, Event]] = [("homework-HW-9999", remote_event)]
|
||||
|
||||
plan = compute_plan(pronote_data, remote_managed)
|
||||
|
||||
assert len(plan.homeworks_to_add) == 0
|
||||
assert len(plan.homeworks_to_update) == 0
|
||||
assert len(plan.homeworks_to_remove) == 1
|
||||
assert plan.homeworks_to_remove[0] == "homework-HW-9999"
|
||||
|
||||
|
||||
# --- Tests for school events ---
|
||||
|
||||
|
||||
def test_school_event_add_when_not_in_remote() -> None:
|
||||
"""Vérifie qu'un événement scolaire non présent à distance va dans school_events_to_add.
|
||||
|
||||
:return: None
|
||||
"""
|
||||
school_event = _make_school_event(
|
||||
label="Vacances de Noël",
|
||||
from_date=date(2026, 12, 20),
|
||||
)
|
||||
pronote_data = _make_pronote_data(school_events=[school_event])
|
||||
remote_managed: list[tuple[str, Event]] = []
|
||||
|
||||
plan = compute_plan(pronote_data, remote_managed)
|
||||
|
||||
assert len(plan.school_events_to_add) == 1
|
||||
assert plan.school_events_to_add[0].label == "Vacances de Noël"
|
||||
assert len(plan.school_events_to_update) == 0
|
||||
assert len(plan.school_events_to_remove) == 0
|
||||
|
||||
|
||||
def test_school_event_update_when_signature_differs() -> None:
|
||||
"""Vérifie qu'un événement scolaire présent à distance avec une signature différente
|
||||
va dans school_events_to_update.
|
||||
|
||||
:return: None
|
||||
"""
|
||||
school_event = _make_school_event(
|
||||
label="Vacances de Noël",
|
||||
from_date=date(2026, 12, 20),
|
||||
to_date=date(2027, 1, 5),
|
||||
kind=SchoolEventKind.HOLIDAY,
|
||||
)
|
||||
pronote_data = _make_pronote_data(school_events=[school_event])
|
||||
|
||||
# Créer un VEVENT distant avec le même UID mais un libellé différent
|
||||
# L'UID doit correspondre à celui généré par school_event_to_vevent
|
||||
remote_event = _make_vevent(
|
||||
uid="school-event-Vacances de Noël-2026-12-20",
|
||||
summary="Vacances d'hiver", # Différent du local "Vacances de Noël"
|
||||
dtstart=datetime(2026, 12, 20, 0, 0),
|
||||
dtend=datetime(2027, 1, 5, 0, 0),
|
||||
)
|
||||
remote_managed: list[tuple[str, Event]] = [
|
||||
("school-event-Vacances de Noël-2026-12-20", remote_event)
|
||||
]
|
||||
|
||||
plan = compute_plan(pronote_data, remote_managed)
|
||||
|
||||
assert len(plan.school_events_to_add) == 0
|
||||
assert len(plan.school_events_to_update) == 1
|
||||
assert len(plan.school_events_to_remove) == 0
|
||||
|
||||
|
||||
def test_school_event_remove_when_not_in_local() -> None:
|
||||
"""Vérifie qu'un UID d'événement scolaire distant non présent en local
|
||||
va dans school_events_to_remove.
|
||||
|
||||
:return: None
|
||||
"""
|
||||
pronote_data = _make_pronote_data(school_events=[])
|
||||
|
||||
remote_event = _make_vevent(
|
||||
uid="school-event-Ancien événement-2026-01-01",
|
||||
summary="Ancien événement",
|
||||
dtstart=datetime(2026, 1, 1, 0, 0),
|
||||
dtend=datetime(2026, 1, 2, 0, 0),
|
||||
)
|
||||
remote_managed: list[tuple[str, Event]] = [
|
||||
("school-event-Ancien événement-2026-01-01", remote_event)
|
||||
]
|
||||
|
||||
plan = compute_plan(pronote_data, remote_managed)
|
||||
|
||||
assert len(plan.school_events_to_add) == 0
|
||||
assert len(plan.school_events_to_update) == 0
|
||||
assert len(plan.school_events_to_remove) == 1
|
||||
assert plan.school_events_to_remove[0] == "school-event-Ancien événement-2026-01-01"
|
||||
|
||||
|
||||
# --- Tests for UID routing ---
|
||||
|
||||
|
||||
def test_uid_routing_homework_to_remove() -> None:
|
||||
"""Vérifie qu'un UID distant commençant par 'homework-' va dans homeworks_to_remove.
|
||||
|
||||
:return: None
|
||||
"""
|
||||
pronote_data = _make_pronote_data(lessons=[], homeworks=[], school_events=[])
|
||||
|
||||
remote_event = _make_vevent(
|
||||
uid="homework-HW-9999",
|
||||
summary="Devoir à supprimer",
|
||||
dtstart=datetime(2026, 1, 20, 8, 0),
|
||||
dtend=datetime(2026, 1, 20, 18, 0),
|
||||
)
|
||||
remote_managed: list[tuple[str, Event]] = [("homework-HW-9999", remote_event)]
|
||||
|
||||
plan = compute_plan(pronote_data, remote_managed)
|
||||
|
||||
# Ne doit PAS aller dans lessons_to_remove
|
||||
assert len(plan.lessons_to_remove) == 0
|
||||
assert len(plan.homeworks_to_remove) == 1
|
||||
assert plan.homeworks_to_remove[0] == "homework-HW-9999"
|
||||
|
||||
|
||||
def test_uid_routing_school_event_to_remove() -> None:
|
||||
"""Vérifie qu'un UID distant commençant par 'school-event-' va dans school_events_to_remove.
|
||||
|
||||
:return: None
|
||||
"""
|
||||
pronote_data = _make_pronote_data(lessons=[], homeworks=[], school_events=[])
|
||||
|
||||
remote_event = _make_vevent(
|
||||
uid="school-event-Vacances-2026-12-20",
|
||||
summary="Événement à supprimer",
|
||||
dtstart=datetime(2026, 12, 20, 0, 0),
|
||||
dtend=datetime(2027, 1, 5, 0, 0),
|
||||
)
|
||||
remote_managed: list[tuple[str, Event]] = [("school-event-Vacances-2026-12-20", remote_event)]
|
||||
|
||||
plan = compute_plan(pronote_data, remote_managed)
|
||||
|
||||
# Ne doit PAS aller dans lessons_to_remove
|
||||
assert len(plan.lessons_to_remove) == 0
|
||||
assert len(plan.school_events_to_remove) == 1
|
||||
assert plan.school_events_to_remove[0] == "school-event-Vacances-2026-12-20"
|
||||
|
||||
|
||||
# --- Tests for empty inputs ---
|
||||
|
||||
|
||||
def test_empty_inputs_empty_plan() -> None:
|
||||
"""Vérifie que des entrées vides produisent un plan vide.
|
||||
|
||||
:return: None
|
||||
"""
|
||||
pronote_data = _make_pronote_data(lessons=[], homeworks=[], school_events=[])
|
||||
remote_managed: list[tuple[str, Event]] = []
|
||||
|
||||
plan = compute_plan(pronote_data, remote_managed)
|
||||
|
||||
assert len(plan.lessons_to_add) == 0
|
||||
assert len(plan.lessons_to_update) == 0
|
||||
assert len(plan.lessons_to_remove) == 0
|
||||
assert len(plan.homeworks_to_add) == 0
|
||||
assert len(plan.homeworks_to_update) == 0
|
||||
assert len(plan.homeworks_to_remove) == 0
|
||||
assert len(plan.school_events_to_add) == 0
|
||||
assert len(plan.school_events_to_update) == 0
|
||||
assert len(plan.school_events_to_remove) == 0
|
||||
|
||||
|
||||
# --- Tests for mixed scenarios ---
|
||||
|
||||
|
||||
def test_mixed_scenario() -> None:
|
||||
"""Vérifie un scénario mixte avec ajouts, mises à jour et suppressions.
|
||||
|
||||
:return: None
|
||||
"""
|
||||
# Données locales
|
||||
lesson1 = _make_lesson(lesson_id="L-0001") # Nouveau
|
||||
lesson2 = _make_lesson(lesson_id="L-0002", subject="Mathématiques") # À mettre à jour
|
||||
homework1 = _make_homework(homework_id="HW-0001") # Nouveau
|
||||
|
||||
pronote_data = _make_pronote_data(
|
||||
lessons=[lesson1, lesson2],
|
||||
homeworks=[homework1],
|
||||
school_events=[],
|
||||
)
|
||||
|
||||
# Événements distants
|
||||
# L-0002 existe mais avec un sujet différent
|
||||
remote_lesson2 = _make_vevent(
|
||||
uid="L-0002",
|
||||
summary="Physique",
|
||||
dtstart=datetime(2026, 1, 15, 8, 0),
|
||||
dtend=datetime(2026, 1, 15, 9, 0),
|
||||
)
|
||||
# L-0003 n'existe plus localement
|
||||
remote_lesson3 = _make_vevent(
|
||||
uid="L-0003",
|
||||
summary="Ancien cours",
|
||||
dtstart=datetime(2026, 1, 15, 8, 0),
|
||||
dtend=datetime(2026, 1, 15, 9, 0),
|
||||
)
|
||||
|
||||
remote_managed: list[tuple[str, Event]] = [
|
||||
("L-0002", remote_lesson2),
|
||||
("L-0003", remote_lesson3),
|
||||
]
|
||||
|
||||
plan = compute_plan(pronote_data, remote_managed)
|
||||
|
||||
# Ajouts
|
||||
assert len(plan.lessons_to_add) == 1
|
||||
assert plan.lessons_to_add[0].id == "L-0001"
|
||||
assert len(plan.homeworks_to_add) == 1
|
||||
assert plan.homeworks_to_add[0].id == "HW-0001"
|
||||
|
||||
# Mises à jour
|
||||
assert len(plan.lessons_to_update) == 1
|
||||
assert plan.lessons_to_update[0].id == "L-0002"
|
||||
|
||||
# Suppressions
|
||||
assert len(plan.lessons_to_remove) == 1
|
||||
assert plan.lessons_to_remove[0] == "L-0003"
|
||||
|
||||
|
||||
def test_unmanaged_events_not_in_remote_managed() -> None:
|
||||
"""Vérifie que remote_managed ne contient que des événements gérés.
|
||||
|
||||
Le contrat indique que remote_managed ne contient déjà que des événements
|
||||
marqués comme gérés. Le planner ne doit pas filtrer.
|
||||
|
||||
:return: None
|
||||
"""
|
||||
lesson = _make_lesson(lesson_id="L-1234")
|
||||
pronote_data = _make_pronote_data(lessons=[lesson])
|
||||
|
||||
# remote_managed ne contient que des événements gérés (par hypothèse)
|
||||
# Donc pas besoin de tester le filtrage ici - c'est la responsabilité de list_managed_events
|
||||
remote_managed: list[tuple[str, Event]] = []
|
||||
|
||||
plan = compute_plan(pronote_data, remote_managed)
|
||||
|
||||
# Le cours doit être dans lessons_to_add
|
||||
assert len(plan.lessons_to_add) == 1
|
||||
|
||||
|
||||
# Ensure trailing newline
|
||||
Reference in New Issue
Block a user