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>
189 lines
6.9 KiB
Python
189 lines
6.9 KiB
Python
"""Tests unitaires pour le calendrier des vacances scolaires.
|
|
|
|
Ce module contient les tests pour les classes :class:`SchoolHolidayCalendar`,
|
|
:class:`HolidayPeriod` et :class:`SchoolHolidayFile` du module
|
|
:mod:`pronote_sync.sources.theoretical.holidays`.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from datetime import date
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from pronote_sync.errors import PronoteSyncError
|
|
from pronote_sync.sources.theoretical.holidays import SchoolHolidayCalendar
|
|
|
|
|
|
class TestSchoolHolidayCalendar:
|
|
"""Tests pour la classe SchoolHolidayCalendar."""
|
|
|
|
def test_load_valid_file(self, tmp_path: Path) -> None:
|
|
"""Teste le chargement d'un fichier JSON valide.
|
|
|
|
:assert: is_holiday retourne True pour une date dans une période.
|
|
"""
|
|
holiday_data = {
|
|
"zone": "A",
|
|
"school_year": "2026-2027",
|
|
"periods": [
|
|
{
|
|
"start_date": "2026-10-17",
|
|
"end_date": "2026-11-02",
|
|
"label": "Toussaint",
|
|
}
|
|
],
|
|
}
|
|
file_path = tmp_path / "holidays.json"
|
|
file_path.write_text(json.dumps(holiday_data), encoding="utf-8")
|
|
|
|
calendar = SchoolHolidayCalendar(file_path)
|
|
# Date dans la période de Toussaint
|
|
assert calendar.is_holiday(date(2026, 10, 20)) is True
|
|
|
|
def test_date_outside_periods(self, tmp_path: Path) -> None:
|
|
"""Teste qu'une date en dehors des périodes retourne False.
|
|
|
|
:assert: is_holiday retourne False pour une date hors période.
|
|
"""
|
|
holiday_data = {
|
|
"zone": "A",
|
|
"school_year": "2026-2027",
|
|
"periods": [
|
|
{
|
|
"start_date": "2026-10-17",
|
|
"end_date": "2026-11-02",
|
|
"label": "Toussaint",
|
|
}
|
|
],
|
|
}
|
|
file_path = tmp_path / "holidays.json"
|
|
file_path.write_text(json.dumps(holiday_data), encoding="utf-8")
|
|
|
|
calendar = SchoolHolidayCalendar(file_path)
|
|
# Date en dehors de la période
|
|
assert calendar.is_holiday(date(2026, 9, 1)) is False
|
|
|
|
def test_start_date_inclusive(self, tmp_path: Path) -> None:
|
|
"""Teste que la date de début est incluse dans la période.
|
|
|
|
:assert: is_holiday retourne True pour une date égale à start_date.
|
|
"""
|
|
holiday_data = {
|
|
"zone": "A",
|
|
"school_year": "2026-2027",
|
|
"periods": [
|
|
{
|
|
"start_date": "2026-10-17",
|
|
"end_date": "2026-11-02",
|
|
"label": "Toussaint",
|
|
}
|
|
],
|
|
}
|
|
file_path = tmp_path / "holidays.json"
|
|
file_path.write_text(json.dumps(holiday_data), encoding="utf-8")
|
|
|
|
calendar = SchoolHolidayCalendar(file_path)
|
|
assert calendar.is_holiday(date(2026, 10, 17)) is True
|
|
|
|
def test_end_date_inclusive(self, tmp_path: Path) -> None:
|
|
"""Teste que la date de fin est incluse dans la période.
|
|
|
|
:assert: is_holiday retourne True pour une date égale à end_date.
|
|
"""
|
|
holiday_data = {
|
|
"zone": "A",
|
|
"school_year": "2026-2027",
|
|
"periods": [
|
|
{
|
|
"start_date": "2026-10-17",
|
|
"end_date": "2026-11-02",
|
|
"label": "Toussaint",
|
|
}
|
|
],
|
|
}
|
|
file_path = tmp_path / "holidays.json"
|
|
file_path.write_text(json.dumps(holiday_data), encoding="utf-8")
|
|
|
|
calendar = SchoolHolidayCalendar(file_path)
|
|
assert calendar.is_holiday(date(2026, 11, 2)) is True
|
|
|
|
def test_file_not_found(self, tmp_path: Path) -> None:
|
|
"""Teste qu'un fichier introuvable lève une PronoteSyncError.
|
|
|
|
:assert: PronoteSyncError est levée pour un fichier introuvable.
|
|
"""
|
|
file_path = tmp_path / "nonexistent.json"
|
|
with pytest.raises(PronoteSyncError) as exc_info:
|
|
SchoolHolidayCalendar(file_path)
|
|
assert "introuvable" in str(exc_info.value)
|
|
# Vérifier qu'aucun secret n'est fuité dans le message d'erreur
|
|
assert "nonexistent" not in str(exc_info.value) or "introuvable" in str(exc_info.value)
|
|
|
|
def test_invalid_json(self, tmp_path: Path, caplog: pytest.LogCaptureFixture) -> None:
|
|
"""Teste qu'un fichier JSON invalide lève une PronoteSyncError.
|
|
|
|
:assert: PronoteSyncError est levée pour un JSON invalide.
|
|
"""
|
|
file_path = tmp_path / "invalid.json"
|
|
file_path.write_text("{ invalid json }", encoding="utf-8")
|
|
|
|
with pytest.raises(PronoteSyncError) as exc_info:
|
|
SchoolHolidayCalendar(file_path)
|
|
assert "invalide" in str(exc_info.value)
|
|
|
|
def test_invalid_period_dates(self, tmp_path: Path, caplog: pytest.LogCaptureFixture) -> None:
|
|
"""Teste qu'une période avec end_date < start_date lève une ValidationError.
|
|
|
|
:assert: PronoteSyncError est levée pour des dates de période invalides.
|
|
"""
|
|
holiday_data = {
|
|
"zone": "A",
|
|
"school_year": "2026-2027",
|
|
"periods": [
|
|
{
|
|
"start_date": "2026-11-02",
|
|
"end_date": "2026-10-17", # Inversé
|
|
"label": "Toussaint",
|
|
}
|
|
],
|
|
}
|
|
file_path = tmp_path / "holidays.json"
|
|
file_path.write_text(json.dumps(holiday_data), encoding="utf-8")
|
|
|
|
with pytest.raises(PronoteSyncError):
|
|
SchoolHolidayCalendar(file_path)
|
|
|
|
def test_empty_periods(self, tmp_path: Path) -> None:
|
|
"""Teste qu'un fichier avec des périodes vides retourne toujours False.
|
|
|
|
:assert: is_holiday retourne False pour toutes les dates.
|
|
"""
|
|
holiday_data = {
|
|
"zone": "A",
|
|
"school_year": "2026-2027",
|
|
"periods": [],
|
|
}
|
|
file_path = tmp_path / "holidays.json"
|
|
file_path.write_text(json.dumps(holiday_data), encoding="utf-8")
|
|
|
|
calendar = SchoolHolidayCalendar(file_path)
|
|
assert calendar.is_holiday(date(2026, 10, 20)) is False
|
|
assert calendar.is_holiday(date(2026, 1, 1)) is False
|
|
|
|
def test_load_from_fixture(self) -> None:
|
|
"""Teste le chargement du fichier de fixture et vérifie une date connue.
|
|
|
|
:assert: is_holiday retourne True pour une date de vacances connue.
|
|
"""
|
|
fixture_path = Path(__file__).parent.parent / "fixtures" / "school_holidays.json"
|
|
calendar = SchoolHolidayCalendar(fixture_path)
|
|
# Date dans les vacances de Toussaint (17 oct - 2 nov 2026)
|
|
assert calendar.is_holiday(date(2026, 10, 20)) is True
|
|
# Date dans les vacances de Noël (19 déc 2026 - 4 janv 2027)
|
|
assert calendar.is_holiday(date(2026, 12, 25)) is True
|
|
# Date en dehors des vacances
|
|
assert calendar.is_holiday(date(2026, 9, 1)) is False
|