Files
Classeo/backend/tests/Unit/Scolarite/Application/Command/CreateScheduleSlot/CreateScheduleSlotHandlerTest.php
Mathias STRASSER d103b34023
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: Permettre la création et modification de l'emploi du temps des classes
L'administration a besoin de construire et maintenir les emplois du temps
hebdomadaires pour chaque classe, en s'assurant que les enseignants ne sont
pas en conflit (même créneau, classes différentes) et que les affectations
enseignant-matière-classe sont respectées.

Cette implémentation couvre le CRUD complet des créneaux (ScheduleSlot),
la détection de conflits (classe, enseignant, salle) avec possibilité de
forcer, la validation des affectations côté serveur (AC2), l'intégration
calendrier pour les jours bloqués, une vue mobile-first avec onglets jour
par jour, et le drag-and-drop pour réorganiser les créneaux sur desktop.
2026-03-03 19:55:11 +01:00

197 lines
6.5 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Tests\Unit\Scolarite\Application\Command\CreateScheduleSlot;
use App\Administration\Domain\Model\SchoolClass\ClassId;
use App\Administration\Domain\Model\Subject\SubjectId;
use App\Administration\Domain\Model\User\UserId;
use App\Scolarite\Application\Command\CreateScheduleSlot\CreateScheduleSlotCommand;
use App\Scolarite\Application\Command\CreateScheduleSlot\CreateScheduleSlotHandler;
use App\Scolarite\Application\Port\EnseignantAffectationChecker;
use App\Scolarite\Domain\Exception\EnseignantNonAffecteException;
use App\Scolarite\Domain\Model\Schedule\DayOfWeek;
use App\Scolarite\Domain\Model\Schedule\ScheduleSlot;
use App\Scolarite\Domain\Service\ScheduleConflictDetector;
use App\Scolarite\Infrastructure\Persistence\InMemory\InMemoryScheduleSlotRepository;
use App\Shared\Domain\Clock;
use App\Shared\Domain\Tenant\TenantId;
use DateTimeImmutable;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
final class CreateScheduleSlotHandlerTest extends TestCase
{
private const string TENANT_ID = '550e8400-e29b-41d4-a716-446655440001';
private const string CLASS_ID = '550e8400-e29b-41d4-a716-446655440020';
private const string SUBJECT_ID = '550e8400-e29b-41d4-a716-446655440030';
private const string TEACHER_ID = '550e8400-e29b-41d4-a716-446655440010';
private InMemoryScheduleSlotRepository $repository;
private CreateScheduleSlotHandler $handler;
protected function setUp(): void
{
$this->repository = new InMemoryScheduleSlotRepository();
$clock = new class implements Clock {
public function now(): DateTimeImmutable
{
return new DateTimeImmutable('2026-03-02 10:00:00');
}
};
$this->handler = new CreateScheduleSlotHandler(
$this->repository,
new ScheduleConflictDetector($this->repository),
$this->createAlwaysAssignedChecker(),
$clock,
);
}
#[Test]
public function createsSlotSuccessfully(): void
{
$result = ($this->handler)($this->createCommand());
/** @var ScheduleSlot $slot */
$slot = $result['slot'];
self::assertTrue($slot->classId->equals(ClassId::fromString(self::CLASS_ID)));
self::assertSame(DayOfWeek::MONDAY, $slot->dayOfWeek);
self::assertSame('08:00', $slot->timeSlot->startTime);
self::assertSame('09:00', $slot->timeSlot->endTime);
self::assertEmpty($result['conflicts']);
}
#[Test]
public function savesSlotToRepository(): void
{
$result = ($this->handler)($this->createCommand());
/** @var ScheduleSlot $slot */
$slot = $result['slot'];
$found = $this->repository->findById($slot->id, TenantId::fromString(self::TENANT_ID));
self::assertNotNull($found);
self::assertTrue($found->id->equals($slot->id));
}
#[Test]
public function detectsConflictsWithoutSaving(): void
{
// Créer un slot existant
($this->handler)($this->createCommand());
// Même enseignant, même créneau, classe différente
$result = ($this->handler)(new CreateScheduleSlotCommand(
tenantId: self::TENANT_ID,
classId: '550e8400-e29b-41d4-a716-446655440021',
subjectId: self::SUBJECT_ID,
teacherId: self::TEACHER_ID,
dayOfWeek: DayOfWeek::MONDAY->value,
startTime: '08:30',
endTime: '09:30',
room: null,
));
self::assertNotEmpty($result['conflicts']);
// Le slot conflictuel ne devrait PAS être sauvegardé
$allSlots = $this->repository->findByTeacher(
UserId::fromString(self::TEACHER_ID),
TenantId::fromString(self::TENANT_ID),
);
self::assertCount(1, $allSlots);
}
#[Test]
public function forceSavesSlotWithConflicts(): void
{
($this->handler)($this->createCommand());
$result = ($this->handler)(new CreateScheduleSlotCommand(
tenantId: self::TENANT_ID,
classId: '550e8400-e29b-41d4-a716-446655440021',
subjectId: self::SUBJECT_ID,
teacherId: self::TEACHER_ID,
dayOfWeek: DayOfWeek::MONDAY->value,
startTime: '08:30',
endTime: '09:30',
room: null,
forceConflicts: true,
));
self::assertNotEmpty($result['conflicts']);
// Malgré les conflits, le slot est sauvegardé
$allSlots = $this->repository->findByTeacher(
UserId::fromString(self::TEACHER_ID),
TenantId::fromString(self::TENANT_ID),
);
self::assertCount(2, $allSlots);
}
#[Test]
public function rejectsUnassignedTeacher(): void
{
$clock = new class implements Clock {
public function now(): DateTimeImmutable
{
return new DateTimeImmutable('2026-03-02 10:00:00');
}
};
$handler = new CreateScheduleSlotHandler(
$this->repository,
new ScheduleConflictDetector($this->repository),
$this->createNeverAssignedChecker(),
$clock,
);
$this->expectException(EnseignantNonAffecteException::class);
($handler)($this->createCommand());
}
private function createCommand(): CreateScheduleSlotCommand
{
return new CreateScheduleSlotCommand(
tenantId: self::TENANT_ID,
classId: self::CLASS_ID,
subjectId: self::SUBJECT_ID,
teacherId: self::TEACHER_ID,
dayOfWeek: DayOfWeek::MONDAY->value,
startTime: '08:00',
endTime: '09:00',
room: null,
);
}
private function createAlwaysAssignedChecker(): EnseignantAffectationChecker
{
return new class implements EnseignantAffectationChecker {
public function estAffecte(
UserId $teacherId,
ClassId $classId,
SubjectId $subjectId,
TenantId $tenantId,
): bool {
return true;
}
};
}
private function createNeverAssignedChecker(): EnseignantAffectationChecker
{
return new class implements EnseignantAffectationChecker {
public function estAffecte(
UserId $teacherId,
ClassId $classId,
SubjectId $subjectId,
TenantId $tenantId,
): bool {
return false;
}
};
}
}