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.
183 lines
6.6 KiB
PHP
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',
|
|
);
|
|
}
|
|
}
|