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.
228 lines
7.8 KiB
PHP
228 lines
7.8 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Tests\Unit\Scolarite\Application\Command\SaveAppreciation;
|
|
|
|
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\SaveAppreciation\SaveAppreciationCommand;
|
|
use App\Scolarite\Application\Command\SaveAppreciation\SaveAppreciationHandler;
|
|
use App\Scolarite\Application\Port\EnseignantAffectationChecker;
|
|
use App\Scolarite\Application\Service\AutorisationSaisieNotesChecker;
|
|
use App\Scolarite\Domain\Exception\AppreciationTropLongueException;
|
|
use App\Scolarite\Domain\Exception\GradeNotFoundException;
|
|
use App\Scolarite\Domain\Exception\NonProprietaireDeLEvaluationException;
|
|
use App\Scolarite\Domain\Model\Evaluation\Coefficient;
|
|
use App\Scolarite\Domain\Model\Evaluation\Evaluation;
|
|
use App\Scolarite\Domain\Model\Evaluation\EvaluationId;
|
|
use App\Scolarite\Domain\Model\Evaluation\EvaluationStatus;
|
|
use App\Scolarite\Domain\Model\Evaluation\GradeScale;
|
|
use App\Scolarite\Domain\Model\Grade\Grade;
|
|
use App\Scolarite\Domain\Model\Grade\GradeStatus;
|
|
use App\Scolarite\Domain\Model\Grade\GradeValue;
|
|
use App\Scolarite\Infrastructure\Persistence\InMemory\InMemoryEvaluationRepository;
|
|
use App\Scolarite\Infrastructure\Persistence\InMemory\InMemoryGradeRepository;
|
|
use App\Scolarite\Infrastructure\Persistence\InMemory\InMemoryTeacherReplacementRepository;
|
|
use App\Shared\Domain\Clock;
|
|
use App\Shared\Domain\Tenant\TenantId;
|
|
use DateTimeImmutable;
|
|
use PHPUnit\Framework\Attributes\Test;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
use function str_repeat;
|
|
|
|
final class SaveAppreciationHandlerTest extends TestCase
|
|
{
|
|
private const string TENANT_ID = '550e8400-e29b-41d4-a716-446655440001';
|
|
private const string EVALUATION_ID = '550e8400-e29b-41d4-a716-446655440040';
|
|
private const string TEACHER_ID = '550e8400-e29b-41d4-a716-446655440010';
|
|
private const string STUDENT_ID = '550e8400-e29b-41d4-a716-446655440050';
|
|
|
|
private InMemoryEvaluationRepository $evaluationRepository;
|
|
private InMemoryGradeRepository $gradeRepository;
|
|
private InMemoryTeacherReplacementRepository $replacementRepository;
|
|
private Clock $clock;
|
|
private string $gradeId;
|
|
|
|
/** @var array<string, bool> */
|
|
private array $affectationResults = [];
|
|
|
|
protected function setUp(): void
|
|
{
|
|
$this->evaluationRepository = new InMemoryEvaluationRepository();
|
|
$this->gradeRepository = new InMemoryGradeRepository();
|
|
$this->replacementRepository = new InMemoryTeacherReplacementRepository();
|
|
$this->clock = new class implements Clock {
|
|
public function now(): DateTimeImmutable
|
|
{
|
|
return new DateTimeImmutable('2026-03-31 10:00:00');
|
|
}
|
|
};
|
|
|
|
$this->affectationResults = [];
|
|
$this->affectationResults[self::TEACHER_ID] = true;
|
|
$this->seedEvaluationAndGrade();
|
|
}
|
|
|
|
public function isTeacherAffecte(string $teacherId): bool
|
|
{
|
|
return $this->affectationResults[$teacherId] ?? false;
|
|
}
|
|
|
|
#[Test]
|
|
public function itSavesAppreciation(): void
|
|
{
|
|
$handler = $this->createHandler();
|
|
|
|
$grade = $handler(new SaveAppreciationCommand(
|
|
tenantId: self::TENANT_ID,
|
|
gradeId: $this->gradeId,
|
|
teacherId: self::TEACHER_ID,
|
|
appreciation: 'Très bon travail',
|
|
));
|
|
|
|
self::assertSame('Très bon travail', $grade->appreciation);
|
|
self::assertNotNull($grade->appreciationUpdatedAt);
|
|
}
|
|
|
|
#[Test]
|
|
public function itClearsAppreciation(): void
|
|
{
|
|
$handler = $this->createHandler();
|
|
|
|
$handler(new SaveAppreciationCommand(
|
|
tenantId: self::TENANT_ID,
|
|
gradeId: $this->gradeId,
|
|
teacherId: self::TEACHER_ID,
|
|
appreciation: 'Bon travail',
|
|
));
|
|
|
|
$grade = $handler(new SaveAppreciationCommand(
|
|
tenantId: self::TENANT_ID,
|
|
gradeId: $this->gradeId,
|
|
teacherId: self::TEACHER_ID,
|
|
appreciation: null,
|
|
));
|
|
|
|
self::assertNull($grade->appreciation);
|
|
}
|
|
|
|
#[Test]
|
|
public function itThrowsWhenTeacherNotOwner(): void
|
|
{
|
|
$handler = $this->createHandler();
|
|
|
|
$this->expectException(NonProprietaireDeLEvaluationException::class);
|
|
|
|
$handler(new SaveAppreciationCommand(
|
|
tenantId: self::TENANT_ID,
|
|
gradeId: $this->gradeId,
|
|
teacherId: '550e8400-e29b-41d4-a716-446655440099',
|
|
appreciation: 'Test',
|
|
));
|
|
}
|
|
|
|
#[Test]
|
|
public function itThrowsWhenGradeNotFound(): void
|
|
{
|
|
$handler = $this->createHandler();
|
|
|
|
$this->expectException(GradeNotFoundException::class);
|
|
|
|
$handler(new SaveAppreciationCommand(
|
|
tenantId: self::TENANT_ID,
|
|
gradeId: '550e8400-e29b-41d4-a716-446655440099',
|
|
teacherId: self::TEACHER_ID,
|
|
appreciation: 'Test',
|
|
));
|
|
}
|
|
|
|
#[Test]
|
|
public function itThrowsWhenAppreciationTooLong(): void
|
|
{
|
|
$handler = $this->createHandler();
|
|
|
|
$this->expectException(AppreciationTropLongueException::class);
|
|
|
|
$handler(new SaveAppreciationCommand(
|
|
tenantId: self::TENANT_ID,
|
|
gradeId: $this->gradeId,
|
|
teacherId: self::TEACHER_ID,
|
|
appreciation: str_repeat('a', 501),
|
|
));
|
|
}
|
|
|
|
private function createHandler(): SaveAppreciationHandler
|
|
{
|
|
return new SaveAppreciationHandler(
|
|
$this->evaluationRepository,
|
|
$this->gradeRepository,
|
|
$this->createAutorisationChecker(),
|
|
$this->clock,
|
|
);
|
|
}
|
|
|
|
private function createAutorisationChecker(): AutorisationSaisieNotesChecker
|
|
{
|
|
$test = $this;
|
|
$affectationChecker = new class($test) implements EnseignantAffectationChecker {
|
|
public function __construct(private readonly SaveAppreciationHandlerTest $test)
|
|
{
|
|
}
|
|
|
|
public function estAffecte(
|
|
UserId $teacherId,
|
|
ClassId $classId,
|
|
SubjectId $subjectId,
|
|
TenantId $tenantId,
|
|
): bool {
|
|
return $this->test->isTeacherAffecte((string) $teacherId);
|
|
}
|
|
};
|
|
|
|
return new AutorisationSaisieNotesChecker(
|
|
$affectationChecker,
|
|
$this->replacementRepository,
|
|
);
|
|
}
|
|
|
|
private function seedEvaluationAndGrade(): void
|
|
{
|
|
$tenantId = TenantId::fromString(self::TENANT_ID);
|
|
|
|
$evaluation = Evaluation::reconstitute(
|
|
id: EvaluationId::fromString(self::EVALUATION_ID),
|
|
tenantId: $tenantId,
|
|
classId: ClassId::fromString('550e8400-e29b-41d4-a716-446655440020'),
|
|
subjectId: SubjectId::fromString('550e8400-e29b-41d4-a716-446655440030'),
|
|
teacherId: UserId::fromString(self::TEACHER_ID),
|
|
title: 'Contrôle',
|
|
description: null,
|
|
evaluationDate: new DateTimeImmutable('2026-04-15'),
|
|
gradeScale: new GradeScale(20),
|
|
coefficient: new Coefficient(1.0),
|
|
status: EvaluationStatus::PUBLISHED,
|
|
createdAt: new DateTimeImmutable('2026-03-12 10:00:00'),
|
|
updatedAt: new DateTimeImmutable('2026-03-12 10:00:00'),
|
|
);
|
|
|
|
$this->evaluationRepository->save($evaluation);
|
|
|
|
$grade = Grade::saisir(
|
|
tenantId: $tenantId,
|
|
evaluationId: EvaluationId::fromString(self::EVALUATION_ID),
|
|
studentId: UserId::fromString(self::STUDENT_ID),
|
|
value: new GradeValue(15.5),
|
|
status: GradeStatus::GRADED,
|
|
gradeScale: new GradeScale(20),
|
|
createdBy: UserId::fromString(self::TEACHER_ID),
|
|
now: new DateTimeImmutable('2026-03-27 10:00:00'),
|
|
);
|
|
|
|
$this->gradeRepository->save($grade);
|
|
$this->gradeId = (string) $grade->id;
|
|
}
|
|
}
|