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>
374 lines
13 KiB
Python
374 lines
13 KiB
Python
"""Tests de construction et sérialisation JSON des modèles Pydantic.
|
|
|
|
Ce module vérifie que chaque modèle figé peut être sérialisé en JSON puis désérialisé
|
|
sans perte de données, garantissant la stabilité des contrats d'échange.
|
|
|
|
Les tests utilisent des données minimales valides extraites des définitions de modèles.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import date, datetime, time
|
|
from typing import Any
|
|
|
|
import pytest
|
|
from pydantic import BaseModel
|
|
|
|
from pronote_sync.models.agenda import (
|
|
HomeworkBlock,
|
|
Lesson,
|
|
LessonStatus,
|
|
SchoolEvent,
|
|
SchoolEventKind,
|
|
TheoreticalLesson,
|
|
)
|
|
from pronote_sync.models.blog import BlogArticle, ExternalInfo
|
|
from pronote_sync.models.diff import AgendaChange, AgendaChangeType, AgendaDiff
|
|
from pronote_sync.models.homework import Homework
|
|
from pronote_sync.models.message import Message, MessageType
|
|
from pronote_sync.models.xmpp import XmppMessage
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"model_class, kwargs",
|
|
[
|
|
(
|
|
Lesson,
|
|
{
|
|
"id": "lesson-001",
|
|
"start": datetime(2024, 9, 6, 8, 0, 0),
|
|
"end": datetime(2024, 9, 6, 9, 30, 0),
|
|
"subject": "Mathématiques",
|
|
"teachers": ("M. Dupont",),
|
|
"rooms": ("Salle 101",),
|
|
"group": "Classe A",
|
|
"status": LessonStatus.NORMAL,
|
|
"content": "Algèbre linéaire",
|
|
"homework_blocks": (
|
|
HomeworkBlock(kind="due", date=date(2024, 9, 13), text="Exercice 42"),
|
|
),
|
|
},
|
|
),
|
|
(
|
|
TheoreticalLesson,
|
|
{
|
|
"id": "theo-lesson-001",
|
|
"day_of_week": 0, # Lundi
|
|
"start_time": time(8, 0, 0),
|
|
"end_time": time(9, 30, 0),
|
|
"subject": "Mathématiques",
|
|
"teachers": ("M. Dupont",),
|
|
"rooms": ("Salle 101",),
|
|
},
|
|
),
|
|
(
|
|
SchoolEvent,
|
|
{
|
|
"kind": SchoolEventKind.HOLIDAY,
|
|
"label": "Vacances de la Toussaint",
|
|
"from_date": date(2024, 10, 19),
|
|
"to_date": date(2024, 11, 3),
|
|
},
|
|
),
|
|
(
|
|
Homework,
|
|
{
|
|
"id": "hw-001",
|
|
"subject": "Mathématiques",
|
|
"teachers": ("M. Dupont",),
|
|
"assigned_on": date(2024, 9, 1),
|
|
"due_on": date(2024, 9, 15),
|
|
"text": "Exercice 42 à rendre",
|
|
"html": "<p>Exercice 42 à rendre</p>",
|
|
},
|
|
),
|
|
(
|
|
AgendaChange,
|
|
{
|
|
"type": AgendaChangeType.ADDED,
|
|
"lesson": Lesson(
|
|
id="lesson-002",
|
|
start=datetime(2024, 9, 7, 10, 0, 0),
|
|
end=datetime(2024, 9, 7, 11, 30, 0),
|
|
subject="Physique",
|
|
teachers=("M. Martin",),
|
|
rooms=("Labo 205",),
|
|
group=None,
|
|
content=None,
|
|
),
|
|
"theoretical_lesson": None,
|
|
"details": "Cours ajouté exceptionnellement",
|
|
},
|
|
),
|
|
(
|
|
AgendaDiff,
|
|
{
|
|
"target_date": date(2024, 9, 6),
|
|
"changes": (
|
|
AgendaChange(
|
|
type=AgendaChangeType.ADDED,
|
|
lesson=Lesson(
|
|
id="lesson-003",
|
|
start=datetime(2024, 9, 8, 14, 0, 0),
|
|
end=datetime(2024, 9, 8, 15, 30, 0),
|
|
subject="Histoire",
|
|
teachers=("Mme Bernard",),
|
|
rooms=("Salle 201",),
|
|
group=None,
|
|
content=None,
|
|
),
|
|
theoretical_lesson=None,
|
|
),
|
|
),
|
|
},
|
|
),
|
|
(
|
|
XmppMessage,
|
|
{
|
|
"target_date": date(2024, 9, 6),
|
|
"synthesis": "Aucun changement majeur aujourd'hui.",
|
|
"homeworks": (
|
|
Homework(
|
|
id="hw-002",
|
|
subject="Physique",
|
|
teachers=("M. Martin",),
|
|
assigned_on=None,
|
|
due_on=date(2024, 9, 7),
|
|
text="TP à préparer",
|
|
),
|
|
),
|
|
"changes": (
|
|
AgendaChange(
|
|
type=AgendaChangeType.MODIFIED,
|
|
lesson=Lesson(
|
|
id="lesson-004",
|
|
start=datetime(2024, 9, 6, 16, 0, 0),
|
|
end=datetime(2024, 9, 6, 17, 30, 0),
|
|
subject="SVT",
|
|
teachers=("M. Lefèvre",),
|
|
rooms=("Salle 302",),
|
|
group=None,
|
|
content=None,
|
|
),
|
|
theoretical_lesson=None,
|
|
),
|
|
),
|
|
"messages": (
|
|
Message(
|
|
id="msg-001",
|
|
type=MessageType.INFORMATION,
|
|
title="Rappel",
|
|
content="Réunion parents-professeurs demain",
|
|
author="Secrétariat",
|
|
date=datetime(2024, 9, 5, 10, 0, 0),
|
|
read=False,
|
|
),
|
|
),
|
|
"external_info": ExternalInfo(blog_articles=(), pronote_messages=(), other_info=()),
|
|
},
|
|
),
|
|
(
|
|
BlogArticle,
|
|
{
|
|
"id": "article-001",
|
|
"title": "Sortie scolaire à Paris",
|
|
"url": "https://college.fr/blog/sortie-paris",
|
|
"published_at": datetime(2024, 9, 5, 14, 30, 0),
|
|
"updated_at": datetime(2024, 9, 5, 16, 45, 0),
|
|
"category": "Actualités",
|
|
"author": "M. Dubois",
|
|
"content_html": "<p>Sortie prévue le 15 octobre</p>",
|
|
"content_text": "Sortie prévue le 15 octobre",
|
|
},
|
|
),
|
|
(
|
|
ExternalInfo,
|
|
{
|
|
"blog_articles": (
|
|
BlogArticle(
|
|
id="article-002",
|
|
title="Nouveau règlement intérieur",
|
|
url="https://college.fr/blog/reglement",
|
|
published_at=datetime(2024, 9, 1, 9, 0, 0),
|
|
updated_at=None,
|
|
category=None,
|
|
author=None,
|
|
content_html="<p>Mise à jour du règlement</p>",
|
|
content_text="Mise à jour du règlement",
|
|
),
|
|
),
|
|
"pronote_messages": (
|
|
Message(
|
|
id="msg-002",
|
|
type=MessageType.DISCUSSION,
|
|
title="Devoirs de maths",
|
|
content="Voir Pronote pour les détails",
|
|
author="Prof Maths",
|
|
date=datetime(2024, 9, 4, 8, 0, 0),
|
|
read=False,
|
|
),
|
|
),
|
|
"other_info": ("Info complémentaire 1", "Info complémentaire 2"),
|
|
},
|
|
),
|
|
(
|
|
Message,
|
|
{
|
|
"id": "msg-003",
|
|
"type": MessageType.SURVEY,
|
|
"title": "Enquête de satisfaction",
|
|
"content": "Merci de répondre au questionnaire",
|
|
"author": "Direction",
|
|
"date": datetime(2024, 9, 5, 11, 0, 0),
|
|
"read": False,
|
|
},
|
|
),
|
|
],
|
|
)
|
|
def test_model_json_roundtrip(model_class: type[BaseModel], kwargs: dict[str, Any]) -> None:
|
|
"""Vérifie qu'un modèle peut être sérialisé en JSON puis désérialisé sans perte.
|
|
|
|
:param model_class: Classe du modèle à tester.
|
|
:type model_class: type[BaseModel]
|
|
:param kwargs: Arguments de constructeur valides.
|
|
:type kwargs: dict[str, Any]
|
|
"""
|
|
# Construction de l'instance
|
|
instance = model_class(**kwargs)
|
|
|
|
# Sérialisation JSON
|
|
json_str = instance.model_dump_json()
|
|
|
|
# Désérialisation depuis JSON
|
|
roundtrip_instance = model_class.model_validate_json(json_str)
|
|
|
|
# Vérification des champs clés (comparaison par valeur)
|
|
for field_name in kwargs:
|
|
if field_name in (
|
|
"start",
|
|
"end",
|
|
"start_time",
|
|
"end_time",
|
|
"published_at",
|
|
"updated_at",
|
|
"date",
|
|
"due_on",
|
|
"assigned_on",
|
|
"generated_at",
|
|
):
|
|
# Pour les dates/heures, comparer les objets directement
|
|
assert getattr(instance, field_name) == getattr(roundtrip_instance, field_name), (
|
|
f"Le champ {field_name} ne correspond pas après round-trip"
|
|
)
|
|
else:
|
|
# Pour les autres champs, comparer directement
|
|
assert getattr(instance, field_name) == getattr(roundtrip_instance, field_name), (
|
|
f"Le champ {field_name} ne correspond pas après round-trip"
|
|
)
|
|
|
|
|
|
def test_lesson_status_enum_values() -> None:
|
|
"""Vérifie que les valeurs de l'enum LessonStatus sont correctes.
|
|
|
|
:raises ValidationError: Si une valeur invalide est utilisée.
|
|
"""
|
|
# Test des valeurs valides
|
|
for status in [LessonStatus.NORMAL, LessonStatus.CANCELLED, LessonStatus.MOVED]:
|
|
assert isinstance(status.value, str)
|
|
Lesson(
|
|
id="test",
|
|
start=datetime(2024, 9, 6, 8, 0),
|
|
end=datetime(2024, 9, 6, 9, 0),
|
|
subject="Test",
|
|
status=status,
|
|
group=None,
|
|
content=None,
|
|
)
|
|
|
|
|
|
def test_school_event_kind_enum_values() -> None:
|
|
"""Vérifie que les valeurs de l'enum SchoolEventKind sont correctes.
|
|
|
|
:raises ValidationError: Si une valeur invalide est utilisée.
|
|
"""
|
|
# Test des valeurs valides
|
|
for kind in [SchoolEventKind.HOLIDAY, SchoolEventKind.PUBLIC_HOLIDAY]:
|
|
assert isinstance(kind.value, str)
|
|
SchoolEvent(
|
|
kind=kind,
|
|
label="Test",
|
|
from_date=date(2024, 9, 1),
|
|
to_date=date(2024, 9, 2),
|
|
)
|
|
|
|
|
|
def test_message_type_enum_values() -> None:
|
|
"""Vérifie que les valeurs de l'enum MessageType sont correctes.
|
|
|
|
:raises ValidationError: Si une valeur invalide est utilisée.
|
|
"""
|
|
# Test des valeurs valides
|
|
for msg_type in [MessageType.DISCUSSION, MessageType.INFORMATION, MessageType.SURVEY]:
|
|
assert isinstance(msg_type.value, str)
|
|
Message(
|
|
id="test",
|
|
type=msg_type,
|
|
title="Test",
|
|
content="Test",
|
|
author="Test",
|
|
date=datetime(2024, 9, 1, 8, 0),
|
|
read=False,
|
|
)
|
|
|
|
|
|
def test_agenda_change_type_enum_values() -> None:
|
|
"""Vérifie que les valeurs de l'enum AgendaChangeType sont correctes.
|
|
|
|
:raises ValidationError: Si une valeur invalide est utilisée.
|
|
"""
|
|
# Test des valeurs valides
|
|
for change_type in [
|
|
AgendaChangeType.ADDED,
|
|
AgendaChangeType.REMOVED,
|
|
AgendaChangeType.MODIFIED,
|
|
]:
|
|
assert isinstance(change_type.value, str)
|
|
if change_type == AgendaChangeType.ADDED:
|
|
AgendaChange(
|
|
type=change_type,
|
|
lesson=Lesson(
|
|
id="test",
|
|
start=datetime(2024, 9, 6, 8, 0),
|
|
end=datetime(2024, 9, 6, 9, 0),
|
|
subject="Test",
|
|
group=None,
|
|
content=None,
|
|
),
|
|
theoretical_lesson=None,
|
|
)
|
|
elif change_type == AgendaChangeType.REMOVED:
|
|
AgendaChange(
|
|
type=change_type,
|
|
lesson=None,
|
|
theoretical_lesson=TheoreticalLesson(
|
|
id="test",
|
|
day_of_week=0,
|
|
start_time=time(8, 0, 0),
|
|
end_time=time(9, 0, 0),
|
|
subject="Test",
|
|
),
|
|
)
|
|
else:
|
|
AgendaChange(
|
|
type=change_type,
|
|
lesson=Lesson(
|
|
id="test",
|
|
start=datetime(2024, 9, 6, 8, 0),
|
|
end=datetime(2024, 9, 6, 9, 0),
|
|
subject="Test",
|
|
group=None,
|
|
content=None,
|
|
),
|
|
theoretical_lesson=None,
|
|
)
|