feat(M6): agenda théorique JSON avec parité des semaines et vacances scolaires
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>
This commit is contained in:
146
tests/unit/test_theoretical_factory.py
Normal file
146
tests/unit/test_theoretical_factory.py
Normal file
@@ -0,0 +1,146 @@
|
||||
"""Tests unitaires pour l'usine de construction du fournisseur d'agenda théorique.
|
||||
|
||||
Ce module contient les tests pour la fonction :func:`get_theoretical_provider`
|
||||
du module :mod:`pronote_sync.sources.theoretical`.
|
||||
"""
|
||||
|
||||
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 import (
|
||||
TheoreticalAgendaProvider,
|
||||
get_theoretical_provider,
|
||||
)
|
||||
|
||||
|
||||
class TestGetTheoreticalProvider:
|
||||
"""Tests pour la fonction get_theoretical_provider."""
|
||||
|
||||
@pytest.fixture
|
||||
def fixture_path(self) -> Path:
|
||||
"""Retourne le chemin du fichier de fixture theoretical.json."""
|
||||
return Path(__file__).parent.parent / "fixtures" / "theoretical.json"
|
||||
|
||||
@pytest.fixture
|
||||
def holidays_path(self) -> Path:
|
||||
"""Retourne le chemin du fichier de fixture school_holidays.json."""
|
||||
return Path(__file__).parent.parent / "fixtures" / "school_holidays.json"
|
||||
|
||||
@pytest.fixture
|
||||
def all_only_path(self, tmp_path: Path) -> Path:
|
||||
"""Crée un fichier JSON avec uniquement des cours "all"."""
|
||||
data = {
|
||||
"version": 1,
|
||||
"lessons": [
|
||||
{
|
||||
"week": "all",
|
||||
"day_of_week": 0,
|
||||
"start_time": "08:00",
|
||||
"end_time": "09:00",
|
||||
"subject": "Test",
|
||||
}
|
||||
],
|
||||
}
|
||||
file_path = tmp_path / "all_only.json"
|
||||
file_path.write_text(json.dumps(data), encoding="utf-8")
|
||||
return file_path
|
||||
|
||||
def test_factory_returns_none_when_path_none(self) -> None:
|
||||
"""Teste que l'usine retourne None quand agenda_path est None.
|
||||
|
||||
:assert: get_theoretical_provider(None, None, None, None) retourne None.
|
||||
"""
|
||||
result = get_theoretical_provider(None, None, None, None)
|
||||
assert result is None
|
||||
|
||||
def test_factory_returns_provider_with_full_config(
|
||||
self, fixture_path: Path, holidays_path: Path
|
||||
) -> None:
|
||||
"""Teste que l'usine retourne un fournisseur avec une configuration complète.
|
||||
|
||||
:assert: Un fournisseur est retourné avec tous les paramètres.
|
||||
"""
|
||||
result = get_theoretical_provider(
|
||||
agenda_path=str(fixture_path),
|
||||
holidays_path=str(holidays_path),
|
||||
anchor_date=date(2026, 9, 1),
|
||||
anchor_type="even",
|
||||
)
|
||||
assert result is not None
|
||||
assert isinstance(result, TheoreticalAgendaProvider)
|
||||
|
||||
def test_factory_partial_parity_config_error(self, fixture_path: Path) -> None:
|
||||
"""Teste qu'une configuration de parité partielle lève une PronoteSyncError.
|
||||
|
||||
:assert: PronoteSyncError est levée quand anchor_date est fourni sans anchor_type.
|
||||
"""
|
||||
with pytest.raises(PronoteSyncError) as exc_info:
|
||||
get_theoretical_provider(
|
||||
agenda_path=str(fixture_path),
|
||||
holidays_path=None,
|
||||
anchor_date=date(2026, 9, 1),
|
||||
anchor_type=None,
|
||||
)
|
||||
assert "incomplète" in str(exc_info.value)
|
||||
|
||||
def test_factory_partial_parity_config_error_type_only(self, fixture_path: Path) -> None:
|
||||
"""Teste qu'une configuration de parité partielle (type seulement) lève une PronoteSyncError.
|
||||
|
||||
:assert: PronoteSyncError est levée quand anchor_type est fourni sans anchor_date.
|
||||
"""
|
||||
with pytest.raises(PronoteSyncError) as exc_info:
|
||||
get_theoretical_provider(
|
||||
agenda_path=str(fixture_path),
|
||||
holidays_path=None,
|
||||
anchor_date=None,
|
||||
anchor_type="even",
|
||||
)
|
||||
assert "incomplète" in str(exc_info.value)
|
||||
|
||||
def test_factory_no_holidays(self, fixture_path: Path) -> None:
|
||||
"""Teste que l'usine retourne un fournisseur sans calendrier de vacances.
|
||||
|
||||
:assert: Un fournisseur est retourné sans calendrier de vacances.
|
||||
"""
|
||||
result = get_theoretical_provider(
|
||||
agenda_path=str(fixture_path),
|
||||
holidays_path=None,
|
||||
anchor_date=date(2026, 9, 1),
|
||||
anchor_type="even",
|
||||
)
|
||||
assert result is not None
|
||||
assert isinstance(result, TheoreticalAgendaProvider)
|
||||
|
||||
def test_factory_no_parity(self, all_only_path: Path) -> None:
|
||||
"""Teste que l'usine retourne un fournisseur sans service de parité pour des cours "all".
|
||||
|
||||
:assert: Un fournisseur est retourné sans service de parité.
|
||||
"""
|
||||
result = get_theoretical_provider(
|
||||
agenda_path=str(all_only_path),
|
||||
holidays_path=None,
|
||||
anchor_date=None,
|
||||
anchor_type=None,
|
||||
)
|
||||
assert result is not None
|
||||
assert isinstance(result, TheoreticalAgendaProvider)
|
||||
|
||||
def test_factory_protocol_compliance(self, fixture_path: Path, holidays_path: Path) -> None:
|
||||
"""Teste que le fournisseur retourné respecte le protocole TheoreticalAgendaProvider.
|
||||
|
||||
:assert: Le fournisseur satisfait isinstance(provider, TheoreticalAgendaProvider).
|
||||
"""
|
||||
result = get_theoretical_provider(
|
||||
agenda_path=str(fixture_path),
|
||||
holidays_path=str(holidays_path),
|
||||
anchor_date=date(2026, 9, 1),
|
||||
anchor_type="even",
|
||||
)
|
||||
assert result is not None
|
||||
assert isinstance(result, TheoreticalAgendaProvider)
|
||||
Reference in New Issue
Block a user