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:
2026-09-07 12:24:22 +02:00
parent b4b0247919
commit a1bae41be8
14 changed files with 735 additions and 298 deletions

View File

@@ -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

View File

@@ -7,6 +7,7 @@ avec une attention particulière à la sécurité (masquage des secrets).
from __future__ import annotations
import logging
from datetime import datetime
from typing import TYPE_CHECKING
from unittest.mock import MagicMock
@@ -22,7 +23,7 @@ from pronote_sync.sync.caldav import CalDAVGateway
from pronote_sync.sync.serialization import MANAGED_PROPERTY, MANAGED_VALUE
if TYPE_CHECKING:
pass
from _pytest.logging import LogCaptureFixture
# --- Fixtures ---
@@ -312,7 +313,7 @@ def test_list_managed_events_returns_only_managed(
mock_calendar = mock_client_factory.return_value.principal.return_value.calendars.return_value[
0
]
mock_calendar.date_search.return_value = [mock_managed_event, mock_unmanaged_event]
mock_calendar.search.return_value = [mock_managed_event, mock_unmanaged_event]
start = datetime(2026, 1, 1)
end = datetime(2026, 12, 31)
@@ -320,8 +321,9 @@ def test_list_managed_events_returns_only_managed(
# Seuls les événements gérés doivent être retournés
assert len(result) == 1
uid, vevent = result[0]
assert uid == "test-uid-123"
raw_uid, canonical_uid, vevent = result[0]
assert raw_uid == "test-uid-123"
assert canonical_uid == "test-uid-123" # Pas de suffixe Pronote : canonique == brut
assert str(vevent.get("UID")) == "test-uid-123"
@@ -359,7 +361,7 @@ def test_list_managed_events_caldav_error_raises(
mock_calendar = mock_client_factory.return_value.principal.return_value.calendars.return_value[
0
]
mock_calendar.date_search.side_effect = Exception("CalDAV error")
mock_calendar.search.side_effect = Exception("CalDAV error")
with pytest.raises(PronoteSyncError) as exc_info:
gateway.list_managed_events(datetime(2026, 1, 1), datetime(2026, 12, 31))
@@ -402,11 +404,13 @@ def test_upsert_event_creates_new_event_when_uid_missing(
def test_upsert_event_updates_existing_event_by_uid(
caldav_settings: CalDAVSettings,
mock_client_factory: MagicMock,
mock_managed_event: MagicMock,
) -> None:
"""Vérifie que upsert_event remplace le contenu d'un événement existant.
"""Vérifie que upsert_event remplace le contenu d'un événement géré existant.
:param caldav_settings: Paramètres CalDAV valides.
:param mock_client_factory: Usine de clients mockée.
:param mock_managed_event: Événement géré mocké.
:return: None
"""
gateway = CalDAVGateway(caldav_settings, client_factory=mock_client_factory)
@@ -415,16 +419,15 @@ def test_upsert_event_updates_existing_event_by_uid(
mock_calendar = mock_client_factory.return_value.principal.return_value.calendars.return_value[
0
]
mock_event = MagicMock()
mock_calendar.get_event_by_uid.return_value = mock_event
mock_calendar.get_event_by_uid.return_value = mock_managed_event
vcalendar_text = "BEGIN:VCALENDAR\nBEGIN:VEVENT\nUID:test-123\nEND:VEVENT\nEND:VCALENDAR"
gateway.upsert_event(vcalendar_text, "test-123")
mock_calendar.get_event_by_uid.assert_called_once_with("test-123")
assert mock_event.data == vcalendar_text
mock_event.save.assert_called_once()
assert mock_managed_event.data == vcalendar_text
mock_managed_event.save.assert_called_once()
mock_calendar.add_event.assert_not_called()
@@ -474,30 +477,66 @@ def test_upsert_event_caldav_error_raises(
# --- delete_event tests ---
def test_delete_event_calls_calendar(
def test_upsert_event_refuses_unmanaged_event(
caldav_settings: CalDAVSettings,
mock_client_factory: MagicMock,
mock_unmanaged_event: MagicMock,
) -> None:
"""Vérifie que delete_event appelle event.delete() sur l'événement trouvé.
"""Vérifie que upsert_event refuse de modifier un événement non géré.
Un événement distant existant sans le marqueur de gestion ne doit jamais
être écrasé : la méthode lève PronoteSyncError avec un message « Conflit »
et n'appelle pas ``add_event``.
:param caldav_settings: Paramètres CalDAV valides.
:param mock_client_factory: Usine de clients mockée.
:param mock_unmanaged_event: Événement non géré mocké.
:return: None
"""
gateway = CalDAVGateway(caldav_settings, client_factory=mock_client_factory)
gateway.connect()
# Configurer le calendrier pour retourner un événement mocké
mock_calendar = mock_client_factory.return_value.principal.return_value.calendars.return_value[
0
]
mock_event = MagicMock()
mock_calendar.get_event_by_uid.return_value = mock_event
mock_calendar.get_event_by_uid.return_value = mock_unmanaged_event
vcalendar_text = "BEGIN:VCALENDAR\nBEGIN:VEVENT\nUID:test-123\nEND:VEVENT\nEND:VCALENDAR"
with pytest.raises(PronoteSyncError) as exc_info:
gateway.upsert_event(vcalendar_text, "test-123")
assert "Conflit" in str(exc_info.value)
mock_calendar.get_event_by_uid.assert_called_once_with("test-123")
mock_calendar.add_event.assert_not_called()
mock_unmanaged_event.save.assert_not_called()
def test_delete_event_calls_calendar(
caldav_settings: CalDAVSettings,
mock_client_factory: MagicMock,
mock_managed_event: MagicMock,
) -> None:
"""Vérifie que delete_event appelle event.delete() sur un événement géré.
:param caldav_settings: Paramètres CalDAV valides.
:param mock_client_factory: Usine de clients mockée.
:param mock_managed_event: Événement géré mocké.
:return: None
"""
gateway = CalDAVGateway(caldav_settings, client_factory=mock_client_factory)
gateway.connect()
# Configurer le calendrier pour retourner un événement géré mocké
mock_calendar = mock_client_factory.return_value.principal.return_value.calendars.return_value[
0
]
mock_calendar.get_event_by_uid.return_value = mock_managed_event
gateway.delete_event("test-uid-123")
mock_calendar.get_event_by_uid.assert_called_once_with("test-uid-123")
mock_event.delete.assert_called_once()
mock_managed_event.delete.assert_called_once()
def test_delete_event_not_connected_raises(
@@ -543,6 +582,66 @@ def test_delete_event_caldav_error_raises(
assert exc_info.value.__context__ is None
def test_delete_event_refuses_unmanaged_event(
caldav_settings: CalDAVSettings,
mock_client_factory: MagicMock,
mock_unmanaged_event: MagicMock,
caplog: LogCaptureFixture,
) -> None:
"""Vérifie que delete_event refuse de supprimer un événement non géré.
L'événement distant sans le marqueur de gestion ne doit jamais être
supprimé : la méthode retourne sans erreur, journalise un avertissement
et n'appelle pas ``event.delete()``.
:param caldav_settings: Paramètres CalDAV valides.
:param mock_client_factory: Usine de clients mockée.
:param mock_unmanaged_event: Événement non géré mocké.
:param caplog: Capture des journaux pytest.
:return: None
"""
gateway = CalDAVGateway(caldav_settings, client_factory=mock_client_factory)
gateway.connect()
mock_calendar = mock_client_factory.return_value.principal.return_value.calendars.return_value[
0
]
mock_calendar.get_event_by_uid.return_value = mock_unmanaged_event
with caplog.at_level(logging.WARNING):
gateway.delete_event("test-uid-123")
mock_calendar.get_event_by_uid.assert_called_once_with("test-uid-123")
mock_unmanaged_event.delete.assert_not_called()
assert any("Suppression refusée" in record.getMessage() for record in caplog.records)
def test_delete_event_idempotent_when_not_found(
caldav_settings: CalDAVSettings,
mock_client_factory: MagicMock,
) -> None:
"""Vérifie que delete_event est idempotent quand l'UID est introuvable.
Une suppression d'un événement déjà absent est un succès silencieux :
aucune exception n'est levée.
:param caldav_settings: Paramètres CalDAV valides.
:param mock_client_factory: Usine de clients mockée.
:return: None
"""
gateway = CalDAVGateway(caldav_settings, client_factory=mock_client_factory)
gateway.connect()
mock_calendar = mock_client_factory.return_value.principal.return_value.calendars.return_value[
0
]
mock_calendar.get_event_by_uid.side_effect = NotFoundError("Event not found")
gateway.delete_event("test-uid-123")
mock_calendar.get_event_by_uid.assert_called_once_with("test-uid-123")
# --- Context manager tests ---

View File

@@ -183,9 +183,9 @@ def test_lesson_add_when_not_in_remote() -> None:
"""
lesson = _make_lesson(lesson_id="L-1234")
pronote_data = _make_pronote_data(lessons=[lesson])
remote_managed: list[tuple[str, Event]] = []
remote_managed: list[tuple[str, str, Event]] = []
plan = compute_plan(pronote_data, remote_managed)
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"
@@ -208,9 +208,9 @@ def test_lesson_update_when_signature_differs() -> None:
dtstart=datetime(2026, 1, 15, 8, 0),
dtend=datetime(2026, 1, 15, 9, 0),
)
remote_managed: list[tuple[str, Event]] = [("L-1234", remote_event)]
remote_managed: list[tuple[str, str, Event]] = [("L-1234", "L-1234", remote_event)]
plan = compute_plan(pronote_data, remote_managed)
plan, raw_mapping = compute_plan(pronote_data, remote_managed)
assert len(plan.lessons_to_add) == 0
assert len(plan.lessons_to_update) == 1
@@ -228,9 +228,9 @@ def test_lesson_idempotent_when_signature_same() -> None:
# 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)]
remote_managed: list[tuple[str, str, Event]] = [("L-1234", "L-1234", remote_event)]
plan = compute_plan(pronote_data, remote_managed)
plan, raw_mapping = compute_plan(pronote_data, remote_managed)
assert len(plan.lessons_to_add) == 0
assert len(plan.lessons_to_update) == 0
@@ -250,9 +250,9 @@ def test_lesson_remove_when_not_in_local() -> None:
dtstart=datetime(2026, 1, 15, 8, 0),
dtend=datetime(2026, 1, 15, 9, 0),
)
remote_managed: list[tuple[str, Event]] = [("L-9999", remote_event)]
remote_managed: list[tuple[str, str, Event]] = [("L-9999", "L-9999", remote_event)]
plan = compute_plan(pronote_data, remote_managed)
plan, raw_mapping = compute_plan(pronote_data, remote_managed)
assert len(plan.lessons_to_add) == 0
assert len(plan.lessons_to_update) == 0
@@ -260,6 +260,62 @@ def test_lesson_remove_when_not_in_local() -> None:
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).
@@ -267,9 +323,9 @@ def test_lesson_cancelled_preserved() -> None:
"""
lesson = _make_lesson(lesson_id="L-1234", status=LessonStatus.CANCELLED)
pronote_data = _make_pronote_data(lessons=[lesson])
remote_managed: list[tuple[str, Event]] = []
remote_managed: list[tuple[str, str, Event]] = []
plan = compute_plan(pronote_data, remote_managed)
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
@@ -287,9 +343,9 @@ def test_homework_add_when_not_in_remote() -> None:
"""
homework = _make_homework(homework_id="HW-5678")
pronote_data = _make_pronote_data(homeworks=[homework])
remote_managed: list[tuple[str, Event]] = []
remote_managed: list[tuple[str, str, Event]] = []
plan = compute_plan(pronote_data, remote_managed)
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"
@@ -312,9 +368,11 @@ def test_homework_update_when_signature_differs() -> None:
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)]
remote_managed: list[tuple[str, str, Event]] = [
("homework-HW-5678", "homework-HW-5678", remote_event)
]
plan = compute_plan(pronote_data, remote_managed)
plan, raw_mapping = compute_plan(pronote_data, remote_managed)
assert len(plan.homeworks_to_add) == 0
assert len(plan.homeworks_to_update) == 1
@@ -335,9 +393,11 @@ def test_homework_remove_when_not_in_local() -> None:
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)]
remote_managed: list[tuple[str, str, Event]] = [
("homework-HW-9999", "homework-HW-9999", remote_event)
]
plan = compute_plan(pronote_data, remote_managed)
plan, raw_mapping = compute_plan(pronote_data, remote_managed)
assert len(plan.homeworks_to_add) == 0
assert len(plan.homeworks_to_update) == 0
@@ -358,9 +418,9 @@ def test_school_event_add_when_not_in_remote() -> None:
from_date=date(2026, 12, 20),
)
pronote_data = _make_pronote_data(school_events=[school_event])
remote_managed: list[tuple[str, Event]] = []
remote_managed: list[tuple[str, str, Event]] = []
plan = compute_plan(pronote_data, remote_managed)
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"
@@ -390,11 +450,15 @@ def test_school_event_update_when_signature_differs() -> None:
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)
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 = compute_plan(pronote_data, remote_managed)
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
@@ -415,11 +479,15 @@ def test_school_event_remove_when_not_in_local() -> None:
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)
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 = compute_plan(pronote_data, remote_managed)
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
@@ -443,9 +511,11 @@ def test_uid_routing_homework_to_remove() -> None:
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)]
remote_managed: list[tuple[str, str, Event]] = [
("homework-HW-9999", "homework-HW-9999", remote_event)
]
plan = compute_plan(pronote_data, remote_managed)
plan, raw_mapping = compute_plan(pronote_data, remote_managed)
# Ne doit PAS aller dans lessons_to_remove
assert len(plan.lessons_to_remove) == 0
@@ -466,9 +536,11 @@ def test_uid_routing_school_event_to_remove() -> None:
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)]
remote_managed: list[tuple[str, str, Event]] = [
("school-event-Vacances-2026-12-20", "school-event-Vacances-2026-12-20", remote_event)
]
plan = compute_plan(pronote_data, remote_managed)
plan, raw_mapping = compute_plan(pronote_data, remote_managed)
# Ne doit PAS aller dans lessons_to_remove
assert len(plan.lessons_to_remove) == 0
@@ -485,9 +557,9 @@ def test_empty_inputs_empty_plan() -> None:
:return: None
"""
pronote_data = _make_pronote_data(lessons=[], homeworks=[], school_events=[])
remote_managed: list[tuple[str, Event]] = []
remote_managed: list[tuple[str, str, Event]] = []
plan = compute_plan(pronote_data, remote_managed)
plan, raw_mapping = compute_plan(pronote_data, remote_managed)
assert len(plan.lessons_to_add) == 0
assert len(plan.lessons_to_update) == 0
@@ -535,12 +607,12 @@ def test_mixed_scenario() -> None:
dtend=datetime(2026, 1, 15, 9, 0),
)
remote_managed: list[tuple[str, Event]] = [
("L-0002", remote_lesson2),
("L-0003", remote_lesson3),
remote_managed: list[tuple[str, str, Event]] = [
("L-0002", "L-0002", remote_lesson2),
("L-0003", "L-0003", remote_lesson3),
]
plan = compute_plan(pronote_data, remote_managed)
plan, raw_mapping = compute_plan(pronote_data, remote_managed)
# Ajouts
assert len(plan.lessons_to_add) == 1
@@ -570,9 +642,9 @@ def test_unmanaged_events_not_in_remote_managed() -> None:
# 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]] = []
remote_managed: list[tuple[str, str, Event]] = []
plan = compute_plan(pronote_data, remote_managed)
plan, raw_mapping = compute_plan(pronote_data, remote_managed)
# Le cours doit être dans lessons_to_add
assert len(plan.lessons_to_add) == 1

View File

@@ -257,7 +257,7 @@ class TestCalDAVGatewaySecurity:
mock_principal = MagicMock()
mock_calendar = MagicMock()
mock_calendar.url = "https://caldav.example.com/cal/"
mock_calendar.date_search.side_effect = Exception(
mock_calendar.search.side_effect = Exception(
f"Search failed at {SENTINEL_URL}?token={SENTINEL_PASSWORD}"
)
mock_principal.calendars.return_value = [mock_calendar]