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.
220 lines
8.1 KiB
PHP
220 lines
8.1 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Tests\Unit\Administration\Infrastructure\Persistence\Doctrine;
|
|
|
|
use App\Administration\Domain\Exception\CalendrierNonTrouveException;
|
|
use App\Administration\Domain\Model\SchoolCalendar\CalendarEntry;
|
|
use App\Administration\Domain\Model\SchoolCalendar\CalendarEntryId;
|
|
use App\Administration\Domain\Model\SchoolCalendar\CalendarEntryType;
|
|
use App\Administration\Domain\Model\SchoolCalendar\SchoolCalendar;
|
|
use App\Administration\Domain\Model\SchoolCalendar\SchoolZone;
|
|
use App\Administration\Domain\Model\SchoolClass\AcademicYearId;
|
|
use App\Administration\Infrastructure\Persistence\Doctrine\DoctrineSchoolCalendarRepository;
|
|
use App\Shared\Domain\Tenant\TenantId;
|
|
use DateTimeImmutable;
|
|
use Doctrine\DBAL\Connection;
|
|
use PHPUnit\Framework\Attributes\Test;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
final class DoctrineSchoolCalendarRepositoryTest extends TestCase
|
|
{
|
|
private const string TENANT_ID = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890';
|
|
private const string ACADEMIC_YEAR_ID = '550e8400-e29b-41d4-a716-446655440010';
|
|
|
|
#[Test]
|
|
public function saveDeletesExistingEntriesThenInsertsNew(): void
|
|
{
|
|
$connection = $this->createMock(Connection::class);
|
|
|
|
$calendar = SchoolCalendar::initialiser(
|
|
tenantId: TenantId::fromString(self::TENANT_ID),
|
|
academicYearId: AcademicYearId::fromString(self::ACADEMIC_YEAR_ID),
|
|
);
|
|
$calendar->ajouterEntree(new CalendarEntry(
|
|
id: CalendarEntryId::fromString('550e8400-e29b-41d4-a716-446655440020'),
|
|
type: CalendarEntryType::HOLIDAY,
|
|
startDate: new DateTimeImmutable('2025-11-01'),
|
|
endDate: new DateTimeImmutable('2025-11-01'),
|
|
label: 'Toussaint',
|
|
));
|
|
|
|
$connection->expects(self::once())
|
|
->method('transactional')
|
|
->willReturnCallback(static function (callable $callback) { $callback(); });
|
|
|
|
// Expect: 1 DELETE + 1 INSERT
|
|
$connection->expects(self::exactly(2))
|
|
->method('executeStatement')
|
|
->with(
|
|
self::logicalOr(
|
|
self::stringContains('DELETE FROM school_calendar_entries'),
|
|
self::stringContains('INSERT INTO school_calendar_entries'),
|
|
),
|
|
self::isType('array'),
|
|
);
|
|
|
|
$repository = new DoctrineSchoolCalendarRepository($connection);
|
|
$repository->save($calendar);
|
|
}
|
|
|
|
#[Test]
|
|
public function saveWithMultipleEntriesExecutesMultipleInserts(): void
|
|
{
|
|
$connection = $this->createMock(Connection::class);
|
|
|
|
$calendar = SchoolCalendar::initialiser(
|
|
tenantId: TenantId::fromString(self::TENANT_ID),
|
|
academicYearId: AcademicYearId::fromString(self::ACADEMIC_YEAR_ID),
|
|
);
|
|
$calendar->ajouterEntree(new CalendarEntry(
|
|
id: CalendarEntryId::fromString('550e8400-e29b-41d4-a716-446655440020'),
|
|
type: CalendarEntryType::HOLIDAY,
|
|
startDate: new DateTimeImmutable('2025-11-01'),
|
|
endDate: new DateTimeImmutable('2025-11-01'),
|
|
label: 'Toussaint',
|
|
));
|
|
$calendar->ajouterEntree(new CalendarEntry(
|
|
id: CalendarEntryId::fromString('550e8400-e29b-41d4-a716-446655440021'),
|
|
type: CalendarEntryType::VACATION,
|
|
startDate: new DateTimeImmutable('2025-12-21'),
|
|
endDate: new DateTimeImmutable('2026-01-05'),
|
|
label: 'Noël',
|
|
));
|
|
|
|
$connection->expects(self::once())
|
|
->method('transactional')
|
|
->willReturnCallback(static function (callable $callback) { $callback(); });
|
|
|
|
// Expect: 1 DELETE + 2 INSERTs = 3 calls
|
|
$connection->expects(self::exactly(3))
|
|
->method('executeStatement');
|
|
|
|
$repository = new DoctrineSchoolCalendarRepository($connection);
|
|
$repository->save($calendar);
|
|
}
|
|
|
|
#[Test]
|
|
public function findByTenantAndYearReturnsNullWhenNoEntries(): void
|
|
{
|
|
$connection = $this->createMock(Connection::class);
|
|
$connection->method('fetchAllAssociative')->willReturn([]);
|
|
|
|
$repository = new DoctrineSchoolCalendarRepository($connection);
|
|
|
|
$calendar = $repository->findByTenantAndYear(
|
|
TenantId::fromString(self::TENANT_ID),
|
|
AcademicYearId::fromString(self::ACADEMIC_YEAR_ID),
|
|
);
|
|
|
|
self::assertNull($calendar);
|
|
}
|
|
|
|
#[Test]
|
|
public function findByTenantAndYearReturnsCalendarWithEntries(): void
|
|
{
|
|
$connection = $this->createMock(Connection::class);
|
|
$connection->method('fetchAllAssociative')
|
|
->willReturn([
|
|
$this->makeRow('550e8400-e29b-41d4-a716-446655440020', 'holiday', '2025-11-01', '2025-11-01', 'Toussaint', null, 'A'),
|
|
$this->makeRow('550e8400-e29b-41d4-a716-446655440021', 'vacation', '2025-12-21', '2026-01-05', 'Noël', 'Vacances de Noël', 'A'),
|
|
]);
|
|
|
|
$repository = new DoctrineSchoolCalendarRepository($connection);
|
|
|
|
$calendar = $repository->findByTenantAndYear(
|
|
TenantId::fromString(self::TENANT_ID),
|
|
AcademicYearId::fromString(self::ACADEMIC_YEAR_ID),
|
|
);
|
|
|
|
self::assertNotNull($calendar);
|
|
self::assertCount(2, $calendar->entries());
|
|
self::assertSame(SchoolZone::A, $calendar->zone);
|
|
self::assertSame('Toussaint', $calendar->entries()[0]->label);
|
|
self::assertSame('Noël', $calendar->entries()[1]->label);
|
|
}
|
|
|
|
#[Test]
|
|
public function findByTenantAndYearHandlesNullZone(): void
|
|
{
|
|
$connection = $this->createMock(Connection::class);
|
|
$connection->method('fetchAllAssociative')
|
|
->willReturn([
|
|
$this->makeRow('550e8400-e29b-41d4-a716-446655440020', 'pedagogical', '2025-03-14', '2025-03-14', 'Formation', null, null),
|
|
]);
|
|
|
|
$repository = new DoctrineSchoolCalendarRepository($connection);
|
|
|
|
$calendar = $repository->findByTenantAndYear(
|
|
TenantId::fromString(self::TENANT_ID),
|
|
AcademicYearId::fromString(self::ACADEMIC_YEAR_ID),
|
|
);
|
|
|
|
self::assertNotNull($calendar);
|
|
self::assertNull($calendar->zone);
|
|
}
|
|
|
|
#[Test]
|
|
public function getByTenantAndYearThrowsWhenNotFound(): void
|
|
{
|
|
$connection = $this->createMock(Connection::class);
|
|
$connection->method('fetchAllAssociative')->willReturn([]);
|
|
|
|
$repository = new DoctrineSchoolCalendarRepository($connection);
|
|
|
|
$this->expectException(CalendrierNonTrouveException::class);
|
|
|
|
$repository->getByTenantAndYear(
|
|
TenantId::fromString(self::TENANT_ID),
|
|
AcademicYearId::fromString(self::ACADEMIC_YEAR_ID),
|
|
);
|
|
}
|
|
|
|
#[Test]
|
|
public function getByTenantAndYearReturnsCalendarWhenFound(): void
|
|
{
|
|
$connection = $this->createMock(Connection::class);
|
|
$connection->method('fetchAllAssociative')
|
|
->willReturn([
|
|
$this->makeRow('550e8400-e29b-41d4-a716-446655440020', 'holiday', '2025-05-01', '2025-05-01', 'Fête du travail', null, 'B'),
|
|
]);
|
|
|
|
$repository = new DoctrineSchoolCalendarRepository($connection);
|
|
|
|
$calendar = $repository->getByTenantAndYear(
|
|
TenantId::fromString(self::TENANT_ID),
|
|
AcademicYearId::fromString(self::ACADEMIC_YEAR_ID),
|
|
);
|
|
|
|
self::assertCount(1, $calendar->entries());
|
|
self::assertSame(SchoolZone::B, $calendar->zone);
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function makeRow(
|
|
string $id,
|
|
string $entryType,
|
|
string $startDate,
|
|
string $endDate,
|
|
string $label,
|
|
?string $description,
|
|
?string $zone,
|
|
): array {
|
|
return [
|
|
'id' => $id,
|
|
'tenant_id' => self::TENANT_ID,
|
|
'academic_year_id' => self::ACADEMIC_YEAR_ID,
|
|
'entry_type' => $entryType,
|
|
'start_date' => $startDate,
|
|
'end_date' => $endDate,
|
|
'label' => $label,
|
|
'description' => $description,
|
|
'zone' => $zone,
|
|
'created_at' => '2026-02-17T10:00:00+00:00',
|
|
];
|
|
}
|
|
}
|