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>
654 lines
21 KiB
Python
654 lines
21 KiB
Python
"""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, str, Event]] = []
|
|
|
|
plan, raw_mapping = 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, str, Event]] = [("L-1234", "L-1234", remote_event)]
|
|
|
|
plan, raw_mapping = 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, str, Event]] = [("L-1234", "L-1234", remote_event)]
|
|
|
|
plan, raw_mapping = 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, str, Event]] = [("L-9999", "L-9999", remote_event)]
|
|
|
|
plan, raw_mapping = 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_plan_with_suffixed_remote_uid_matches_canonical() -> None:
|
|
"""Vérifie qu'un UID distant suffixé apparié par UID canonique ne produit rien.
|
|
|
|
L'événement distant porte un UID brut suffixé
|
|
(``L-1234-20260905T080000Z-Index-Education``) dont la forme canonique
|
|
(``L-1234``) correspond au cours local ; les signatures étant identiques,
|
|
le plan doit être vide — sans ajout, mise à jour ou suppression artificiels.
|
|
|
|
:return: None
|
|
"""
|
|
lesson = _make_lesson(lesson_id="L-1234", subject="Mathématiques")
|
|
pronote_data = _make_pronote_data(lessons=[lesson])
|
|
|
|
# VEVENT distant construit à partir du même cours : contenu sémantique
|
|
# identique (seul l'UID brut stocké diffère, capturé par le triplet).
|
|
remote_event = lesson_to_vevent(lesson)
|
|
remote_managed: list[tuple[str, str, Event]] = [
|
|
("L-1234-20260905T080000Z-Index-Education", "L-1234", remote_event)
|
|
]
|
|
|
|
plan, raw_mapping = 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_plan_with_suffixed_remote_uid_and_no_local_adds_to_remove() -> None:
|
|
"""Vérifie que la suppression d'un UID distant suffixé utilise l'UID brut.
|
|
|
|
Un événement distant orphelin (aucun cours local) dont l'UID brut est
|
|
suffixé doit être supprimé en ciblant l'UID brut stocké sur le serveur,
|
|
et non sa forme canonique.
|
|
|
|
:return: None
|
|
"""
|
|
pronote_data = _make_pronote_data(lessons=[])
|
|
|
|
raw_uid = "L-9999-20260101T080000Z-Index-Education"
|
|
remote_event = _make_vevent(
|
|
uid=raw_uid,
|
|
summary="Ancien cours",
|
|
dtstart=datetime(2026, 1, 15, 8, 0),
|
|
dtend=datetime(2026, 1, 15, 9, 0),
|
|
)
|
|
remote_managed: list[tuple[str, str, Event]] = [(raw_uid, "L-9999", remote_event)]
|
|
|
|
plan, raw_mapping = 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
|
|
# La liste de suppression contient l'UID brut, pas la forme canonique.
|
|
assert plan.lessons_to_remove[0] == raw_uid
|
|
|
|
|
|
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, str, Event]] = []
|
|
|
|
plan, raw_mapping = 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, str, Event]] = []
|
|
|
|
plan, raw_mapping = 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, str, Event]] = [
|
|
("homework-HW-5678", "homework-HW-5678", remote_event)
|
|
]
|
|
|
|
plan, raw_mapping = 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, str, Event]] = [
|
|
("homework-HW-9999", "homework-HW-9999", remote_event)
|
|
]
|
|
|
|
plan, raw_mapping = 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, str, Event]] = []
|
|
|
|
plan, raw_mapping = 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, str, Event]] = [
|
|
(
|
|
"school-event-Vacances de Noël-2026-12-20",
|
|
"school-event-Vacances de Noël-2026-12-20",
|
|
remote_event,
|
|
)
|
|
]
|
|
|
|
plan, raw_mapping = 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, str, Event]] = [
|
|
(
|
|
"school-event-Ancien événement-2026-01-01",
|
|
"school-event-Ancien événement-2026-01-01",
|
|
remote_event,
|
|
)
|
|
]
|
|
|
|
plan, raw_mapping = 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, str, Event]] = [
|
|
("homework-HW-9999", "homework-HW-9999", remote_event)
|
|
]
|
|
|
|
plan, raw_mapping = 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, str, Event]] = [
|
|
("school-event-Vacances-2026-12-20", "school-event-Vacances-2026-12-20", remote_event)
|
|
]
|
|
|
|
plan, raw_mapping = 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, str, Event]] = []
|
|
|
|
plan, raw_mapping = 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, str, Event]] = [
|
|
("L-0002", "L-0002", remote_lesson2),
|
|
("L-0003", "L-0003", remote_lesson3),
|
|
]
|
|
|
|
plan, raw_mapping = 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, str, Event]] = []
|
|
|
|
plan, raw_mapping = compute_plan(pronote_data, remote_managed)
|
|
|
|
# Le cours doit être dans lessons_to_add
|
|
assert len(plan.lessons_to_add) == 1
|
|
|
|
|
|
# Ensure trailing newline
|