"""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": "

Exercice 42 à rendre

", }, ), ( 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": "

Sortie prévue le 15 octobre

", "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="

Mise à jour du règlement

", 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, )