Immuabilité : - Collections des modèles frozen : list → tuple (Lesson, TheoreticalLesson, SchoolEvent, AgendaDiff, Homework, XmppMessage, ExternalInfo) - Gel de HomeworkBlock et ExternalInfo (sous-modèles imbriqués dans contrats) - Valeurs par défaut immuables : Field(default=(), ...) Validation de domaine : - TheoreticalLesson.day_of_week : Field(ge=0, le=6) - Lesson/TheoreticalLesson : start < end (validateur de modèle) - SchoolEvent : from_date <= to_date - AgendaChange : cohérence type/payload (ADDED/MODIFIED → lesson, REMOVED → theoretical_lesson) - CalDAVSyncResult : compteurs Field(ge=0) sur added/updated/removed Tests : 66 tests (construction/sérialisation, immuabilité, invariants) couvrant le gel, la mutabilité légitime, et les cas invalides par invariant. Co-authored-by: opencode/coder <coder@agents.invalid> Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
288 lines
11 KiB
Python
288 lines
11 KiB
Python
"""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=<valide> 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=<valide> 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=<valide> 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
|