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.
229 lines
7.9 KiB
PHP
229 lines
7.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Tests\Unit\Scolarite\Application\Command\PublishGrades;
|
|
|
|
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\PublishGrades\PublishGradesCommand;
|
|
use App\Scolarite\Application\Command\PublishGrades\PublishGradesHandler;
|
|
use App\Scolarite\Application\Port\EnseignantAffectationChecker;
|
|
use App\Scolarite\Application\Service\AutorisationSaisieNotesChecker;
|
|
use App\Scolarite\Domain\Exception\AucuneNoteSaisieException;
|
|
use App\Scolarite\Domain\Exception\NonProprietaireDeLEvaluationException;
|
|
use App\Scolarite\Domain\Exception\NotesDejaPublieesException;
|
|
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;
|
|
|
|
final class PublishGradesHandlerTest 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;
|
|
|
|
/** @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-27 14:00:00');
|
|
}
|
|
};
|
|
|
|
$this->affectationResults = [];
|
|
$this->affectationResults[self::TEACHER_ID] = true;
|
|
$this->seedEvaluation();
|
|
}
|
|
|
|
public function isTeacherAffecte(string $teacherId): bool
|
|
{
|
|
return $this->affectationResults[$teacherId] ?? false;
|
|
}
|
|
|
|
#[Test]
|
|
public function itPublishesGradesWhenGradesExist(): void
|
|
{
|
|
$this->seedGrade();
|
|
$handler = $this->createHandler();
|
|
$command = new PublishGradesCommand(
|
|
tenantId: self::TENANT_ID,
|
|
evaluationId: self::EVALUATION_ID,
|
|
teacherId: self::TEACHER_ID,
|
|
);
|
|
|
|
$evaluation = $handler($command);
|
|
|
|
self::assertTrue($evaluation->notesPubliees());
|
|
self::assertEquals(
|
|
new DateTimeImmutable('2026-03-27 14:00:00'),
|
|
$evaluation->gradesPublishedAt,
|
|
);
|
|
}
|
|
|
|
#[Test]
|
|
public function itPersistsPublishedEvaluation(): void
|
|
{
|
|
$this->seedGrade();
|
|
$handler = $this->createHandler();
|
|
$handler(new PublishGradesCommand(
|
|
tenantId: self::TENANT_ID,
|
|
evaluationId: self::EVALUATION_ID,
|
|
teacherId: self::TEACHER_ID,
|
|
));
|
|
|
|
$evaluation = $this->evaluationRepository->get(
|
|
EvaluationId::fromString(self::EVALUATION_ID),
|
|
TenantId::fromString(self::TENANT_ID),
|
|
);
|
|
|
|
self::assertTrue($evaluation->notesPubliees());
|
|
}
|
|
|
|
#[Test]
|
|
public function itThrowsWhenNoGradesExist(): void
|
|
{
|
|
$handler = $this->createHandler();
|
|
|
|
$this->expectException(AucuneNoteSaisieException::class);
|
|
|
|
$handler(new PublishGradesCommand(
|
|
tenantId: self::TENANT_ID,
|
|
evaluationId: self::EVALUATION_ID,
|
|
teacherId: self::TEACHER_ID,
|
|
));
|
|
}
|
|
|
|
#[Test]
|
|
public function itThrowsWhenTeacherNotAssigned(): void
|
|
{
|
|
$this->seedGrade();
|
|
$handler = $this->createHandler();
|
|
$unassignedTeacher = '550e8400-e29b-41d4-a716-446655440099';
|
|
|
|
$this->expectException(NonProprietaireDeLEvaluationException::class);
|
|
|
|
$handler(new PublishGradesCommand(
|
|
tenantId: self::TENANT_ID,
|
|
evaluationId: self::EVALUATION_ID,
|
|
teacherId: $unassignedTeacher,
|
|
));
|
|
}
|
|
|
|
#[Test]
|
|
public function itThrowsWhenAlreadyPublished(): void
|
|
{
|
|
$this->seedGrade();
|
|
$handler = $this->createHandler();
|
|
$command = new PublishGradesCommand(
|
|
tenantId: self::TENANT_ID,
|
|
evaluationId: self::EVALUATION_ID,
|
|
teacherId: self::TEACHER_ID,
|
|
);
|
|
|
|
$handler($command);
|
|
|
|
$this->expectException(NotesDejaPublieesException::class);
|
|
|
|
$handler($command);
|
|
}
|
|
|
|
private function createHandler(): PublishGradesHandler
|
|
{
|
|
return new PublishGradesHandler(
|
|
$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 PublishGradesHandlerTest $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 seedEvaluation(): void
|
|
{
|
|
$evaluation = Evaluation::reconstitute(
|
|
id: EvaluationId::fromString(self::EVALUATION_ID),
|
|
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: 'Contrôle chapitre 5',
|
|
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);
|
|
}
|
|
|
|
private function seedGrade(): void
|
|
{
|
|
$grade = Grade::saisir(
|
|
tenantId: TenantId::fromString(self::TENANT_ID),
|
|
evaluationId: EvaluationId::fromString(self::EVALUATION_ID),
|
|
studentId: UserId::fromString(self::STUDENT_ID),
|
|
value: new GradeValue(15.0),
|
|
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);
|
|
}
|
|
}
|