Files
Classeo/backend/tests/Integration/Administration/Infrastructure/Service/GouvFrCalendarApiTest.php
Mathias STRASSER 18c54e6d67
Some checks failed
CI / Backend Tests (push) Has been cancelled
CI / Frontend Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Naming Conventions (push) Has been cancelled
CI / Build Check (push) Has been cancelled
feat: Provisionner automatiquement un nouvel établissement
Lorsqu'un super-admin crée un établissement via l'interface, le système
doit automatiquement créer la base tenant, exécuter les migrations,
créer le premier utilisateur admin et envoyer l'invitation — le tout
de manière asynchrone pour ne pas bloquer la réponse HTTP.

Ce mécanisme rend chaque établissement opérationnel dès sa création
sans intervention manuelle sur l'infrastructure.
2026-04-11 20:52:41 +02:00

183 lines
6.6 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 Throwable;
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
{
// Skip si l'API externe est injoignable (timeout réseau, DNS, etc.)
try {
$check = HttpClient::create()->request('GET', 'https://data.education.gouv.fr', [
'timeout' => 5,
]);
$check->getStatusCode();
} catch (Throwable) {
self::markTestSkipped('API data.education.gouv.fr injoignable — test ignoré.');
}
$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
{
if (!isset($this->tempDir) || !is_dir($this->tempDir)) {
return;
}
// 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',
);
}
}