"""Tests d'invariants et de validation des modèles Pydantic. Ce module vérifie que les validateurs de modèle rejettent les données invalides conformément aux spécifications (ex: ordre des dates, plages de valeurs). """ from __future__ import annotations from datetime import date, datetime, time import pytest from pydantic import ValidationError from pronote_sync.models.agenda import ( Lesson, SchoolEvent, SchoolEventKind, TheoreticalLesson, ) from pronote_sync.models.diff import AgendaChange, AgendaChangeType from pronote_sync.models.sync import CalDAVSyncResult, CalDAVSyncStatus class TestTheoreticalLessonDayOfWeek: """Tests de validation du champ day_of_week pour TheoreticalLesson.""" def test_day_of_week_7_invalid(self) -> None: """Vérifie que day_of_week=7 lève une ValidationError.""" with pytest.raises(ValidationError): TheoreticalLesson( id="test", day_of_week=7, start_time=time(8, 0, 0), end_time=time(9, 30, 0), subject="Mathématiques", ) def test_day_of_week_minus_1_invalid(self) -> None: """Vérifie que day_of_week=-1 lève une ValidationError.""" with pytest.raises(ValidationError) as exc_info: TheoreticalLesson( id="test", day_of_week=-1, start_time=time(8, 0, 0), end_time=time(9, 30, 0), subject="Mathématiques", ) assert any( "greater than or equal to 0" in str(error).lower() or "le 6" in str(error).lower() for error in exc_info.value.errors() ) def test_day_of_week_0_valid(self) -> None: """Vérifie que day_of_week=0 (lundi) est valide.""" instance = TheoreticalLesson( id="test", day_of_week=0, start_time=time(8, 0, 0), end_time=time(9, 30, 0), subject="Mathématiques", ) assert instance.day_of_week == 0 def test_day_of_week_6_valid(self) -> None: """Vérifie que day_of_week=6 (dimanche) est valide.""" instance = TheoreticalLesson( id="test", day_of_week=6, start_time=time(8, 0, 0), end_time=time(9, 30, 0), subject="Mathématiques", ) assert instance.day_of_week == 6 class TestLessonTimeOrder: """Tests de validation de l'ordre temporel pour Lesson.""" def test_lesson_end_before_start_invalid(self) -> None: """Vérifie que end <= start lève une ValidationError.""" with pytest.raises(ValidationError) as exc_info: Lesson( id="test", start=datetime(2024, 9, 6, 10, 0, 0), end=datetime(2024, 9, 6, 9, 0, 0), # end avant start subject="Mathématiques", group=None, content=None, ) assert any( "end doit être postérieur à start" in str(error) for error in exc_info.value.errors() ) def test_lesson_end_after_start_valid(self) -> None: """Vérifie que end > start est valide.""" instance = Lesson( id="test", start=datetime(2024, 9, 6, 8, 0, 0), end=datetime(2024, 9, 6, 9, 30, 0), subject="Mathématiques", group=None, content=None, ) assert instance.end > instance.start class TestTheoreticalLessonTimeOrder: """Tests de validation de l'ordre temporel pour TheoreticalLesson.""" def test_theoretical_lesson_end_time_before_start_time_invalid(self) -> None: """Vérifie que end_time <= start_time lève une ValidationError.""" with pytest.raises(ValidationError) as exc_info: TheoreticalLesson( id="test", day_of_week=0, start_time=time(9, 0, 0), end_time=time(8, 0, 0), # end_time avant start_time subject="Mathématiques", ) assert any( "end_time doit être postérieur à start_time" in str(error) for error in exc_info.value.errors() ) def test_theoretical_lesson_end_time_after_start_time_valid(self) -> None: """Vérifie que end_time > start_time est valide.""" instance = TheoreticalLesson( id="test", day_of_week=0, start_time=time(8, 0, 0), end_time=time(9, 30, 0), subject="Mathématiques", ) assert instance.end_time > instance.start_time class TestSchoolEventDateOrder: """Tests de validation de l'ordre des dates pour SchoolEvent.""" def test_school_event_to_date_before_from_date_invalid(self) -> None: """Vérifie que to_date < from_date lève une ValidationError.""" with pytest.raises(ValidationError) as exc_info: SchoolEvent( kind=SchoolEventKind.HOLIDAY, label="Test", from_date=date(2024, 10, 20), to_date=date(2024, 10, 19), # to_date avant from_date ) assert any( "to_date doit être postérieur ou égal à from_date" in str(error) for error in exc_info.value.errors() ) def test_school_event_to_date_equal_from_date_valid(self) -> None: """Vérifie que to_date == from_date est valide.""" instance = SchoolEvent( kind=SchoolEventKind.HOLIDAY, label="Test", from_date=date(2024, 10, 20), to_date=date(2024, 10, 20), # égal ) assert instance.to_date >= instance.from_date def test_school_event_to_date_after_from_date_valid(self) -> None: """Vérifie que to_date > from_date est valide.""" instance = SchoolEvent( kind=SchoolEventKind.HOLIDAY, label="Test", from_date=date(2024, 10, 20), to_date=date(2024, 10, 25), ) assert instance.to_date >= instance.from_date class TestAgendaChangeConsistency: """Tests de cohérence des changements d'agenda.""" def test_agenda_change_added_without_lesson_invalid(self) -> None: """Vérifie que type=ADDED avec lesson=None lève une ValidationError.""" with pytest.raises(ValidationError) as exc_info: AgendaChange(type=AgendaChangeType.ADDED, lesson=None, theoretical_lesson=None) assert any( "lesson est requis pour le type" in str(error) for error in exc_info.value.errors() ) def test_agenda_change_removed_without_theoretical_lesson_invalid(self) -> None: """Vérifie que type=REMOVED avec theoretical_lesson=None lève une ValidationError.""" with pytest.raises(ValidationError) as exc_info: AgendaChange( type=AgendaChangeType.REMOVED, lesson=None, theoretical_lesson=None, ) assert any( "theoretical_lesson est requis pour le type" in str(error) for error in exc_info.value.errors() ) def test_agenda_change_modified_without_lesson_invalid(self) -> None: """Vérifie que type=MODIFIED avec lesson=None lève une ValidationError.""" with pytest.raises(ValidationError) as exc_info: AgendaChange(type=AgendaChangeType.MODIFIED, lesson=None, theoretical_lesson=None) assert any( "lesson est requis pour le type" in str(error) for error in exc_info.value.errors() ) def test_agenda_change_added_with_lesson_valid(self) -> None: """Vérifie que type=ADDED avec lesson= est valide.""" lesson = Lesson( id="lesson-valid", start=datetime(2024, 9, 6, 8, 0, 0), end=datetime(2024, 9, 6, 9, 30, 0), subject="Mathématiques", group=None, content=None, ) instance = AgendaChange( type=AgendaChangeType.ADDED, lesson=lesson, theoretical_lesson=None, ) assert instance.type == AgendaChangeType.ADDED assert instance.lesson is not None def test_agenda_change_removed_with_theoretical_lesson_valid(self) -> None: """Vérifie que type=REMOVED avec theoretical_lesson= est valide.""" theoretical_lesson = TheoreticalLesson( id="theo-lesson-valid", day_of_week=0, start_time=time(8, 0, 0), end_time=time(9, 30, 0), subject="Mathématiques", ) instance = AgendaChange( type=AgendaChangeType.REMOVED, lesson=None, theoretical_lesson=theoretical_lesson, ) assert instance.type == AgendaChangeType.REMOVED assert instance.theoretical_lesson is not None def test_agenda_change_modified_with_lesson_valid(self) -> None: """Vérifie que type=MODIFIED avec lesson= est valide.""" lesson = Lesson( id="lesson-valid-mod", start=datetime(2024, 9, 6, 10, 0, 0), end=datetime(2024, 9, 6, 11, 30, 0), subject="Physique", group=None, content=None, ) instance = AgendaChange( type=AgendaChangeType.MODIFIED, lesson=lesson, theoretical_lesson=None, ) assert instance.type == AgendaChangeType.MODIFIED assert instance.lesson is not None class TestCalDAVSyncResultInvariants: """Tests de validation des champs numériques de CalDAVSyncResult.""" def test_caldav_sync_result_added_negative_invalid(self) -> None: """Vérifie que added=-1 lève une ValidationError.""" with pytest.raises(ValidationError) as exc_info: CalDAVSyncResult(status=CalDAVSyncStatus.SUCCESS, added=-1, updated=0, removed=0) assert any("greater than or equal to 0" in str(error) for error in exc_info.value.errors()) def test_caldav_sync_result_updated_negative_invalid(self) -> None: """Vérifie que updated=-1 lève une ValidationError.""" with pytest.raises(ValidationError) as exc_info: CalDAVSyncResult(status=CalDAVSyncStatus.SUCCESS, added=0, updated=-1, removed=0) assert any("greater than or equal to 0" in str(error) for error in exc_info.value.errors()) def test_caldav_sync_result_removed_negative_invalid(self) -> None: """Vérifie que removed=-1 lève une ValidationError.""" with pytest.raises(ValidationError) as exc_info: CalDAVSyncResult(status=CalDAVSyncStatus.SUCCESS, added=0, updated=0, removed=-1) assert any("greater than or equal to 0" in str(error) for error in exc_info.value.errors()) def test_caldav_sync_result_zero_values_valid(self) -> None: """Vérifie que les valeurs à zéro sont valides.""" instance = CalDAVSyncResult(status=CalDAVSyncStatus.SUCCESS, added=0, updated=0, removed=0) assert instance.added == 0 assert instance.updated == 0 assert instance.removed == 0