Files
Classeo/backend/tests/Unit/Administration/Application/Command/ConfigureCalendar/ConfigureCalendarHandlerTest.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

201 lines
8.0 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Tests\Unit\Administration\Application\Command\ConfigureCalendar;
use App\Administration\Application\Command\ConfigureCalendar\ConfigureCalendarCommand;
use App\Administration\Application\Command\ConfigureCalendar\ConfigureCalendarHandler;
use App\Administration\Domain\Model\SchoolCalendar\CalendarEntryType;
use App\Administration\Domain\Model\SchoolCalendar\SchoolZone;
use App\Administration\Domain\Model\SchoolClass\AcademicYearId;
use App\Administration\Infrastructure\Persistence\InMemory\InMemorySchoolCalendarRepository;
use App\Administration\Infrastructure\Service\FrenchPublicHolidaysCalculator;
use App\Administration\Infrastructure\Service\JsonOfficialCalendarProvider;
use App\Shared\Domain\Clock;
use App\Shared\Domain\Tenant\TenantId;
use DateTimeImmutable;
use function json_encode;
use const JSON_THROW_ON_ERROR;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger;
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\Response\MockResponse;
use function sys_get_temp_dir;
final class ConfigureCalendarHandlerTest extends TestCase
{
private const string TENANT_ID = '550e8400-e29b-41d4-a716-446655440001';
private const string ACADEMIC_YEAR_ID = '550e8400-e29b-41d4-a716-446655440010';
private InMemorySchoolCalendarRepository $repository;
private ConfigureCalendarHandler $handler;
private string $tempDir;
protected function setUp(): void
{
$this->tempDir = sys_get_temp_dir() . '/classeo-handler-test-' . uniqid();
$this->repository = new InMemorySchoolCalendarRepository();
$clock = new class implements Clock {
public function now(): DateTimeImmutable
{
return new DateTimeImmutable('2026-02-17 10:00:00');
}
};
$this->handler = new ConfigureCalendarHandler(
calendarRepository: $this->repository,
calendarProvider: new JsonOfficialCalendarProvider(
dataDirectory: $this->tempDir,
httpClient: new MockHttpClient($this->mockApiResponse()),
holidaysCalculator: new FrenchPublicHolidaysCalculator(),
logger: new NullLogger(),
),
clock: $clock,
);
}
protected function tearDown(): void
{
$files = glob($this->tempDir . '/*');
if ($files !== false) {
foreach ($files as $file) {
unlink($file);
}
}
if (is_dir($this->tempDir)) {
rmdir($this->tempDir);
}
}
#[Test]
public function itConfiguresCalendarWithZoneA(): void
{
$command = new ConfigureCalendarCommand(
tenantId: self::TENANT_ID,
academicYearId: self::ACADEMIC_YEAR_ID,
zone: 'A',
academicYear: '2024-2025',
);
$calendar = ($this->handler)($command);
self::assertSame(SchoolZone::A, $calendar->zone);
self::assertNotEmpty($calendar->entries());
}
#[Test]
public function itImportsHolidaysAndVacations(): void
{
$command = new ConfigureCalendarCommand(
tenantId: self::TENANT_ID,
academicYearId: self::ACADEMIC_YEAR_ID,
zone: 'B',
academicYear: '2024-2025',
);
$calendar = ($this->handler)($command);
$holidays = array_filter(
$calendar->entries(),
static fn ($e) => $e->type === CalendarEntryType::HOLIDAY,
);
$vacations = array_filter(
$calendar->entries(),
static fn ($e) => $e->type === CalendarEntryType::VACATION,
);
self::assertNotEmpty($holidays);
self::assertNotEmpty($vacations);
}
#[Test]
public function itSavesCalendarToRepository(): void
{
$command = new ConfigureCalendarCommand(
tenantId: self::TENANT_ID,
academicYearId: self::ACADEMIC_YEAR_ID,
zone: 'C',
academicYear: '2024-2025',
);
($this->handler)($command);
$saved = $this->repository->findByTenantAndYear(
TenantId::fromString(self::TENANT_ID),
AcademicYearId::fromString(self::ACADEMIC_YEAR_ID),
);
self::assertNotNull($saved);
self::assertSame(SchoolZone::C, $saved->zone);
}
#[Test]
public function itReconfiguresExistingCalendar(): void
{
$command = new ConfigureCalendarCommand(
tenantId: self::TENANT_ID,
academicYearId: self::ACADEMIC_YEAR_ID,
zone: 'A',
academicYear: '2024-2025',
);
($this->handler)($command);
$commandB = new ConfigureCalendarCommand(
tenantId: self::TENANT_ID,
academicYearId: self::ACADEMIC_YEAR_ID,
zone: 'B',
academicYear: '2024-2025',
);
$calendar = ($this->handler)($commandB);
self::assertSame(SchoolZone::B, $calendar->zone);
}
#[Test]
public function itRecordsCalendrierConfigureEvent(): void
{
$command = new ConfigureCalendarCommand(
tenantId: self::TENANT_ID,
academicYearId: self::ACADEMIC_YEAR_ID,
zone: 'A',
academicYear: '2024-2025',
);
$calendar = ($this->handler)($command);
$events = $calendar->pullDomainEvents();
self::assertNotEmpty($events);
}
private function mockApiResponse(): MockResponse
{
$records = [
['description' => 'Vacances de la Toussaint', 'start_date' => '2024-10-18T23:00:00+00:00', 'end_date' => '2024-11-03T23:00:00+00:00', 'zones' => 'Zone A'],
['description' => 'Vacances de Noël', 'start_date' => '2024-12-20T23:00:00+00:00', 'end_date' => '2025-01-05T23:00:00+00:00', 'zones' => 'Zone A'],
['description' => 'Vacances d\'hiver', 'start_date' => '2025-02-21T23:00:00+00:00', 'end_date' => '2025-03-09T23:00:00+00:00', 'zones' => 'Zone A'],
['description' => 'Vacances de printemps', 'start_date' => '2025-04-18T22:00:00+00:00', 'end_date' => '2025-05-04T22:00:00+00:00', 'zones' => 'Zone A'],
['description' => 'Vacances d\'été', 'start_date' => '2025-07-04T22:00:00+00:00', 'end_date' => '2025-08-31T22:00:00+00:00', 'zones' => 'Zone A'],
['description' => 'Vacances de la Toussaint', 'start_date' => '2024-10-18T23:00:00+00:00', 'end_date' => '2024-11-03T23:00:00+00:00', 'zones' => 'Zone B'],
['description' => 'Vacances de Noël', 'start_date' => '2024-12-20T23:00:00+00:00', 'end_date' => '2025-01-05T23:00:00+00:00', 'zones' => 'Zone B'],
['description' => 'Vacances d\'hiver', 'start_date' => '2025-02-07T23:00:00+00:00', 'end_date' => '2025-02-23T23:00:00+00:00', 'zones' => 'Zone B'],
['description' => 'Vacances de printemps', 'start_date' => '2025-04-04T22:00:00+00:00', 'end_date' => '2025-04-20T22:00:00+00:00', 'zones' => 'Zone B'],
['description' => 'Vacances d\'été', 'start_date' => '2025-07-04T22:00:00+00:00', 'end_date' => '2025-08-31T22:00:00+00:00', 'zones' => 'Zone B'],
['description' => 'Vacances de la Toussaint', 'start_date' => '2024-10-18T23:00:00+00:00', 'end_date' => '2024-11-03T23:00:00+00:00', 'zones' => 'Zone C'],
['description' => 'Vacances de Noël', 'start_date' => '2024-12-20T23:00:00+00:00', 'end_date' => '2025-01-05T23:00:00+00:00', 'zones' => 'Zone C'],
['description' => 'Vacances d\'hiver', 'start_date' => '2025-02-14T23:00:00+00:00', 'end_date' => '2025-03-02T23:00:00+00:00', 'zones' => 'Zone C'],
['description' => 'Vacances de printemps', 'start_date' => '2025-04-11T22:00:00+00:00', 'end_date' => '2025-04-27T22:00:00+00:00', 'zones' => 'Zone C'],
['description' => 'Vacances d\'été', 'start_date' => '2025-07-04T22:00:00+00:00', 'end_date' => '2025-08-31T22:00:00+00:00', 'zones' => 'Zone C'],
];
return new MockResponse(json_encode(['results' => $records], JSON_THROW_ON_ERROR), ['http_code' => 200]);
}
}