Files
Classeo/backend/tests/Integration/Administration/Infrastructure/Service/GouvFrCalendarApiTest.php
Mathias STRASSER e06fd5424d 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.
2026-02-18 12:09:19 +01:00

166 lines
6.1 KiB
PHP

<?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',
);
}
}