Files
Classeo/backend/tests/Unit/Scolarite/Infrastructure/Messaging/OnExceptionDevoirDemandeeHandlerTest.php
Mathias STRASSER 14c7849179
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 aux enseignants de contourner les règles de devoirs avec justification
Akeneo permet de configurer des règles de devoirs en mode Hard qui bloquent
totalement la création. Or certains cas légitimes (sorties scolaires, événements
exceptionnels) nécessitent de passer outre ces règles. Sans mécanisme d'exception,
l'enseignant est bloqué et doit contacter manuellement la direction.

Cette implémentation ajoute un flux complet d'exception : l'enseignant justifie
sa demande (min 20 caractères), le devoir est créé immédiatement, et la direction
est notifiée par email. Le handler vérifie côté serveur que les règles sont
réellement bloquantes avant d'accepter l'exception, empêchant toute fabrication
de fausses exceptions via l'API. La direction dispose d'un rapport filtrable
par période, enseignant et type de règle.
2026-03-20 18:35:02 +01:00

225 lines
8.4 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Tests\Unit\Scolarite\Infrastructure\Messaging;
use App\Administration\Domain\Model\SchoolClass\ClassId;
use App\Administration\Domain\Model\Subject\SubjectId;
use App\Administration\Domain\Model\User\Email;
use App\Administration\Domain\Model\User\Role;
use App\Administration\Domain\Model\User\StatutCompte;
use App\Administration\Domain\Model\User\User;
use App\Administration\Domain\Model\User\UserId;
use App\Administration\Domain\Repository\UserRepository;
use App\Scolarite\Domain\Event\ExceptionDevoirDemandee;
use App\Scolarite\Domain\Model\Homework\Homework;
use App\Scolarite\Domain\Model\Homework\HomeworkId;
use App\Scolarite\Domain\Model\Homework\HomeworkRuleExceptionId;
use App\Scolarite\Domain\Repository\HomeworkRepository;
use App\Scolarite\Infrastructure\Messaging\OnExceptionDevoirDemandeeHandler;
use App\Shared\Domain\Tenant\TenantId;
use DateTimeImmutable;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger;
use RuntimeException;
use Symfony\Component\Mailer\MailerInterface;
use Twig\Environment;
final class OnExceptionDevoirDemandeeHandlerTest extends TestCase
{
private const string TENANT_ID = '550e8400-e29b-41d4-a716-446655440001';
private const string TEACHER_ID = '550e8400-e29b-41d4-a716-446655440010';
#[Test]
public function itSendsEmailToAllDirectorsInTenant(): void
{
$mailer = $this->createMock(MailerInterface::class);
$twig = $this->createMock(Environment::class);
$homeworkRepo = $this->createMock(HomeworkRepository::class);
$userRepo = $this->createMock(UserRepository::class);
$homework = $this->createHomework();
$homeworkRepo->method('findById')->willReturn($homework);
$teacher = $this->createUser('teacher@school.fr', [Role::PROF], 'Jean', 'Dupont');
$userRepo->method('findById')->willReturn($teacher);
$userRepo->method('findAllByTenant')->willReturn([
$this->createUser('director@school.fr', [Role::ADMIN], 'Marie', 'Martin'),
$this->createUser('teacher@school.fr', [Role::PROF], 'Jean', 'Dupont'),
$this->createUser('parent@school.fr', [Role::PARENT], 'Pierre', 'Durand'),
]);
$twig->method('render')->willReturn('<html>notification</html>');
$mailer->expects(self::exactly(1))->method('send');
$handler = new OnExceptionDevoirDemandeeHandler(
$homeworkRepo,
$userRepo,
$mailer,
$twig,
new NullLogger(),
);
($handler)($this->createEvent());
}
#[Test]
public function itSkipsWhenNoDirectorsInTenant(): void
{
$mailer = $this->createMock(MailerInterface::class);
$twig = $this->createMock(Environment::class);
$homeworkRepo = $this->createMock(HomeworkRepository::class);
$userRepo = $this->createMock(UserRepository::class);
$homeworkRepo->method('findById')->willReturn($this->createHomework());
$userRepo->method('findById')->willReturn($this->createUser('teacher@school.fr', [Role::PROF]));
$userRepo->method('findAllByTenant')->willReturn([
$this->createUser('teacher@school.fr', [Role::PROF]),
]);
$mailer->expects(self::never())->method('send');
$handler = new OnExceptionDevoirDemandeeHandler(
$homeworkRepo,
$userRepo,
$mailer,
$twig,
new NullLogger(),
);
($handler)($this->createEvent());
}
#[Test]
public function itHandlesMailerFailureGracefully(): void
{
$mailer = $this->createMock(MailerInterface::class);
$twig = $this->createMock(Environment::class);
$homeworkRepo = $this->createMock(HomeworkRepository::class);
$userRepo = $this->createMock(UserRepository::class);
$homeworkRepo->method('findById')->willReturn($this->createHomework());
$userRepo->method('findById')->willReturn($this->createUser('teacher@school.fr', [Role::PROF]));
$userRepo->method('findAllByTenant')->willReturn([
$this->createUser('director@school.fr', [Role::ADMIN]),
]);
$twig->method('render')->willReturn('<html>notification</html>');
$mailer->method('send')->willThrowException(new RuntimeException('SMTP error'));
$handler = new OnExceptionDevoirDemandeeHandler(
$homeworkRepo,
$userRepo,
$mailer,
$twig,
new NullLogger(),
);
($handler)($this->createEvent());
$this->addToAssertionCount(1);
}
#[Test]
public function itPassesCorrectDataToTemplate(): void
{
$mailer = $this->createMock(MailerInterface::class);
$twig = $this->createMock(Environment::class);
$homeworkRepo = $this->createMock(HomeworkRepository::class);
$userRepo = $this->createMock(UserRepository::class);
$homeworkRepo->method('findById')->willReturn($this->createHomework());
$teacher = $this->createUser('teacher@school.fr', [Role::PROF], 'Jean', 'Dupont');
$userRepo->method('findById')->willReturn($teacher);
$userRepo->method('findAllByTenant')->willReturn([
$this->createUser('director@school.fr', [Role::ADMIN]),
]);
$twig->expects(self::once())
->method('render')
->with(
'emails/homework_exception_notification.html.twig',
self::callback(static function (array $params): bool {
return $params['teacherName'] === 'Jean Dupont'
&& $params['homeworkTitle'] === 'Exercices chapitre 5'
&& $params['ruleTypes'] === ['minimum_delay']
&& $params['justification'] === 'Sortie scolaire prévue, devoir urgent.'
&& $params['dueDate'] === '15/04/2026';
}),
)
->willReturn('<html>notification</html>');
$mailer->expects(self::once())->method('send');
$handler = new OnExceptionDevoirDemandeeHandler(
$homeworkRepo,
$userRepo,
$mailer,
$twig,
new NullLogger(),
);
($handler)($this->createEvent());
}
private function createEvent(): ExceptionDevoirDemandee
{
return new ExceptionDevoirDemandee(
exceptionId: HomeworkRuleExceptionId::generate(),
tenantId: TenantId::fromString(self::TENANT_ID),
homeworkId: HomeworkId::fromString('550e8400-e29b-41d4-a716-446655440050'),
ruleTypes: ['minimum_delay'],
justification: 'Sortie scolaire prévue, devoir urgent.',
createdBy: UserId::fromString(self::TEACHER_ID),
occurredOn: new DateTimeImmutable('2026-03-19 10:00:00'),
);
}
private function createHomework(): Homework
{
return Homework::reconstitute(
id: HomeworkId::fromString('550e8400-e29b-41d4-a716-446655440050'),
tenantId: TenantId::fromString(self::TENANT_ID),
classId: ClassId::fromString('550e8400-e29b-41d4-a716-446655440020'),
subjectId: SubjectId::fromString('550e8400-e29b-41d4-a716-446655440030'),
teacherId: UserId::fromString(self::TEACHER_ID),
title: 'Exercices chapitre 5',
description: 'Faire les exercices 1 à 10',
dueDate: new DateTimeImmutable('2026-04-15'),
status: \App\Scolarite\Domain\Model\Homework\HomeworkStatus::PUBLISHED,
createdAt: new DateTimeImmutable('2026-03-19 10:00:00'),
updatedAt: new DateTimeImmutable('2026-03-19 10:00:00'),
);
}
/**
* @param Role[] $roles
*/
private function createUser(
string $email,
array $roles,
string $firstName = 'Test',
string $lastName = 'User',
): User {
return User::reconstitute(
id: UserId::generate(),
email: new Email($email),
roles: $roles,
tenantId: TenantId::fromString(self::TENANT_ID),
schoolName: 'École Test',
statut: StatutCompte::ACTIF,
dateNaissance: null,
createdAt: new DateTimeImmutable('2026-01-01'),
hashedPassword: 'hashed',
activatedAt: new DateTimeImmutable('2026-01-02'),
consentementParental: null,
firstName: $firstName,
lastName: $lastName,
);
}
}