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>
623 lines
19 KiB
Python
623 lines
19 KiB
Python
"""Tests unitaires pour l'exécuteur de synchronisation CalDAV.
|
|
|
|
Ce module vérifie que :class:`CalDAVSyncExecutor` applique correctement
|
|
un plan de synchronisation contre une passerelle CalDAV mockée, avec
|
|
la bonne gestion du mode dry-run, des compteurs et des erreurs.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import date, datetime
|
|
from typing import TYPE_CHECKING
|
|
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
|
|
from pronote_sync.errors import PronoteSyncError
|
|
from pronote_sync.models.agenda import Lesson, LessonStatus, SchoolEvent, SchoolEventKind
|
|
from pronote_sync.models.homework import Homework
|
|
from pronote_sync.models.sync import CalDAVSyncPlan, CalDAVSyncStatus
|
|
from pronote_sync.sync.executor import CalDAVSyncExecutor
|
|
|
|
if TYPE_CHECKING:
|
|
pass
|
|
|
|
|
|
# --- 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,
|
|
)
|
|
|
|
|
|
# --- Fixtures ---
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_gateway() -> MagicMock:
|
|
"""Fournit une passerelle CalDAV mockée.
|
|
|
|
:return: MagicMock configuré comme une CalDAVGateway.
|
|
:rtype: MagicMock
|
|
"""
|
|
gateway = MagicMock()
|
|
gateway.upsert_event = MagicMock()
|
|
gateway.delete_event = MagicMock()
|
|
return gateway
|
|
|
|
|
|
@pytest.fixture
|
|
def empty_plan() -> CalDAVSyncPlan:
|
|
"""Fournit un plan de synchronisation vide.
|
|
|
|
:return: CalDAVSyncPlan vide.
|
|
:rtype: CalDAVSyncPlan
|
|
"""
|
|
return CalDAVSyncPlan()
|
|
|
|
|
|
@pytest.fixture
|
|
def plan_with_lesson_add() -> CalDAVSyncPlan:
|
|
"""Fournit un plan avec un cours à ajouter.
|
|
|
|
:return: CalDAVSyncPlan avec un cours à ajouter.
|
|
:rtype: CalDAVSyncPlan
|
|
"""
|
|
lesson = _make_lesson(lesson_id="L-1234")
|
|
return CalDAVSyncPlan(lessons_to_add=[lesson])
|
|
|
|
|
|
@pytest.fixture
|
|
def plan_with_lesson_update() -> CalDAVSyncPlan:
|
|
"""Fournit un plan avec un cours à mettre à jour.
|
|
|
|
:return: CalDAVSyncPlan avec un cours à mettre à jour.
|
|
:rtype: CalDAVSyncPlan
|
|
"""
|
|
lesson = _make_lesson(lesson_id="L-1234")
|
|
return CalDAVSyncPlan(lessons_to_update=[lesson])
|
|
|
|
|
|
@pytest.fixture
|
|
def plan_with_lesson_remove() -> CalDAVSyncPlan:
|
|
"""Fournit un plan avec un cours à supprimer.
|
|
|
|
:return: CalDAVSyncPlan avec un cours à supprimer.
|
|
:rtype: CalDAVSyncPlan
|
|
"""
|
|
return CalDAVSyncPlan(lessons_to_remove=["L-1234"])
|
|
|
|
|
|
@pytest.fixture
|
|
def plan_with_homework_add() -> CalDAVSyncPlan:
|
|
"""Fournit un plan avec un devoir à ajouter.
|
|
|
|
:return: CalDAVSyncPlan avec un devoir à ajouter.
|
|
:rtype: CalDAVSyncPlan
|
|
"""
|
|
homework = _make_homework(homework_id="HW-5678")
|
|
return CalDAVSyncPlan(homeworks_to_add=[homework])
|
|
|
|
|
|
@pytest.fixture
|
|
def plan_with_homework_remove() -> CalDAVSyncPlan:
|
|
"""Fournit un plan avec un devoir à supprimer.
|
|
|
|
:return: CalDAVSyncPlan avec un devoir à supprimer.
|
|
:rtype: CalDAVSyncPlan
|
|
"""
|
|
return CalDAVSyncPlan(homeworks_to_remove=["homework-HW-5678"])
|
|
|
|
|
|
@pytest.fixture
|
|
def plan_with_school_event_add() -> CalDAVSyncPlan:
|
|
"""Fournit un plan avec un événement scolaire à ajouter.
|
|
|
|
:return: CalDAVSyncPlan avec un événement scolaire à ajouter.
|
|
:rtype: CalDAVSyncPlan
|
|
"""
|
|
school_event = _make_school_event()
|
|
return CalDAVSyncPlan(school_events_to_add=[school_event])
|
|
|
|
|
|
@pytest.fixture
|
|
def plan_with_school_event_remove() -> CalDAVSyncPlan:
|
|
"""Fournit un plan avec un événement scolaire à supprimer.
|
|
|
|
:return: CalDAVSyncPlan avec un événement scolaire à supprimer.
|
|
:rtype: CalDAVSyncPlan
|
|
"""
|
|
return CalDAVSyncPlan(school_events_to_remove=["school-event-Vacances-2026-12-20"])
|
|
|
|
|
|
# --- Dry-run tests ---
|
|
|
|
|
|
def test_dry_run_add_does_not_call_gateway(
|
|
mock_gateway: MagicMock, plan_with_lesson_add: CalDAVSyncPlan
|
|
) -> None:
|
|
"""Vérifie que dry_run=True n'appelle pas gateway.upsert_event pour un ajout.
|
|
|
|
:param mock_gateway: Passerelle mockée.
|
|
:param plan_with_lesson_add: Plan avec un cours à ajouter.
|
|
:return: None
|
|
"""
|
|
executor = CalDAVSyncExecutor(mock_gateway, dry_run=True)
|
|
result = executor.execute(plan_with_lesson_add)
|
|
|
|
mock_gateway.upsert_event.assert_not_called()
|
|
assert result.added == 1
|
|
assert result.updated == 0
|
|
assert result.removed == 0
|
|
|
|
|
|
def test_dry_run_update_does_not_call_gateway(
|
|
mock_gateway: MagicMock, plan_with_lesson_update: CalDAVSyncPlan
|
|
) -> None:
|
|
"""Vérifie que dry_run=True n'appelle pas gateway.upsert_event pour une mise à jour.
|
|
|
|
: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=True)
|
|
result = executor.execute(plan_with_lesson_update)
|
|
|
|
mock_gateway.upsert_event.assert_not_called()
|
|
assert result.added == 0
|
|
assert result.updated == 1
|
|
assert result.removed == 0
|
|
|
|
|
|
def test_dry_run_delete_does_not_call_gateway(
|
|
mock_gateway: MagicMock, plan_with_lesson_remove: CalDAVSyncPlan
|
|
) -> None:
|
|
"""Vérifie que dry_run=True n'appelle pas gateway.delete_event.
|
|
|
|
:param mock_gateway: Passerelle mockée.
|
|
:param plan_with_lesson_remove: Plan avec un cours à supprimer.
|
|
:return: None
|
|
"""
|
|
executor = CalDAVSyncExecutor(mock_gateway, dry_run=True)
|
|
result = executor.execute(plan_with_lesson_remove)
|
|
|
|
mock_gateway.delete_event.assert_not_called()
|
|
assert result.added == 0
|
|
assert result.updated == 0
|
|
assert result.removed == 1
|
|
|
|
|
|
def test_dry_run_all_operations(
|
|
mock_gateway: MagicMock,
|
|
) -> None:
|
|
"""Vérifie que dry_run=True fonctionne pour toutes les catégories.
|
|
|
|
:param mock_gateway: Passerelle mockée.
|
|
:return: None
|
|
"""
|
|
plan = CalDAVSyncPlan(
|
|
lessons_to_add=[_make_lesson(lesson_id="L-0001")],
|
|
lessons_to_update=[_make_lesson(lesson_id="L-0002")],
|
|
lessons_to_remove=["L-0003"],
|
|
homeworks_to_add=[_make_homework(homework_id="HW-0001")],
|
|
homeworks_to_update=[_make_homework(homework_id="HW-0002")],
|
|
homeworks_to_remove=["homework-HW-0003"],
|
|
school_events_to_add=[_make_school_event()],
|
|
school_events_to_update=[_make_school_event(label="Événement 2")],
|
|
school_events_to_remove=["school-event-Événement-2026-01-01"],
|
|
)
|
|
|
|
executor = CalDAVSyncExecutor(mock_gateway, dry_run=True)
|
|
result = executor.execute(plan)
|
|
|
|
mock_gateway.upsert_event.assert_not_called()
|
|
mock_gateway.delete_event.assert_not_called()
|
|
|
|
assert result.added == 3 # 1 lesson + 1 homework + 1 school event
|
|
assert result.updated == 3 # 1 lesson + 1 homework + 1 school event
|
|
assert result.removed == 3 # 1 lesson + 1 homework + 1 school event
|
|
|
|
|
|
# --- Real execution tests ---
|
|
|
|
|
|
def test_real_add_calls_gateway(
|
|
mock_gateway: MagicMock, plan_with_lesson_add: CalDAVSyncPlan
|
|
) -> None:
|
|
"""Vérifie que dry_run=False appelle gateway.upsert_event pour un ajout.
|
|
|
|
:param mock_gateway: Passerelle mockée.
|
|
:param plan_with_lesson_add: Plan avec un cours à ajouter.
|
|
:return: None
|
|
"""
|
|
executor = CalDAVSyncExecutor(mock_gateway, dry_run=False)
|
|
result = executor.execute(plan_with_lesson_add)
|
|
|
|
mock_gateway.upsert_event.assert_called_once()
|
|
# Vérifier que les arguments sont (texte iCalendar, uid)
|
|
call_args = mock_gateway.upsert_event.call_args
|
|
assert isinstance(call_args[0][0], str)
|
|
assert "BEGIN:VEVENT" in call_args[0][0]
|
|
assert call_args[0][1] == "L-1234"
|
|
assert result.added == 1
|
|
|
|
|
|
def test_real_update_calls_gateway(
|
|
mock_gateway: MagicMock, plan_with_lesson_update: CalDAVSyncPlan
|
|
) -> None:
|
|
"""Vérifie que dry_run=False appelle gateway.upsert_event pour une mise à jour.
|
|
|
|
: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_called_once()
|
|
assert result.updated == 1
|
|
|
|
|
|
def test_real_delete_calls_gateway(
|
|
mock_gateway: MagicMock, plan_with_lesson_remove: CalDAVSyncPlan
|
|
) -> None:
|
|
"""Vérifie que dry_run=False appelle gateway.delete_event.
|
|
|
|
:param mock_gateway: Passerelle mockée.
|
|
:param plan_with_lesson_remove: Plan avec un cours à supprimer.
|
|
:return: None
|
|
"""
|
|
executor = CalDAVSyncExecutor(mock_gateway, dry_run=False)
|
|
result = executor.execute(plan_with_lesson_remove)
|
|
|
|
mock_gateway.delete_event.assert_called_once_with("L-1234")
|
|
assert result.removed == 1
|
|
|
|
|
|
def test_real_execution_all_operations(
|
|
mock_gateway: MagicMock,
|
|
) -> None:
|
|
"""Vérifie que dry_run=False appelle la passerelle pour toutes les opérations.
|
|
|
|
:param mock_gateway: Passerelle mockée.
|
|
:return: None
|
|
"""
|
|
plan = CalDAVSyncPlan(
|
|
lessons_to_add=[_make_lesson(lesson_id="L-0001")],
|
|
lessons_to_update=[_make_lesson(lesson_id="L-0002")],
|
|
lessons_to_remove=["L-0003"],
|
|
homeworks_to_add=[_make_homework(homework_id="HW-0001")],
|
|
homeworks_to_remove=["homework-HW-0002"],
|
|
school_events_to_add=[_make_school_event()],
|
|
school_events_to_remove=["school-event-Vacances-2026-12-20"],
|
|
)
|
|
|
|
executor = CalDAVSyncExecutor(mock_gateway, dry_run=False)
|
|
result = executor.execute(plan)
|
|
|
|
# upsert_event appelé pour les ajouts et mises à jour
|
|
# 1 lesson add + 1 lesson update + 1 homework add + 1 school event add = 4
|
|
assert mock_gateway.upsert_event.call_count == 4
|
|
# delete_event appelé pour les suppressions
|
|
assert mock_gateway.delete_event.call_count == 3 # 1 lesson + 1 homework + 1 school event
|
|
|
|
assert result.added == 3 # 1 lesson + 1 homework + 1 school event
|
|
assert result.updated == 1 # 1 lesson
|
|
assert result.removed == 3 # 1 lesson + 1 homework + 1 school event
|
|
|
|
|
|
# --- Error handling tests ---
|
|
|
|
|
|
def test_error_isolation_save_failure(
|
|
mock_gateway: MagicMock,
|
|
) -> None:
|
|
"""Vérifie qu'une erreur sur upsert_event est capturée et le batch continue.
|
|
|
|
:param mock_gateway: Passerelle mockée.
|
|
:return: None
|
|
"""
|
|
lesson1 = _make_lesson(lesson_id="L-0001")
|
|
lesson2 = _make_lesson(lesson_id="L-0002")
|
|
|
|
# Configurer le mock pour lever une erreur sur le premier appel
|
|
mock_gateway.upsert_event.side_effect = [
|
|
PronoteSyncError("Échec de l'écriture"),
|
|
None, # Le deuxième appel réussit
|
|
]
|
|
|
|
plan = CalDAVSyncPlan(lessons_to_add=[lesson1, lesson2])
|
|
|
|
executor = CalDAVSyncExecutor(mock_gateway, dry_run=False)
|
|
result = executor.execute(plan)
|
|
|
|
# Les deux appels ont été tentés
|
|
assert mock_gateway.upsert_event.call_count == 2
|
|
# Une erreur a été capturée
|
|
assert len(result.errors) == 1
|
|
assert "Échec de l'écriture" in result.errors[0]
|
|
# Le compteur d'ajouts est à 1 (seul le deuxième a réussi)
|
|
assert result.added == 1
|
|
# Le statut est FAILED car il y a des erreurs
|
|
assert result.status == CalDAVSyncStatus.FAILED
|
|
|
|
|
|
def test_error_isolation_delete_failure(
|
|
mock_gateway: MagicMock,
|
|
) -> None:
|
|
"""Vérifie qu'une erreur sur delete_event est capturée et le batch continue.
|
|
|
|
:param mock_gateway: Passerelle mockée.
|
|
:return: None
|
|
"""
|
|
mock_gateway.delete_event.side_effect = PronoteSyncError("Échec de la suppression")
|
|
|
|
plan = CalDAVSyncPlan(
|
|
lessons_to_remove=["L-0001", "L-0002"],
|
|
)
|
|
|
|
executor = CalDAVSyncExecutor(mock_gateway, dry_run=False)
|
|
result = executor.execute(plan)
|
|
|
|
# Les deux appels ont été tentés
|
|
assert mock_gateway.delete_event.call_count == 2
|
|
# Deux erreurs ont été capturées
|
|
assert len(result.errors) == 2
|
|
assert all("Échec de la suppression" in err for err in result.errors)
|
|
# Aucun compteur de suppression n'a été incrémenté
|
|
assert result.removed == 0
|
|
# Le statut est FAILED
|
|
assert result.status == CalDAVSyncStatus.FAILED
|
|
|
|
|
|
def test_error_isolation_mixed_operations(
|
|
mock_gateway: MagicMock,
|
|
) -> None:
|
|
"""Vérifie que les erreurs sont isolées entre différentes opérations.
|
|
|
|
:param mock_gateway: Passerelle mockée.
|
|
:return: None
|
|
"""
|
|
lesson = _make_lesson(lesson_id="L-0001")
|
|
|
|
# upsert_event échoue, delete_event réussit
|
|
mock_gateway.upsert_event.side_effect = PronoteSyncError("Échec save")
|
|
mock_gateway.delete_event.return_value = None
|
|
|
|
plan = CalDAVSyncPlan(
|
|
lessons_to_add=[lesson],
|
|
lessons_to_remove=["L-0002"],
|
|
)
|
|
|
|
executor = CalDAVSyncExecutor(mock_gateway, dry_run=False)
|
|
result = executor.execute(plan)
|
|
|
|
assert mock_gateway.upsert_event.call_count == 1
|
|
assert mock_gateway.delete_event.call_count == 1
|
|
assert len(result.errors) == 1
|
|
assert "Échec save" in result.errors[0]
|
|
assert result.added == 0
|
|
assert result.removed == 1
|
|
assert result.status == CalDAVSyncStatus.FAILED
|
|
|
|
|
|
# --- Status tests ---
|
|
|
|
|
|
def test_status_skipped_when_no_operations(
|
|
mock_gateway: MagicMock, empty_plan: CalDAVSyncPlan
|
|
) -> None:
|
|
"""Vérifie que le statut est SKIPPED quand aucune opération n'est à effectuer.
|
|
|
|
:param mock_gateway: Passerelle mockée.
|
|
:param empty_plan: Plan vide.
|
|
:return: None
|
|
"""
|
|
executor = CalDAVSyncExecutor(mock_gateway, dry_run=False)
|
|
result = executor.execute(empty_plan)
|
|
|
|
assert result.status == CalDAVSyncStatus.SKIPPED
|
|
assert result.added == 0
|
|
assert result.updated == 0
|
|
assert result.removed == 0
|
|
|
|
|
|
def test_status_success_when_no_errors(
|
|
mock_gateway: MagicMock, plan_with_lesson_add: CalDAVSyncPlan
|
|
) -> None:
|
|
"""Vérifie que le statut est SUCCESS quand les opérations réussissent sans erreur.
|
|
|
|
:param mock_gateway: Passerelle mockée.
|
|
:param plan_with_lesson_add: Plan avec un cours à ajouter.
|
|
:return: None
|
|
"""
|
|
executor = CalDAVSyncExecutor(mock_gateway, dry_run=False)
|
|
result = executor.execute(plan_with_lesson_add)
|
|
|
|
assert result.status == CalDAVSyncStatus.SUCCESS
|
|
|
|
|
|
def test_status_failed_when_errors_present(
|
|
mock_gateway: MagicMock,
|
|
) -> None:
|
|
"""Vérifie que le statut est FAILED quand des erreurs sont présentes.
|
|
|
|
:param mock_gateway: Passerelle mockée.
|
|
:return: None
|
|
"""
|
|
lesson = _make_lesson(lesson_id="L-0001")
|
|
mock_gateway.upsert_event.side_effect = PronoteSyncError("Échec")
|
|
|
|
plan = CalDAVSyncPlan(lessons_to_add=[lesson])
|
|
|
|
executor = CalDAVSyncExecutor(mock_gateway, dry_run=False)
|
|
result = executor.execute(plan)
|
|
|
|
assert result.status == CalDAVSyncStatus.FAILED
|
|
|
|
|
|
# --- Homework and School Event tests ---
|
|
|
|
|
|
def test_homework_add_real_execution(
|
|
mock_gateway: MagicMock, plan_with_homework_add: CalDAVSyncPlan
|
|
) -> None:
|
|
"""Vérifie l'exécution réelle pour l'ajout d'un devoir.
|
|
|
|
:param mock_gateway: Passerelle mockée.
|
|
:param plan_with_homework_add: Plan avec un devoir à ajouter.
|
|
:return: None
|
|
"""
|
|
executor = CalDAVSyncExecutor(mock_gateway, dry_run=False)
|
|
result = executor.execute(plan_with_homework_add)
|
|
|
|
mock_gateway.upsert_event.assert_called_once()
|
|
call_args = mock_gateway.upsert_event.call_args
|
|
vcalendar_text = call_args[0][0]
|
|
assert "homework-HW-5678" in vcalendar_text
|
|
assert call_args[0][1] == "homework-HW-5678"
|
|
assert "Devoir: Mathématiques" in vcalendar_text
|
|
assert result.added == 1
|
|
|
|
|
|
def test_homework_delete_real_execution(
|
|
mock_gateway: MagicMock, plan_with_homework_remove: CalDAVSyncPlan
|
|
) -> None:
|
|
"""Vérifie l'exécution réelle pour la suppression d'un devoir.
|
|
|
|
:param mock_gateway: Passerelle mockée.
|
|
:param plan_with_homework_remove: Plan avec un devoir à supprimer.
|
|
:return: None
|
|
"""
|
|
executor = CalDAVSyncExecutor(mock_gateway, dry_run=False)
|
|
result = executor.execute(plan_with_homework_remove)
|
|
|
|
mock_gateway.delete_event.assert_called_once_with("homework-HW-5678")
|
|
assert result.removed == 1
|
|
|
|
|
|
def test_school_event_add_real_execution(
|
|
mock_gateway: MagicMock, plan_with_school_event_add: CalDAVSyncPlan
|
|
) -> None:
|
|
"""Vérifie l'exécution réelle pour l'ajout d'un événement scolaire.
|
|
|
|
:param mock_gateway: Passerelle mockée.
|
|
:param plan_with_school_event_add: Plan avec un événement scolaire à ajouter.
|
|
:return: None
|
|
"""
|
|
executor = CalDAVSyncExecutor(mock_gateway, dry_run=False)
|
|
result = executor.execute(plan_with_school_event_add)
|
|
|
|
mock_gateway.upsert_event.assert_called_once()
|
|
call_args = mock_gateway.upsert_event.call_args
|
|
vcalendar_text = call_args[0][0]
|
|
assert "school-event-Vacances de Noël-2026-12-20" in vcalendar_text
|
|
assert call_args[0][1] == "school-event-Vacances de Noël-2026-12-20"
|
|
assert result.added == 1
|
|
|
|
|
|
def test_school_event_delete_real_execution(
|
|
mock_gateway: MagicMock, plan_with_school_event_remove: CalDAVSyncPlan
|
|
) -> None:
|
|
"""Vérifie l'exécution réelle pour la suppression d'un événement scolaire.
|
|
|
|
:param mock_gateway: Passerelle mockée.
|
|
:param plan_with_school_event_remove: Plan avec un événement scolaire à supprimer.
|
|
:return: None
|
|
"""
|
|
executor = CalDAVSyncExecutor(mock_gateway, dry_run=False)
|
|
result = executor.execute(plan_with_school_event_remove)
|
|
|
|
mock_gateway.delete_event.assert_called_once_with("school-event-Vacances-2026-12-20")
|
|
assert result.removed == 1
|
|
|
|
|
|
# Ensure trailing newline
|