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:
@@ -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
|
||||
|
||||
@@ -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 ---
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user