Implémentation complète de la source d'agenda théorique : - model.py : modèles Pydantic de parsing JSON (TheoreticalLessonEntry, TheoreticalAgendaFile) avec validation des formats d'heure et de l'ordre début/fin. - parity.py : WeekParityService déterministe calculant la parité d'une semaine (paire/impaire) à partir d'une date de référence. - holidays.py : SchoolHolidayCalendar lisant un fichier JSON de vacances scolaires (zone A) avec bornes inclusives. - provider.py : protocole TheoreticalAgendaProvider (get_lessons, get_lessons_for_range). - file.py : JsonTheoreticalAgendaProvider implémentant le protocole : filtrage par parité et vacances, génération d'IDs déterministes incluant le type de semaine, validation de l'unicité des IDs, tri stable par identifiant. - __init__.py : factory get_theoretical_provider câblant la configuration (None si désactivé, erreur si config de parité partielle). - Fixtures : theoretical.json (9 leçons all/even/odd) et school_holidays.json (zone A, 4 périodes). - 57 tests unitaires couvrant parsing, parité, vacances, provider, factory, déduplication de range, collisions d'IDs. - Guide : §8 et §12 alignés avec le format JSON. Co-authored-by: opencode/coder <coder@agents.invalid> Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
202 lines
6.4 KiB
Python
202 lines
6.4 KiB
Python
"""Tests unitaires pour les modèles de parsing de l'agenda théorique.
|
|
|
|
Ce module contient les tests pour les classes :class:`TheoreticalLessonEntry`
|
|
et :class:`TheoreticalAgendaFile` du module
|
|
:mod:`pronote_sync.sources.theoretical.model`.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
from pydantic import ValidationError
|
|
|
|
from pronote_sync.sources.theoretical.model import (
|
|
TheoreticalAgendaFile,
|
|
TheoreticalLessonEntry,
|
|
)
|
|
|
|
|
|
class TestTheoreticalLessonEntry:
|
|
"""Tests pour la classe TheoreticalLessonEntry."""
|
|
|
|
def test_valid_lesson_entry(self) -> None:
|
|
"""Teste la construction d'une entrée de cours valide.
|
|
|
|
:assert: Tous les champs sont correctement initialisés.
|
|
"""
|
|
entry = TheoreticalLessonEntry(
|
|
week="all",
|
|
day_of_week=0,
|
|
start_time="08:00",
|
|
end_time="09:00",
|
|
subject="Mathématiques",
|
|
teachers=("Mme Martin",),
|
|
rooms=("101",),
|
|
id="math-1",
|
|
)
|
|
assert entry.week == "all"
|
|
assert entry.day_of_week == 0
|
|
assert entry.start_time == "08:00"
|
|
assert entry.end_time == "09:00"
|
|
assert entry.subject == "Mathématiques"
|
|
assert entry.teachers == ("Mme Martin",)
|
|
assert entry.rooms == ("101",)
|
|
assert entry.id == "math-1"
|
|
|
|
def test_invalid_time_format(self) -> None:
|
|
"""Teste qu'une heure invalide (25:00) lève une ValidationError.
|
|
|
|
:assert: ValidationError est levée pour start_time="25:00".
|
|
"""
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
TheoreticalLessonEntry(
|
|
week="all",
|
|
day_of_week=0,
|
|
start_time="25:00",
|
|
end_time="09:00",
|
|
subject="Test",
|
|
)
|
|
assert "start_time" in str(exc_info.value)
|
|
|
|
def test_invalid_time_format_minutes(self) -> None:
|
|
"""Teste qu'une minute invalide (60) lève une ValidationError.
|
|
|
|
:assert: ValidationError est levée pour start_time="08:60".
|
|
"""
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
TheoreticalLessonEntry(
|
|
week="all",
|
|
day_of_week=0,
|
|
start_time="08:60",
|
|
end_time="09:00",
|
|
subject="Test",
|
|
)
|
|
assert "start_time" in str(exc_info.value)
|
|
|
|
def test_end_before_start(self) -> None:
|
|
"""Teste qu'une fin avant le début lève une ValidationError.
|
|
|
|
:assert: ValidationError est levée pour end_time < start_time.
|
|
"""
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
TheoreticalLessonEntry(
|
|
week="all",
|
|
day_of_week=0,
|
|
start_time="10:00",
|
|
end_time="09:00",
|
|
subject="Test",
|
|
)
|
|
assert "end_time" in str(exc_info.value)
|
|
|
|
def test_end_equal_start(self) -> None:
|
|
"""Teste qu'une fin égale au début lève une ValidationError.
|
|
|
|
:assert: ValidationError est levée pour end_time == start_time.
|
|
"""
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
TheoreticalLessonEntry(
|
|
week="all",
|
|
day_of_week=0,
|
|
start_time="09:00",
|
|
end_time="09:00",
|
|
subject="Test",
|
|
)
|
|
assert "end_time" in str(exc_info.value)
|
|
|
|
def test_invalid_week(self) -> None:
|
|
"""Teste qu'une semaine invalide lève une ValidationError.
|
|
|
|
:assert: ValidationError est levée pour week="weekly".
|
|
"""
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
TheoreticalLessonEntry(
|
|
week="weekly", # type: ignore[arg-type]
|
|
day_of_week=0,
|
|
start_time="08:00",
|
|
end_time="09:00",
|
|
subject="Test",
|
|
)
|
|
assert "week" in str(exc_info.value)
|
|
|
|
def test_invalid_day_of_week(self) -> None:
|
|
"""Teste qu'un jour de semaine invalide (7) lève une ValidationError.
|
|
|
|
:assert: ValidationError est levée pour day_of_week=7.
|
|
"""
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
TheoreticalLessonEntry(
|
|
week="all",
|
|
day_of_week=7,
|
|
start_time="08:00",
|
|
end_time="09:00",
|
|
subject="Test",
|
|
)
|
|
assert "day_of_week" in str(exc_info.value)
|
|
|
|
def test_optional_fields_defaults(self) -> None:
|
|
"""Teste que les champs optionnels ont des valeurs par défaut.
|
|
|
|
:assert: teachers et rooms valent () par défaut.
|
|
"""
|
|
entry = TheoreticalLessonEntry(
|
|
week="all",
|
|
day_of_week=0,
|
|
start_time="08:00",
|
|
end_time="09:00",
|
|
subject="Test",
|
|
)
|
|
assert entry.teachers == ()
|
|
assert entry.rooms == ()
|
|
|
|
def test_frozen_model(self) -> None:
|
|
"""Teste que le modèle est immuable (frozen).
|
|
|
|
:assert: La tentative de mutation lève une exception.
|
|
"""
|
|
entry = TheoreticalLessonEntry(
|
|
week="all",
|
|
day_of_week=0,
|
|
start_time="08:00",
|
|
end_time="09:00",
|
|
subject="Test",
|
|
)
|
|
with pytest.raises((ValidationError, AttributeError)):
|
|
entry.subject = "Nouveau"
|
|
|
|
|
|
class TestTheoreticalAgendaFile:
|
|
"""Tests pour la classe TheoreticalAgendaFile."""
|
|
|
|
def test_agenda_file_valid(self) -> None:
|
|
"""Teste la construction d'un fichier d'agenda valide.
|
|
|
|
:assert: version == 1 et lessons a le bon nombre d'entrées.
|
|
"""
|
|
lessons = [
|
|
TheoreticalLessonEntry(
|
|
week="all",
|
|
day_of_week=0,
|
|
start_time="08:00",
|
|
end_time="09:00",
|
|
subject="Mathématiques",
|
|
),
|
|
TheoreticalLessonEntry(
|
|
week="even",
|
|
day_of_week=1,
|
|
start_time="10:00",
|
|
end_time="11:00",
|
|
subject="Anglais",
|
|
),
|
|
]
|
|
agenda = TheoreticalAgendaFile(version=1, lessons=tuple(lessons))
|
|
assert agenda.version == 1
|
|
assert len(agenda.lessons) == 2
|
|
|
|
def test_agenda_file_default_version(self) -> None:
|
|
"""Teste que la version par défaut est 1.
|
|
|
|
:assert: version vaut 1 par défaut.
|
|
"""
|
|
agenda = TheoreticalAgendaFile(lessons=())
|
|
assert agenda.version == 1
|