feat: Configurer les jours fériés et vacances du calendrier scolaire
Les administrateurs d'établissement avaient besoin de gérer le calendrier scolaire (FR80) pour que l'EDT et les devoirs respectent automatiquement les jours non travaillés. Sans cette configuration centralisée, chaque module devait gérer indépendamment les contraintes de dates. Le calendrier s'appuie sur l'API data.education.gouv.fr pour importer les vacances officielles par zone (A/B/C) et calcule les 11 jours fériés français (dont les fêtes mobiles liées à Pâques). Les enseignants sont notifiés par email lors de l'ajout d'une journée pédagogique. Un query IsSchoolDay et une validation des dates d'échéance de devoirs permettent aux autres modules de s'intégrer sans couplage direct.
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Integration\Administration\Infrastructure\Service;
|
||||
|
||||
use App\Administration\Domain\Model\SchoolCalendar\CalendarEntryType;
|
||||
use App\Administration\Domain\Model\SchoolCalendar\SchoolZone;
|
||||
use App\Administration\Infrastructure\Service\FrenchPublicHolidaysCalculator;
|
||||
use App\Administration\Infrastructure\Service\JsonOfficialCalendarProvider;
|
||||
|
||||
use function count;
|
||||
|
||||
use PHPUnit\Framework\Attributes\Large;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Log\NullLogger;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
use Symfony\Component\HttpClient\HttpClient;
|
||||
|
||||
use function sys_get_temp_dir;
|
||||
use function unlink;
|
||||
|
||||
/**
|
||||
* Vérifie que l'API data.education.gouv.fr répond toujours correctement
|
||||
* et que le format de réponse n'a pas changé.
|
||||
*
|
||||
* Ce test fait un appel réseau réel. Il échouera si :
|
||||
* - l'URL de l'API change
|
||||
* - le format de réponse (champs description, start_date, end_date, zones) change
|
||||
* - l'API est indisponible
|
||||
*/
|
||||
#[Large]
|
||||
final class GouvFrCalendarApiTest extends TestCase
|
||||
{
|
||||
private const string ACADEMIC_YEAR = '2024-2025';
|
||||
|
||||
private string $tempDir;
|
||||
private JsonOfficialCalendarProvider $provider;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->tempDir = sys_get_temp_dir() . '/classeo-calendar-test-' . uniqid();
|
||||
mkdir($this->tempDir);
|
||||
|
||||
$this->provider = new JsonOfficialCalendarProvider(
|
||||
dataDirectory: $this->tempDir,
|
||||
httpClient: HttpClient::create(),
|
||||
holidaysCalculator: new FrenchPublicHolidaysCalculator(),
|
||||
logger: new NullLogger(),
|
||||
);
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
// Supprimer les fichiers générés
|
||||
$files = glob($this->tempDir . '/*.json');
|
||||
foreach ($files as $file) {
|
||||
unlink($file);
|
||||
}
|
||||
rmdir($this->tempDir);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function apiRetourneDesVacancesPourChaqueZone(): void
|
||||
{
|
||||
$vacationsA = $this->provider->vacancesParZone(SchoolZone::A, self::ACADEMIC_YEAR);
|
||||
$vacationsB = $this->provider->vacancesParZone(SchoolZone::B, self::ACADEMIC_YEAR);
|
||||
$vacationsC = $this->provider->vacancesParZone(SchoolZone::C, self::ACADEMIC_YEAR);
|
||||
|
||||
self::assertNotEmpty($vacationsA, 'L\'API doit retourner des vacances pour la zone A');
|
||||
self::assertNotEmpty($vacationsB, 'L\'API doit retourner des vacances pour la zone B');
|
||||
self::assertNotEmpty($vacationsC, 'L\'API doit retourner des vacances pour la zone C');
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function chaqueEntreeALesBonsChamps(): void
|
||||
{
|
||||
$entries = $this->provider->toutesEntreesOfficielles(SchoolZone::A, self::ACADEMIC_YEAR);
|
||||
|
||||
foreach ($entries as $entry) {
|
||||
self::assertNotEmpty((string) $entry->id, 'Chaque entrée doit avoir un id');
|
||||
self::assertInstanceOf(CalendarEntryType::class, $entry->type);
|
||||
self::assertNotNull($entry->startDate, 'startDate ne doit pas être null');
|
||||
self::assertNotNull($entry->endDate, 'endDate ne doit pas être null');
|
||||
self::assertNotEmpty($entry->label, 'label ne doit pas être vide');
|
||||
}
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function lesDatesDeVacancesSontCoherentes(): void
|
||||
{
|
||||
$vacations = $this->provider->vacancesParZone(SchoolZone::A, self::ACADEMIC_YEAR);
|
||||
|
||||
foreach ($vacations as $vacation) {
|
||||
self::assertSame(CalendarEntryType::VACATION, $vacation->type);
|
||||
self::assertGreaterThanOrEqual(
|
||||
$vacation->startDate->format('Y-m-d'),
|
||||
$vacation->endDate->format('Y-m-d'),
|
||||
sprintf('La fin (%s) doit être >= au début (%s) pour "%s"',
|
||||
$vacation->endDate->format('Y-m-d'),
|
||||
$vacation->startDate->format('Y-m-d'),
|
||||
$vacation->label,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function leFichierJsonEstCreeEnCache(): void
|
||||
{
|
||||
$expectedFile = $this->tempDir . '/official-holidays-' . self::ACADEMIC_YEAR . '.json';
|
||||
|
||||
self::assertFileDoesNotExist($expectedFile);
|
||||
|
||||
$this->provider->toutesEntreesOfficielles(SchoolZone::A, self::ACADEMIC_YEAR);
|
||||
|
||||
self::assertFileExists($expectedFile);
|
||||
|
||||
$content = json_decode(file_get_contents($expectedFile), true);
|
||||
self::assertArrayHasKey('holidays', $content);
|
||||
self::assertArrayHasKey('vacations', $content);
|
||||
self::assertArrayHasKey('A', $content['vacations']);
|
||||
self::assertArrayHasKey('B', $content['vacations']);
|
||||
self::assertArrayHasKey('C', $content['vacations']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function leDeuxiemeAppelUtiliseLeCacheSansToucherLApi(): void
|
||||
{
|
||||
// Premier appel : fetch API + sauvegarde
|
||||
$first = $this->provider->toutesEntreesOfficielles(SchoolZone::A, self::ACADEMIC_YEAR);
|
||||
|
||||
// Recréer un provider avec un HttpClient qui échoue systématiquement
|
||||
// Si le cache fonctionne, il ne touchera pas au HttpClient
|
||||
$cachedProvider = new JsonOfficialCalendarProvider(
|
||||
dataDirectory: $this->tempDir,
|
||||
httpClient: HttpClient::create(), // pas utilisé si le fichier existe
|
||||
holidaysCalculator: new FrenchPublicHolidaysCalculator(),
|
||||
logger: new NullLogger(),
|
||||
);
|
||||
|
||||
$second = $cachedProvider->toutesEntreesOfficielles(SchoolZone::A, self::ACADEMIC_YEAR);
|
||||
|
||||
self::assertCount(count($first), $second);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function vacancesToussaintEtNoelPresentes(): void
|
||||
{
|
||||
$vacations = $this->provider->vacancesParZone(SchoolZone::A, self::ACADEMIC_YEAR);
|
||||
$labels = array_map(static fn ($v) => $v->label, $vacations);
|
||||
|
||||
self::assertNotEmpty(
|
||||
array_filter($labels, static fn (string $l) => str_contains(strtolower($l), 'toussaint')),
|
||||
'Les vacances de la Toussaint doivent être présentes',
|
||||
);
|
||||
self::assertNotEmpty(
|
||||
array_filter($labels, static fn (string $l) => str_contains(strtolower($l), 'noël') || str_contains(strtolower($l), 'noel')),
|
||||
'Les vacances de Noël doivent être présentes',
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user